@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,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>;