@aiaiai-pt/design-system 0.25.0 → 0.27.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/CallToAction.svelte +180 -0
- package/components/CallToAction.svelte.d.ts +43 -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/MapPicker.svelte +17 -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/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 +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component CallToAction
|
|
3
|
+
|
|
4
|
+
A conversion band: a heading, optional body, and one or two action buttons.
|
|
5
|
+
The reusable "ready to act?" strip at the foot of landing / campaign / content
|
|
6
|
+
pages. Presentational — heading, body and buttons (label + href) are DATA, so
|
|
7
|
+
it drops into any vertical.
|
|
8
|
+
|
|
9
|
+
`variant="accent"` fills the band with the brand accent (text flips to
|
|
10
|
+
`--color-text-on-accent`); `"surface"` (default) is a quiet bordered band.
|
|
11
|
+
|
|
12
|
+
@example
|
|
13
|
+
<CallToAction
|
|
14
|
+
heading="Tem uma ideia para o seu município?"
|
|
15
|
+
body="Submeta a sua proposta ao Orçamento Participativo."
|
|
16
|
+
primary={{ label: "Submeter proposta", href: "/propor" }}
|
|
17
|
+
secondary={{ label: "Ver propostas", href: "/mapa" }}
|
|
18
|
+
variant="accent"
|
|
19
|
+
/>
|
|
20
|
+
-->
|
|
21
|
+
<script>
|
|
22
|
+
import Button from "./Button.svelte";
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
/** @type {string} */
|
|
26
|
+
heading = "",
|
|
27
|
+
/** @type {string} */
|
|
28
|
+
body = "",
|
|
29
|
+
/** @type {2 | 3} Heading level for `heading`. */
|
|
30
|
+
headingLevel = 2,
|
|
31
|
+
/** @type {{ label?: string, href?: string } | undefined} Primary CTA. */
|
|
32
|
+
primary = undefined,
|
|
33
|
+
/** @type {{ label?: string, href?: string } | undefined} Secondary CTA. */
|
|
34
|
+
secondary = undefined,
|
|
35
|
+
/** @type {'surface' | 'accent'} Band fill. */
|
|
36
|
+
variant = "surface",
|
|
37
|
+
/** @type {string} */
|
|
38
|
+
class: className = "",
|
|
39
|
+
...rest
|
|
40
|
+
} = $props();
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<section class="cta cta-{variant} {className}" {...rest}>
|
|
44
|
+
<div class="cta-inner">
|
|
45
|
+
{#if heading}
|
|
46
|
+
<svelte:element this={`h${headingLevel}`} class="cta-heading"
|
|
47
|
+
>{heading}</svelte:element
|
|
48
|
+
>
|
|
49
|
+
{/if}
|
|
50
|
+
{#if body}<p class="cta-body">{body}</p>{/if}
|
|
51
|
+
{#if (primary && primary.label) || (secondary && secondary.label)}
|
|
52
|
+
<div class="cta-actions">
|
|
53
|
+
{#if variant === "accent"}
|
|
54
|
+
<!-- On an accent (brand-coloured) fill the DS button variants don't
|
|
55
|
+
guarantee contrast (the theme's `secondary`/`ghost` text/bg sit
|
|
56
|
+
too close to the accent). Render explicit INVERTED controls whose
|
|
57
|
+
colours are the on-accent/accent token pair — the same contrast
|
|
58
|
+
ratio as a primary button, just flipped. -->
|
|
59
|
+
{#if primary && primary.label}
|
|
60
|
+
<a class="cta-btn-inverted" href={primary.href || undefined}
|
|
61
|
+
>{primary.label}</a
|
|
62
|
+
>
|
|
63
|
+
{/if}
|
|
64
|
+
{#if secondary && secondary.label}
|
|
65
|
+
<a class="cta-link-on-accent" href={secondary.href || undefined}
|
|
66
|
+
>{secondary.label}</a
|
|
67
|
+
>
|
|
68
|
+
{/if}
|
|
69
|
+
{:else}
|
|
70
|
+
{#if primary && primary.label}
|
|
71
|
+
<Button href={primary.href || undefined} variant="primary"
|
|
72
|
+
>{primary.label}</Button
|
|
73
|
+
>
|
|
74
|
+
{/if}
|
|
75
|
+
{#if secondary && secondary.label}
|
|
76
|
+
<Button href={secondary.href || undefined} variant="ghost"
|
|
77
|
+
>{secondary.label}</Button
|
|
78
|
+
>
|
|
79
|
+
{/if}
|
|
80
|
+
{/if}
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<style>
|
|
87
|
+
.cta {
|
|
88
|
+
border-radius: var(--card-radius);
|
|
89
|
+
overflow-wrap: break-word;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.cta-surface {
|
|
93
|
+
background: var(--color-surface-secondary);
|
|
94
|
+
border: 1px solid var(--color-border);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.cta-accent {
|
|
98
|
+
background: var(--color-accent);
|
|
99
|
+
color: var(--color-text-on-accent);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.cta-inner {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: var(--space-md);
|
|
106
|
+
align-items: flex-start;
|
|
107
|
+
max-width: var(--content-width-wide);
|
|
108
|
+
margin-inline: auto;
|
|
109
|
+
padding: var(--space-2xl) var(--content-padding-x);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.cta-heading {
|
|
113
|
+
margin: 0;
|
|
114
|
+
font-family: var(--type-heading-font);
|
|
115
|
+
font-size: var(--type-heading-size);
|
|
116
|
+
line-height: var(--type-heading-leading);
|
|
117
|
+
color: inherit;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.cta-surface .cta-heading {
|
|
121
|
+
color: var(--color-text);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.cta-body {
|
|
125
|
+
margin: 0;
|
|
126
|
+
max-width: var(--content-width-narrow);
|
|
127
|
+
font-family: var(--type-body-font);
|
|
128
|
+
font-size: var(--type-body-size);
|
|
129
|
+
line-height: var(--type-body-leading);
|
|
130
|
+
color: inherit;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.cta-surface .cta-body {
|
|
134
|
+
color: var(--color-text-secondary);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.cta-actions {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-wrap: wrap;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: var(--space-md);
|
|
142
|
+
margin-top: var(--space-xs);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Inverted solid button for the accent band: the on-accent surface as the
|
|
146
|
+
fill, the accent as the text — the primary-button token pair, flipped, so
|
|
147
|
+
it carries the same contrast guarantee against the coloured band. */
|
|
148
|
+
.cta-btn-inverted {
|
|
149
|
+
display: inline-flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
height: var(--button-md-height);
|
|
152
|
+
padding-inline: var(--button-md-padding-x);
|
|
153
|
+
border-radius: var(--button-radius);
|
|
154
|
+
background: var(--color-text-on-accent);
|
|
155
|
+
color: var(--color-accent);
|
|
156
|
+
font-family: var(--button-font);
|
|
157
|
+
font-size: var(--button-md-font-size);
|
|
158
|
+
letter-spacing: var(--button-tracking);
|
|
159
|
+
text-decoration: none;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.cta-btn-inverted:focus-visible {
|
|
164
|
+
outline: var(--focus-ring-width) solid var(--color-text-on-accent);
|
|
165
|
+
outline-offset: var(--focus-ring-offset);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.cta-link-on-accent {
|
|
169
|
+
color: var(--color-text-on-accent);
|
|
170
|
+
font-family: var(--type-body-font);
|
|
171
|
+
font-size: var(--type-body-size);
|
|
172
|
+
text-decoration: underline;
|
|
173
|
+
text-underline-offset: 0.2em;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.cta-link-on-accent:focus-visible {
|
|
177
|
+
outline: var(--focus-ring-width) solid var(--color-text-on-accent);
|
|
178
|
+
outline-offset: var(--focus-ring-offset);
|
|
179
|
+
}
|
|
180
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export default CallToAction;
|
|
2
|
+
type CallToAction = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* CallToAction
|
|
8
|
+
*
|
|
9
|
+
* A conversion band: a heading, optional body, and one or two action buttons.
|
|
10
|
+
* The reusable "ready to act?" strip at the foot of landing / campaign / content
|
|
11
|
+
* pages. Presentational — heading, body and buttons (label + href) are DATA, so
|
|
12
|
+
* it drops into any vertical.
|
|
13
|
+
*
|
|
14
|
+
* `variant="accent"` fills the band with the brand accent (text flips to
|
|
15
|
+
* `--color-text-on-accent`); `"surface"` (default) is a quiet bordered band.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <CallToAction
|
|
19
|
+
* heading="Tem uma ideia para o seu município?"
|
|
20
|
+
* body="Submeta a sua proposta ao Orçamento Participativo."
|
|
21
|
+
* primary={{ label: "Submeter proposta", href: "/propor" }}
|
|
22
|
+
* secondary={{ label: "Ver propostas", href: "/mapa" }}
|
|
23
|
+
* variant="accent"
|
|
24
|
+
* />
|
|
25
|
+
*/
|
|
26
|
+
declare const CallToAction: import("svelte").Component<{
|
|
27
|
+
heading?: string;
|
|
28
|
+
body?: string;
|
|
29
|
+
headingLevel?: number;
|
|
30
|
+
primary?: any;
|
|
31
|
+
secondary?: any;
|
|
32
|
+
variant?: string;
|
|
33
|
+
class?: string;
|
|
34
|
+
} & Record<string, any>, {}, "">;
|
|
35
|
+
type $$ComponentProps = {
|
|
36
|
+
heading?: string;
|
|
37
|
+
body?: string;
|
|
38
|
+
headingLevel?: number;
|
|
39
|
+
primary?: any;
|
|
40
|
+
secondary?: any;
|
|
41
|
+
variant?: string;
|
|
42
|
+
class?: string;
|
|
43
|
+
} & 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;
|