@jjlmoya/utils-civic 1.2.0 → 1.4.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.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +4 -0
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/coalition-majority-calculator/bibliography.astro +6 -0
  8. package/src/tool/coalition-majority-calculator/bibliography.ts +12 -0
  9. package/src/tool/coalition-majority-calculator/coalition-majority-calculator.css +467 -0
  10. package/src/tool/coalition-majority-calculator/component.astro +62 -0
  11. package/src/tool/coalition-majority-calculator/contract.test.ts +14 -0
  12. package/src/tool/coalition-majority-calculator/controller.ts +89 -0
  13. package/src/tool/coalition-majority-calculator/dom-views.ts +145 -0
  14. package/src/tool/coalition-majority-calculator/entry.ts +28 -0
  15. package/src/tool/coalition-majority-calculator/evaluator.ts +13 -0
  16. package/src/tool/coalition-majority-calculator/i18n/de.ts +51 -0
  17. package/src/tool/coalition-majority-calculator/i18n/en.ts +74 -0
  18. package/src/tool/coalition-majority-calculator/i18n/es.ts +51 -0
  19. package/src/tool/coalition-majority-calculator/i18n/fr.ts +12 -0
  20. package/src/tool/coalition-majority-calculator/i18n/id.ts +45 -0
  21. package/src/tool/coalition-majority-calculator/i18n/it.ts +12 -0
  22. package/src/tool/coalition-majority-calculator/i18n/ja.ts +21 -0
  23. package/src/tool/coalition-majority-calculator/i18n/ko.ts +21 -0
  24. package/src/tool/coalition-majority-calculator/i18n/nl.ts +12 -0
  25. package/src/tool/coalition-majority-calculator/i18n/pl.ts +12 -0
  26. package/src/tool/coalition-majority-calculator/i18n/pt.ts +12 -0
  27. package/src/tool/coalition-majority-calculator/i18n/ru.ts +12 -0
  28. package/src/tool/coalition-majority-calculator/i18n/sv.ts +12 -0
  29. package/src/tool/coalition-majority-calculator/i18n/tr.ts +12 -0
  30. package/src/tool/coalition-majority-calculator/i18n/zh.ts +12 -0
  31. package/src/tool/coalition-majority-calculator/index.ts +11 -0
  32. package/src/tool/coalition-majority-calculator/logic.test.ts +60 -0
  33. package/src/tool/coalition-majority-calculator/logic.ts +146 -0
  34. package/src/tool/coalition-majority-calculator/seo.astro +12 -0
  35. package/src/tool/coalition-majority-calculator/storage.ts +32 -0
  36. package/src/tool/coalition-majority-calculator/ui.ts +80 -0
  37. package/src/tool/referendum-threshold-calculator/bibliography.astro +6 -0
  38. package/src/tool/referendum-threshold-calculator/bibliography.ts +6 -0
  39. package/src/tool/referendum-threshold-calculator/component.astro +117 -0
  40. package/src/tool/referendum-threshold-calculator/controller.ts +141 -0
  41. package/src/tool/referendum-threshold-calculator/dom-views.ts +71 -0
  42. package/src/tool/referendum-threshold-calculator/entry.ts +27 -0
  43. package/src/tool/referendum-threshold-calculator/evaluator.ts +19 -0
  44. package/src/tool/referendum-threshold-calculator/i18n/de.ts +56 -0
  45. package/src/tool/referendum-threshold-calculator/i18n/en.ts +79 -0
  46. package/src/tool/referendum-threshold-calculator/i18n/es.ts +56 -0
  47. package/src/tool/referendum-threshold-calculator/i18n/fr.ts +56 -0
  48. package/src/tool/referendum-threshold-calculator/i18n/id.ts +56 -0
  49. package/src/tool/referendum-threshold-calculator/i18n/it.ts +56 -0
  50. package/src/tool/referendum-threshold-calculator/i18n/ja.ts +56 -0
  51. package/src/tool/referendum-threshold-calculator/i18n/ko.ts +56 -0
  52. package/src/tool/referendum-threshold-calculator/i18n/nl.ts +56 -0
  53. package/src/tool/referendum-threshold-calculator/i18n/pl.ts +56 -0
  54. package/src/tool/referendum-threshold-calculator/i18n/pt.ts +56 -0
  55. package/src/tool/referendum-threshold-calculator/i18n/ru.ts +56 -0
  56. package/src/tool/referendum-threshold-calculator/i18n/sv.ts +56 -0
  57. package/src/tool/referendum-threshold-calculator/i18n/tr.ts +56 -0
  58. package/src/tool/referendum-threshold-calculator/i18n/zh.ts +56 -0
  59. package/src/tool/referendum-threshold-calculator/index.ts +11 -0
  60. package/src/tool/referendum-threshold-calculator/logic.test.ts +69 -0
  61. package/src/tool/referendum-threshold-calculator/logic.ts +120 -0
  62. package/src/tool/referendum-threshold-calculator/referendum-threshold-calculator.css +640 -0
  63. package/src/tool/referendum-threshold-calculator/seo.astro +14 -0
  64. package/src/tool/referendum-threshold-calculator/sharing.test.ts +23 -0
  65. package/src/tool/referendum-threshold-calculator/sharing.ts +42 -0
  66. package/src/tool/referendum-threshold-calculator/storage.ts +39 -0
  67. package/src/tool/referendum-threshold-calculator/ui.ts +126 -0
  68. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-civic",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,10 +1,12 @@
1
+ import { coalitionMajorityCalculator } from '../tool/coalition-majority-calculator/entry';
1
2
  import { electionSeatApportionmentCalculator } from '../tool/election-seat-apportionment-calculator/entry';
3
+ import { referendumThresholdCalculator } from '../tool/referendum-threshold-calculator/entry';
2
4
  import { voteSeatDisproportionalityAnalyzer } from '../tool/vote-seat-disproportionality-analyzer/entry';
3
5
  import type { CategoryLocaleContent, KnownLocale } from '../types';
4
6
 
5
7
  export const civicCategory = {
6
8
  icon: 'mdi:bank-outline',
7
- tools: [electionSeatApportionmentCalculator, voteSeatDisproportionalityAnalyzer],
9
+ tools: [coalitionMajorityCalculator, electionSeatApportionmentCalculator, referendumThresholdCalculator, voteSeatDisproportionalityAnalyzer],
8
10
  i18n: {
9
11
  en: () => import('./i18n/en').then((m) => m.content),
10
12
  de: () => import('./i18n/de').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -4,10 +4,14 @@ export { voteSeatDisproportionalityAnalyzer } from './tool/vote-seat-disproporti
4
4
  export type { VoteSeatUI, VoteSeatLocaleContent } from './tool/vote-seat-disproportionality-analyzer/entry';
5
5
  export { civicCategory } from './category';
6
6
 
7
+ import { coalitionMajorityCalculator } from './tool/coalition-majority-calculator/entry';
7
8
  import { electionSeatApportionmentCalculator } from './tool/election-seat-apportionment-calculator/entry';
9
+ import { referendumThresholdCalculator } from './tool/referendum-threshold-calculator/entry';
8
10
  import { voteSeatDisproportionalityAnalyzer } from './tool/vote-seat-disproportionality-analyzer/entry';
9
11
 
10
12
  export const ALL_ENTRIES = [
13
+ coalitionMajorityCalculator,
11
14
  electionSeatApportionmentCalculator,
15
+ referendumThresholdCalculator,
12
16
  voteSeatDisproportionalityAnalyzer,
13
17
  ];
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export { civicCategory } from './category';
2
2
  export const civicCategorySEO = () => import('./category/CivicCategorySEO.astro').then((m) => m.default);
3
3
 
4
4
  export { electionSeatApportionmentCalculator, ELECTION_SEAT_APPORTIONMENT_CALCULATOR_TOOL } from './tool/election-seat-apportionment-calculator';
5
+ export { referendumThresholdCalculator, REFERENDUM_THRESHOLD_CALCULATOR_TOOL } from './tool/referendum-threshold-calculator';
5
6
  export { voteSeatDisproportionalityAnalyzer, VOTE_SEAT_DISPROPORTIONALITY_ANALYZER_TOOL } from './tool/vote-seat-disproportionality-analyzer';
6
7
 
7
8
  export type {
@@ -18,6 +18,6 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
 
20
20
  it('all tools registered', () => {
21
- expect(ALL_TOOLS.length).toBe(2);
21
+ expect(ALL_TOOLS.length).toBe(4);
22
22
  });
23
23
  });
@@ -5,7 +5,7 @@ import { civicCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(2);
8
+ expect(ALL_TOOLS.length).toBe(4);
9
9
  });
10
10
 
11
11
  it('civicCategory should be defined', () => {
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+ ---
5
+
6
+ <Bibliography links={bibliography} />
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Deutscher Bundestag, Koalition',
6
+ url: 'https://www.bundestag.de/services/glossar/glossar/K/koalition-245478',
7
+ },
8
+ {
9
+ name: 'Madeleine O. Hosli, The New Voting Weights in the Council of the European Union',
10
+ url: 'https://ciaotest.cc.columbia.edu/isa/hom01/',
11
+ },
12
+ ];
@@ -0,0 +1,467 @@
1
+ :root {
2
+ --n-paper: #f5f1e8;
3
+ --n-card: #fffdf8;
4
+ --n-ink: #17202a;
5
+ --n-muted: #5e6872;
6
+ --n-line: #d8d2c8;
7
+ --n-accent: #a44a2f;
8
+ --n-accent-soft: #f0d7c8;
9
+ --n-orbit: #d1a23a;
10
+ --n-good: #2f7759;
11
+ --n-warn: #a44a2f;
12
+ }
13
+
14
+ .theme-dark {
15
+ --n-paper: #161817;
16
+ --n-card: #202321;
17
+ --n-ink: #f1eee6;
18
+ --n-muted: #b5b9b2;
19
+ --n-line: #454945;
20
+ --n-accent: #ef8b63;
21
+ --n-accent-soft: #4b3028;
22
+ --n-orbit: #e0bd57;
23
+ --n-good: #79bd96;
24
+ --n-warn: #ef8b63;
25
+ }
26
+
27
+ .coalition-tool {
28
+ --n-surface: var(--n-card);
29
+ --n-field: var(--n-paper);
30
+
31
+ background: var(--n-paper);
32
+ border: 1px solid var(--n-line);
33
+ color: var(--n-ink);
34
+ margin: 0 auto;
35
+ max-width: 1120px;
36
+ overflow: hidden;
37
+ }
38
+
39
+ .coalition-workspace {
40
+ display: grid;
41
+ grid-template-columns: minmax(300px, .8fr) minmax(400px, 1.2fr);
42
+ }
43
+
44
+ .coalition-controls,
45
+ .coalition-result {
46
+ min-width: 0;
47
+ padding: 1.4rem;
48
+ }
49
+
50
+ .coalition-controls {
51
+ border-right: 1px solid var(--n-line);
52
+ }
53
+
54
+ .coalition-result {
55
+ background: var(--n-surface);
56
+ }
57
+
58
+ .section-kicker {
59
+ color: var(--n-accent);
60
+ font-size: .75rem;
61
+ font-weight: 800;
62
+ letter-spacing: .11em;
63
+ text-transform: uppercase;
64
+ }
65
+
66
+ .party-list {
67
+ display: grid;
68
+ gap: .7rem;
69
+ margin: 1rem 0;
70
+ }
71
+
72
+ .party-row {
73
+ align-items: end;
74
+ display: grid;
75
+ gap: .55rem;
76
+ grid-template-columns: minmax(0, 1fr) 6rem 2rem;
77
+ }
78
+
79
+ label {
80
+ color: var(--n-muted);
81
+ display: grid;
82
+ font-size: .76rem;
83
+ font-weight: 700;
84
+ gap: .3rem;
85
+ }
86
+
87
+ input {
88
+ background: var(--n-field);
89
+ border: 1px solid var(--n-line);
90
+ border-radius: .35rem;
91
+ box-sizing: border-box;
92
+ color: var(--n-ink);
93
+ font: inherit;
94
+ min-height: 2.55rem;
95
+ padding: .5rem .6rem;
96
+ width: 100%;
97
+ }
98
+
99
+ input:focus-visible,
100
+ button:focus-visible {
101
+ outline: 3px solid var(--n-orbit);
102
+ outline-offset: 2px;
103
+ }
104
+
105
+ .icon-button,
106
+ .secondary-button,
107
+ .text-button {
108
+ border: 0;
109
+ cursor: pointer;
110
+ font: inherit;
111
+ }
112
+
113
+ .icon-button {
114
+ background: transparent;
115
+ color: var(--n-muted);
116
+ font-size: 1.1rem;
117
+ min-height: 2.55rem;
118
+ }
119
+
120
+ .control-actions {
121
+ align-items: center;
122
+ display: flex;
123
+ gap: .8rem;
124
+ margin: .8rem 0 1.4rem;
125
+ }
126
+
127
+ .secondary-button {
128
+ background: var(--n-accent-soft);
129
+ border-radius: 999px;
130
+ color: var(--n-ink);
131
+ padding: .65rem .9rem;
132
+ }
133
+
134
+ .text-button {
135
+ background: transparent;
136
+ color: var(--n-accent);
137
+ padding: .5rem 0;
138
+ text-decoration: underline;
139
+ }
140
+
141
+ .threshold-control {
142
+ border-top: 1px solid var(--n-line);
143
+ padding-top: 1rem;
144
+ }
145
+
146
+ .threshold-help,
147
+ .threshold-control small,
148
+ .control-note,
149
+ .limit-note,
150
+ .method-note,
151
+ .scene-empty {
152
+ color: var(--n-muted);
153
+ }
154
+
155
+ .control-note,
156
+ .limit-note {
157
+ font-size: .88rem;
158
+ line-height: 1.5;
159
+ }
160
+
161
+ .limit-note {
162
+ border-left: 3px solid var(--n-orbit);
163
+ padding-left: .7rem;
164
+ }
165
+
166
+ .result-heading {
167
+ align-items: baseline;
168
+ display: flex;
169
+ justify-content: space-between;
170
+ gap: 1rem;
171
+ }
172
+
173
+ .result-heading p {
174
+ color: var(--n-good);
175
+ font-size: .86rem;
176
+ margin: 0;
177
+ text-align: right;
178
+ }
179
+
180
+ .result-heading p[data-tone="warn"] {
181
+ color: var(--n-warn);
182
+ }
183
+
184
+ .scene-wrap {
185
+ align-items: center;
186
+ border-bottom: 1px solid var(--n-line);
187
+ display: flex;
188
+ justify-content: center;
189
+ min-height: 300px;
190
+ }
191
+
192
+ .scene-empty {
193
+ font-size: .9rem;
194
+ line-height: 1.5;
195
+ max-width: 20rem;
196
+ text-align: center;
197
+ }
198
+
199
+ .coalition-orbit {
200
+ fill: none;
201
+ stroke: var(--n-orbit);
202
+ stroke-dasharray: 3 7;
203
+ stroke-width: 2;
204
+ }
205
+
206
+ .coalition-core {
207
+ fill: var(--n-accent-soft);
208
+ stroke: var(--n-accent);
209
+ stroke-width: 2;
210
+ }
211
+
212
+ .coalition-link {
213
+ stroke: var(--n-line);
214
+ stroke-width: 1.2;
215
+ }
216
+
217
+ .coalition-node circle {
218
+ fill: var(--n-field);
219
+ stroke: var(--n-muted);
220
+ stroke-width: 1.5;
221
+ }
222
+
223
+ .coalition-winning-link {
224
+ opacity: .42;
225
+ stroke: var(--n-accent);
226
+ stroke-width: 2.4;
227
+ }
228
+
229
+ .coalition-node.is-pivotal circle {
230
+ fill: var(--n-accent-soft);
231
+ stroke: var(--n-accent);
232
+ stroke-width: 3;
233
+ }
234
+
235
+ .coalition-core-label,
236
+ .coalition-node-name,
237
+ .coalition-node-seats,
238
+ .coalition-core-caption {
239
+ fill: var(--n-ink);
240
+ }
241
+
242
+ .coalition-core-label {
243
+ font-size: 1.25rem;
244
+ font-weight: 800;
245
+ }
246
+
247
+ .coalition-core-caption,
248
+ .coalition-node-seats {
249
+ fill: var(--n-muted);
250
+ font-size: .62rem;
251
+ }
252
+
253
+ .coalition-node-name {
254
+ font-size: .68rem;
255
+ font-weight: 800;
256
+ text-anchor: middle;
257
+ }
258
+
259
+ .coalition-node-seats {
260
+ text-anchor: middle;
261
+ }
262
+
263
+ .coalition-stats {
264
+ display: grid;
265
+ gap: .7rem;
266
+ grid-template-columns: repeat(2, 1fr);
267
+ margin: 1.2rem 0;
268
+ }
269
+
270
+ .coalition-stat {
271
+ border-left: 2px solid var(--n-orbit);
272
+ padding-left: .65rem;
273
+ }
274
+
275
+ .coalition-stat dt {
276
+ color: var(--n-muted);
277
+ font-size: .72rem;
278
+ }
279
+
280
+ .coalition-stat dd {
281
+ font-size: 1.1rem;
282
+ font-weight: 800;
283
+ margin: .15rem 0 0;
284
+ }
285
+
286
+ .pivots,
287
+ .coalition-table-wrap {
288
+ border-top: 1px solid var(--n-line);
289
+ padding-top: 1rem;
290
+ }
291
+
292
+ .pivots h3,
293
+ .coalition-table-wrap h3 {
294
+ font-size: .9rem;
295
+ margin: 0;
296
+ }
297
+
298
+ .table-heading {
299
+ align-items: end;
300
+ display: flex;
301
+ gap: 1rem;
302
+ justify-content: space-between;
303
+ margin-bottom: .65rem;
304
+ }
305
+
306
+ .table-filter {
307
+ align-items: center;
308
+ display: flex;
309
+ flex-direction: row;
310
+ font-size: .7rem;
311
+ gap: .4rem;
312
+ }
313
+
314
+ .table-filter select {
315
+ background: var(--n-field);
316
+ border: 1px solid var(--n-line);
317
+ border-radius: .35rem;
318
+ color: var(--n-ink);
319
+ font: inherit;
320
+ min-height: 2rem;
321
+ padding: .25rem .4rem;
322
+ }
323
+
324
+ .pivots ul {
325
+ display: grid;
326
+ gap: .45rem;
327
+ grid-template-columns: repeat(2, 1fr);
328
+ list-style: none;
329
+ margin: 0 0 1.2rem;
330
+ padding: 0;
331
+ }
332
+
333
+ .pivots li {
334
+ display: grid;
335
+ grid-template-columns: 1fr auto;
336
+ gap: .2rem .5rem;
337
+ }
338
+
339
+ .pivots small {
340
+ color: var(--n-muted);
341
+ font-size: .7rem;
342
+ grid-column: 1 / -1;
343
+ }
344
+
345
+ .table-scroll {
346
+ overflow-x: auto;
347
+ }
348
+
349
+ table {
350
+ border-collapse: collapse;
351
+ font-size: .78rem;
352
+ width: 100%;
353
+ }
354
+
355
+ th,
356
+ td {
357
+ border-bottom: 1px solid var(--n-line);
358
+ padding: .55rem .4rem;
359
+ text-align: left;
360
+ vertical-align: top;
361
+ }
362
+
363
+ thead th {
364
+ color: var(--n-muted);
365
+ font-size: .7rem;
366
+ position: sticky;
367
+ text-transform: uppercase;
368
+ top: 0;
369
+ }
370
+
371
+ .row-badge {
372
+ color: var(--n-muted);
373
+ display: block;
374
+ font-size: .65rem;
375
+ font-weight: 800;
376
+ margin-bottom: .15rem;
377
+ text-transform: uppercase;
378
+ }
379
+
380
+ .row-badge.is-minimal {
381
+ color: var(--n-accent);
382
+ }
383
+
384
+ .method-note {
385
+ border-top: 1px solid var(--n-line);
386
+ display: grid;
387
+ font-size: .8rem;
388
+ gap: 1rem;
389
+ grid-template-columns: repeat(3, 1fr);
390
+ line-height: 1.45;
391
+ padding: 1rem 1.4rem;
392
+ }
393
+
394
+ .method-note section {
395
+ display: grid;
396
+ gap: .35rem;
397
+ }
398
+
399
+ .method-note strong {
400
+ color: var(--n-ink);
401
+ }
402
+
403
+ .coalition-seo,
404
+ .coalition-bibliography {
405
+ color: var(--n-ink);
406
+ margin: 2rem auto;
407
+ max-width: 840px;
408
+ }
409
+
410
+ .coalition-seo h2,
411
+ .coalition-bibliography h2 {
412
+ margin-top: 2rem;
413
+ }
414
+
415
+ .coalition-seo p,
416
+ .coalition-seo li,
417
+ .coalition-bibliography li {
418
+ line-height: 1.65;
419
+ }
420
+
421
+ .coalition-seo aside {
422
+ border-left: 3px solid var(--n-orbit);
423
+ margin: 1.2rem 0;
424
+ padding: .2rem 1rem;
425
+ }
426
+
427
+ .coalition-seo table {
428
+ margin: 1rem 0;
429
+ }
430
+
431
+ .coalition-bibliography a {
432
+ color: var(--n-accent);
433
+ }
434
+
435
+ @media (max-width: 760px) {
436
+ .coalition-workspace {
437
+ grid-template-columns: 1fr;
438
+ }
439
+
440
+ .coalition-controls {
441
+ border-bottom: 1px solid var(--n-line);
442
+ border-right: 0;
443
+ }
444
+
445
+ .coalition-result {
446
+ padding-top: 1.1rem;
447
+ }
448
+ }
449
+
450
+ @media (max-width: 460px) {
451
+ .coalition-controls,
452
+ .coalition-result,
453
+ .method-note {
454
+ padding-left: .9rem;
455
+ padding-right: .9rem;
456
+ }
457
+
458
+ .party-row {
459
+ grid-template-columns: minmax(0, 1fr) 5rem 1.7rem;
460
+ }
461
+
462
+ .pivots ul {
463
+ grid-template-columns: 1fr;
464
+ }
465
+
466
+ .method-note { grid-template-columns: 1fr; }
467
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ import './coalition-majority-calculator.css';
3
+ import type { CoalitionMajorityUI } from './ui';
4
+
5
+ interface Props {
6
+ ui: CoalitionMajorityUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ const initialParties = [
11
+ { name: 'Cedar', seats: 38 },
12
+ { name: 'River', seats: 27 },
13
+ { name: 'North Star', seats: 19 },
14
+ { name: 'Harbor', seats: 11 },
15
+ ];
16
+ const initialThreshold = 49;
17
+ const config = JSON.stringify({ ui }).replace(/</g, '\\u003c');
18
+ ---
19
+
20
+ <div class="coalition-tool" data-coalition-tool>
21
+ <div class="coalition-workspace">
22
+ <section class="coalition-controls" aria-labelledby="coalition-controls-title">
23
+ <div class="section-kicker" id="coalition-controls-title">{ui.partyTable}</div>
24
+ <div class="party-list" data-parties>
25
+ {initialParties.map((party, index) => <div class="party-row" data-party-row>
26
+ <label><span>{ui.partyName} {index + 1}</span><input data-party-name type="text" value={party.name} /></label>
27
+ <label><span>{ui.partySeats}</span><input data-party-seats type="number" min="0" step="1" value={party.seats} /></label>
28
+ <button type="button" class="icon-button" data-remove-party aria-label={`${ui.removeParty} ${index + 1}`}>x</button>
29
+ </div>)}
30
+ </div>
31
+ <div class="control-actions">
32
+ <button type="button" class="secondary-button" data-add-party>{ui.addParty}</button>
33
+ <button type="button" class="text-button" data-reset>{ui.reset}</button>
34
+ </div>
35
+ <label class="threshold-control"><span>{ui.majorityThreshold}</span><input data-threshold type="number" min="1" step="1" value={initialThreshold} aria-describedby="threshold-hint" /></label>
36
+ <small class="threshold-help" id="threshold-hint">{ui.thresholdHint}</small>
37
+ <p class="control-note">{ui.explanation}</p>
38
+ <p class="limit-note">{ui.maximumParties}</p>
39
+ </section>
40
+
41
+ <section class="coalition-result" aria-labelledby="coalition-result-title">
42
+ <div class="result-heading"><div class="section-kicker" id="coalition-result-title">{ui.resultTitle}</div><p data-coalition-status aria-label={ui.winningCoalitions} role="status"></p></div>
43
+ <div class="scene-wrap" data-coalition-scene aria-label={ui.coalitionSceneLabel}><div class="scene-empty">{ui.emptyState}</div></div>
44
+ <dl class="coalition-stats" data-coalition-stats></dl>
45
+ <section class="pivots" aria-labelledby="pivots-title"><h3 id="pivots-title">{ui.pivotalParty}</h3><ul data-coalition-pivots></ul></section>
46
+ <section class="coalition-table-wrap" aria-labelledby="coalition-table-title"><div class="table-heading"><h3 id="coalition-table-title">{ui.partyTable}</h3><label class="table-filter"><span>{ui.filterCoalitions}</span><select data-coalition-filter><option value="all">{ui.allCoalitions}</option><option value="minimal">{ui.minimalCoalition}</option><option value="surplus">{ui.surplusCoalition}</option></select></label></div><div class="table-scroll"><table><thead><tr><th scope="col">{ui.partyTable}</th><th scope="col">{ui.seats}</th><th scope="col">{ui.surplus}</th><th scope="col">{ui.redundantMembers}</th></tr></thead><tbody data-coalition-rows></tbody></table></div></section>
47
+ </section>
48
+ </div>
49
+ <div class="method-note">
50
+ <section><strong>{ui.methodTitle}</strong><span>{ui.methodText}</span></section>
51
+ <section><strong>{ui.notDoTitle}</strong><span>{ui.notDoText}</span></section>
52
+ <section><strong>{ui.edgeCasesTitle}</strong><span>{ui.edgeCasesText}</span></section>
53
+ </div>
54
+ </div>
55
+
56
+ <script is:inline type="application/json" data-coalition-config set:html={config}></script>
57
+ <script>
58
+ import { mountCoalitionTool } from './controller';
59
+ const root = document.querySelector<HTMLElement>('[data-coalition-tool]');
60
+ const configNode = document.querySelector<HTMLScriptElement>('[data-coalition-config]');
61
+ if (root && configNode?.textContent) mountCoalitionTool(root, JSON.parse(configNode.textContent));
62
+ </script>
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { coalitionMajorityCalculator } from './entry';
3
+ import { COALITION_MAJORITY_CALCULATOR_TOOL } from './index';
4
+
5
+ describe('coalition majority runtime contract', () => {
6
+ it('exposes the English loader and all page component loaders', async () => {
7
+ const content = await coalitionMajorityCalculator.i18n.en?.();
8
+ expect(content?.slug).toBe('coalition-majority-calculator');
9
+ expect(content?.ui).toBeDefined();
10
+ expect(typeof COALITION_MAJORITY_CALCULATOR_TOOL.Component).toBe('function');
11
+ expect(typeof COALITION_MAJORITY_CALCULATOR_TOOL.SEOComponent).toBe('function');
12
+ expect(typeof COALITION_MAJORITY_CALCULATOR_TOOL.BibliographyComponent).toBe('function');
13
+ });
14
+ });