@jjlmoya/utils-civic 1.6.0 → 1.7.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 +2 -1
- package/src/entries.ts +2 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- 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 +2 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--n-paper: #f8f6ef;
|
|
3
|
+
--n-surface: #fffdf8;
|
|
4
|
+
--n-ink: #1d252c;
|
|
5
|
+
--n-muted: #5b6870;
|
|
6
|
+
--n-line: #cbd5d2;
|
|
7
|
+
--n-accent: #a6442f;
|
|
8
|
+
--n-accent-strong: #7d2f27;
|
|
9
|
+
--n-segment-a: #1e6870;
|
|
10
|
+
--n-segment-b: #c97943;
|
|
11
|
+
--n-good: #286c4a;
|
|
12
|
+
--n-warn-bg: #fff1d2;
|
|
13
|
+
--n-warn-ink: #704b0b;
|
|
14
|
+
--n-error-bg: #f9dfdb;
|
|
15
|
+
--n-error-ink: #7d2f27;
|
|
16
|
+
--n-shadow: rgba(29, 37, 44, 0.12);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.theme-dark {
|
|
20
|
+
--n-paper: #15171b;
|
|
21
|
+
--n-surface: #20242a;
|
|
22
|
+
--n-ink: #f3eee3;
|
|
23
|
+
--n-muted: #bcc3c5;
|
|
24
|
+
--n-line: #4b555b;
|
|
25
|
+
--n-accent: #e48663;
|
|
26
|
+
--n-accent-strong: #ffad86;
|
|
27
|
+
--n-segment-a: #63bcc0;
|
|
28
|
+
--n-segment-b: #e5a068;
|
|
29
|
+
--n-good: #7ad49e;
|
|
30
|
+
--n-warn-bg: #473818;
|
|
31
|
+
--n-warn-ink: #ffe09b;
|
|
32
|
+
--n-error-bg: #512923;
|
|
33
|
+
--n-error-ink: #ffb5a5;
|
|
34
|
+
--n-shadow: rgba(0, 0, 0, 0.3);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.public-budget-analyzer {
|
|
38
|
+
--n-paper: #f8f6ef;
|
|
39
|
+
--n-surface: #fffdf8;
|
|
40
|
+
--n-ink: #1d252c;
|
|
41
|
+
--n-muted: #5b6870;
|
|
42
|
+
--n-line: #cbd5d2;
|
|
43
|
+
--n-accent: #a6442f;
|
|
44
|
+
--n-accent-strong: #7d2f27;
|
|
45
|
+
--n-segment-a: #1e6870;
|
|
46
|
+
--n-segment-b: #c97943;
|
|
47
|
+
--n-good: #286c4a;
|
|
48
|
+
--n-warn-bg: #fff1d2;
|
|
49
|
+
--n-warn-ink: #704b0b;
|
|
50
|
+
--n-error-bg: #f9dfdb;
|
|
51
|
+
--n-error-ink: #7d2f27;
|
|
52
|
+
--n-shadow: rgba(29, 37, 44, 0.12);
|
|
53
|
+
|
|
54
|
+
background: var(--n-paper);
|
|
55
|
+
border: 1px solid var(--n-line);
|
|
56
|
+
border-radius: 1.25rem;
|
|
57
|
+
box-shadow: 0 1rem 2rem var(--n-shadow);
|
|
58
|
+
color: var(--n-ink);
|
|
59
|
+
min-width: 0;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.theme-dark .public-budget-analyzer {
|
|
64
|
+
--n-paper: #15171b;
|
|
65
|
+
--n-surface: #20242a;
|
|
66
|
+
--n-ink: #f3eee3;
|
|
67
|
+
--n-muted: #bcc3c5;
|
|
68
|
+
--n-line: #4b555b;
|
|
69
|
+
--n-accent: #e48663;
|
|
70
|
+
--n-accent-strong: #ffad86;
|
|
71
|
+
--n-segment-a: #63bcc0;
|
|
72
|
+
--n-segment-b: #e5a068;
|
|
73
|
+
--n-good: #7ad49e;
|
|
74
|
+
--n-warn-bg: #473818;
|
|
75
|
+
--n-warn-ink: #ffe09b;
|
|
76
|
+
--n-error-bg: #512923;
|
|
77
|
+
--n-error-ink: #ffb5a5;
|
|
78
|
+
--n-shadow: rgba(0, 0, 0, 0.3);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main:has(.public-budget-analyzer) {
|
|
82
|
+
min-width: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.budget-command-bar,
|
|
86
|
+
.budget-result,
|
|
87
|
+
.budget-guidance {
|
|
88
|
+
padding: 1.5rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.budget-command-bar {
|
|
92
|
+
align-items: stretch;
|
|
93
|
+
border-bottom: 1px solid var(--n-line);
|
|
94
|
+
display: grid;
|
|
95
|
+
gap: 1rem;
|
|
96
|
+
grid-template-columns: minmax(0, 1fr);
|
|
97
|
+
min-width: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.budget-dropzone {
|
|
101
|
+
border: 1px dashed var(--n-accent);
|
|
102
|
+
border-radius: 0.75rem;
|
|
103
|
+
padding: 1rem;
|
|
104
|
+
position: relative;
|
|
105
|
+
min-width: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.budget-dropzone.is-dragging {
|
|
109
|
+
background: var(--n-warn-bg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.budget-dropzone input {
|
|
113
|
+
block-size: 100%;
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
inline-size: 100%;
|
|
116
|
+
inset: 0;
|
|
117
|
+
opacity: 0;
|
|
118
|
+
position: absolute;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.budget-dropzone label {
|
|
122
|
+
color: var(--n-accent-strong);
|
|
123
|
+
display: block;
|
|
124
|
+
font-size: 1rem;
|
|
125
|
+
font-weight: 800;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.budget-dropzone p,
|
|
129
|
+
.budget-dropzone small {
|
|
130
|
+
color: var(--n-muted);
|
|
131
|
+
display: block;
|
|
132
|
+
line-height: 1.5;
|
|
133
|
+
margin: 0.35rem 0 0;
|
|
134
|
+
overflow-wrap: anywhere;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.budget-actions {
|
|
138
|
+
align-items: center;
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-wrap: wrap;
|
|
141
|
+
gap: 0.5rem;
|
|
142
|
+
justify-content: flex-start;
|
|
143
|
+
min-width: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.budget-actions button {
|
|
147
|
+
background: var(--n-surface);
|
|
148
|
+
border: 1px solid var(--n-line);
|
|
149
|
+
border-radius: 999px;
|
|
150
|
+
color: var(--n-ink);
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
flex: 1 1 8rem;
|
|
153
|
+
font: inherit;
|
|
154
|
+
font-size: 0.82rem;
|
|
155
|
+
min-width: 0;
|
|
156
|
+
max-width: 16rem;
|
|
157
|
+
padding: 0.65rem 0.85rem;
|
|
158
|
+
white-space: normal;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.budget-actions button:first-child {
|
|
162
|
+
background: var(--n-accent);
|
|
163
|
+
border-color: var(--n-accent);
|
|
164
|
+
color: var(--n-surface);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.budget-actions button:hover,
|
|
168
|
+
.budget-actions button:focus-visible {
|
|
169
|
+
border-color: var(--n-accent-strong);
|
|
170
|
+
outline: 2px solid var(--n-accent);
|
|
171
|
+
outline-offset: 2px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.budget-result {
|
|
175
|
+
background: var(--n-surface);
|
|
176
|
+
min-height: 16rem;
|
|
177
|
+
min-width: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.budget-result-head,
|
|
181
|
+
.budget-detail-heading,
|
|
182
|
+
.budget-landscape-axis,
|
|
183
|
+
.budget-category-label,
|
|
184
|
+
.budget-facts {
|
|
185
|
+
align-items: baseline;
|
|
186
|
+
display: flex;
|
|
187
|
+
gap: 1rem;
|
|
188
|
+
justify-content: space-between;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.budget-result-head h2,
|
|
192
|
+
.budget-detail-heading h3,
|
|
193
|
+
.budget-guidance h2,
|
|
194
|
+
.budget-notice h3 {
|
|
195
|
+
color: var(--n-ink);
|
|
196
|
+
margin: 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.budget-result-head > p,
|
|
200
|
+
.budget-detail-heading > span,
|
|
201
|
+
.budget-category-row small,
|
|
202
|
+
.budget-landscape-axis,
|
|
203
|
+
.budget-facts span {
|
|
204
|
+
color: var(--n-muted);
|
|
205
|
+
font-size: 0.78rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.budget-status {
|
|
209
|
+
border: 1px solid var(--n-line);
|
|
210
|
+
border-radius: 999px;
|
|
211
|
+
display: inline-block;
|
|
212
|
+
font-size: 0.7rem;
|
|
213
|
+
font-weight: 800;
|
|
214
|
+
letter-spacing: 0.04em;
|
|
215
|
+
margin-bottom: 0.55rem;
|
|
216
|
+
padding: 0.25rem 0.55rem;
|
|
217
|
+
text-transform: uppercase;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.budget-status-ready { color: var(--n-good); }
|
|
221
|
+
.budget-status-review { color: var(--n-warn-ink); }
|
|
222
|
+
.budget-status-empty { color: var(--n-muted); }
|
|
223
|
+
|
|
224
|
+
.budget-metrics {
|
|
225
|
+
display: grid;
|
|
226
|
+
gap: 0.6rem;
|
|
227
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
228
|
+
margin: 1.5rem 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.budget-metric {
|
|
232
|
+
border-left: 3px solid var(--n-accent);
|
|
233
|
+
min-width: 0;
|
|
234
|
+
padding: 0.25rem 0 0.25rem 0.75rem;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.budget-metric span,
|
|
238
|
+
.budget-metric strong,
|
|
239
|
+
.budget-facts strong,
|
|
240
|
+
.budget-facts span {
|
|
241
|
+
display: block;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.budget-metric span {
|
|
245
|
+
color: var(--n-muted);
|
|
246
|
+
font-size: 0.75rem;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.budget-metric strong {
|
|
250
|
+
font-size: 1.08rem;
|
|
251
|
+
margin-top: 0.25rem;
|
|
252
|
+
overflow-wrap: anywhere;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.budget-landscape {
|
|
256
|
+
border-top: 1px solid var(--n-line);
|
|
257
|
+
padding-top: 0.8rem;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.budget-category-row {
|
|
261
|
+
margin-top: 0.9rem;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.budget-category-label strong {
|
|
265
|
+
font-size: 0.86rem;
|
|
266
|
+
overflow-wrap: anywhere;
|
|
267
|
+
white-space: normal;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.budget-category-label span {
|
|
271
|
+
overflow-wrap: anywhere;
|
|
272
|
+
min-width: 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.budget-bar {
|
|
276
|
+
background: var(--n-paper);
|
|
277
|
+
border: 1px solid var(--n-line);
|
|
278
|
+
height: 1rem;
|
|
279
|
+
margin-top: 0.35rem;
|
|
280
|
+
overflow: hidden;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.budget-bar span {
|
|
284
|
+
background: var(--n-segment-a);
|
|
285
|
+
display: block;
|
|
286
|
+
height: 100%;
|
|
287
|
+
width: var(--segment-width);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.budget-bar .budget-segment-b {
|
|
291
|
+
background: var(--n-segment-b);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.budget-category-row small {
|
|
295
|
+
display: block;
|
|
296
|
+
margin-top: 0.3rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.budget-detail {
|
|
300
|
+
border-top: 1px solid var(--n-line);
|
|
301
|
+
margin-top: 1.5rem;
|
|
302
|
+
min-width: 0;
|
|
303
|
+
padding-top: 1.25rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.budget-table-wrap {
|
|
307
|
+
min-width: 0;
|
|
308
|
+
overflow-x: auto;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.budget-table-wrap table {
|
|
312
|
+
border-collapse: collapse;
|
|
313
|
+
margin-top: 0.8rem;
|
|
314
|
+
min-width: 42rem;
|
|
315
|
+
width: 100%;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.budget-table-wrap th,
|
|
319
|
+
.budget-table-wrap td {
|
|
320
|
+
border-bottom: 1px solid var(--n-line);
|
|
321
|
+
padding: 0.65rem 0.5rem;
|
|
322
|
+
text-align: left;
|
|
323
|
+
vertical-align: top;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.budget-table-wrap th {
|
|
327
|
+
color: var(--n-muted);
|
|
328
|
+
font-size: 0.72rem;
|
|
329
|
+
text-transform: uppercase;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.budget-table-wrap td {
|
|
333
|
+
font-size: 0.84rem;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.budget-notice {
|
|
337
|
+
border-left: 4px solid var(--n-accent);
|
|
338
|
+
margin-top: 1.25rem;
|
|
339
|
+
padding: 0.8rem 1rem;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.budget-notice-warning {
|
|
343
|
+
background: var(--n-warn-bg);
|
|
344
|
+
border-color: var(--n-warn-ink);
|
|
345
|
+
color: var(--n-warn-ink);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.budget-notice-error {
|
|
349
|
+
background: var(--n-error-bg);
|
|
350
|
+
border-color: var(--n-error-ink);
|
|
351
|
+
color: var(--n-error-ink);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.budget-notice h3 {
|
|
355
|
+
color: var(--n-ink);
|
|
356
|
+
font-size: 0.95rem;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.budget-notice ul {
|
|
360
|
+
line-height: 1.5;
|
|
361
|
+
margin: 0.5rem 0 0;
|
|
362
|
+
padding-left: 1.15rem;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.budget-facts {
|
|
366
|
+
border-top: 1px solid var(--n-line);
|
|
367
|
+
flex-wrap: wrap;
|
|
368
|
+
margin-top: 1.5rem;
|
|
369
|
+
padding-top: 1rem;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.budget-facts div {
|
|
373
|
+
min-width: 10rem;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.budget-facts strong {
|
|
377
|
+
margin-top: 0.25rem;
|
|
378
|
+
overflow-wrap: anywhere;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.budget-empty-note,
|
|
382
|
+
.budget-inline-message {
|
|
383
|
+
border: 1px solid var(--n-line);
|
|
384
|
+
color: var(--n-muted);
|
|
385
|
+
margin: 0;
|
|
386
|
+
padding: 1rem;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.budget-inline-error {
|
|
390
|
+
background: var(--n-error-bg);
|
|
391
|
+
border-color: var(--n-error-ink);
|
|
392
|
+
color: var(--n-error-ink);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.budget-guidance {
|
|
396
|
+
border-top: 1px solid var(--n-line);
|
|
397
|
+
display: grid;
|
|
398
|
+
gap: 1rem;
|
|
399
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.budget-guidance section {
|
|
403
|
+
border-left: 2px solid var(--n-accent);
|
|
404
|
+
padding-left: 0.8rem;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.budget-guidance h2 {
|
|
408
|
+
font-size: 0.92rem;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.budget-guidance p {
|
|
412
|
+
color: var(--n-muted);
|
|
413
|
+
font-size: 0.83rem;
|
|
414
|
+
line-height: 1.55;
|
|
415
|
+
margin: 0.5rem 0 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@media (max-width: 800px) {
|
|
419
|
+
.budget-command-bar,
|
|
420
|
+
.budget-guidance {
|
|
421
|
+
grid-template-columns: 1fr;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.budget-actions {
|
|
425
|
+
justify-content: flex-start;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.budget-metrics {
|
|
429
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@media (max-width: 480px) {
|
|
434
|
+
.budget-command-bar,
|
|
435
|
+
.budget-result,
|
|
436
|
+
.budget-guidance {
|
|
437
|
+
padding: 1rem;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.budget-metrics {
|
|
441
|
+
grid-template-columns: 1fr;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.budget-result-head,
|
|
445
|
+
.budget-detail-heading,
|
|
446
|
+
.budget-category-label {
|
|
447
|
+
align-items: flex-start;
|
|
448
|
+
flex-direction: column;
|
|
449
|
+
gap: 0.25rem;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
@media print {
|
|
454
|
+
.public-budget-analyzer {
|
|
455
|
+
box-shadow: none;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.budget-command-bar {
|
|
459
|
+
display: none;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } 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 ? <SEORenderer content={{ locale, sections: content.seo }} /> : null}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const storageKey = 'jjlmoya-public-budget-analyzer';
|
|
2
|
+
|
|
3
|
+
export interface SavedBudgetState {
|
|
4
|
+
format: 'csv' | 'json';
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function loadSavedBudget(): SavedBudgetState | undefined {
|
|
9
|
+
try {
|
|
10
|
+
const value = window.localStorage.getItem(storageKey);
|
|
11
|
+
if (!value) return undefined;
|
|
12
|
+
const parsed: unknown = JSON.parse(value);
|
|
13
|
+
if (!parsed || typeof parsed !== 'object') return undefined;
|
|
14
|
+
const candidate = parsed as Record<string, unknown>;
|
|
15
|
+
if ((candidate.format !== 'csv' && candidate.format !== 'json') || typeof candidate.text !== 'string') return undefined;
|
|
16
|
+
return { format: candidate.format, text: candidate.text };
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function saveBudgetState(state: SavedBudgetState): void {
|
|
23
|
+
try {
|
|
24
|
+
window.localStorage.setItem(storageKey, JSON.stringify(state));
|
|
25
|
+
} catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface PublicBudgetUI {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
uploadLabel: string;
|
|
4
|
+
uploadHelp: string;
|
|
5
|
+
loadExample: string;
|
|
6
|
+
supportedFormat: string;
|
|
7
|
+
tableHeading: string;
|
|
8
|
+
categoryLabel: string;
|
|
9
|
+
amountLabel: string;
|
|
10
|
+
populationLabel: string;
|
|
11
|
+
sourceRowsLabel: string;
|
|
12
|
+
analyzeEmpty: string;
|
|
13
|
+
resultHeading: string;
|
|
14
|
+
totalLabel: string;
|
|
15
|
+
perCapitaLabel: string;
|
|
16
|
+
variationLabel: string;
|
|
17
|
+
topCategoryLabel: string;
|
|
18
|
+
shareLabel: string;
|
|
19
|
+
sourceLabel: string;
|
|
20
|
+
noData: string;
|
|
21
|
+
warningsHeading: string;
|
|
22
|
+
errorsHeading: string;
|
|
23
|
+
methodHeading: string;
|
|
24
|
+
methodText: string;
|
|
25
|
+
limitsHeading: string;
|
|
26
|
+
limitsText: string;
|
|
27
|
+
edgeCasesHeading: string;
|
|
28
|
+
edgeCasesText: string;
|
|
29
|
+
downloadCsv: string;
|
|
30
|
+
printAction: string;
|
|
31
|
+
rowsLabel: string;
|
|
32
|
+
categoriesLabel: string;
|
|
33
|
+
positiveTotalLabel: string;
|
|
34
|
+
previousTotalLabel: string;
|
|
35
|
+
invalidFile: string;
|
|
36
|
+
fileReadError: string;
|
|
37
|
+
exampleName: string;
|
|
38
|
+
noPreviousData: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ui: PublicBudgetUI = {
|
|
42
|
+
uploadLabel: 'Drop a budget CSV or JSON file here',
|
|
43
|
+
uploadHelp: 'Required fields: category and amount. Optional fields: population, period, priorAmount, currency, and parentCategory.',
|
|
44
|
+
loadExample: 'Load example budget',
|
|
45
|
+
supportedFormat: 'CSV or JSON, processed only in this browser',
|
|
46
|
+
tableHeading: 'Source rows and category totals',
|
|
47
|
+
categoryLabel: 'Category',
|
|
48
|
+
amountLabel: 'Amount',
|
|
49
|
+
populationLabel: 'Population',
|
|
50
|
+
sourceRowsLabel: 'Source rows',
|
|
51
|
+
analyzeEmpty: 'Choose a file or load the example to inspect a budget.',
|
|
52
|
+
resultHeading: 'Budget landscape',
|
|
53
|
+
totalLabel: 'Total spending',
|
|
54
|
+
perCapitaLabel: 'Spending per person',
|
|
55
|
+
variationLabel: 'Change from prior period',
|
|
56
|
+
topCategoryLabel: 'Largest category',
|
|
57
|
+
shareLabel: 'Share of total',
|
|
58
|
+
sourceLabel: 'Source rows',
|
|
59
|
+
noData: 'No valid budget rows are available yet.',
|
|
60
|
+
warningsHeading: 'Warnings to review',
|
|
61
|
+
errorsHeading: 'Rows not included',
|
|
62
|
+
methodHeading: 'Method applied',
|
|
63
|
+
methodText: 'The tool validates the required fields, preserves each source row, groups exact category paths, and calculates totals from the normalized amounts. Category share is amount divided by the total when the total is positive. Per capita spending divides the total by the first consistent positive population denominator. Period change is (current minus prior) divided by prior, and is omitted when the prior amount is zero.',
|
|
64
|
+
limitsHeading: 'What this tool does not do',
|
|
65
|
+
limitsText: 'It does not prove that a budget is complete, comparable, legal, efficient, fair, unbiased, or correctly classified. It does not audit the source, infer policy quality, reconcile currencies, or identify whether a row is an appropriation, obligation, payment, or forecast.',
|
|
66
|
+
edgeCasesHeading: 'Edge cases and data warnings',
|
|
67
|
+
edgeCasesText: 'Mixed currencies, changing populations, duplicate categories, negative adjustments, parent rows beside child rows, locale-specific number formats, and zero prior amounts need human review. The browser keeps the original row number so every aggregate can be traced back to its input.',
|
|
68
|
+
downloadCsv: 'Download reviewed table',
|
|
69
|
+
printAction: 'Print analysis',
|
|
70
|
+
rowsLabel: 'valid rows',
|
|
71
|
+
categoriesLabel: 'categories',
|
|
72
|
+
positiveTotalLabel: 'positive amounts visualized',
|
|
73
|
+
previousTotalLabel: 'Previous total',
|
|
74
|
+
invalidFile: 'The file could not be understood as a non-empty CSV or JSON table.',
|
|
75
|
+
fileReadError: 'The file could not be read in this browser.',
|
|
76
|
+
exampleName: 'Community budget sample',
|
|
77
|
+
noPreviousData: 'No comparable prior-period amount was supplied.',
|
|
78
|
+
};
|
package/src/tools.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { ELECTION_SEAT_APPORTIONMENT_CALCULATOR_TOOL } from './tool/election-sea
|
|
|
6
6
|
import { REFERENDUM_THRESHOLD_CALCULATOR_TOOL } from './tool/referendum-threshold-calculator';
|
|
7
7
|
import { VOTE_SEAT_DISPROPORTIONALITY_ANALYZER_TOOL } from './tool/vote-seat-disproportionality-analyzer';
|
|
8
8
|
import { PARTICIPATORY_BUDGET_ALLOCATOR_TOOL } from './tool/participatory-budget-allocator';
|
|
9
|
+
import { PUBLIC_BUDGET_ANALYZER_TOOL } from './tool/public-budget-analyzer';
|
|
9
10
|
|
|
10
11
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
11
12
|
COALITION_MAJORITY_CALCULATOR_TOOL,
|
|
@@ -14,4 +15,5 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
14
15
|
REFERENDUM_THRESHOLD_CALCULATOR_TOOL,
|
|
15
16
|
VOTE_SEAT_DISPROPORTIONALITY_ANALYZER_TOOL,
|
|
16
17
|
PARTICIPATORY_BUDGET_ALLOCATOR_TOOL,
|
|
18
|
+
PUBLIC_BUDGET_ANALYZER_TOOL,
|
|
17
19
|
];
|