@aiaiai-pt/design-system 0.20.0 → 0.21.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>;
@@ -81,6 +81,9 @@ 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
+
84
87
  // Feedback
85
88
  export { default as Alert } from "./Alert.svelte";
86
89
  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.21.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",