@jjlmoya/utils-civic 1.4.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 (38) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -0
  4. package/src/index.ts +1 -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/tools.ts +2 -0
@@ -0,0 +1,12 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { civicPriorityMatrix } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props { locale?: KnownLocale; }
7
+
8
+ const { locale = 'en' } = Astro.props as Props;
9
+ const content = await civicPriorityMatrix.i18n[locale]?.() ?? await civicPriorityMatrix.i18n.en?.();
10
+ ---
11
+
12
+ {content && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,50 @@
1
+ import type { InitiativeInput } from './logic';
2
+
3
+ export interface CivicPriorityState {
4
+ initiatives: InitiativeInput[];
5
+ }
6
+
7
+ const STORAGE_KEY = 'jjlmoya-civic-priority-matrix';
8
+
9
+ export const defaultInitiatives: InitiativeInput[] = [
10
+ { id: 'accessible-bus-stops', name: 'Accessible bus stops', impact: 5, urgency: 4, effort: 2 },
11
+ { id: 'cooling-centres', name: 'Cooling centres', impact: 4, urgency: 5, effort: 3 },
12
+ { id: 'meeting-transcripts', name: 'Public meeting transcripts', impact: 3, urgency: 2, effort: 1 },
13
+ { id: 'park-lighting', name: 'Park lighting repair', impact: 4, urgency: 3, effort: 4 },
14
+ ];
15
+
16
+ function isInitiative(value: unknown): value is InitiativeInput {
17
+ if (!value || typeof value !== 'object') return false;
18
+ const item = value as Record<string, unknown>;
19
+ return typeof item.id === 'string'
20
+ && typeof item.name === 'string'
21
+ && typeof item.impact === 'number'
22
+ && typeof item.urgency === 'number'
23
+ && typeof item.effort === 'number';
24
+ }
25
+
26
+ function isState(value: unknown): value is CivicPriorityState {
27
+ if (!value || typeof value !== 'object') return false;
28
+ const state = value as Record<string, unknown>;
29
+ return Array.isArray(state.initiatives) && state.initiatives.every(isInitiative);
30
+ }
31
+
32
+ export function loadCivicPriorityState(): CivicPriorityState | null {
33
+ try {
34
+ if (typeof window === 'undefined') return null;
35
+ const raw = window.localStorage.getItem(STORAGE_KEY);
36
+ if (!raw) return null;
37
+ const parsed: unknown = JSON.parse(raw);
38
+ return isState(parsed) ? parsed : null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export function saveCivicPriorityState(state: CivicPriorityState): void {
45
+ try {
46
+ if (typeof window !== 'undefined') window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
47
+ } catch {
48
+ return;
49
+ }
50
+ }
@@ -0,0 +1,96 @@
1
+ export interface CivicPriorityUI {
2
+ [key: string]: string;
3
+ addInitiative: string;
4
+ initiativeList: string;
5
+ inputHint: string;
6
+ initiative: string;
7
+ impact: string;
8
+ urgency: string;
9
+ effort: string;
10
+ scoreScale: string;
11
+ scoreHint: string;
12
+ removeInitiative: string;
13
+ reset: string;
14
+ resultTitle: string;
15
+ rankingTitle: string;
16
+ matrixLabel: string;
17
+ summaryPrefix: string;
18
+ summarySuffix: string;
19
+ rank: string;
20
+ priorityScore: string;
21
+ denominator: string;
22
+ quadrant: string;
23
+ emptyState: string;
24
+ invalidState: string;
25
+ nameRequired: string;
26
+ impactInvalid: string;
27
+ urgencyInvalid: string;
28
+ effortInvalid: string;
29
+ actNow: string;
30
+ plan: string;
31
+ quickResponse: string;
32
+ defer: string;
33
+ highImpact: string;
34
+ lowImpact: string;
35
+ highUrgency: string;
36
+ lowUrgency: string;
37
+ methodTitle: string;
38
+ methodText: string;
39
+ notDoTitle: string;
40
+ notDoText: string;
41
+ edgeCasesTitle: string;
42
+ edgeCasesText: string;
43
+ warningTitle: string;
44
+ zeroEffortWarning: string;
45
+ invalidRowsWarning: string;
46
+ noInitiatives: string;
47
+ scoreOutOf: string;
48
+ }
49
+
50
+ export const ui: CivicPriorityUI = {
51
+ addInitiative: 'Add initiative',
52
+ initiativeList: 'Initiatives to score',
53
+ inputHint: 'Give every proposal the same 1 to 5 scale before comparing them.',
54
+ initiative: 'Initiative',
55
+ impact: 'Impact',
56
+ urgency: 'Urgency',
57
+ effort: 'Effort',
58
+ scoreScale: 'Score from 1 to 5',
59
+ scoreHint: 'Higher means more impact, more urgency, or more effort.',
60
+ removeInitiative: 'Remove initiative',
61
+ reset: 'Reset example',
62
+ resultTitle: 'Decision field',
63
+ rankingTitle: 'Explainable order',
64
+ matrixLabel: 'Initiatives placed by impact and urgency',
65
+ summaryPrefix: 'Showing',
66
+ summarySuffix: 'scored initiatives',
67
+ rank: 'Rank',
68
+ priorityScore: 'Priority score',
69
+ denominator: 'Effort denominator',
70
+ quadrant: 'Quadrant',
71
+ emptyState: 'Add an initiative to open the decision field.',
72
+ invalidState: 'Fix the highlighted values to calculate a priority order.',
73
+ nameRequired: 'Name is required.',
74
+ impactInvalid: 'Impact must be a whole number from 1 to 5.',
75
+ urgencyInvalid: 'Urgency must be a whole number from 1 to 5.',
76
+ effortInvalid: 'Effort must be a whole number from 0 to 5.',
77
+ actNow: 'Act now',
78
+ plan: 'Plan',
79
+ quickResponse: 'Quick response',
80
+ defer: 'Defer',
81
+ highImpact: 'High impact',
82
+ lowImpact: 'Low impact',
83
+ highUrgency: 'High urgency',
84
+ lowUrgency: 'Low urgency',
85
+ methodTitle: 'Method applied',
86
+ methodText: 'The priority score is impact multiplied by urgency, divided by effort. Impact, urgency, and effort use a visible 1 to 5 scale. Effort 0 is treated as 1 so a free or already-resourced action remains finite rather than becoming an infinite score. The ranking breaks ties by impact, urgency, lower effort, then input order.',
87
+ notDoTitle: 'What this tool does not do',
88
+ notDoText: 'It does not establish objective social value, measure outcomes automatically, or replace deliberation. A high score is a transparent scenario under your assumptions, not a mandate or a prediction of public benefit.',
89
+ edgeCasesTitle: 'Edge cases and warnings',
90
+ edgeCasesText: 'Keep the same interpretation of each score across every row. A missing or non-integer score excludes that row until fixed. Score 3 is the dividing line in the field. Equal scores remain in stable input order after the documented tie-breakers. Change one assessment at a time when discussing a result with a group.',
91
+ warningTitle: 'Review before deciding',
92
+ zeroEffortWarning: 'An effort score of 0 uses a denominator of 1 to keep the score finite and comparable.',
93
+ invalidRowsWarning: 'Some rows are excluded until their highlighted values are fixed.',
94
+ noInitiatives: 'No initiatives entered yet.',
95
+ scoreOutOf: 'of 25 maximum',
96
+ };
package/src/tools.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  export { ALL_ENTRIES } from './entries';
2
2
  import type { ToolDefinition } from './types';
3
3
  import { COALITION_MAJORITY_CALCULATOR_TOOL } from './tool/coalition-majority-calculator';
4
+ import { CIVIC_PRIORITY_MATRIX_TOOL } from './tool/civic-priority-matrix';
4
5
  import { ELECTION_SEAT_APPORTIONMENT_CALCULATOR_TOOL } from './tool/election-seat-apportionment-calculator';
5
6
  import { REFERENDUM_THRESHOLD_CALCULATOR_TOOL } from './tool/referendum-threshold-calculator';
6
7
  import { VOTE_SEAT_DISPROPORTIONALITY_ANALYZER_TOOL } from './tool/vote-seat-disproportionality-analyzer';
7
8
 
8
9
  export const ALL_TOOLS: ToolDefinition[] = [
9
10
  COALITION_MAJORITY_CALCULATOR_TOOL,
11
+ CIVIC_PRIORITY_MATRIX_TOOL,
10
12
  ELECTION_SEAT_APPORTIONMENT_CALCULATOR_TOOL,
11
13
  REFERENDUM_THRESHOLD_CALCULATOR_TOOL,
12
14
  VOTE_SEAT_DISPROPORTIONALITY_ANALYZER_TOOL,