@aiaiai-pt/design-system 0.24.0 → 0.25.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.
@@ -85,6 +85,22 @@
85
85
  /** Inline error rendered by the geo MapPicker(s) (e.g. the consumer's
86
86
  * out-of-bounds copy) — forwarded as MapPicker's `error`. */
87
87
  geoError?: string;
88
+ /** Service-flow wizard (#105 P6): render ONLY the section whose
89
+ * name (its label, falling back to its key) matches — one screen per
90
+ * step. The value bag still spans ALL parameters (so answers persist
91
+ * across steps and the final payload is complete); only the DISPLAY is
92
+ * scoped. Null (default) renders every section, byte-identical to today. */
93
+ activeSectionKey?: string | null;
94
+ /** Service-flow wizard (#105 P6): suppress the renderer's own submit
95
+ * button / captcha / error — the wizard shell owns the nav and posts the
96
+ * payload once at the end. Default false (the standalone form keeps its
97
+ * button). */
98
+ hideSubmit?: boolean;
99
+ /** Service-flow wizard (#105 P6): fires with the freshly built payload
100
+ * whenever a value or attachment changes, so the wizard always holds the
101
+ * complete payload (incl. attachment_keys) to review and submit. Read-only
102
+ * consumers omit it. */
103
+ onchange?: (payload: Record<string, unknown>) => void;
88
104
  }
89
105
 
90
106
  let {
@@ -102,6 +118,9 @@
102
118
  layers = [],
103
119
  onoutofbounds = undefined,
104
120
  geoError = undefined,
121
+ activeSectionKey = null,
122
+ hideSubmit = false,
123
+ onchange = undefined,
105
124
  }: Props = $props();
106
125
 
107
126
  let values = $state<Record<string, unknown>>({});
@@ -125,7 +144,9 @@
125
144
  // consumer wired an apply. Preview modes (admin-preview/adapter-preview) and a
126
145
  // missing onApply stay read-only — the button never renders.
127
146
  const isSubmitMode = $derived(mode === "public-submit" || mode === "admin-execute");
128
- const showSubmit = $derived(isSubmitMode && onApply !== undefined);
147
+ // The wizard (#105 P6) suppresses the internal button via hideSubmit — it
148
+ // owns the nav and posts once at the end.
149
+ const showSubmit = $derived(isSubmitMode && onApply !== undefined && !hideSubmit);
129
150
 
130
151
  const renderedAction = $derived((schema?.action as Entity | null | undefined) ?? action);
131
152
  const renderedPlacement = $derived((schema?.placement as Entity | null | undefined) ?? placement);
@@ -146,9 +167,23 @@
146
167
  );
147
168
  const schemaSections = $derived(schemaSectionsFromContract());
148
169
  const sections = $derived(schemaSections ?? groupIntoSections(visibleParameters));
170
+ // Wizard pagination (#105 P6): when a section is active, render only it. The
171
+ // value bag is unaffected — every parameter is still seeded and submitted.
172
+ const displaySections = $derived(
173
+ activeSectionKey == null
174
+ ? sections
175
+ : sections.filter((section) => section.name === activeSectionKey),
176
+ );
149
177
  const payload = $derived(buildPayload());
150
178
  const payloadJson = $derived(JSON.stringify(payload, null, 2));
151
179
 
180
+ // Wizard (#105 P6): surface the live payload so the shell can review + submit
181
+ // it. Reading `payload` registers the dependency; the effect re-runs whenever
182
+ // a value or attachment changes.
183
+ $effect(() => {
184
+ onchange?.(payload);
185
+ });
186
+
152
187
  // Client-side submit gate (layer 1 — see the ADL): every required, visible
153
188
  // parameter must have a non-empty value. Server-side submission criteria are
154
189
  // enforced by the BFF on submit and surfaced via `submitError`.
@@ -520,21 +555,26 @@
520
555
  {/snippet}
521
556
 
522
557
  <div class="renderer" data-testid={`action-form-renderer-${mode}`} data-layout={resolvedLayout.key}>
523
- <div class="renderer-header">
524
- <div>
525
- <!-- The placement-preview eyebrow + surface badge are operator chrome
526
- hidden in public-submit so a citizen sees a clean form title. -->
558
+ <!-- Wizard pagination (#105 P6): the ServiceFlow shell owns the step
559
+ heading, so the renderer's own action-title header is suppressed when a
560
+ section is active to avoid a duplicate heading per step. -->
561
+ {#if activeSectionKey == null}
562
+ <div class="renderer-header">
563
+ <div>
564
+ <!-- The placement-preview eyebrow + surface badge are operator chrome —
565
+ hidden in public-submit so a citizen sees a clean form title. -->
566
+ {#if mode !== "public-submit"}
567
+ <p class="eyebrow">Placement preview</p>
568
+ {/if}
569
+ <h3>{String(renderedAction?.label ?? renderedAction?.key ?? "Select an action")}</h3>
570
+ </div>
527
571
  {#if mode !== "public-submit"}
528
- <p class="eyebrow">Placement preview</p>
572
+ <Badge variant={renderedPlacement ? "info" : "neutral"}>
573
+ {String(renderedPlacement?.surface ?? "No placement")}
574
+ </Badge>
529
575
  {/if}
530
- <h3>{String(renderedAction?.label ?? renderedAction?.key ?? "Select an action")}</h3>
531
576
  </div>
532
- {#if mode !== "public-submit"}
533
- <Badge variant={renderedPlacement ? "info" : "neutral"}>
534
- {String(renderedPlacement?.surface ?? "No placement")}
535
- </Badge>
536
- {/if}
537
- </div>
577
+ {/if}
538
578
 
539
579
  {#if !renderedAction}
540
580
  <p class="muted">Select an action to preview its placement-aware form contract.</p>
@@ -550,7 +590,7 @@
550
590
  renderedAction?.label ?? renderedAction?.key ?? "Form",
551
591
  )}
552
592
  >
553
- <Layout {sections} field={fieldRow} />
593
+ <Layout sections={displaySections} field={fieldRow} />
554
594
  </form>
555
595
  {/if}
556
596
 
@@ -70,6 +70,22 @@ interface Props {
70
70
  /** Inline error rendered by the geo MapPicker(s) (e.g. the consumer's
71
71
  * out-of-bounds copy) — forwarded as MapPicker's `error`. */
72
72
  geoError?: string;
73
+ /** Service-flow wizard (#105 P6): render ONLY the section whose
74
+ * name (its label, falling back to its key) matches — one screen per
75
+ * step. The value bag still spans ALL parameters (so answers persist
76
+ * across steps and the final payload is complete); only the DISPLAY is
77
+ * scoped. Null (default) renders every section, byte-identical to today. */
78
+ activeSectionKey?: string | null;
79
+ /** Service-flow wizard (#105 P6): suppress the renderer's own submit
80
+ * button / captcha / error — the wizard shell owns the nav and posts the
81
+ * payload once at the end. Default false (the standalone form keeps its
82
+ * button). */
83
+ hideSubmit?: boolean;
84
+ /** Service-flow wizard (#105 P6): fires with the freshly built payload
85
+ * whenever a value or attachment changes, so the wizard always holds the
86
+ * complete payload (incl. attachment_keys) to review and submit. Read-only
87
+ * consumers omit it. */
88
+ onchange?: (payload: Record<string, unknown>) => void;
73
89
  }
74
90
  declare const ActionFormRenderer: import("svelte").Component<Props, {}, "">;
75
91
  type ActionFormRenderer = ReturnType<typeof ActionFormRenderer>;
@@ -0,0 +1,197 @@
1
+ <!--
2
+ @component CheckAnswers
3
+
4
+ The "review your answers before submitting" summary — a grouped description
5
+ list of every answer the citizen gave, each with a "Change" link back to the
6
+ step that captured it. Vertical-agnostic: it renders whatever label/value
7
+ pairs the consumer collected (from the action's parameter labels), grouped by
8
+ the flow's sections. Pure presentation — no state, no network.
9
+
10
+ Accessibility (WCAG 3.3.4, Error Prevention): every answer is reviewable and
11
+ editable before the irreversible submit. Each "Change" control carries an
12
+ accessible name that includes WHAT it changes (the visible "Change" plus a
13
+ visually-hidden field label), so a screen-reader user never meets a row of
14
+ identical "Change" links. Answers are a `<dl>` (the semantic structure for
15
+ term/description pairs); section headings are `<h3>` under the widget's `h2`.
16
+
17
+ @example
18
+ <CheckAnswers
19
+ label="Check your answers"
20
+ groups={[
21
+ { label: "About you", items: [
22
+ { key: "name", label: "Full name", value: "Ana Silva", onChange: () => goTo(0) },
23
+ ]},
24
+ { label: "Your idea", items: [
25
+ { key: "title", label: "Title", value: "Bike lanes", onChange: () => goTo(1) },
26
+ ]},
27
+ ]}
28
+ />
29
+ -->
30
+ <script module>
31
+ /**
32
+ * @typedef {{ key: string, label: string, value: string, onChange?: () => void }} AnswerItem
33
+ * @typedef {{ label?: string, items: AnswerItem[] }} AnswerGroup
34
+ */
35
+ </script>
36
+
37
+ <script>
38
+ let {
39
+ /** @type {AnswerGroup[]} The collected answers, grouped by section. */
40
+ groups = [],
41
+ /** @type {string} Accessible name for the review region (localize it). */
42
+ label = "Check your answers",
43
+ /** @type {string} Visible text on each change control (localize it). */
44
+ changeLabel = "Change",
45
+ /** @type {string} Shown when there are no answers yet (localize it). */
46
+ emptyText = "There are no answers to review yet.",
47
+ /** @type {string} Placeholder for an answer the citizen left blank (localize it). */
48
+ blankText = "Not provided",
49
+
50
+ /** @type {string} */
51
+ class: className = "",
52
+ ...rest
53
+ } = $props();
54
+
55
+ const hasAnswers = $derived(groups.some((g) => g.items.length > 0));
56
+ </script>
57
+
58
+ <section class="check-answers {className}" aria-label={label} {...rest}>
59
+ {#if hasAnswers}
60
+ {#each groups as group (group.label ?? group.items[0]?.key)}
61
+ {#if group.items.length > 0}
62
+ <div class="check-answers-group">
63
+ {#if group.label}
64
+ <h3 class="check-answers-group-heading">{group.label}</h3>
65
+ {/if}
66
+ <dl class="check-answers-list">
67
+ {#each group.items as item (item.key)}
68
+ <div class="check-answers-row">
69
+ <dt class="check-answers-term">{item.label}</dt>
70
+ <dd class="check-answers-value">
71
+ {item.value || blankText}
72
+ </dd>
73
+ <dd class="check-answers-action">
74
+ {#if item.onChange}
75
+ <button
76
+ type="button"
77
+ class="check-answers-change"
78
+ onclick={() => item.onChange?.()}
79
+ >
80
+ {changeLabel}<span class="check-answers-sr"
81
+ >&nbsp;{item.label}</span
82
+ >
83
+ </button>
84
+ {/if}
85
+ </dd>
86
+ </div>
87
+ {/each}
88
+ </dl>
89
+ </div>
90
+ {/if}
91
+ {/each}
92
+ {:else}
93
+ <p class="check-answers-empty">{emptyText}</p>
94
+ {/if}
95
+ </section>
96
+
97
+ <style>
98
+ .check-answers {
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: var(--space-lg);
102
+ }
103
+
104
+ .check-answers-group {
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: var(--space-sm);
108
+ }
109
+
110
+ .check-answers-group-heading {
111
+ font-family: var(--type-heading-sm-font);
112
+ font-size: var(--type-heading-sm-size);
113
+ color: var(--color-text);
114
+ margin: 0;
115
+ }
116
+
117
+ .check-answers-list {
118
+ margin: 0;
119
+ display: flex;
120
+ flex-direction: column;
121
+ }
122
+
123
+ .check-answers-row {
124
+ display: grid;
125
+ grid-template-columns: minmax(8rem, 1fr) 2fr auto;
126
+ gap: var(--space-sm);
127
+ align-items: start;
128
+ padding: var(--space-sm) 0;
129
+ border-bottom: 1px solid var(--color-border);
130
+ }
131
+
132
+ .check-answers-term {
133
+ font-family: var(--type-body-sm-font);
134
+ font-size: var(--type-body-sm-size);
135
+ color: var(--color-text-muted);
136
+ margin: 0;
137
+ }
138
+
139
+ .check-answers-value {
140
+ font-family: var(--type-body-font);
141
+ font-size: var(--type-body-size);
142
+ color: var(--color-text);
143
+ margin: 0;
144
+ overflow-wrap: anywhere;
145
+ }
146
+
147
+ .check-answers-action {
148
+ margin: 0;
149
+ justify-self: end;
150
+ }
151
+
152
+ .check-answers-change {
153
+ appearance: none;
154
+ background: none;
155
+ border: none;
156
+ padding: 0;
157
+ cursor: pointer;
158
+ font-family: var(--type-body-sm-font);
159
+ font-size: var(--type-body-sm-size);
160
+ color: var(--color-accent);
161
+ text-decoration: underline;
162
+ text-underline-offset: 2px;
163
+ }
164
+
165
+ .check-answers-change:hover {
166
+ color: var(--color-accent-hover);
167
+ }
168
+
169
+ .check-answers-empty {
170
+ font-family: var(--type-body-font);
171
+ font-size: var(--type-body-size);
172
+ color: var(--color-text-muted);
173
+ margin: 0;
174
+ }
175
+
176
+ /* Visually-hidden text that screen readers still announce (WCAG 3.3.4). */
177
+ .check-answers-sr {
178
+ position: absolute;
179
+ width: 1px;
180
+ height: 1px;
181
+ padding: 0;
182
+ margin: -1px;
183
+ overflow: hidden;
184
+ clip: rect(0, 0, 0, 0);
185
+ white-space: nowrap;
186
+ border: 0;
187
+ }
188
+
189
+ @media (max-width: 30rem) {
190
+ .check-answers-row {
191
+ grid-template-columns: 1fr auto;
192
+ }
193
+ .check-answers-term {
194
+ grid-column: 1 / -1;
195
+ }
196
+ }
197
+ </style>
@@ -0,0 +1,60 @@
1
+ export default CheckAnswers;
2
+ export type AnswerItem = {
3
+ key: string;
4
+ label: string;
5
+ value: string;
6
+ onChange?: () => void;
7
+ };
8
+ export type AnswerGroup = {
9
+ label?: string;
10
+ items: AnswerItem[];
11
+ };
12
+ type CheckAnswers = {
13
+ $on?(type: string, callback: (e: any) => void): () => void;
14
+ $set?(props: Partial<$$ComponentProps>): void;
15
+ };
16
+ /**
17
+ * CheckAnswers
18
+ *
19
+ * The "review your answers before submitting" summary — a grouped description
20
+ * list of every answer the citizen gave, each with a "Change" link back to the
21
+ * step that captured it. Vertical-agnostic: it renders whatever label/value
22
+ * pairs the consumer collected (from the action's parameter labels), grouped by
23
+ * the flow's sections. Pure presentation — no state, no network.
24
+ *
25
+ * Accessibility (WCAG 3.3.4, Error Prevention): every answer is reviewable and
26
+ * editable before the irreversible submit. Each "Change" control carries an
27
+ * accessible name that includes WHAT it changes (the visible "Change" plus a
28
+ * visually-hidden field label), so a screen-reader user never meets a row of
29
+ * identical "Change" links. Answers are a `<dl>` (the semantic structure for
30
+ * term/description pairs); section headings are `<h3>` under the widget's `h2`.
31
+ *
32
+ * @example
33
+ * <CheckAnswers
34
+ * label="Check your answers"
35
+ * groups={[
36
+ * { label: "About you", items: [
37
+ * { key: "name", label: "Full name", value: "Ana Silva", onChange: () => goTo(0) },
38
+ * ]},
39
+ * { label: "Your idea", items: [
40
+ * { key: "title", label: "Title", value: "Bike lanes", onChange: () => goTo(1) },
41
+ * ]},
42
+ * ]}
43
+ * />
44
+ */
45
+ declare const CheckAnswers: import("svelte").Component<{
46
+ groups?: any[];
47
+ label?: string;
48
+ changeLabel?: string;
49
+ emptyText?: string;
50
+ blankText?: string;
51
+ class?: string;
52
+ } & Record<string, any>, {}, "">;
53
+ type $$ComponentProps = {
54
+ groups?: any[];
55
+ label?: string;
56
+ changeLabel?: string;
57
+ emptyText?: string;
58
+ blankText?: string;
59
+ class?: string;
60
+ } & Record<string, any>;
@@ -0,0 +1,172 @@
1
+ <!--
2
+ @component Confirmation
3
+
4
+ The receipt panel shown after a successful submission: a success banner with
5
+ the citizen's reference number front-and-centre, a "what happens next" body,
6
+ and an optional link to track the submission. Vertical-agnostic — it knows
7
+ nothing about proposals or occurrences, only "here is your reference and what
8
+ to expect". Pure presentation; the consumer fetched the reference.
9
+
10
+ Accessibility: a `<section>` region named by `label`; the success heading is
11
+ an `<h2>` (the page `<h1>` is the consumer's, same split as the other account
12
+ widgets) carrying a decorative check. The reference is emphasised text inside
13
+ a labelled block, not colour alone. Semantic tokens throughout, so the
14
+ success styling survives dark / high-contrast schemes (#244).
15
+
16
+ @example
17
+ <Confirmation
18
+ label="Submission received"
19
+ title="Your proposal has been submitted"
20
+ reference="PRP-2026-0042"
21
+ referenceLabel="Your reference number"
22
+ body="We've sent a confirmation. The committee will review your proposal."
23
+ trackHref="/participacao/acompanhar/PRP-2026-0042"
24
+ trackLabel="Track this proposal"
25
+ />
26
+ -->
27
+ <script>
28
+ import Button from "./Button.svelte";
29
+
30
+ let {
31
+ /** @type {string} Accessible name for the confirmation region (localize it). */
32
+ label = "Confirmation",
33
+ /** @type {string} The success headline (localize it). */
34
+ title = "Submission received",
35
+ /** @type {string} The reference / tracking number to surface. */
36
+ reference = "",
37
+ /** @type {string} Label above the reference number (localize it). */
38
+ referenceLabel = "Your reference number",
39
+ /** @type {string} The "what happens next" copy (localize it). */
40
+ body = "",
41
+ /** @type {string | undefined} Link to track the submission. */
42
+ trackHref = undefined,
43
+ /** @type {string} Label for the track link (localize it). */
44
+ trackLabel = "Track your submission",
45
+
46
+ /** @type {import('svelte').Snippet | undefined} Extra content (e.g. a next-steps list). */
47
+ children = undefined,
48
+
49
+ /** @type {string} */
50
+ class: className = "",
51
+ ...rest
52
+ } = $props();
53
+ </script>
54
+
55
+ <section class="confirmation {className}" aria-label={label} {...rest}>
56
+ <div class="confirmation-banner">
57
+ <span class="confirmation-icon" aria-hidden="true">
58
+ <svg viewBox="0 0 24 24" fill="none">
59
+ <path
60
+ d="M5 12.5l4.5 4.5L19 7.5"
61
+ stroke="currentColor"
62
+ stroke-width="2.5"
63
+ stroke-linecap="round"
64
+ stroke-linejoin="round"
65
+ />
66
+ </svg>
67
+ </span>
68
+ <h2 class="confirmation-title">{title}</h2>
69
+ </div>
70
+
71
+ {#if reference}
72
+ <div class="confirmation-reference">
73
+ <span class="confirmation-reference-label">{referenceLabel}</span>
74
+ <strong class="confirmation-reference-value">{reference}</strong>
75
+ </div>
76
+ {/if}
77
+
78
+ {#if body}
79
+ <p class="confirmation-body">{body}</p>
80
+ {/if}
81
+
82
+ {#if children}
83
+ <div class="confirmation-extra">{@render children()}</div>
84
+ {/if}
85
+
86
+ {#if trackHref}
87
+ <div class="confirmation-actions">
88
+ <Button variant="secondary" href={trackHref}>{trackLabel}</Button>
89
+ </div>
90
+ {/if}
91
+ </section>
92
+
93
+ <style>
94
+ .confirmation {
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: var(--space-lg);
98
+ padding: var(--space-xl);
99
+ border: 1px solid var(--color-success);
100
+ border-radius: var(--radius-lg);
101
+ background: var(--color-success-subtle);
102
+ }
103
+
104
+ .confirmation-banner {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: var(--space-md);
108
+ }
109
+
110
+ .confirmation-icon {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ flex-shrink: 0;
115
+ width: 2.5rem;
116
+ height: 2.5rem;
117
+ border-radius: var(--radius-circle);
118
+ background: var(--color-success);
119
+ color: var(--color-surface);
120
+ }
121
+
122
+ .confirmation-icon svg {
123
+ width: 1.5rem;
124
+ height: 1.5rem;
125
+ }
126
+
127
+ .confirmation-title {
128
+ font-family: var(--type-heading-font);
129
+ font-size: var(--type-heading-size);
130
+ color: var(--color-text);
131
+ margin: 0;
132
+ }
133
+
134
+ .confirmation-reference {
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: var(--space-2xs);
138
+ }
139
+
140
+ .confirmation-reference-label {
141
+ font-family: var(--type-body-sm-font);
142
+ font-size: var(--type-body-sm-size);
143
+ color: var(--color-text-muted);
144
+ }
145
+
146
+ .confirmation-reference-value {
147
+ font-family: var(--type-data-font);
148
+ font-size: var(--type-heading-sm-size);
149
+ letter-spacing: var(--type-data-tracking);
150
+ color: var(--color-text);
151
+ }
152
+
153
+ .confirmation-body {
154
+ font-family: var(--type-body-font);
155
+ font-size: var(--type-body-size);
156
+ line-height: var(--type-body-leading);
157
+ color: var(--color-text);
158
+ margin: 0;
159
+ }
160
+
161
+ .confirmation-extra {
162
+ font-family: var(--type-body-font);
163
+ font-size: var(--type-body-size);
164
+ color: var(--color-text);
165
+ }
166
+
167
+ .confirmation-actions {
168
+ display: flex;
169
+ flex-wrap: wrap;
170
+ gap: var(--space-sm);
171
+ }
172
+ </style>
@@ -0,0 +1,53 @@
1
+ export default Confirmation;
2
+ type Confirmation = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * Confirmation
8
+ *
9
+ * The receipt panel shown after a successful submission: a success banner with
10
+ * the citizen's reference number front-and-centre, a "what happens next" body,
11
+ * and an optional link to track the submission. Vertical-agnostic — it knows
12
+ * nothing about proposals or occurrences, only "here is your reference and what
13
+ * to expect". Pure presentation; the consumer fetched the reference.
14
+ *
15
+ * Accessibility: a `<section>` region named by `label`; the success heading is
16
+ * an `<h2>` (the page `<h1>` is the consumer's, same split as the other account
17
+ * widgets) carrying a decorative check. The reference is emphasised text inside
18
+ * a labelled block, not colour alone. Semantic tokens throughout, so the
19
+ * success styling survives dark / high-contrast schemes (#244).
20
+ *
21
+ * @example
22
+ * <Confirmation
23
+ * label="Submission received"
24
+ * title="Your proposal has been submitted"
25
+ * reference="PRP-2026-0042"
26
+ * referenceLabel="Your reference number"
27
+ * body="We've sent a confirmation. The committee will review your proposal."
28
+ * trackHref="/participacao/acompanhar/PRP-2026-0042"
29
+ * trackLabel="Track this proposal"
30
+ * />
31
+ */
32
+ declare const Confirmation: import("svelte").Component<{
33
+ label?: string;
34
+ title?: string;
35
+ reference?: string;
36
+ referenceLabel?: string;
37
+ body?: string;
38
+ trackHref?: any;
39
+ trackLabel?: string;
40
+ children?: any;
41
+ class?: string;
42
+ } & Record<string, any>, {}, "">;
43
+ type $$ComponentProps = {
44
+ label?: string;
45
+ title?: string;
46
+ reference?: string;
47
+ referenceLabel?: string;
48
+ body?: string;
49
+ trackHref?: any;
50
+ trackLabel?: string;
51
+ children?: any;
52
+ class?: string;
53
+ } & Record<string, any>;
@@ -0,0 +1,264 @@
1
+ <!--
2
+ @component ServiceFlow
3
+
4
+ The chrome for a multi-step submit journey (a "service flow"): a step
5
+ indicator, a progress bar, an error-summary region, the active step's body,
6
+ and the prev / next / submit navigation. Vertical-agnostic — it renders
7
+ whatever the consumer puts in the `children` snippet for the active step and
8
+ never knows what an "eligibility check" or "proposal" is. The consumer owns
9
+ the wizard STATE (which step, the collected answers) and the network; this
10
+ shell owns the layout, the accessible step semantics, and focus management.
11
+
12
+ Controlled: pass `current` (0-based active step) and the `steps` labels;
13
+ drive it with `onPrev` / `onNext` (advance) and `onSubmit` (the final step's
14
+ primary action). `canProceed=false` disables the primary button (e.g. an
15
+ eligibility gate failed or the step is invalid); `busy=true` shows it loading
16
+ and locks the nav (a dry-run or the submit is in flight).
17
+
18
+ Accessibility (WCAG): a `<section>` region named by `label`; a polite live
19
+ region announces "Step X of Y"; on every step change focus moves to the step
20
+ body (or, if the consumer surfaces `errors`, to the error summary — an
21
+ `role="alert"` list the citizen lands on so the reason is read first). The
22
+ error summary, the step nav buttons, and the progress bar all ride semantic
23
+ tokens, so dark / high-contrast schemes (#244) work unchanged.
24
+
25
+ @example
26
+ <ServiceFlow
27
+ label="Submit a proposal"
28
+ steps={[{ label: "Eligibility" }, { label: "Your idea" }, { label: "Review" }]}
29
+ current={step}
30
+ canProceed={eligible}
31
+ busy={checking}
32
+ errors={violations}
33
+ onPrev={() => step--}
34
+ onNext={() => step++}
35
+ onSubmit={() => submit()}
36
+ >
37
+ {@render activeStepBody()}
38
+ </ServiceFlow>
39
+ -->
40
+ <script module>
41
+ /**
42
+ * @typedef {{ label: string }} FlowStep
43
+ */
44
+ </script>
45
+
46
+ <script>
47
+ import Stepper from "./Stepper.svelte";
48
+ import Progress from "./Progress.svelte";
49
+ import Button from "./Button.svelte";
50
+
51
+ let {
52
+ /** @type {FlowStep[]} Ordered step labels for the indicator. */
53
+ steps = [],
54
+ /** @type {number} The active step, 0-based. */
55
+ current = 0,
56
+ /** @type {string} Accessible name for the flow region (localize it). */
57
+ label = "Step-by-step form",
58
+ /** @type {boolean} Whether the primary (next/submit) button is enabled. */
59
+ canProceed = true,
60
+ /** @type {boolean} A step action is in flight — locks nav, loads the button. */
61
+ busy = false,
62
+ /** @type {string[]} Blocking messages for the error summary (localize them). */
63
+ errors = [],
64
+
65
+ /** @type {string} Label for the error-summary heading (localize it). */
66
+ errorSummaryLabel = "There is a problem",
67
+ /** @type {string} Back-button label (localize it). */
68
+ prevLabel = "Back",
69
+ /** @type {string} Advance-button label (localize it). */
70
+ nextLabel = "Continue",
71
+ /** @type {string} Final-step primary label (localize it). */
72
+ submitLabel = "Submit",
73
+ /**
74
+ * @type {(stepNumber: number, total: number) => string}
75
+ * Builds the "Step X of Y" announcement (localize it).
76
+ */
77
+ progressLabel = (n, total) => `Step ${n} of ${total}`,
78
+
79
+ /** @type {(() => void) | undefined} Go to the previous step. */
80
+ onPrev = undefined,
81
+ /** @type {(() => void) | undefined} Advance to the next step. */
82
+ onNext = undefined,
83
+ /** @type {(() => void) | undefined} Run the final step's primary action. */
84
+ onSubmit = undefined,
85
+
86
+ /** @type {import('svelte').Snippet | undefined} The active step's body. */
87
+ children = undefined,
88
+
89
+ /** @type {string} */
90
+ class: className = "",
91
+ ...rest
92
+ } = $props();
93
+
94
+ const total = $derived(steps.length || 1);
95
+ const isFirst = $derived(current <= 0);
96
+ const isLast = $derived(current >= total - 1);
97
+ const hasErrors = $derived(errors.length > 0);
98
+
99
+ /** Map the flat step labels to the Stepper's status model. */
100
+ const indicatorSteps = $derived(
101
+ steps.map((s, i) => ({
102
+ label: s.label,
103
+ status:
104
+ i < current ? "complete" : i === current ? "active" : "upcoming",
105
+ })),
106
+ );
107
+
108
+ /** @type {HTMLElement | undefined} */
109
+ let bodyRef = $state(undefined);
110
+ /** @type {HTMLElement | undefined} */
111
+ let errorRef = $state(undefined);
112
+
113
+ // Focus management: after the first render, every step change (or a newly
114
+ // surfaced error) moves focus so a keyboard / screen-reader user is taken to
115
+ // the right place — the error summary when blocked, otherwise the step body.
116
+ // The initial mount is skipped so the page doesn't steal focus on load.
117
+ let mounted = false;
118
+ $effect(() => {
119
+ // Re-run when the step or the error state changes.
120
+ void current;
121
+ void hasErrors;
122
+ if (!mounted) {
123
+ mounted = true;
124
+ return;
125
+ }
126
+ const target = hasErrors ? errorRef : bodyRef;
127
+ target?.focus();
128
+ });
129
+ </script>
130
+
131
+ <section class="service-flow {className}" aria-label={label} {...rest}>
132
+ {#if steps.length > 1}
133
+ <Stepper steps={indicatorSteps} class="service-flow-stepper" />
134
+ {/if}
135
+
136
+ <Progress
137
+ value={current + 1}
138
+ max={total}
139
+ class="service-flow-progress"
140
+ aria-label={progressLabel(current + 1, total)}
141
+ />
142
+ <p class="service-flow-progress-text" aria-live="polite">
143
+ {progressLabel(current + 1, total)}
144
+ </p>
145
+
146
+ {#if hasErrors}
147
+ <div
148
+ class="service-flow-errors"
149
+ role="alert"
150
+ tabindex="-1"
151
+ bind:this={errorRef}
152
+ >
153
+ <h2 class="service-flow-errors-heading">{errorSummaryLabel}</h2>
154
+ <ul class="service-flow-errors-list">
155
+ {#each errors as message}
156
+ <li>{message}</li>
157
+ {/each}
158
+ </ul>
159
+ </div>
160
+ {/if}
161
+
162
+ <div class="service-flow-body" tabindex="-1" bind:this={bodyRef}>
163
+ {@render children?.()}
164
+ </div>
165
+
166
+ <div class="service-flow-nav">
167
+ {#if !isFirst}
168
+ <Button variant="ghost" disabled={busy} onclick={() => onPrev?.()}>
169
+ {prevLabel}
170
+ </Button>
171
+ {/if}
172
+ {#if isLast}
173
+ <Button
174
+ variant="primary"
175
+ loading={busy}
176
+ disabled={!canProceed || busy}
177
+ onclick={() => onSubmit?.()}
178
+ >
179
+ {submitLabel}
180
+ </Button>
181
+ {:else}
182
+ <Button
183
+ variant="primary"
184
+ loading={busy}
185
+ disabled={!canProceed || busy}
186
+ onclick={() => onNext?.()}
187
+ >
188
+ {nextLabel}
189
+ </Button>
190
+ {/if}
191
+ </div>
192
+ </section>
193
+
194
+ <style>
195
+ .service-flow {
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: var(--space-lg);
199
+ }
200
+
201
+ .service-flow-progress-text {
202
+ font-family: var(--type-body-sm-font);
203
+ font-size: var(--type-body-sm-size);
204
+ color: var(--color-text-muted);
205
+ margin: calc(-1 * var(--space-sm)) 0 0;
206
+ }
207
+
208
+ .service-flow-errors {
209
+ display: flex;
210
+ flex-direction: column;
211
+ gap: var(--space-sm);
212
+ padding: var(--space-md);
213
+ border: 1px solid var(--color-destructive);
214
+ border-radius: var(--radius-md);
215
+ background: var(--color-destructive-subtle);
216
+ }
217
+
218
+ /* The alert region is programmatically focusable but shows no focus ring. */
219
+ .service-flow-errors:focus {
220
+ outline: none;
221
+ }
222
+
223
+ .service-flow-errors-heading {
224
+ font-family: var(--type-heading-sm-font);
225
+ font-size: var(--type-heading-sm-size);
226
+ color: var(--color-text);
227
+ margin: 0;
228
+ }
229
+
230
+ .service-flow-errors-list {
231
+ margin: 0;
232
+ padding-left: var(--space-lg);
233
+ display: flex;
234
+ flex-direction: column;
235
+ gap: var(--space-2xs);
236
+ font-family: var(--type-body-font);
237
+ font-size: var(--type-body-size);
238
+ color: var(--color-text);
239
+ }
240
+
241
+ .service-flow-body {
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: var(--space-md);
245
+ }
246
+
247
+ .service-flow-body:focus {
248
+ outline: none;
249
+ }
250
+
251
+ .service-flow-nav {
252
+ display: flex;
253
+ flex-wrap: wrap;
254
+ justify-content: space-between;
255
+ gap: var(--space-sm);
256
+ padding-top: var(--space-md);
257
+ border-top: 1px solid var(--color-border);
258
+ }
259
+
260
+ /* When only the primary button shows (first step), keep it right-aligned. */
261
+ .service-flow-nav:has(> :only-child) {
262
+ justify-content: flex-end;
263
+ }
264
+ </style>
@@ -0,0 +1,83 @@
1
+ export default ServiceFlow;
2
+ export type FlowStep = {
3
+ label: string;
4
+ };
5
+ type ServiceFlow = {
6
+ $on?(type: string, callback: (e: any) => void): () => void;
7
+ $set?(props: Partial<$$ComponentProps>): void;
8
+ };
9
+ /**
10
+ * ServiceFlow
11
+ *
12
+ * The chrome for a multi-step submit journey (a "service flow"): a step
13
+ * indicator, a progress bar, an error-summary region, the active step's body,
14
+ * and the prev / next / submit navigation. Vertical-agnostic — it renders
15
+ * whatever the consumer puts in the `children` snippet for the active step and
16
+ * never knows what an "eligibility check" or "proposal" is. The consumer owns
17
+ * the wizard STATE (which step, the collected answers) and the network; this
18
+ * shell owns the layout, the accessible step semantics, and focus management.
19
+ *
20
+ * Controlled: pass `current` (0-based active step) and the `steps` labels;
21
+ * drive it with `onPrev` / `onNext` (advance) and `onSubmit` (the final step's
22
+ * primary action). `canProceed=false` disables the primary button (e.g. an
23
+ * eligibility gate failed or the step is invalid); `busy=true` shows it loading
24
+ * and locks the nav (a dry-run or the submit is in flight).
25
+ *
26
+ * Accessibility (WCAG): a `<section>` region named by `label`; a polite live
27
+ * region announces "Step X of Y"; on every step change focus moves to the step
28
+ * body (or, if the consumer surfaces `errors`, to the error summary — an
29
+ * `role="alert"` list the citizen lands on so the reason is read first). The
30
+ * error summary, the step nav buttons, and the progress bar all ride semantic
31
+ * tokens, so dark / high-contrast schemes (#244) work unchanged.
32
+ *
33
+ * @example
34
+ * <ServiceFlow
35
+ * label="Submit a proposal"
36
+ * steps={[{ label: "Eligibility" }, { label: "Your idea" }, { label: "Review" }]}
37
+ * current={step}
38
+ * canProceed={eligible}
39
+ * busy={checking}
40
+ * errors={violations}
41
+ * onPrev={() => step--}
42
+ * onNext={() => step++}
43
+ * onSubmit={() => submit()}
44
+ * >
45
+ * {@render activeStepBody()}
46
+ * </ServiceFlow>
47
+ */
48
+ declare const ServiceFlow: import("svelte").Component<{
49
+ steps?: any[];
50
+ current?: number;
51
+ label?: string;
52
+ canProceed?: boolean;
53
+ busy?: boolean;
54
+ errors?: any[];
55
+ errorSummaryLabel?: string;
56
+ prevLabel?: string;
57
+ nextLabel?: string;
58
+ submitLabel?: string;
59
+ progressLabel?: Function;
60
+ onPrev?: any;
61
+ onNext?: any;
62
+ onSubmit?: any;
63
+ children?: any;
64
+ class?: string;
65
+ } & Record<string, any>, {}, "">;
66
+ type $$ComponentProps = {
67
+ steps?: any[];
68
+ current?: number;
69
+ label?: string;
70
+ canProceed?: boolean;
71
+ busy?: boolean;
72
+ errors?: any[];
73
+ errorSummaryLabel?: string;
74
+ prevLabel?: string;
75
+ nextLabel?: string;
76
+ submitLabel?: string;
77
+ progressLabel?: Function;
78
+ onPrev?: any;
79
+ onNext?: any;
80
+ onSubmit?: any;
81
+ children?: any;
82
+ class?: string;
83
+ } & Record<string, any>;
@@ -97,6 +97,13 @@ export { default as NotificationPrefs } from "./NotificationPrefs.svelte";
97
97
  // data portability (export) + type-to-confirm account erasure.
98
98
  export { default as ConsentManager } from "./ConsentManager.svelte";
99
99
 
100
+ // Service-flow steps (#105 Phase 6) — the multi-step submit journey: a wizard
101
+ // shell (step nav + progress + focus mgmt + error summary), the review-before-
102
+ // submit answer list (WCAG 3.3.4 change links), and the success receipt.
103
+ export { default as ServiceFlow } from "./ServiceFlow.svelte";
104
+ export { default as CheckAnswers } from "./CheckAnswers.svelte";
105
+ export { default as Confirmation } from "./Confirmation.svelte";
106
+
100
107
  // Feedback
101
108
  export { default as Alert } from "./Alert.svelte";
102
109
  export { default as Toast } from "./Toast.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",