@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.
- package/dist/cells/RelationshipCardCell.d.ts +10 -0
- package/dist/cells/RelationshipCardCell.d.ts.map +1 -0
- package/dist/cells/RelationshipCardCell.js +91 -0
- package/dist/cells/RelationshipCardCell.js.map +1 -0
- package/dist/cells/TacticCardCell.d.ts +12 -0
- package/dist/cells/TacticCardCell.d.ts.map +1 -0
- package/dist/cells/TacticCardCell.js +77 -0
- package/dist/cells/TacticCardCell.js.map +1 -0
- package/dist/cells/TemplateCardCell.d.ts +29 -0
- package/dist/cells/TemplateCardCell.d.ts.map +1 -0
- package/dist/cells/TemplateCardCell.js +202 -0
- package/dist/cells/TemplateCardCell.js.map +1 -0
- package/dist/cells/index.d.ts +15 -0
- package/dist/cells/index.d.ts.map +1 -0
- package/dist/cells/index.js +85 -0
- package/dist/cells/index.js.map +1 -0
- package/dist/components/ConditionCards.d.ts +18 -0
- package/dist/components/ConditionCards.d.ts.map +1 -0
- package/dist/components/ConditionCards.js +28 -0
- package/dist/components/ConditionCards.js.map +1 -0
- package/dist/components/EvidenceTrail.d.ts +54 -0
- package/dist/components/EvidenceTrail.d.ts.map +1 -0
- package/dist/components/EvidenceTrail.js +98 -0
- package/dist/components/EvidenceTrail.js.map +1 -0
- package/dist/dispatch/SubRendererDispatch.d.ts +39 -0
- package/dist/dispatch/SubRendererDispatch.d.ts.map +1 -0
- package/dist/dispatch/SubRendererDispatch.js +153 -0
- package/dist/dispatch/SubRendererDispatch.js.map +1 -0
- package/dist/hooks/useProseExtraction.d.ts +38 -0
- package/dist/hooks/useProseExtraction.d.ts.map +1 -0
- package/dist/hooks/useProseExtraction.js +93 -0
- package/dist/hooks/useProseExtraction.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/provenance/ProvenanceSectionIcon.d.ts +15 -0
- package/dist/provenance/ProvenanceSectionIcon.d.ts.map +1 -0
- package/dist/provenance/ProvenanceSectionIcon.js +11 -0
- package/dist/provenance/ProvenanceSectionIcon.js.map +1 -0
- package/dist/renderers/AccordionRenderer.d.ts +29 -0
- package/dist/renderers/AccordionRenderer.d.ts.map +1 -0
- package/dist/renderers/AccordionRenderer.js +315 -0
- package/dist/renderers/AccordionRenderer.js.map +1 -0
- package/dist/renderers/CardGridRenderer.d.ts +24 -0
- package/dist/renderers/CardGridRenderer.d.ts.map +1 -0
- package/dist/renderers/CardGridRenderer.js +321 -0
- package/dist/renderers/CardGridRenderer.js.map +1 -0
- package/dist/renderers/CardRenderer.d.ts +27 -0
- package/dist/renderers/CardRenderer.d.ts.map +1 -0
- package/dist/renderers/CardRenderer.js +337 -0
- package/dist/renderers/CardRenderer.js.map +1 -0
- package/dist/renderers/IdeaEvolutionRenderer.d.ts +16 -0
- package/dist/renderers/IdeaEvolutionRenderer.d.ts.map +1 -0
- package/dist/renderers/IdeaEvolutionRenderer.js +187 -0
- package/dist/renderers/IdeaEvolutionRenderer.js.map +1 -0
- package/dist/renderers/ProseRenderer.d.ts +10 -0
- package/dist/renderers/ProseRenderer.d.ts.map +1 -0
- package/dist/renderers/ProseRenderer.js +42 -0
- package/dist/renderers/ProseRenderer.js.map +1 -0
- package/dist/renderers/RawJsonRenderer.d.ts +8 -0
- package/dist/renderers/RawJsonRenderer.d.ts.map +1 -0
- package/dist/renderers/RawJsonRenderer.js +17 -0
- package/dist/renderers/RawJsonRenderer.js.map +1 -0
- package/dist/renderers/StatSummaryRenderer.d.ts +12 -0
- package/dist/renderers/StatSummaryRenderer.d.ts.map +1 -0
- package/dist/renderers/StatSummaryRenderer.js +93 -0
- package/dist/renderers/StatSummaryRenderer.js.map +1 -0
- package/dist/renderers/SynthesisRenderer.d.ts +15 -0
- package/dist/renderers/SynthesisRenderer.d.ts.map +1 -0
- package/dist/renderers/SynthesisRenderer.js +60 -0
- package/dist/renderers/SynthesisRenderer.js.map +1 -0
- package/dist/renderers/TableRenderer.d.ts +19 -0
- package/dist/renderers/TableRenderer.d.ts.map +1 -0
- package/dist/renderers/TableRenderer.js +273 -0
- package/dist/renderers/TableRenderer.js.map +1 -0
- package/dist/styles/accordion.css +376 -0
- package/dist/styles/index.css +5 -0
- package/dist/styles/renderers.css +1049 -0
- package/dist/sub-renderers/SubRenderers.d.ts +73 -0
- package/dist/sub-renderers/SubRenderers.d.ts.map +1 -0
- package/dist/sub-renderers/SubRenderers.js +2462 -0
- package/dist/sub-renderers/SubRenderers.js.map +1 -0
- package/dist/tokens/DesignTokenContext.d.ts +40 -0
- package/dist/tokens/DesignTokenContext.d.ts.map +1 -0
- package/dist/tokens/DesignTokenContext.js +408 -0
- package/dist/tokens/DesignTokenContext.js.map +1 -0
- package/dist/types/designTokens.d.ts +220 -0
- package/dist/types/designTokens.d.ts.map +1 -0
- package/dist/types/designTokens.js +8 -0
- package/dist/types/designTokens.js.map +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/styles.d.ts +38 -0
- package/dist/types/styles.d.ts.map +1 -0
- package/dist/types/styles.js +14 -0
- package/dist/types/styles.js.map +1 -0
- package/dist/utils/tokenFlattener.d.ts +14 -0
- package/dist/utils/tokenFlattener.d.ts.map +1 -0
- package/dist/utils/tokenFlattener.js +56 -0
- package/dist/utils/tokenFlattener.js.map +1 -0
- package/package.json +31 -0
- package/src/cells/TemplateCardCell.tsx +439 -0
- package/src/cells/index.ts +98 -0
- package/src/components/ConditionCards.tsx +109 -0
- package/src/components/EvidenceTrail.tsx +203 -0
- package/src/dispatch/SubRendererDispatch.tsx +282 -0
- package/src/hooks/useProseExtraction.ts +125 -0
- package/src/index.ts +82 -0
- package/src/provenance/ProvenanceSectionIcon.tsx +19 -0
- package/src/renderers/AccordionRenderer.tsx +609 -0
- package/src/renderers/CardGridRenderer.tsx +608 -0
- package/src/renderers/CardRenderer.tsx +517 -0
- package/src/renderers/ProseRenderer.tsx +85 -0
- package/src/renderers/RawJsonRenderer.tsx +37 -0
- package/src/renderers/StatSummaryRenderer.tsx +182 -0
- package/src/renderers/TableRenderer.tsx +470 -0
- package/src/styles/accordion.css +376 -0
- package/src/styles/index.css +5 -0
- package/src/styles/renderers.css +1049 -0
- package/src/sub-renderers/SubRenderers.tsx +3487 -0
- package/src/tokens/DesignTokenContext.tsx +502 -0
- package/src/types/designTokens.ts +236 -0
- package/src/types/index.ts +53 -0
- package/src/types/styles.ts +44 -0
- 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
|
+
}
|