@aiaiai-pt/design-system 0.20.0 → 0.22.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,220 @@
1
+ <!--
2
+ @component PhaseTimeline
3
+
4
+ Scheduled-phase timeline for the citizen portal — the spine of a phase-driven
5
+ process (participatory budgeting: submission → analysis → voting → execution;
6
+ but equally an SLA cycle, a maintenance plan, or an energy-tariff window).
7
+
8
+ Distinct from StatusTimeline: StatusTimeline tracks a single item's PROGRESS
9
+ (submitted → resolved). PhaseTimeline shows a SCHEDULE — each phase carries a
10
+ date window and a derived state (closed / open / upcoming), and one phase is
11
+ marked the authoritative current phase. The state is computed by the caller
12
+ from the window dates against the request clock so the widget itself is
13
+ clock-free (SSR-stable, reduced-motion-safe — no animation).
14
+
15
+ Renders an `<ol>` so order and position are structural; the current phase
16
+ carries `aria-current="step"`, and each phase's state is announced via a
17
+ visually-hidden label (pass `stateLabels` to localise — the portal feeds
18
+ Wuchale strings here).
19
+
20
+ @example
21
+ <PhaseTimeline
22
+ label="Participatory budgeting calendar"
23
+ phases={[
24
+ { label: "Proposals", status: "closed", window: "1–31 Mar" },
25
+ { label: "Analysis", status: "closed", window: "1–15 Apr" },
26
+ { label: "Voting", status: "open", current: true, window: "16–30 Apr", description: "Vote now" },
27
+ { label: "Execution", status: "upcoming", window: "from 1 Jun" },
28
+ ]}
29
+ />
30
+
31
+ @example Localised state labels (portal i18n)
32
+ <PhaseTimeline phases={phases} stateLabels={{ closed: "Encerrada", open: "A decorrer", upcoming: "Brevemente" }} openBadge="A decorrer" />
33
+ -->
34
+ <script>
35
+ /**
36
+ * @typedef {'closed' | 'open' | 'upcoming'} PhaseStatus
37
+ * @typedef {{ label: string, status?: PhaseStatus, window?: string, current?: boolean, description?: string }} Phase
38
+ */
39
+
40
+ let {
41
+ /** @type {Phase[]} Ordered phases, earliest first. */
42
+ phases = [],
43
+ /** @type {string} Accessible name for the timeline list (localize it). */
44
+ label = "Phases",
45
+ /**
46
+ * @type {Record<PhaseStatus, string>}
47
+ * Visually-hidden state text announced per phase (localize it).
48
+ */
49
+ stateLabels = {
50
+ closed: "Closed",
51
+ open: "Open now",
52
+ upcoming: "Upcoming",
53
+ },
54
+ /** @type {string} Visible badge text on the open phase (localize it). */
55
+ openBadge = "Open now",
56
+ /** @type {string} */
57
+ class: className = "",
58
+ ...rest
59
+ } = $props();
60
+
61
+ /** @param {Phase} phase @returns {PhaseStatus} */
62
+ const statusOf = (phase) => phase.status ?? "upcoming";
63
+ </script>
64
+
65
+ {#if phases.length > 0}
66
+ <ol class="phase-timeline {className}" aria-label={label} {...rest}>
67
+ {#each phases as phase, i (i)}
68
+ {@const status = statusOf(phase)}
69
+ <li
70
+ class="phase-timeline-item phase-timeline-item-{status}"
71
+ class:phase-timeline-item-current={phase.current}
72
+ aria-current={phase.current ? "step" : undefined}
73
+ >
74
+ <span class="phase-timeline-marker" aria-hidden="true">
75
+ <span class="phase-timeline-node"></span>
76
+ </span>
77
+ <div class="phase-timeline-body">
78
+ <span class="phase-timeline-label">
79
+ <span class="phase-timeline-state">{stateLabels[status]}: </span>
80
+ {phase.label}
81
+ {#if status === "open"}
82
+ <span class="phase-timeline-badge">{openBadge}</span>
83
+ {/if}
84
+ </span>
85
+ {#if phase.window}
86
+ <span class="phase-timeline-window">{phase.window}</span>
87
+ {/if}
88
+ {#if phase.description}
89
+ <p class="phase-timeline-desc">{phase.description}</p>
90
+ {/if}
91
+ </div>
92
+ </li>
93
+ {/each}
94
+ </ol>
95
+ {/if}
96
+
97
+ <style>
98
+ .phase-timeline {
99
+ list-style: none;
100
+ margin: 0;
101
+ padding: 0;
102
+ display: flex;
103
+ flex-direction: column;
104
+ }
105
+
106
+ .phase-timeline-item {
107
+ position: relative;
108
+ display: grid;
109
+ grid-template-columns: auto 1fr;
110
+ gap: var(--space-md);
111
+ padding-bottom: var(--space-lg);
112
+ }
113
+ .phase-timeline-item:last-child {
114
+ padding-bottom: 0;
115
+ }
116
+
117
+ /* Connector line down the marker column, behind the nodes; stops at the last
118
+ node so it never dangles past the final phase. */
119
+ .phase-timeline-marker {
120
+ position: relative;
121
+ display: flex;
122
+ justify-content: center;
123
+ width: var(--space-md);
124
+ }
125
+ .phase-timeline-item:not(:last-child) .phase-timeline-marker::before {
126
+ content: "";
127
+ position: absolute;
128
+ top: var(--space-md);
129
+ bottom: calc(-1 * var(--space-lg));
130
+ inset-inline-start: 50%;
131
+ transform: translateX(-50%);
132
+ width: var(--border-width-thick);
133
+ background: var(--color-border);
134
+ }
135
+ /* A closed (past) phase's connector is filled — the schedule has advanced. */
136
+ .phase-timeline-item-closed:not(:last-child) .phase-timeline-marker::before {
137
+ background: var(--color-accent);
138
+ }
139
+
140
+ .phase-timeline-node {
141
+ position: relative;
142
+ z-index: 1;
143
+ width: var(--space-md);
144
+ height: var(--space-md);
145
+ border-radius: var(--radius-circle);
146
+ background: var(--color-surface);
147
+ box-shadow: inset 0 0 0 var(--border-width-thick) var(--color-border);
148
+ }
149
+ .phase-timeline-item-closed .phase-timeline-node {
150
+ background: var(--color-accent);
151
+ box-shadow: inset 0 0 0 var(--border-width-thick) var(--color-accent);
152
+ }
153
+ .phase-timeline-item-open .phase-timeline-node {
154
+ background: var(--color-surface);
155
+ box-shadow:
156
+ inset 0 0 0 var(--border-width-thick) var(--color-accent),
157
+ 0 0 0 var(--border-width-thick) var(--color-accent-subtle);
158
+ }
159
+
160
+ .phase-timeline-body {
161
+ padding-top: calc((var(--space-md) - 1em) / 2);
162
+ min-width: 0;
163
+ }
164
+
165
+ .phase-timeline-label {
166
+ font-family: var(--type-label-font);
167
+ font-size: var(--type-label-size);
168
+ color: var(--color-text-secondary);
169
+ }
170
+ .phase-timeline-item-open .phase-timeline-label,
171
+ .phase-timeline-item-closed .phase-timeline-label {
172
+ color: var(--color-text);
173
+ }
174
+ .phase-timeline-item-current .phase-timeline-label {
175
+ font-weight: var(--raw-font-weight-semibold);
176
+ color: var(--color-text);
177
+ }
178
+
179
+ /* Sighted affordance that the open phase is actionable now. */
180
+ .phase-timeline-badge {
181
+ display: inline-block;
182
+ margin-inline-start: var(--space-2xs);
183
+ padding: var(--space-3xs, 0.125rem) var(--space-2xs);
184
+ border-radius: var(--radius-sm);
185
+ background: var(--color-accent-subtle);
186
+ color: var(--color-accent);
187
+ font-family: var(--type-caption-font);
188
+ font-size: var(--type-caption-size);
189
+ font-weight: var(--raw-font-weight-semibold);
190
+ vertical-align: middle;
191
+ }
192
+
193
+ /* State text is for assistive tech only — the node + badge carry it sighted. */
194
+ .phase-timeline-state {
195
+ position: absolute;
196
+ width: 1px;
197
+ height: 1px;
198
+ margin: -1px;
199
+ padding: 0;
200
+ overflow: hidden;
201
+ clip: rect(0, 0, 0, 0);
202
+ white-space: nowrap;
203
+ border: 0;
204
+ }
205
+
206
+ .phase-timeline-window {
207
+ display: block;
208
+ margin-top: var(--space-2xs);
209
+ font-family: var(--type-caption-font);
210
+ font-size: var(--type-caption-size);
211
+ color: var(--color-text-muted);
212
+ }
213
+
214
+ .phase-timeline-desc {
215
+ margin: var(--space-2xs) 0 0;
216
+ font-family: var(--type-body-sm-font);
217
+ font-size: var(--type-body-sm-size);
218
+ color: var(--color-text-secondary);
219
+ }
220
+ </style>
@@ -0,0 +1,52 @@
1
+ export default PhaseTimeline;
2
+ type PhaseTimeline = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * PhaseTimeline
8
+ *
9
+ * Scheduled-phase timeline for the citizen portal — the spine of a phase-driven
10
+ * process (participatory budgeting: submission → analysis → voting → execution;
11
+ * but equally an SLA cycle, a maintenance plan, or an energy-tariff window).
12
+ *
13
+ * Distinct from StatusTimeline: StatusTimeline tracks a single item's PROGRESS
14
+ * (submitted → resolved). PhaseTimeline shows a SCHEDULE — each phase carries a
15
+ * date window and a derived state (closed / open / upcoming), and one phase is
16
+ * marked the authoritative current phase. The state is computed by the caller
17
+ * from the window dates against the request clock so the widget itself is
18
+ * clock-free (SSR-stable, reduced-motion-safe — no animation).
19
+ *
20
+ * Renders an `<ol>` so order and position are structural; the current phase
21
+ * carries `aria-current="step"`, and each phase's state is announced via a
22
+ * visually-hidden label (pass `stateLabels` to localise — the portal feeds
23
+ * Wuchale strings here).
24
+ *
25
+ * @example
26
+ * <PhaseTimeline
27
+ * label="Participatory budgeting calendar"
28
+ * phases={[
29
+ * { label: "Proposals", status: "closed", window: "1–31 Mar" },
30
+ * { label: "Analysis", status: "closed", window: "1–15 Apr" },
31
+ * { label: "Voting", status: "open", current: true, window: "16–30 Apr", description: "Vote now" },
32
+ * { label: "Execution", status: "upcoming", window: "from 1 Jun" },
33
+ * ]}
34
+ * />
35
+ *
36
+ * @example Localised state labels (portal i18n)
37
+ * <PhaseTimeline phases={phases} stateLabels={{ closed: "Encerrada", open: "A decorrer", upcoming: "Brevemente" }} openBadge="A decorrer" />
38
+ */
39
+ declare const PhaseTimeline: import("svelte").Component<{
40
+ phases?: any[];
41
+ label?: string;
42
+ stateLabels?: Record<string, any>;
43
+ openBadge?: string;
44
+ class?: string;
45
+ } & Record<string, any>, {}, "">;
46
+ type $$ComponentProps = {
47
+ phases?: any[];
48
+ label?: string;
49
+ stateLabels?: Record<string, any>;
50
+ openBadge?: string;
51
+ class?: string;
52
+ } & Record<string, any>;
@@ -0,0 +1,159 @@
1
+ <!--
2
+ @component RankingBoard
3
+
4
+ An ordered leaderboard of entities ranked by a single aggregate value — the
5
+ citizen portal's "results" surface (winning participatory-budget proposals by
6
+ votes, most-reported categories, top-consuming meters). Vertical-agnostic:
7
+ `items` is already shaped to `{ label, value }`; the consumer (a portal widget
8
+ over a public aggregate VIEW) does the field mapping and sorting.
9
+
10
+ Accessibility-first: renders a semantic `<ol>` so rank + position are conveyed
11
+ structurally (not just visually), with a visible rank badge and a proportional
12
+ bar (decorative, `aria-hidden`) for at-a-glance weight. The value is real text,
13
+ never encoded only in the bar width — so the ranking reads identically with CSS
14
+ off or to a screen reader. Consumes semantic tokens so dark / high-contrast
15
+ schemes (#244) ride through. Soft-empty: no items → renders nothing.
16
+
17
+ @example
18
+ <RankingBoard
19
+ label="Winning proposals by votes"
20
+ items={[
21
+ { label: "Ciclovia da Marginal", value: 1284 },
22
+ { label: "Parque infantil de Gaia", value: 967 },
23
+ { label: "Iluminação LED do centro", value: 612 },
24
+ ]}
25
+ valueSuffix=" votos"
26
+ locale="pt"
27
+ />
28
+ -->
29
+ <script module>
30
+ /**
31
+ * @typedef {{ label: string, value: number }} RankingItem
32
+ */
33
+ </script>
34
+
35
+ <script>
36
+ let {
37
+ /** @type {RankingItem[]} Pre-sorted (desc) items; index 0 is rank 1. */
38
+ items = [],
39
+ /** @type {string} Accessible name for the ranked list (localize it). */
40
+ label = "Ranking",
41
+ /** @type {string} Appended after each value (e.g. " votos"); localize it. */
42
+ valueSuffix = "",
43
+ /** @type {string} BCP-47 locale for number formatting. */
44
+ locale = "en",
45
+ /** @type {string} */
46
+ class: className = "",
47
+ ...rest
48
+ } = $props();
49
+
50
+ const max = $derived(
51
+ items.reduce((m, it) => (it.value > m ? it.value : m), 0),
52
+ );
53
+
54
+ /** @param {number} value @returns {string} */
55
+ function fmt(value) {
56
+ return new Intl.NumberFormat(locale).format(value);
57
+ }
58
+
59
+ /** Bar width as an integer percent of the max (0 when max is 0).
60
+ * @param {number} value @returns {number} */
61
+ function pct(value) {
62
+ return max > 0 ? Math.round((value / max) * 100) : 0;
63
+ }
64
+ </script>
65
+
66
+ {#if items.length > 0}
67
+ <ol class="ranking-board {className}" aria-label={label} {...rest}>
68
+ {#each items as item, i (item.label + i)}
69
+ <li class="ranking-row">
70
+ <span class="ranking-rank" aria-hidden="true">{i + 1}</span>
71
+ <span class="ranking-body">
72
+ <span class="ranking-label">{item.label}</span>
73
+ <span class="ranking-bar" aria-hidden="true">
74
+ <span class="ranking-bar-fill" style="inline-size: {pct(item.value)}%"
75
+ ></span>
76
+ </span>
77
+ </span>
78
+ <span class="ranking-value">{fmt(item.value)}{valueSuffix}</span>
79
+ </li>
80
+ {/each}
81
+ </ol>
82
+ {/if}
83
+
84
+ <style>
85
+ .ranking-board {
86
+ list-style: none;
87
+ margin: 0;
88
+ padding: 0;
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: var(--space-xs);
92
+ }
93
+
94
+ .ranking-row {
95
+ display: grid;
96
+ grid-template-columns: auto 1fr auto;
97
+ align-items: center;
98
+ gap: var(--space-sm);
99
+ }
100
+
101
+ .ranking-rank {
102
+ inline-size: 1.75rem;
103
+ block-size: 1.75rem;
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ border-radius: var(--radius-full, 999px);
108
+ background: transparent;
109
+ border: 1px solid var(--color-border);
110
+ color: var(--color-text-secondary);
111
+ font-size: var(--type-body-sm-size);
112
+ font-weight: 600;
113
+ font-variant-numeric: tabular-nums;
114
+ }
115
+
116
+ /* The top three carry the accent fill so the podium reads at a glance —
117
+ redundant with the ordinal position (never the only signal). */
118
+ .ranking-row:nth-child(-n + 3) .ranking-rank {
119
+ background: var(--color-accent);
120
+ border-color: var(--color-accent);
121
+ color: var(--color-text-on-accent);
122
+ }
123
+
124
+ .ranking-body {
125
+ min-inline-size: 0;
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: var(--space-3xs, 2px);
129
+ }
130
+
131
+ .ranking-label {
132
+ overflow: hidden;
133
+ text-overflow: ellipsis;
134
+ white-space: nowrap;
135
+ color: var(--color-text-primary);
136
+ }
137
+
138
+ .ranking-bar {
139
+ block-size: var(--space-2xs, 6px);
140
+ inline-size: 100%;
141
+ border-radius: var(--radius-full, 999px);
142
+ background: var(--color-border);
143
+ overflow: hidden;
144
+ }
145
+
146
+ .ranking-bar-fill {
147
+ display: block;
148
+ block-size: 100%;
149
+ background: var(--color-accent);
150
+ border-radius: inherit;
151
+ }
152
+
153
+ .ranking-value {
154
+ font-variant-numeric: tabular-nums;
155
+ font-weight: 600;
156
+ color: var(--color-text-primary);
157
+ white-space: nowrap;
158
+ }
159
+ </style>
@@ -0,0 +1,51 @@
1
+ export default RankingBoard;
2
+ export type RankingItem = {
3
+ label: string;
4
+ value: number;
5
+ };
6
+ type RankingBoard = {
7
+ $on?(type: string, callback: (e: any) => void): () => void;
8
+ $set?(props: Partial<$$ComponentProps>): void;
9
+ };
10
+ /**
11
+ * RankingBoard
12
+ *
13
+ * An ordered leaderboard of entities ranked by a single aggregate value — the
14
+ * citizen portal's "results" surface (winning participatory-budget proposals by
15
+ * votes, most-reported categories, top-consuming meters). Vertical-agnostic:
16
+ * `items` is already shaped to `{ label, value }`; the consumer (a portal widget
17
+ * over a public aggregate VIEW) does the field mapping and sorting.
18
+ *
19
+ * Accessibility-first: renders a semantic `<ol>` so rank + position are conveyed
20
+ * structurally (not just visually), with a visible rank badge and a proportional
21
+ * bar (decorative, `aria-hidden`) for at-a-glance weight. The value is real text,
22
+ * never encoded only in the bar width — so the ranking reads identically with CSS
23
+ * off or to a screen reader. Consumes semantic tokens so dark / high-contrast
24
+ * schemes (#244) ride through. Soft-empty: no items → renders nothing.
25
+ *
26
+ * @example
27
+ * <RankingBoard
28
+ * label="Winning proposals by votes"
29
+ * items={[
30
+ * { label: "Ciclovia da Marginal", value: 1284 },
31
+ * { label: "Parque infantil de Gaia", value: 967 },
32
+ * { label: "Iluminação LED do centro", value: 612 },
33
+ * ]}
34
+ * valueSuffix=" votos"
35
+ * locale="pt"
36
+ * />
37
+ */
38
+ declare const RankingBoard: import("svelte").Component<{
39
+ items?: any[];
40
+ label?: string;
41
+ valueSuffix?: string;
42
+ locale?: string;
43
+ class?: string;
44
+ } & Record<string, any>, {}, "">;
45
+ type $$ComponentProps = {
46
+ items?: any[];
47
+ label?: string;
48
+ valueSuffix?: string;
49
+ locale?: string;
50
+ class?: string;
51
+ } & Record<string, any>;
@@ -0,0 +1,185 @@
1
+ <!--
2
+ @component ResultsChart
3
+
4
+ A horizontal bar chart for a single categorical aggregate (votes per proposal,
5
+ reports per category, consumption per meter) that SHIPS ITS DATA TABLE. The
6
+ visual bars are decorative (`aria-hidden`); the accompanying `<table>` is the
7
+ accessible source of truth, always rendered — so the data is reachable with CSS
8
+ off, at any zoom, and to assistive tech. This is a hard requirement (WCAG /
9
+ ARTE: a chart must not be the only encoding of its data), not a toggle.
10
+
11
+ Vertical-agnostic: `items` is already shaped to `{ label, value }`; the consumer
12
+ (a portal widget over a public aggregate VIEW) maps the view columns and orders
13
+ the rows. Dependency-free (pure CSS bars) so it stays a11y- and SSR-clean.
14
+ Consumes semantic tokens so dark / high-contrast schemes (#244) ride through.
15
+ Soft-empty: no items → renders nothing.
16
+
17
+ @example
18
+ <ResultsChart
19
+ caption="Votes per proposal"
20
+ labelHeader="Proposal"
21
+ valueHeader="Votes"
22
+ items={[
23
+ { label: "Ciclovia da Marginal", value: 1284 },
24
+ { label: "Parque infantil de Gaia", value: 967 },
25
+ ]}
26
+ locale="pt"
27
+ />
28
+ -->
29
+ <script module>
30
+ /**
31
+ * @typedef {{ label: string, value: number }} ChartItem
32
+ */
33
+ </script>
34
+
35
+ <script>
36
+ let {
37
+ /** @type {ChartItem[]} The categorical rows to plot. */
38
+ items = [],
39
+ /** @type {string} Accessible caption / heading for the chart + table. */
40
+ caption = "Results",
41
+ /** @type {string} Column header for the category (localize it). */
42
+ labelHeader = "Item",
43
+ /** @type {string} Column header for the value (localize it). */
44
+ valueHeader = "Value",
45
+ /** @type {string} BCP-47 locale for number formatting. */
46
+ locale = "en",
47
+ /** @type {string} */
48
+ class: className = "",
49
+ ...rest
50
+ } = $props();
51
+
52
+ const max = $derived(
53
+ items.reduce((m, it) => (it.value > m ? it.value : m), 0),
54
+ );
55
+
56
+ /** @param {number} value @returns {string} */
57
+ function fmt(value) {
58
+ return new Intl.NumberFormat(locale).format(value);
59
+ }
60
+
61
+ /** @param {number} value @returns {number} */
62
+ function pct(value) {
63
+ return max > 0 ? Math.round((value / max) * 100) : 0;
64
+ }
65
+ </script>
66
+
67
+ {#if items.length > 0}
68
+ <figure class="results-chart {className}" aria-label={caption} {...rest}>
69
+ <!-- Decorative visual: the data lives in the table below. -->
70
+ <div class="results-chart-bars" aria-hidden="true">
71
+ {#each items as item, i (item.label + i)}
72
+ <div class="results-bar-row">
73
+ <span class="results-bar-label">{item.label}</span>
74
+ <span class="results-bar-track">
75
+ <span
76
+ class="results-bar-fill"
77
+ style="inline-size: {pct(item.value)}%"
78
+ ></span>
79
+ </span>
80
+ <span class="results-bar-value">{fmt(item.value)}</span>
81
+ </div>
82
+ {/each}
83
+ </div>
84
+
85
+ <!-- Accessible source of truth — always rendered (ARTE #3/#6). Named via
86
+ aria-label, not a <caption>: position:absolute/clip is unreliable on a
87
+ display:table-caption element (it leaks visibly in some engines). -->
88
+ <table class="results-chart-table" aria-label={caption}>
89
+ <thead>
90
+ <tr>
91
+ <th scope="col">{labelHeader}</th>
92
+ <th scope="col">{valueHeader}</th>
93
+ </tr>
94
+ </thead>
95
+ <tbody>
96
+ {#each items as item, i (item.label + i)}
97
+ <tr>
98
+ <th scope="row">{item.label}</th>
99
+ <td>{fmt(item.value)}</td>
100
+ </tr>
101
+ {/each}
102
+ </tbody>
103
+ </table>
104
+ </figure>
105
+ {/if}
106
+
107
+ <style>
108
+ .results-chart {
109
+ margin: 0;
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: var(--space-md);
113
+ }
114
+
115
+ .results-chart-bars {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: var(--space-sm);
119
+ }
120
+
121
+ .results-bar-row {
122
+ display: grid;
123
+ grid-template-columns: minmax(6rem, 14rem) 1fr auto;
124
+ align-items: center;
125
+ gap: var(--space-sm);
126
+ }
127
+
128
+ .results-bar-label {
129
+ overflow: hidden;
130
+ text-overflow: ellipsis;
131
+ white-space: nowrap;
132
+ color: var(--color-text-primary);
133
+ }
134
+
135
+ .results-bar-track {
136
+ block-size: var(--space-sm, 12px);
137
+ inline-size: 100%;
138
+ background: var(--color-border);
139
+ border-radius: var(--radius-full, 999px);
140
+ overflow: hidden;
141
+ }
142
+
143
+ .results-bar-fill {
144
+ display: block;
145
+ block-size: 100%;
146
+ background: var(--color-accent);
147
+ border-radius: inherit;
148
+ }
149
+
150
+ .results-bar-value {
151
+ font-variant-numeric: tabular-nums;
152
+ font-weight: 600;
153
+ color: var(--color-text-primary);
154
+ white-space: nowrap;
155
+ }
156
+
157
+ .results-chart-table {
158
+ inline-size: 100%;
159
+ border-collapse: collapse;
160
+ font-size: var(--type-body-sm-size);
161
+ }
162
+
163
+ .results-chart-table th,
164
+ .results-chart-table td {
165
+ text-align: start;
166
+ padding: var(--space-2xs) var(--space-xs);
167
+ border-block-end: 1px solid var(--color-border);
168
+ }
169
+
170
+ .results-chart-table thead th {
171
+ color: var(--color-text-secondary);
172
+ font-weight: 600;
173
+ }
174
+
175
+ .results-chart-table tbody th {
176
+ font-weight: 400;
177
+ color: var(--color-text-primary);
178
+ }
179
+
180
+ .results-chart-table td {
181
+ text-align: end;
182
+ font-variant-numeric: tabular-nums;
183
+ color: var(--color-text-primary);
184
+ }
185
+ </style>
@@ -0,0 +1,53 @@
1
+ export default ResultsChart;
2
+ export type ChartItem = {
3
+ label: string;
4
+ value: number;
5
+ };
6
+ type ResultsChart = {
7
+ $on?(type: string, callback: (e: any) => void): () => void;
8
+ $set?(props: Partial<$$ComponentProps>): void;
9
+ };
10
+ /**
11
+ * ResultsChart
12
+ *
13
+ * A horizontal bar chart for a single categorical aggregate (votes per proposal,
14
+ * reports per category, consumption per meter) that SHIPS ITS DATA TABLE. The
15
+ * visual bars are decorative (`aria-hidden`); the accompanying `<table>` is the
16
+ * accessible source of truth, always rendered — so the data is reachable with CSS
17
+ * off, at any zoom, and to assistive tech. This is a hard requirement (WCAG /
18
+ * ARTE: a chart must not be the only encoding of its data), not a toggle.
19
+ *
20
+ * Vertical-agnostic: `items` is already shaped to `{ label, value }`; the consumer
21
+ * (a portal widget over a public aggregate VIEW) maps the view columns and orders
22
+ * the rows. Dependency-free (pure CSS bars) so it stays a11y- and SSR-clean.
23
+ * Consumes semantic tokens so dark / high-contrast schemes (#244) ride through.
24
+ * Soft-empty: no items → renders nothing.
25
+ *
26
+ * @example
27
+ * <ResultsChart
28
+ * caption="Votes per proposal"
29
+ * labelHeader="Proposal"
30
+ * valueHeader="Votes"
31
+ * items={[
32
+ * { label: "Ciclovia da Marginal", value: 1284 },
33
+ * { label: "Parque infantil de Gaia", value: 967 },
34
+ * ]}
35
+ * locale="pt"
36
+ * />
37
+ */
38
+ declare const ResultsChart: import("svelte").Component<{
39
+ items?: any[];
40
+ caption?: string;
41
+ labelHeader?: string;
42
+ valueHeader?: string;
43
+ locale?: string;
44
+ class?: string;
45
+ } & Record<string, any>, {}, "">;
46
+ type $$ComponentProps = {
47
+ items?: any[];
48
+ caption?: string;
49
+ labelHeader?: string;
50
+ valueHeader?: string;
51
+ locale?: string;
52
+ class?: string;
53
+ } & Record<string, any>;
@@ -81,6 +81,15 @@ export { default as FeedView } from "./FeedView.svelte";
81
81
  export { default as ActionPanel } from "./ActionPanel.svelte";
82
82
  export { default as RecordList } from "./RecordList.svelte";
83
83
 
84
+ // Process awareness (#105 Phase 3)
85
+ export { default as PhaseTimeline } from "./PhaseTimeline.svelte";
86
+
87
+ // Results / aggregates (#105 Phase 4) — citizen-facing outcomes over a public
88
+ // aggregate VIEW. ResultsChart ships its data table (a chart is never the only
89
+ // encoding of its data); RankingBoard is a semantic ordered leaderboard.
90
+ export { default as RankingBoard } from "./RankingBoard.svelte";
91
+ export { default as ResultsChart } from "./ResultsChart.svelte";
92
+
84
93
  // Feedback
85
94
  export { default as Alert } from "./Alert.svelte";
86
95
  export { default as Toast } from "./Toast.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",