@jjlmoya/utils-civic 1.6.0 → 1.8.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/package.json +1 -1
- package/src/category/index.ts +3 -1
- package/src/entries.ts +4 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tests/translation_copy.test.ts +1 -1
- package/src/tool/parliamentary-voting-analyzer/bibliography.astro +14 -0
- package/src/tool/parliamentary-voting-analyzer/bibliography.ts +6 -0
- package/src/tool/parliamentary-voting-analyzer/component.astro +63 -0
- package/src/tool/parliamentary-voting-analyzer/contract.test.ts +16 -0
- package/src/tool/parliamentary-voting-analyzer/controller.ts +65 -0
- package/src/tool/parliamentary-voting-analyzer/dom-views.ts +63 -0
- package/src/tool/parliamentary-voting-analyzer/entry.ts +27 -0
- package/src/tool/parliamentary-voting-analyzer/evaluator.ts +12 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/de.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/en.ts +79 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/es.ts +49 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/fr.ts +43 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/id.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/it.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/ja.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/ko.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/nl.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/pl.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/pt.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/ru.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/sv.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/tr.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/zh.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/index.ts +11 -0
- package/src/tool/parliamentary-voting-analyzer/logic.test.ts +52 -0
- package/src/tool/parliamentary-voting-analyzer/logic.ts +279 -0
- package/src/tool/parliamentary-voting-analyzer/parliamentary-voting-analyzer.css +491 -0
- package/src/tool/parliamentary-voting-analyzer/seo.astro +14 -0
- package/src/tool/parliamentary-voting-analyzer/storage.ts +25 -0
- package/src/tool/parliamentary-voting-analyzer/ui.ts +98 -0
- package/src/tool/public-budget-analyzer/bibliography.astro +15 -0
- package/src/tool/public-budget-analyzer/bibliography.ts +7 -0
- package/src/tool/public-budget-analyzer/component.astro +52 -0
- package/src/tool/public-budget-analyzer/controller.ts +84 -0
- package/src/tool/public-budget-analyzer/dom-views.ts +52 -0
- package/src/tool/public-budget-analyzer/entry.ts +27 -0
- package/src/tool/public-budget-analyzer/evaluator.ts +15 -0
- package/src/tool/public-budget-analyzer/i18n/de.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/en.ts +78 -0
- package/src/tool/public-budget-analyzer/i18n/es.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/fr.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/id.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/it.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/ja.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/ko.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/nl.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/pl.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/pt.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/ru.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/sv.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/tr.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/zh.ts +115 -0
- package/src/tool/public-budget-analyzer/index.ts +11 -0
- package/src/tool/public-budget-analyzer/logic.test.ts +81 -0
- package/src/tool/public-budget-analyzer/logic.ts +249 -0
- package/src/tool/public-budget-analyzer/public-budget-analyzer.css +461 -0
- package/src/tool/public-budget-analyzer/seo.astro +15 -0
- package/src/tool/public-budget-analyzer/storage.ts +28 -0
- package/src/tool/public-budget-analyzer/ui.ts +78 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
.parliamentary-voting-analyzer {
|
|
2
|
+
--n-paper: #f6f1e8;
|
|
3
|
+
--n-surface: #fffdf8;
|
|
4
|
+
--n-surface-soft: #eee8dc;
|
|
5
|
+
--n-ink: #162737;
|
|
6
|
+
--n-muted: #607180;
|
|
7
|
+
--n-line: #c9c0b1;
|
|
8
|
+
--n-blue: #1b648a;
|
|
9
|
+
--n-blue-soft: #cce5ed;
|
|
10
|
+
--n-rust: #b55038;
|
|
11
|
+
--n-rust-soft: #f0d2c8;
|
|
12
|
+
--n-quiet: #9ba6a9;
|
|
13
|
+
--n-shadow: #1627371a;
|
|
14
|
+
|
|
15
|
+
background: var(--n-paper);
|
|
16
|
+
border: 1px solid var(--n-line);
|
|
17
|
+
border-radius: 1.25rem;
|
|
18
|
+
box-shadow: 0 1rem 2rem var(--n-shadow);
|
|
19
|
+
color: var(--n-ink);
|
|
20
|
+
display: grid;
|
|
21
|
+
gap: 1rem;
|
|
22
|
+
padding: clamp(1rem, 3vw, 2rem);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.theme-dark .parliamentary-voting-analyzer {
|
|
26
|
+
--n-paper: #172129;
|
|
27
|
+
--n-surface: #202d36;
|
|
28
|
+
--n-surface-soft: #2b3a43;
|
|
29
|
+
--n-ink: #f2eee5;
|
|
30
|
+
--n-muted: #b8c2c3;
|
|
31
|
+
--n-line: #52636a;
|
|
32
|
+
--n-blue: #76c4df;
|
|
33
|
+
--n-blue-soft: #214b5d;
|
|
34
|
+
--n-rust: #ef9475;
|
|
35
|
+
--n-rust-soft: #5a3028;
|
|
36
|
+
--n-quiet: #839397;
|
|
37
|
+
--n-shadow: #00000040;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.pv-input-zone,
|
|
41
|
+
.pv-mode-zone,
|
|
42
|
+
.pv-result,
|
|
43
|
+
.pv-note-zone {
|
|
44
|
+
background: var(--n-surface);
|
|
45
|
+
border: 1px solid var(--n-line);
|
|
46
|
+
border-radius: .9rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.pv-input-zone {
|
|
50
|
+
display: grid;
|
|
51
|
+
gap: .75rem;
|
|
52
|
+
padding: clamp(1rem, 3vw, 1.5rem);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.pv-input-copy {
|
|
56
|
+
display: grid;
|
|
57
|
+
gap: .35rem;
|
|
58
|
+
max-width: 50rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.pv-input-copy p,
|
|
62
|
+
.pv-mode-zone p,
|
|
63
|
+
.pv-result p,
|
|
64
|
+
.pv-note-zone p {
|
|
65
|
+
color: var(--n-muted);
|
|
66
|
+
line-height: 1.55;
|
|
67
|
+
margin: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.pv-input-copy .pv-eyebrow,
|
|
71
|
+
.pv-result .pv-eyebrow {
|
|
72
|
+
color: var(--n-rust);
|
|
73
|
+
font-size: .72rem;
|
|
74
|
+
font-weight: 800;
|
|
75
|
+
letter-spacing: .14em;
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.pv-field-label {
|
|
80
|
+
font-size: .84rem;
|
|
81
|
+
font-weight: 750;
|
|
82
|
+
margin-top: .35rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.pv-csv-hint {
|
|
86
|
+
color: var(--n-muted);
|
|
87
|
+
font-size: .78rem;
|
|
88
|
+
margin: -.25rem 0 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.parliamentary-voting-analyzer textarea {
|
|
92
|
+
background: var(--n-paper);
|
|
93
|
+
border: 1px solid var(--n-line);
|
|
94
|
+
border-radius: .6rem;
|
|
95
|
+
box-sizing: border-box;
|
|
96
|
+
color: var(--n-ink);
|
|
97
|
+
font: .86rem/1.55 ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
98
|
+
min-height: 10rem;
|
|
99
|
+
padding: .85rem;
|
|
100
|
+
resize: vertical;
|
|
101
|
+
width: 100%;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.parliamentary-voting-analyzer textarea:focus,
|
|
105
|
+
.parliamentary-voting-analyzer button:focus-visible,
|
|
106
|
+
.parliamentary-voting-analyzer input:focus-visible {
|
|
107
|
+
outline: 3px solid var(--n-blue-soft);
|
|
108
|
+
outline-offset: 2px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.pv-actions {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-wrap: wrap;
|
|
114
|
+
gap: .55rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.parliamentary-voting-analyzer button {
|
|
118
|
+
border: 1px solid var(--n-line);
|
|
119
|
+
border-radius: 999px;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
font: inherit;
|
|
122
|
+
font-size: .82rem;
|
|
123
|
+
font-weight: 750;
|
|
124
|
+
min-height: 2.7rem;
|
|
125
|
+
padding: .55rem 1rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.pv-primary-action {
|
|
129
|
+
background: var(--n-blue);
|
|
130
|
+
border-color: var(--n-blue);
|
|
131
|
+
color: var(--n-surface);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.pv-secondary-action,
|
|
135
|
+
.pv-quiet-action {
|
|
136
|
+
background: var(--n-surface-soft);
|
|
137
|
+
color: var(--n-ink);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.pv-quiet-action {
|
|
141
|
+
min-height: 2.35rem;
|
|
142
|
+
padding: .4rem .75rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.pv-feedback {
|
|
146
|
+
font-size: .8rem;
|
|
147
|
+
min-height: 1.2rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.pv-mode-zone {
|
|
151
|
+
align-items: center;
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-wrap: wrap;
|
|
154
|
+
gap: 1.4rem;
|
|
155
|
+
justify-content: space-between;
|
|
156
|
+
padding: .9rem 1.2rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.parliamentary-voting-analyzer fieldset {
|
|
160
|
+
border: 0;
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-wrap: wrap;
|
|
163
|
+
gap: .65rem 1rem;
|
|
164
|
+
margin: 0;
|
|
165
|
+
padding: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.parliamentary-voting-analyzer legend {
|
|
169
|
+
color: var(--n-muted);
|
|
170
|
+
font-size: .72rem;
|
|
171
|
+
font-weight: 800;
|
|
172
|
+
letter-spacing: .08em;
|
|
173
|
+
margin-bottom: .45rem;
|
|
174
|
+
text-transform: uppercase;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.parliamentary-voting-analyzer fieldset label {
|
|
178
|
+
align-items: center;
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
font-size: .84rem;
|
|
181
|
+
gap: .35rem;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.parliamentary-voting-analyzer input[type='radio'] {
|
|
185
|
+
accent-color: var(--n-blue);
|
|
186
|
+
block-size: 1rem;
|
|
187
|
+
inline-size: 1rem;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.pv-method-strip {
|
|
191
|
+
align-items: center;
|
|
192
|
+
color: var(--n-muted);
|
|
193
|
+
display: flex;
|
|
194
|
+
font-size: .78rem;
|
|
195
|
+
gap: .55rem;
|
|
196
|
+
max-width: 33rem;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.pv-method-line {
|
|
200
|
+
background: var(--n-rust);
|
|
201
|
+
block-size: 2px;
|
|
202
|
+
display: block;
|
|
203
|
+
inline-size: 2.5rem;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.pv-empty {
|
|
207
|
+
align-items: center;
|
|
208
|
+
background: var(--n-surface-soft);
|
|
209
|
+
border-radius: .9rem;
|
|
210
|
+
color: var(--n-muted);
|
|
211
|
+
display: flex;
|
|
212
|
+
gap: 1rem;
|
|
213
|
+
justify-content: center;
|
|
214
|
+
min-height: 11rem;
|
|
215
|
+
padding: 1.25rem;
|
|
216
|
+
text-align: center;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.pv-empty-mark {
|
|
220
|
+
border: 2px solid var(--n-blue);
|
|
221
|
+
border-radius: 50%;
|
|
222
|
+
block-size: 3.2rem;
|
|
223
|
+
inline-size: 3.2rem;
|
|
224
|
+
position: relative;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.pv-empty-mark::before,
|
|
228
|
+
.pv-empty-mark::after {
|
|
229
|
+
background: var(--n-rust);
|
|
230
|
+
content: '';
|
|
231
|
+
inset: 50% auto auto 50%;
|
|
232
|
+
position: absolute;
|
|
233
|
+
transform: translate(-50%, -50%);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.pv-empty-mark::before {
|
|
237
|
+
block-size: 1px;
|
|
238
|
+
inline-size: 2rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.pv-empty-mark::after {
|
|
242
|
+
block-size: 2rem;
|
|
243
|
+
inline-size: 1px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.pv-result {
|
|
247
|
+
display: grid;
|
|
248
|
+
gap: 1.1rem;
|
|
249
|
+
padding: clamp(1rem, 3vw, 1.5rem);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.pv-result-intro,
|
|
253
|
+
.pv-ribbon-head,
|
|
254
|
+
.pv-ribbon-row,
|
|
255
|
+
.pv-summary-line {
|
|
256
|
+
align-items: center;
|
|
257
|
+
display: grid;
|
|
258
|
+
gap: .75rem;
|
|
259
|
+
grid-template-columns: minmax(7rem, 1fr) auto;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.pv-result-intro {
|
|
263
|
+
grid-template-columns: 1fr auto;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.pv-countline {
|
|
267
|
+
display: flex;
|
|
268
|
+
flex-wrap: wrap;
|
|
269
|
+
gap: .7rem;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.pv-countline strong {
|
|
273
|
+
color: var(--n-ink);
|
|
274
|
+
font-size: 1.1rem;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.pv-ribbon {
|
|
278
|
+
background: var(--n-paper);
|
|
279
|
+
border: 1px solid var(--n-line);
|
|
280
|
+
border-radius: .65rem;
|
|
281
|
+
overflow-x: auto;
|
|
282
|
+
padding: 1rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.pv-ribbon-head {
|
|
286
|
+
color: var(--n-muted);
|
|
287
|
+
font-size: .68rem;
|
|
288
|
+
font-weight: 800;
|
|
289
|
+
letter-spacing: .08em;
|
|
290
|
+
margin-bottom: .7rem;
|
|
291
|
+
text-transform: uppercase;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.pv-ribbon-votes {
|
|
295
|
+
display: flex;
|
|
296
|
+
gap: .45rem;
|
|
297
|
+
justify-content: flex-start;
|
|
298
|
+
min-width: 16rem;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.pv-ribbon-votes span {
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
text-overflow: ellipsis;
|
|
304
|
+
width: 2.2rem;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.pv-ribbon-row {
|
|
308
|
+
grid-template-columns: minmax(7rem, 1fr) minmax(16rem, 3fr) 4.5rem;
|
|
309
|
+
min-width: 29rem;
|
|
310
|
+
padding: .35rem 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.pv-ribbon-name {
|
|
314
|
+
font-size: .82rem;
|
|
315
|
+
font-weight: 750;
|
|
316
|
+
overflow: hidden;
|
|
317
|
+
text-overflow: ellipsis;
|
|
318
|
+
white-space: nowrap;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.pv-ribbon-track {
|
|
322
|
+
display: flex;
|
|
323
|
+
gap: .45rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.pv-ribbon-cell {
|
|
327
|
+
background: var(--n-quiet);
|
|
328
|
+
border-radius: .25rem;
|
|
329
|
+
display: block;
|
|
330
|
+
height: 1.25rem;
|
|
331
|
+
min-width: 2.2rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.pv-ribbon-cell.is-yes,
|
|
335
|
+
.pv-dot.is-yes {
|
|
336
|
+
background: var(--n-blue);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.pv-ribbon-cell.is-no,
|
|
340
|
+
.pv-dot.is-no {
|
|
341
|
+
background: var(--n-rust);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.pv-ribbon-cell.is-abstain,
|
|
345
|
+
.pv-dot.is-abstain {
|
|
346
|
+
background: transparent;
|
|
347
|
+
border: 2px solid var(--n-rust);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.pv-ribbon-cell.is-tie {
|
|
351
|
+
background: var(--n-surface-soft);
|
|
352
|
+
border: 2px dashed var(--n-rust);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.pv-ribbon-cell.is-missing {
|
|
356
|
+
background: transparent;
|
|
357
|
+
border: 1px dashed var(--n-line);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.pv-ribbon-score {
|
|
361
|
+
font-size: .86rem;
|
|
362
|
+
font-variant-numeric: tabular-nums;
|
|
363
|
+
font-weight: 800;
|
|
364
|
+
text-align: right;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.pv-legend {
|
|
368
|
+
display: flex;
|
|
369
|
+
flex-wrap: wrap;
|
|
370
|
+
gap: .45rem .75rem;
|
|
371
|
+
font-size: .75rem;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.pv-dot {
|
|
375
|
+
border-radius: 50%;
|
|
376
|
+
display: inline-block;
|
|
377
|
+
height: .7rem;
|
|
378
|
+
margin-right: -.3rem;
|
|
379
|
+
width: .7rem;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.pv-result-detail,
|
|
383
|
+
.pv-note-zone {
|
|
384
|
+
display: grid;
|
|
385
|
+
gap: 1.2rem;
|
|
386
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.pv-result-detail h3,
|
|
390
|
+
.pv-note-zone h3 {
|
|
391
|
+
color: var(--n-ink);
|
|
392
|
+
font-size: 1rem;
|
|
393
|
+
margin: 0 0 .35rem;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.pv-result-detail > div,
|
|
397
|
+
.pv-note-zone > div {
|
|
398
|
+
border-top: 2px solid var(--n-line);
|
|
399
|
+
padding-top: .8rem;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.pv-summary-list {
|
|
403
|
+
display: grid;
|
|
404
|
+
gap: .3rem;
|
|
405
|
+
margin-top: .9rem;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.pv-summary-line {
|
|
409
|
+
border-bottom: 1px solid var(--n-line);
|
|
410
|
+
font-size: .76rem;
|
|
411
|
+
grid-template-columns: 1.2fr .8fr 1fr;
|
|
412
|
+
padding: .45rem 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.pv-summary-line span:last-child {
|
|
416
|
+
color: var(--n-muted);
|
|
417
|
+
grid-column: 1 / -1;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.pv-table-wrap {
|
|
421
|
+
margin-top: .9rem;
|
|
422
|
+
max-height: 13rem;
|
|
423
|
+
overflow: auto;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.parliamentary-voting-analyzer table {
|
|
427
|
+
border-collapse: collapse;
|
|
428
|
+
font-size: .76rem;
|
|
429
|
+
min-width: 100%;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.parliamentary-voting-analyzer th,
|
|
433
|
+
.parliamentary-voting-analyzer td {
|
|
434
|
+
border-bottom: 1px solid var(--n-line);
|
|
435
|
+
padding: .5rem .4rem;
|
|
436
|
+
text-align: left;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.parliamentary-voting-analyzer th {
|
|
440
|
+
color: var(--n-muted);
|
|
441
|
+
font-size: .68rem;
|
|
442
|
+
letter-spacing: .04em;
|
|
443
|
+
text-transform: uppercase;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.pv-warnings {
|
|
447
|
+
background: var(--n-rust-soft);
|
|
448
|
+
border-left: 3px solid var(--n-rust);
|
|
449
|
+
padding: .75rem 1rem;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.pv-warnings h3 {
|
|
453
|
+
color: var(--n-ink);
|
|
454
|
+
font-size: .85rem;
|
|
455
|
+
margin: 0 0 .35rem;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.pv-warnings ul {
|
|
459
|
+
color: var(--n-ink);
|
|
460
|
+
font-size: .8rem;
|
|
461
|
+
line-height: 1.5;
|
|
462
|
+
margin: 0;
|
|
463
|
+
padding-left: 1.1rem;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.pv-note-zone {
|
|
467
|
+
padding: 1rem 1.2rem;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@media (max-width: 42rem) {
|
|
471
|
+
.parliamentary-voting-analyzer {
|
|
472
|
+
border-radius: .8rem;
|
|
473
|
+
padding: .75rem;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.pv-result-detail,
|
|
477
|
+
.pv-note-zone {
|
|
478
|
+
grid-template-columns: 1fr;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.pv-result-intro {
|
|
482
|
+
align-items: start;
|
|
483
|
+
grid-template-columns: 1fr;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.pv-mode-zone {
|
|
487
|
+
align-items: start;
|
|
488
|
+
display: grid;
|
|
489
|
+
gap: .8rem;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import SEORenderer from '@jjlmoya/utils-shared/seo/SEORenderer.astro';
|
|
3
|
+
import { parliamentaryVotingAnalyzer } from './entry';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { locale = 'en' } = Astro.props;
|
|
10
|
+
const loader = parliamentaryVotingAnalyzer.i18n[locale as keyof typeof parliamentaryVotingAnalyzer.i18n] ?? parliamentaryVotingAnalyzer.i18n.en;
|
|
11
|
+
const content = await loader!();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<SEORenderer content={{ locale, sections: content.seo }} />
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const storageKey = 'jjlmoya-civic-parliamentary-voting-analyzer';
|
|
2
|
+
|
|
3
|
+
export const loadDraft = (): string => {
|
|
4
|
+
try {
|
|
5
|
+
return window.localStorage.getItem(storageKey) ?? '';
|
|
6
|
+
} catch {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const saveDraft = (value: string): void => {
|
|
12
|
+
try {
|
|
13
|
+
window.localStorage.setItem(storageKey, value);
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const clearDraft = (): void => {
|
|
20
|
+
try {
|
|
21
|
+
window.localStorage.removeItem(storageKey);
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface ParliamentaryVotingUI {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
dataHeading: string;
|
|
4
|
+
dataHelp: string;
|
|
5
|
+
csvLabel: string;
|
|
6
|
+
csvHint: string;
|
|
7
|
+
loadExample: string;
|
|
8
|
+
analyzeAction: string;
|
|
9
|
+
clearAction: string;
|
|
10
|
+
compareLabel: string;
|
|
11
|
+
groupsOption: string;
|
|
12
|
+
membersOption: string;
|
|
13
|
+
resultHeading: string;
|
|
14
|
+
emptyResult: string;
|
|
15
|
+
rowsLabel: string;
|
|
16
|
+
groupsLabel: string;
|
|
17
|
+
votesLabel: string;
|
|
18
|
+
warningsHeading: string;
|
|
19
|
+
methodHeading: string;
|
|
20
|
+
methodText: string;
|
|
21
|
+
limitsHeading: string;
|
|
22
|
+
limitsText: string;
|
|
23
|
+
edgeHeading: string;
|
|
24
|
+
edgeText: string;
|
|
25
|
+
cohesionHeading: string;
|
|
26
|
+
cohesionHelp: string;
|
|
27
|
+
agreementHeading: string;
|
|
28
|
+
agreementHelp: string;
|
|
29
|
+
groupLabel: string;
|
|
30
|
+
voteLabel: string;
|
|
31
|
+
riceLabel: string;
|
|
32
|
+
comparableLabel: string;
|
|
33
|
+
participationLabel: string;
|
|
34
|
+
abstentionsLabel: string;
|
|
35
|
+
absencesLabel: string;
|
|
36
|
+
unknownLabel: string;
|
|
37
|
+
noComparableData: string;
|
|
38
|
+
noDataWarning: string;
|
|
39
|
+
malformedRowWarning: string;
|
|
40
|
+
duplicateWarning: string;
|
|
41
|
+
unknownChoiceWarning: string;
|
|
42
|
+
lowSampleWarning: string;
|
|
43
|
+
tieWarning: string;
|
|
44
|
+
importedMessage: string;
|
|
45
|
+
invalidCsvMessage: string;
|
|
46
|
+
copyAction: string;
|
|
47
|
+
copiedMessage: string;
|
|
48
|
+
copyFailure: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const ui: ParliamentaryVotingUI = {
|
|
52
|
+
dataHeading: 'Bring a roll call into view',
|
|
53
|
+
dataHelp: 'Paste one row per member and vote. Use four columns named member, group, vote, and choice.',
|
|
54
|
+
csvLabel: 'Roll call CSV',
|
|
55
|
+
csvHint: 'Required columns: member, group, vote, choice. Accepted choices: yes, no, abstain, absent, unknown.',
|
|
56
|
+
loadExample: 'Load example',
|
|
57
|
+
analyzeAction: 'Analyze votes',
|
|
58
|
+
clearAction: 'Clear data',
|
|
59
|
+
compareLabel: 'Agreement view',
|
|
60
|
+
groupsOption: 'Groups',
|
|
61
|
+
membersOption: 'Members',
|
|
62
|
+
resultHeading: 'The voting record at a glance',
|
|
63
|
+
emptyResult: 'Paste a roll call or load the example to reveal the voting ribbon.',
|
|
64
|
+
rowsLabel: 'records',
|
|
65
|
+
groupsLabel: 'groups',
|
|
66
|
+
votesLabel: 'votes',
|
|
67
|
+
warningsHeading: 'Checks before interpretation',
|
|
68
|
+
methodHeading: 'Method applied',
|
|
69
|
+
methodText: 'The Rice Index for each group and vote is absolute yes minus no divided by yes plus no. Abstentions, absences, and unknown values are excluded from that binary denominator. Group means are unweighted averages of votes with at least one yes or no. Agreement compares binary choices on the same vote and always shows the comparable vote count.',
|
|
70
|
+
limitsHeading: 'What this tool does not do',
|
|
71
|
+
limitsText: 'It does not infer ideology, intent, discipline, collusion, misconduct, policy positions, or causation. It does not decide whether a vote was valid or whether a member was legally required to attend. Similar voting can have many explanations.',
|
|
72
|
+
edgeHeading: 'Edge cases and data warnings',
|
|
73
|
+
edgeText: 'Unanimous votes produce a score of 1, not proof of discipline. Ties produce zero cohesion and are marked separately from abstentions. Missing votes reduce the comparable sample. Keep changing group membership, duplicate identifiers, and unknown choices visible in the source data.',
|
|
74
|
+
cohesionHeading: 'Cohesion ribbon',
|
|
75
|
+
cohesionHelp: 'Each row is a group. A bright mark is yes, a dark mark is no, a dashed mark is a tie, a hollow mark is abstention, and a quiet mark is absence or unknown.',
|
|
76
|
+
agreementHeading: 'Pairwise agreement',
|
|
77
|
+
agreementHelp: 'Agreement uses only comparable yes and no choices shared by both entities. In the group view, each group contributes its non tied majority choice. Abstentions and nonvotes stay out of the numerator and denominator.',
|
|
78
|
+
groupLabel: 'Group',
|
|
79
|
+
voteLabel: 'Vote',
|
|
80
|
+
riceLabel: 'Rice Index',
|
|
81
|
+
comparableLabel: 'Comparable votes',
|
|
82
|
+
participationLabel: 'Recorded choices',
|
|
83
|
+
abstentionsLabel: 'abstentions',
|
|
84
|
+
absencesLabel: 'absences',
|
|
85
|
+
unknownLabel: 'unknown',
|
|
86
|
+
noComparableData: 'No comparable yes or no choices',
|
|
87
|
+
noDataWarning: 'No usable records were found. Add member, group, vote, and choice values.',
|
|
88
|
+
malformedRowWarning: 'Some rows were skipped because a required field was empty.',
|
|
89
|
+
duplicateWarning: 'Duplicate member and vote pairs were kept only once.',
|
|
90
|
+
unknownChoiceWarning: 'Some choices were retained as unknown because they were not recognized as yes, no, abstention, or absence.',
|
|
91
|
+
lowSampleWarning: 'Some pairwise results have fewer than two comparable votes and should not be treated as stable patterns.',
|
|
92
|
+
tieWarning: 'A group or entity tie was excluded from group level comparison for that vote.',
|
|
93
|
+
importedMessage: 'Example loaded. You can edit the rows before analyzing.',
|
|
94
|
+
invalidCsvMessage: 'The data could not be read as a row based CSV.',
|
|
95
|
+
copyAction: 'Copy summary',
|
|
96
|
+
copiedMessage: 'Summary copied to the clipboard.',
|
|
97
|
+
copyFailure: 'Copy failed. Select the summary text manually.',
|
|
98
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { publicBudgetAnalyzer } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale } = Astro.props as Props;
|
|
11
|
+
const loader = publicBudgetAnalyzer.i18n[locale] ?? publicBudgetAnalyzer.i18n.en;
|
|
12
|
+
const content = loader ? await loader() : undefined;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content ? <Bibliography links={content.bibliography} /> : null}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{ name: 'International Monetary Fund, Government Finance Statistics Manual 2014', url: 'https://www.imf.org/external/pubs/ft/gfs/manual/2014/gfsfinal.pdf' },
|
|
5
|
+
{ name: 'Intervencion General de la Administracion del Estado, Clasificacion funcional del gasto de las Administraciones Publicas COFOG', url: 'https://www.igae.pap.hacienda.gob.es/sitios/igae/es-ES/Contabilidad/ContabilidadNacional/Publicaciones/paginas/iacogof.aspx' },
|
|
6
|
+
{ name: 'OECD, Budget Transparency Toolkit', url: 'https://www.oecd.org/en/publications/oecd-budget-transparency-toolkit_9789264282070-en.html' },
|
|
7
|
+
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { PublicBudgetUI } from './ui';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: PublicBudgetUI;
|
|
6
|
+
locale?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props as Props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="public-budget-analyzer" data-public-budget-analyzer>
|
|
13
|
+
<div class="budget-command-bar">
|
|
14
|
+
<div class="budget-dropzone" data-budget-dropzone>
|
|
15
|
+
<input id="budget-file-input" data-budget-file type="file" accept=".csv,.json,text/csv,application/json" aria-label={ui.uploadLabel} />
|
|
16
|
+
<label for="budget-file-input">{ui.uploadLabel}</label>
|
|
17
|
+
<p>{ui.uploadHelp}</p>
|
|
18
|
+
<small>{ui.supportedFormat}</small>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="budget-actions">
|
|
21
|
+
<button type="button" data-budget-example aria-label={`${ui.loadExample}: ${ui.exampleName}`}>{ui.loadExample}</button>
|
|
22
|
+
<button type="button" data-budget-download>{ui.downloadCsv}</button>
|
|
23
|
+
<button type="button" data-budget-print>{ui.printAction}</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="budget-result" data-budget-result aria-live="polite">
|
|
27
|
+
<p class="budget-empty-note">{ui.analyzeEmpty}</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="budget-guidance">
|
|
30
|
+
<section>
|
|
31
|
+
<h2>{ui.methodHeading}</h2>
|
|
32
|
+
<p>{ui.methodText}</p>
|
|
33
|
+
</section>
|
|
34
|
+
<section>
|
|
35
|
+
<h2>{ui.limitsHeading}</h2>
|
|
36
|
+
<p>{ui.limitsText}</p>
|
|
37
|
+
</section>
|
|
38
|
+
<section>
|
|
39
|
+
<h2>{ui.edgeCasesHeading}</h2>
|
|
40
|
+
<p>{ui.edgeCasesText}</p>
|
|
41
|
+
</section>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script is:inline type="application/json" data-budget-ui set:html={JSON.stringify(ui)}></script>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
import { mountPublicBudgetAnalyzer } from './controller';
|
|
49
|
+
const root = document.querySelector<HTMLElement>('[data-public-budget-analyzer]');
|
|
50
|
+
const data = document.querySelector<HTMLScriptElement>('[data-budget-ui]');
|
|
51
|
+
if (root && data) mountPublicBudgetAnalyzer(root, JSON.parse(data.textContent || '{}'));
|
|
52
|
+
</script>
|