@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,439 @@
1
+ /**
2
+ * TemplateCardCell -- Schema-driven card renderer.
3
+ *
4
+ * Instead of hardcoding field→UI mapping in React, this cell renderer
5
+ * interprets a `card_template` JSON structure from the view definition.
6
+ * Each "slot" in the template maps to an atomic rendering block:
7
+ *
8
+ * badge_row -- Row of colored badges (category or semantic colors)
9
+ * heading -- Title text, optionally hidden when it matches a label
10
+ * prose -- Paragraph text with optional subdued state
11
+ * chip_list -- Array of string chips with optional link hints
12
+ * evidence_trail -- Multi-step evidence chain (prior -> current -> assessment)
13
+ * key_value -- Key-value pairs rendered as a compact table
14
+ * separator -- Visual divider
15
+ *
16
+ * Template schema (in renderer_config.card_template):
17
+ * {
18
+ * wrapper_class?: string, -- CSS class for outer div
19
+ * border_accent?: ColorRef, -- Border-left color from design tokens
20
+ * border_weight_field?: string, -- Field to read for border thickness
21
+ * border_weight_map?: Record<string, string>, -- value -> CSS width
22
+ * slots: SlotConfig[] -- Ordered list of content blocks
23
+ * }
24
+ *
25
+ * New renderers = new JSON. No React code needed.
26
+ */
27
+
28
+ import React, { useState } from 'react';
29
+ import { CellRendererProps } from '../types';
30
+ import { useDesignTokens } from '../tokens/DesignTokenContext';
31
+ import { EvidenceTrail, EvidenceTrailStep, EvidenceTrailItem } from '../components/EvidenceTrail';
32
+
33
+ // ── Types ──────────────────────────────────────────────
34
+
35
+ interface ColorRef {
36
+ category: string;
37
+ key_field: string;
38
+ }
39
+
40
+ interface BadgeConfig {
41
+ field: string;
42
+ color_mode: 'category' | 'semantic';
43
+ color_category?: string;
44
+ color_scale?: string;
45
+ transform?: 'lowercase' | 'uppercase' | 'titlecase';
46
+ value_map?: Record<string, string>;
47
+ default_value?: string;
48
+ }
49
+
50
+ interface EvidenceStepConfig {
51
+ label: string;
52
+ field: string;
53
+ variant: 'prior' | 'current' | 'assessment';
54
+ item_title_field?: string;
55
+ item_quote_field?: string;
56
+ item_cite_field?: string;
57
+ is_text?: boolean;
58
+ }
59
+
60
+ interface SlotConfig {
61
+ type: string;
62
+ field?: string;
63
+ label?: string;
64
+ // badge_row
65
+ badges?: BadgeConfig[];
66
+ // heading
67
+ hide_when_matches_label?: ColorRef;
68
+ // prose
69
+ subdued_when?: { field: string; equals: string };
70
+ // chip_list
71
+ link_title_pattern?: string;
72
+ // evidence_trail
73
+ steps?: EvidenceStepConfig[];
74
+ accent_from?: ColorRef;
75
+ collapsed_by_default?: boolean;
76
+ // key_value
77
+ entries?: Array<{ field: string; label: string }>;
78
+ }
79
+
80
+ interface CardTemplate {
81
+ wrapper_class?: string;
82
+ border_accent?: ColorRef;
83
+ border_weight_field?: string;
84
+ border_weight_map?: Record<string, string>;
85
+ slots: SlotConfig[];
86
+ }
87
+
88
+ type GetCategoryColor = (cat: string, key: string) => { bg: string; text: string; border: string; label?: string } | null;
89
+ type GetSemanticColor = (scale: string, level: string) => { bg: string; text: string; border: string } | null;
90
+ type GetLabel = (cat: string, key: string) => string;
91
+
92
+ // ── Block: badge_row ───────────────────────────────────
93
+
94
+ function BadgeRowBlock({ badges, item, getCategoryColor, getSemanticColor, getLabel }: {
95
+ badges: BadgeConfig[];
96
+ item: Record<string, unknown>;
97
+ getCategoryColor: GetCategoryColor;
98
+ getSemanticColor: GetSemanticColor;
99
+ getLabel: GetLabel;
100
+ }) {
101
+ return (
102
+ <div className="ar-card-badge-row">
103
+ {badges.map((badge, idx) => {
104
+ const rawValue = String(item[badge.field] || badge.default_value || '');
105
+ if (!rawValue) return null;
106
+
107
+ let displayValue = rawValue;
108
+ let colors: { bg: string; text: string; border?: string } | null = null;
109
+
110
+ if (badge.color_mode === 'category' && badge.color_category) {
111
+ colors = getCategoryColor(badge.color_category, rawValue);
112
+ displayValue = getLabel(badge.color_category, rawValue) || rawValue.replace(/_/g, ' ');
113
+ } else if (badge.color_mode === 'semantic' && badge.color_scale) {
114
+ const key = rawValue.toLowerCase();
115
+ const tokenKey = badge.value_map?.[key] || key;
116
+ colors = getSemanticColor(badge.color_scale, tokenKey);
117
+ displayValue = key;
118
+ }
119
+
120
+ if (badge.transform === 'lowercase') displayValue = displayValue.toLowerCase();
121
+ else if (badge.transform === 'uppercase') displayValue = displayValue.toUpperCase();
122
+ else if (badge.transform === 'titlecase') displayValue = displayValue.replace(/\b\w/g, c => c.toUpperCase());
123
+
124
+ const fallback = { bg: '#f8fafc', text: '#334155', border: '#e2e8f0' };
125
+ const c = colors || fallback;
126
+
127
+ const isSemantic = badge.color_mode === 'semantic';
128
+ const className = isSemantic
129
+ ? `ar-severity-badge ar-severity-badge--${rawValue.toLowerCase()}`
130
+ : 'ar-card-type-badge';
131
+
132
+ return (
133
+ <span
134
+ key={idx}
135
+ className={className}
136
+ style={{ background: c.bg, color: c.text, borderColor: c.border }}
137
+ >
138
+ {displayValue}
139
+ </span>
140
+ );
141
+ })}
142
+ </div>
143
+ );
144
+ }
145
+
146
+ // ── Block: heading ─────────────────────────────────────
147
+
148
+ function HeadingBlock({ field, item, hideWhenMatchesLabel, getLabel }: {
149
+ field: string;
150
+ item: Record<string, unknown>;
151
+ hideWhenMatchesLabel?: ColorRef;
152
+ getLabel: GetLabel;
153
+ }) {
154
+ const text = String(item[field] || '');
155
+ if (!text) return null;
156
+
157
+ if (hideWhenMatchesLabel) {
158
+ const keyValue = String(item[hideWhenMatchesLabel.key_field] || '');
159
+ const label = getLabel(hideWhenMatchesLabel.category, keyValue);
160
+ if (text === label) return null;
161
+ }
162
+
163
+ return <h4 className="ar-card-heading">{text}</h4>;
164
+ }
165
+
166
+ // ── Block: prose ───────────────────────────────────────
167
+
168
+ function ProseBlock({ field, item, subduedWhen }: {
169
+ field: string;
170
+ item: Record<string, unknown>;
171
+ subduedWhen?: { field: string; equals: string };
172
+ }) {
173
+ const text = String(item[field] || '');
174
+ if (!text) return null;
175
+
176
+ const isSubdued = subduedWhen
177
+ && String(item[subduedWhen.field] || '').toLowerCase() === subduedWhen.equals;
178
+
179
+ return (
180
+ <p className={`ar-card-prose${isSubdued ? ' ar-card-prose--subdued' : ''}`}>
181
+ {text}
182
+ </p>
183
+ );
184
+ }
185
+
186
+ // ── Block: chip_list ───────────────────────────────────
187
+
188
+ function ChipListBlock({ field, item, label, linkTitlePattern }: {
189
+ field: string;
190
+ item: Record<string, unknown>;
191
+ label?: string;
192
+ linkTitlePattern?: string;
193
+ }) {
194
+ const values = item[field] as string[] | undefined;
195
+ if (!values || !Array.isArray(values) || values.length === 0) return null;
196
+
197
+ return (
198
+ <div className="ar-card-chip-list">
199
+ {label && <span className="ar-card-section-label">{label}</span>}
200
+ <div className="gen-idea-tags">
201
+ {values.map(val => (
202
+ <span
203
+ key={val}
204
+ className="gen-idea-tag gen-idea-tag--linked"
205
+ title={linkTitlePattern ? linkTitlePattern.replace('{value}', val) : undefined}
206
+ >
207
+ {val}
208
+ </span>
209
+ ))}
210
+ </div>
211
+ </div>
212
+ );
213
+ }
214
+
215
+ // ── Block: evidence_trail ──────────────────────────────
216
+
217
+ function EvidenceTrailBlock({ steps, item, label, accentFrom, collapsedByDefault, getCategoryColor }: {
218
+ steps: EvidenceStepConfig[];
219
+ item: Record<string, unknown>;
220
+ label?: string;
221
+ accentFrom?: ColorRef;
222
+ collapsedByDefault?: boolean;
223
+ getCategoryColor: GetCategoryColor;
224
+ }) {
225
+ const [collapsed, setCollapsed] = useState(collapsedByDefault ?? false);
226
+
227
+ const trailSteps: EvidenceTrailStep[] = [];
228
+
229
+ for (const sc of steps) {
230
+ const raw = item[sc.field];
231
+ if (raw === undefined || raw === null || raw === '') continue;
232
+
233
+ const variant = sc.variant as EvidenceTrailStep['variant'];
234
+
235
+ if (sc.is_text || typeof raw === 'string') {
236
+ trailSteps.push({ label: sc.label, variant, text: String(raw) });
237
+ } else if (Array.isArray(raw)) {
238
+ const items: EvidenceTrailItem[] = raw.map((entry: unknown) => {
239
+ if (typeof entry === 'string') return { quote: entry };
240
+ if (typeof entry === 'object' && entry !== null) {
241
+ const e = entry as Record<string, unknown>;
242
+ return {
243
+ title: sc.item_title_field ? String(e[sc.item_title_field] || '') : undefined,
244
+ quote: sc.item_quote_field ? String(e[sc.item_quote_field] || '') : undefined,
245
+ cite: sc.item_cite_field ? String(e[sc.item_cite_field] || '') : undefined,
246
+ };
247
+ }
248
+ return { quote: String(entry) };
249
+ });
250
+ if (items.length > 0) {
251
+ trailSteps.push({ label: sc.label, variant, items });
252
+ }
253
+ }
254
+ }
255
+
256
+ if (trailSteps.length === 0) return null;
257
+
258
+ let accentColor: string | undefined;
259
+ let borderColor: string | undefined;
260
+ if (accentFrom) {
261
+ const keyValue = String(item[accentFrom.key_field] || '');
262
+ const colors = getCategoryColor(accentFrom.category, keyValue);
263
+ if (colors) {
264
+ accentColor = colors.text;
265
+ borderColor = colors.border;
266
+ }
267
+ }
268
+
269
+ // Count total evidence pieces for the summary
270
+ const pieceCount = trailSteps.reduce((n, s) => n + (s.items?.length || (s.text ? 1 : 0)), 0);
271
+
272
+ return (
273
+ <div className="gen-evidence-trail">
274
+ <button
275
+ type="button"
276
+ className={`gen-evidence-toggle${collapsed ? ' gen-evidence-toggle--collapsed' : ''}`}
277
+ onClick={(e) => { e.stopPropagation(); setCollapsed(!collapsed); }}
278
+ >
279
+ <span className="gen-evidence-toggle-icon">{'\u25B6'}</span>
280
+ <span className="gen-evidence-toggle-label">
281
+ {label || 'Evidence Trail'}
282
+ </span>
283
+ <span className="gen-evidence-count">{pieceCount}</span>
284
+ </button>
285
+ {!collapsed && (
286
+ <EvidenceTrail steps={trailSteps} accentColor={accentColor} borderColor={borderColor} />
287
+ )}
288
+ </div>
289
+ );
290
+ }
291
+
292
+ // ── Block: key_value ───────────────────────────────────
293
+
294
+ function KeyValueBlock({ entries, item }: {
295
+ entries: Array<{ field: string; label: string }>;
296
+ item: Record<string, unknown>;
297
+ }) {
298
+ const pairs = entries
299
+ .map(e => ({ label: e.label, value: item[e.field] }))
300
+ .filter(p => p.value != null && p.value !== '');
301
+
302
+ if (pairs.length === 0) return null;
303
+
304
+ return (
305
+ <div className="gen-template-kv">
306
+ {pairs.map((p, idx) => (
307
+ <div key={idx} className="gen-template-kv-row">
308
+ <span className="gen-template-kv-label">{p.label}</span>
309
+ <span className="gen-template-kv-value">{String(p.value)}</span>
310
+ </div>
311
+ ))}
312
+ </div>
313
+ );
314
+ }
315
+
316
+ // ── Block: separator ───────────────────────────────────
317
+
318
+ function SeparatorBlock() {
319
+ return <hr className="gen-template-separator" />;
320
+ }
321
+
322
+ // ── Slot dispatcher ────────────────────────────────────
323
+
324
+ function TemplateSlot({ slot, item, getCategoryColor, getSemanticColor, getLabel }: {
325
+ slot: SlotConfig;
326
+ item: Record<string, unknown>;
327
+ getCategoryColor: GetCategoryColor;
328
+ getSemanticColor: GetSemanticColor;
329
+ getLabel: GetLabel;
330
+ }) {
331
+ switch (slot.type) {
332
+ case 'badge_row':
333
+ return slot.badges ? (
334
+ <BadgeRowBlock
335
+ badges={slot.badges}
336
+ item={item}
337
+ getCategoryColor={getCategoryColor}
338
+ getSemanticColor={getSemanticColor}
339
+ getLabel={getLabel}
340
+ />
341
+ ) : null;
342
+
343
+ case 'heading':
344
+ return slot.field ? (
345
+ <HeadingBlock
346
+ field={slot.field}
347
+ item={item}
348
+ hideWhenMatchesLabel={slot.hide_when_matches_label}
349
+ getLabel={getLabel}
350
+ />
351
+ ) : null;
352
+
353
+ case 'prose':
354
+ return slot.field ? (
355
+ <ProseBlock field={slot.field} item={item} subduedWhen={slot.subdued_when} />
356
+ ) : null;
357
+
358
+ case 'chip_list':
359
+ return slot.field ? (
360
+ <ChipListBlock
361
+ field={slot.field}
362
+ item={item}
363
+ label={slot.label}
364
+ linkTitlePattern={slot.link_title_pattern}
365
+ />
366
+ ) : null;
367
+
368
+ case 'evidence_trail':
369
+ return slot.steps ? (
370
+ <EvidenceTrailBlock
371
+ steps={slot.steps}
372
+ item={item}
373
+ label={slot.label}
374
+ accentFrom={slot.accent_from}
375
+ collapsedByDefault={slot.collapsed_by_default}
376
+ getCategoryColor={getCategoryColor}
377
+ />
378
+ ) : null;
379
+
380
+ case 'key_value':
381
+ return slot.entries ? (
382
+ <KeyValueBlock entries={slot.entries} item={item} />
383
+ ) : null;
384
+
385
+ case 'separator':
386
+ return <SeparatorBlock />;
387
+
388
+ default:
389
+ return null;
390
+ }
391
+ }
392
+
393
+ // ── Main component ─────────────────────────────────────
394
+
395
+ export function TemplateCardCell({ item, config }: CellRendererProps) {
396
+ const { getCategoryColor, getSemanticColor, getLabel } = useDesignTokens();
397
+
398
+ const template = config.card_template as CardTemplate | undefined;
399
+ if (!template || !template.slots) {
400
+ return <p className="gen-empty">No card template configured.</p>;
401
+ }
402
+
403
+ // Compute wrapper styles from template config
404
+ const wrapperClass = template.wrapper_class || '';
405
+ const severityField = template.border_weight_field;
406
+ const severityValue = severityField ? String(item[severityField] || '').toLowerCase() : '';
407
+ const severityModifier = severityValue ? ` ${wrapperClass}--${severityValue}` : '';
408
+
409
+ let borderStyle: React.CSSProperties | undefined;
410
+ if (template.border_accent) {
411
+ const keyValue = String(item[template.border_accent.key_field] || '');
412
+ const colors = getCategoryColor(template.border_accent.category, keyValue);
413
+ if (colors) {
414
+ const width = template.border_weight_map?.[severityValue] || '4px';
415
+ borderStyle = {
416
+ borderTopColor: colors.text,
417
+ borderTopWidth: width,
418
+ borderTopStyle: 'solid',
419
+ };
420
+ }
421
+ }
422
+
423
+ const className = `${wrapperClass}${severityModifier}`.trim() || undefined;
424
+
425
+ return (
426
+ <div className={className} style={borderStyle}>
427
+ {template.slots.map((slot, idx) => (
428
+ <TemplateSlot
429
+ key={idx}
430
+ slot={slot}
431
+ item={item}
432
+ getCategoryColor={getCategoryColor}
433
+ getSemanticColor={getSemanticColor}
434
+ getLabel={getLabel}
435
+ />
436
+ ))}
437
+ </div>
438
+ );
439
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Cell Renderer Registry — Domain-specific card content components.
3
+ */
4
+
5
+ import React from 'react';
6
+ import { TemplateCardCell } from './TemplateCardCell';
7
+ import type { CellRendererProps, CellRendererComponent } from '../types';
8
+
9
+ export type { CellRendererProps, CellRendererComponent };
10
+
11
+ /** Registry of cell renderers keyed by config.cell_renderer string */
12
+ export const cellRenderers: Record<string, CellRendererComponent> = {
13
+ template_card: TemplateCardCell,
14
+ };
15
+
16
+ // Field classification for intelligent rendering
17
+ const TITLE_FIELDS = ['name', 'title', 'finding', 'concept', 'idea', 'theme'];
18
+ const BODY_FIELDS = ['condition', 'description', 'analysis', 'significance', 'explanation', 'mechanism', 'details', 'summary', 'how_it_enables', 'how_it_constrains', 'effect'];
19
+ const EVIDENCE_FIELDS = ['evidence', 'reasoning', 'supporting_evidence', 'rationale', 'justification'];
20
+ const META_FIELDS = new Set(['_category', 'docKey', 'condition_id', 'id', 'index', 'order']);
21
+
22
+ function classifyField(key: string): 'title' | 'body' | 'evidence' | 'tag' | 'skip' {
23
+ if (META_FIELDS.has(key)) return 'skip';
24
+ if (TITLE_FIELDS.includes(key)) return 'title';
25
+ if (BODY_FIELDS.includes(key)) return 'body';
26
+ if (EVIDENCE_FIELDS.includes(key)) return 'evidence';
27
+ return 'tag';
28
+ }
29
+
30
+ function formatLabel(key: string): string {
31
+ return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
32
+ }
33
+
34
+ /**
35
+ * Default cell renderer — auto-classifies item fields into visual tiers.
36
+ */
37
+ export function DefaultCardCell({ item, config }: CellRendererProps) {
38
+ const titleFieldOverride = config.card_title_field as string | undefined;
39
+ const bodyFieldOverride = config.card_body_field as string | undefined;
40
+
41
+ let titleText = '';
42
+ let bodyText = '';
43
+ let evidenceText = '';
44
+ const tags: Array<[string, string]> = [];
45
+
46
+ for (const [key, value] of Object.entries(item)) {
47
+ if (value == null || value === '') continue;
48
+ if (typeof value === 'object') continue;
49
+
50
+ const strVal = String(value);
51
+
52
+ if (titleFieldOverride && key === titleFieldOverride) {
53
+ titleText = strVal;
54
+ continue;
55
+ }
56
+ if (bodyFieldOverride && key === bodyFieldOverride) {
57
+ bodyText = strVal;
58
+ continue;
59
+ }
60
+
61
+ const cls = classifyField(key);
62
+ if (cls === 'skip') continue;
63
+ if (cls === 'title' && !titleText) { titleText = strVal; continue; }
64
+ if (cls === 'body' && !bodyText) { bodyText = strVal; continue; }
65
+ if (cls === 'evidence' && !evidenceText) { evidenceText = strVal; continue; }
66
+ if (cls === 'tag') tags.push([key, strVal]);
67
+ }
68
+
69
+ if (!titleText && bodyText && bodyText.length < 120) {
70
+ titleText = bodyText;
71
+ bodyText = '';
72
+ }
73
+
74
+ return React.createElement('div', { className: 'card-cell-default' },
75
+ titleText
76
+ ? React.createElement('div', { className: 'card-cell-title' },
77
+ titleText.length > 200 ? titleText.slice(0, 200) + '\u2026' : titleText
78
+ )
79
+ : null,
80
+ bodyText
81
+ ? React.createElement('p', { className: 'card-cell-body' }, bodyText)
82
+ : null,
83
+ evidenceText
84
+ ? React.createElement('blockquote', { className: 'card-cell-evidence' }, evidenceText)
85
+ : null,
86
+ tags.length > 0
87
+ ? React.createElement('div', { className: 'card-cell-tags' },
88
+ ...tags.slice(0, 4).map(([k, v]) =>
89
+ React.createElement('span', { key: k, className: 'card-cell-tag' },
90
+ React.createElement('span', { className: 'card-cell-tag-label' }, formatLabel(k)),
91
+ ' ',
92
+ v.replace(/_/g, ' ')
93
+ )
94
+ )
95
+ )
96
+ : null
97
+ );
98
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ConditionCards — Domain-specific sub-renderers for Conditions of Possibility sections.
3
+ *
4
+ * Extracted from AccordionRenderer to follow the data-driven sub-renderer pattern.
5
+ * These preserve the rich visual treatment for enabling/constraining conditions:
6
+ * - Condition type colored chips
7
+ * - How-managed semantic badges
8
+ * - Prior works tags
9
+ * - Evidence blockquotes
10
+ *
11
+ * Registered as named sub-renderers in SubRenderers.tsx:
12
+ * enabling_conditions → EnableConditionsSubRenderer
13
+ * constraining_conditions → ConstrainConditionsSubRenderer
14
+ */
15
+
16
+ import React from 'react';
17
+ import type { SubRendererProps } from '../types';
18
+ import { useDesignTokens } from '../tokens/DesignTokenContext';
19
+ import type { StyleOverrides } from '../types/styles';
20
+
21
+ // ── Enabling Conditions Sub-Renderer ─────────────────────
22
+
23
+ export function EnableConditionsSubRenderer({ data, config }: SubRendererProps) {
24
+ const { getCategoryColor } = useDesignTokens();
25
+
26
+ if (!data || !Array.isArray(data) || data.length === 0) return null;
27
+ const conditions = data as Array<Record<string, unknown>>;
28
+ const sectionDesc = (config.section_description as string) ||
29
+ 'How the author\'s prior work makes the current argument possible';
30
+ const so = config._style_overrides as StyleOverrides | undefined;
31
+
32
+ return (
33
+ <>
34
+ <p className="gen-section-desc" style={so?.prose || undefined}>{sectionDesc}</p>
35
+ <div className="gen-conditions-grid">
36
+ {conditions.map(cond => (
37
+ <div key={String(cond.condition_id)} className="gen-condition-card enabling" style={so?.card || undefined}>
38
+ <div className="gen-condition-header">
39
+ <span
40
+ className="gen-condition-type"
41
+ style={{
42
+ borderColor: getCategoryColor('condition', String(cond.condition_type))?.text || 'var(--dt-text-muted, #64748b)',
43
+ color: getCategoryColor('condition', String(cond.condition_type))?.text || 'var(--dt-text-muted, #64748b)',
44
+ ...so?.chip,
45
+ }}
46
+ >
47
+ {String(cond.condition_type || '').replace(/_/g, ' ')}
48
+ </span>
49
+ {cond.how_managed ? (
50
+ <span className={`gen-managed-badge managed-${cond.how_managed}`} style={so?.badge || undefined}>
51
+ {String(cond.how_managed)}
52
+ </span>
53
+ ) : null}
54
+ </div>
55
+ <p className="gen-condition-desc">{String(cond.description || '')}</p>
56
+ {cond.how_it_enables ? (
57
+ <p className="gen-condition-enables">
58
+ <strong>How it enables:</strong> {String(cond.how_it_enables)}
59
+ </p>
60
+ ) : null}
61
+ {Array.isArray(cond.prior_works_involved) && cond.prior_works_involved.length > 0 && (
62
+ <div className="gen-condition-works">
63
+ {(cond.prior_works_involved as string[]).map((w, i) => (
64
+ <span key={i} className="gen-work-tag">{w}</span>
65
+ ))}
66
+ </div>
67
+ )}
68
+ {cond.evidence ? (
69
+ <blockquote className="gen-condition-evidence">{String(cond.evidence)}</blockquote>
70
+ ) : null}
71
+ </div>
72
+ ))}
73
+ </div>
74
+ </>
75
+ );
76
+ }
77
+
78
+ // ── Constraining Conditions Sub-Renderer ─────────────────
79
+
80
+ export function ConstrainConditionsSubRenderer({ data, config }: SubRendererProps) {
81
+ if (!data || !Array.isArray(data) || data.length === 0) return null;
82
+ const constraints = data as Array<Record<string, unknown>>;
83
+ const sectionDesc = (config.section_description as string) ||
84
+ 'How prior work limits or constrains the current argument';
85
+ const so = config._style_overrides as StyleOverrides | undefined;
86
+
87
+ return (
88
+ <>
89
+ <p className="gen-section-desc" style={so?.prose || undefined}>{sectionDesc}</p>
90
+ <div className="gen-constraints-list">
91
+ {constraints.map((constraint, i) => (
92
+ <div key={String(constraint.constraint_id || i)} className="gen-constraint-card" style={so?.card || undefined}>
93
+ {constraint.type ? (
94
+ <span className="gen-constraint-type" style={so?.chip || undefined}>
95
+ {String(constraint.type).replace(/_/g, ' ')}
96
+ </span>
97
+ ) : null}
98
+ <p>{String(constraint.description || '')}</p>
99
+ {constraint.how_navigated ? (
100
+ <p className="gen-constraint-nav">
101
+ <strong>How navigated:</strong> {String(constraint.how_navigated)}
102
+ </p>
103
+ ) : null}
104
+ </div>
105
+ ))}
106
+ </div>
107
+ </>
108
+ );
109
+ }