@aiaiai-pt/design-system 0.25.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/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/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,116 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component LogoStrip
|
|
3
|
+
|
|
4
|
+
A row of partner / sponsor logos. Presentational — the logos (image URL + alt
|
|
5
|
+
text + optional link) are DATA. The strip wraps on narrow viewports. When a
|
|
6
|
+
logo has an `href` it renders a link; otherwise a plain image. Each logo MUST
|
|
7
|
+
carry meaningful `alt` (the partner's name) or be explicitly decorative.
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
<LogoStrip
|
|
11
|
+
label="Parceiros"
|
|
12
|
+
logos={[
|
|
13
|
+
{ src: "/logos/cm-valongo.svg", alt: "Câmara Municipal de Valongo", href: "https://cm-valongo.pt" },
|
|
14
|
+
{ src: "/logos/ue.svg", alt: "União Europeia" },
|
|
15
|
+
]}
|
|
16
|
+
/>
|
|
17
|
+
-->
|
|
18
|
+
<script>
|
|
19
|
+
let {
|
|
20
|
+
/** @type {string} Accessible label for the logo group (localize it). */
|
|
21
|
+
label = "Partners",
|
|
22
|
+
/** @type {string} Optional visible heading (renders above the strip). */
|
|
23
|
+
heading = "",
|
|
24
|
+
/** @type {2 | 3} Heading level for `heading`. */
|
|
25
|
+
headingLevel = 2,
|
|
26
|
+
/**
|
|
27
|
+
* @type {Array<{ src?: string, alt?: string, href?: string }>} The logos.
|
|
28
|
+
*/
|
|
29
|
+
logos = [],
|
|
30
|
+
/** @type {string} */
|
|
31
|
+
class: className = "",
|
|
32
|
+
...rest
|
|
33
|
+
} = $props();
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<section class="logo-strip {className}" aria-label={heading ? undefined : label} {...rest}>
|
|
37
|
+
{#if heading}
|
|
38
|
+
<svelte:element this={`h${headingLevel}`} class="logo-strip-heading"
|
|
39
|
+
>{heading}</svelte:element
|
|
40
|
+
>
|
|
41
|
+
{/if}
|
|
42
|
+
<ul class="logo-strip-list">
|
|
43
|
+
{#each logos as logo (logo.src)}
|
|
44
|
+
<li class="logo-strip-item">
|
|
45
|
+
{#if logo.href}
|
|
46
|
+
<a class="logo-strip-link" href={logo.href}>
|
|
47
|
+
<img class="logo-strip-img" src={logo.src} alt={logo.alt ?? ""} loading="lazy" />
|
|
48
|
+
</a>
|
|
49
|
+
{:else}
|
|
50
|
+
<img class="logo-strip-img" src={logo.src} alt={logo.alt ?? ""} loading="lazy" />
|
|
51
|
+
{/if}
|
|
52
|
+
</li>
|
|
53
|
+
{/each}
|
|
54
|
+
</ul>
|
|
55
|
+
</section>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.logo-strip {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: var(--space-lg);
|
|
62
|
+
align-items: center;
|
|
63
|
+
overflow-wrap: break-word;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.logo-strip-heading {
|
|
67
|
+
margin: 0;
|
|
68
|
+
font-family: var(--type-label-font);
|
|
69
|
+
font-size: var(--type-label-size);
|
|
70
|
+
font-weight: var(--type-label-weight);
|
|
71
|
+
letter-spacing: var(--type-label-tracking);
|
|
72
|
+
text-transform: uppercase;
|
|
73
|
+
color: var(--color-text-secondary);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.logo-strip-list {
|
|
77
|
+
list-style: none;
|
|
78
|
+
margin: 0;
|
|
79
|
+
padding: 0;
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-wrap: wrap;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
gap: var(--space-2xl);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.logo-strip-item {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.logo-strip-link:focus-visible {
|
|
93
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
94
|
+
outline-offset: var(--focus-ring-offset);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.logo-strip-img {
|
|
98
|
+
display: block;
|
|
99
|
+
max-height: 3rem;
|
|
100
|
+
width: auto;
|
|
101
|
+
/* Quiet the logos to a single visual weight; full opacity on hover/focus. */
|
|
102
|
+
opacity: 0.75;
|
|
103
|
+
transition: opacity var(--duration-fast) var(--easing-default);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.logo-strip-link:hover .logo-strip-img,
|
|
107
|
+
.logo-strip-link:focus-visible .logo-strip-img {
|
|
108
|
+
opacity: 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@media (prefers-reduced-motion: reduce) {
|
|
112
|
+
.logo-strip-img {
|
|
113
|
+
transition: none;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export default LogoStrip;
|
|
2
|
+
type LogoStrip = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* LogoStrip
|
|
8
|
+
*
|
|
9
|
+
* A row of partner / sponsor logos. Presentational — the logos (image URL + alt
|
|
10
|
+
* text + optional link) are DATA. The strip wraps on narrow viewports. When a
|
|
11
|
+
* logo has an `href` it renders a link; otherwise a plain image. Each logo MUST
|
|
12
|
+
* carry meaningful `alt` (the partner's name) or be explicitly decorative.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <LogoStrip
|
|
16
|
+
* label="Parceiros"
|
|
17
|
+
* logos={[
|
|
18
|
+
* { src: "/logos/cm-valongo.svg", alt: "Câmara Municipal de Valongo", href: "https://cm-valongo.pt" },
|
|
19
|
+
* { src: "/logos/ue.svg", alt: "União Europeia" },
|
|
20
|
+
* ]}
|
|
21
|
+
* />
|
|
22
|
+
*/
|
|
23
|
+
declare const LogoStrip: import("svelte").Component<{
|
|
24
|
+
label?: string;
|
|
25
|
+
heading?: string;
|
|
26
|
+
headingLevel?: number;
|
|
27
|
+
logos?: any[];
|
|
28
|
+
class?: string;
|
|
29
|
+
} & Record<string, any>, {}, "">;
|
|
30
|
+
type $$ComponentProps = {
|
|
31
|
+
label?: string;
|
|
32
|
+
heading?: string;
|
|
33
|
+
headingLevel?: number;
|
|
34
|
+
logos?: any[];
|
|
35
|
+
class?: string;
|
|
36
|
+
} & Record<string, any>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component MediaGallery
|
|
3
|
+
|
|
4
|
+
A landing-grade media gallery — a responsive grid (or a horizontal scroll-snap
|
|
5
|
+
rail) of captioned images. Distinct from the detail-page image strip: this is
|
|
6
|
+
presentational showcase content for landing / campaign / about pages. No JS
|
|
7
|
+
carousel/autoplay (an accessibility and reduced-motion liability); on narrow
|
|
8
|
+
viewports the `rail` layout becomes a swipeable scroll-snap row that is fully
|
|
9
|
+
keyboard-scrollable. Each image carries operator-authored `alt`; captions are
|
|
10
|
+
optional `<figcaption>`s.
|
|
11
|
+
|
|
12
|
+
@example Grid
|
|
13
|
+
<MediaGallery
|
|
14
|
+
heading="Projetos executados"
|
|
15
|
+
items={[
|
|
16
|
+
{ src: "/g/parque.jpg", alt: "Novo parque urbano", caption: "Parque da Cidade — 2025" },
|
|
17
|
+
{ src: "/g/ciclovia.jpg", alt: "Ciclovia ribeirinha", caption: "Ciclovia — 2024" },
|
|
18
|
+
]}
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
@example Horizontal rail
|
|
22
|
+
<MediaGallery layout="rail" label="Galeria" items={photos} />
|
|
23
|
+
-->
|
|
24
|
+
<script>
|
|
25
|
+
let {
|
|
26
|
+
/** @type {string} Optional visible heading. */
|
|
27
|
+
heading = "",
|
|
28
|
+
/** @type {2 | 3} Heading level for `heading`. */
|
|
29
|
+
headingLevel = 2,
|
|
30
|
+
/** @type {string} Accessible label when there is no visible heading. */
|
|
31
|
+
label = "Gallery",
|
|
32
|
+
/** @type {'grid' | 'rail'} Grid (wrap) or horizontal scroll-snap rail. */
|
|
33
|
+
layout = "grid",
|
|
34
|
+
/** @type {'2' | '3' | '4'} Grid columns at desktop (grid layout only). */
|
|
35
|
+
columns = "3",
|
|
36
|
+
/**
|
|
37
|
+
* @type {Array<{ src?: string, alt?: string, caption?: string }>} The media.
|
|
38
|
+
*/
|
|
39
|
+
items = [],
|
|
40
|
+
/** @type {string} */
|
|
41
|
+
class: className = "",
|
|
42
|
+
...rest
|
|
43
|
+
} = $props();
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<section
|
|
47
|
+
class="media-gallery {className}"
|
|
48
|
+
aria-label={heading ? undefined : label}
|
|
49
|
+
{...rest}
|
|
50
|
+
>
|
|
51
|
+
{#if heading}
|
|
52
|
+
<svelte:element this={`h${headingLevel}`} class="media-gallery-heading"
|
|
53
|
+
>{heading}</svelte:element
|
|
54
|
+
>
|
|
55
|
+
{/if}
|
|
56
|
+
<!-- A horizontal scroll-snap rail is a scrollable region: give it keyboard
|
|
57
|
+
access (tabindex=0) and an accessible name so it isn't an axe
|
|
58
|
+
`scrollable-region-focusable` violation. Keep the native list semantics
|
|
59
|
+
(no role override) so the <li>s stay valid list items. The grid layout
|
|
60
|
+
doesn't scroll, so it's a plain list. -->
|
|
61
|
+
<ul
|
|
62
|
+
class="media-gallery-list media-gallery-{layout} media-gallery-cols-{columns}"
|
|
63
|
+
tabindex={layout === "rail" ? 0 : undefined}
|
|
64
|
+
aria-label={layout === "rail" ? heading || label : undefined}
|
|
65
|
+
>
|
|
66
|
+
{#each items as item (item.src)}
|
|
67
|
+
<li class="media-gallery-item">
|
|
68
|
+
<figure class="media-gallery-figure">
|
|
69
|
+
<img class="media-gallery-img" src={item.src} alt={item.alt ?? ""} loading="lazy" />
|
|
70
|
+
{#if item.caption}
|
|
71
|
+
<figcaption class="media-gallery-caption">{item.caption}</figcaption>
|
|
72
|
+
{/if}
|
|
73
|
+
</figure>
|
|
74
|
+
</li>
|
|
75
|
+
{/each}
|
|
76
|
+
</ul>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.media-gallery {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
gap: var(--space-xl);
|
|
84
|
+
overflow-wrap: break-word;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.media-gallery-heading {
|
|
88
|
+
margin: 0;
|
|
89
|
+
font-family: var(--type-heading-font);
|
|
90
|
+
font-size: var(--type-heading-size);
|
|
91
|
+
line-height: var(--type-heading-leading);
|
|
92
|
+
color: var(--color-text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.media-gallery-list {
|
|
96
|
+
list-style: none;
|
|
97
|
+
margin: 0;
|
|
98
|
+
padding: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ─── Grid layout ─── */
|
|
102
|
+
.media-gallery-grid {
|
|
103
|
+
display: grid;
|
|
104
|
+
gap: var(--grid-gutter);
|
|
105
|
+
grid-template-columns: 1fr;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@media (min-width: 768px) {
|
|
109
|
+
.media-gallery-grid.media-gallery-cols-2 {
|
|
110
|
+
grid-template-columns: repeat(2, 1fr);
|
|
111
|
+
}
|
|
112
|
+
.media-gallery-grid.media-gallery-cols-3 {
|
|
113
|
+
grid-template-columns: repeat(2, 1fr);
|
|
114
|
+
}
|
|
115
|
+
.media-gallery-grid.media-gallery-cols-4 {
|
|
116
|
+
grid-template-columns: repeat(2, 1fr);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@media (min-width: 1024px) {
|
|
121
|
+
.media-gallery-grid.media-gallery-cols-3 {
|
|
122
|
+
grid-template-columns: repeat(3, 1fr);
|
|
123
|
+
}
|
|
124
|
+
.media-gallery-grid.media-gallery-cols-4 {
|
|
125
|
+
grid-template-columns: repeat(4, 1fr);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ─── Horizontal scroll-snap rail ─── */
|
|
130
|
+
.media-gallery-rail {
|
|
131
|
+
display: flex;
|
|
132
|
+
gap: var(--grid-gutter);
|
|
133
|
+
overflow-x: auto;
|
|
134
|
+
scroll-snap-type: x mandatory;
|
|
135
|
+
padding-bottom: var(--space-sm);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.media-gallery-rail:focus-visible {
|
|
139
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
140
|
+
outline-offset: var(--focus-ring-offset);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.media-gallery-rail .media-gallery-item {
|
|
144
|
+
flex: 0 0 80%;
|
|
145
|
+
scroll-snap-align: start;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (min-width: 768px) {
|
|
149
|
+
.media-gallery-rail .media-gallery-item {
|
|
150
|
+
flex-basis: 40%;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.media-gallery-figure {
|
|
155
|
+
margin: 0;
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
gap: var(--space-xs);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.media-gallery-img {
|
|
162
|
+
display: block;
|
|
163
|
+
width: 100%;
|
|
164
|
+
aspect-ratio: 4 / 3;
|
|
165
|
+
object-fit: cover;
|
|
166
|
+
border-radius: var(--card-radius);
|
|
167
|
+
background: var(--color-surface-secondary);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.media-gallery-caption {
|
|
171
|
+
font-family: var(--type-caption-font);
|
|
172
|
+
font-size: var(--type-caption-size);
|
|
173
|
+
color: var(--color-text-muted);
|
|
174
|
+
}
|
|
175
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export default MediaGallery;
|
|
2
|
+
type MediaGallery = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* MediaGallery
|
|
8
|
+
*
|
|
9
|
+
* A landing-grade media gallery — a responsive grid (or a horizontal scroll-snap
|
|
10
|
+
* rail) of captioned images. Distinct from the detail-page image strip: this is
|
|
11
|
+
* presentational showcase content for landing / campaign / about pages. No JS
|
|
12
|
+
* carousel/autoplay (an accessibility and reduced-motion liability); on narrow
|
|
13
|
+
* viewports the `rail` layout becomes a swipeable scroll-snap row that is fully
|
|
14
|
+
* keyboard-scrollable. Each image carries operator-authored `alt`; captions are
|
|
15
|
+
* optional `<figcaption>`s.
|
|
16
|
+
*
|
|
17
|
+
* @example Grid
|
|
18
|
+
* <MediaGallery
|
|
19
|
+
* heading="Projetos executados"
|
|
20
|
+
* items={[
|
|
21
|
+
* { src: "/g/parque.jpg", alt: "Novo parque urbano", caption: "Parque da Cidade — 2025" },
|
|
22
|
+
* { src: "/g/ciclovia.jpg", alt: "Ciclovia ribeirinha", caption: "Ciclovia — 2024" },
|
|
23
|
+
* ]}
|
|
24
|
+
* />
|
|
25
|
+
*
|
|
26
|
+
* @example Horizontal rail
|
|
27
|
+
* <MediaGallery layout="rail" label="Galeria" items={photos} />
|
|
28
|
+
*/
|
|
29
|
+
declare const MediaGallery: import("svelte").Component<{
|
|
30
|
+
heading?: string;
|
|
31
|
+
headingLevel?: number;
|
|
32
|
+
label?: string;
|
|
33
|
+
layout?: string;
|
|
34
|
+
columns?: string;
|
|
35
|
+
items?: any[];
|
|
36
|
+
class?: string;
|
|
37
|
+
} & Record<string, any>, {}, "">;
|
|
38
|
+
type $$ComponentProps = {
|
|
39
|
+
heading?: string;
|
|
40
|
+
headingLevel?: number;
|
|
41
|
+
label?: string;
|
|
42
|
+
layout?: string;
|
|
43
|
+
columns?: string;
|
|
44
|
+
items?: any[];
|
|
45
|
+
class?: string;
|
|
46
|
+
} & Record<string, any>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component MediaText
|
|
3
|
+
|
|
4
|
+
An image (or video) beside a prose column — the alternating "about / feature"
|
|
5
|
+
section. Presentational: heading, body, media URL + alt text, and the optional
|
|
6
|
+
CTA are DATA, so it drops into any vertical. `reverse` flips the media to the
|
|
7
|
+
other side for the classic zig-zag layout; columns stack on narrow viewports.
|
|
8
|
+
|
|
9
|
+
The body slot is the consumer's pre-sanitised rich content (the portal passes
|
|
10
|
+
sanitised markdown through `bodyHtml`); a plain `body` string is rendered as a
|
|
11
|
+
paragraph when no HTML is given. The image carries operator-authored `alt`.
|
|
12
|
+
|
|
13
|
+
@example
|
|
14
|
+
<MediaText
|
|
15
|
+
heading="Orçamento Participativo"
|
|
16
|
+
body="Os cidadãos decidem como investir parte do orçamento municipal."
|
|
17
|
+
image="/media/op.jpg"
|
|
18
|
+
alt="Sessão pública de participação"
|
|
19
|
+
cta={{ label: "Saber mais", href: "/info/regulamento" }}
|
|
20
|
+
/>
|
|
21
|
+
-->
|
|
22
|
+
<script>
|
|
23
|
+
import Button from "./Button.svelte";
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
/** @type {string} */
|
|
27
|
+
heading = "",
|
|
28
|
+
/** @type {2 | 3} Heading level for `heading`. */
|
|
29
|
+
headingLevel = 2,
|
|
30
|
+
/** @type {string} Plain-text body (used when `bodyHtml`/`children` absent). */
|
|
31
|
+
body = "",
|
|
32
|
+
/** @type {string | undefined} Pre-sanitised HTML body (XSS boundary is the consumer). */
|
|
33
|
+
bodyHtml = undefined,
|
|
34
|
+
/** @type {string | undefined} Image URL. */
|
|
35
|
+
image = undefined,
|
|
36
|
+
/** @type {string} Image alt text (operator data; "" = decorative). */
|
|
37
|
+
alt = "",
|
|
38
|
+
/** @type {string | undefined} Video URL (rendered instead of the image). */
|
|
39
|
+
video = undefined,
|
|
40
|
+
/** @type {boolean} Place the media on the right (zig-zag). */
|
|
41
|
+
reverse = false,
|
|
42
|
+
/** @type {{ label?: string, href?: string } | undefined} */
|
|
43
|
+
cta = undefined,
|
|
44
|
+
/** @type {string} */
|
|
45
|
+
class: className = "",
|
|
46
|
+
/** @type {import('svelte').Snippet | undefined} Rich body override. */
|
|
47
|
+
children = undefined,
|
|
48
|
+
...rest
|
|
49
|
+
} = $props();
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<section class="media-text {className}" class:media-text-reverse={reverse} {...rest}>
|
|
53
|
+
<div class="media-text-media">
|
|
54
|
+
{#if video}
|
|
55
|
+
<!-- svelte-ignore a11y_media_has_caption -->
|
|
56
|
+
<video class="media-text-asset" src={video} controls></video>
|
|
57
|
+
{:else if image}
|
|
58
|
+
<img class="media-text-asset" src={image} {alt} loading="lazy" />
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
<div class="media-text-body">
|
|
62
|
+
{#if heading}
|
|
63
|
+
<svelte:element this={`h${headingLevel}`} class="media-text-heading"
|
|
64
|
+
>{heading}</svelte:element
|
|
65
|
+
>
|
|
66
|
+
{/if}
|
|
67
|
+
{#if children}
|
|
68
|
+
{@render children()}
|
|
69
|
+
{:else if bodyHtml !== undefined}
|
|
70
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags — caller-sanitised -->
|
|
71
|
+
<div class="media-text-prose">{@html bodyHtml}</div>
|
|
72
|
+
{:else if body}
|
|
73
|
+
<p class="media-text-prose">{body}</p>
|
|
74
|
+
{/if}
|
|
75
|
+
{#if cta && cta.label}
|
|
76
|
+
<div class="media-text-cta">
|
|
77
|
+
<Button href={cta.href || undefined} variant="secondary"
|
|
78
|
+
>{cta.label}</Button
|
|
79
|
+
>
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.media-text {
|
|
87
|
+
display: grid;
|
|
88
|
+
grid-template-columns: 1fr;
|
|
89
|
+
gap: var(--space-xl);
|
|
90
|
+
align-items: center;
|
|
91
|
+
overflow-wrap: break-word;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (min-width: 768px) {
|
|
95
|
+
.media-text {
|
|
96
|
+
grid-template-columns: 1fr 1fr;
|
|
97
|
+
gap: var(--space-3xl);
|
|
98
|
+
}
|
|
99
|
+
.media-text-reverse .media-text-media {
|
|
100
|
+
order: 2;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.media-text-asset {
|
|
105
|
+
display: block;
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: auto;
|
|
108
|
+
border-radius: var(--card-radius);
|
|
109
|
+
background: var(--color-surface-secondary);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.media-text-body {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
gap: var(--space-md);
|
|
116
|
+
/* Let the grid column shrink so its prose can wrap a long token. */
|
|
117
|
+
min-width: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.media-text-heading {
|
|
121
|
+
margin: 0;
|
|
122
|
+
font-family: var(--type-heading-font);
|
|
123
|
+
font-size: var(--type-heading-size);
|
|
124
|
+
line-height: var(--type-heading-leading);
|
|
125
|
+
color: var(--color-text);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.media-text-prose {
|
|
129
|
+
margin: 0;
|
|
130
|
+
font-family: var(--type-body-font);
|
|
131
|
+
font-size: var(--type-body-size);
|
|
132
|
+
line-height: var(--type-body-leading);
|
|
133
|
+
color: var(--color-text-secondary);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.media-text-prose :global(p) {
|
|
137
|
+
margin: 0 0 var(--space-sm);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.media-text-cta {
|
|
141
|
+
margin-top: var(--space-xs);
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export default MediaText;
|
|
2
|
+
type MediaText = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* MediaText
|
|
8
|
+
*
|
|
9
|
+
* An image (or video) beside a prose column — the alternating "about / feature"
|
|
10
|
+
* section. Presentational: heading, body, media URL + alt text, and the optional
|
|
11
|
+
* CTA are DATA, so it drops into any vertical. `reverse` flips the media to the
|
|
12
|
+
* other side for the classic zig-zag layout; columns stack on narrow viewports.
|
|
13
|
+
*
|
|
14
|
+
* The body slot is the consumer's pre-sanitised rich content (the portal passes
|
|
15
|
+
* sanitised markdown through `bodyHtml`); a plain `body` string is rendered as a
|
|
16
|
+
* paragraph when no HTML is given. The image carries operator-authored `alt`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <MediaText
|
|
20
|
+
* heading="Orçamento Participativo"
|
|
21
|
+
* body="Os cidadãos decidem como investir parte do orçamento municipal."
|
|
22
|
+
* image="/media/op.jpg"
|
|
23
|
+
* alt="Sessão pública de participação"
|
|
24
|
+
* cta={{ label: "Saber mais", href: "/info/regulamento" }}
|
|
25
|
+
* />
|
|
26
|
+
*/
|
|
27
|
+
declare const MediaText: import("svelte").Component<{
|
|
28
|
+
heading?: string;
|
|
29
|
+
headingLevel?: number;
|
|
30
|
+
body?: string;
|
|
31
|
+
bodyHtml?: any;
|
|
32
|
+
image?: any;
|
|
33
|
+
alt?: string;
|
|
34
|
+
video?: any;
|
|
35
|
+
reverse?: boolean;
|
|
36
|
+
cta?: any;
|
|
37
|
+
class?: string;
|
|
38
|
+
children?: any;
|
|
39
|
+
} & Record<string, any>, {}, "">;
|
|
40
|
+
type $$ComponentProps = {
|
|
41
|
+
heading?: string;
|
|
42
|
+
headingLevel?: number;
|
|
43
|
+
body?: string;
|
|
44
|
+
bodyHtml?: any;
|
|
45
|
+
image?: any;
|
|
46
|
+
alt?: string;
|
|
47
|
+
video?: any;
|
|
48
|
+
reverse?: boolean;
|
|
49
|
+
cta?: any;
|
|
50
|
+
class?: string;
|
|
51
|
+
children?: any;
|
|
52
|
+
} & 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>
|