@aiaiai-pt/design-system 0.8.4 → 0.9.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/AppFrame.svelte +75 -0
- package/components/AppFrame.svelte.d.ts +42 -0
- package/components/ContentBlock.svelte +100 -0
- package/components/ContentBlock.svelte.d.ts +37 -0
- package/components/Hero.svelte +87 -0
- package/components/Hero.svelte.d.ts +37 -0
- package/components/Link.svelte +100 -0
- package/components/Link.svelte.d.ts +46 -0
- package/components/ServiceNavigation.svelte +160 -0
- package/components/ServiceNavigation.svelte.d.ts +49 -0
- package/components/SiteFooter.svelte +83 -0
- package/components/SiteFooter.svelte.d.ts +37 -0
- package/components/SiteHeader.svelte +90 -0
- package/components/SiteHeader.svelte.d.ts +36 -0
- package/components/SkipLink.svelte +63 -0
- package/components/SkipLink.svelte.d.ts +33 -0
- package/components/StatusTimeline.svelte +193 -0
- package/components/StatusTimeline.svelte.d.ts +44 -0
- package/components/VotingWidget.svelte +292 -0
- package/components/VotingWidget.svelte.d.ts +80 -0
- package/components/WidgetGrid.svelte +83 -0
- package/components/WidgetGrid.svelte.d.ts +42 -0
- package/components/index.js +13 -0
- package/package.json +1 -1
- package/tokens/themes/valongo.css +44 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component VotingWidget
|
|
3
|
+
|
|
4
|
+
Participation vote panel — the Valongo V10 participation-grid primitive
|
|
5
|
+
(participatory budget, consultations, local polls). One question, a radio
|
|
6
|
+
group of options, a submit path, and a pseudonymised receipt after voting.
|
|
7
|
+
Presentational: it emits `onsubmit(optionId)`; the portal wires that to the
|
|
8
|
+
BFF public submit surface (`POST /{app}/public/submit`, voting placement —
|
|
9
|
+
#70 M4), and renders the bot-protection challenge into the `captcha` slot.
|
|
10
|
+
|
|
11
|
+
States (mutually exclusive vote-ability):
|
|
12
|
+
- open — radio group + submit (the default)
|
|
13
|
+
- submitted — confirmation + the `receipt` slot, voting locked
|
|
14
|
+
- closed — voting period over; results only
|
|
15
|
+
- disabled — not eligible (e.g. anonymous where login is required);
|
|
16
|
+
options shown read-only with an announced `disabledReason`
|
|
17
|
+
Pass `showResults` to render the tally as labelled bars (typically once
|
|
18
|
+
`closed` or `submitted`).
|
|
19
|
+
|
|
20
|
+
Accessibility:
|
|
21
|
+
- `<fieldset>` + `<legend>` (the question) groups the options as a native
|
|
22
|
+
radio group — arrow-key navigation and "n of m" come for free.
|
|
23
|
+
- Submit is disabled until an option is chosen; the confirmation + receipt
|
|
24
|
+
land in an `aria-live="polite"` region so a screen reader announces them.
|
|
25
|
+
- Result bars are `aria-hidden`; each option's percentage is in its visible
|
|
26
|
+
text, so the meaning isn't colour/width-only.
|
|
27
|
+
- Radios keep a visible focus ring; reduced-motion disables the bar fill.
|
|
28
|
+
|
|
29
|
+
@example
|
|
30
|
+
<VotingWidget
|
|
31
|
+
question="Which project should Valongo fund first?"
|
|
32
|
+
name="pb-2026"
|
|
33
|
+
options={[{ id: "a", label: "Riverside path" }, { id: "b", label: "School playground" }]}
|
|
34
|
+
bind:selected
|
|
35
|
+
onsubmit={(id) => postVote(id)}
|
|
36
|
+
>
|
|
37
|
+
{#snippet captcha()}<TurnstileWidget /> {/snippet}
|
|
38
|
+
</VotingWidget>
|
|
39
|
+
-->
|
|
40
|
+
<script>
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {{ id: string, label: string, votes?: number }} VoteOption
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
/** @type {string} The question (rendered as the fieldset legend). */
|
|
47
|
+
question = "",
|
|
48
|
+
/** @type {VoteOption[]} */
|
|
49
|
+
options = [],
|
|
50
|
+
/** @type {string | undefined} Bindable selected option id. */
|
|
51
|
+
selected = $bindable(undefined),
|
|
52
|
+
/** @type {string} Radio-group name — unique per widget on the page. */
|
|
53
|
+
name = "vote",
|
|
54
|
+
/** @type {boolean} Voting period is over. */
|
|
55
|
+
closed = false,
|
|
56
|
+
/** @type {boolean} This citizen has already voted. */
|
|
57
|
+
submitted = false,
|
|
58
|
+
/** @type {boolean} Voting unavailable (e.g. not eligible / login required). */
|
|
59
|
+
disabled = false,
|
|
60
|
+
/** @type {boolean} Render the tally as labelled bars. */
|
|
61
|
+
showResults = false,
|
|
62
|
+
/** @type {string} Submit button text (localize it). */
|
|
63
|
+
submitLabel = "Submit vote",
|
|
64
|
+
/** @type {string} Heading shown in the submitted state (localize it). */
|
|
65
|
+
submittedLabel = "Your vote has been recorded.",
|
|
66
|
+
/** @type {string} Notice shown in the closed state (localize it). */
|
|
67
|
+
closedLabel = "Voting has closed.",
|
|
68
|
+
/** @type {string} Reason voting is unavailable, announced (localize it). */
|
|
69
|
+
disabledReason = "",
|
|
70
|
+
/** @type {((optionId: string) => void) | undefined} */
|
|
71
|
+
onsubmit = undefined,
|
|
72
|
+
/** @type {string} */
|
|
73
|
+
class: className = "",
|
|
74
|
+
/** @type {import('svelte').Snippet | undefined} Bot-protection challenge (Turnstile). */
|
|
75
|
+
captcha = undefined,
|
|
76
|
+
/** @type {import('svelte').Snippet | undefined} Pseudonymised receipt, shown after voting. */
|
|
77
|
+
receipt = undefined,
|
|
78
|
+
...rest
|
|
79
|
+
} = $props();
|
|
80
|
+
|
|
81
|
+
const canVote = $derived(!closed && !submitted && !disabled);
|
|
82
|
+
const total = $derived(
|
|
83
|
+
options.reduce((sum, o) => sum + (o.votes ?? 0), 0),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
/** @param {number | undefined} votes */
|
|
87
|
+
const pct = (votes) => (total > 0 ? Math.round(((votes ?? 0) / total) * 100) : 0);
|
|
88
|
+
|
|
89
|
+
/** @param {SubmitEvent} e */
|
|
90
|
+
function handleSubmit(e) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
if (canVote && selected) onsubmit?.(selected);
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<section class="voting-widget {className}" {...rest}>
|
|
97
|
+
<form class="voting-form" onsubmit={handleSubmit} novalidate>
|
|
98
|
+
<fieldset class="voting-fieldset" disabled={!canVote}>
|
|
99
|
+
<legend class="voting-legend">{question}</legend>
|
|
100
|
+
|
|
101
|
+
{#if disabled && disabledReason}
|
|
102
|
+
<p class="voting-reason" role="note">{disabledReason}</p>
|
|
103
|
+
{/if}
|
|
104
|
+
|
|
105
|
+
<ul class="voting-options">
|
|
106
|
+
{#each options as option (option.id)}
|
|
107
|
+
<li class="voting-option">
|
|
108
|
+
<label class="voting-label">
|
|
109
|
+
<input
|
|
110
|
+
type="radio"
|
|
111
|
+
class="voting-radio"
|
|
112
|
+
{name}
|
|
113
|
+
value={option.id}
|
|
114
|
+
bind:group={selected}
|
|
115
|
+
disabled={!canVote}
|
|
116
|
+
/>
|
|
117
|
+
<span class="voting-option-text">{option.label}</span>
|
|
118
|
+
{#if showResults}
|
|
119
|
+
<span class="voting-pct">{pct(option.votes)}%</span>
|
|
120
|
+
{/if}
|
|
121
|
+
</label>
|
|
122
|
+
{#if showResults}
|
|
123
|
+
<div class="voting-bar" aria-hidden="true">
|
|
124
|
+
<div class="voting-bar-fill" style:width={`${pct(option.votes)}%`}></div>
|
|
125
|
+
</div>
|
|
126
|
+
{/if}
|
|
127
|
+
</li>
|
|
128
|
+
{/each}
|
|
129
|
+
</ul>
|
|
130
|
+
</fieldset>
|
|
131
|
+
|
|
132
|
+
{#if canVote}
|
|
133
|
+
{#if captcha}<div class="voting-captcha">{@render captcha()}</div>{/if}
|
|
134
|
+
<button type="submit" class="voting-submit" disabled={!selected}>
|
|
135
|
+
{submitLabel}
|
|
136
|
+
</button>
|
|
137
|
+
{/if}
|
|
138
|
+
</form>
|
|
139
|
+
|
|
140
|
+
<!-- Status region: confirmation / closed notice land here so AT announces them. -->
|
|
141
|
+
<div class="voting-status" role="status" aria-live="polite">
|
|
142
|
+
{#if submitted}
|
|
143
|
+
<p class="voting-confirm">{submittedLabel}</p>
|
|
144
|
+
{#if receipt}<div class="voting-receipt">{@render receipt()}</div>{/if}
|
|
145
|
+
{:else if closed}
|
|
146
|
+
<p class="voting-closed">{closedLabel}</p>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
<style>
|
|
152
|
+
.voting-widget {
|
|
153
|
+
display: flex;
|
|
154
|
+
flex-direction: column;
|
|
155
|
+
gap: var(--space-lg);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.voting-form {
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
gap: var(--space-lg);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.voting-fieldset {
|
|
165
|
+
margin: 0;
|
|
166
|
+
padding: 0;
|
|
167
|
+
border: 0;
|
|
168
|
+
min-width: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.voting-legend {
|
|
172
|
+
padding: 0;
|
|
173
|
+
margin-bottom: var(--space-md);
|
|
174
|
+
font-family: var(--type-heading-sm-font);
|
|
175
|
+
font-size: var(--type-heading-sm-size);
|
|
176
|
+
color: var(--color-text);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.voting-reason {
|
|
180
|
+
margin: 0 0 var(--space-md);
|
|
181
|
+
font-family: var(--type-body-sm-font);
|
|
182
|
+
font-size: var(--type-body-sm-size);
|
|
183
|
+
color: var(--color-text-muted);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.voting-options {
|
|
187
|
+
list-style: none;
|
|
188
|
+
margin: 0;
|
|
189
|
+
padding: 0;
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
gap: var(--space-sm);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.voting-label {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: var(--space-sm);
|
|
199
|
+
padding: var(--space-sm) var(--space-md);
|
|
200
|
+
border: 1px solid var(--color-border);
|
|
201
|
+
border-radius: var(--radius-md);
|
|
202
|
+
background: var(--color-surface);
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
}
|
|
205
|
+
/* Selected row reads as chosen without relying on the native dot alone. */
|
|
206
|
+
.voting-label:has(.voting-radio:checked) {
|
|
207
|
+
border-color: var(--color-accent);
|
|
208
|
+
background: var(--color-accent-subtle);
|
|
209
|
+
}
|
|
210
|
+
.voting-label:has(.voting-radio:focus-visible) {
|
|
211
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
212
|
+
outline-offset: var(--focus-ring-offset);
|
|
213
|
+
}
|
|
214
|
+
.voting-fieldset:disabled .voting-label {
|
|
215
|
+
cursor: default;
|
|
216
|
+
opacity: 0.7;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.voting-radio {
|
|
220
|
+
flex-shrink: 0;
|
|
221
|
+
accent-color: var(--color-accent);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.voting-option-text {
|
|
225
|
+
flex: 1;
|
|
226
|
+
font-family: var(--type-body-font);
|
|
227
|
+
font-size: var(--type-body-size);
|
|
228
|
+
color: var(--color-text);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.voting-pct {
|
|
232
|
+
font-family: var(--type-data-font);
|
|
233
|
+
font-size: var(--type-data-size);
|
|
234
|
+
color: var(--color-text-secondary);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.voting-bar {
|
|
238
|
+
height: var(--space-2xs);
|
|
239
|
+
margin-top: var(--space-2xs);
|
|
240
|
+
border-radius: var(--radius-pill);
|
|
241
|
+
background: var(--color-surface-tertiary);
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
}
|
|
244
|
+
.voting-bar-fill {
|
|
245
|
+
height: 100%;
|
|
246
|
+
background: var(--color-accent);
|
|
247
|
+
transition: width var(--duration-normal) var(--easing-default);
|
|
248
|
+
}
|
|
249
|
+
@media (prefers-reduced-motion: reduce) {
|
|
250
|
+
.voting-bar-fill {
|
|
251
|
+
transition: none;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.voting-submit {
|
|
256
|
+
align-self: flex-start;
|
|
257
|
+
padding: var(--space-sm) var(--space-lg);
|
|
258
|
+
border: 0;
|
|
259
|
+
border-radius: var(--radius-md);
|
|
260
|
+
background: var(--color-accent);
|
|
261
|
+
color: var(--color-text-on-accent);
|
|
262
|
+
font-family: var(--type-label-font);
|
|
263
|
+
font-size: var(--type-label-size);
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
}
|
|
266
|
+
.voting-submit:hover:not(:disabled) {
|
|
267
|
+
background: var(--color-accent-hover);
|
|
268
|
+
}
|
|
269
|
+
.voting-submit:focus-visible {
|
|
270
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
271
|
+
outline-offset: var(--focus-ring-offset);
|
|
272
|
+
}
|
|
273
|
+
.voting-submit:disabled {
|
|
274
|
+
opacity: 0.5;
|
|
275
|
+
cursor: not-allowed;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.voting-confirm,
|
|
279
|
+
.voting-closed {
|
|
280
|
+
margin: 0;
|
|
281
|
+
font-family: var(--type-label-font);
|
|
282
|
+
font-size: var(--type-label-size);
|
|
283
|
+
color: var(--color-text);
|
|
284
|
+
}
|
|
285
|
+
.voting-confirm {
|
|
286
|
+
color: var(--color-success);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.voting-receipt {
|
|
290
|
+
margin-top: var(--space-sm);
|
|
291
|
+
}
|
|
292
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export default VotingWidget;
|
|
2
|
+
type VotingWidget = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* VotingWidget
|
|
8
|
+
*
|
|
9
|
+
* Participation vote panel — the Valongo V10 participation-grid primitive
|
|
10
|
+
* (participatory budget, consultations, local polls). One question, a radio
|
|
11
|
+
* group of options, a submit path, and a pseudonymised receipt after voting.
|
|
12
|
+
* Presentational: it emits `onsubmit(optionId)`; the portal wires that to the
|
|
13
|
+
* BFF public submit surface (`POST /{app}/public/submit`, voting placement —
|
|
14
|
+
* #70 M4), and renders the bot-protection challenge into the `captcha` slot.
|
|
15
|
+
*
|
|
16
|
+
* States (mutually exclusive vote-ability):
|
|
17
|
+
* - open — radio group + submit (the default)
|
|
18
|
+
* - submitted — confirmation + the `receipt` slot, voting locked
|
|
19
|
+
* - closed — voting period over; results only
|
|
20
|
+
* - disabled — not eligible (e.g. anonymous where login is required);
|
|
21
|
+
* options shown read-only with an announced `disabledReason`
|
|
22
|
+
* Pass `showResults` to render the tally as labelled bars (typically once
|
|
23
|
+
* `closed` or `submitted`).
|
|
24
|
+
*
|
|
25
|
+
* Accessibility:
|
|
26
|
+
* - `<fieldset>` + `<legend>` (the question) groups the options as a native
|
|
27
|
+
* radio group — arrow-key navigation and "n of m" come for free.
|
|
28
|
+
* - Submit is disabled until an option is chosen; the confirmation + receipt
|
|
29
|
+
* land in an `aria-live="polite"` region so a screen reader announces them.
|
|
30
|
+
* - Result bars are `aria-hidden`; each option's percentage is in its visible
|
|
31
|
+
* text, so the meaning isn't colour/width-only.
|
|
32
|
+
* - Radios keep a visible focus ring; reduced-motion disables the bar fill.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* <VotingWidget
|
|
36
|
+
* question="Which project should Valongo fund first?"
|
|
37
|
+
* name="pb-2026"
|
|
38
|
+
* options={[{ id: "a", label: "Riverside path" }, { id: "b", label: "School playground" }]}
|
|
39
|
+
* bind:selected
|
|
40
|
+
* onsubmit={(id) => postVote(id)}
|
|
41
|
+
* >
|
|
42
|
+
* {#snippet captcha()}<TurnstileWidget /> {/snippet}
|
|
43
|
+
* </VotingWidget>
|
|
44
|
+
*/
|
|
45
|
+
declare const VotingWidget: import("svelte").Component<{
|
|
46
|
+
question?: string;
|
|
47
|
+
options?: any[];
|
|
48
|
+
selected?: any;
|
|
49
|
+
name?: string;
|
|
50
|
+
closed?: boolean;
|
|
51
|
+
submitted?: boolean;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
showResults?: boolean;
|
|
54
|
+
submitLabel?: string;
|
|
55
|
+
submittedLabel?: string;
|
|
56
|
+
closedLabel?: string;
|
|
57
|
+
disabledReason?: string;
|
|
58
|
+
onsubmit?: any;
|
|
59
|
+
class?: string;
|
|
60
|
+
captcha?: any;
|
|
61
|
+
receipt?: any;
|
|
62
|
+
} & Record<string, any>, {}, "selected">;
|
|
63
|
+
type $$ComponentProps = {
|
|
64
|
+
question?: string;
|
|
65
|
+
options?: any[];
|
|
66
|
+
selected?: any;
|
|
67
|
+
name?: string;
|
|
68
|
+
closed?: boolean;
|
|
69
|
+
submitted?: boolean;
|
|
70
|
+
disabled?: boolean;
|
|
71
|
+
showResults?: boolean;
|
|
72
|
+
submitLabel?: string;
|
|
73
|
+
submittedLabel?: string;
|
|
74
|
+
closedLabel?: string;
|
|
75
|
+
disabledReason?: string;
|
|
76
|
+
onsubmit?: any;
|
|
77
|
+
class?: string;
|
|
78
|
+
captcha?: any;
|
|
79
|
+
receipt?: any;
|
|
80
|
+
} & Record<string, any>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component WidgetGrid
|
|
3
|
+
|
|
4
|
+
Responsive dashboard layout for the citizen portal's `landing` /
|
|
5
|
+
information-portal pages, where blocks vary in weight — a hero stat, a list of
|
|
6
|
+
recent consultations, a map, a voting panel. Where CardGrid lays out equal
|
|
7
|
+
cards, WidgetGrid is a column grid whose children may declare a span, so a
|
|
8
|
+
feature widget can sit beside two narrow ones.
|
|
9
|
+
|
|
10
|
+
Collapses to one column on small screens (every widget full-width), goes to
|
|
11
|
+
two at tablet, and to `columns` at desktop. Children opt into width via the
|
|
12
|
+
span helper classes (applied to the direct child, e.g. a `<Card>` or
|
|
13
|
+
`<section>`):
|
|
14
|
+
|
|
15
|
+
- `widget-span-2` — span two columns from tablet up
|
|
16
|
+
- `widget-span-full` — span the full row at every breakpoint
|
|
17
|
+
|
|
18
|
+
Layout only — it sets no landmark and imposes no semantics; the page supplies
|
|
19
|
+
headings and regions.
|
|
20
|
+
|
|
21
|
+
@example
|
|
22
|
+
<WidgetGrid columns="3">
|
|
23
|
+
<section class="widget-span-2"><Hero ... /></section>
|
|
24
|
+
<Card>…</Card>
|
|
25
|
+
<Card class="widget-span-full">…</Card>
|
|
26
|
+
</WidgetGrid>
|
|
27
|
+
-->
|
|
28
|
+
<script>
|
|
29
|
+
let {
|
|
30
|
+
/** @type {'2' | '3' | '4'} Column count at the desktop breakpoint. */
|
|
31
|
+
columns = "3",
|
|
32
|
+
/** @type {string} */
|
|
33
|
+
class: className = "",
|
|
34
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
35
|
+
children = undefined,
|
|
36
|
+
...rest
|
|
37
|
+
} = $props();
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="widget-grid widget-grid-{columns} {className}" {...rest}>
|
|
41
|
+
{#if children}{@render children()}{/if}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.widget-grid {
|
|
46
|
+
display: grid;
|
|
47
|
+
gap: var(--grid-gutter);
|
|
48
|
+
grid-template-columns: 1fr;
|
|
49
|
+
align-items: start;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Keep widgets from blowing out their track. */
|
|
53
|
+
.widget-grid > :global(*) {
|
|
54
|
+
min-width: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ─── Tablet: two columns, span helpers come alive ─── */
|
|
58
|
+
@media (min-width: 768px) {
|
|
59
|
+
.widget-grid-2,
|
|
60
|
+
.widget-grid-3,
|
|
61
|
+
.widget-grid-4 {
|
|
62
|
+
grid-template-columns: repeat(2, 1fr);
|
|
63
|
+
}
|
|
64
|
+
.widget-grid > :global(.widget-span-2) {
|
|
65
|
+
grid-column: span 2;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ─── Desktop: open up to the configured column count ─── */
|
|
70
|
+
@media (min-width: 1024px) {
|
|
71
|
+
.widget-grid-3 {
|
|
72
|
+
grid-template-columns: repeat(3, 1fr);
|
|
73
|
+
}
|
|
74
|
+
.widget-grid-4 {
|
|
75
|
+
grid-template-columns: repeat(4, 1fr);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Full-width span at every breakpoint. */
|
|
80
|
+
.widget-grid > :global(.widget-span-full) {
|
|
81
|
+
grid-column: 1 / -1;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default WidgetGrid;
|
|
2
|
+
type WidgetGrid = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* WidgetGrid
|
|
8
|
+
*
|
|
9
|
+
* Responsive dashboard layout for the citizen portal's `landing` /
|
|
10
|
+
* information-portal pages, where blocks vary in weight — a hero stat, a list of
|
|
11
|
+
* recent consultations, a map, a voting panel. Where CardGrid lays out equal
|
|
12
|
+
* cards, WidgetGrid is a column grid whose children may declare a span, so a
|
|
13
|
+
* feature widget can sit beside two narrow ones.
|
|
14
|
+
*
|
|
15
|
+
* Collapses to one column on small screens (every widget full-width), goes to
|
|
16
|
+
* two at tablet, and to `columns` at desktop. Children opt into width via the
|
|
17
|
+
* span helper classes (applied to the direct child, e.g. a `<Card>` or
|
|
18
|
+
* `<section>`):
|
|
19
|
+
*
|
|
20
|
+
* - `widget-span-2` — span two columns from tablet up
|
|
21
|
+
* - `widget-span-full` — span the full row at every breakpoint
|
|
22
|
+
*
|
|
23
|
+
* Layout only — it sets no landmark and imposes no semantics; the page supplies
|
|
24
|
+
* headings and regions.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <WidgetGrid columns="3">
|
|
28
|
+
* <section class="widget-span-2"><Hero ... /></section>
|
|
29
|
+
* <Card>…</Card>
|
|
30
|
+
* <Card class="widget-span-full">…</Card>
|
|
31
|
+
* </WidgetGrid>
|
|
32
|
+
*/
|
|
33
|
+
declare const WidgetGrid: import("svelte").Component<{
|
|
34
|
+
columns?: string;
|
|
35
|
+
class?: string;
|
|
36
|
+
children?: any;
|
|
37
|
+
} & Record<string, any>, {}, "">;
|
|
38
|
+
type $$ComponentProps = {
|
|
39
|
+
columns?: string;
|
|
40
|
+
class?: string;
|
|
41
|
+
children?: any;
|
|
42
|
+
} & Record<string, any>;
|
package/components/index.js
CHANGED
|
@@ -33,6 +33,19 @@ export { default as List } from "./List.svelte";
|
|
|
33
33
|
export { default as ListItem } from "./ListItem.svelte";
|
|
34
34
|
export { default as PageContainer } from "./PageContainer.svelte";
|
|
35
35
|
|
|
36
|
+
// Public site shell (locked a11y chrome — citizen portal, #7/#71)
|
|
37
|
+
export { default as AppFrame } from "./AppFrame.svelte";
|
|
38
|
+
export { default as SiteHeader } from "./SiteHeader.svelte";
|
|
39
|
+
export { default as SiteFooter } from "./SiteFooter.svelte";
|
|
40
|
+
export { default as SkipLink } from "./SkipLink.svelte";
|
|
41
|
+
export { default as ServiceNavigation } from "./ServiceNavigation.svelte";
|
|
42
|
+
export { default as Link } from "./Link.svelte";
|
|
43
|
+
export { default as Hero } from "./Hero.svelte";
|
|
44
|
+
export { default as ContentBlock } from "./ContentBlock.svelte";
|
|
45
|
+
export { default as StatusTimeline } from "./StatusTimeline.svelte";
|
|
46
|
+
export { default as WidgetGrid } from "./WidgetGrid.svelte";
|
|
47
|
+
export { default as VotingWidget } from "./VotingWidget.svelte";
|
|
48
|
+
|
|
36
49
|
// Containers
|
|
37
50
|
export { default as Card } from "./Card.svelte";
|
|
38
51
|
export { default as Panel } from "./Panel.svelte";
|
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* aiaiai Design System — Tier 1: Tenant Theme — Município de Valongo
|
|
3
|
+
*
|
|
4
|
+
* Example citizen-portal tenant theme (#7 portal template system). Demonstrates
|
|
5
|
+
* the Tier-1 contract: a municipality re-skins brand tokens only; structure,
|
|
6
|
+
* typography, spacing, motion, radius, and a11y stay frozen and
|
|
7
|
+
* conformance-checkable (Designers Italia "Modello Comuni" model).
|
|
8
|
+
*
|
|
9
|
+
* In production these overrides come from the tenant's `portal` ontology row
|
|
10
|
+
* and are injected at runtime by the portal's hooks.server.ts; this static
|
|
11
|
+
* file is the seed/example.
|
|
12
|
+
*
|
|
13
|
+
* Tier-1 rules: override < 20 semantic tokens; change accent + surface tint;
|
|
14
|
+
* keep everything else.
|
|
15
|
+
*
|
|
16
|
+
* Apply via: <html data-theme="valongo">
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
[data-theme="valongo"] {
|
|
20
|
+
/* ─── Accent: Valongo civic green ─── */
|
|
21
|
+
--color-accent: #2e7d32;
|
|
22
|
+
--color-accent-hover: #1b5e20;
|
|
23
|
+
--color-accent-subtle: #eaf4ea;
|
|
24
|
+
|
|
25
|
+
/* ─── Surface: warm neutral ─── */
|
|
26
|
+
--color-surface: #ffffff;
|
|
27
|
+
--color-surface-secondary: #f4f6f3;
|
|
28
|
+
--color-surface-tertiary: #e8ece6;
|
|
29
|
+
|
|
30
|
+
/* ─── Borders ─── */
|
|
31
|
+
--color-border: #d7ded3;
|
|
32
|
+
--color-border-strong: #abb8a4;
|
|
33
|
+
|
|
34
|
+
/* ─── Text ─── */
|
|
35
|
+
--color-text: #1c241b;
|
|
36
|
+
--color-text-secondary: #44513f;
|
|
37
|
+
--color-text-muted: #71806b;
|
|
38
|
+
|
|
39
|
+
/* ─── Overlay + focus follow the brand ─── */
|
|
40
|
+
--color-overlay: rgba(28, 36, 27, 0.5);
|
|
41
|
+
--focus-ring-color: var(--color-accent);
|
|
42
|
+
|
|
43
|
+
/* Total overrides: 14 tokens. Still an aiaiai product, in Valongo's colours. */
|
|
44
|
+
}
|