@aiaiai-pt/design-system 0.22.0 → 0.24.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,249 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component ConsentManager
|
|
3
|
+
|
|
4
|
+
The citizen account area's RGPD panel — consent management + the two data
|
|
5
|
+
rights (portability + erasure). Three parts, all vertical-agnostic:
|
|
6
|
+
|
|
7
|
+
1. A toggle list of CONSENT PURPOSES (`items`, already mapped to
|
|
8
|
+
`{ id, label, description?, active }`). `active` = "I hold a current
|
|
9
|
+
consent for this purpose". Toggling calls `onToggle(id, next)`; the
|
|
10
|
+
consumer grants (next=true) or revokes (next=false) and re-feeds `items`.
|
|
11
|
+
2. EXPORT MY DATA — a button that triggers `onExport` (the consumer
|
|
12
|
+
downloads the citizen's own data, RGPD portability / Anexo II 121).
|
|
13
|
+
3. DELETE MY ACCOUNT — a DESTRUCTIVE, type-to-confirm flow (Anexo II 122):
|
|
14
|
+
the citizen must type `confirmPhrase` before the irreversible
|
|
15
|
+
`onErase` is reachable. No single-click deletion.
|
|
16
|
+
|
|
17
|
+
Accessibility-first: a `<section>` region named by `label` (the visible page
|
|
18
|
+
heading is the consumer's job — same split as NotificationPrefs/RankingBoard,
|
|
19
|
+
so one widget can own the page `<h1>`). The data-rights block is a labelled
|
|
20
|
+
`<h3>` sub-section (the widget heading sits at h2 above it). Toggles are
|
|
21
|
+
`role="switch"` with their own accessible name; the confirm button stays
|
|
22
|
+
`disabled` until the typed phrase matches. Semantic tokens only, so dark /
|
|
23
|
+
high-contrast schemes (#244) ride through. Soft-empty: no purposes → the
|
|
24
|
+
`emptyText` note (the data rights still render).
|
|
25
|
+
|
|
26
|
+
@example
|
|
27
|
+
<ConsentManager
|
|
28
|
+
label="Privacy & consent"
|
|
29
|
+
items={[
|
|
30
|
+
{ id: "campaign_outreach", label: "Campaign updates", description: "Email about participatory budgeting", active: true },
|
|
31
|
+
{ id: "analytics", label: "Usage analytics", active: false },
|
|
32
|
+
]}
|
|
33
|
+
onToggle={(id, next) => save(id, next)}
|
|
34
|
+
busyId={savingId}
|
|
35
|
+
onExport={() => downloadMyData()}
|
|
36
|
+
confirmPhrase="DELETE"
|
|
37
|
+
onErase={() => eraseAccount()}
|
|
38
|
+
/>
|
|
39
|
+
-->
|
|
40
|
+
<script module>
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {{ id: string, label: string, description?: string, active: boolean }} ConsentItem
|
|
43
|
+
*/
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<script>
|
|
47
|
+
import List from "./List.svelte";
|
|
48
|
+
import ListItem from "./ListItem.svelte";
|
|
49
|
+
import Toggle from "./Toggle.svelte";
|
|
50
|
+
import Button from "./Button.svelte";
|
|
51
|
+
import Input from "./Input.svelte";
|
|
52
|
+
|
|
53
|
+
let {
|
|
54
|
+
/** @type {ConsentItem[]} Consent purposes, already mapped to labels. */
|
|
55
|
+
items = [],
|
|
56
|
+
/** @type {string} Accessible name for the consent region (localize it). */
|
|
57
|
+
label = "Consent preferences",
|
|
58
|
+
/** @type {string} Shown when there are no purposes (localize it). */
|
|
59
|
+
emptyText = "There are no consent purposes to manage yet.",
|
|
60
|
+
/** @type {((id: string, active: boolean) => void) | undefined} Toggle a purpose on/off. */
|
|
61
|
+
onToggle = undefined,
|
|
62
|
+
/** @type {string | null} The purpose currently saving — its toggle disables. */
|
|
63
|
+
busyId = null,
|
|
64
|
+
|
|
65
|
+
/** @type {string} Heading for the data-rights sub-section (localize it). */
|
|
66
|
+
dataRightsLabel = "Your data",
|
|
67
|
+
/** @type {string} Label for the export button (localize it). */
|
|
68
|
+
exportLabel = "Export my data",
|
|
69
|
+
/** @type {string} Helper line under the export button (localize it). */
|
|
70
|
+
exportHelp = "Download a copy of your personal data.",
|
|
71
|
+
/** @type {(() => void) | undefined} Called when the citizen exports their data. */
|
|
72
|
+
onExport = undefined,
|
|
73
|
+
/** @type {boolean} The export is in flight. */
|
|
74
|
+
exporting = false,
|
|
75
|
+
|
|
76
|
+
/** @type {string} Label for the destructive trigger button (localize it). */
|
|
77
|
+
eraseLabel = "Delete my account",
|
|
78
|
+
/** @type {string} The irreversibility warning shown when confirming (localize it). */
|
|
79
|
+
erasePrompt = "This permanently deletes your account and personal data and cannot be undone.",
|
|
80
|
+
/** @type {string} The exact phrase the citizen must type to confirm. */
|
|
81
|
+
confirmPhrase = "DELETE",
|
|
82
|
+
/** @type {string} Label for the type-to-confirm input (localize it). */
|
|
83
|
+
confirmInputLabel = "Type the word below to confirm",
|
|
84
|
+
/** @type {string} Label for the final destructive button (localize it). */
|
|
85
|
+
confirmButtonLabel = "Delete my account permanently",
|
|
86
|
+
/** @type {string} Label for the cancel button (localize it). */
|
|
87
|
+
cancelLabel = "Cancel",
|
|
88
|
+
/** @type {(() => void) | undefined} Called once the typed phrase matches. */
|
|
89
|
+
onErase = undefined,
|
|
90
|
+
/** @type {boolean} The erasure request is in flight. */
|
|
91
|
+
erasing = false,
|
|
92
|
+
|
|
93
|
+
/** @type {string} */
|
|
94
|
+
class: className = "",
|
|
95
|
+
...rest
|
|
96
|
+
} = $props();
|
|
97
|
+
|
|
98
|
+
// Local confirm state — the destructive flow stays closed until the citizen
|
|
99
|
+
// opts in, and the irreversible action stays unreachable until they type the
|
|
100
|
+
// exact phrase. The consumer owns the network call (onErase).
|
|
101
|
+
let confirming = $state(false);
|
|
102
|
+
let typed = $state("");
|
|
103
|
+
const canErase = $derived(typed.trim() === confirmPhrase);
|
|
104
|
+
|
|
105
|
+
function startConfirm() {
|
|
106
|
+
confirming = true;
|
|
107
|
+
typed = "";
|
|
108
|
+
}
|
|
109
|
+
function cancelConfirm() {
|
|
110
|
+
confirming = false;
|
|
111
|
+
typed = "";
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<section class="consent-manager {className}" aria-label={label} {...rest}>
|
|
116
|
+
{#if items.length > 0}
|
|
117
|
+
<List variant="bordered">
|
|
118
|
+
{#each items as item (item.id)}
|
|
119
|
+
<ListItem>
|
|
120
|
+
{#snippet leading()}
|
|
121
|
+
<span class="consent-manager-label">{item.label}</span>
|
|
122
|
+
{#if item.description}
|
|
123
|
+
<span class="consent-manager-desc">{item.description}</span>
|
|
124
|
+
{/if}
|
|
125
|
+
{/snippet}
|
|
126
|
+
{#snippet trailing()}
|
|
127
|
+
<Toggle
|
|
128
|
+
checked={item.active}
|
|
129
|
+
disabled={busyId === item.id}
|
|
130
|
+
aria-label={item.label}
|
|
131
|
+
onchange={(next) => onToggle?.(item.id, next)}
|
|
132
|
+
/>
|
|
133
|
+
{/snippet}
|
|
134
|
+
</ListItem>
|
|
135
|
+
{/each}
|
|
136
|
+
</List>
|
|
137
|
+
{:else}
|
|
138
|
+
<p class="consent-manager-empty">{emptyText}</p>
|
|
139
|
+
{/if}
|
|
140
|
+
|
|
141
|
+
<div class="consent-manager-rights">
|
|
142
|
+
<h3 class="consent-manager-rights-heading">{dataRightsLabel}</h3>
|
|
143
|
+
|
|
144
|
+
<div class="consent-manager-action">
|
|
145
|
+
<Button variant="secondary" loading={exporting} onclick={() => onExport?.()}>
|
|
146
|
+
{exportLabel}
|
|
147
|
+
</Button>
|
|
148
|
+
<p class="consent-manager-help">{exportHelp}</p>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="consent-manager-danger">
|
|
152
|
+
{#if !confirming}
|
|
153
|
+
<Button variant="destructive" onclick={startConfirm}>{eraseLabel}</Button>
|
|
154
|
+
{:else}
|
|
155
|
+
<p class="consent-manager-warning" role="alert">{erasePrompt}</p>
|
|
156
|
+
<Input
|
|
157
|
+
label={confirmInputLabel}
|
|
158
|
+
placeholder={confirmPhrase}
|
|
159
|
+
bind:value={typed}
|
|
160
|
+
autocomplete="off"
|
|
161
|
+
/>
|
|
162
|
+
<p class="consent-manager-help">{confirmPhrase}</p>
|
|
163
|
+
<div class="consent-manager-confirm-row">
|
|
164
|
+
<Button
|
|
165
|
+
variant="destructive"
|
|
166
|
+
disabled={!canErase || erasing}
|
|
167
|
+
loading={erasing}
|
|
168
|
+
onclick={() => onErase?.()}
|
|
169
|
+
>
|
|
170
|
+
{confirmButtonLabel}
|
|
171
|
+
</Button>
|
|
172
|
+
<Button variant="ghost" disabled={erasing} onclick={cancelConfirm}>
|
|
173
|
+
{cancelLabel}
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</section>
|
|
180
|
+
|
|
181
|
+
<style>
|
|
182
|
+
.consent-manager {
|
|
183
|
+
display: flex;
|
|
184
|
+
flex-direction: column;
|
|
185
|
+
gap: var(--space-lg);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.consent-manager-label {
|
|
189
|
+
font-family: var(--type-body-font);
|
|
190
|
+
font-size: var(--type-body-size);
|
|
191
|
+
color: var(--color-text);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.consent-manager-desc {
|
|
195
|
+
font-family: var(--type-body-sm-font);
|
|
196
|
+
font-size: var(--type-body-sm-size);
|
|
197
|
+
color: var(--color-text-muted);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.consent-manager-empty {
|
|
201
|
+
font-family: var(--type-body-font);
|
|
202
|
+
font-size: var(--type-body-size);
|
|
203
|
+
color: var(--color-text-muted);
|
|
204
|
+
margin: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.consent-manager-rights {
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
gap: var(--space-md);
|
|
211
|
+
padding-top: var(--space-md);
|
|
212
|
+
border-top: 1px solid var(--color-border);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.consent-manager-rights-heading {
|
|
216
|
+
font-family: var(--type-heading-sm-font);
|
|
217
|
+
font-size: var(--type-heading-sm-size);
|
|
218
|
+
color: var(--color-text);
|
|
219
|
+
margin: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.consent-manager-action,
|
|
223
|
+
.consent-manager-danger {
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: var(--space-xs);
|
|
227
|
+
align-items: flex-start;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.consent-manager-help {
|
|
231
|
+
font-family: var(--type-body-sm-font);
|
|
232
|
+
font-size: var(--type-body-sm-size);
|
|
233
|
+
color: var(--color-text-muted);
|
|
234
|
+
margin: 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.consent-manager-warning {
|
|
238
|
+
font-family: var(--type-body-font);
|
|
239
|
+
font-size: var(--type-body-size);
|
|
240
|
+
color: var(--color-destructive);
|
|
241
|
+
margin: 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.consent-manager-confirm-row {
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-wrap: wrap;
|
|
247
|
+
gap: var(--space-sm);
|
|
248
|
+
}
|
|
249
|
+
</style>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export default ConsentManager;
|
|
2
|
+
export type ConsentItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
active: boolean;
|
|
7
|
+
};
|
|
8
|
+
type ConsentManager = {
|
|
9
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
10
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* ConsentManager
|
|
14
|
+
*
|
|
15
|
+
* The citizen account area's RGPD panel — consent management + the two data
|
|
16
|
+
* rights (portability + erasure). Three parts, all vertical-agnostic:
|
|
17
|
+
*
|
|
18
|
+
* 1. A toggle list of CONSENT PURPOSES (`items`, already mapped to
|
|
19
|
+
* `{ id, label, description?, active }`). `active` = "I hold a current
|
|
20
|
+
* consent for this purpose". Toggling calls `onToggle(id, next)`; the
|
|
21
|
+
* consumer grants (next=true) or revokes (next=false) and re-feeds `items`.
|
|
22
|
+
* 2. EXPORT MY DATA — a button that triggers `onExport` (the consumer
|
|
23
|
+
* downloads the citizen's own data, RGPD portability / Anexo II 121).
|
|
24
|
+
* 3. DELETE MY ACCOUNT — a DESTRUCTIVE, type-to-confirm flow (Anexo II 122):
|
|
25
|
+
* the citizen must type `confirmPhrase` before the irreversible
|
|
26
|
+
* `onErase` is reachable. No single-click deletion.
|
|
27
|
+
*
|
|
28
|
+
* Accessibility-first: a `<section>` region named by `label` (the visible page
|
|
29
|
+
* heading is the consumer's job — same split as NotificationPrefs/RankingBoard,
|
|
30
|
+
* so one widget can own the page `<h1>`). The data-rights block is a labelled
|
|
31
|
+
* `<h3>` sub-section (the widget heading sits at h2 above it). Toggles are
|
|
32
|
+
* `role="switch"` with their own accessible name; the confirm button stays
|
|
33
|
+
* `disabled` until the typed phrase matches. Semantic tokens only, so dark /
|
|
34
|
+
* high-contrast schemes (#244) ride through. Soft-empty: no purposes → the
|
|
35
|
+
* `emptyText` note (the data rights still render).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* <ConsentManager
|
|
39
|
+
* label="Privacy & consent"
|
|
40
|
+
* items={[
|
|
41
|
+
* { id: "campaign_outreach", label: "Campaign updates", description: "Email about participatory budgeting", active: true },
|
|
42
|
+
* { id: "analytics", label: "Usage analytics", active: false },
|
|
43
|
+
* ]}
|
|
44
|
+
* onToggle={(id, next) => save(id, next)}
|
|
45
|
+
* busyId={savingId}
|
|
46
|
+
* onExport={() => downloadMyData()}
|
|
47
|
+
* confirmPhrase="DELETE"
|
|
48
|
+
* onErase={() => eraseAccount()}
|
|
49
|
+
* />
|
|
50
|
+
*/
|
|
51
|
+
declare const ConsentManager: import("svelte").Component<{
|
|
52
|
+
items?: any[];
|
|
53
|
+
label?: string;
|
|
54
|
+
emptyText?: string;
|
|
55
|
+
onToggle?: any;
|
|
56
|
+
busyId?: any;
|
|
57
|
+
dataRightsLabel?: string;
|
|
58
|
+
exportLabel?: string;
|
|
59
|
+
exportHelp?: string;
|
|
60
|
+
onExport?: any;
|
|
61
|
+
exporting?: boolean;
|
|
62
|
+
eraseLabel?: string;
|
|
63
|
+
erasePrompt?: string;
|
|
64
|
+
confirmPhrase?: string;
|
|
65
|
+
confirmInputLabel?: string;
|
|
66
|
+
confirmButtonLabel?: string;
|
|
67
|
+
cancelLabel?: string;
|
|
68
|
+
onErase?: any;
|
|
69
|
+
erasing?: boolean;
|
|
70
|
+
class?: string;
|
|
71
|
+
} & Record<string, any>, {}, "">;
|
|
72
|
+
type $$ComponentProps = {
|
|
73
|
+
items?: any[];
|
|
74
|
+
label?: string;
|
|
75
|
+
emptyText?: string;
|
|
76
|
+
onToggle?: any;
|
|
77
|
+
busyId?: any;
|
|
78
|
+
dataRightsLabel?: string;
|
|
79
|
+
exportLabel?: string;
|
|
80
|
+
exportHelp?: string;
|
|
81
|
+
onExport?: any;
|
|
82
|
+
exporting?: boolean;
|
|
83
|
+
eraseLabel?: string;
|
|
84
|
+
erasePrompt?: string;
|
|
85
|
+
confirmPhrase?: string;
|
|
86
|
+
confirmInputLabel?: string;
|
|
87
|
+
confirmButtonLabel?: string;
|
|
88
|
+
cancelLabel?: string;
|
|
89
|
+
onErase?: any;
|
|
90
|
+
erasing?: boolean;
|
|
91
|
+
class?: string;
|
|
92
|
+
} & Record<string, any>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component NotificationPrefs
|
|
3
|
+
|
|
4
|
+
The citizen account area's notification-preferences panel — a toggle list over
|
|
5
|
+
a citizen's subscriptions. Vertical-agnostic: `items` is already shaped to
|
|
6
|
+
`{ id, label, description?, active }`; the consumer (a portal widget over the
|
|
7
|
+
account subscription lane) maps the backend rows and owns the persistence.
|
|
8
|
+
Toggling a row calls `onToggle(id, next)` — the consumer performs the write
|
|
9
|
+
and re-feeds `items` (or sets `busyId` while it's in flight).
|
|
10
|
+
|
|
11
|
+
Accessibility-first: a bordered semantic list inside a `<section>` region
|
|
12
|
+
named by `label` (the visible page heading is the consumer's job — same split
|
|
13
|
+
as RankingBoard, so one widget can own the page `<h1>`). Each row pairs a real
|
|
14
|
+
text label (and optional description) with a `Toggle` (`role="switch"`,
|
|
15
|
+
`aria-checked`); the switch carries its own accessible name via `aria-label`.
|
|
16
|
+
A row that's saving is `disabled` (no double-submit). Consumes semantic tokens
|
|
17
|
+
so dark / high-contrast schemes (#244) ride through. Soft-empty: no items →
|
|
18
|
+
renders the `emptyText` note (never an empty shell).
|
|
19
|
+
|
|
20
|
+
@example
|
|
21
|
+
<NotificationPrefs
|
|
22
|
+
label="Notifications"
|
|
23
|
+
items={[
|
|
24
|
+
{ id: "all", label: "All updates", description: "Every proposal and phase change", active: true },
|
|
25
|
+
{ id: "sub-2", label: "Cycling lane", active: false },
|
|
26
|
+
]}
|
|
27
|
+
onToggle={(id, next) => save(id, next)}
|
|
28
|
+
busyId={savingId}
|
|
29
|
+
/>
|
|
30
|
+
-->
|
|
31
|
+
<script module>
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {{ id: string, label: string, description?: string, active: boolean }} PrefItem
|
|
34
|
+
*/
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
import List from "./List.svelte";
|
|
39
|
+
import ListItem from "./ListItem.svelte";
|
|
40
|
+
import Toggle from "./Toggle.svelte";
|
|
41
|
+
|
|
42
|
+
let {
|
|
43
|
+
/** @type {PrefItem[]} The subscription rows, already mapped to labels. */
|
|
44
|
+
items = [],
|
|
45
|
+
/** @type {string} Accessible name for the preferences region (localize it). */
|
|
46
|
+
label = "Notification preferences",
|
|
47
|
+
/** @type {string} Shown when there are no items (localize it). */
|
|
48
|
+
emptyText = "You have no notification preferences yet.",
|
|
49
|
+
/** @type {((id: string, active: boolean) => void) | undefined} Called on toggle. */
|
|
50
|
+
onToggle = undefined,
|
|
51
|
+
/** @type {string | null} The row currently saving — its toggle disables. */
|
|
52
|
+
busyId = null,
|
|
53
|
+
/** @type {string} */
|
|
54
|
+
class: className = "",
|
|
55
|
+
...rest
|
|
56
|
+
} = $props();
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<section class="notification-prefs {className}" aria-label={label} {...rest}>
|
|
60
|
+
{#if items.length > 0}
|
|
61
|
+
<List variant="bordered">
|
|
62
|
+
{#each items as item (item.id)}
|
|
63
|
+
<ListItem>
|
|
64
|
+
{#snippet leading()}
|
|
65
|
+
<span class="notification-prefs-label">{item.label}</span>
|
|
66
|
+
{#if item.description}
|
|
67
|
+
<span class="notification-prefs-desc">{item.description}</span>
|
|
68
|
+
{/if}
|
|
69
|
+
{/snippet}
|
|
70
|
+
{#snippet trailing()}
|
|
71
|
+
<Toggle
|
|
72
|
+
checked={item.active}
|
|
73
|
+
disabled={busyId === item.id}
|
|
74
|
+
aria-label={item.label}
|
|
75
|
+
onchange={(next) => onToggle?.(item.id, next)}
|
|
76
|
+
/>
|
|
77
|
+
{/snippet}
|
|
78
|
+
</ListItem>
|
|
79
|
+
{/each}
|
|
80
|
+
</List>
|
|
81
|
+
{:else}
|
|
82
|
+
<p class="notification-prefs-empty">{emptyText}</p>
|
|
83
|
+
{/if}
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<style>
|
|
87
|
+
.notification-prefs {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: var(--space-md);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.notification-prefs-label {
|
|
94
|
+
font-family: var(--type-body-font);
|
|
95
|
+
font-size: var(--type-body-size);
|
|
96
|
+
color: var(--color-text);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.notification-prefs-desc {
|
|
100
|
+
font-family: var(--type-body-sm-font);
|
|
101
|
+
font-size: var(--type-body-sm-size);
|
|
102
|
+
color: var(--color-text-muted);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.notification-prefs-empty {
|
|
106
|
+
font-family: var(--type-body-font);
|
|
107
|
+
font-size: var(--type-body-size);
|
|
108
|
+
color: var(--color-text-muted);
|
|
109
|
+
margin: 0;
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export default NotificationPrefs;
|
|
2
|
+
export type PrefItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
active: boolean;
|
|
7
|
+
};
|
|
8
|
+
type NotificationPrefs = {
|
|
9
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
10
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* NotificationPrefs
|
|
14
|
+
*
|
|
15
|
+
* The citizen account area's notification-preferences panel — a toggle list over
|
|
16
|
+
* a citizen's subscriptions. Vertical-agnostic: `items` is already shaped to
|
|
17
|
+
* `{ id, label, description?, active }`; the consumer (a portal widget over the
|
|
18
|
+
* account subscription lane) maps the backend rows and owns the persistence.
|
|
19
|
+
* Toggling a row calls `onToggle(id, next)` — the consumer performs the write
|
|
20
|
+
* and re-feeds `items` (or sets `busyId` while it's in flight).
|
|
21
|
+
*
|
|
22
|
+
* Accessibility-first: a bordered semantic list; each row pairs a real text
|
|
23
|
+
* label (and optional description) with a `Toggle` (`role="switch"`,
|
|
24
|
+
* `aria-checked`). The switch carries an accessible name via `aria-label` so the
|
|
25
|
+
* control reads on its own. A row that's saving is `disabled` (no double-submit).
|
|
26
|
+
* Consumes semantic tokens so dark / high-contrast schemes (#244) ride through.
|
|
27
|
+
* Soft-empty: no items → renders the `emptyText` note (never an empty shell).
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* <NotificationPrefs
|
|
31
|
+
* heading="Notifications"
|
|
32
|
+
* items={[
|
|
33
|
+
* { id: "all", label: "All updates", description: "Every proposal and phase change", active: true },
|
|
34
|
+
* { id: "sub-2", label: "Cycling lane", active: false },
|
|
35
|
+
* ]}
|
|
36
|
+
* onToggle={(id, next) => save(id, next)}
|
|
37
|
+
* busyId={savingId}
|
|
38
|
+
* />
|
|
39
|
+
*/
|
|
40
|
+
declare const NotificationPrefs: import("svelte").Component<{
|
|
41
|
+
items?: any[];
|
|
42
|
+
heading?: string;
|
|
43
|
+
emptyText?: string;
|
|
44
|
+
onToggle?: any;
|
|
45
|
+
busyId?: any;
|
|
46
|
+
class?: string;
|
|
47
|
+
} & Record<string, any>, {}, "">;
|
|
48
|
+
type $$ComponentProps = {
|
|
49
|
+
items?: any[];
|
|
50
|
+
heading?: string;
|
|
51
|
+
emptyText?: string;
|
|
52
|
+
onToggle?: any;
|
|
53
|
+
busyId?: any;
|
|
54
|
+
class?: string;
|
|
55
|
+
} & Record<string, any>;
|
package/components/index.js
CHANGED
|
@@ -90,6 +90,13 @@ export { default as PhaseTimeline } from "./PhaseTimeline.svelte";
|
|
|
90
90
|
export { default as RankingBoard } from "./RankingBoard.svelte";
|
|
91
91
|
export { default as ResultsChart } from "./ResultsChart.svelte";
|
|
92
92
|
|
|
93
|
+
// Account / preferences (#105 Phase 5) — the citizen account area's
|
|
94
|
+
// notification-preferences panel: a toggle list over a citizen's subscriptions.
|
|
95
|
+
export { default as NotificationPrefs } from "./NotificationPrefs.svelte";
|
|
96
|
+
// Account / RGPD (#105 Phase 5 slice 2) — consent purposes toggle list +
|
|
97
|
+
// data portability (export) + type-to-confirm account erasure.
|
|
98
|
+
export { default as ConsentManager } from "./ConsentManager.svelte";
|
|
99
|
+
|
|
93
100
|
// Feedback
|
|
94
101
|
export { default as Alert } from "./Alert.svelte";
|
|
95
102
|
export { default as Toast } from "./Toast.svelte";
|