@the-syllabus/analysis-renderers 0.2.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 (128) hide show
  1. package/dist/cells/RelationshipCardCell.d.ts +10 -0
  2. package/dist/cells/RelationshipCardCell.d.ts.map +1 -0
  3. package/dist/cells/RelationshipCardCell.js +91 -0
  4. package/dist/cells/RelationshipCardCell.js.map +1 -0
  5. package/dist/cells/TacticCardCell.d.ts +12 -0
  6. package/dist/cells/TacticCardCell.d.ts.map +1 -0
  7. package/dist/cells/TacticCardCell.js +77 -0
  8. package/dist/cells/TacticCardCell.js.map +1 -0
  9. package/dist/cells/TemplateCardCell.d.ts +29 -0
  10. package/dist/cells/TemplateCardCell.d.ts.map +1 -0
  11. package/dist/cells/TemplateCardCell.js +202 -0
  12. package/dist/cells/TemplateCardCell.js.map +1 -0
  13. package/dist/cells/index.d.ts +15 -0
  14. package/dist/cells/index.d.ts.map +1 -0
  15. package/dist/cells/index.js +85 -0
  16. package/dist/cells/index.js.map +1 -0
  17. package/dist/components/ConditionCards.d.ts +18 -0
  18. package/dist/components/ConditionCards.d.ts.map +1 -0
  19. package/dist/components/ConditionCards.js +28 -0
  20. package/dist/components/ConditionCards.js.map +1 -0
  21. package/dist/components/EvidenceTrail.d.ts +54 -0
  22. package/dist/components/EvidenceTrail.d.ts.map +1 -0
  23. package/dist/components/EvidenceTrail.js +98 -0
  24. package/dist/components/EvidenceTrail.js.map +1 -0
  25. package/dist/dispatch/SubRendererDispatch.d.ts +39 -0
  26. package/dist/dispatch/SubRendererDispatch.d.ts.map +1 -0
  27. package/dist/dispatch/SubRendererDispatch.js +153 -0
  28. package/dist/dispatch/SubRendererDispatch.js.map +1 -0
  29. package/dist/hooks/useProseExtraction.d.ts +38 -0
  30. package/dist/hooks/useProseExtraction.d.ts.map +1 -0
  31. package/dist/hooks/useProseExtraction.js +93 -0
  32. package/dist/hooks/useProseExtraction.js.map +1 -0
  33. package/dist/index.d.ts +32 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +38 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/provenance/ProvenanceSectionIcon.d.ts +15 -0
  38. package/dist/provenance/ProvenanceSectionIcon.d.ts.map +1 -0
  39. package/dist/provenance/ProvenanceSectionIcon.js +11 -0
  40. package/dist/provenance/ProvenanceSectionIcon.js.map +1 -0
  41. package/dist/renderers/AccordionRenderer.d.ts +29 -0
  42. package/dist/renderers/AccordionRenderer.d.ts.map +1 -0
  43. package/dist/renderers/AccordionRenderer.js +315 -0
  44. package/dist/renderers/AccordionRenderer.js.map +1 -0
  45. package/dist/renderers/CardGridRenderer.d.ts +24 -0
  46. package/dist/renderers/CardGridRenderer.d.ts.map +1 -0
  47. package/dist/renderers/CardGridRenderer.js +321 -0
  48. package/dist/renderers/CardGridRenderer.js.map +1 -0
  49. package/dist/renderers/CardRenderer.d.ts +27 -0
  50. package/dist/renderers/CardRenderer.d.ts.map +1 -0
  51. package/dist/renderers/CardRenderer.js +337 -0
  52. package/dist/renderers/CardRenderer.js.map +1 -0
  53. package/dist/renderers/IdeaEvolutionRenderer.d.ts +16 -0
  54. package/dist/renderers/IdeaEvolutionRenderer.d.ts.map +1 -0
  55. package/dist/renderers/IdeaEvolutionRenderer.js +187 -0
  56. package/dist/renderers/IdeaEvolutionRenderer.js.map +1 -0
  57. package/dist/renderers/ProseRenderer.d.ts +10 -0
  58. package/dist/renderers/ProseRenderer.d.ts.map +1 -0
  59. package/dist/renderers/ProseRenderer.js +42 -0
  60. package/dist/renderers/ProseRenderer.js.map +1 -0
  61. package/dist/renderers/RawJsonRenderer.d.ts +8 -0
  62. package/dist/renderers/RawJsonRenderer.d.ts.map +1 -0
  63. package/dist/renderers/RawJsonRenderer.js +17 -0
  64. package/dist/renderers/RawJsonRenderer.js.map +1 -0
  65. package/dist/renderers/StatSummaryRenderer.d.ts +12 -0
  66. package/dist/renderers/StatSummaryRenderer.d.ts.map +1 -0
  67. package/dist/renderers/StatSummaryRenderer.js +93 -0
  68. package/dist/renderers/StatSummaryRenderer.js.map +1 -0
  69. package/dist/renderers/SynthesisRenderer.d.ts +15 -0
  70. package/dist/renderers/SynthesisRenderer.d.ts.map +1 -0
  71. package/dist/renderers/SynthesisRenderer.js +60 -0
  72. package/dist/renderers/SynthesisRenderer.js.map +1 -0
  73. package/dist/renderers/TableRenderer.d.ts +19 -0
  74. package/dist/renderers/TableRenderer.d.ts.map +1 -0
  75. package/dist/renderers/TableRenderer.js +273 -0
  76. package/dist/renderers/TableRenderer.js.map +1 -0
  77. package/dist/styles/accordion.css +376 -0
  78. package/dist/styles/index.css +5 -0
  79. package/dist/styles/renderers.css +1049 -0
  80. package/dist/sub-renderers/SubRenderers.d.ts +73 -0
  81. package/dist/sub-renderers/SubRenderers.d.ts.map +1 -0
  82. package/dist/sub-renderers/SubRenderers.js +2462 -0
  83. package/dist/sub-renderers/SubRenderers.js.map +1 -0
  84. package/dist/tokens/DesignTokenContext.d.ts +40 -0
  85. package/dist/tokens/DesignTokenContext.d.ts.map +1 -0
  86. package/dist/tokens/DesignTokenContext.js +408 -0
  87. package/dist/tokens/DesignTokenContext.js.map +1 -0
  88. package/dist/types/designTokens.d.ts +220 -0
  89. package/dist/types/designTokens.d.ts.map +1 -0
  90. package/dist/types/designTokens.js +8 -0
  91. package/dist/types/designTokens.js.map +1 -0
  92. package/dist/types/index.d.ts +32 -0
  93. package/dist/types/index.d.ts.map +1 -0
  94. package/dist/types/index.js +5 -0
  95. package/dist/types/index.js.map +1 -0
  96. package/dist/types/styles.d.ts +38 -0
  97. package/dist/types/styles.d.ts.map +1 -0
  98. package/dist/types/styles.js +14 -0
  99. package/dist/types/styles.js.map +1 -0
  100. package/dist/utils/tokenFlattener.d.ts +14 -0
  101. package/dist/utils/tokenFlattener.d.ts.map +1 -0
  102. package/dist/utils/tokenFlattener.js +56 -0
  103. package/dist/utils/tokenFlattener.js.map +1 -0
  104. package/package.json +31 -0
  105. package/src/cells/TemplateCardCell.tsx +439 -0
  106. package/src/cells/index.ts +98 -0
  107. package/src/components/ConditionCards.tsx +109 -0
  108. package/src/components/EvidenceTrail.tsx +203 -0
  109. package/src/dispatch/SubRendererDispatch.tsx +282 -0
  110. package/src/hooks/useProseExtraction.ts +125 -0
  111. package/src/index.ts +82 -0
  112. package/src/provenance/ProvenanceSectionIcon.tsx +19 -0
  113. package/src/renderers/AccordionRenderer.tsx +609 -0
  114. package/src/renderers/CardGridRenderer.tsx +608 -0
  115. package/src/renderers/CardRenderer.tsx +517 -0
  116. package/src/renderers/ProseRenderer.tsx +85 -0
  117. package/src/renderers/RawJsonRenderer.tsx +37 -0
  118. package/src/renderers/StatSummaryRenderer.tsx +182 -0
  119. package/src/renderers/TableRenderer.tsx +470 -0
  120. package/src/styles/accordion.css +376 -0
  121. package/src/styles/index.css +5 -0
  122. package/src/styles/renderers.css +1049 -0
  123. package/src/sub-renderers/SubRenderers.tsx +3487 -0
  124. package/src/tokens/DesignTokenContext.tsx +502 -0
  125. package/src/types/designTokens.ts +236 -0
  126. package/src/types/index.ts +53 -0
  127. package/src/types/styles.ts +44 -0
  128. package/src/utils/tokenFlattener.ts +64 -0
@@ -0,0 +1,502 @@
1
+ /**
2
+ * DesignTokenContext - Provides design tokens to the component tree.
3
+ *
4
+ * Fetches tokens from the style school API endpoint, flattens them to CSS
5
+ * custom properties, and applies them to a wrapper div. Falls back to
6
+ * FALLBACK_TOKENS (a snapshot of the current humanist_craft visual values)
7
+ * when the API is unavailable.
8
+ *
9
+ * Usage:
10
+ * <DesignTokenProvider schoolKey="humanist_craft" jobId={jobId}>
11
+ * <YourComponents />
12
+ * </DesignTokenProvider>
13
+ *
14
+ * const { getCategoryColor, getSemanticColor, getLabel } = useDesignTokens();
15
+ */
16
+
17
+ import React, {
18
+ createContext,
19
+ useContext,
20
+ useState,
21
+ useEffect,
22
+ useRef,
23
+ useCallback,
24
+ useMemo,
25
+ } from 'react';
26
+ import {
27
+ DesignTokenSet,
28
+ SemanticTriple,
29
+ CategoricalItem,
30
+ SemanticTokens,
31
+ CategoricalTokens,
32
+ } from '../types/designTokens';
33
+ import { flattenTokens } from '../utils/tokenFlattener';
34
+
35
+ // ── API base URL ────────────────────────────────────────────────
36
+ // Consumer apps set this via env var. Supports both CRA and Next.js conventions.
37
+ const ANALYZER_V2_URL =
38
+ (typeof process !== 'undefined' && process.env?.REACT_APP_ANALYZER_V2_URL) ||
39
+ (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_ANALYZER_V2_URL) ||
40
+ 'http://localhost:8001';
41
+
42
+ // ── FALLBACK_TOKENS ─────────────────────────────────────────────
43
+ // Hardcoded snapshot of the current humanist_craft visual values.
44
+ // Extracted from:
45
+ // - webapp/src/constants/genealogyStyles.ts (all color maps)
46
+ // - webapp/src/pages/GenealogyPage.css (CSS custom properties)
47
+ // - webapp/src/components/renderers/AccordionRenderer.tsx (ENUM_COLORS)
48
+
49
+ const FALLBACK_TOKENS: DesignTokenSet = {
50
+ school_key: 'humanist_craft',
51
+ school_name: 'Humanist Craft',
52
+ generated_at: '2026-03-02T00:00:00Z',
53
+ version: '1.0.0-fallback',
54
+
55
+ primitives: {
56
+ color_primary: '#b5343a',
57
+ color_secondary: '#1e40af',
58
+ color_tertiary: '#166534',
59
+ color_accent: '#b5343a',
60
+ color_accent_alt: '#9a2c32',
61
+ color_background: '#ffffff',
62
+ color_highlight: 'rgba(181, 52, 58, 0.08)',
63
+ color_text: '#1a1d23',
64
+ color_muted: '#6b7280',
65
+ color_positive: '#16a34a',
66
+ color_negative: '#dc2626',
67
+ series_palette: [
68
+ '#b5343a', '#1e40af', '#166534', '#92400e', '#6b21a8',
69
+ '#155e75', '#9f1239', '#3730a3', '#9a3412', '#334155',
70
+ ],
71
+ font_primary: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
72
+ font_title: "'Georgia', 'Times New Roman', serif",
73
+ font_caption: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
74
+ font_number: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
75
+ },
76
+
77
+ surfaces: {
78
+ surface_default: '#ffffff',
79
+ surface_alt: '#f8f9fa',
80
+ surface_elevated: '#f5f3f0',
81
+ surface_inset: '#f0ede8',
82
+ border_default: '#e2e5e9',
83
+ border_light: '#eef0f2',
84
+ border_accent: '#b5343a',
85
+ text_default: '#1a1d23',
86
+ text_muted: '#6b7280',
87
+ text_faint: '#9ca3af',
88
+ text_on_accent: '#ffffff',
89
+ text_inverse: '#ffffff',
90
+ shadow_sm: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
91
+ shadow_md: '0 4px 6px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.04)',
92
+ shadow_lg: '0 10px 15px rgba(0,0,0,0.06), 0 4px 6px rgba(0,0,0,0.03)',
93
+ },
94
+
95
+ scales: {
96
+ // Typography sizes from GenealogyPage.css --type-* custom properties
97
+ type_display: '2rem',
98
+ type_heading: '1.375rem',
99
+ type_subheading: '1.125rem',
100
+ type_body: '0.9375rem',
101
+ type_caption: '0.8125rem',
102
+ type_label: '0.6875rem',
103
+ // Font weights from GenealogyPage.css --weight-* custom properties
104
+ weight_light: '300',
105
+ weight_regular: '400',
106
+ weight_medium: '500',
107
+ weight_semibold: '600',
108
+ weight_bold: '700',
109
+ weight_title: '700',
110
+ // Line heights from GenealogyPage.css --leading-* custom properties
111
+ leading_tight: '1.2',
112
+ leading_normal: '1.5',
113
+ leading_loose: '1.8',
114
+ // Spacing scale from GenealogyPage.css --space-* custom properties
115
+ space_2xs: '0.125rem',
116
+ space_xs: '0.25rem',
117
+ space_sm: '0.5rem',
118
+ space_md: '1rem',
119
+ space_lg: '1.5rem',
120
+ space_xl: '2rem',
121
+ space_2xl: '3rem',
122
+ space_3xl: '4rem',
123
+ // Border radii from GenealogyPage.css --radius-* custom properties
124
+ radius_sm: '4px',
125
+ radius_md: '8px',
126
+ radius_lg: '12px',
127
+ radius_xl: '16px',
128
+ radius_pill: '9999px',
129
+ },
130
+
131
+ semantic: {
132
+ // Severity - from SEVERITY_STYLES in genealogyStyles.ts + ENUM_COLORS in AccordionRenderer.tsx
133
+ severity_high: { bg: 'rgba(239, 68, 68, 0.15)', text: '#dc2626', border: '#fecaca' },
134
+ severity_medium: { bg: 'rgba(234, 179, 8, 0.15)', text: '#ca8a04', border: '#fde68a' },
135
+ severity_low: { bg: 'rgba(34, 197, 94, 0.15)', text: '#16a34a', border: '#bbf7d0' },
136
+ // Visibility - from ENUM_COLORS in AccordionRenderer.tsx
137
+ visibility_explicit: { bg: 'rgba(34, 197, 94, 0.12)', text: '#16a34a', border: '#bbf7d0' },
138
+ visibility_implicit: { bg: 'rgba(234, 179, 8, 0.12)', text: '#ca8a04', border: '#fde68a' },
139
+ visibility_hidden: { bg: 'rgba(239, 68, 68, 0.12)', text: '#dc2626', border: '#fecaca' },
140
+ // Modality - from ENUM_COLORS in AccordionRenderer.tsx
141
+ modality_ontological: { bg: '#eff6ff', text: '#1e40af', border: '#bfdbfe' },
142
+ modality_methodological: { bg: '#f0fdf4', text: '#166534', border: '#bbf7d0' },
143
+ modality_normative: { bg: '#fef2f2', text: '#991b1b', border: '#fecaca' },
144
+ modality_epistemic: { bg: '#fdf4ff', text: '#86198f', border: '#e9d5ff' },
145
+ modality_causal: { bg: '#fff7ed', text: '#9a3412', border: '#fed7aa' },
146
+ // Change types - from ENUM_COLORS in AccordionRenderer.tsx
147
+ change_stable: { bg: 'rgba(34, 197, 94, 0.12)', text: '#16a34a', border: '#bbf7d0' },
148
+ change_narrowed: { bg: 'rgba(234, 179, 8, 0.12)', text: '#ca8a04', border: '#fde68a' },
149
+ change_expanded: { bg: 'rgba(59, 130, 246, 0.12)', text: '#2563eb', border: '#93c5fd' },
150
+ change_inverted: { bg: 'rgba(239, 68, 68, 0.12)', text: '#dc2626', border: '#fecaca' },
151
+ change_metaphorized: { bg: 'rgba(139, 92, 246, 0.12)', text: '#7c3aed', border: '#c4b5fd' },
152
+ // Centrality - derived from severity pattern (core=high, supporting=medium, peripheral=low)
153
+ centrality_core: { bg: 'rgba(239, 68, 68, 0.12)', text: '#dc2626', border: '#fecaca' },
154
+ centrality_supporting: { bg: 'rgba(59, 130, 246, 0.12)', text: '#2563eb', border: '#93c5fd' },
155
+ centrality_peripheral: { bg: 'rgba(148, 163, 184, 0.12)', text: '#64748b', border: '#cbd5e1' },
156
+ // Status
157
+ status_completed: { bg: 'rgba(34, 197, 94, 0.12)', text: '#16a34a', border: '#bbf7d0' },
158
+ status_running: { bg: 'rgba(59, 130, 246, 0.12)', text: '#2563eb', border: '#93c5fd' },
159
+ status_pending: { bg: 'rgba(234, 179, 8, 0.12)', text: '#ca8a04', border: '#fde68a' },
160
+ status_failed: { bg: 'rgba(239, 68, 68, 0.12)', text: '#dc2626', border: '#fecaca' },
161
+ },
162
+
163
+ categorical: {
164
+ // ── Tactics (from TACTIC_COLORS + TACTIC_LABELS in genealogyStyles.ts) ──
165
+ tactic_conceptual_recycling: { bg: '#eff6ff', text: '#1e40af', border: '#bfdbfe', label: 'Conceptual Recycling' },
166
+ tactic_silent_revision: { bg: '#fef2f2', text: '#991b1b', border: '#fecaca', label: 'Silent Revision' },
167
+ tactic_selective_continuity: { bg: '#fffbeb', text: '#92400e', border: '#fde68a', label: 'Selective Continuity' },
168
+ tactic_retroactive_framing: { bg: '#faf5ff', text: '#6b21a8', border: '#e9d5ff', label: 'Retroactive Framing' },
169
+ tactic_escalation: { bg: '#fff7ed', text: '#9a3412', border: '#fed7aa', label: 'Escalation' },
170
+ tactic_narrative_bootstrapping: { bg: '#f0fdf4', text: '#166534', border: '#bbf7d0', label: 'Narrative Bootstrapping' },
171
+ tactic_framework_migration: { bg: '#ecfeff', text: '#155e75', border: '#a5f3fc', label: 'Framework Migration' },
172
+ tactic_condition_shift: { bg: '#fff1f2', text: '#9f1239', border: '#fecdd3', label: 'Condition Shift' },
173
+ tactic_biographical_teleology: { bg: '#eef2ff', text: '#3730a3', border: '#c7d2fe', label: 'Biographical Teleology' },
174
+ tactic_strategic_amnesia: { bg: '#f8fafc', text: '#334155', border: '#e2e8f0', label: 'Strategic Amnesia' },
175
+
176
+ // ── Forms (from FORM_LABELS in genealogyStyles.ts) ──
177
+ // FORM_LABELS has { label, color }; we derive bg/text/border from the single color
178
+ form_proto_form: { bg: 'rgba(148, 163, 184, 0.12)', text: '#94a3b8', border: '#cbd5e1', label: 'Proto-form' },
179
+ form_full_form: { bg: 'rgba(34, 197, 94, 0.12)', text: '#22c55e', border: '#86efac', label: 'Full Form' },
180
+ form_contradictory_form: { bg: 'rgba(239, 68, 68, 0.12)', text: '#ef4444', border: '#fca5a5', label: 'Contradictory' },
181
+ form_absent_but_implied: { bg: 'rgba(245, 158, 11, 0.12)', text: '#f59e0b', border: '#fde68a', label: 'Absent (Implied)' },
182
+ form_different_framing: { bg: 'rgba(139, 92, 246, 0.12)', text: '#8b5cf6', border: '#c4b5fd', label: 'Different Framing' },
183
+
184
+ // ── Idea types (from IDEA_FORM_COLORS in genealogyStyles.ts) ──
185
+ idea_central_thesis: { bg: '#fef2f2', text: '#991b1b', border: '#f87171', label: 'Central Thesis' },
186
+ idea_supporting_argument: { bg: '#eff6ff', text: '#1e40af', border: '#60a5fa', label: 'Supporting Argument' },
187
+ idea_supporting_framework: { bg: '#eff6ff', text: '#1e40af', border: '#60a5fa', label: 'Supporting Framework' },
188
+ idea_methodological_tool: { bg: '#f0fdf4', text: '#166534', border: '#4ade80', label: 'Methodological Tool' },
189
+ idea_conceptual_framework: { bg: '#faf5ff', text: '#6b21a8', border: '#a78bfa', label: 'Conceptual Framework' },
190
+ idea_empirical_finding: { bg: '#fffbeb', text: '#92400e', border: '#fbbf24', label: 'Empirical Finding' },
191
+ idea_normative_claim: { bg: '#fff1f2', text: '#9f1239', border: '#fb7185', label: 'Normative Claim' },
192
+ idea_analytical_distinction: { bg: '#ecfeff', text: '#155e75', border: '#22d3ee', label: 'Analytical Distinction' },
193
+ idea_rhetorical_strategy: { bg: '#fff7ed', text: '#9a3412', border: '#fb923c', label: 'Rhetorical Strategy' },
194
+ idea_rhetorical_device: { bg: '#fff7ed', text: '#9a3412', border: '#fb923c', label: 'Rhetorical Device' },
195
+ idea_historical_analysis: { bg: '#eef2ff', text: '#3730a3', border: '#a5b4fc', label: 'Historical Analysis' },
196
+
197
+ // ── Condition types (from CONDITION_TYPE_COLORS in genealogyStyles.ts) ──
198
+ // CONDITION_TYPE_COLORS has a single color; we derive bg (lighten) and text (as-is) and border (as-is)
199
+ condition_conceptual_foundation: { bg: 'rgba(59, 130, 246, 0.12)', text: '#3b82f6', border: '#93c5fd', label: 'Conceptual Foundation' },
200
+ condition_audience_preparation: { bg: 'rgba(139, 92, 246, 0.12)', text: '#8b5cf6', border: '#c4b5fd', label: 'Audience Preparation' },
201
+ condition_authority_establishment: { bg: 'rgba(239, 68, 68, 0.12)', text: '#ef4444', border: '#fca5a5', label: 'Authority Establishment' },
202
+ condition_framework_provision: { bg: 'rgba(34, 197, 94, 0.12)', text: '#22c55e', border: '#86efac', label: 'Framework Provision' },
203
+ condition_problem_definition: { bg: 'rgba(245, 158, 11, 0.12)', text: '#f59e0b', border: '#fde68a', label: 'Problem Definition' },
204
+ condition_methodological_precedent: { bg: 'rgba(6, 182, 212, 0.12)', text: '#06b6d4', border: '#67e8f9', label: 'Methodological Precedent' },
205
+ condition_intellectual_toolkit: { bg: 'rgba(236, 72, 153, 0.12)', text: '#ec4899', border: '#f9a8d4', label: 'Intellectual Toolkit' },
206
+ condition_cross_domain_transfer: { bg: 'rgba(20, 184, 166, 0.12)', text: '#14b8a6', border: '#5eead4', label: 'Cross-Domain Transfer' },
207
+
208
+ // ── Relationship types (from RELATIONSHIP_TYPE_STYLES in genealogyStyles.ts) ──
209
+ relationship_direct_precursor: { bg: '#eff6ff', text: '#1e40af', border: '#93c5fd', label: 'Direct Precursor' },
210
+ relationship_methodological_ancestor: { bg: '#f0fdf4', text: '#166534', border: '#86efac', label: 'Methodological Ancestor' },
211
+ relationship_counter_position: { bg: '#fff1f2', text: '#9f1239', border: '#fda4af', label: 'Counter-Position' },
212
+ relationship_indirect_contextualizer: { bg: '#fdf4ff', text: '#86198f', border: '#e879f9', label: 'Indirect Contextualizer' },
213
+ relationship_stylistic_influence: { bg: '#fffbeb', text: '#92400e', border: '#fde68a', label: 'Stylistic Influence' },
214
+ relationship_conceptual_sibling: { bg: '#ecfeff', text: '#155e75', border: '#a5f3fc', label: 'Conceptual Sibling' },
215
+ relationship_different_field_relevant: { bg: '#f0fdfa', text: '#115e59', border: '#5eead4', label: 'Different Field' },
216
+ relationship_tangential: { bg: '#f8fafc', text: '#64748b', border: '#cbd5e1', label: 'Tangential' },
217
+
218
+ // ── Strength (from RELATIONSHIP_STRENGTH_STYLES in genealogyStyles.ts) ──
219
+ strength_strong: { bg: 'rgba(34, 197, 94, 0.12)', text: '#16a34a', border: '#86efac', label: 'Strong' },
220
+ strength_moderate: { bg: 'rgba(234, 179, 8, 0.12)', text: '#ca8a04', border: '#fde68a', label: 'Moderate' },
221
+ strength_weak: { bg: 'rgba(148, 163, 184, 0.12)', text: '#64748b', border: '#cbd5e1', label: 'Weak' },
222
+
223
+ // ── Awareness (from AWARENESS_LABELS in genealogyStyles.ts) ──
224
+ awareness_explicit: { bg: 'rgba(34, 197, 94, 0.12)', text: '#22c55e', border: '#86efac', label: 'Explicit' },
225
+ awareness_implicit: { bg: 'rgba(245, 158, 11, 0.12)', text: '#f59e0b', border: '#fde68a', label: 'Implicit' },
226
+ awareness_unconscious: { bg: 'rgba(239, 68, 68, 0.12)', text: '#ef4444', border: '#fca5a5', label: 'Unconscious' },
227
+
228
+ // ── Pattern types (from PATTERN_TYPE_LABELS in genealogyStyles.ts) ──
229
+ // PATTERN_TYPE_LABELS only has labels, no colors; use neutral styling
230
+ pattern_analytical_method: { bg: '#eff6ff', text: '#1e40af', border: '#bfdbfe', label: 'Analytical Method' },
231
+ pattern_cognitive_habit: { bg: '#faf5ff', text: '#6b21a8', border: '#e9d5ff', label: 'Cognitive Habit' },
232
+ pattern_recurring_metaphor: { bg: '#fff7ed', text: '#9a3412', border: '#fed7aa', label: 'Recurring Metaphor' },
233
+ pattern_problem_solving_approach: { bg: '#f0fdf4', text: '#166534', border: '#bbf7d0', label: 'Problem-Solving' },
234
+ pattern_theoretical_framework: { bg: '#ecfeff', text: '#155e75', border: '#a5f3fc', label: 'Theoretical Framework' },
235
+ pattern_argumentative_structure: { bg: '#fef2f2', text: '#991b1b', border: '#fecaca', label: 'Argumentative Structure' },
236
+ pattern_epistemic_commitment: { bg: '#eef2ff', text: '#3730a3', border: '#c7d2fe', label: 'Epistemic Commitment' },
237
+ },
238
+
239
+ components: {
240
+ // ── Page accent (from GenealogyPage.css: #b5343a used throughout) ──
241
+ page_accent: '#b5343a',
242
+ page_accent_hover: '#9a2c32',
243
+ page_accent_bg: 'rgba(181, 52, 58, 0.08)',
244
+ // ── Section headers ──
245
+ section_header_bg: '#f8f9fa',
246
+ section_header_border: '#e2e5e9',
247
+ section_header_text: '#1a1d23',
248
+ // ── Cards (from GenealogyPage.css gen-config-card and --bg-surface/--border-color) ──
249
+ card_bg: '#ffffff',
250
+ card_border: '#e2e5e9',
251
+ card_border_accent: '#b5343a',
252
+ card_header_bg: '#f5f3f0',
253
+ card_header_text: '#1a1a1a',
254
+ // ── Chip weight stops (gradient from neutral to accent) ──
255
+ chip_weight_0_bg: '#f8fafc',
256
+ chip_weight_0_text: '#94a3b8',
257
+ chip_weight_0_border: '#e2e8f0',
258
+ chip_weight_25_bg: '#eff6ff',
259
+ chip_weight_25_text: '#3b82f6',
260
+ chip_weight_25_border: '#bfdbfe',
261
+ chip_weight_50_bg: '#fef3c7',
262
+ chip_weight_50_text: '#d97706',
263
+ chip_weight_50_border: '#fde68a',
264
+ chip_weight_75_bg: '#fee2e2',
265
+ chip_weight_75_text: '#dc2626',
266
+ chip_weight_75_border: '#fecaca',
267
+ chip_weight_100_bg: 'rgba(181, 52, 58, 0.15)',
268
+ chip_weight_100_text: '#b5343a',
269
+ chip_weight_100_border: '#b5343a',
270
+ chip_header_bg: '#f5f3f0',
271
+ chip_header_text: '#1a1a1a',
272
+ // ── Prose styling ──
273
+ prose_lede_color: '#1a1d23',
274
+ prose_lede_weight: '500',
275
+ prose_blockquote_border: '#b5343a',
276
+ prose_blockquote_bg: 'rgba(181, 52, 58, 0.04)',
277
+ // ── Timeline components ──
278
+ timeline_connector: '#e2e5e9',
279
+ timeline_connector_width: '2px',
280
+ timeline_node_bg: '#ffffff',
281
+ timeline_node_border: '#b5343a',
282
+ // ── Evidence markers ──
283
+ evidence_dot_bg: '#b5343a',
284
+ evidence_connector: '#e2e5e9',
285
+ // ── Stats ──
286
+ stat_number_color: '#b5343a',
287
+ stat_label_color: '#6b7280',
288
+ stat_card_bg: '#f8f9fa',
289
+ },
290
+ };
291
+
292
+ // ── Context types ───────────────────────────────────────────────
293
+
294
+ interface DesignTokenContextValue {
295
+ tokens: DesignTokenSet;
296
+ loading: boolean;
297
+ schoolKey: string;
298
+ getCategoryColor: (category: string, key: string) => CategoricalItem | null;
299
+ getSemanticColor: (scale: string, level: string) => SemanticTriple | null;
300
+ getLabel: (category: string, key: string) => string;
301
+ getChipWeight: (weight: number) => { bg: string; text: string; border: string };
302
+ }
303
+
304
+ interface DesignTokenProviderProps {
305
+ schoolKey: string;
306
+ jobId?: string;
307
+ children: React.ReactNode;
308
+ }
309
+
310
+ // ── Context ─────────────────────────────────────────────────────
311
+
312
+ const defaultContextValue: DesignTokenContextValue = {
313
+ tokens: FALLBACK_TOKENS,
314
+ loading: false,
315
+ schoolKey: 'humanist_craft',
316
+ getCategoryColor: () => null,
317
+ getSemanticColor: () => null,
318
+ getLabel: () => '',
319
+ getChipWeight: () => ({ bg: '#f8fafc', text: '#94a3b8', border: '#e2e8f0' }),
320
+ };
321
+
322
+ const DesignTokenContext = createContext<DesignTokenContextValue>(defaultContextValue);
323
+
324
+ // ── Provider component ──────────────────────────────────────────
325
+
326
+ export function DesignTokenProvider({ schoolKey, jobId, children }: DesignTokenProviderProps) {
327
+ const [tokens, setTokens] = useState<DesignTokenSet>(FALLBACK_TOKENS);
328
+ const [loading, setLoading] = useState(false);
329
+ const wrapperRef = useRef<HTMLDivElement>(null);
330
+
331
+ // Track previous CSS vars so we can clean them up on token change
332
+ const appliedVarsRef = useRef<string[]>([]);
333
+
334
+ // Fetch tokens when schoolKey changes
335
+ useEffect(() => {
336
+ if (!schoolKey) return;
337
+
338
+ // If it's the fallback school and we already have fallback tokens, skip fetch
339
+ // (still try the API, but don't block on it)
340
+ let cancelled = false;
341
+ setLoading(true);
342
+
343
+ async function fetchTokens() {
344
+ try {
345
+ const resp = await fetch(
346
+ `${ANALYZER_V2_URL}/v1/styles/tokens/${encodeURIComponent(schoolKey)}`
347
+ );
348
+ if (cancelled) return;
349
+
350
+ if (resp.ok) {
351
+ const data: DesignTokenSet = await resp.json();
352
+ setTokens(data);
353
+
354
+ // Persist school choice if jobId provided
355
+ if (jobId) {
356
+ try {
357
+ localStorage.setItem(`style_school_${jobId}`, schoolKey);
358
+ } catch {
359
+ // localStorage may be unavailable
360
+ }
361
+ }
362
+ } else {
363
+ console.warn(
364
+ `[DesignTokens] Failed to fetch tokens for "${schoolKey}" (${resp.status}), using fallback`
365
+ );
366
+ setTokens(FALLBACK_TOKENS);
367
+ }
368
+ } catch (err) {
369
+ if (cancelled) return;
370
+ console.warn(
371
+ `[DesignTokens] Network error fetching tokens for "${schoolKey}", using fallback:`,
372
+ err
373
+ );
374
+ setTokens(FALLBACK_TOKENS);
375
+ } finally {
376
+ if (!cancelled) setLoading(false);
377
+ }
378
+ }
379
+
380
+ fetchTokens();
381
+ return () => {
382
+ cancelled = true;
383
+ };
384
+ }, [schoolKey, jobId]);
385
+
386
+ // Apply CSS vars when tokens change
387
+ useEffect(() => {
388
+ const vars = flattenTokens(tokens);
389
+ const el = wrapperRef.current;
390
+ if (!el) return;
391
+
392
+ // Remove previously applied vars that are no longer in the new set
393
+ for (const oldVar of appliedVarsRef.current) {
394
+ if (!(oldVar in vars)) {
395
+ el.style.removeProperty(oldVar);
396
+ }
397
+ }
398
+
399
+ // Apply new vars
400
+ for (const [prop, value] of Object.entries(vars)) {
401
+ el.style.setProperty(prop, value);
402
+ }
403
+
404
+ appliedVarsRef.current = Object.keys(vars);
405
+ }, [tokens]);
406
+
407
+ // ── Helper: getCategoryColor ──────────────────────────────────
408
+ const getCategoryColor = useCallback(
409
+ (category: string, key: string): CategoricalItem | null => {
410
+ const lookupKey = `${category}_${key}` as keyof CategoricalTokens;
411
+ const item = tokens.categorical[lookupKey];
412
+ return item || null;
413
+ },
414
+ [tokens]
415
+ );
416
+
417
+ // ── Helper: getSemanticColor ──────────────────────────────────
418
+ const getSemanticColor = useCallback(
419
+ (scale: string, level: string): SemanticTriple | null => {
420
+ const lookupKey = `${scale}_${level}` as keyof SemanticTokens;
421
+ const item = tokens.semantic[lookupKey];
422
+ return item || null;
423
+ },
424
+ [tokens]
425
+ );
426
+
427
+ // ── Helper: getLabel ──────────────────────────────────────────
428
+ const getLabel = useCallback(
429
+ (category: string, key: string): string => {
430
+ const lookupKey = `${category}_${key}` as keyof CategoricalTokens;
431
+ const item = tokens.categorical[lookupKey];
432
+ return item?.label || key.replace(/_/g, ' ');
433
+ },
434
+ [tokens]
435
+ );
436
+
437
+ // ── Helper: getChipWeight ─────────────────────────────────────
438
+ // Maps a 0-1 weight to the nearest chip weight stop (0, 25, 50, 75, 100)
439
+ const getChipWeight = useCallback(
440
+ (weight: number): { bg: string; text: string; border: string } => {
441
+ const pct = Math.round(weight * 100);
442
+ const stops = [0, 25, 50, 75, 100];
443
+ let nearest = 0;
444
+ let minDist = Infinity;
445
+ for (const stop of stops) {
446
+ const dist = Math.abs(pct - stop);
447
+ if (dist < minDist) {
448
+ minDist = dist;
449
+ nearest = stop;
450
+ }
451
+ }
452
+
453
+ const comp = tokens.components;
454
+ switch (nearest) {
455
+ case 0:
456
+ return { bg: comp.chip_weight_0_bg, text: comp.chip_weight_0_text, border: comp.chip_weight_0_border };
457
+ case 25:
458
+ return { bg: comp.chip_weight_25_bg, text: comp.chip_weight_25_text, border: comp.chip_weight_25_border };
459
+ case 50:
460
+ return { bg: comp.chip_weight_50_bg, text: comp.chip_weight_50_text, border: comp.chip_weight_50_border };
461
+ case 75:
462
+ return { bg: comp.chip_weight_75_bg, text: comp.chip_weight_75_text, border: comp.chip_weight_75_border };
463
+ case 100:
464
+ return { bg: comp.chip_weight_100_bg, text: comp.chip_weight_100_text, border: comp.chip_weight_100_border };
465
+ default:
466
+ return { bg: comp.chip_weight_0_bg, text: comp.chip_weight_0_text, border: comp.chip_weight_0_border };
467
+ }
468
+ },
469
+ [tokens]
470
+ );
471
+
472
+ // ── Memoize context value ─────────────────────────────────────
473
+ const contextValue = useMemo<DesignTokenContextValue>(
474
+ () => ({
475
+ tokens,
476
+ loading,
477
+ schoolKey,
478
+ getCategoryColor,
479
+ getSemanticColor,
480
+ getLabel,
481
+ getChipWeight,
482
+ }),
483
+ [tokens, loading, schoolKey, getCategoryColor, getSemanticColor, getLabel, getChipWeight]
484
+ );
485
+
486
+ return (
487
+ <DesignTokenContext.Provider value={contextValue}>
488
+ <div ref={wrapperRef} className="design-token-wrapper" style={{ display: 'contents' }}>
489
+ {children}
490
+ </div>
491
+ </DesignTokenContext.Provider>
492
+ );
493
+ }
494
+
495
+ // ── Hook ────────────────────────────────────────────────────────
496
+
497
+ export function useDesignTokens(): DesignTokenContextValue {
498
+ return useContext(DesignTokenContext);
499
+ }
500
+
501
+ // ── Export fallback for testing / direct use ────────────────────
502
+ export { FALLBACK_TOKENS };