@jjlmoya/utils-civic 1.3.0 → 1.5.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 (69) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +6 -0
  4. package/src/index.ts +2 -0
  5. package/src/pages/[locale]/[slug].astro +7 -4
  6. package/src/tests/locale_completeness.test.ts +1 -1
  7. package/src/tests/tool_validation.test.ts +1 -1
  8. package/src/tool/civic-priority-matrix/bibliography.astro +6 -0
  9. package/src/tool/civic-priority-matrix/bibliography.ts +12 -0
  10. package/src/tool/civic-priority-matrix/civic-priority-matrix.css +531 -0
  11. package/src/tool/civic-priority-matrix/component.astro +108 -0
  12. package/src/tool/civic-priority-matrix/contract.test.ts +16 -0
  13. package/src/tool/civic-priority-matrix/controller.ts +168 -0
  14. package/src/tool/civic-priority-matrix/dom-views.ts +123 -0
  15. package/src/tool/civic-priority-matrix/entry.ts +28 -0
  16. package/src/tool/civic-priority-matrix/evaluator.ts +13 -0
  17. package/src/tool/civic-priority-matrix/i18n/de.ts +52 -0
  18. package/src/tool/civic-priority-matrix/i18n/en.ts +73 -0
  19. package/src/tool/civic-priority-matrix/i18n/es.ts +52 -0
  20. package/src/tool/civic-priority-matrix/i18n/fr.ts +50 -0
  21. package/src/tool/civic-priority-matrix/i18n/id.ts +50 -0
  22. package/src/tool/civic-priority-matrix/i18n/it.ts +50 -0
  23. package/src/tool/civic-priority-matrix/i18n/ja.ts +121 -0
  24. package/src/tool/civic-priority-matrix/i18n/ko.ts +121 -0
  25. package/src/tool/civic-priority-matrix/i18n/nl.ts +45 -0
  26. package/src/tool/civic-priority-matrix/i18n/pl.ts +35 -0
  27. package/src/tool/civic-priority-matrix/i18n/pt.ts +32 -0
  28. package/src/tool/civic-priority-matrix/i18n/ru.ts +32 -0
  29. package/src/tool/civic-priority-matrix/i18n/sv.ts +32 -0
  30. package/src/tool/civic-priority-matrix/i18n/tr.ts +32 -0
  31. package/src/tool/civic-priority-matrix/i18n/zh.ts +121 -0
  32. package/src/tool/civic-priority-matrix/index.ts +11 -0
  33. package/src/tool/civic-priority-matrix/logic.test.ts +68 -0
  34. package/src/tool/civic-priority-matrix/logic.ts +124 -0
  35. package/src/tool/civic-priority-matrix/seo.astro +12 -0
  36. package/src/tool/civic-priority-matrix/storage.ts +50 -0
  37. package/src/tool/civic-priority-matrix/ui.ts +96 -0
  38. package/src/tool/referendum-threshold-calculator/bibliography.astro +6 -0
  39. package/src/tool/referendum-threshold-calculator/bibliography.ts +6 -0
  40. package/src/tool/referendum-threshold-calculator/component.astro +117 -0
  41. package/src/tool/referendum-threshold-calculator/controller.ts +141 -0
  42. package/src/tool/referendum-threshold-calculator/dom-views.ts +71 -0
  43. package/src/tool/referendum-threshold-calculator/entry.ts +27 -0
  44. package/src/tool/referendum-threshold-calculator/evaluator.ts +19 -0
  45. package/src/tool/referendum-threshold-calculator/i18n/de.ts +56 -0
  46. package/src/tool/referendum-threshold-calculator/i18n/en.ts +79 -0
  47. package/src/tool/referendum-threshold-calculator/i18n/es.ts +56 -0
  48. package/src/tool/referendum-threshold-calculator/i18n/fr.ts +56 -0
  49. package/src/tool/referendum-threshold-calculator/i18n/id.ts +56 -0
  50. package/src/tool/referendum-threshold-calculator/i18n/it.ts +56 -0
  51. package/src/tool/referendum-threshold-calculator/i18n/ja.ts +56 -0
  52. package/src/tool/referendum-threshold-calculator/i18n/ko.ts +56 -0
  53. package/src/tool/referendum-threshold-calculator/i18n/nl.ts +56 -0
  54. package/src/tool/referendum-threshold-calculator/i18n/pl.ts +56 -0
  55. package/src/tool/referendum-threshold-calculator/i18n/pt.ts +56 -0
  56. package/src/tool/referendum-threshold-calculator/i18n/ru.ts +56 -0
  57. package/src/tool/referendum-threshold-calculator/i18n/sv.ts +56 -0
  58. package/src/tool/referendum-threshold-calculator/i18n/tr.ts +56 -0
  59. package/src/tool/referendum-threshold-calculator/i18n/zh.ts +56 -0
  60. package/src/tool/referendum-threshold-calculator/index.ts +11 -0
  61. package/src/tool/referendum-threshold-calculator/logic.test.ts +69 -0
  62. package/src/tool/referendum-threshold-calculator/logic.ts +120 -0
  63. package/src/tool/referendum-threshold-calculator/referendum-threshold-calculator.css +640 -0
  64. package/src/tool/referendum-threshold-calculator/seo.astro +14 -0
  65. package/src/tool/referendum-threshold-calculator/sharing.test.ts +23 -0
  66. package/src/tool/referendum-threshold-calculator/sharing.ts +42 -0
  67. package/src/tool/referendum-threshold-calculator/storage.ts +39 -0
  68. package/src/tool/referendum-threshold-calculator/ui.ts +126 -0
  69. package/src/tools.ts +4 -0
@@ -0,0 +1,108 @@
1
+ ---
2
+ import './civic-priority-matrix.css';
3
+ import type { CivicPriorityUI } from './ui';
4
+ import { defaultInitiatives } from './storage';
5
+
6
+ interface Props {
7
+ ui: CivicPriorityUI;
8
+ locale: string;
9
+ }
10
+
11
+ const { ui, locale } = Astro.props;
12
+ const exampleNames: Record<string, string[]> = {
13
+ de: ['Barrierefreie Bushaltestellen', 'Kühlzentren', 'Protokolle öffentlicher Sitzungen', 'Reparatur der Parkbeleuchtung'],
14
+ en: ['Accessible bus stops', 'Cooling centres', 'Public meeting transcripts', 'Park lighting repair'],
15
+ es: ['Paradas de autobús accesibles', 'Centros de refrigeración', 'Actas de reuniones públicas', 'Reparación del alumbrado del parque'],
16
+ fr: ['Arrêts de bus accessibles', 'Centres de rafraîchissement', 'Procès-verbaux des réunions publiques', 'Réparation de l\'éclairage du parc'],
17
+ id: ['Halte bus yang mudah diakses', 'Pusat penyejuk', 'Transkrip rapat publik', 'Perbaikan lampu taman'],
18
+ it: ['Fermate accessibili', 'Centri di raffrescamento', 'Verbali delle riunioni pubbliche', 'Riparazione delle luci del parco'],
19
+ ja: ['バリアフリーのバス停', 'クーリングセンター', '公開会議の議事録', '公園照明の修理'],
20
+ ko: ['접근 가능한 버스 정류장', '무더위 쉼터', '공개 회의록', '공원 조명 수리'],
21
+ nl: ['Toegankelijke bushaltes', 'Koelcentra', 'Verslagen van openbare bijeenkomsten', 'Reparatie van parkverlichting'],
22
+ pl: ['Dostępne przystanki autobusowe', 'Centra chłodzenia', 'Protokoły zebrań publicznych', 'Naprawa oświetlenia parku'],
23
+ pt: ['Paragens de autocarro acessíveis', 'Centros de arrefecimento', 'Atas de reuniões públicas', 'Reparação da iluminação do parque'],
24
+ ru: ['Доступные автобусные остановки', 'Центры охлаждения', 'Протоколы публичных собраний', 'Ремонт освещения в парке'],
25
+ sv: ['Tillgängliga busshållplatser', 'Kylcenter', 'Protokoll från offentliga möten', 'Reparation av parkbelysning'],
26
+ tr: ['Erişilebilir otobüs durakları', 'Serinleme merkezleri', 'Açık toplantı tutanakları', 'Park aydınlatması onarımı'],
27
+ zh: ['无障碍公交站', '避暑中心', '公开会议记录', '公园照明维修'],
28
+ };
29
+ const localizedInitiatives = defaultInitiatives.map((item, index) => ({
30
+ ...item,
31
+ name: exampleNames[locale]?.[index] ?? item.name,
32
+ }));
33
+ const stateJson = JSON.stringify({ initiatives: localizedInitiatives }).replace(/</g, '\\u003c');
34
+ const uiJson = JSON.stringify(ui).replace(/</g, '\\u003c');
35
+ ---
36
+
37
+ <section class="civic-priority-tool" data-civic-priority>
38
+ <div class="priority-main-surface">
39
+ <div class="priority-main-flow">
40
+ <div class="priority-control-surface">
41
+ <div class="priority-control-header">
42
+ <div>
43
+ <p class="priority-kicker">{ui.initiativeList}</p>
44
+ <p class="priority-input-hint">{ui.inputHint}</p>
45
+ </div>
46
+ <button class="priority-add" type="button" data-action="add">{ui.addInitiative}</button>
47
+ </div>
48
+ <div class="priority-scale-note">
49
+ <span>{ui.scoreScale}</span>
50
+ <span>{ui.scoreHint}</span>
51
+ </div>
52
+ <div class="priority-initiative-list" data-initiative-list></div>
53
+ <div class="priority-control-footer">
54
+ <button class="priority-reset" type="button" data-action="reset">{ui.reset}</button>
55
+ <p class="priority-form-status" data-priority-status aria-live="polite"></p>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="priority-result-surface">
60
+ <div class="priority-result-heading">
61
+ <div>
62
+ <p class="priority-kicker">{ui.resultTitle}</p>
63
+ <p class="priority-summary" data-priority-summary aria-live="polite">{ui.emptyState}</p>
64
+ </div>
65
+ </div>
66
+ <div class="priority-scene-wrap">
67
+ <div class="priority-scene" data-priority-scene></div>
68
+ <div class="priority-legend" aria-label={ui.matrixLabel}>
69
+ <span><i class="priority-legend-dot priority-dot-plan"></i>{ui.plan}</span>
70
+ <span><i class="priority-legend-dot priority-dot-act"></i>{ui.actNow}</span>
71
+ <span><i class="priority-legend-dot priority-dot-defer"></i>{ui.defer}</span>
72
+ <span><i class="priority-legend-dot priority-dot-quick"></i>{ui.quickResponse}</span>
73
+ </div>
74
+ </div>
75
+ <div class="priority-ranking-block">
76
+ <h2>{ui.rankingTitle}</h2>
77
+ <div data-priority-ranking></div>
78
+ </div>
79
+ <div class="priority-warnings" data-priority-warnings aria-live="polite"></div>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="priority-method-grid">
84
+ <article class="priority-method-card">
85
+ <h2>{ui.methodTitle}</h2>
86
+ <p>{ui.methodText}</p>
87
+ </article>
88
+ <article class="priority-method-card">
89
+ <h2>{ui.notDoTitle}</h2>
90
+ <p>{ui.notDoText}</p>
91
+ </article>
92
+ <article class="priority-method-card">
93
+ <h2>{ui.edgeCasesTitle}</h2>
94
+ <p>{ui.edgeCasesText}</p>
95
+ </article>
96
+ </div>
97
+ </div>
98
+ </section>
99
+
100
+ <script is:inline type="application/json" data-civic-priority-state set:html={stateJson}></script>
101
+ <script is:inline type="application/json" data-civic-priority-ui set:html={uiJson}></script>
102
+ <script>
103
+ import { mountCivicPriorityMatrix } from './controller';
104
+
105
+ const root = document.querySelector<HTMLElement>('[data-civic-priority]');
106
+ const uiScript = document.querySelector<HTMLScriptElement>('[data-civic-priority-ui]');
107
+ if (root && uiScript) mountCivicPriorityMatrix(root, JSON.parse(uiScript.textContent ?? '{}'));
108
+ </script>
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { civicPriorityMatrix } from './entry';
3
+ import { CIVIC_PRIORITY_MATRIX_TOOL } from './index';
4
+
5
+ describe('civic priority matrix runtime contract', () => {
6
+ it('exposes the English loader and page component loaders', async () => {
7
+ const content = await civicPriorityMatrix.i18n.en?.();
8
+
9
+ expect(content?.slug).toBe('civic-priority-matrix');
10
+ expect(content?.ui).toBeDefined();
11
+ expect(content?.seo.length).toBeGreaterThan(0);
12
+ expect(typeof CIVIC_PRIORITY_MATRIX_TOOL.Component).toBe('function');
13
+ expect(typeof CIVIC_PRIORITY_MATRIX_TOOL.SEOComponent).toBe('function');
14
+ expect(typeof CIVIC_PRIORITY_MATRIX_TOOL.BibliographyComponent).toBe('function');
15
+ });
16
+ });
@@ -0,0 +1,168 @@
1
+ import { analyzeInitiatives, type InitiativeInput } from './logic';
2
+ import { renderAnalysis } from './dom-views';
3
+ import { defaultInitiatives, loadCivicPriorityState, saveCivicPriorityState, type CivicPriorityState } from './storage';
4
+ import type { CivicPriorityUI } from './ui';
5
+
6
+ function createId(): string {
7
+ return `initiative-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8
+ }
9
+
10
+ function inputElement(field: string, value: string, ui: CivicPriorityUI): HTMLInputElement {
11
+ const input = document.createElement('input');
12
+ input.className = 'priority-input';
13
+ input.dataset.field = field;
14
+ input.value = value;
15
+ input.setAttribute('aria-label', field === 'name' ? ui.initiative : field);
16
+ if (field !== 'name') {
17
+ input.type = 'number';
18
+ input.min = field === 'effort' ? '0' : '1';
19
+ input.max = '5';
20
+ input.step = '1';
21
+ }
22
+ return input;
23
+ }
24
+
25
+ function createField(label: string, input: HTMLInputElement): HTMLLabelElement {
26
+ const field = document.createElement('label');
27
+ field.className = 'priority-field-control';
28
+ const text = document.createElement('span');
29
+ text.textContent = label;
30
+ field.append(text, input);
31
+ return field;
32
+ }
33
+
34
+ function createRow(item: InitiativeInput, index: number, ui: CivicPriorityUI): HTMLElement {
35
+ const row = document.createElement('div');
36
+ row.className = 'priority-initiative-row';
37
+ row.dataset.initiativeRow = '';
38
+ row.dataset.index = String(index);
39
+ row.dataset.id = item.id;
40
+ row.append(
41
+ createField(ui.initiative, inputElement('name', item.name, ui)),
42
+ createField(ui.impact, inputElement('impact', String(item.impact), ui)),
43
+ createField(ui.urgency, inputElement('urgency', String(item.urgency), ui)),
44
+ createField(ui.effort, inputElement('effort', String(item.effort), ui)),
45
+ );
46
+ const remove = document.createElement('button');
47
+ remove.type = 'button';
48
+ remove.className = 'priority-remove';
49
+ remove.dataset.action = 'remove';
50
+ remove.textContent = '×';
51
+ remove.setAttribute('aria-label', ui.removeInitiative);
52
+ row.appendChild(remove);
53
+ const error = document.createElement('p');
54
+ error.className = 'priority-row-error';
55
+ error.dataset.rowError = '';
56
+ row.appendChild(error);
57
+ return row;
58
+ }
59
+
60
+ function renderRows(list: HTMLElement, initiatives: InitiativeInput[], ui: CivicPriorityUI): void {
61
+ list.replaceChildren(...initiatives.map((item, index) => createRow(item, index, ui)));
62
+ }
63
+
64
+ function readInputs(list: HTMLElement): InitiativeInput[] {
65
+ return Array.from(list.querySelectorAll<HTMLElement>('[data-initiative-row]')).map((row, index) => {
66
+ const read = (field: string): string => row.querySelector<HTMLInputElement>(`[data-field="${field}"]`)?.value ?? '';
67
+ return { id: row.dataset.id ?? `initiative-${index}`, name: read('name'), impact: Number(read('impact')), urgency: Number(read('urgency')), effort: Number(read('effort')) };
68
+ });
69
+ }
70
+
71
+ function markErrors(list: HTMLElement, analysis: ReturnType<typeof analyzeInitiatives>, ui: CivicPriorityUI): void {
72
+ list.querySelectorAll<HTMLElement>('[data-initiative-row]').forEach((row) => {
73
+ row.querySelectorAll<HTMLInputElement>('[data-field]').forEach((input) => input.removeAttribute('aria-invalid'));
74
+ const error = row.querySelector<HTMLElement>('[data-row-error]');
75
+ if (error) error.textContent = '';
76
+ });
77
+ analysis.errors.forEach((entry) => {
78
+ const row = list.querySelector<HTMLElement>(`[data-index="${entry.index}"]`);
79
+ const input = row?.querySelector<HTMLInputElement>(`[data-field="${entry.field}"]`);
80
+ input?.setAttribute('aria-invalid', 'true');
81
+ const error = row?.querySelector<HTMLElement>('[data-row-error]');
82
+ if (error && !error.textContent) error.textContent = errorMessage(entry.field, entry.message, ui);
83
+ });
84
+ }
85
+
86
+ function errorMessage(field: string, fallback: string, ui: CivicPriorityUI): string {
87
+ if (field === 'name') return ui.nameRequired;
88
+ if (field === 'impact') return ui.impactInvalid;
89
+ if (field === 'urgency') return ui.urgencyInvalid;
90
+ if (field === 'effort') return ui.effortInvalid;
91
+ return fallback;
92
+ }
93
+
94
+ function readInitialState(): CivicPriorityState {
95
+ const script = document.querySelector<HTMLScriptElement>('[data-civic-priority-state]');
96
+ try {
97
+ const parsed = script ? JSON.parse(script.textContent ?? '') as CivicPriorityState : null;
98
+ if (parsed && Array.isArray(parsed.initiatives)) return parsed;
99
+ } catch {
100
+ return { initiatives: defaultInitiatives };
101
+ }
102
+ return { initiatives: defaultInitiatives };
103
+ }
104
+
105
+ interface ActionContext {
106
+ targets: ReturnType<typeof getTargets>;
107
+ ui: CivicPriorityUI;
108
+ refresh: () => void;
109
+ }
110
+
111
+ function addInitiative(context: ActionContext): void {
112
+ const current = readInputs(context.targets.list);
113
+ current.push({ id: createId(), name: '', impact: 0, urgency: 0, effort: 0 });
114
+ renderRows(context.targets.list, current, context.ui);
115
+ context.refresh();
116
+ }
117
+
118
+ function removeInitiative(target: HTMLElement, context: ActionContext): void {
119
+ const row = target.closest<HTMLElement>('[data-initiative-row]');
120
+ const index = Number(row?.dataset.index ?? -1);
121
+ const current = readInputs(context.targets.list).filter((_item, itemIndex) => itemIndex !== index);
122
+ renderRows(context.targets.list, current, context.ui);
123
+ context.refresh();
124
+ }
125
+
126
+ function resetInitiatives(context: ActionContext): void {
127
+ const current = readInitialState().initiatives.map((item) => ({ ...item }));
128
+ renderRows(context.targets.list, current, context.ui);
129
+ context.refresh();
130
+ }
131
+
132
+ function handleClick(event: Event, context: ActionContext): void {
133
+ const target = event.target as HTMLElement;
134
+ const action = target.closest<HTMLElement>('[data-action]')?.dataset.action;
135
+ if (action === 'add') addInitiative(context);
136
+ if (action === 'remove') removeInitiative(target, context);
137
+ if (action === 'reset') resetInitiatives(context);
138
+ }
139
+
140
+ function getTargets(root: HTMLElement) {
141
+ const list = root.querySelector<HTMLElement>('[data-initiative-list]');
142
+ const scene = root.querySelector<HTMLElement>('[data-priority-scene]');
143
+ const summary = root.querySelector<HTMLElement>('[data-priority-summary]');
144
+ const ranking = root.querySelector<HTMLElement>('[data-priority-ranking]');
145
+ const warnings = root.querySelector<HTMLElement>('[data-priority-warnings]');
146
+ const status = root.querySelector<HTMLElement>('[data-priority-status]');
147
+ if (!list || !scene || !summary || !ranking || !warnings || !status) throw new Error('Civic priority matrix mount points are missing.');
148
+ return { list, scene, summary, ranking, warnings, status };
149
+ }
150
+
151
+ export function mountCivicPriorityMatrix(root: HTMLElement, ui: CivicPriorityUI): void {
152
+ const targets = getTargets(root);
153
+ let state = loadCivicPriorityState() ?? readInitialState();
154
+ renderRows(targets.list, state.initiatives, ui);
155
+
156
+ const refresh = (): void => {
157
+ state = { initiatives: readInputs(targets.list) };
158
+ const analysis = analyzeInitiatives(state.initiatives);
159
+ saveCivicPriorityState(state);
160
+ markErrors(targets.list, analysis, ui);
161
+ renderAnalysis(targets, analysis, ui);
162
+ };
163
+
164
+ root.addEventListener('input', refresh);
165
+ const context: ActionContext = { targets, ui, refresh };
166
+ root.addEventListener('click', (event) => handleClick(event, context));
167
+ refresh();
168
+ }
@@ -0,0 +1,123 @@
1
+ import type { PriorityAnalysis, PriorityQuadrant, InitiativeResult } from './logic';
2
+ import { getPriorityBadge } from './evaluator';
3
+ import type { CivicPriorityUI } from './ui';
4
+
5
+ interface ViewTargets {
6
+ scene: HTMLElement;
7
+ summary: HTMLElement;
8
+ ranking: HTMLElement;
9
+ warnings: HTMLElement;
10
+ status: HTMLElement;
11
+ }
12
+
13
+ function escapeHtml(value: string): string {
14
+ return value.replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[character] ?? character));
15
+ }
16
+
17
+ function number(value: number): string {
18
+ return new Intl.NumberFormat('en', { maximumFractionDigits: 2 }).format(value);
19
+ }
20
+
21
+ function quadrantLabel(ui: CivicPriorityUI, quadrant: PriorityQuadrant): string {
22
+ if (quadrant === 'act-now') return ui.actNow;
23
+ if (quadrant === 'plan') return ui.plan;
24
+ if (quadrant === 'quick-response') return ui.quickResponse;
25
+ return ui.defer;
26
+ }
27
+
28
+ function pointMarkup(item: InitiativeResult): string {
29
+ const x = 76 + ((item.impact - 1) / 4) * 368;
30
+ const y = 294 - ((item.urgency - 1) / 4) * 232;
31
+ const labelX = x > 340 ? x - 10 : x + 10;
32
+ const anchor = x > 340 ? 'end' : 'start';
33
+ const placeLabelAbove = y <= 64 || (y > 150 && y < 205);
34
+ const labelY = placeLabelAbove ? Math.max(12, y - 32) : y - 3;
35
+ return `<g class="priority-point priority-point-${item.quadrant}"><circle cx="${x}" cy="${y}" r="${Math.max(9, Math.min(18, 8 + item.priorityScore / 3))}"/><text x="${labelX}" y="${labelY}" text-anchor="${anchor}">${escapeHtml(item.normalizedName)} <tspan class="priority-point-score">(${number(item.priorityScore)})</tspan></text></g>`;
36
+ }
37
+
38
+ function renderScene(target: HTMLElement, analysis: PriorityAnalysis, ui: CivicPriorityUI): void {
39
+ if (!analysis.valid) {
40
+ const message = analysis.initiatives.length === 0 ? ui.noInitiatives : ui.invalidState;
41
+ target.innerHTML = `<div class="priority-empty"><span class="priority-empty-mark" aria-hidden="true"></span><p>${escapeHtml(message)}</p></div>`;
42
+ return;
43
+ }
44
+ const points = analysis.initiatives.map(pointMarkup).join('');
45
+ target.innerHTML = `<svg class="priority-field" viewBox="0 0 520 370" role="img" aria-label="${escapeHtml(ui.matrixLabel)}"><rect class="priority-quadrant priority-quadrant-plan" x="54" y="24" width="205" height="128" rx="18"/><rect class="priority-quadrant priority-quadrant-act" x="259" y="24" width="207" height="128" rx="18"/><rect class="priority-quadrant priority-quadrant-defer" x="54" y="152" width="205" height="128" rx="18"/><rect class="priority-quadrant priority-quadrant-quick" x="259" y="152" width="207" height="128" rx="18"/><path class="priority-axis" d="M54 152H466M259 24V280"/><text class="priority-axis-label" x="259" y="330" text-anchor="middle">${escapeHtml(ui.impact)}</text><text class="priority-axis-label" x="18" y="152" text-anchor="middle" transform="rotate(-90 18 152)">${escapeHtml(ui.urgency)}</text><text class="priority-axis-end" x="58" y="315">${escapeHtml(ui.lowImpact)}</text><text class="priority-axis-end" x="462" y="315" text-anchor="end">${escapeHtml(ui.highImpact)}</text><text class="priority-axis-end" x="42" y="276" text-anchor="end">${escapeHtml(ui.lowUrgency)}</text><text class="priority-axis-end" x="42" y="34" text-anchor="end">${escapeHtml(ui.highUrgency)}</text><text class="priority-quadrant-label" x="70" y="52">${escapeHtml(ui.plan)}</text><text class="priority-quadrant-label" x="275" y="52">${escapeHtml(ui.actNow)}</text><text class="priority-quadrant-label" x="70" y="180">${escapeHtml(ui.defer)}</text><text class="priority-quadrant-label" x="275" y="180">${escapeHtml(ui.quickResponse)}</text>${points}</svg>`;
46
+ }
47
+
48
+ function addCell(row: HTMLTableRowElement, text: string, className = ''): void {
49
+ const cell = document.createElement('td');
50
+ cell.textContent = text;
51
+ if (className) cell.className = className;
52
+ row.appendChild(cell);
53
+ }
54
+
55
+ function createHeaderRow(ui: CivicPriorityUI): HTMLTableRowElement {
56
+ const row = document.createElement('tr');
57
+ [ui.rank, ui.initiative, ui.priorityScore, ui.denominator, ui.quadrant].forEach((label) => addCell(row, label, 'priority-header-cell'));
58
+ return row;
59
+ }
60
+
61
+ function createRankingRow(item: InitiativeResult, index: number, ui: CivicPriorityUI): HTMLTableRowElement {
62
+ const row = document.createElement('tr');
63
+ row.dataset.quadrant = item.quadrant;
64
+ addCell(row, String(index + 1), 'priority-rank-cell');
65
+ addCell(row, item.normalizedName);
66
+ addCell(row, `${number(item.priorityScore)} ${ui.scoreOutOf}`);
67
+ addCell(row, `${item.denominator} (${ui.effort} ${item.effort})`);
68
+ const badge = document.createElement('span');
69
+ const badgeInfo = getPriorityBadge(item.quadrant);
70
+ badge.className = `priority-badge priority-badge-${badgeInfo.tone}`;
71
+ badge.textContent = quadrantLabel(ui, item.quadrant);
72
+ const quadrantCell = document.createElement('td');
73
+ quadrantCell.appendChild(badge);
74
+ row.appendChild(quadrantCell);
75
+ return row;
76
+ }
77
+
78
+ function renderRanking(target: HTMLElement, analysis: PriorityAnalysis, ui: CivicPriorityUI): void {
79
+ target.replaceChildren();
80
+ if (!analysis.valid) return;
81
+ const table = document.createElement('table');
82
+ table.className = 'priority-ranking-table';
83
+ const caption = document.createElement('caption');
84
+ caption.className = 'visually-hidden';
85
+ caption.textContent = ui.rankingTitle;
86
+ table.appendChild(caption);
87
+ const head = document.createElement('thead');
88
+ head.appendChild(createHeaderRow(ui));
89
+ table.appendChild(head);
90
+ const body = document.createElement('tbody');
91
+ analysis.initiatives.forEach((item, index) => body.appendChild(createRankingRow(item, index, ui)));
92
+ table.appendChild(body);
93
+ target.appendChild(table);
94
+ }
95
+
96
+ function renderWarnings(target: HTMLElement, analysis: PriorityAnalysis, ui: CivicPriorityUI): void {
97
+ target.replaceChildren();
98
+ analysis.warnings.forEach((warning) => {
99
+ const item = document.createElement('p');
100
+ item.className = 'priority-warning';
101
+ const message = warning.includes('effort score of 0') ? ui.zeroEffortWarning : ui.invalidRowsWarning;
102
+ item.innerHTML = `<strong>${escapeHtml(ui.warningTitle)}</strong> ${escapeHtml(message)}`;
103
+ target.appendChild(item);
104
+ });
105
+ }
106
+
107
+ function summaryText(analysis: PriorityAnalysis, ui: CivicPriorityUI): string {
108
+ if (analysis.valid) return `${ui.summaryPrefix} ${analysis.initiatives.length} ${ui.summarySuffix}.`;
109
+ if (analysis.initiatives.length === 0) return ui.noInitiatives;
110
+ return ui.emptyState;
111
+ }
112
+
113
+ function statusText(analysis: PriorityAnalysis, ui: CivicPriorityUI): string {
114
+ return analysis.valid || analysis.initiatives.length === 0 ? '' : ui.invalidState;
115
+ }
116
+
117
+ export function renderAnalysis(targets: ViewTargets, analysis: PriorityAnalysis, ui: CivicPriorityUI): void {
118
+ targets.summary.textContent = summaryText(analysis, ui);
119
+ targets.status.textContent = statusText(analysis, ui);
120
+ renderScene(targets.scene, analysis, ui);
121
+ renderRanking(targets.ranking, analysis, ui);
122
+ renderWarnings(targets.warnings, analysis, ui);
123
+ }
@@ -0,0 +1,28 @@
1
+ import type { CivicToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { CivicPriorityUI } from './ui';
3
+
4
+ export type { CivicPriorityUI } from './ui';
5
+ export type CivicPriorityLocaleContent = ToolLocaleContent<CivicPriorityUI>;
6
+
7
+ export const civicPriorityMatrix: CivicToolEntry<CivicPriorityUI> = {
8
+ id: 'civic-priority-matrix',
9
+ phase: 'localized',
10
+ icons: { bg: 'mdi:bank', fg: 'mdi:format-list-numbered' },
11
+ i18n: {
12
+ de: () => import('./i18n/de').then((module) => module.content),
13
+ en: () => import('./i18n/en').then((module) => module.content),
14
+ es: () => import('./i18n/es').then((module) => module.content),
15
+ fr: () => import('./i18n/fr').then((module) => module.content),
16
+ id: () => import('./i18n/id').then((module) => module.content),
17
+ it: () => import('./i18n/it').then((module) => module.content),
18
+ ja: () => import('./i18n/ja').then((module) => module.content),
19
+ ko: () => import('./i18n/ko').then((module) => module.content),
20
+ nl: () => import('./i18n/nl').then((module) => module.content),
21
+ pl: () => import('./i18n/pl').then((module) => module.content),
22
+ pt: () => import('./i18n/pt').then((module) => module.content),
23
+ ru: () => import('./i18n/ru').then((module) => module.content),
24
+ sv: () => import('./i18n/sv').then((module) => module.content),
25
+ tr: () => import('./i18n/tr').then((module) => module.content),
26
+ zh: () => import('./i18n/zh').then((module) => module.content),
27
+ },
28
+ };
@@ -0,0 +1,13 @@
1
+ import type { PriorityQuadrant } from './logic';
2
+
3
+ export interface PriorityBadge {
4
+ key: 'actNow' | 'plan' | 'quickResponse' | 'defer';
5
+ tone: 'warm' | 'cool' | 'bright' | 'quiet';
6
+ }
7
+
8
+ export function getPriorityBadge(quadrant: PriorityQuadrant): PriorityBadge {
9
+ if (quadrant === 'act-now') return { key: 'actNow', tone: 'warm' };
10
+ if (quadrant === 'plan') return { key: 'plan', tone: 'cool' };
11
+ if (quadrant === 'quick-response') return { key: 'quickResponse', tone: 'bright' };
12
+ return { key: 'defer', tone: 'quiet' };
13
+ }
@@ -0,0 +1,52 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { CivicPriorityLocaleContent } from '../entry';
4
+ import type { CivicPriorityUI } from '../ui';
5
+
6
+ const ui: CivicPriorityUI = {
7
+ addInitiative: 'Vorhaben hinzufügen', initiativeList: 'Vorhaben bewerten', inputHint: 'Bewerte jeden Vorschlag auf derselben Skala von 1 bis 5.', initiative: 'Vorhaben', impact: 'Wirkung', urgency: 'Dringlichkeit', effort: 'Aufwand', scoreScale: 'Skala von 1 bis 5', scoreHint: 'Höher bedeutet mehr Wirkung, Dringlichkeit oder Aufwand.', removeInitiative: 'Vorhaben entfernen', reset: 'Beispiel zurücksetzen', resultTitle: 'Entscheidungsfeld', rankingTitle: 'Nachvollziehbare Reihenfolge', matrixLabel: 'Vorhaben nach Wirkung und Dringlichkeit', summaryPrefix: 'Angezeigt werden', summarySuffix: 'bewertete Vorhaben', rank: 'Rang', priorityScore: 'Prioritätswert', denominator: 'Aufwandsnenner', quadrant: 'Bereich', emptyState: 'Füge ein Vorhaben hinzu, um das Entscheidungsfeld zu öffnen.', invalidState: 'Korrigiere die markierten Werte, um eine Reihenfolge zu berechnen.', nameRequired: 'Name erforderlich.', impactInvalid: 'Wirkung muss eine ganze Zahl von 1 bis 5 sein.', urgencyInvalid: 'Dringlichkeit muss eine ganze Zahl von 1 bis 5 sein.', effortInvalid: 'Aufwand muss eine ganze Zahl von 0 bis 5 sein.', actNow: 'Jetzt handeln', plan: 'Planen', quickResponse: 'Schnell reagieren', defer: 'Zurückstellen', highImpact: 'Hohe Wirkung', lowImpact: 'Geringe Wirkung', highUrgency: 'Hohe Dringlichkeit', lowUrgency: 'Geringe Dringlichkeit', methodTitle: 'Angewandte Methode', methodText: 'Der Prioritätswert ist Wirkung mal Dringlichkeit geteilt durch Aufwand. Wirkung, Dringlichkeit und Aufwand nutzen eine sichtbare Skala von 1 bis 5. Aufwand 0 wird als 1 behandelt, damit eine kostenlose oder bereits vorbereitete Maßnahme einen endlichen Wert behält. Bei Gleichstand zählen Wirkung, Dringlichkeit, geringerer Aufwand und danach die Eingabereihenfolge.', notDoTitle: 'Was dieses Werkzeug nicht leistet', notDoText: 'Es bestimmt keinen objektiven gesellschaftlichen Wert, misst keine Ergebnisse automatisch und ersetzt keine Beratung. Ein hoher Wert ist ein nachvollziehbares Szenario unter deinen Annahmen, kein Auftrag und keine Vorhersage des Gemeinwohls.', edgeCasesTitle: 'Sonderfälle und Warnungen', edgeCasesText: 'Verwende für jede Zeile dieselbe Bedeutung der Werte. Ein fehlender oder nicht ganzzahliger Wert schließt die Zeile bis zur Korrektur aus. Der Wert 3 trennt hohe und niedrige Seiten. Gleiche Werte behalten nach den dokumentierten Regeln eine stabile Reihenfolge. Ändere in einer Gruppendiskussion jeweils nur eine Einschätzung.', warningTitle: 'Vor der Entscheidung prüfen', zeroEffortWarning: 'Aufwand 0 verwendet den Nenner 1, damit der Wert endlich und vergleichbar bleibt.', invalidRowsWarning: 'Einige Zeilen bleiben ausgeschlossen, bis die markierten Werte korrigiert sind.', noInitiatives: 'Noch keine Vorhaben eingegeben.', scoreOutOf: 'von maximal 25',
8
+ };
9
+
10
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Bürgerliche Prioritätsmatrix', applicationCategory: 'BusinessApplication', operatingSystem: 'Any', description: 'Bewerte bürgerschaftliche Vorhaben nach Wirkung, Dringlichkeit und Aufwand für eine nachvollziehbare Reihenfolge.', url: 'https://gamebob.dev/de/prioritaetsmatrix-buergerliche-vorhaben', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
11
+
12
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: [
13
+ { '@type': 'Question', name: 'Wie wird der Prioritätswert berechnet?', acceptedAnswer: { '@type': 'Answer', text: 'Multipliziere Wirkung und Dringlichkeit und teile durch den Aufwand. Aufwand 0 erhält den Nenner 1, damit kein unendlicher Wert entsteht.' } },
14
+ { '@type': 'Question', name: 'Was bedeutet die Position im Bereich?', acceptedAnswer: { '@type': 'Answer', text: 'Wirkung und Dringlichkeit von 3 bis 5 gelten als hoch. Die vier Bereiche geben Hinweise zum Handeln, Planen, schnellen Reagieren oder späteren Prüfen.' } },
15
+ { '@type': 'Question', name: 'Beweist ein hoher Wert, dass ein Vorhaben am besten ist?', acceptedAnswer: { '@type': 'Answer', text: 'Nein. Der Wert beschreibt nur die eingegebenen Annahmen. Er misst keinen objektiven gesellschaftlichen Nutzen und ersetzt keine Diskussion mit Betroffenen.' } },
16
+ { '@type': 'Question', name: 'Warum können zwei Vorhaben denselben Wert haben?', acceptedAnswer: { '@type': 'Answer', text: 'Verschiedene Kombinationen aus Wirkung, Dringlichkeit und Aufwand können denselben Quotienten ergeben. Danach gelten transparente Gleichstandsregeln.' } },
17
+ ] };
18
+
19
+ const howTo: HowTo = { '@type': 'HowTo', name: 'Eine nachvollziehbare Prioritätenliste für Bürgerprojekte erstellen', step: [
20
+ { '@type': 'HowToStep', name: 'Vorhaben eintragen', text: 'Trage pro Zeile einen klar benannten Vorschlag ein und bündele keine voneinander abhängigen Maßnahmen.' },
21
+ { '@type': 'HowToStep', name: 'Skala gemeinsam festlegen', text: 'Definiere Wirkung, Dringlichkeit und Aufwand vor dem Vergleich und verwende die Skala in jeder Zeile gleich.' },
22
+ { '@type': 'HowToStep', name: 'Entscheidungsfeld lesen', text: 'Nutze die vier Bereiche als Gesprächsanstoß und prüfe, ob 3 konsequent als Grenze verwendet wird.' },
23
+ { '@type': 'HowToStep', name: 'Reihenfolge prüfen', text: 'Vergleiche jeden Wert mit seinen drei Eingaben und dem Aufwandsnenner, bevor du eine Schlussfolgerung ziehst.' },
24
+ { '@type': 'HowToStep', name: 'Entscheidung dokumentieren', text: 'Verwende die Liste als Tagesordnung und ergänze Belege, Zuständigkeiten, Einschränkungen und betroffene Stimmen.' },
25
+ ] };
26
+
27
+ export const content: CivicPriorityLocaleContent = {
28
+ slug: 'prioritaetsmatrix-buergerliche-vorhaben', title: 'Bürgerliche Prioritätsmatrix', description: 'Ordne bürgerschaftliche Vorhaben mit transparenten Werten für Wirkung, Dringlichkeit und Aufwand. Erkenne, was jetzt handeln, planen, schnell reagieren oder später prüfen bedeutet.', ui,
29
+ seo: [
30
+ { type: 'title', text: 'Bürgerliche Ideen in eine gemeinsame Reihenfolge bringen', level: 2 },
31
+ { type: 'paragraph', html: 'Gruppen haben oft mehr gute Vorschläge als Zeit, Geld oder Mitarbeitende. Die Bürgerliche Prioritätsmatrix stellt jedem Vorhaben dieselben drei Fragen: Wie groß könnte die Wirkung sein, wie dringend ist das Problem und wie viel Aufwand braucht die Umsetzung? So entsteht ein sichtbarer Ausgangspunkt für ein Gespräch über Reihenfolge und verfügbare Kapazität.' },
32
+ { type: 'title', text: 'So funktioniert der Prioritätswert', level: 2 },
33
+ { type: 'paragraph', html: 'Der Wert lautet Wirkung mal Dringlichkeit geteilt durch Aufwand. Ein Vorhaben mit 5 Wirkung, 4 Dringlichkeit und 2 Aufwand erhält 10 Punkte. Das ist eine kompakte Heuristik, kein Maß für gesellschaftlichen Wert. Ihre Stärke liegt darin, dass jede Annahme sichtbar bleibt und andere Personen die Rechnung nachvollziehen können.' },
34
+ { type: 'table', headers: ['Eingabe', 'Skala', 'Frage für die Gruppe'], rows: [['Wirkung', '1 bis 5', 'Wie viel könnte die Veränderung für die betroffenen Menschen oder das Ergebnis bedeuten?'], ['Dringlichkeit', '1 bis 5', 'Was kostet das Warten oder welcher Zeitdruck besteht?'], ['Aufwand', '0 bis 5', 'Wie schwierig, teuer oder ressourcenintensiv ist die Umsetzung?']] },
35
+ { type: 'title', text: 'Die vier Entscheidungsbereiche lesen', level: 2 },
36
+ { type: 'paragraph', html: 'Das Feld ordnet Vorhaben nach Wirkung und Dringlichkeit. Der Wert 3 ist auf beiden Achsen die sichtbare Grenze. Jetzt handeln verbindet hohe Wirkung mit hoher Dringlichkeit. Planen beschreibt hohe Wirkung bei niedrigerer Dringlichkeit. Schnell reagieren weist auf niedrige Wirkung, aber hohe Dringlichkeit hin. Zurückstellen bedeutet niedrige Werte auf beiden Achsen und ist eine Einladung zur späteren Prüfung, keine automatische Ablehnung.' },
37
+ { type: 'list', items: ['Vereinbare zuerst, was Wirkung in dieser Gruppe bedeutet.', 'Verwende für alle Vorhaben denselben Zeithorizont bei der Dringlichkeit.', 'Bewerte Aufwand als benötigte Ressourcen, nicht als Beliebtheitsstrafe.', 'Lies neben jedem Rang auch die drei Einzelwerte.', 'Bewerte nach neuen Belegen, Fristen oder Kapazitätsänderungen erneut.'] },
38
+ { type: 'title', text: 'Das Ergebnis für eine bürgerschaftliche Diskussion nutzen', level: 2 },
39
+ { type: 'paragraph', html: 'Eine Matrix ist besonders hilfreich, wenn mehrere Personen dieselben Vorhaben bewerten und ihre Unterschiede erklären. Gibt eine Person einem kleinen, tief betroffenen Personenkreis hohe Wirkung und eine andere wegen der begrenzten Reichweite eine niedrigere, wird eine unklare Definition sichtbar. Halte die Gründe und die einbezogenen Stimmen fest, statt die Abweichung zu verstecken.' },
40
+ { type: 'tip', title: 'Der Wert ist ein erklärtes Szenario', html: 'Ein hoher Wert bedeutet, dass die eingegebene Kombination unter dieser Formel weit oben landet. Er begründet keine rechtliche Pflicht, beweist keine Fairness und ersetzt keine öffentliche Entscheidung. Bewahre die Begründungen neben jeder Entscheidung auf, die diese Reihenfolge nutzt.' },
41
+ { type: 'title', text: 'Grenzen und Sonderfälle', level: 2 },
42
+ { type: 'paragraph', html: 'Aufwand 0 wird mit dem Nenner 1 geschützt, damit kein unendlicher Wert entsteht. Fehlende, gebrochene oder außerhalb der Skala liegende Werte schließen eine Zeile aus, bis sie korrigiert sind. Gleiche Werte werden mit Wirkung, Dringlichkeit, geringerem Aufwand und stabiler Eingabereihenfolge geordnet. Ein Bereich ist außerdem keine Risikoanalyse: Dringende Vorhaben mit hoher Wirkung können weiterhin rechtliche, finanzielle, sicherheitsbezogene oder barrierefreie Prüfungen brauchen.' },
43
+ ],
44
+ faq: [
45
+ { question: 'Wie wird der Prioritätswert berechnet?', answer: 'Multipliziere Wirkung und Dringlichkeit und teile durch Aufwand. Bei Aufwand 0 wird der Nenner 1 verwendet.' },
46
+ { question: 'Was bedeutet die Position im Entscheidungsfeld?', answer: 'Wirkung und Dringlichkeit ab 3 liegen auf der hohen Seite. Die Bereiche helfen, die Reihenfolge gemeinsam zu besprechen.' },
47
+ { question: 'Ist ein hoher Wert ein Beweis für das beste Vorhaben?', answer: 'Nein. Er ist ein nachvollziehbares Ergebnis unter den gewählten Annahmen und keine Vorhersage des gesellschaftlichen Nutzens.' },
48
+ { question: 'Warum entstehen gleiche Werte?', answer: 'Unterschiedliche Eingabekombinationen können denselben Quotienten ergeben. Transparente Gleichstandsregeln halten die Reihenfolge reproduzierbar.' },
49
+ ], bibliography, howTo: [
50
+ { name: 'Vorhaben eintragen', text: 'Trage je Zeile einen klaren Vorschlag ein.' }, { name: 'Skala vereinbaren', text: 'Verwende die Bedeutung von 1 bis 5 in jeder Zeile gleich.' }, { name: 'Feld lesen', text: 'Nutze die vier Bereiche als Gesprächsrahmen für die Reihenfolge.' }, { name: 'Rang prüfen', text: 'Vergleiche jede Platzierung mit Wirkung, Dringlichkeit, Aufwand und Nenner.' }, { name: 'Belege ergänzen', text: 'Dokumentiere nach der Diskussion Belege, Grenzen und betroffene Stimmen.' },
51
+ ], schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[],
52
+ };
@@ -0,0 +1,73 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { CivicPriorityLocaleContent } from '../entry';
4
+ import { ui } from '../ui';
5
+
6
+ const softwareApplication: SoftwareApplication = {
7
+ '@type': 'SoftwareApplication',
8
+ name: 'Civic Priority Matrix',
9
+ applicationCategory: 'BusinessApplication',
10
+ operatingSystem: 'Any',
11
+ description: 'Score civic initiatives by impact, urgency, and effort to create an explainable priority order.',
12
+ url: 'https://gamebob.dev/en/civic-priority-matrix',
13
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
14
+ };
15
+
16
+ const faqPage: FAQPage = {
17
+ '@type': 'FAQPage',
18
+ mainEntity: [
19
+ { '@type': 'Question', name: 'How is the priority score calculated?', acceptedAnswer: { '@type': 'Answer', text: 'The score is impact multiplied by urgency, divided by effort. Each score uses a 1 to 5 scale. Effort 0 uses a denominator of 1 so the result stays finite and its assumption remains visible.' } },
20
+ { '@type': 'Question', name: 'What does the quadrant position mean?', acceptedAnswer: { '@type': 'Answer', text: 'The field uses impact and urgency only. Scores of 3, 4, or 5 are placed on the high side of an axis, while 1 or 2 are placed on the low side. The zones suggest act now, plan, quick response, or defer.' } },
21
+ { '@type': 'Question', name: 'Does a high score prove that an initiative is best?', acceptedAnswer: { '@type': 'Answer', text: 'No. A high score is the result of the assumptions entered by the group. It does not measure objective social value, predict outcomes, or replace discussion with affected people.' } },
22
+ { '@type': 'Question', name: 'Why can two initiatives have the same rank score?', acceptedAnswer: { '@type': 'Answer', text: 'Different combinations of impact, urgency, and effort can produce the same quotient. The tool then uses impact, urgency, lower effort, and stable input order as transparent tie breakers.' } },
23
+ ],
24
+ };
25
+
26
+ const howTo: HowTo = {
27
+ '@type': 'HowTo',
28
+ name: 'Build an explainable civic priority order',
29
+ step: [
30
+ { '@type': 'HowToStep', name: 'List the initiatives', text: 'Enter one clearly named proposal per row. Avoid combining several actions in one item because the group cannot score a bundle consistently.' },
31
+ { '@type': 'HowToStep', name: 'Agree the scoring scale', text: 'Use the same meaning for impact, urgency, and effort across every row. Treat 1 as low and 5 as high, and use 0 effort only when no effort is being declared.' },
32
+ { '@type': 'HowToStep', name: 'Review the decision field', text: 'Read the quadrant as a conversation prompt: act now, plan, quick response, or defer. Check whether a score of 3 is being used consistently as the dividing line.' },
33
+ { '@type': 'HowToStep', name: 'Inspect the order', text: 'Compare each priority score with its three inputs and the effort denominator. A surprising rank is a reason to revisit an assumption, not to hide the result.' },
34
+ { '@type': 'HowToStep', name: 'Record the decision', text: 'Use the ranked list as an agenda for group discussion, then add evidence, owners, constraints, and affected voices before committing resources.' },
35
+ ],
36
+ };
37
+
38
+ export const content: CivicPriorityLocaleContent = {
39
+ slug: 'civic-priority-matrix',
40
+ title: 'Civic Priority Matrix',
41
+ description: 'Rank civic initiatives with a transparent impact, urgency, and effort score. See which proposals deserve action now, planning, a quick response, or a later review.',
42
+ ui,
43
+ seo: [
44
+ { type: 'title', text: 'Turn Civic Ideas Into a Shared Priority Order', level: 2 },
45
+ { type: 'paragraph', html: 'Groups often have more useful proposals than time, money, or people to act on them. The Civic Priority Matrix gives each initiative the same three questions: how much impact could it have, how urgent is the problem, and how much effort would implementation require? The result is a visible starting point for a conversation about sequence and trade offs.' },
46
+ { type: 'title', text: 'How the Priority Score Works', level: 2 },
47
+ { type: 'paragraph', html: 'The score is impact multiplied by urgency, divided by effort. For example, an initiative scored 5 for impact, 4 for urgency, and 2 for effort receives 10 points. The score is a compact heuristic, not a measurement of social value. It is useful because every component stays visible and another person can reproduce the arithmetic.' },
48
+ { type: 'table', headers: ['Input', 'Scale', 'Question to ask'], rows: [['Impact', '1 to 5', 'How much could this change matter to the people or outcome concerned?'], ['Urgency', '1 to 5', 'What is the cost of waiting or the time pressure?'], ['Effort', '0 to 5', 'How difficult, expensive, or resource intensive is delivery?']] },
49
+ { type: 'title', text: 'Read the Four Decision Zones', level: 2 },
50
+ { type: 'paragraph', html: 'The field positions initiatives by impact and urgency, with 3 as the dividing line on both axes. Act now contains high impact and high urgency items. Plan contains high impact items with lower urgency. Quick response contains lower impact items with high urgency, where a small intervention may still remove pressure. Defer contains items that are low on both axes and need a later review rather than automatic rejection.' },
51
+ { type: 'list', items: ['Agree what impact means for this specific group before entering scores.', 'Use the same time horizon when judging urgency across all initiatives.', 'Treat effort as the resources needed to deliver, not as a penalty for an unpopular idea.', 'Read the three component values beside every rank before discussing the order.', 'Re-score after new evidence, a deadline change, or a change in available capacity.'] },
52
+ { type: 'title', text: 'Make the Result Useful in a Civic Discussion', level: 2 },
53
+ { type: 'paragraph', html: 'A matrix is strongest when several people score the same initiatives and explain their differences. If one person gives an item high impact because it affects a small group deeply, while another gives it a lower score because the reach is limited, the disagreement reveals a criterion that needs definition. Record the reasons and the voices that informed the final choice.' },
54
+ { type: 'tip', title: 'The score is a declared scenario', html: 'A high result means that the entered combination of impact, urgency, and effort rises to the top under this formula. It does not establish a legal duty, prove fairness, forecast outcomes, or replace a public decision process. Keep the scoring notes beside any decision that uses the ranking.' },
55
+ { type: 'title', text: 'Limits and Edge Cases', level: 2 },
56
+ { type: 'paragraph', html: 'Effort 0 is protected with a denominator of 1 so it does not create an infinite result. A missing, fractional, or out of range value excludes that row until it is fixed. Equal scores are not silently rearranged: the tool applies documented tie breakers and then preserves input order. A quadrant is also not a risk assessment; urgent and high impact work may still need safety, legal, financial, or accessibility review.' },
57
+ ],
58
+ faq: [
59
+ { question: 'How is the priority score calculated?', answer: 'Multiply impact by urgency and divide by effort. An effort of 0 uses a denominator of 1 so the score stays finite.' },
60
+ { question: 'What does the quadrant position mean?', answer: 'Impact and urgency of 3 or more are on the high side. The four zones suggest act now, plan, quick response, or defer.' },
61
+ { question: 'Does a high score prove that an initiative is best?', answer: 'No. It is a transparent result under declared assumptions, not proof of social value or a prediction.' },
62
+ { question: 'Why can two initiatives have the same score?', answer: 'Different input combinations can produce the same quotient. The tool then uses transparent tie breakers and stable input order.' },
63
+ ],
64
+ bibliography,
65
+ howTo: [
66
+ { name: 'List the initiatives', text: 'Enter one clearly named proposal per row.' },
67
+ { name: 'Agree the scale', text: 'Use the same meaning for 1 to 5 across every row.' },
68
+ { name: 'Read the field', text: 'Use the four zones to frame the discussion around sequence.' },
69
+ { name: 'Inspect the order', text: 'Compare every rank with impact, urgency, effort, and the denominator.' },
70
+ { name: 'Add evidence', text: 'Use the result as an agenda, then record constraints and affected voices.' },
71
+ ],
72
+ schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[],
73
+ };