@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.
@@ -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>