@aiaiai-pt/design-system 0.8.4 → 0.10.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/ActionFormRenderer.svelte +459 -0
- package/components/ActionFormRenderer.svelte.d.ts +35 -0
- package/components/AppFrame.svelte +75 -0
- package/components/AppFrame.svelte.d.ts +42 -0
- package/components/ContentBlock.svelte +100 -0
- package/components/ContentBlock.svelte.d.ts +37 -0
- package/components/Hero.svelte +87 -0
- package/components/Hero.svelte.d.ts +37 -0
- package/components/LayoutCompactMobile.svelte +72 -0
- package/components/LayoutCompactMobile.svelte.d.ts +19 -0
- package/components/LayoutInlineRow.svelte +74 -0
- package/components/LayoutInlineRow.svelte.d.ts +9 -0
- package/components/LayoutStackedDefault.svelte +66 -0
- package/components/LayoutStackedDefault.svelte.d.ts +9 -0
- package/components/Link.svelte +100 -0
- package/components/Link.svelte.d.ts +46 -0
- package/components/ServiceNavigation.svelte +160 -0
- package/components/ServiceNavigation.svelte.d.ts +49 -0
- package/components/SiteFooter.svelte +83 -0
- package/components/SiteFooter.svelte.d.ts +37 -0
- package/components/SiteHeader.svelte +90 -0
- package/components/SiteHeader.svelte.d.ts +36 -0
- package/components/SkipLink.svelte +63 -0
- package/components/SkipLink.svelte.d.ts +33 -0
- package/components/StatusTimeline.svelte +193 -0
- package/components/StatusTimeline.svelte.d.ts +44 -0
- package/components/VotingWidget.svelte +292 -0
- package/components/VotingWidget.svelte.d.ts +80 -0
- package/components/WidgetGrid.svelte +83 -0
- package/components/WidgetGrid.svelte.d.ts +42 -0
- package/components/action-form-renderer-layouts.d.ts +53 -0
- package/components/action-form-renderer-layouts.ts +82 -0
- package/components/action-form-renderer-payload.d.ts +90 -0
- package/components/action-form-renderer-payload.ts +203 -0
- package/components/index.js +16 -0
- package/package.json +2 -1
- package/tokens/themes/valongo.css +44 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// DS-internal sibling imports (this component lives in the DS now, #73 73f —
|
|
3
|
+
// converged so admin preview AND portal runtime mount the SAME renderer).
|
|
4
|
+
import Badge from "./Badge.svelte";
|
|
5
|
+
import CodeBlock from "./CodeBlock.svelte";
|
|
6
|
+
import Input from "./Input.svelte";
|
|
7
|
+
import Select from "./Select.svelte";
|
|
8
|
+
import Tag from "./Tag.svelte";
|
|
9
|
+
import {
|
|
10
|
+
buildActionPayload,
|
|
11
|
+
placementConsequenceRows as buildPlacementConsequenceRows,
|
|
12
|
+
type RendererMode,
|
|
13
|
+
} from "./action-form-renderer-payload";
|
|
14
|
+
// S7 (#27): the renderer dispatches arrangement to one of three
|
|
15
|
+
// registered layout components. The key is read from
|
|
16
|
+
// `target_config.layout_key` and validated through `resolveLayout`,
|
|
17
|
+
// so an operator-supplied string never reaches the DOM verbatim
|
|
18
|
+
// (RH#11 / R-SEC-07 / TH-08).
|
|
19
|
+
import { resolveLayout } from "./action-form-renderer-layouts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The renderer treats every parameter/action/placement as a loose record
|
|
23
|
+
* (it reads optional fields defensively). It used to import the admin app's
|
|
24
|
+
* `Entity`; in the DS it carries its own structural alias so it has no app
|
|
25
|
+
* dependency. The consuming app may pass its richer `Entity` — structurally
|
|
26
|
+
* compatible.
|
|
27
|
+
*/
|
|
28
|
+
type Entity = Record<string, unknown>;
|
|
29
|
+
type SelectOption = { value: string; label: string };
|
|
30
|
+
type ActionSchema = {
|
|
31
|
+
found?: boolean;
|
|
32
|
+
schema_version?: string;
|
|
33
|
+
action?: Entity | null;
|
|
34
|
+
placement?: Entity | null;
|
|
35
|
+
target?: Record<string, unknown> | null;
|
|
36
|
+
source?: Record<string, unknown> | null;
|
|
37
|
+
sections?: Array<{ key?: string; label?: string; parameters?: Entity[] }>;
|
|
38
|
+
parameters?: Entity[];
|
|
39
|
+
criteria?: Entity[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
action: Entity | null;
|
|
44
|
+
placement?: Entity | null;
|
|
45
|
+
parameters?: Entity[];
|
|
46
|
+
criteria?: Entity[];
|
|
47
|
+
mode?: RendererMode;
|
|
48
|
+
schema?: ActionSchema | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let {
|
|
52
|
+
action,
|
|
53
|
+
placement = null,
|
|
54
|
+
parameters = [],
|
|
55
|
+
criteria = [],
|
|
56
|
+
mode = "admin-preview",
|
|
57
|
+
schema = null,
|
|
58
|
+
}: Props = $props();
|
|
59
|
+
|
|
60
|
+
let values = $state<Record<string, unknown>>({});
|
|
61
|
+
|
|
62
|
+
const renderedAction = $derived((schema?.action as Entity | null | undefined) ?? action);
|
|
63
|
+
const renderedPlacement = $derived((schema?.placement as Entity | null | undefined) ?? placement);
|
|
64
|
+
const renderedParameters = $derived((schema?.parameters as Entity[] | undefined) ?? parameters);
|
|
65
|
+
const renderedCriteria = $derived((schema?.criteria as Entity[] | undefined) ?? criteria);
|
|
66
|
+
const orderedParameters = $derived(
|
|
67
|
+
[...renderedParameters]
|
|
68
|
+
.filter((parameter) => parameter.is_active !== false)
|
|
69
|
+
.sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0)),
|
|
70
|
+
);
|
|
71
|
+
const visibleParameters = $derived(orderedParameters.filter((parameter) => isVisible(parameter)));
|
|
72
|
+
const hiddenOrDefaultedParameters = $derived(
|
|
73
|
+
orderedParameters.filter(
|
|
74
|
+
(parameter) =>
|
|
75
|
+
!isVisible(parameter) ||
|
|
76
|
+
(parameter.default_value !== null && parameter.default_value !== undefined),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
const schemaSections = $derived(schemaSectionsFromContract());
|
|
80
|
+
const sections = $derived(schemaSections ?? groupIntoSections(visibleParameters));
|
|
81
|
+
const payload = $derived(buildPayload());
|
|
82
|
+
const payloadJson = $derived(JSON.stringify(payload, null, 2));
|
|
83
|
+
const criteriaSummary = $derived(
|
|
84
|
+
renderedCriteria
|
|
85
|
+
.filter((criterion) => criterion.is_active !== false)
|
|
86
|
+
.sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0)),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
$effect(() => {
|
|
90
|
+
const next = { ...values };
|
|
91
|
+
let touched = false;
|
|
92
|
+
for (const parameter of orderedParameters) {
|
|
93
|
+
const key = parameterKey(parameter);
|
|
94
|
+
if (!key || key in next) continue;
|
|
95
|
+
next[key] = initialValue(parameter);
|
|
96
|
+
touched = true;
|
|
97
|
+
}
|
|
98
|
+
if (touched) values = next;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function parameterKey(parameter: Entity): string {
|
|
102
|
+
return String(parameter.source_field_path ?? parameter.key ?? "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parameterType(parameter: Entity): string {
|
|
106
|
+
return String(parameter.type ?? "string");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isVisible(parameter: Entity): boolean {
|
|
110
|
+
const visibility = parameter.visibility;
|
|
111
|
+
if (visibility && typeof visibility === "object" && !Array.isArray(visibility)) {
|
|
112
|
+
return (visibility as Record<string, unknown>).visible !== false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sectionName(parameter: Entity): string {
|
|
118
|
+
const visibility = parameter.visibility;
|
|
119
|
+
if (visibility && typeof visibility === "object" && !Array.isArray(visibility)) {
|
|
120
|
+
const section = (visibility as Record<string, unknown>).section;
|
|
121
|
+
if (section) return String(section);
|
|
122
|
+
}
|
|
123
|
+
const uiSchema = parameter.ui_schema;
|
|
124
|
+
if (uiSchema && typeof uiSchema === "object" && !Array.isArray(uiSchema)) {
|
|
125
|
+
const section = (uiSchema as Record<string, unknown>).section;
|
|
126
|
+
if (section) return String(section);
|
|
127
|
+
}
|
|
128
|
+
return "Details";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function groupIntoSections(items: Entity[]): Array<{ name: string; items: Entity[] }> {
|
|
132
|
+
const byName = new Map<string, Entity[]>();
|
|
133
|
+
for (const parameter of items) {
|
|
134
|
+
const name = sectionName(parameter);
|
|
135
|
+
byName.set(name, [...(byName.get(name) ?? []), parameter]);
|
|
136
|
+
}
|
|
137
|
+
return [...byName.entries()].map(([name, sectionItems]) => ({ name, items: sectionItems }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function schemaSectionsFromContract(): Array<{ name: string; items: Entity[] }> | null {
|
|
141
|
+
if (!Array.isArray(schema?.sections)) return null;
|
|
142
|
+
const next = schema.sections
|
|
143
|
+
.map((section) => {
|
|
144
|
+
const items = Array.isArray(section.parameters)
|
|
145
|
+
? section.parameters
|
|
146
|
+
.filter((parameter) => parameter.is_active !== false && isVisible(parameter))
|
|
147
|
+
.sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0))
|
|
148
|
+
: [];
|
|
149
|
+
return { name: String(section.label ?? section.key ?? "Details"), items };
|
|
150
|
+
})
|
|
151
|
+
.filter((section) => section.items.length);
|
|
152
|
+
return next.length ? next : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function enumOptions(parameter: Entity): SelectOption[] {
|
|
156
|
+
let options: unknown[] = Array.isArray(parameter.options) ? parameter.options : [];
|
|
157
|
+
if (!options.length) {
|
|
158
|
+
const constraints = Array.isArray(parameter.constraints) ? parameter.constraints : [];
|
|
159
|
+
const oneOf = constraints.find((constraint) => {
|
|
160
|
+
if (!constraint || typeof constraint !== "object") return false;
|
|
161
|
+
const type = (constraint as Record<string, unknown>).type;
|
|
162
|
+
return type === "oneOf" || type === "one_of";
|
|
163
|
+
}) as Record<string, unknown> | undefined;
|
|
164
|
+
options = Array.isArray(oneOf?.values) ? oneOf.values : [];
|
|
165
|
+
}
|
|
166
|
+
return options
|
|
167
|
+
.map((option: unknown) => {
|
|
168
|
+
if (option && typeof option === "object") {
|
|
169
|
+
const row = option as Record<string, unknown>;
|
|
170
|
+
const value = String(row.value ?? row.key ?? "");
|
|
171
|
+
return value ? { value, label: String(row.label ?? row.name ?? value) } : null;
|
|
172
|
+
}
|
|
173
|
+
const value = String(option ?? "");
|
|
174
|
+
return value ? { value, label: value } : null;
|
|
175
|
+
})
|
|
176
|
+
.filter((option): option is SelectOption => !!option);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function initialValue(parameter: Entity): unknown {
|
|
180
|
+
if (parameter.default_value !== null && parameter.default_value !== undefined) {
|
|
181
|
+
return parameter.default_value;
|
|
182
|
+
}
|
|
183
|
+
const type = parameterType(parameter);
|
|
184
|
+
if (type === "bool" || type === "boolean") return false;
|
|
185
|
+
if (type === "number" || type === "integer") return "";
|
|
186
|
+
if (type === "enum" || type === "select") return enumOptions(parameter)[0]?.value ?? "";
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function setValue(key: string, value: unknown) {
|
|
191
|
+
values = { ...values, [key]: value };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function visibleValueBag(): Record<string, unknown> {
|
|
195
|
+
const bag: Record<string, unknown> = {};
|
|
196
|
+
for (const parameter of orderedParameters) {
|
|
197
|
+
const key = parameterKey(parameter);
|
|
198
|
+
if (!key) continue;
|
|
199
|
+
bag[key] = values[key] ?? initialValue(parameter);
|
|
200
|
+
}
|
|
201
|
+
return bag;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function targetConfig(): Record<string, unknown> {
|
|
205
|
+
const schemaTarget = schema?.target;
|
|
206
|
+
if (schemaTarget && typeof schemaTarget === "object" && !Array.isArray(schemaTarget)) {
|
|
207
|
+
return schemaTarget as Record<string, unknown>;
|
|
208
|
+
}
|
|
209
|
+
const value = renderedPlacement?.target_config;
|
|
210
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
211
|
+
? (value as Record<string, unknown>)
|
|
212
|
+
: {};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function sourceSchema(): Record<string, unknown> {
|
|
216
|
+
const schemaSource = schema?.source;
|
|
217
|
+
if (schemaSource && typeof schemaSource === "object" && !Array.isArray(schemaSource)) {
|
|
218
|
+
return schemaSource as Record<string, unknown>;
|
|
219
|
+
}
|
|
220
|
+
const value = renderedPlacement?.source_schema;
|
|
221
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
222
|
+
? (value as Record<string, unknown>)
|
|
223
|
+
: {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildPayload(): Record<string, unknown> {
|
|
227
|
+
return buildActionPayload({
|
|
228
|
+
action: renderedAction ?? null,
|
|
229
|
+
placement: renderedPlacement ?? null,
|
|
230
|
+
targetConfig: targetConfig(),
|
|
231
|
+
sourceSchema: sourceSchema(),
|
|
232
|
+
rawValues: visibleValueBag(),
|
|
233
|
+
schemaVersion: schema?.schema_version ?? null,
|
|
234
|
+
mode,
|
|
235
|
+
}) as unknown as Record<string, unknown>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function placementConsequenceRows(): Array<{ label: string; value: string }> {
|
|
239
|
+
return buildPlacementConsequenceRows({
|
|
240
|
+
placement: renderedPlacement ?? null,
|
|
241
|
+
targetConfig: targetConfig(),
|
|
242
|
+
sourceSchema: sourceSchema(),
|
|
243
|
+
mode,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// S7 (#27): resolve the layout component from target_config.layout_key.
|
|
248
|
+
// Unknown / null / typo'd keys collapse to stacked-default; the
|
|
249
|
+
// returned `key` is one of three known literals safe to render as
|
|
250
|
+
// `data-layout={resolvedLayout.key}` (TH-08 mitigation).
|
|
251
|
+
const resolvedLayout = $derived(resolveLayout(targetConfig().layout_key));
|
|
252
|
+
</script>
|
|
253
|
+
|
|
254
|
+
{#snippet fieldRow(rawParameter: Record<string, unknown>)}
|
|
255
|
+
{@const parameter = rawParameter as Entity}
|
|
256
|
+
{@const key = parameterKey(parameter)}
|
|
257
|
+
{@const type = parameterType(parameter)}
|
|
258
|
+
{#if type === "enum" || type === "select" || enumOptions(parameter).length}
|
|
259
|
+
<Select
|
|
260
|
+
label={String(parameter.label ?? key)}
|
|
261
|
+
name={key}
|
|
262
|
+
value={String(values[key] ?? initialValue(parameter) ?? "")}
|
|
263
|
+
options={enumOptions(parameter)}
|
|
264
|
+
placeholder="Select value"
|
|
265
|
+
onchange={(value: string) => setValue(key, value)}
|
|
266
|
+
/>
|
|
267
|
+
{:else if type === "number" || type === "integer"}
|
|
268
|
+
<Input
|
|
269
|
+
label={String(parameter.label ?? key)}
|
|
270
|
+
name={key}
|
|
271
|
+
type="number"
|
|
272
|
+
value={String(values[key] ?? "")}
|
|
273
|
+
oninput={(event: Event) => {
|
|
274
|
+
const value = (event.target as HTMLInputElement).value;
|
|
275
|
+
setValue(key, value === "" ? "" : Number(value));
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
{:else if type === "bool" || type === "boolean"}
|
|
279
|
+
<Select
|
|
280
|
+
label={String(parameter.label ?? key)}
|
|
281
|
+
name={key}
|
|
282
|
+
value={values[key] ? "true" : "false"}
|
|
283
|
+
options={[
|
|
284
|
+
{ value: "true", label: "Yes" },
|
|
285
|
+
{ value: "false", label: "No" },
|
|
286
|
+
]}
|
|
287
|
+
onchange={(value: string) => setValue(key, value === "true")}
|
|
288
|
+
/>
|
|
289
|
+
{:else}
|
|
290
|
+
<Input
|
|
291
|
+
label={String(parameter.label ?? key)}
|
|
292
|
+
name={key}
|
|
293
|
+
value={String(values[key] ?? "")}
|
|
294
|
+
oninput={(event: Event) => setValue(key, (event.target as HTMLInputElement).value)}
|
|
295
|
+
/>
|
|
296
|
+
{/if}
|
|
297
|
+
<p class="field-meta">
|
|
298
|
+
{String(parameter.required ? "Required" : "Optional")} / {type}
|
|
299
|
+
</p>
|
|
300
|
+
{/snippet}
|
|
301
|
+
|
|
302
|
+
<div class="renderer" data-testid={`action-form-renderer-${mode}`} data-layout={resolvedLayout.key}>
|
|
303
|
+
<div class="renderer-header">
|
|
304
|
+
<div>
|
|
305
|
+
<p class="eyebrow">{mode === "public-submit" ? "Public portal" : "Placement preview"}</p>
|
|
306
|
+
<h3>{String(renderedAction?.label ?? renderedAction?.key ?? "Select an action")}</h3>
|
|
307
|
+
</div>
|
|
308
|
+
<Badge variant={renderedPlacement ? "info" : "neutral"}>
|
|
309
|
+
{String(renderedPlacement?.surface ?? "No placement")}
|
|
310
|
+
</Badge>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{#if !renderedAction}
|
|
314
|
+
<p class="muted">Select an action to preview its placement-aware form contract.</p>
|
|
315
|
+
{:else if orderedParameters.length === 0}
|
|
316
|
+
<p class="muted">This action has no fields yet.</p>
|
|
317
|
+
{:else}
|
|
318
|
+
{@const Layout = resolvedLayout.component}
|
|
319
|
+
<form class="rendered-form">
|
|
320
|
+
<Layout {sections} field={fieldRow} />
|
|
321
|
+
</form>
|
|
322
|
+
{/if}
|
|
323
|
+
|
|
324
|
+
{#if mode === "admin-preview"}
|
|
325
|
+
<div class="admin-preview">
|
|
326
|
+
<section class="preview-block">
|
|
327
|
+
<h4>Hidden and defaulted fields</h4>
|
|
328
|
+
{#if hiddenOrDefaultedParameters.length}
|
|
329
|
+
<div class="tag-grid">
|
|
330
|
+
{#each hiddenOrDefaultedParameters as parameter (parameterKey(parameter))}
|
|
331
|
+
<Tag>
|
|
332
|
+
{String(parameter.key)}
|
|
333
|
+
{#if !isVisible(parameter)}
|
|
334
|
+
/ hidden{/if}
|
|
335
|
+
{#if parameter.default_value !== null && parameter.default_value !== undefined}
|
|
336
|
+
/ defaulted
|
|
337
|
+
{/if}
|
|
338
|
+
</Tag>
|
|
339
|
+
{/each}
|
|
340
|
+
</div>
|
|
341
|
+
{:else}
|
|
342
|
+
<p class="muted">No hidden or defaulted fields declared.</p>
|
|
343
|
+
{/if}
|
|
344
|
+
</section>
|
|
345
|
+
|
|
346
|
+
<section class="preview-block">
|
|
347
|
+
<h4>Placement consequences</h4>
|
|
348
|
+
<dl class="consequence-list">
|
|
349
|
+
{#each placementConsequenceRows() as row}
|
|
350
|
+
<div>
|
|
351
|
+
<dt>{row.label}</dt>
|
|
352
|
+
<dd>{row.value}</dd>
|
|
353
|
+
</div>
|
|
354
|
+
{/each}
|
|
355
|
+
</dl>
|
|
356
|
+
</section>
|
|
357
|
+
|
|
358
|
+
<section class="preview-block">
|
|
359
|
+
<h4>Validation used by server</h4>
|
|
360
|
+
{#if criteriaSummary.length}
|
|
361
|
+
<div class="tag-grid">
|
|
362
|
+
{#each criteriaSummary as criterion (String(criterion.id))}
|
|
363
|
+
<Tag>{String(criterion.criteria_type)} / {String(criterion.key)}</Tag>
|
|
364
|
+
{/each}
|
|
365
|
+
</div>
|
|
366
|
+
{:else}
|
|
367
|
+
<p class="muted">No submission criteria configured yet.</p>
|
|
368
|
+
{/if}
|
|
369
|
+
</section>
|
|
370
|
+
|
|
371
|
+
<section class="preview-block">
|
|
372
|
+
<h4>Normalized payload</h4>
|
|
373
|
+
<CodeBlock code={payloadJson} language="json" lineNumbers={false} copyable={false} />
|
|
374
|
+
</section>
|
|
375
|
+
</div>
|
|
376
|
+
{/if}
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<style>
|
|
380
|
+
.renderer {
|
|
381
|
+
display: flex;
|
|
382
|
+
flex-direction: column;
|
|
383
|
+
gap: var(--space-lg);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.renderer-header {
|
|
387
|
+
display: flex;
|
|
388
|
+
align-items: flex-start;
|
|
389
|
+
justify-content: space-between;
|
|
390
|
+
flex-wrap: wrap;
|
|
391
|
+
gap: var(--space-md);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.renderer-header h3 {
|
|
395
|
+
margin: 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.eyebrow {
|
|
399
|
+
margin: 0 0 var(--space-xs);
|
|
400
|
+
color: var(--color-text-muted);
|
|
401
|
+
font-size: var(--type-caption-size);
|
|
402
|
+
/* #63 typography — uppercase reserved for code tokens. */
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.muted,
|
|
406
|
+
.field-meta {
|
|
407
|
+
color: var(--color-text-muted);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.rendered-form,
|
|
411
|
+
.admin-preview,
|
|
412
|
+
.preview-block {
|
|
413
|
+
display: flex;
|
|
414
|
+
flex-direction: column;
|
|
415
|
+
gap: var(--space-md);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.preview-block h4 {
|
|
419
|
+
margin: 0;
|
|
420
|
+
font-size: var(--type-body-size);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.field-meta {
|
|
424
|
+
margin: 0;
|
|
425
|
+
font-size: var(--type-caption-size);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.preview-block {
|
|
429
|
+
padding-top: var(--space-md);
|
|
430
|
+
border-top: var(--elevation-border);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.tag-grid {
|
|
434
|
+
display: flex;
|
|
435
|
+
flex-wrap: wrap;
|
|
436
|
+
gap: var(--space-xs);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.consequence-list {
|
|
440
|
+
display: grid;
|
|
441
|
+
gap: var(--space-sm);
|
|
442
|
+
margin: 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.consequence-list div {
|
|
446
|
+
display: grid;
|
|
447
|
+
grid-template-columns: minmax(calc(var(--space-4xl) * 2), 0.4fr) minmax(0, 1fr);
|
|
448
|
+
gap: var(--space-md);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.consequence-list dt {
|
|
452
|
+
color: var(--color-text-muted);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.consequence-list dd {
|
|
456
|
+
margin: 0;
|
|
457
|
+
word-break: break-word;
|
|
458
|
+
}
|
|
459
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type RendererMode } from "./action-form-renderer-payload";
|
|
2
|
+
/**
|
|
3
|
+
* The renderer treats every parameter/action/placement as a loose record
|
|
4
|
+
* (it reads optional fields defensively). It used to import the admin app's
|
|
5
|
+
* `Entity`; in the DS it carries its own structural alias so it has no app
|
|
6
|
+
* dependency. The consuming app may pass its richer `Entity` — structurally
|
|
7
|
+
* compatible.
|
|
8
|
+
*/
|
|
9
|
+
type Entity = Record<string, unknown>;
|
|
10
|
+
type ActionSchema = {
|
|
11
|
+
found?: boolean;
|
|
12
|
+
schema_version?: string;
|
|
13
|
+
action?: Entity | null;
|
|
14
|
+
placement?: Entity | null;
|
|
15
|
+
target?: Record<string, unknown> | null;
|
|
16
|
+
source?: Record<string, unknown> | null;
|
|
17
|
+
sections?: Array<{
|
|
18
|
+
key?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
parameters?: Entity[];
|
|
21
|
+
}>;
|
|
22
|
+
parameters?: Entity[];
|
|
23
|
+
criteria?: Entity[];
|
|
24
|
+
};
|
|
25
|
+
interface Props {
|
|
26
|
+
action: Entity | null;
|
|
27
|
+
placement?: Entity | null;
|
|
28
|
+
parameters?: Entity[];
|
|
29
|
+
criteria?: Entity[];
|
|
30
|
+
mode?: RendererMode;
|
|
31
|
+
schema?: ActionSchema | null;
|
|
32
|
+
}
|
|
33
|
+
declare const ActionFormRenderer: import("svelte").Component<Props, {}, "">;
|
|
34
|
+
type ActionFormRenderer = ReturnType<typeof ActionFormRenderer>;
|
|
35
|
+
export default ActionFormRenderer;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component AppFrame
|
|
3
|
+
|
|
4
|
+
The locked public site-shell scaffold: skip-link → header → a single `<main>`
|
|
5
|
+
landmark → footer, in fixed DOM (= reading) order. This is the
|
|
6
|
+
accessibility-by-construction frame for the citizen portal — every page is
|
|
7
|
+
wrapped in exactly one AppFrame, guaranteeing one main landmark, a working
|
|
8
|
+
skip target, and linear reading order (ARTE Silver / WCAG 1.3.2, 2.4.1).
|
|
9
|
+
Structure is frozen and conformance-checkable; only brand tokens vary per
|
|
10
|
+
tenant via `data-theme`.
|
|
11
|
+
|
|
12
|
+
`header` and `footer` are slots so a tenant composes `SiteHeader` / `SiteFooter`
|
|
13
|
+
with its own brand + nav; the page content is the default `children`.
|
|
14
|
+
|
|
15
|
+
@example
|
|
16
|
+
<AppFrame>
|
|
17
|
+
{#snippet header()}<SiteHeader>…</SiteHeader>{/snippet}
|
|
18
|
+
{#snippet footer()}<SiteFooter>…</SiteFooter>{/snippet}
|
|
19
|
+
<PageContainer>{@render page()}</PageContainer>
|
|
20
|
+
</AppFrame>
|
|
21
|
+
-->
|
|
22
|
+
<script>
|
|
23
|
+
import SkipLink from "./SkipLink.svelte";
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
/** @type {string} id of the main landmark (skip-link target). */
|
|
27
|
+
mainId = "main",
|
|
28
|
+
/** @type {string} */
|
|
29
|
+
class: className = "",
|
|
30
|
+
/** @type {import('svelte').Snippet | undefined} Override the default skip link (e.g. localized). */
|
|
31
|
+
skipLink = undefined,
|
|
32
|
+
/** @type {import('svelte').Snippet | undefined} Site banner (compose `SiteHeader`). */
|
|
33
|
+
header = undefined,
|
|
34
|
+
/** @type {import('svelte').Snippet | undefined} Site footer (compose `SiteFooter`). */
|
|
35
|
+
footer = undefined,
|
|
36
|
+
/** @type {import('svelte').Snippet | undefined} Page content (rendered inside `<main>`). */
|
|
37
|
+
children = undefined,
|
|
38
|
+
...rest
|
|
39
|
+
} = $props();
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div class="app-frame {className}" {...rest}>
|
|
43
|
+
{#if skipLink}{@render skipLink()}{:else}<SkipLink href={`#${mainId}`} />{/if}
|
|
44
|
+
|
|
45
|
+
{#if header}{@render header()}{/if}
|
|
46
|
+
|
|
47
|
+
<!-- Single main landmark + focus target for the skip link. tabindex=-1 so
|
|
48
|
+
the skip link can move focus here without making it tabbable otherwise. -->
|
|
49
|
+
<main id={mainId} tabindex="-1" class="app-frame-main">
|
|
50
|
+
{#if children}{@render children()}{/if}
|
|
51
|
+
</main>
|
|
52
|
+
|
|
53
|
+
{#if footer}{@render footer()}{/if}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
.app-frame {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
min-height: 100vh;
|
|
61
|
+
min-height: 100dvh;
|
|
62
|
+
background: var(--color-surface);
|
|
63
|
+
color: var(--color-text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Main grows to push the footer down; the focus target shows no outline box
|
|
67
|
+
(focus is programmatic via the skip link, not a visible widget). */
|
|
68
|
+
.app-frame-main {
|
|
69
|
+
flex: 1 0 auto;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.app-frame-main:focus {
|
|
73
|
+
outline: none;
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default AppFrame;
|
|
2
|
+
type AppFrame = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* AppFrame
|
|
8
|
+
*
|
|
9
|
+
* The locked public site-shell scaffold: skip-link → header → a single `<main>`
|
|
10
|
+
* landmark → footer, in fixed DOM (= reading) order. This is the
|
|
11
|
+
* accessibility-by-construction frame for the citizen portal — every page is
|
|
12
|
+
* wrapped in exactly one AppFrame, guaranteeing one main landmark, a working
|
|
13
|
+
* skip target, and linear reading order (ARTE Silver / WCAG 1.3.2, 2.4.1).
|
|
14
|
+
* Structure is frozen and conformance-checkable; only brand tokens vary per
|
|
15
|
+
* tenant via `data-theme`.
|
|
16
|
+
*
|
|
17
|
+
* `header` and `footer` are slots so a tenant composes `SiteHeader` / `SiteFooter`
|
|
18
|
+
* with its own brand + nav; the page content is the default `children`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <AppFrame>
|
|
22
|
+
* {#snippet header()}<SiteHeader>…</SiteHeader>{/snippet}
|
|
23
|
+
* {#snippet footer()}<SiteFooter>…</SiteFooter>{/snippet}
|
|
24
|
+
* <PageContainer>{@render page()}</PageContainer>
|
|
25
|
+
* </AppFrame>
|
|
26
|
+
*/
|
|
27
|
+
declare const AppFrame: import("svelte").Component<{
|
|
28
|
+
mainId?: string;
|
|
29
|
+
class?: string;
|
|
30
|
+
skipLink?: any;
|
|
31
|
+
header?: any;
|
|
32
|
+
footer?: any;
|
|
33
|
+
children?: any;
|
|
34
|
+
} & Record<string, any>, {}, "">;
|
|
35
|
+
type $$ComponentProps = {
|
|
36
|
+
mainId?: string;
|
|
37
|
+
class?: string;
|
|
38
|
+
skipLink?: any;
|
|
39
|
+
header?: any;
|
|
40
|
+
footer?: any;
|
|
41
|
+
children?: any;
|
|
42
|
+
} & Record<string, any>;
|