@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,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,159 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component FaqList
|
|
3
|
+
|
|
4
|
+
A frequently-asked-questions accordion. Built on native `<details>`/`<summary>`
|
|
5
|
+
so it expand/collapses with zero JS and is keyboard + screen-reader accessible
|
|
6
|
+
out of the box. Distinct from `CollapsibleSection` (filter-group chrome, label
|
|
7
|
+
styling): this renders question-weight summaries for content/help pages.
|
|
8
|
+
Presentational — the Q/A pairs are DATA. Answers accept pre-sanitised HTML
|
|
9
|
+
(the consumer owns the XSS boundary) or plain text.
|
|
10
|
+
|
|
11
|
+
@example
|
|
12
|
+
<FaqList
|
|
13
|
+
heading="Perguntas frequentes"
|
|
14
|
+
items={[
|
|
15
|
+
{ q: "Quem pode participar?", a: "Qualquer cidadão recenseado no município." },
|
|
16
|
+
{ q: "Como submeto?", aHtml: "<p>Use o <a href='/propor'>formulário</a>.</p>" },
|
|
17
|
+
]}
|
|
18
|
+
/>
|
|
19
|
+
-->
|
|
20
|
+
<script>
|
|
21
|
+
let {
|
|
22
|
+
/** @type {string} Optional heading above the list. */
|
|
23
|
+
heading = "",
|
|
24
|
+
/** @type {2 | 3} Heading level for `heading`. */
|
|
25
|
+
headingLevel = 2,
|
|
26
|
+
/**
|
|
27
|
+
* @type {Array<{ q?: string, a?: string, aHtml?: string }>} The questions.
|
|
28
|
+
* `aHtml` (pre-sanitised) takes precedence over the plain `a` text.
|
|
29
|
+
*/
|
|
30
|
+
items = [],
|
|
31
|
+
/** @type {string} */
|
|
32
|
+
class: className = "",
|
|
33
|
+
...rest
|
|
34
|
+
} = $props();
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<section class="faq {className}" {...rest}>
|
|
38
|
+
{#if heading}
|
|
39
|
+
<svelte:element this={`h${headingLevel}`} class="faq-heading"
|
|
40
|
+
>{heading}</svelte:element
|
|
41
|
+
>
|
|
42
|
+
{/if}
|
|
43
|
+
<div class="faq-list">
|
|
44
|
+
{#each items as item (item.q)}
|
|
45
|
+
<details class="faq-item">
|
|
46
|
+
<summary class="faq-question">
|
|
47
|
+
<span class="faq-question-text">{item.q}</span>
|
|
48
|
+
<svg
|
|
49
|
+
class="faq-caret"
|
|
50
|
+
width="14"
|
|
51
|
+
height="14"
|
|
52
|
+
viewBox="0 0 14 14"
|
|
53
|
+
fill="none"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
56
|
+
<path
|
|
57
|
+
d="M3 5L7 10L11 5"
|
|
58
|
+
stroke="currentColor"
|
|
59
|
+
stroke-width="1.5"
|
|
60
|
+
stroke-linecap="round"
|
|
61
|
+
stroke-linejoin="round"
|
|
62
|
+
/>
|
|
63
|
+
</svg>
|
|
64
|
+
</summary>
|
|
65
|
+
<div class="faq-answer">
|
|
66
|
+
{#if item.aHtml !== undefined}
|
|
67
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags — caller-sanitised -->
|
|
68
|
+
{@html item.aHtml}
|
|
69
|
+
{:else if item.a}
|
|
70
|
+
<p>{item.a}</p>
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
73
|
+
</details>
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.faq {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
gap: var(--space-lg);
|
|
83
|
+
overflow-wrap: break-word;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.faq-heading {
|
|
87
|
+
margin: 0;
|
|
88
|
+
font-family: var(--type-heading-font);
|
|
89
|
+
font-size: var(--type-heading-size);
|
|
90
|
+
line-height: var(--type-heading-leading);
|
|
91
|
+
color: var(--color-text);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.faq-item {
|
|
95
|
+
border-bottom: 1px solid var(--color-border);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.faq-question {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
gap: var(--space-md);
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
padding: var(--space-md) 0;
|
|
105
|
+
list-style: none;
|
|
106
|
+
font-family: var(--type-heading-sm-font);
|
|
107
|
+
font-size: var(--type-heading-sm-size);
|
|
108
|
+
line-height: var(--type-heading-sm-leading);
|
|
109
|
+
color: var(--color-text);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.faq-question::-webkit-details-marker {
|
|
113
|
+
display: none;
|
|
114
|
+
}
|
|
115
|
+
.faq-question::marker {
|
|
116
|
+
content: "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.faq-question:focus-visible {
|
|
120
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
121
|
+
outline-offset: var(--focus-ring-offset);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Flex item: shrink past the caret so a long question wraps/breaks. */
|
|
125
|
+
.faq-question-text {
|
|
126
|
+
min-width: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.faq-caret {
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
color: var(--color-text-secondary);
|
|
132
|
+
transition: transform var(--duration-fast) var(--easing-default);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.faq-item[open] .faq-caret {
|
|
136
|
+
transform: rotate(180deg);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.faq-answer {
|
|
140
|
+
padding: 0 0 var(--space-md);
|
|
141
|
+
font-family: var(--type-body-font);
|
|
142
|
+
font-size: var(--type-body-size);
|
|
143
|
+
line-height: var(--type-body-leading);
|
|
144
|
+
color: var(--color-text-secondary);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.faq-answer :global(p) {
|
|
148
|
+
margin: 0 0 var(--space-sm);
|
|
149
|
+
}
|
|
150
|
+
.faq-answer :global(p:last-child) {
|
|
151
|
+
margin-bottom: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@media (prefers-reduced-motion: reduce) {
|
|
155
|
+
.faq-caret {
|
|
156
|
+
transition: none;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export default FaqList;
|
|
2
|
+
type FaqList = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* FaqList
|
|
8
|
+
*
|
|
9
|
+
* A frequently-asked-questions accordion. Built on native `<details>`/`<summary>`
|
|
10
|
+
* so it expand/collapses with zero JS and is keyboard + screen-reader accessible
|
|
11
|
+
* out of the box. Distinct from `CollapsibleSection` (filter-group chrome, label
|
|
12
|
+
* styling): this renders question-weight summaries for content/help pages.
|
|
13
|
+
* Presentational — the Q/A pairs are DATA. Answers accept pre-sanitised HTML
|
|
14
|
+
* (the consumer owns the XSS boundary) or plain text.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <FaqList
|
|
18
|
+
* heading="Perguntas frequentes"
|
|
19
|
+
* items={[
|
|
20
|
+
* { q: "Quem pode participar?", a: "Qualquer cidadão recenseado no município." },
|
|
21
|
+
* { q: "Como submeto?", aHtml: "<p>Use o <a href='/propor'>formulário</a>.</p>" },
|
|
22
|
+
* ]}
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
declare const FaqList: import("svelte").Component<{
|
|
26
|
+
heading?: string;
|
|
27
|
+
headingLevel?: number;
|
|
28
|
+
items?: any[];
|
|
29
|
+
class?: string;
|
|
30
|
+
} & Record<string, any>, {}, "">;
|
|
31
|
+
type $$ComponentProps = {
|
|
32
|
+
heading?: string;
|
|
33
|
+
headingLevel?: number;
|
|
34
|
+
items?: any[];
|
|
35
|
+
class?: string;
|
|
36
|
+
} & Record<string, any>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component FeatureGrid
|
|
3
|
+
|
|
4
|
+
A responsive grid of feature cards — each an optional icon, a heading, and a
|
|
5
|
+
short description (optionally a link). The landing/marketing "what you can do"
|
|
6
|
+
/ highlights band. Purely presentational: the items are DATA (page props), so
|
|
7
|
+
it drops into any vertical with different copy. Built on CardGrid + Card.
|
|
8
|
+
|
|
9
|
+
Headings render at `headingLevel` (default h3 — the band sits under a section
|
|
10
|
+
heading the consumer owns). Icons are DECORATIVE: the DS stays icon-set
|
|
11
|
+
agnostic, so the consumer supplies an `icon` snippet that renders an icon for
|
|
12
|
+
each item (e.g. mapping `item.icon` to its own icon library). No snippet → no
|
|
13
|
+
icons, cards still render.
|
|
14
|
+
|
|
15
|
+
@example
|
|
16
|
+
<FeatureGrid
|
|
17
|
+
heading="Como participar"
|
|
18
|
+
items={[
|
|
19
|
+
{ icon: "lightbulb", title: "Proponha", text: "Submeta a sua ideia." },
|
|
20
|
+
{ icon: "check-circle", title: "Vote", text: "Escolha as vencedoras.", href: "/propor" },
|
|
21
|
+
]}
|
|
22
|
+
>
|
|
23
|
+
{#snippet icon(item)}<MyIcon name={item.icon} />{/snippet}
|
|
24
|
+
</FeatureGrid>
|
|
25
|
+
-->
|
|
26
|
+
<script>
|
|
27
|
+
import CardGrid from "./CardGrid.svelte";
|
|
28
|
+
import Card from "./Card.svelte";
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
/** @type {string} Optional band heading (rendered above the grid). */
|
|
32
|
+
heading = "",
|
|
33
|
+
/** @type {string} Optional supporting text under the heading. */
|
|
34
|
+
intro = "",
|
|
35
|
+
/** @type {2 | 3 | 4} Heading level for `heading`. */
|
|
36
|
+
headingLevel = 2,
|
|
37
|
+
/** @type {'2' | '3' | '4'} Grid columns at desktop. */
|
|
38
|
+
columns = "3",
|
|
39
|
+
/**
|
|
40
|
+
* @type {Array<{ icon?: string, title?: string, text?: string, href?: string }>}
|
|
41
|
+
* Feature items. `icon` is an opaque name passed to the `icon` snippet;
|
|
42
|
+
* `href` makes the whole card a link.
|
|
43
|
+
*/
|
|
44
|
+
items = [],
|
|
45
|
+
/**
|
|
46
|
+
* @type {import('svelte').Snippet<[{ icon?: string }]> | undefined}
|
|
47
|
+
* Decorative per-item icon. Receives the item; the consumer maps `item.icon`
|
|
48
|
+
* to its own icon component (the DS doesn't bundle an icon set).
|
|
49
|
+
*/
|
|
50
|
+
icon = undefined,
|
|
51
|
+
/** @type {string} */
|
|
52
|
+
class: className = "",
|
|
53
|
+
...rest
|
|
54
|
+
} = $props();
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div class="feature-grid {className}" {...rest}>
|
|
58
|
+
{#if heading}
|
|
59
|
+
<div class="feature-grid-head">
|
|
60
|
+
<svelte:element this={`h${headingLevel}`} class="feature-grid-heading"
|
|
61
|
+
>{heading}</svelte:element
|
|
62
|
+
>
|
|
63
|
+
{#if intro}<p class="feature-grid-intro">{intro}</p>{/if}
|
|
64
|
+
</div>
|
|
65
|
+
{/if}
|
|
66
|
+
|
|
67
|
+
<CardGrid {columns}>
|
|
68
|
+
{#each items as item (item.title)}
|
|
69
|
+
<Card href={item.href || undefined} interactive={!!item.href}>
|
|
70
|
+
{#if icon && item.icon}
|
|
71
|
+
<span class="feature-icon" aria-hidden="true">{@render icon(item)}</span>
|
|
72
|
+
{/if}
|
|
73
|
+
{#if item.title}
|
|
74
|
+
<svelte:element
|
|
75
|
+
this={`h${Math.min(headingLevel + 1, 4)}`}
|
|
76
|
+
class="feature-title">{item.title}</svelte:element
|
|
77
|
+
>
|
|
78
|
+
{/if}
|
|
79
|
+
{#if item.text}<p class="feature-text">{item.text}</p>{/if}
|
|
80
|
+
</Card>
|
|
81
|
+
{/each}
|
|
82
|
+
</CardGrid>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.feature-grid {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
gap: var(--space-xl);
|
|
90
|
+
/* Break a pathological unbreakable token instead of blowing out a card
|
|
91
|
+
(CardGrid already sets min-width:0 on its children). Inherited. */
|
|
92
|
+
overflow-wrap: break-word;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.feature-grid-head {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
gap: var(--space-sm);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.feature-grid-heading {
|
|
102
|
+
margin: 0;
|
|
103
|
+
font-family: var(--type-heading-font);
|
|
104
|
+
font-size: var(--type-heading-size);
|
|
105
|
+
line-height: var(--type-heading-leading);
|
|
106
|
+
color: var(--color-text);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.feature-grid-intro {
|
|
110
|
+
margin: 0;
|
|
111
|
+
max-width: var(--content-width-narrow);
|
|
112
|
+
font-family: var(--type-body-font);
|
|
113
|
+
font-size: var(--type-body-size);
|
|
114
|
+
color: var(--color-text-secondary);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.feature-icon {
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
color: var(--color-accent);
|
|
120
|
+
margin-bottom: var(--space-sm);
|
|
121
|
+
line-height: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.feature-title {
|
|
125
|
+
margin: 0 0 var(--space-xs);
|
|
126
|
+
font-family: var(--type-heading-sm-font);
|
|
127
|
+
font-size: var(--type-heading-sm-size);
|
|
128
|
+
line-height: var(--type-heading-sm-leading);
|
|
129
|
+
color: var(--color-text);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.feature-text {
|
|
133
|
+
margin: 0;
|
|
134
|
+
font-family: var(--type-body-font);
|
|
135
|
+
font-size: var(--type-body-size);
|
|
136
|
+
line-height: var(--type-body-leading);
|
|
137
|
+
color: var(--color-text-secondary);
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export default FeatureGrid;
|
|
2
|
+
type FeatureGrid = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* FeatureGrid
|
|
8
|
+
*
|
|
9
|
+
* A responsive grid of feature cards — each an optional icon, a heading, and a
|
|
10
|
+
* short description (optionally a link). The landing/marketing "what you can do"
|
|
11
|
+
* / highlights band. Purely presentational: the items are DATA (page props), so
|
|
12
|
+
* it drops into any vertical with different copy. Built on CardGrid + Card.
|
|
13
|
+
*
|
|
14
|
+
* Headings render at `headingLevel` (default h3 — the band sits under a section
|
|
15
|
+
* heading the consumer owns). Icons are DECORATIVE: the DS stays icon-set
|
|
16
|
+
* agnostic, so the consumer supplies an `icon` snippet that renders an icon for
|
|
17
|
+
* each item (e.g. mapping `item.icon` to its own icon library). No snippet → no
|
|
18
|
+
* icons, cards still render.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <FeatureGrid
|
|
22
|
+
* heading="Como participar"
|
|
23
|
+
* items={[
|
|
24
|
+
* { icon: "lightbulb", title: "Proponha", text: "Submeta a sua ideia." },
|
|
25
|
+
* { icon: "check-circle", title: "Vote", text: "Escolha as vencedoras.", href: "/propor" },
|
|
26
|
+
* ]}
|
|
27
|
+
* >
|
|
28
|
+
* {#snippet icon(item)}<MyIcon name={item.icon} />{/snippet}
|
|
29
|
+
* </FeatureGrid>
|
|
30
|
+
*/
|
|
31
|
+
declare const FeatureGrid: import("svelte").Component<{
|
|
32
|
+
heading?: string;
|
|
33
|
+
intro?: string;
|
|
34
|
+
headingLevel?: number;
|
|
35
|
+
columns?: string;
|
|
36
|
+
items?: any[];
|
|
37
|
+
icon?: any;
|
|
38
|
+
class?: string;
|
|
39
|
+
} & Record<string, any>, {}, "">;
|
|
40
|
+
type $$ComponentProps = {
|
|
41
|
+
heading?: string;
|
|
42
|
+
intro?: string;
|
|
43
|
+
headingLevel?: number;
|
|
44
|
+
columns?: string;
|
|
45
|
+
items?: any[];
|
|
46
|
+
icon?: any;
|
|
47
|
+
class?: string;
|
|
48
|
+
} & Record<string, any>;
|
package/components/Hero.svelte
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
@example Background image (a theme-tinted scrim keeps text contrast)
|
|
18
18
|
<Hero title="Report a problem" image="/images/city.jpg" />
|
|
19
|
+
|
|
20
|
+
@example Centered marketing hero with a call-to-action
|
|
21
|
+
<Hero title="Participe" subtitle="As suas ideias." align="center" image="/op.jpg">
|
|
22
|
+
{#snippet actions()}<Button href="/propor">Submeter</Button>{/snippet}
|
|
23
|
+
</Hero>
|
|
19
24
|
-->
|
|
20
25
|
<script>
|
|
21
26
|
let {
|
|
@@ -28,6 +33,10 @@
|
|
|
28
33
|
/** @type {string | undefined} Background image URL — rendered cover/center
|
|
29
34
|
* under a `--hero-scrim` overlay so the text tokens keep contrast. */
|
|
30
35
|
image = undefined,
|
|
36
|
+
/** @type {'start' | 'center'} Content alignment — `center` is the marketing
|
|
37
|
+
* variant (centered title/subtitle/actions); `start` (default) is the
|
|
38
|
+
* left-aligned process hero. */
|
|
39
|
+
align = "start",
|
|
31
40
|
/** @type {string} */
|
|
32
41
|
class: className = "",
|
|
33
42
|
/** @type {import('svelte').Snippet | undefined} Title override (rich content). */
|
|
@@ -46,7 +55,7 @@
|
|
|
46
55
|
</script>
|
|
47
56
|
|
|
48
57
|
<section
|
|
49
|
-
class="hero {className}"
|
|
58
|
+
class="hero hero-align-{align} {className}"
|
|
50
59
|
class:hero-has-image={!!image}
|
|
51
60
|
style={bgStyle}
|
|
52
61
|
{...rest}
|
|
@@ -69,6 +78,9 @@
|
|
|
69
78
|
<style>
|
|
70
79
|
.hero {
|
|
71
80
|
background: var(--color-surface);
|
|
81
|
+
/* Break a pathological unbreakable token (long URL/word) instead of
|
|
82
|
+
overflowing the band. overflow-wrap is inherited → covers all text. */
|
|
83
|
+
overflow-wrap: break-word;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
.hero-has-image {
|
|
@@ -120,4 +132,14 @@
|
|
|
120
132
|
gap: var(--space-sm);
|
|
121
133
|
margin-top: var(--space-sm);
|
|
122
134
|
}
|
|
135
|
+
|
|
136
|
+
/* Centered marketing variant — title/subtitle/actions all centre-aligned. */
|
|
137
|
+
.hero-align-center .hero-inner {
|
|
138
|
+
align-items: center;
|
|
139
|
+
text-align: center;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.hero-align-center .hero-subtitle {
|
|
143
|
+
margin-inline: auto;
|
|
144
|
+
}
|
|
123
145
|
</style>
|
|
@@ -21,12 +21,18 @@ type Hero = {
|
|
|
21
21
|
*
|
|
22
22
|
* @example Background image (a theme-tinted scrim keeps text contrast)
|
|
23
23
|
* <Hero title="Report a problem" image="/images/city.jpg" />
|
|
24
|
+
*
|
|
25
|
+
* @example Centered marketing hero with a call-to-action
|
|
26
|
+
* <Hero title="Participe" subtitle="As suas ideias." align="center" image="/op.jpg">
|
|
27
|
+
* {#snippet actions()}<Button href="/propor">Submeter</Button>{/snippet}
|
|
28
|
+
* </Hero>
|
|
24
29
|
*/
|
|
25
30
|
declare const Hero: import("svelte").Component<{
|
|
26
31
|
title?: string;
|
|
27
32
|
subtitle?: string;
|
|
28
33
|
headingLevel?: number;
|
|
29
34
|
image?: any;
|
|
35
|
+
align?: string;
|
|
30
36
|
class?: string;
|
|
31
37
|
children?: any;
|
|
32
38
|
actions?: any;
|
|
@@ -36,6 +42,7 @@ type $$ComponentProps = {
|
|
|
36
42
|
subtitle?: string;
|
|
37
43
|
headingLevel?: number;
|
|
38
44
|
image?: any;
|
|
45
|
+
align?: string;
|
|
39
46
|
class?: string;
|
|
40
47
|
children?: any;
|
|
41
48
|
actions?: any;
|