@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.
Files changed (37) hide show
  1. package/components/ActionFormRenderer.svelte +459 -0
  2. package/components/ActionFormRenderer.svelte.d.ts +35 -0
  3. package/components/AppFrame.svelte +75 -0
  4. package/components/AppFrame.svelte.d.ts +42 -0
  5. package/components/ContentBlock.svelte +100 -0
  6. package/components/ContentBlock.svelte.d.ts +37 -0
  7. package/components/Hero.svelte +87 -0
  8. package/components/Hero.svelte.d.ts +37 -0
  9. package/components/LayoutCompactMobile.svelte +72 -0
  10. package/components/LayoutCompactMobile.svelte.d.ts +19 -0
  11. package/components/LayoutInlineRow.svelte +74 -0
  12. package/components/LayoutInlineRow.svelte.d.ts +9 -0
  13. package/components/LayoutStackedDefault.svelte +66 -0
  14. package/components/LayoutStackedDefault.svelte.d.ts +9 -0
  15. package/components/Link.svelte +100 -0
  16. package/components/Link.svelte.d.ts +46 -0
  17. package/components/ServiceNavigation.svelte +160 -0
  18. package/components/ServiceNavigation.svelte.d.ts +49 -0
  19. package/components/SiteFooter.svelte +83 -0
  20. package/components/SiteFooter.svelte.d.ts +37 -0
  21. package/components/SiteHeader.svelte +90 -0
  22. package/components/SiteHeader.svelte.d.ts +36 -0
  23. package/components/SkipLink.svelte +63 -0
  24. package/components/SkipLink.svelte.d.ts +33 -0
  25. package/components/StatusTimeline.svelte +193 -0
  26. package/components/StatusTimeline.svelte.d.ts +44 -0
  27. package/components/VotingWidget.svelte +292 -0
  28. package/components/VotingWidget.svelte.d.ts +80 -0
  29. package/components/WidgetGrid.svelte +83 -0
  30. package/components/WidgetGrid.svelte.d.ts +42 -0
  31. package/components/action-form-renderer-layouts.d.ts +53 -0
  32. package/components/action-form-renderer-layouts.ts +82 -0
  33. package/components/action-form-renderer-payload.d.ts +90 -0
  34. package/components/action-form-renderer-payload.ts +203 -0
  35. package/components/index.js +16 -0
  36. package/package.json +2 -1
  37. 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>;