@jjlmoya/utils-civic 1.2.0 → 1.3.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +2 -0
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +1 -1
  6. package/src/tool/coalition-majority-calculator/bibliography.astro +6 -0
  7. package/src/tool/coalition-majority-calculator/bibliography.ts +12 -0
  8. package/src/tool/coalition-majority-calculator/coalition-majority-calculator.css +467 -0
  9. package/src/tool/coalition-majority-calculator/component.astro +62 -0
  10. package/src/tool/coalition-majority-calculator/contract.test.ts +14 -0
  11. package/src/tool/coalition-majority-calculator/controller.ts +89 -0
  12. package/src/tool/coalition-majority-calculator/dom-views.ts +145 -0
  13. package/src/tool/coalition-majority-calculator/entry.ts +28 -0
  14. package/src/tool/coalition-majority-calculator/evaluator.ts +13 -0
  15. package/src/tool/coalition-majority-calculator/i18n/de.ts +51 -0
  16. package/src/tool/coalition-majority-calculator/i18n/en.ts +74 -0
  17. package/src/tool/coalition-majority-calculator/i18n/es.ts +51 -0
  18. package/src/tool/coalition-majority-calculator/i18n/fr.ts +12 -0
  19. package/src/tool/coalition-majority-calculator/i18n/id.ts +45 -0
  20. package/src/tool/coalition-majority-calculator/i18n/it.ts +12 -0
  21. package/src/tool/coalition-majority-calculator/i18n/ja.ts +21 -0
  22. package/src/tool/coalition-majority-calculator/i18n/ko.ts +21 -0
  23. package/src/tool/coalition-majority-calculator/i18n/nl.ts +12 -0
  24. package/src/tool/coalition-majority-calculator/i18n/pl.ts +12 -0
  25. package/src/tool/coalition-majority-calculator/i18n/pt.ts +12 -0
  26. package/src/tool/coalition-majority-calculator/i18n/ru.ts +12 -0
  27. package/src/tool/coalition-majority-calculator/i18n/sv.ts +12 -0
  28. package/src/tool/coalition-majority-calculator/i18n/tr.ts +12 -0
  29. package/src/tool/coalition-majority-calculator/i18n/zh.ts +12 -0
  30. package/src/tool/coalition-majority-calculator/index.ts +11 -0
  31. package/src/tool/coalition-majority-calculator/logic.test.ts +60 -0
  32. package/src/tool/coalition-majority-calculator/logic.ts +146 -0
  33. package/src/tool/coalition-majority-calculator/seo.astro +12 -0
  34. package/src/tool/coalition-majority-calculator/storage.ts +32 -0
  35. package/src/tool/coalition-majority-calculator/ui.ts +80 -0
  36. package/src/tools.ts +2 -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.3.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,10 +1,11 @@
1
+ import { coalitionMajorityCalculator } from '../tool/coalition-majority-calculator/entry';
1
2
  import { electionSeatApportionmentCalculator } from '../tool/election-seat-apportionment-calculator/entry';
2
3
  import { voteSeatDisproportionalityAnalyzer } from '../tool/vote-seat-disproportionality-analyzer/entry';
3
4
  import type { CategoryLocaleContent, KnownLocale } from '../types';
4
5
 
5
6
  export const civicCategory = {
6
7
  icon: 'mdi:bank-outline',
7
- tools: [electionSeatApportionmentCalculator, voteSeatDisproportionalityAnalyzer],
8
+ tools: [coalitionMajorityCalculator, electionSeatApportionmentCalculator, voteSeatDisproportionalityAnalyzer],
8
9
  i18n: {
9
10
  en: () => import('./i18n/en').then((m) => m.content),
10
11
  de: () => import('./i18n/de').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -4,10 +4,12 @@ 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';
8
9
  import { voteSeatDisproportionalityAnalyzer } from './tool/vote-seat-disproportionality-analyzer/entry';
9
10
 
10
11
  export const ALL_ENTRIES = [
12
+ coalitionMajorityCalculator,
11
13
  electionSeatApportionmentCalculator,
12
14
  voteSeatDisproportionalityAnalyzer,
13
15
  ];
@@ -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(3);
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(3);
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
+ });