@aiaiai-pt/design-system 0.24.0 → 0.26.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 +54 -14
- package/components/ActionFormRenderer.svelte.d.ts +16 -0
- package/components/CallToAction.svelte +180 -0
- package/components/CallToAction.svelte.d.ts +43 -0
- package/components/CheckAnswers.svelte +197 -0
- package/components/CheckAnswers.svelte.d.ts +60 -0
- package/components/Confirmation.svelte +172 -0
- package/components/Confirmation.svelte.d.ts +53 -0
- package/components/FaqList.svelte +159 -0
- package/components/FaqList.svelte.d.ts +36 -0
- package/components/FeatureGrid.svelte +139 -0
- package/components/FeatureGrid.svelte.d.ts +48 -0
- package/components/Hero.svelte +23 -1
- package/components/Hero.svelte.d.ts +7 -0
- package/components/LogoStrip.svelte +116 -0
- package/components/LogoStrip.svelte.d.ts +36 -0
- package/components/MediaGallery.svelte +175 -0
- package/components/MediaGallery.svelte.d.ts +46 -0
- package/components/MediaText.svelte +143 -0
- package/components/MediaText.svelte.d.ts +52 -0
- package/components/ServiceFlow.svelte +264 -0
- package/components/ServiceFlow.svelte.d.ts +83 -0
- package/components/Steps.svelte +137 -0
- package/components/Steps.svelte.d.ts +37 -0
- package/components/Testimonial.svelte +161 -0
- package/components/Testimonial.svelte.d.ts +45 -0
- package/components/index.d.ts +16 -0
- package/components/index.js +19 -0
- package/package.json +1 -1
|
@@ -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>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Steps
|
|
3
|
+
|
|
4
|
+
A numbered "how it works" explainer — an ordered list of steps, each a number
|
|
5
|
+
badge, a heading and a short description. Distinct from `ServiceFlow`/`Stepper`
|
|
6
|
+
(which drive a live multi-step form): this is presentational marketing content
|
|
7
|
+
that explains a process. The numbers come from the list order (decorative
|
|
8
|
+
badges, `aria-hidden`) so screen readers get the native `<ol>` sequence.
|
|
9
|
+
|
|
10
|
+
@example
|
|
11
|
+
<Steps
|
|
12
|
+
heading="Como funciona"
|
|
13
|
+
steps={[
|
|
14
|
+
{ title: "Proponha", text: "Submeta a sua ideia até 31 de março." },
|
|
15
|
+
{ title: "Análise", text: "A comissão verifica a elegibilidade." },
|
|
16
|
+
{ title: "Votação", text: "Os cidadãos votam nas finalistas." },
|
|
17
|
+
{ title: "Execução", text: "As propostas vencedoras são executadas." },
|
|
18
|
+
]}
|
|
19
|
+
/>
|
|
20
|
+
-->
|
|
21
|
+
<script>
|
|
22
|
+
let {
|
|
23
|
+
/** @type {string} Optional band heading. */
|
|
24
|
+
heading = "",
|
|
25
|
+
/** @type {2 | 3} Heading level for `heading`. */
|
|
26
|
+
headingLevel = 2,
|
|
27
|
+
/**
|
|
28
|
+
* @type {Array<{ title?: string, text?: string }>} The ordered steps.
|
|
29
|
+
*/
|
|
30
|
+
steps = [],
|
|
31
|
+
/** @type {string} */
|
|
32
|
+
class: className = "",
|
|
33
|
+
...rest
|
|
34
|
+
} = $props();
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<section class="steps {className}" {...rest}>
|
|
38
|
+
{#if heading}
|
|
39
|
+
<svelte:element this={`h${headingLevel}`} class="steps-heading"
|
|
40
|
+
>{heading}</svelte:element
|
|
41
|
+
>
|
|
42
|
+
{/if}
|
|
43
|
+
<ol class="steps-list">
|
|
44
|
+
{#each steps as step, i (step.title)}
|
|
45
|
+
<li class="steps-item">
|
|
46
|
+
<span class="steps-number" aria-hidden="true">{i + 1}</span>
|
|
47
|
+
<div class="steps-body">
|
|
48
|
+
{#if step.title}
|
|
49
|
+
<svelte:element
|
|
50
|
+
this={`h${Math.min(headingLevel + 1, 4)}`}
|
|
51
|
+
class="steps-title">{step.title}</svelte:element
|
|
52
|
+
>
|
|
53
|
+
{/if}
|
|
54
|
+
{#if step.text}<p class="steps-text">{step.text}</p>{/if}
|
|
55
|
+
</div>
|
|
56
|
+
</li>
|
|
57
|
+
{/each}
|
|
58
|
+
</ol>
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.steps {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: var(--space-xl);
|
|
66
|
+
overflow-wrap: break-word;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.steps-heading {
|
|
70
|
+
margin: 0;
|
|
71
|
+
font-family: var(--type-heading-font);
|
|
72
|
+
font-size: var(--type-heading-size);
|
|
73
|
+
line-height: var(--type-heading-leading);
|
|
74
|
+
color: var(--color-text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.steps-list {
|
|
78
|
+
list-style: none;
|
|
79
|
+
margin: 0;
|
|
80
|
+
padding: 0;
|
|
81
|
+
display: grid;
|
|
82
|
+
grid-template-columns: 1fr;
|
|
83
|
+
gap: var(--space-lg);
|
|
84
|
+
counter-reset: step;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@media (min-width: 768px) {
|
|
88
|
+
.steps-list {
|
|
89
|
+
grid-template-columns: repeat(2, 1fr);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.steps-item {
|
|
94
|
+
display: flex;
|
|
95
|
+
gap: var(--space-md);
|
|
96
|
+
align-items: flex-start;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.steps-number {
|
|
100
|
+
flex-shrink: 0;
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
width: 2.25rem;
|
|
105
|
+
height: 2.25rem;
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
background: var(--color-accent);
|
|
108
|
+
color: var(--color-text-on-accent);
|
|
109
|
+
font-family: var(--type-data-font);
|
|
110
|
+
font-size: var(--type-data-size);
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.steps-body {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
gap: var(--space-2xs);
|
|
118
|
+
/* Shrink past the number badge so the text can wrap a long token. */
|
|
119
|
+
min-width: 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.steps-title {
|
|
123
|
+
margin: 0;
|
|
124
|
+
font-family: var(--type-heading-sm-font);
|
|
125
|
+
font-size: var(--type-heading-sm-size);
|
|
126
|
+
line-height: var(--type-heading-sm-leading);
|
|
127
|
+
color: var(--color-text);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.steps-text {
|
|
131
|
+
margin: 0;
|
|
132
|
+
font-family: var(--type-body-font);
|
|
133
|
+
font-size: var(--type-body-size);
|
|
134
|
+
line-height: var(--type-body-leading);
|
|
135
|
+
color: var(--color-text-secondary);
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export default Steps;
|
|
2
|
+
type Steps = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Steps
|
|
8
|
+
*
|
|
9
|
+
* A numbered "how it works" explainer — an ordered list of steps, each a number
|
|
10
|
+
* badge, a heading and a short description. Distinct from `ServiceFlow`/`Stepper`
|
|
11
|
+
* (which drive a live multi-step form): this is presentational marketing content
|
|
12
|
+
* that explains a process. The numbers come from the list order (decorative
|
|
13
|
+
* badges, `aria-hidden`) so screen readers get the native `<ol>` sequence.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <Steps
|
|
17
|
+
* heading="Como funciona"
|
|
18
|
+
* steps={[
|
|
19
|
+
* { title: "Proponha", text: "Submeta a sua ideia até 31 de março." },
|
|
20
|
+
* { title: "Análise", text: "A comissão verifica a elegibilidade." },
|
|
21
|
+
* { title: "Votação", text: "Os cidadãos votam nas finalistas." },
|
|
22
|
+
* { title: "Execução", text: "As propostas vencedoras são executadas." },
|
|
23
|
+
* ]}
|
|
24
|
+
* />
|
|
25
|
+
*/
|
|
26
|
+
declare const Steps: import("svelte").Component<{
|
|
27
|
+
heading?: string;
|
|
28
|
+
headingLevel?: number;
|
|
29
|
+
steps?: any[];
|
|
30
|
+
class?: string;
|
|
31
|
+
} & Record<string, any>, {}, "">;
|
|
32
|
+
type $$ComponentProps = {
|
|
33
|
+
heading?: string;
|
|
34
|
+
headingLevel?: number;
|
|
35
|
+
steps?: any[];
|
|
36
|
+
class?: string;
|
|
37
|
+
} & Record<string, any>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Testimonial
|
|
3
|
+
|
|
4
|
+
A quote / testimonial band — one prominent pull-quote, or a small grid of
|
|
5
|
+
them. Semantic `<figure>` + `<blockquote>` + `<figcaption>` so the attribution
|
|
6
|
+
is correctly associated with the quote. Presentational: the quotes (text +
|
|
7
|
+
author + role + optional avatar) are DATA.
|
|
8
|
+
|
|
9
|
+
@example Single quote
|
|
10
|
+
<Testimonial quote="Finalmente a minha rua tem iluminação nova." author="Maria S." role="Junta de Freguesia" />
|
|
11
|
+
|
|
12
|
+
@example A grid of quotes
|
|
13
|
+
<Testimonial
|
|
14
|
+
heading="O que dizem os cidadãos"
|
|
15
|
+
items={[
|
|
16
|
+
{ quote: "Simples e rápido.", author: "João P." },
|
|
17
|
+
{ quote: "A minha proposta foi executada!", author: "Ana R.", role: "Valongo" },
|
|
18
|
+
]}
|
|
19
|
+
/>
|
|
20
|
+
-->
|
|
21
|
+
<script>
|
|
22
|
+
let {
|
|
23
|
+
/** @type {string} Optional band heading (for the grid form). */
|
|
24
|
+
heading = "",
|
|
25
|
+
/** @type {2 | 3} Heading level for `heading`. */
|
|
26
|
+
headingLevel = 2,
|
|
27
|
+
/** @type {string} Single-quote text (when `items` is empty). */
|
|
28
|
+
quote = "",
|
|
29
|
+
/** @type {string} */
|
|
30
|
+
author = "",
|
|
31
|
+
/** @type {string} */
|
|
32
|
+
role = "",
|
|
33
|
+
/** @type {string | undefined} Avatar image URL. */
|
|
34
|
+
avatar = undefined,
|
|
35
|
+
/**
|
|
36
|
+
* @type {Array<{ quote?: string, author?: string, role?: string, avatar?: string }>}
|
|
37
|
+
* Multiple testimonials (renders a grid). Overrides the single-quote props.
|
|
38
|
+
*/
|
|
39
|
+
items = [],
|
|
40
|
+
/** @type {string} */
|
|
41
|
+
class: className = "",
|
|
42
|
+
...rest
|
|
43
|
+
} = $props();
|
|
44
|
+
|
|
45
|
+
const list = $derived(
|
|
46
|
+
items.length > 0
|
|
47
|
+
? items
|
|
48
|
+
: quote
|
|
49
|
+
? [{ quote, author, role, avatar }]
|
|
50
|
+
: [],
|
|
51
|
+
);
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<section class="testimonial {className}" {...rest}>
|
|
55
|
+
{#if heading}
|
|
56
|
+
<svelte:element this={`h${headingLevel}`} class="testimonial-heading"
|
|
57
|
+
>{heading}</svelte:element
|
|
58
|
+
>
|
|
59
|
+
{/if}
|
|
60
|
+
<div class="testimonial-grid" class:testimonial-grid-multi={list.length > 1}>
|
|
61
|
+
{#each list as item (item.quote)}
|
|
62
|
+
<figure class="testimonial-item">
|
|
63
|
+
<blockquote class="testimonial-quote">{item.quote}</blockquote>
|
|
64
|
+
{#if item.author || item.role}
|
|
65
|
+
<figcaption class="testimonial-cite">
|
|
66
|
+
{#if item.avatar}
|
|
67
|
+
<img class="testimonial-avatar" src={item.avatar} alt="" loading="lazy" />
|
|
68
|
+
{/if}
|
|
69
|
+
<span class="testimonial-attrib">
|
|
70
|
+
{#if item.author}<span class="testimonial-author">{item.author}</span>{/if}
|
|
71
|
+
{#if item.role}<span class="testimonial-role">{item.role}</span>{/if}
|
|
72
|
+
</span>
|
|
73
|
+
</figcaption>
|
|
74
|
+
{/if}
|
|
75
|
+
</figure>
|
|
76
|
+
{/each}
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.testimonial {
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
gap: var(--space-xl);
|
|
85
|
+
overflow-wrap: break-word;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.testimonial-heading {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-family: var(--type-heading-font);
|
|
91
|
+
font-size: var(--type-heading-size);
|
|
92
|
+
line-height: var(--type-heading-leading);
|
|
93
|
+
color: var(--color-text);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.testimonial-grid {
|
|
97
|
+
display: grid;
|
|
98
|
+
grid-template-columns: 1fr;
|
|
99
|
+
gap: var(--space-lg);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@media (min-width: 768px) {
|
|
103
|
+
.testimonial-grid-multi {
|
|
104
|
+
grid-template-columns: repeat(2, 1fr);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.testimonial-item {
|
|
109
|
+
margin: 0;
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
gap: var(--space-md);
|
|
113
|
+
padding: var(--space-xl);
|
|
114
|
+
background: var(--color-surface-secondary);
|
|
115
|
+
border-left: 3px solid var(--color-accent);
|
|
116
|
+
border-radius: var(--card-radius);
|
|
117
|
+
/* Grid child: shrink so a long quote/name wraps instead of overflowing. */
|
|
118
|
+
min-width: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.testimonial-quote {
|
|
122
|
+
margin: 0;
|
|
123
|
+
font-family: var(--type-heading-sm-font);
|
|
124
|
+
font-size: var(--type-heading-sm-size);
|
|
125
|
+
line-height: var(--type-heading-sm-leading);
|
|
126
|
+
color: var(--color-text);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.testimonial-cite {
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: var(--space-sm);
|
|
133
|
+
font-style: normal;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.testimonial-avatar {
|
|
137
|
+
width: 2.5rem;
|
|
138
|
+
height: 2.5rem;
|
|
139
|
+
border-radius: 50%;
|
|
140
|
+
object-fit: cover;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.testimonial-attrib {
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
min-width: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.testimonial-author {
|
|
150
|
+
font-family: var(--type-label-font);
|
|
151
|
+
font-size: var(--type-body-sm-size);
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
color: var(--color-text);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.testimonial-role {
|
|
157
|
+
font-family: var(--type-caption-font);
|
|
158
|
+
font-size: var(--type-caption-size);
|
|
159
|
+
color: var(--color-text-muted);
|
|
160
|
+
}
|
|
161
|
+
</style>
|