@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,2462 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* SubRenderers — Section-level rendering components for accordion sections.
|
|
4
|
+
*
|
|
5
|
+
* These are small, focused renderers that handle one section's data within
|
|
6
|
+
* an accordion. The AccordionRenderer dispatches to these based on
|
|
7
|
+
* config.section_renderers[sectionKey].renderer_type.
|
|
8
|
+
*
|
|
9
|
+
* Available sub-renderers:
|
|
10
|
+
* chip_grid — Array of strings/objects → weighted chip cloud
|
|
11
|
+
* definition_list — Array of "Term: Definition" strings or objects → glossary layout
|
|
12
|
+
* mini_card_list — Array of objects → hero + grid insight cards
|
|
13
|
+
* key_value_table — Object or [{key, value}] → styled two-column table
|
|
14
|
+
* prose_block — String → formatted analysis with lede, blockquotes
|
|
15
|
+
* stat_row — Object → monospace stat cards
|
|
16
|
+
* comparison_panel — Array of objects → side-by-side comparison with headers
|
|
17
|
+
* timeline_strip — Array of objects with stages → evolution arc with progression
|
|
18
|
+
* evidence_trail — Vertical chain of evidence steps with dot markers and connectors
|
|
19
|
+
* ordered_flow — Ordered sequence of content units with connecting line and category badges
|
|
20
|
+
* intensity_matrix — Dashboard rows with horizontal intensity bars for quantitative dimensions
|
|
21
|
+
* move_repertoire — Grouped card list with collapsible category headers and count badges
|
|
22
|
+
* dialectical_pair — Two-panel tension visualization for thesis/antithesis contrasts
|
|
23
|
+
* rich_description_list — Stacked items with colored borders for paragraph-length descriptions
|
|
24
|
+
* phase_timeline — Connected timeline with prominent phase nodes for temporal data
|
|
25
|
+
* distribution_summary — Visual bar chart with dominant highlight, counts, and optional narrative
|
|
26
|
+
*/
|
|
27
|
+
import React, { useState } from 'react';
|
|
28
|
+
import { EvidenceTrailSubRenderer } from '../components/EvidenceTrail';
|
|
29
|
+
import { EnableConditionsSubRenderer, ConstrainConditionsSubRenderer } from '../components/ConditionCards';
|
|
30
|
+
import { getSO } from '../types/styles';
|
|
31
|
+
import { useDesignTokens } from '../tokens/DesignTokenContext';
|
|
32
|
+
// ── Registry ─────────────────────────────────────────────
|
|
33
|
+
const SUB_RENDERER_MAP = {
|
|
34
|
+
chip_grid: ChipGrid,
|
|
35
|
+
definition_list: DefinitionList,
|
|
36
|
+
mini_card_list: MiniCardList,
|
|
37
|
+
key_value_table: KeyValueTable,
|
|
38
|
+
prose_block: ProseBlock,
|
|
39
|
+
stat_row: StatRow,
|
|
40
|
+
comparison_panel: ComparisonPanel,
|
|
41
|
+
timeline_strip: TimelineStrip,
|
|
42
|
+
evidence_trail: EvidenceTrailSubRenderer,
|
|
43
|
+
enabling_conditions: EnableConditionsSubRenderer,
|
|
44
|
+
constraining_conditions: ConstrainConditionsSubRenderer,
|
|
45
|
+
ordered_flow: OrderedFlow,
|
|
46
|
+
intensity_matrix: IntensityMatrix,
|
|
47
|
+
move_repertoire: MoveRepertoire,
|
|
48
|
+
grouped_card_list: MoveRepertoire, // alias for generic usage
|
|
49
|
+
dialectical_pair: DialecticalPair,
|
|
50
|
+
rich_description_list: RichDescriptionList,
|
|
51
|
+
phase_timeline: PhaseTimeline,
|
|
52
|
+
distribution_summary: DistributionSummary,
|
|
53
|
+
};
|
|
54
|
+
export function resolveSubRenderer(rendererType) {
|
|
55
|
+
return SUB_RENDERER_MAP[rendererType] || null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Auto-detect the best sub-renderer for a data shape.
|
|
59
|
+
* Used by AccordionRenderer when no section_renderer is explicitly configured.
|
|
60
|
+
*
|
|
61
|
+
* Decision tree:
|
|
62
|
+
* string → prose_block
|
|
63
|
+
* string[] → chip_grid
|
|
64
|
+
* object[] with nested array fields → timeline_strip (concept evolution, stage progressions)
|
|
65
|
+
* object[] with title+description → mini_card_list
|
|
66
|
+
* object[] short items (≤3 fields, all short) → chip_grid
|
|
67
|
+
* flat object (no nested arrays) → key_value_table
|
|
68
|
+
* object with only numeric values → stat_row
|
|
69
|
+
*/
|
|
70
|
+
export function autoDetectSubRenderer(data) {
|
|
71
|
+
if (data === null || data === undefined)
|
|
72
|
+
return null;
|
|
73
|
+
if (typeof data === 'string')
|
|
74
|
+
return 'prose_block';
|
|
75
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
76
|
+
if (data.every(d => typeof d === 'string')) {
|
|
77
|
+
// Check if strings look like "Term: Definition" → use definition_list
|
|
78
|
+
const strs = data;
|
|
79
|
+
const defCount = strs.filter(s => {
|
|
80
|
+
const colonIdx = s.indexOf(':');
|
|
81
|
+
// Term before colon (1-60 chars), definition after colon (10+ chars)
|
|
82
|
+
return colonIdx > 1 && colonIdx < 60 && s.length > colonIdx + 10;
|
|
83
|
+
}).length;
|
|
84
|
+
if (defCount >= strs.length * 0.5)
|
|
85
|
+
return 'definition_list';
|
|
86
|
+
return 'chip_grid';
|
|
87
|
+
}
|
|
88
|
+
const firstObj = data.find(d => typeof d === 'object' && d !== null);
|
|
89
|
+
if (firstObj) {
|
|
90
|
+
const entries = Object.entries(firstObj);
|
|
91
|
+
const hasArrayField = entries.some(([, v]) => Array.isArray(v) && v.length > 0);
|
|
92
|
+
const fieldCount = entries.length;
|
|
93
|
+
const shortStringCount = entries.filter(([, v]) => typeof v === 'string' && v.length < 60).length;
|
|
94
|
+
const longStringCount = entries.filter(([, v]) => typeof v === 'string' && v.length >= 60).length;
|
|
95
|
+
if (hasArrayField)
|
|
96
|
+
return 'timeline_strip';
|
|
97
|
+
if (fieldCount <= 3 && shortStringCount >= fieldCount - 1 && longStringCount === 0)
|
|
98
|
+
return 'chip_grid';
|
|
99
|
+
if (longStringCount > 0)
|
|
100
|
+
return 'mini_card_list';
|
|
101
|
+
return 'mini_card_list';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (typeof data === 'object' && !Array.isArray(data)) {
|
|
105
|
+
const obj = data;
|
|
106
|
+
const entries = Object.entries(obj).filter(([, v]) => v !== null && v !== undefined);
|
|
107
|
+
if (entries.length > 0 && entries.every(([, v]) => typeof v === 'number'))
|
|
108
|
+
return 'stat_row';
|
|
109
|
+
const allScalar = entries.every(([, v]) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean');
|
|
110
|
+
if (allScalar)
|
|
111
|
+
return 'key_value_table';
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
116
|
+
function getField(obj, field) {
|
|
117
|
+
if (!field)
|
|
118
|
+
return '';
|
|
119
|
+
const val = obj[field];
|
|
120
|
+
if (val === null || val === undefined)
|
|
121
|
+
return '';
|
|
122
|
+
return String(val);
|
|
123
|
+
}
|
|
124
|
+
function getNumField(obj, field) {
|
|
125
|
+
if (!field)
|
|
126
|
+
return null;
|
|
127
|
+
const val = obj[field];
|
|
128
|
+
if (typeof val === 'number')
|
|
129
|
+
return val;
|
|
130
|
+
if (typeof val === 'string') {
|
|
131
|
+
const n = parseFloat(val);
|
|
132
|
+
return isNaN(n) ? null : n;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const TITLE_HINTS = ['name', 'title', 'label', 'term', 'concept', 'framework_name', 'heading'];
|
|
137
|
+
const SUBTITLE_HINTS = ['type', 'category', 'kind', 'centrality', 'status', 'role', 'level'];
|
|
138
|
+
const DESC_HINTS = ['description', 'summary', 'definition', 'explanation', 'text', 'content', 'methodological_signature'];
|
|
139
|
+
const KEY_HINTS = ['key', 'term', 'concept', 'name', 'label'];
|
|
140
|
+
const VALUE_HINTS = ['value', 'definition', 'meaning', 'description', 'explanation'];
|
|
141
|
+
function autoDetectFields(sample) {
|
|
142
|
+
const result = {};
|
|
143
|
+
const entries = Object.entries(sample);
|
|
144
|
+
const shortStrings = [];
|
|
145
|
+
const longStrings = [];
|
|
146
|
+
const numericFields = [];
|
|
147
|
+
for (const [k, v] of entries) {
|
|
148
|
+
if (typeof v === 'string') {
|
|
149
|
+
if (v.length > 80)
|
|
150
|
+
longStrings.push(k);
|
|
151
|
+
else
|
|
152
|
+
shortStrings.push(k);
|
|
153
|
+
}
|
|
154
|
+
else if (typeof v === 'number') {
|
|
155
|
+
numericFields.push(k);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
result.title = shortStrings.find(k => TITLE_HINTS.includes(k)) || shortStrings[0];
|
|
159
|
+
result.label = result.title;
|
|
160
|
+
const remaining = shortStrings.filter(k => k !== result.title);
|
|
161
|
+
result.subtitle = remaining.find(k => SUBTITLE_HINTS.includes(k)) || remaining[0];
|
|
162
|
+
result.description = longStrings.find(k => DESC_HINTS.includes(k)) || longStrings[0];
|
|
163
|
+
result.badge = numericFields[0];
|
|
164
|
+
result.count = numericFields[0];
|
|
165
|
+
result.key = shortStrings.find(k => KEY_HINTS.includes(k)) || shortStrings[0];
|
|
166
|
+
const valCandidates = [...longStrings, ...shortStrings.filter(k => k !== result.key)];
|
|
167
|
+
result.value = valCandidates.find(k => VALUE_HINTS.includes(k)) || valCandidates[0];
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
function resolveField(config, configKey, auto, autoKey) {
|
|
171
|
+
return config[configKey] || auto[autoKey];
|
|
172
|
+
}
|
|
173
|
+
// ── Color Utilities ──────────────────────────────────────
|
|
174
|
+
function parseAccentHSL(hex) {
|
|
175
|
+
if (!hex || !hex.startsWith('#') || hex.length < 7)
|
|
176
|
+
return { h: 220, s: 55, l: 45 };
|
|
177
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
178
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
179
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
180
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
181
|
+
let h = 0, s = 0;
|
|
182
|
+
const l = (max + min) / 2;
|
|
183
|
+
if (max !== min) {
|
|
184
|
+
const d = max - min;
|
|
185
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
186
|
+
if (max === r)
|
|
187
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
188
|
+
else if (max === g)
|
|
189
|
+
h = ((b - r) / d + 2) / 6;
|
|
190
|
+
else
|
|
191
|
+
h = ((r - g) / d + 4) / 6;
|
|
192
|
+
}
|
|
193
|
+
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
194
|
+
}
|
|
195
|
+
/** Render inline markdown: **bold** → accent-underlined bold, *italic* → em */
|
|
196
|
+
function renderInlineMarkdown(text, accentColor) {
|
|
197
|
+
const parts = [];
|
|
198
|
+
let lastIndex = 0;
|
|
199
|
+
const regex = /(\*\*(.+?)\*\*|\*(.+?)\*)/g;
|
|
200
|
+
let match;
|
|
201
|
+
while ((match = regex.exec(text)) !== null) {
|
|
202
|
+
if (match.index > lastIndex) {
|
|
203
|
+
parts.push(text.slice(lastIndex, match.index));
|
|
204
|
+
}
|
|
205
|
+
if (match[2]) {
|
|
206
|
+
parts.push(_jsx("strong", { style: {
|
|
207
|
+
fontWeight: 'var(--weight-semibold, 600)',
|
|
208
|
+
textDecoration: 'underline',
|
|
209
|
+
textDecorationColor: accentColor,
|
|
210
|
+
textUnderlineOffset: '3px',
|
|
211
|
+
textDecorationThickness: '2px',
|
|
212
|
+
}, children: match[2] }, match.index));
|
|
213
|
+
}
|
|
214
|
+
else if (match[3]) {
|
|
215
|
+
parts.push(_jsx("em", { children: match[3] }, match.index));
|
|
216
|
+
}
|
|
217
|
+
lastIndex = match.index + match[0].length;
|
|
218
|
+
}
|
|
219
|
+
if (lastIndex < text.length) {
|
|
220
|
+
parts.push(text.slice(lastIndex));
|
|
221
|
+
}
|
|
222
|
+
return parts.length > 0 ? parts : [text];
|
|
223
|
+
}
|
|
224
|
+
// ── ChipGrid ─────────────────────────────────────────────
|
|
225
|
+
function ChipGrid({ data, config }) {
|
|
226
|
+
const [expandedIdx, setExpandedIdx] = React.useState(null);
|
|
227
|
+
const { getChipWeight, tokens } = useDesignTokens();
|
|
228
|
+
const so = getSO(config);
|
|
229
|
+
if (!data || !Array.isArray(data))
|
|
230
|
+
return null;
|
|
231
|
+
const firstObj = data.find(d => typeof d === 'object' && d !== null);
|
|
232
|
+
const auto = firstObj ? autoDetectFields(firstObj) : {};
|
|
233
|
+
const labelField = resolveField(config, 'label_field', auto, 'label');
|
|
234
|
+
const countField = resolveField(config, 'count_field', auto, 'count');
|
|
235
|
+
const subtitleField = resolveField(config, 'subtitle_field', auto, 'subtitle');
|
|
236
|
+
const descField = resolveField(config, 'description_field', auto, 'description');
|
|
237
|
+
const hasDetails = firstObj && ((descField && getField(firstObj, descField).length > 0) ||
|
|
238
|
+
Object.values(firstObj).some(v => Array.isArray(v)));
|
|
239
|
+
// Collect numeric values for size variation
|
|
240
|
+
const numericValues = [];
|
|
241
|
+
if (countField) {
|
|
242
|
+
data.forEach(item => {
|
|
243
|
+
if (typeof item === 'object' && item !== null) {
|
|
244
|
+
const n = getNumField(item, countField);
|
|
245
|
+
if (n !== null)
|
|
246
|
+
numericValues.push(n);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const hasNumeric = numericValues.length > 0;
|
|
251
|
+
const minVal = hasNumeric ? Math.min(...numericValues) : 0;
|
|
252
|
+
const maxVal = hasNumeric ? Math.max(...numericValues) : 1;
|
|
253
|
+
const valRange = maxVal - minVal || 1;
|
|
254
|
+
// Build chip items with weight for sorting
|
|
255
|
+
const chipItems = data.map((item, i) => {
|
|
256
|
+
const label = typeof item === 'string'
|
|
257
|
+
? item
|
|
258
|
+
: typeof item === 'object' && item !== null
|
|
259
|
+
? getField(item, labelField)
|
|
260
|
+
|| String(Object.values(item).find(v => typeof v === 'string' && v.length < 80) || `Item ${i + 1}`)
|
|
261
|
+
: String(item);
|
|
262
|
+
const subtitle = typeof item === 'object' && item !== null && subtitleField
|
|
263
|
+
? getField(item, subtitleField)
|
|
264
|
+
: '';
|
|
265
|
+
const count = typeof item === 'object' && item !== null && countField
|
|
266
|
+
? getNumField(item, countField)
|
|
267
|
+
: null;
|
|
268
|
+
const weight = hasNumeric && count !== null
|
|
269
|
+
? (count - minVal) / valRange
|
|
270
|
+
: 0.5;
|
|
271
|
+
return { item, label, subtitle, count, weight, originalIndex: i };
|
|
272
|
+
});
|
|
273
|
+
// Sort by weight descending for weighted cloud layout
|
|
274
|
+
const sortedChips = [...chipItems].sort((a, b) => b.weight - a.weight);
|
|
275
|
+
return (_jsxs("div", { children: [_jsx("div", { style: {
|
|
276
|
+
display: 'flex', gap: 'var(--space-sm, 0.5rem)', flexWrap: 'wrap',
|
|
277
|
+
padding: 'var(--space-md, 0.75rem)',
|
|
278
|
+
backgroundColor: 'var(--color-surface-alt, #f8f9fa)',
|
|
279
|
+
borderRadius: 'var(--radius-lg, 12px)',
|
|
280
|
+
border: '1px solid var(--color-border-light, #eef0f2)',
|
|
281
|
+
...so?.items_container,
|
|
282
|
+
}, children: sortedChips.map(({ item, label, subtitle, count, weight, originalIndex }) => {
|
|
283
|
+
const chipColors = getChipWeight(weight);
|
|
284
|
+
const colors = { ...chipColors, headerBg: tokens.components.chip_header_bg, headerText: tokens.components.chip_header_text };
|
|
285
|
+
const isExpanded = expandedIdx === originalIndex;
|
|
286
|
+
const isClickable = hasDetails && typeof item === 'object' && item !== null;
|
|
287
|
+
// Size variation: large chips (weight > 0.7) get bigger padding/font
|
|
288
|
+
const sizeClass = weight > 0.7 ? 'large' : weight > 0.3 ? 'medium' : 'small';
|
|
289
|
+
const padH = sizeClass === 'large' ? '18px' : sizeClass === 'medium' ? '14px' : '10px';
|
|
290
|
+
const padV = sizeClass === 'large' ? '8px' : sizeClass === 'medium' ? '6px' : '5px';
|
|
291
|
+
const fontSize = sizeClass === 'large'
|
|
292
|
+
? 'var(--type-body, 0.9375rem)'
|
|
293
|
+
: sizeClass === 'medium'
|
|
294
|
+
? 'var(--type-caption, 0.8125rem)'
|
|
295
|
+
: 'var(--type-label, 0.6875rem)';
|
|
296
|
+
return (_jsxs("span", { onClick: isClickable ? () => setExpandedIdx(isExpanded ? null : originalIndex) : undefined, title: isClickable ? 'Click to expand details' : undefined, style: {
|
|
297
|
+
display: 'inline-flex', alignItems: 'center',
|
|
298
|
+
gap: 'var(--space-xs, 0.25rem)',
|
|
299
|
+
padding: `${padV} ${padH}`,
|
|
300
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
301
|
+
fontSize,
|
|
302
|
+
fontWeight: 'var(--weight-semibold, 600)',
|
|
303
|
+
backgroundColor: isExpanded ? colors.headerBg : colors.bg,
|
|
304
|
+
color: isExpanded ? colors.headerText : colors.text,
|
|
305
|
+
border: `1.5px solid ${isExpanded ? colors.headerBg : colors.border}`,
|
|
306
|
+
boxShadow: isExpanded
|
|
307
|
+
? 'var(--shadow-md, 0 4px 6px rgba(0,0,0,0.05))'
|
|
308
|
+
: 'var(--shadow-xs, 0 1px 2px rgba(0,0,0,0.04))',
|
|
309
|
+
cursor: isClickable ? 'pointer' : 'default',
|
|
310
|
+
transition: `all var(--duration-fast, 150ms) var(--ease-out, ease)`,
|
|
311
|
+
...so?.chip,
|
|
312
|
+
}, children: [_jsx("span", { style: so?.chip_label, children: label }), subtitle && (_jsx("span", { style: {
|
|
313
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
314
|
+
fontWeight: 'var(--weight-medium, 500)',
|
|
315
|
+
padding: '1px 7px', borderRadius: 'var(--radius-pill, 9999px)',
|
|
316
|
+
backgroundColor: isExpanded ? 'rgba(255,255,255,0.25)' : colors.headerBg,
|
|
317
|
+
color: colors.headerText,
|
|
318
|
+
letterSpacing: '0.02em',
|
|
319
|
+
...so?.badge,
|
|
320
|
+
}, children: subtitle })), count !== null && (_jsx("span", { style: {
|
|
321
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
322
|
+
fontWeight: 'var(--weight-bold, 700)',
|
|
323
|
+
backgroundColor: isExpanded ? 'rgba(255,255,255,0.25)' : colors.headerBg,
|
|
324
|
+
color: colors.headerText,
|
|
325
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
326
|
+
padding: '1px 6px',
|
|
327
|
+
...so?.badge,
|
|
328
|
+
}, children: count }))] }, originalIndex));
|
|
329
|
+
}) }), expandedIdx !== null && typeof data[expandedIdx] === 'object' && data[expandedIdx] !== null && (() => {
|
|
330
|
+
const obj = data[expandedIdx];
|
|
331
|
+
const label = getField(obj, labelField);
|
|
332
|
+
const subtitle = getField(obj, subtitleField);
|
|
333
|
+
const desc = descField ? getField(obj, descField) : '';
|
|
334
|
+
const expandChipColors = getChipWeight(0.6);
|
|
335
|
+
const colors = { ...expandChipColors, headerBg: tokens.components.chip_header_bg, headerText: tokens.components.chip_header_text };
|
|
336
|
+
const skipKeys = new Set([labelField, subtitleField, descField].filter(Boolean));
|
|
337
|
+
const remaining = Object.entries(obj).filter(([k, v]) => !skipKeys.has(k) && v !== null && v !== undefined && v !== '');
|
|
338
|
+
const arrayFields = remaining.filter(([, v]) => Array.isArray(v));
|
|
339
|
+
const scalarFields = remaining.filter(([, v]) => !Array.isArray(v) && typeof v === 'string' && v.length > 40);
|
|
340
|
+
const shortFields = remaining.filter(([, v]) => !Array.isArray(v) && (typeof v !== 'string' || v.length <= 40));
|
|
341
|
+
return (_jsxs("div", { style: {
|
|
342
|
+
margin: 'var(--space-sm, 0.5rem) 0',
|
|
343
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
344
|
+
overflow: 'hidden',
|
|
345
|
+
border: `1.5px solid ${colors.border}`,
|
|
346
|
+
boxShadow: 'var(--shadow-md, 0 4px 6px rgba(0,0,0,0.05))',
|
|
347
|
+
...so?.chip_expanded,
|
|
348
|
+
}, children: [_jsxs("div", { style: {
|
|
349
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
350
|
+
backgroundColor: colors.headerBg,
|
|
351
|
+
display: 'flex', alignItems: 'center', gap: 'var(--space-sm, 0.5rem)',
|
|
352
|
+
}, children: [_jsx("strong", { style: {
|
|
353
|
+
fontSize: 'var(--type-subheading, 1.125rem)',
|
|
354
|
+
color: colors.headerText,
|
|
355
|
+
}, children: label }), subtitle && (_jsx("span", { style: {
|
|
356
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
357
|
+
fontWeight: 'var(--weight-medium, 500)',
|
|
358
|
+
padding: '2px 8px', borderRadius: 'var(--radius-pill, 9999px)',
|
|
359
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
360
|
+
color: colors.headerText,
|
|
361
|
+
letterSpacing: '0.02em',
|
|
362
|
+
}, children: subtitle })), _jsx("span", { onClick: () => setExpandedIdx(null), style: {
|
|
363
|
+
marginLeft: 'auto', cursor: 'pointer',
|
|
364
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
365
|
+
color: 'rgba(255,255,255,0.7)',
|
|
366
|
+
padding: '2px 8px', borderRadius: 'var(--radius-sm, 4px)',
|
|
367
|
+
transition: `opacity var(--duration-fast, 150ms)`,
|
|
368
|
+
}, children: "close" })] }), _jsxs("div", { style: {
|
|
369
|
+
padding: 'var(--space-md, 1rem)',
|
|
370
|
+
backgroundColor: colors.bg,
|
|
371
|
+
}, children: [desc && (_jsx("p", { style: {
|
|
372
|
+
margin: '0 0 var(--space-sm, 0.5rem) 0',
|
|
373
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
374
|
+
color: 'var(--color-text, #1a1d23)',
|
|
375
|
+
lineHeight: 'var(--leading-relaxed, 1.65)',
|
|
376
|
+
}, children: desc })), shortFields.length > 0 && (_jsx("div", { style: {
|
|
377
|
+
display: 'flex', gap: 'var(--space-md, 0.75rem)', flexWrap: 'wrap',
|
|
378
|
+
marginBottom: arrayFields.length > 0 || scalarFields.length > 0 ? 'var(--space-sm, 0.5rem)' : 0,
|
|
379
|
+
}, children: shortFields.map(([k, v]) => (_jsxs("span", { style: { fontSize: 'var(--type-caption, 0.8125rem)', color: 'var(--color-text-muted, #6b7280)' }, children: [_jsxs("span", { className: "gen-inline-label", children: [k.replace(/_/g, ' '), ":"] }), ' ', String(v)] }, k))) })), scalarFields.map(([k, v]) => (_jsxs("div", { style: { marginBottom: 'var(--space-xs, 0.375rem)' }, children: [_jsx("span", { className: "gen-inline-label", style: { display: 'block', marginBottom: 'var(--space-2xs, 0.125rem)' }, children: k.replace(/_/g, ' ') }), _jsx("span", { style: {
|
|
380
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
381
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
382
|
+
lineHeight: 'var(--leading-normal, 1.5)',
|
|
383
|
+
}, children: String(v) })] }, k))), arrayFields.map(([k, v]) => (_jsxs("div", { style: { marginTop: 'var(--space-xs, 0.375rem)' }, children: [_jsxs("span", { className: "gen-inline-label", style: { marginRight: 'var(--space-xs, 0.375rem)' }, children: [k.replace(/_/g, ' '), ":"] }), _jsx("span", { style: { display: 'inline-flex', gap: 'var(--space-xs, 0.25rem)', flexWrap: 'wrap' }, children: v.map((chip, ci) => (_jsx("span", { className: "gen-keyword-tag", children: String(chip) }, ci))) })] }, k)))] })] }));
|
|
384
|
+
})()] }));
|
|
385
|
+
}
|
|
386
|
+
// ── DefinitionList → Glossary/Vocabulary ─────────────────
|
|
387
|
+
/**
|
|
388
|
+
* Purpose-built renderer for glossary/vocabulary data.
|
|
389
|
+
* Handles:
|
|
390
|
+
* - Array of "Term: Definition" strings (splits on first colon)
|
|
391
|
+
* - Array of objects with term/definition fields
|
|
392
|
+
* Renders as a visually rich definition list with:
|
|
393
|
+
* - Prominent term styling with accent color
|
|
394
|
+
* - Clear definition text
|
|
395
|
+
* - Alternating subtle backgrounds
|
|
396
|
+
* - Compact, scannable layout
|
|
397
|
+
*/
|
|
398
|
+
function DefinitionList({ data, config }) {
|
|
399
|
+
const [expandedIdx, setExpandedIdx] = React.useState(null);
|
|
400
|
+
const { tokens } = useDesignTokens();
|
|
401
|
+
const so = getSO(config);
|
|
402
|
+
// Capture mode support (threaded from AccordionRenderer)
|
|
403
|
+
const captureMode = config._captureMode;
|
|
404
|
+
const onCapture = config._onCapture;
|
|
405
|
+
const captureJobId = config._captureJobId;
|
|
406
|
+
const captureViewKey = config._captureViewKey;
|
|
407
|
+
const captureSourceType = config._captureSourceType;
|
|
408
|
+
const captureEntityId = config._captureEntityId;
|
|
409
|
+
const parentSectionKey = config._parentSectionKey;
|
|
410
|
+
const parentSectionTitle = config._parentSectionTitle;
|
|
411
|
+
if (!data || !Array.isArray(data) || data.length === 0)
|
|
412
|
+
return null;
|
|
413
|
+
// Parse items into {term, definition} pairs
|
|
414
|
+
const termField = config.term_field;
|
|
415
|
+
const defField = config.definition_field;
|
|
416
|
+
const items = data.map((item, i) => {
|
|
417
|
+
if (typeof item === 'string') {
|
|
418
|
+
// Split "Term: Definition" on first colon
|
|
419
|
+
const colonIdx = item.indexOf(':');
|
|
420
|
+
if (colonIdx > 0 && colonIdx < 80) {
|
|
421
|
+
return {
|
|
422
|
+
term: item.slice(0, colonIdx).trim(),
|
|
423
|
+
definition: item.slice(colonIdx + 1).trim(),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return { term: `Entry ${i + 1}`, definition: item };
|
|
427
|
+
}
|
|
428
|
+
if (typeof item === 'object' && item !== null) {
|
|
429
|
+
const obj = item;
|
|
430
|
+
const t = getField(obj, termField) || getField(obj, 'term') || getField(obj, 'name') || getField(obj, 'concept') || getField(obj, 'label') || '';
|
|
431
|
+
const d = getField(obj, defField) || getField(obj, 'definition') || getField(obj, 'description') || getField(obj, 'meaning') || '';
|
|
432
|
+
return { term: t || `Entry ${i + 1}`, definition: d || JSON.stringify(obj) };
|
|
433
|
+
}
|
|
434
|
+
return { term: `Entry ${i + 1}`, definition: String(item) };
|
|
435
|
+
});
|
|
436
|
+
// Color rotation for visual variety using series palette
|
|
437
|
+
const palette = tokens.primitives.series_palette;
|
|
438
|
+
const termColor = (idx) => {
|
|
439
|
+
const color = palette[idx % palette.length];
|
|
440
|
+
return {
|
|
441
|
+
termBg: color,
|
|
442
|
+
termText: 'var(--dt-text-inverse)',
|
|
443
|
+
dotColor: color,
|
|
444
|
+
hoverBg: tokens.surfaces.surface_alt,
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
return (_jsx("div", { style: {
|
|
448
|
+
display: 'flex',
|
|
449
|
+
flexDirection: 'column',
|
|
450
|
+
gap: '2px',
|
|
451
|
+
borderRadius: 'var(--radius-lg, 12px)',
|
|
452
|
+
overflow: 'hidden',
|
|
453
|
+
border: '1px solid var(--color-border-light, #eef0f2)',
|
|
454
|
+
boxShadow: 'var(--shadow-xs, 0 1px 2px rgba(0,0,0,0.04))',
|
|
455
|
+
...so?.items_container,
|
|
456
|
+
}, children: items.map((item, i) => {
|
|
457
|
+
const colors = termColor(i);
|
|
458
|
+
const isExpanded = expandedIdx === i;
|
|
459
|
+
const isLong = item.definition.length > 180;
|
|
460
|
+
const displayDef = !isLong || isExpanded
|
|
461
|
+
? item.definition
|
|
462
|
+
: item.definition.slice(0, 180) + '...';
|
|
463
|
+
return (_jsxs("div", { onClick: isLong ? () => setExpandedIdx(isExpanded ? null : i) : undefined, style: {
|
|
464
|
+
display: 'grid',
|
|
465
|
+
gridTemplateColumns: 'auto 1fr',
|
|
466
|
+
gap: 0,
|
|
467
|
+
backgroundColor: i % 2 === 0
|
|
468
|
+
? 'var(--color-surface, #ffffff)'
|
|
469
|
+
: 'var(--color-surface-alt, #f8f9fa)',
|
|
470
|
+
cursor: isLong ? 'pointer' : 'default',
|
|
471
|
+
transition: 'background-color 150ms ease',
|
|
472
|
+
}, children: [_jsxs("div", { style: {
|
|
473
|
+
padding: '10px 14px',
|
|
474
|
+
display: 'flex',
|
|
475
|
+
alignItems: 'flex-start',
|
|
476
|
+
gap: '8px',
|
|
477
|
+
minWidth: '200px',
|
|
478
|
+
maxWidth: '280px',
|
|
479
|
+
borderRight: `3px solid ${colors.dotColor}`,
|
|
480
|
+
backgroundColor: i % 2 === 0 ? tokens.surfaces.surface_alt : tokens.surfaces.surface_inset,
|
|
481
|
+
}, children: [_jsx("span", { style: {
|
|
482
|
+
display: 'inline-block',
|
|
483
|
+
width: '7px',
|
|
484
|
+
height: '7px',
|
|
485
|
+
borderRadius: '50%',
|
|
486
|
+
backgroundColor: colors.dotColor,
|
|
487
|
+
marginTop: '6px',
|
|
488
|
+
flexShrink: 0,
|
|
489
|
+
} }), _jsx("span", { style: {
|
|
490
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
491
|
+
fontWeight: 'var(--weight-bold, 700)',
|
|
492
|
+
color: tokens.surfaces.text_default,
|
|
493
|
+
lineHeight: '1.3',
|
|
494
|
+
letterSpacing: '0.01em',
|
|
495
|
+
...so?.stat_label,
|
|
496
|
+
}, children: item.term })] }), _jsxs("div", { style: {
|
|
497
|
+
padding: '10px 16px',
|
|
498
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
499
|
+
fontWeight: 'var(--weight-normal, 400)',
|
|
500
|
+
color: 'var(--color-text, #1a1d23)',
|
|
501
|
+
lineHeight: 'var(--leading-relaxed, 1.6)',
|
|
502
|
+
display: 'flex',
|
|
503
|
+
alignItems: 'flex-start',
|
|
504
|
+
gap: '8px',
|
|
505
|
+
}, children: [_jsxs("div", { style: { flex: 1 }, children: [renderInlineMarkdown(displayDef, colors.dotColor), isLong && (_jsx("span", { className: "gen-show-more-link", style: { marginLeft: '6px' }, children: isExpanded ? 'show less' : 'show more' }))] }), captureMode && onCapture && (_jsx("button", { title: "Capture this item", onClick: e => {
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
onCapture({
|
|
508
|
+
source_view_key: captureViewKey || '',
|
|
509
|
+
source_section_key: parentSectionKey,
|
|
510
|
+
source_item_index: i,
|
|
511
|
+
source_renderer_type: 'definition_list',
|
|
512
|
+
content_type: 'item',
|
|
513
|
+
selected_text: `${item.term}: ${item.definition}`.slice(0, 500),
|
|
514
|
+
structured_data: data[i],
|
|
515
|
+
context_title: parentSectionKey
|
|
516
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > ${item.term}`
|
|
517
|
+
: `${captureViewKey || 'Analysis'} > ${item.term}`,
|
|
518
|
+
source_type: (captureSourceType || 'analysis'),
|
|
519
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
520
|
+
depth_level: parentSectionKey ? 'L2_element' : 'L1_section',
|
|
521
|
+
parent_context: parentSectionKey ? {
|
|
522
|
+
section_key: parentSectionKey,
|
|
523
|
+
section_title: parentSectionTitle || '',
|
|
524
|
+
} : undefined,
|
|
525
|
+
});
|
|
526
|
+
}, style: {
|
|
527
|
+
flexShrink: 0,
|
|
528
|
+
background: 'none',
|
|
529
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
530
|
+
borderRadius: '4px',
|
|
531
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
532
|
+
cursor: 'pointer',
|
|
533
|
+
padding: '2px 6px',
|
|
534
|
+
fontSize: '0.7rem',
|
|
535
|
+
lineHeight: 1,
|
|
536
|
+
marginTop: '2px',
|
|
537
|
+
}, children: "\uD83D\uDCCC" }))] })] }, i));
|
|
538
|
+
}) }));
|
|
539
|
+
}
|
|
540
|
+
// ── MiniCardList → Insight Cards ─────────────────────────
|
|
541
|
+
function MiniCardList({ data, config }) {
|
|
542
|
+
const [expandedCards, setExpandedCards] = React.useState(new Set());
|
|
543
|
+
const [hoveredIdx, setHoveredIdx] = React.useState(null);
|
|
544
|
+
const { tokens } = useDesignTokens();
|
|
545
|
+
// Capture mode support (threaded from AccordionRenderer)
|
|
546
|
+
const captureMode = config._captureMode;
|
|
547
|
+
const onCapture = config._onCapture;
|
|
548
|
+
const captureJobId = config._captureJobId;
|
|
549
|
+
const captureViewKey = config._captureViewKey;
|
|
550
|
+
const captureSourceType = config._captureSourceType;
|
|
551
|
+
const captureEntityId = config._captureEntityId;
|
|
552
|
+
const parentSectionKey = config._parentSectionKey;
|
|
553
|
+
const parentSectionTitle = config._parentSectionTitle;
|
|
554
|
+
if (!data || !Array.isArray(data))
|
|
555
|
+
return null;
|
|
556
|
+
const so = getSO(config);
|
|
557
|
+
const firstObj = data.find(d => typeof d === 'object' && d !== null);
|
|
558
|
+
const auto = firstObj ? autoDetectFields(firstObj) : {};
|
|
559
|
+
const titleField = resolveField(config, 'title_field', auto, 'title');
|
|
560
|
+
const subtitleField = resolveField(config, 'subtitle_field', auto, 'subtitle');
|
|
561
|
+
const badgeField = resolveField(config, 'badge_field', auto, 'badge');
|
|
562
|
+
const descriptionField = resolveField(config, 'description_field', auto, 'description');
|
|
563
|
+
// Hero card is opt-in: set hero:true in config to enable the hero pattern
|
|
564
|
+
const useHero = config.hero === true;
|
|
565
|
+
// Determine hero card: highest significance/importance/priority, or first
|
|
566
|
+
const SIGNIFICANCE_KEYS = ['significance', 'importance', 'priority', 'weight', 'relevance'];
|
|
567
|
+
let heroIdx = useHero ? 0 : -1;
|
|
568
|
+
if (useHero && data.length > 1 && firstObj) {
|
|
569
|
+
const sigField = Object.keys(firstObj).find(k => SIGNIFICANCE_KEYS.includes(k));
|
|
570
|
+
if (sigField) {
|
|
571
|
+
let maxVal = -Infinity;
|
|
572
|
+
data.forEach((item, i) => {
|
|
573
|
+
if (typeof item === 'object' && item !== null) {
|
|
574
|
+
const val = getNumField(item, sigField);
|
|
575
|
+
if (val !== null && val > maxVal) {
|
|
576
|
+
maxVal = val;
|
|
577
|
+
heroIdx = i;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const toggleExpand = (idx) => {
|
|
584
|
+
setExpandedCards(prev => {
|
|
585
|
+
const next = new Set(prev);
|
|
586
|
+
if (next.has(idx))
|
|
587
|
+
next.delete(idx);
|
|
588
|
+
else
|
|
589
|
+
next.add(idx);
|
|
590
|
+
return next;
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
const DESC_TRUNCATE_LEN = Infinity; // Show full text — no truncation
|
|
594
|
+
const renderCard = (item, idx, isHero) => {
|
|
595
|
+
if (typeof item !== 'object' || item === null)
|
|
596
|
+
return null;
|
|
597
|
+
const obj = item;
|
|
598
|
+
const title = getField(obj, titleField);
|
|
599
|
+
const subtitle = getField(obj, subtitleField);
|
|
600
|
+
const badge = getField(obj, badgeField);
|
|
601
|
+
const description = getField(obj, descriptionField);
|
|
602
|
+
const shownFields = new Set([titleField, subtitleField, badgeField, descriptionField]);
|
|
603
|
+
const remaining = Object.entries(obj).filter(([k, v]) => !shownFields.has(k) && v !== null && v !== undefined && v !== '');
|
|
604
|
+
const chipFields = remaining.filter(([, v]) => Array.isArray(v) && v.every(x => typeof x === 'string'));
|
|
605
|
+
const scalarFields = remaining.filter(([, v]) => !Array.isArray(v) || !v.every(x => typeof x === 'string'));
|
|
606
|
+
const seriesColor = tokens.primitives.series_palette[idx % tokens.primitives.series_palette.length];
|
|
607
|
+
const colors = {
|
|
608
|
+
headerBg: seriesColor,
|
|
609
|
+
headerText: tokens.surfaces.text_on_accent,
|
|
610
|
+
accent: seriesColor,
|
|
611
|
+
lightBg: tokens.surfaces.surface_alt,
|
|
612
|
+
darkText: tokens.surfaces.text_default,
|
|
613
|
+
border: tokens.surfaces.border_default,
|
|
614
|
+
};
|
|
615
|
+
const isHovered = hoveredIdx === idx;
|
|
616
|
+
const isContentExpanded = expandedCards.has(idx);
|
|
617
|
+
const needsTruncation = !isHero && description.length > DESC_TRUNCATE_LEN;
|
|
618
|
+
return (_jsxs("div", { onMouseEnter: () => setHoveredIdx(idx), onMouseLeave: () => setHoveredIdx(null), style: {
|
|
619
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
620
|
+
overflow: 'hidden',
|
|
621
|
+
borderLeft: `4px solid ${colors.accent}`,
|
|
622
|
+
border: `1px solid var(--color-border, #e2e5e9)`,
|
|
623
|
+
borderLeftWidth: '4px',
|
|
624
|
+
borderLeftColor: colors.accent,
|
|
625
|
+
boxShadow: isHovered
|
|
626
|
+
? 'var(--shadow-md, 0 4px 6px rgba(0,0,0,0.05))'
|
|
627
|
+
: 'var(--shadow-sm, 0 1px 3px rgba(0,0,0,0.06))',
|
|
628
|
+
transform: isHovered ? 'translateY(-2px)' : 'none',
|
|
629
|
+
transition: `box-shadow var(--duration-fast, 150ms) var(--ease-out, ease), transform var(--duration-fast, 150ms) var(--ease-out, ease)`,
|
|
630
|
+
backgroundColor: 'var(--color-surface, #ffffff)',
|
|
631
|
+
...(isHero ? so?.hero_card : {}),
|
|
632
|
+
...so?.card,
|
|
633
|
+
}, children: [_jsxs("div", { style: {
|
|
634
|
+
padding: isHero
|
|
635
|
+
? 'var(--space-md, 1rem) var(--space-lg, 1.5rem)'
|
|
636
|
+
: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
637
|
+
display: 'flex', alignItems: 'center', gap: 'var(--space-sm, 0.5rem)', flexWrap: 'wrap',
|
|
638
|
+
...so?.card_header,
|
|
639
|
+
}, children: [title && (_jsx("strong", { style: {
|
|
640
|
+
fontSize: isHero
|
|
641
|
+
? 'var(--type-heading, 1.375rem)'
|
|
642
|
+
: 'var(--type-body, 0.9375rem)',
|
|
643
|
+
fontWeight: 'var(--weight-semibold, 600)',
|
|
644
|
+
color: 'var(--color-text, #1a1d23)',
|
|
645
|
+
lineHeight: 'var(--leading-tight, 1.2)',
|
|
646
|
+
}, children: title })), subtitle && (_jsx("span", { style: {
|
|
647
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
648
|
+
fontWeight: 'var(--weight-medium, 500)',
|
|
649
|
+
color: colors.headerBg,
|
|
650
|
+
padding: '2px 10px',
|
|
651
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
652
|
+
backgroundColor: colors.lightBg,
|
|
653
|
+
letterSpacing: '0.02em',
|
|
654
|
+
...so?.badge,
|
|
655
|
+
}, children: subtitle })), badge && (_jsx("span", { style: {
|
|
656
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
657
|
+
fontWeight: 'var(--weight-bold, 700)',
|
|
658
|
+
padding: '2px 8px',
|
|
659
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
660
|
+
backgroundColor: colors.lightBg,
|
|
661
|
+
color: colors.darkText,
|
|
662
|
+
marginLeft: 'auto',
|
|
663
|
+
...so?.badge,
|
|
664
|
+
}, children: badge })), captureMode && onCapture && (_jsx("button", { title: "Capture this card", onClick: e => {
|
|
665
|
+
e.stopPropagation();
|
|
666
|
+
onCapture({
|
|
667
|
+
source_view_key: captureViewKey || '',
|
|
668
|
+
source_item_index: idx,
|
|
669
|
+
source_renderer_type: 'mini_card_list',
|
|
670
|
+
content_type: 'card',
|
|
671
|
+
selected_text: `${title}: ${description}`.slice(0, 500),
|
|
672
|
+
structured_data: obj,
|
|
673
|
+
context_title: parentSectionKey
|
|
674
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > ${title || `Card ${idx + 1}`}`
|
|
675
|
+
: `${captureViewKey || 'Analysis'} > ${title || `Card ${idx + 1}`}`,
|
|
676
|
+
source_type: (captureSourceType || 'analysis'),
|
|
677
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
678
|
+
depth_level: parentSectionKey ? 'L2_element' : 'L1_section',
|
|
679
|
+
parent_context: parentSectionKey ? {
|
|
680
|
+
section_key: parentSectionKey,
|
|
681
|
+
section_title: parentSectionTitle || '',
|
|
682
|
+
} : undefined,
|
|
683
|
+
});
|
|
684
|
+
}, style: {
|
|
685
|
+
background: 'none',
|
|
686
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
687
|
+
borderRadius: '4px',
|
|
688
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
689
|
+
cursor: 'pointer',
|
|
690
|
+
padding: '2px 6px',
|
|
691
|
+
fontSize: '0.7rem',
|
|
692
|
+
lineHeight: 1,
|
|
693
|
+
marginLeft: badge ? '0' : 'auto',
|
|
694
|
+
}, children: "\uD83D\uDCCC" }))] }), description && (_jsxs("div", { style: {
|
|
695
|
+
padding: isHero
|
|
696
|
+
? '0 var(--space-lg, 1.5rem) var(--space-md, 1rem)'
|
|
697
|
+
: '0 var(--space-md, 1rem) var(--space-sm, 0.5rem)',
|
|
698
|
+
...so?.card_body,
|
|
699
|
+
}, children: [_jsx("p", { style: {
|
|
700
|
+
fontSize: isHero
|
|
701
|
+
? 'var(--type-body, 0.9375rem)'
|
|
702
|
+
: 'var(--type-caption, 0.8125rem)',
|
|
703
|
+
color: 'var(--color-text, #1a1d23)',
|
|
704
|
+
lineHeight: 'var(--leading-relaxed, 1.65)',
|
|
705
|
+
margin: 0,
|
|
706
|
+
...so?.prose,
|
|
707
|
+
}, children: needsTruncation && !isContentExpanded
|
|
708
|
+
? description.slice(0, DESC_TRUNCATE_LEN) + '...'
|
|
709
|
+
: description }), needsTruncation && (_jsx("button", { className: "gen-show-more-link", onClick: (e) => { e.stopPropagation(); toggleExpand(idx); }, style: {
|
|
710
|
+
marginTop: 'var(--space-xs, 0.25rem)',
|
|
711
|
+
}, children: isContentExpanded ? 'show less' : 'show more' }))] })), scalarFields.length > 0 && (_jsx("div", { style: {
|
|
712
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
713
|
+
backgroundColor: 'var(--color-surface-alt, #f8f9fa)',
|
|
714
|
+
borderTop: '1px solid var(--color-border-light, #eef0f2)',
|
|
715
|
+
}, children: scalarFields.map(([key, value]) => (_jsxs("div", { style: { marginBottom: 'var(--space-2xs, 0.25rem)' }, children: [_jsxs("span", { className: "gen-inline-label", children: [key.replace(/_/g, ' '), ":"] }), _jsx("span", { style: {
|
|
716
|
+
marginLeft: 'var(--space-xs, 0.375rem)',
|
|
717
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
718
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
719
|
+
}, children: typeof value === 'object' ? JSON.stringify(value) : String(value) })] }, key))) })), chipFields.length > 0 && (_jsx("div", { style: {
|
|
720
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
721
|
+
backgroundColor: colors.lightBg,
|
|
722
|
+
borderTop: '1px solid var(--color-border-light, #eef0f2)',
|
|
723
|
+
}, children: chipFields.map(([key, value]) => (_jsxs("div", { style: { marginBottom: 'var(--space-xs, 0.375rem)' }, children: [_jsx("span", { className: "gen-inline-label", style: {
|
|
724
|
+
display: 'block',
|
|
725
|
+
marginBottom: 'var(--space-2xs, 0.25rem)',
|
|
726
|
+
}, children: key.replace(/_/g, ' ') }), _jsx("div", { style: { display: 'flex', gap: 'var(--space-xs, 0.25rem)', flexWrap: 'wrap' }, children: value.map((v, vi) => (_jsx("span", { className: "gen-keyword-tag", children: String(v) }, vi))) })] }, key))) }))] }, idx));
|
|
727
|
+
};
|
|
728
|
+
// Single card → render as hero only if hero mode enabled
|
|
729
|
+
if (data.length === 1) {
|
|
730
|
+
return (_jsx("div", { style: { ...so?.items_container }, children: renderCard(data[0], 0, useHero) }));
|
|
731
|
+
}
|
|
732
|
+
// Default: uniform grid layout
|
|
733
|
+
if (!useHero) {
|
|
734
|
+
return (_jsx("div", { style: {
|
|
735
|
+
display: 'grid',
|
|
736
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
|
737
|
+
gap: 'var(--space-md, 1rem)',
|
|
738
|
+
...so?.items_container,
|
|
739
|
+
}, children: data.map((item, i) => renderCard(item, i, false)) }));
|
|
740
|
+
}
|
|
741
|
+
// Multi-card layout: hero on top, rest in 2-column grid
|
|
742
|
+
return (_jsxs("div", { style: {
|
|
743
|
+
display: 'flex', flexDirection: 'column',
|
|
744
|
+
gap: 'var(--space-md, 1rem)',
|
|
745
|
+
...so?.items_container,
|
|
746
|
+
}, children: [renderCard(data[heroIdx], heroIdx, true), data.length > 1 && (_jsx("div", { style: {
|
|
747
|
+
display: 'grid',
|
|
748
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
|
749
|
+
gap: 'var(--space-md, 1rem)',
|
|
750
|
+
}, children: data.map((item, i) => {
|
|
751
|
+
if (i === heroIdx)
|
|
752
|
+
return null;
|
|
753
|
+
return renderCard(item, i, false);
|
|
754
|
+
}) }))] }));
|
|
755
|
+
}
|
|
756
|
+
// ── KeyValueTable ────────────────────────────────────────
|
|
757
|
+
function KeyValueTable({ data, config }) {
|
|
758
|
+
const { tokens } = useDesignTokens();
|
|
759
|
+
const so = getSO(config);
|
|
760
|
+
const firstObj = Array.isArray(data)
|
|
761
|
+
? data.find(d => typeof d === 'object' && d !== null)
|
|
762
|
+
: undefined;
|
|
763
|
+
const auto = firstObj ? autoDetectFields(firstObj) : {};
|
|
764
|
+
const keyField = resolveField(config, 'key_field', auto, 'key');
|
|
765
|
+
const valueField = resolveField(config, 'value_field', auto, 'value');
|
|
766
|
+
let rows = [];
|
|
767
|
+
if (Array.isArray(data)) {
|
|
768
|
+
rows = data.map(item => {
|
|
769
|
+
if (typeof item !== 'object' || item === null)
|
|
770
|
+
return { key: '', value: String(item) };
|
|
771
|
+
const obj = item;
|
|
772
|
+
return {
|
|
773
|
+
key: getField(obj, keyField) || Object.keys(obj)[0] || '',
|
|
774
|
+
value: getField(obj, valueField) || String(Object.values(obj).find(v => typeof v === 'string' && v.length > 20) ?? Object.values(obj)[1] ?? ''),
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
else if (typeof data === 'object' && data !== null) {
|
|
779
|
+
rows = Object.entries(data).map(([k, v]) => ({
|
|
780
|
+
key: k,
|
|
781
|
+
value: typeof v === 'object' ? JSON.stringify(v) : String(v ?? ''),
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
if (rows.length === 0)
|
|
785
|
+
return null;
|
|
786
|
+
const isNumeric = (val) => /^[\d,.]+%?$/.test(val.trim());
|
|
787
|
+
return (_jsx("div", { style: {
|
|
788
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
789
|
+
overflow: 'hidden',
|
|
790
|
+
border: '1px solid var(--color-border, #e2e5e9)',
|
|
791
|
+
boxShadow: 'var(--shadow-xs, 0 1px 2px rgba(0,0,0,0.04))',
|
|
792
|
+
...so?.card,
|
|
793
|
+
}, children: _jsx("table", { style: { width: '100%', borderCollapse: 'collapse' }, children: _jsx("tbody", { children: rows.map((row, i) => (_jsxs("tr", { style: {
|
|
794
|
+
backgroundColor: i % 2 === 0
|
|
795
|
+
? 'var(--color-surface, #ffffff)'
|
|
796
|
+
: 'var(--color-surface-alt, #f8f9fa)',
|
|
797
|
+
}, children: [_jsx("td", { style: {
|
|
798
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
799
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
800
|
+
fontWeight: 'var(--weight-medium, 500)',
|
|
801
|
+
color: tokens.surfaces.text_default,
|
|
802
|
+
width: '30%', verticalAlign: 'top',
|
|
803
|
+
textTransform: 'capitalize',
|
|
804
|
+
letterSpacing: '0.02em',
|
|
805
|
+
borderRight: `2px solid ${tokens.surfaces.border_accent}`,
|
|
806
|
+
backgroundColor: i % 2 === 0
|
|
807
|
+
? tokens.surfaces.surface_alt
|
|
808
|
+
: tokens.surfaces.surface_inset,
|
|
809
|
+
...so?.stat_label,
|
|
810
|
+
}, children: row.key.replace(/_/g, ' ') }), _jsx("td", { style: {
|
|
811
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
812
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
813
|
+
fontWeight: isNumeric(row.value)
|
|
814
|
+
? 'var(--weight-semibold, 600)'
|
|
815
|
+
: 'var(--weight-normal, 400)',
|
|
816
|
+
fontFamily: isNumeric(row.value) ? 'var(--font-mono, monospace)' : 'inherit',
|
|
817
|
+
color: 'var(--color-text, #1a1d23)',
|
|
818
|
+
lineHeight: 'var(--leading-normal, 1.5)',
|
|
819
|
+
...(isNumeric(row.value) ? so?.stat_number : {}),
|
|
820
|
+
}, children: row.value })] }, i))) }) }) }));
|
|
821
|
+
}
|
|
822
|
+
function parseProseContent(text) {
|
|
823
|
+
const lines = text.split('\n');
|
|
824
|
+
const segments = [];
|
|
825
|
+
let currentParagraph = [];
|
|
826
|
+
let currentBlockquote = [];
|
|
827
|
+
function flushParagraph() {
|
|
828
|
+
if (currentParagraph.length > 0) {
|
|
829
|
+
const joined = currentParagraph.join(' ').trim();
|
|
830
|
+
if (joined)
|
|
831
|
+
segments.push({ type: 'paragraph', content: joined });
|
|
832
|
+
currentParagraph = [];
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function flushBlockquote() {
|
|
836
|
+
if (currentBlockquote.length > 0) {
|
|
837
|
+
const joined = currentBlockquote.join(' ').trim();
|
|
838
|
+
if (joined)
|
|
839
|
+
segments.push({ type: 'blockquote', content: joined });
|
|
840
|
+
currentBlockquote = [];
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
for (const line of lines) {
|
|
844
|
+
const trimmed = line.trim();
|
|
845
|
+
if (/^(---+|\*\*\*+)$/.test(trimmed)) {
|
|
846
|
+
flushParagraph();
|
|
847
|
+
flushBlockquote();
|
|
848
|
+
segments.push({ type: 'hr', content: '' });
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const headingMatch = trimmed.match(/^(#{1,3})\s+(.+)/);
|
|
852
|
+
if (headingMatch) {
|
|
853
|
+
flushParagraph();
|
|
854
|
+
flushBlockquote();
|
|
855
|
+
segments.push({ type: 'heading', content: headingMatch[2], level: headingMatch[1].length });
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (trimmed.startsWith('>')) {
|
|
859
|
+
flushParagraph();
|
|
860
|
+
currentBlockquote.push(trimmed.replace(/^>\s*/, ''));
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (!trimmed) {
|
|
864
|
+
flushBlockquote();
|
|
865
|
+
flushParagraph();
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
flushBlockquote();
|
|
869
|
+
currentParagraph.push(trimmed);
|
|
870
|
+
}
|
|
871
|
+
flushParagraph();
|
|
872
|
+
flushBlockquote();
|
|
873
|
+
return segments;
|
|
874
|
+
}
|
|
875
|
+
function ProseBlock({ data, config }) {
|
|
876
|
+
const { tokens } = useDesignTokens();
|
|
877
|
+
const so = getSO(config);
|
|
878
|
+
if (!data)
|
|
879
|
+
return null;
|
|
880
|
+
const text = typeof data === 'string'
|
|
881
|
+
? data
|
|
882
|
+
: typeof data === 'object' && data !== null
|
|
883
|
+
? Object.values(data).filter(v => typeof v === 'string').join('\n\n')
|
|
884
|
+
: String(data);
|
|
885
|
+
if (!text.trim())
|
|
886
|
+
return null;
|
|
887
|
+
const accentHex = so?.accent_color || tokens.components.page_accent;
|
|
888
|
+
const segments = parseProseContent(text);
|
|
889
|
+
let paragraphIndex = 0;
|
|
890
|
+
return (_jsx("div", { style: {
|
|
891
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
892
|
+
lineHeight: 'var(--leading-relaxed, 1.65)',
|
|
893
|
+
color: 'var(--color-text, #1a1d23)',
|
|
894
|
+
...so?.prose,
|
|
895
|
+
}, children: segments.map((segment, i) => {
|
|
896
|
+
if (segment.type === 'hr') {
|
|
897
|
+
return (_jsx("hr", { style: {
|
|
898
|
+
border: 'none',
|
|
899
|
+
height: '1px',
|
|
900
|
+
backgroundColor: 'var(--color-border, #e2e5e9)',
|
|
901
|
+
margin: 'var(--space-lg, 1.5rem) var(--space-xl, 2rem)',
|
|
902
|
+
} }, i));
|
|
903
|
+
}
|
|
904
|
+
if (segment.type === 'heading') {
|
|
905
|
+
const headingSizes = {
|
|
906
|
+
1: 'var(--type-heading, 1.375rem)',
|
|
907
|
+
2: 'var(--type-subheading, 1.125rem)',
|
|
908
|
+
3: 'var(--type-body, 0.9375rem)',
|
|
909
|
+
};
|
|
910
|
+
return (_jsx("p", { style: {
|
|
911
|
+
fontSize: headingSizes[segment.level || 3],
|
|
912
|
+
fontWeight: 'var(--weight-bold, 700)',
|
|
913
|
+
color: tokens.surfaces.text_default,
|
|
914
|
+
marginTop: 'var(--space-lg, 1.5rem)',
|
|
915
|
+
marginBottom: 'var(--space-sm, 0.5rem)',
|
|
916
|
+
lineHeight: 'var(--leading-tight, 1.2)',
|
|
917
|
+
}, children: segment.content }, i));
|
|
918
|
+
}
|
|
919
|
+
if (segment.type === 'blockquote') {
|
|
920
|
+
return (_jsx("blockquote", { style: {
|
|
921
|
+
margin: 'var(--space-md, 1rem) 0',
|
|
922
|
+
padding: 'var(--space-md, 1rem) var(--space-lg, 1.5rem)',
|
|
923
|
+
borderLeft: `4px solid ${accentHex}`,
|
|
924
|
+
backgroundColor: tokens.components.prose_blockquote_bg,
|
|
925
|
+
borderRadius: '0 var(--radius-md, 8px) var(--radius-md, 8px) 0',
|
|
926
|
+
fontStyle: 'italic',
|
|
927
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
928
|
+
lineHeight: 'var(--leading-loose, 1.8)',
|
|
929
|
+
...so?.prose_quote,
|
|
930
|
+
}, children: renderInlineMarkdown(segment.content, accentHex) }, i));
|
|
931
|
+
}
|
|
932
|
+
// Paragraph: first paragraph is lede
|
|
933
|
+
const isLede = paragraphIndex === 0;
|
|
934
|
+
paragraphIndex++;
|
|
935
|
+
if (isLede) {
|
|
936
|
+
return (_jsx("p", { style: {
|
|
937
|
+
fontSize: 'var(--type-subheading, 1.125rem)',
|
|
938
|
+
fontWeight: 'var(--weight-medium, 500)',
|
|
939
|
+
lineHeight: 'var(--leading-snug, 1.35)',
|
|
940
|
+
color: 'var(--color-text, #1a1d23)',
|
|
941
|
+
marginTop: 0,
|
|
942
|
+
marginBottom: 'var(--space-md, 1rem)',
|
|
943
|
+
...so?.prose_lede,
|
|
944
|
+
}, children: renderInlineMarkdown(segment.content, accentHex) }, i));
|
|
945
|
+
}
|
|
946
|
+
return (_jsx("p", { style: {
|
|
947
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
948
|
+
fontWeight: 'var(--weight-normal, 400)',
|
|
949
|
+
lineHeight: 'var(--leading-relaxed, 1.65)',
|
|
950
|
+
color: 'var(--color-text, #1a1d23)',
|
|
951
|
+
marginTop: 0,
|
|
952
|
+
marginBottom: 'var(--space-md, 1rem)',
|
|
953
|
+
...so?.prose_body,
|
|
954
|
+
}, children: renderInlineMarkdown(segment.content, accentHex) }, i));
|
|
955
|
+
}) }));
|
|
956
|
+
}
|
|
957
|
+
// ── StatRow ──────────────────────────────────────────────
|
|
958
|
+
function StatRow({ data, config }) {
|
|
959
|
+
const { tokens } = useDesignTokens();
|
|
960
|
+
if (!data || typeof data !== 'object')
|
|
961
|
+
return null;
|
|
962
|
+
const so = getSO(config);
|
|
963
|
+
const obj = data;
|
|
964
|
+
const entries = Object.entries(obj).filter(([, v]) => v !== null && v !== undefined);
|
|
965
|
+
if (entries.length === 0)
|
|
966
|
+
return null;
|
|
967
|
+
return (_jsx("div", { style: {
|
|
968
|
+
display: 'grid',
|
|
969
|
+
gridTemplateColumns: `repeat(${Math.min(entries.length, 4)}, 1fr)`,
|
|
970
|
+
gap: 'var(--space-md, 0.75rem)',
|
|
971
|
+
...so?.items_container,
|
|
972
|
+
}, children: entries.map(([key, value]) => (_jsxs("div", { style: {
|
|
973
|
+
padding: 'var(--space-md, 1rem)',
|
|
974
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
975
|
+
backgroundColor: 'var(--color-surface, #ffffff)',
|
|
976
|
+
border: '1px solid var(--color-border-light, #eef0f2)',
|
|
977
|
+
boxShadow: 'var(--shadow-xs, 0 1px 2px rgba(0,0,0,0.04))',
|
|
978
|
+
textAlign: 'center',
|
|
979
|
+
...so?.card,
|
|
980
|
+
}, children: [_jsx("div", { style: {
|
|
981
|
+
fontSize: 'var(--type-number, 1.25rem)',
|
|
982
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
983
|
+
fontWeight: 'var(--weight-bold, 700)',
|
|
984
|
+
color: tokens.components.stat_number_color,
|
|
985
|
+
lineHeight: 'var(--leading-tight, 1.2)',
|
|
986
|
+
...so?.stat_number,
|
|
987
|
+
}, children: typeof value === 'number' ? value.toLocaleString() : String(value) }), _jsx("div", { className: "gen-inline-label", style: {
|
|
988
|
+
marginTop: 'var(--space-xs, 0.25rem)',
|
|
989
|
+
...so?.stat_label,
|
|
990
|
+
}, children: key.replace(/_/g, ' ') })] }, key))) }));
|
|
991
|
+
}
|
|
992
|
+
// ── ComparisonPanel ──────────────────────────────────────
|
|
993
|
+
function ComparisonPanel({ data, config }) {
|
|
994
|
+
const { tokens } = useDesignTokens();
|
|
995
|
+
if (!data || !Array.isArray(data))
|
|
996
|
+
return null;
|
|
997
|
+
const so = getSO(config);
|
|
998
|
+
// Capture mode support (threaded from AccordionRenderer)
|
|
999
|
+
const captureMode = config._captureMode;
|
|
1000
|
+
const onCapture = config._onCapture;
|
|
1001
|
+
const captureJobId = config._captureJobId;
|
|
1002
|
+
const captureViewKey = config._captureViewKey;
|
|
1003
|
+
const captureSourceType = config._captureSourceType;
|
|
1004
|
+
const captureEntityId = config._captureEntityId;
|
|
1005
|
+
const parentSectionKey = config._parentSectionKey;
|
|
1006
|
+
const parentSectionTitle = config._parentSectionTitle;
|
|
1007
|
+
const firstObj = data.find(d => typeof d === 'object' && d !== null);
|
|
1008
|
+
const longStrings = firstObj
|
|
1009
|
+
? Object.entries(firstObj).filter(([, v]) => typeof v === 'string' && v.length > 20).map(([k]) => k)
|
|
1010
|
+
: [];
|
|
1011
|
+
const leftField = config.left_field || longStrings[0];
|
|
1012
|
+
const rightField = config.right_field || longStrings[1];
|
|
1013
|
+
const leftLabel = leftField ? leftField.replace(/_/g, ' ') : 'Left';
|
|
1014
|
+
const rightLabel = rightField ? rightField.replace(/_/g, ' ') : 'Right';
|
|
1015
|
+
return (_jsxs("div", { style: {
|
|
1016
|
+
display: 'flex', flexDirection: 'column',
|
|
1017
|
+
gap: 'var(--space-md, 0.75rem)',
|
|
1018
|
+
...so?.items_container,
|
|
1019
|
+
}, children: [_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1px' }, children: [_jsx("div", { style: {
|
|
1020
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
1021
|
+
backgroundColor: tokens.primitives.series_palette[0],
|
|
1022
|
+
color: tokens.surfaces.text_on_accent,
|
|
1023
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1024
|
+
fontWeight: 'var(--weight-semibold, 600)',
|
|
1025
|
+
textTransform: 'uppercase',
|
|
1026
|
+
letterSpacing: '0.06em',
|
|
1027
|
+
borderRadius: 'var(--radius-md, 8px) 0 0 0',
|
|
1028
|
+
...so?.card_header,
|
|
1029
|
+
}, children: leftLabel }), _jsx("div", { style: {
|
|
1030
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
1031
|
+
backgroundColor: tokens.primitives.series_palette[1],
|
|
1032
|
+
color: 'var(--dt-text-inverse)',
|
|
1033
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1034
|
+
fontWeight: 'var(--weight-semibold, 600)',
|
|
1035
|
+
textTransform: 'uppercase',
|
|
1036
|
+
letterSpacing: '0.06em',
|
|
1037
|
+
borderRadius: '0 var(--radius-md, 8px) 0 0',
|
|
1038
|
+
...so?.card_header,
|
|
1039
|
+
}, children: rightLabel })] }), data.map((item, i) => {
|
|
1040
|
+
if (typeof item !== 'object' || item === null)
|
|
1041
|
+
return null;
|
|
1042
|
+
const obj = item;
|
|
1043
|
+
const left = getField(obj, leftField);
|
|
1044
|
+
const right = getField(obj, rightField);
|
|
1045
|
+
const otherFields = Object.entries(obj).filter(([k, v]) => k !== leftField && k !== rightField && v !== null && v !== undefined && v !== '');
|
|
1046
|
+
// Build a label for this comparison row
|
|
1047
|
+
const rowLabel = otherFields.length > 0
|
|
1048
|
+
? otherFields.map(([k, v]) => `${k.replace(/_/g, ' ')}: ${String(v)}`).join(', ')
|
|
1049
|
+
: `Row ${i + 1}`;
|
|
1050
|
+
return (_jsxs("div", { style: {
|
|
1051
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
1052
|
+
border: '1px solid var(--color-border, #e2e5e9)',
|
|
1053
|
+
overflow: 'hidden',
|
|
1054
|
+
boxShadow: 'var(--shadow-xs, 0 1px 2px rgba(0,0,0,0.04))',
|
|
1055
|
+
}, children: [(otherFields.length > 0 || (captureMode && onCapture)) && (_jsxs("div", { style: {
|
|
1056
|
+
padding: 'var(--space-xs, 0.375rem) var(--space-md, 0.75rem)',
|
|
1057
|
+
backgroundColor: 'var(--color-surface-alt, #f8f9fa)',
|
|
1058
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1059
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
1060
|
+
borderBottom: '1px solid var(--color-border-light, #eef0f2)',
|
|
1061
|
+
display: 'flex', gap: 'var(--space-sm, 0.5rem)', flexWrap: 'wrap',
|
|
1062
|
+
alignItems: 'center',
|
|
1063
|
+
}, children: [otherFields.map(([k, v]) => (_jsxs("span", { children: [_jsx("strong", { style: { textTransform: 'capitalize' }, children: k.replace(/_/g, ' ') }), ": ", String(v)] }, k))), captureMode && onCapture && (_jsx("button", { title: "Capture this comparison", onClick: e => {
|
|
1064
|
+
e.stopPropagation();
|
|
1065
|
+
onCapture({
|
|
1066
|
+
source_view_key: captureViewKey || '',
|
|
1067
|
+
source_section_key: parentSectionKey,
|
|
1068
|
+
source_item_index: i,
|
|
1069
|
+
source_renderer_type: 'comparison_panel',
|
|
1070
|
+
content_type: 'item',
|
|
1071
|
+
selected_text: `${leftLabel}: ${left || '—'} vs ${rightLabel}: ${right || '—'}`.slice(0, 500),
|
|
1072
|
+
structured_data: obj,
|
|
1073
|
+
context_title: parentSectionKey
|
|
1074
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > ${rowLabel}`
|
|
1075
|
+
: `${captureViewKey || 'Analysis'} > ${rowLabel}`,
|
|
1076
|
+
source_type: (captureSourceType || 'analysis'),
|
|
1077
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
1078
|
+
depth_level: parentSectionKey ? 'L2_element' : 'L1_section',
|
|
1079
|
+
parent_context: parentSectionKey ? {
|
|
1080
|
+
section_key: parentSectionKey,
|
|
1081
|
+
section_title: parentSectionTitle || '',
|
|
1082
|
+
} : undefined,
|
|
1083
|
+
});
|
|
1084
|
+
}, style: {
|
|
1085
|
+
marginLeft: 'auto',
|
|
1086
|
+
flexShrink: 0,
|
|
1087
|
+
background: 'none',
|
|
1088
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
1089
|
+
borderRadius: '4px',
|
|
1090
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
1091
|
+
cursor: 'pointer',
|
|
1092
|
+
padding: '2px 6px',
|
|
1093
|
+
fontSize: '0.7rem',
|
|
1094
|
+
lineHeight: 1,
|
|
1095
|
+
}, children: "\uD83D\uDCCC" }))] })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr auto 1fr' }, children: [_jsx("div", { style: {
|
|
1096
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
1097
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
1098
|
+
color: 'var(--color-text, #1a1d23)',
|
|
1099
|
+
lineHeight: 'var(--leading-normal, 1.5)',
|
|
1100
|
+
...so?.card_body,
|
|
1101
|
+
}, children: left || _jsx("span", { style: { color: 'var(--color-text-faint, #9ca3af)', fontStyle: 'italic' }, children: "\u2014" }) }), _jsx("div", { style: {
|
|
1102
|
+
width: '2px',
|
|
1103
|
+
background: `linear-gradient(to bottom, ${tokens.surfaces.border_light}, ${tokens.surfaces.border_accent}, ${tokens.surfaces.border_light})`,
|
|
1104
|
+
} }), _jsx("div", { style: {
|
|
1105
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
1106
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
1107
|
+
color: 'var(--color-text, #1a1d23)',
|
|
1108
|
+
lineHeight: 'var(--leading-normal, 1.5)',
|
|
1109
|
+
...so?.card_body,
|
|
1110
|
+
}, children: right || _jsx("span", { style: { color: 'var(--color-text-faint, #9ca3af)', fontStyle: 'italic' }, children: "\u2014" }) })] })] }, i));
|
|
1111
|
+
})] }));
|
|
1112
|
+
}
|
|
1113
|
+
// ── TimelineStrip → Evolution Arc ────────────────────────
|
|
1114
|
+
function TimelineStrip({ data, config }) {
|
|
1115
|
+
const { tokens } = useDesignTokens();
|
|
1116
|
+
const so = getSO(config);
|
|
1117
|
+
const [expandedCard, setExpandedCard] = React.useState(null);
|
|
1118
|
+
if (!data || !Array.isArray(data))
|
|
1119
|
+
return null;
|
|
1120
|
+
// TimelineStrip uses HSL-derived progressive coloring for evolution arcs.
|
|
1121
|
+
// Derive accent HSL from the token system's accent color.
|
|
1122
|
+
const accent = parseAccentHSL(so?.accent_color || tokens.components.page_accent);
|
|
1123
|
+
const firstObj = data.find(d => typeof d === 'object' && d !== null);
|
|
1124
|
+
const auto = firstObj ? autoDetectFields(firstObj) : {};
|
|
1125
|
+
const labelField = resolveField(config, 'label_field', auto, 'label');
|
|
1126
|
+
const stagesField = config.stages_field
|
|
1127
|
+
|| (firstObj ? Object.entries(firstObj).find(([, v]) => Array.isArray(v))?.[0] : undefined);
|
|
1128
|
+
function renderStageNode(stage, j, totalStages) {
|
|
1129
|
+
// Visual progression: size and saturation increase from left to right
|
|
1130
|
+
const progress = totalStages > 1 ? j / (totalStages - 1) : 0.5;
|
|
1131
|
+
const saturation = Math.max(accent.s * 0.3, 12) + progress * 30;
|
|
1132
|
+
const bgLightness = 96 - progress * 8;
|
|
1133
|
+
const textLightness = 30 - progress * 10;
|
|
1134
|
+
const borderSat = Math.max(accent.s * 0.4, 15) + progress * 20;
|
|
1135
|
+
// Size increases with progress
|
|
1136
|
+
const padV = `${0.4 + progress * 0.3}rem`;
|
|
1137
|
+
const padH = `${0.6 + progress * 0.4}rem`;
|
|
1138
|
+
const fontSize = progress > 0.6
|
|
1139
|
+
? 'var(--type-caption, 0.8125rem)'
|
|
1140
|
+
: 'var(--type-label, 0.6875rem)';
|
|
1141
|
+
if (typeof stage === 'string') {
|
|
1142
|
+
return (_jsx("div", { style: {
|
|
1143
|
+
padding: `${padV} ${padH}`,
|
|
1144
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
1145
|
+
backgroundColor: `hsl(${accent.h}, ${saturation}%, ${bgLightness}%)`,
|
|
1146
|
+
border: `1.5px solid hsl(${accent.h}, ${borderSat}%, ${78 - progress * 10}%)`,
|
|
1147
|
+
fontSize,
|
|
1148
|
+
fontWeight: 'var(--weight-medium, 500)',
|
|
1149
|
+
lineHeight: 'var(--leading-snug, 1.35)',
|
|
1150
|
+
color: `hsl(${accent.h}, ${Math.min(accent.s + 10, 75)}%, ${textLightness}%)`,
|
|
1151
|
+
minWidth: `${130 + progress * 40}px`,
|
|
1152
|
+
maxWidth: '280px',
|
|
1153
|
+
flexShrink: 0,
|
|
1154
|
+
boxShadow: 'var(--shadow-xs, 0 1px 2px rgba(0,0,0,0.04))',
|
|
1155
|
+
...so?.timeline_node,
|
|
1156
|
+
}, children: stage }));
|
|
1157
|
+
}
|
|
1158
|
+
if (typeof stage === 'object' && stage !== null) {
|
|
1159
|
+
const obj = stage;
|
|
1160
|
+
const stageAuto = autoDetectFields(obj);
|
|
1161
|
+
const primaryLabel = getField(obj, 'form') || getField(obj, stageAuto.title)
|
|
1162
|
+
|| getField(obj, 'label') || getField(obj, 'name');
|
|
1163
|
+
const periodLabel = getField(obj, 'period') || getField(obj, 'era') || getField(obj, 'date') || getField(obj, 'year');
|
|
1164
|
+
const secondaryFields = Object.entries(obj).filter(([k, v]) => !['form', 'name', 'title', 'label', 'period', 'era', 'date', 'year'].includes(k)
|
|
1165
|
+
&& typeof v === 'string' && v.length > 0);
|
|
1166
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', flexShrink: 0 }, children: [periodLabel && (_jsx("div", { style: {
|
|
1167
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1168
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
1169
|
+
fontWeight: 'var(--weight-semibold, 600)',
|
|
1170
|
+
color: `hsl(${accent.h}, ${Math.min(accent.s + 10, 70)}%, 45%)`,
|
|
1171
|
+
letterSpacing: '0.04em',
|
|
1172
|
+
marginBottom: 'var(--space-xs, 0.25rem)',
|
|
1173
|
+
textAlign: 'center',
|
|
1174
|
+
}, children: periodLabel })), _jsxs("div", { style: {
|
|
1175
|
+
padding: `${padV} ${padH}`,
|
|
1176
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
1177
|
+
backgroundColor: `hsl(${accent.h}, ${saturation}%, ${bgLightness}%)`,
|
|
1178
|
+
border: `1.5px solid hsl(${accent.h}, ${borderSat}%, ${78 - progress * 10}%)`,
|
|
1179
|
+
fontSize,
|
|
1180
|
+
lineHeight: 'var(--leading-snug, 1.35)',
|
|
1181
|
+
color: `hsl(${accent.h}, ${Math.min(accent.s + 10, 75)}%, ${textLightness}%)`,
|
|
1182
|
+
minWidth: `${130 + progress * 40}px`,
|
|
1183
|
+
maxWidth: '280px',
|
|
1184
|
+
boxShadow: 'var(--shadow-xs, 0 1px 2px rgba(0,0,0,0.04))',
|
|
1185
|
+
...so?.timeline_node,
|
|
1186
|
+
}, children: [_jsx("div", { style: {
|
|
1187
|
+
fontWeight: 'var(--weight-semibold, 600)',
|
|
1188
|
+
marginBottom: secondaryFields.length > 0 ? 'var(--space-2xs, 0.25rem)' : 0,
|
|
1189
|
+
}, children: primaryLabel || 'Stage ' + (j + 1) }), secondaryFields.slice(0, 2).map(([k, v]) => (_jsxs("div", { style: {
|
|
1190
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1191
|
+
color: `hsl(${accent.h}, ${Math.max(accent.s * 0.5, 15)}%, ${40 - progress * 5}%)`,
|
|
1192
|
+
marginTop: 'var(--space-2xs, 0.15rem)',
|
|
1193
|
+
}, children: [_jsxs("span", { className: "gen-inline-label", children: [k.replace(/_/g, ' '), ":"] }), ' ', String(v).length > 80 ? String(v).slice(0, 77) + '...' : String(v)] }, k)))] })] }));
|
|
1194
|
+
}
|
|
1195
|
+
return (_jsx("div", { style: {
|
|
1196
|
+
padding: `${padV} ${padH}`,
|
|
1197
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
1198
|
+
backgroundColor: `hsl(${accent.h}, ${saturation}%, ${bgLightness}%)`,
|
|
1199
|
+
border: `1px solid hsl(${accent.h}, ${borderSat}%, ${80 - progress * 8}%)`,
|
|
1200
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1201
|
+
color: `hsl(${accent.h}, ${Math.min(accent.s + 10, 70)}%, ${textLightness}%)`,
|
|
1202
|
+
flexShrink: 0,
|
|
1203
|
+
...so?.timeline_node,
|
|
1204
|
+
}, children: String(stage) }));
|
|
1205
|
+
}
|
|
1206
|
+
/** Render the connecting arrow between stages with gradient */
|
|
1207
|
+
function renderConnector(j, totalStages) {
|
|
1208
|
+
const progress = totalStages > 1 ? j / (totalStages - 1) : 0;
|
|
1209
|
+
const nextProgress = totalStages > 1 ? (j + 1) / (totalStages - 1) : 1;
|
|
1210
|
+
const startLight = 82 - progress * 25;
|
|
1211
|
+
const endLight = 82 - nextProgress * 25;
|
|
1212
|
+
const startSat = Math.max(accent.s * 0.3, 12) + progress * 25;
|
|
1213
|
+
const endSat = Math.max(accent.s * 0.3, 12) + nextProgress * 25;
|
|
1214
|
+
return (_jsx("div", { style: {
|
|
1215
|
+
display: 'flex', alignItems: 'center',
|
|
1216
|
+
flexShrink: 0, padding: '0 2px',
|
|
1217
|
+
alignSelf: 'center',
|
|
1218
|
+
...so?.timeline_connector,
|
|
1219
|
+
}, children: _jsx("div", { style: {
|
|
1220
|
+
width: '24px', height: '3px',
|
|
1221
|
+
background: `linear-gradient(to right, hsl(${accent.h}, ${startSat}%, ${startLight}%), hsl(${accent.h}, ${endSat}%, ${endLight}%))`,
|
|
1222
|
+
borderRadius: '2px',
|
|
1223
|
+
position: 'relative',
|
|
1224
|
+
}, children: _jsx("div", { style: {
|
|
1225
|
+
position: 'absolute', right: '-5px', top: '-4px',
|
|
1226
|
+
width: 0, height: 0,
|
|
1227
|
+
borderTop: '5.5px solid transparent',
|
|
1228
|
+
borderBottom: '5.5px solid transparent',
|
|
1229
|
+
borderLeft: `8px solid hsl(${accent.h}, ${endSat}%, ${endLight}%)`,
|
|
1230
|
+
} }) }) }));
|
|
1231
|
+
}
|
|
1232
|
+
return (_jsx("div", { style: {
|
|
1233
|
+
display: 'flex', flexDirection: 'column',
|
|
1234
|
+
gap: 'var(--space-lg, 1.25rem)',
|
|
1235
|
+
...so?.items_container,
|
|
1236
|
+
}, children: data.map((item, i) => {
|
|
1237
|
+
if (typeof item !== 'object' || item === null)
|
|
1238
|
+
return null;
|
|
1239
|
+
const obj = item;
|
|
1240
|
+
const label = getField(obj, labelField);
|
|
1241
|
+
const stages = stagesField ? obj[stagesField] : null;
|
|
1242
|
+
const cardId = `${label || i}`;
|
|
1243
|
+
const isExpanded = expandedCard === cardId;
|
|
1244
|
+
const metaFields = Object.entries(obj).filter(([k, v]) => k !== labelField && k !== stagesField && v !== null && v !== undefined && v !== '');
|
|
1245
|
+
return (_jsxs("div", { style: {
|
|
1246
|
+
minWidth: 0, overflow: 'hidden',
|
|
1247
|
+
borderRadius: 'var(--radius-lg, 12px)',
|
|
1248
|
+
border: '1px solid var(--color-border, #e2e5e9)',
|
|
1249
|
+
backgroundColor: 'var(--color-surface, #ffffff)',
|
|
1250
|
+
boxShadow: 'var(--shadow-sm, 0 1px 3px rgba(0,0,0,0.06))',
|
|
1251
|
+
...so?.card,
|
|
1252
|
+
}, children: [label && (_jsxs("div", { onClick: () => setExpandedCard(isExpanded ? null : cardId), style: {
|
|
1253
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
1254
|
+
fontWeight: 'var(--weight-semibold, 600)',
|
|
1255
|
+
color: 'var(--color-text, #1a1d23)',
|
|
1256
|
+
padding: 'var(--space-sm, 0.625rem) var(--space-md, 1rem)',
|
|
1257
|
+
backgroundColor: 'var(--color-surface-alt, #f8f9fa)',
|
|
1258
|
+
borderBottom: '1px solid var(--color-border-light, #eef0f2)',
|
|
1259
|
+
cursor: metaFields.length > 0 ? 'pointer' : 'default',
|
|
1260
|
+
display: 'flex', alignItems: 'center',
|
|
1261
|
+
gap: 'var(--space-sm, 0.5rem)',
|
|
1262
|
+
...so?.card_header,
|
|
1263
|
+
}, children: [label, Array.isArray(stages) && (_jsxs("span", { style: {
|
|
1264
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1265
|
+
fontWeight: 'var(--weight-medium, 500)',
|
|
1266
|
+
color: 'var(--color-text-faint, #9ca3af)',
|
|
1267
|
+
backgroundColor: tokens.surfaces.surface_inset,
|
|
1268
|
+
padding: '2px 8px',
|
|
1269
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
1270
|
+
...so?.badge,
|
|
1271
|
+
}, children: [stages.length, " stages"] }))] })), Array.isArray(stages) && stages.length > 0 && (_jsx("div", { style: {
|
|
1272
|
+
padding: 'var(--space-md, 0.75rem)',
|
|
1273
|
+
overflowX: 'auto',
|
|
1274
|
+
}, children: _jsx("div", { style: {
|
|
1275
|
+
display: 'flex', alignItems: 'flex-end',
|
|
1276
|
+
gap: 0,
|
|
1277
|
+
minWidth: 'min-content',
|
|
1278
|
+
}, children: stages.map((stage, j) => (_jsxs(React.Fragment, { children: [renderStageNode(stage, j, stages.length), j < stages.length - 1 && renderConnector(j, stages.length)] }, j))) }) })), isExpanded && metaFields.length > 0 && (_jsx("div", { style: {
|
|
1279
|
+
padding: 'var(--space-sm, 0.625rem) var(--space-md, 1rem)',
|
|
1280
|
+
borderTop: '1px solid var(--color-border-light, #eef0f2)',
|
|
1281
|
+
backgroundColor: 'var(--color-surface, #ffffff)',
|
|
1282
|
+
}, children: metaFields.map(([k, v]) => (_jsxs("div", { style: { marginBottom: 'var(--space-2xs, 0.25rem)' }, children: [_jsxs("span", { className: "gen-inline-label", children: [k.replace(/_/g, ' '), ":"] }), _jsx("span", { style: {
|
|
1283
|
+
marginLeft: 'var(--space-xs, 0.375rem)',
|
|
1284
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1285
|
+
color: 'var(--color-text, #1a1d23)',
|
|
1286
|
+
}, children: typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v) })] }, k))) })), (!stages || !Array.isArray(stages) || stages.length === 0) && metaFields.length > 0 && (_jsx("div", { style: {
|
|
1287
|
+
padding: 'var(--space-sm, 0.625rem) var(--space-md, 1rem)',
|
|
1288
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1289
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
1290
|
+
}, children: metaFields.map(([k, v]) => (_jsxs("div", { style: { marginBottom: 'var(--space-2xs, 0.25rem)' }, children: [_jsxs("strong", { style: {
|
|
1291
|
+
textTransform: 'capitalize',
|
|
1292
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1293
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
1294
|
+
}, children: [k.replace(/_/g, ' '), ":"] }), ' ', typeof v === 'string' ? v : JSON.stringify(v)] }, k))) }))] }, i));
|
|
1295
|
+
}) }));
|
|
1296
|
+
}
|
|
1297
|
+
// ── IntensityMatrix → Dashboard-style Intensity Bars ────────
|
|
1298
|
+
//
|
|
1299
|
+
// Each row: title + category badge + horizontal bar + expandable description.
|
|
1300
|
+
// Bars width proportional to ordinal intensity level. Sorted by intensity (highest first).
|
|
1301
|
+
//
|
|
1302
|
+
// Config:
|
|
1303
|
+
// title_field — row title
|
|
1304
|
+
// subtitle_field — category badge
|
|
1305
|
+
// intensity_field — ordinal intensity value
|
|
1306
|
+
// intensity_scale — ordered levels, e.g. ['low', 'medium', 'high']
|
|
1307
|
+
// description_field — expandable text
|
|
1308
|
+
// sort_by_intensity — sort by intensity descending (default true)
|
|
1309
|
+
function IntensityMatrix({ data, config }) {
|
|
1310
|
+
const [expandedIdx, setExpandedIdx] = React.useState(new Set());
|
|
1311
|
+
const { tokens, getSemanticColor } = useDesignTokens();
|
|
1312
|
+
const captureMode = config._captureMode;
|
|
1313
|
+
const onCapture = config._onCapture;
|
|
1314
|
+
const captureJobId = config._captureJobId;
|
|
1315
|
+
const captureViewKey = config._captureViewKey;
|
|
1316
|
+
const captureSourceType = config._captureSourceType;
|
|
1317
|
+
const captureEntityId = config._captureEntityId;
|
|
1318
|
+
const parentSectionKey = config._parentSectionKey;
|
|
1319
|
+
const parentSectionTitle = config._parentSectionTitle;
|
|
1320
|
+
if (!data || !Array.isArray(data) || data.length === 0)
|
|
1321
|
+
return null;
|
|
1322
|
+
const titleField = config.title_field || 'name';
|
|
1323
|
+
const subtitleField = config.subtitle_field || undefined;
|
|
1324
|
+
const intensityField = config.intensity_field || 'intensity';
|
|
1325
|
+
const descField = config.description_field || 'description';
|
|
1326
|
+
const sortByIntensity = config.sort_by_intensity !== false;
|
|
1327
|
+
// Build intensity scale from config or infer from data
|
|
1328
|
+
const configScale = config.intensity_scale;
|
|
1329
|
+
const intensityScale = configScale
|
|
1330
|
+
|| (() => {
|
|
1331
|
+
const vals = new Set();
|
|
1332
|
+
data.forEach(item => {
|
|
1333
|
+
if (typeof item === 'object' && item !== null) {
|
|
1334
|
+
const v = item[intensityField];
|
|
1335
|
+
if (typeof v === 'string')
|
|
1336
|
+
vals.add(v.toLowerCase());
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
// Default ordering heuristic
|
|
1340
|
+
const ordered = ['rare', 'low', 'minimal', 'occasional', 'moderate', 'medium', 'frequent', 'significant', 'high', 'very_high', 'critical'];
|
|
1341
|
+
const found = ordered.filter(l => vals.has(l));
|
|
1342
|
+
return found.length > 0 ? found : Array.from(vals);
|
|
1343
|
+
})();
|
|
1344
|
+
// Map intensity value to 0–1 fraction
|
|
1345
|
+
const getIntensityFraction = (val) => {
|
|
1346
|
+
const lower = val.toLowerCase().replace(/\s+/g, '_');
|
|
1347
|
+
const idx = intensityScale.findIndex(s => s.toLowerCase().replace(/\s+/g, '_') === lower);
|
|
1348
|
+
if (idx === -1)
|
|
1349
|
+
return 0.5;
|
|
1350
|
+
if (intensityScale.length <= 1)
|
|
1351
|
+
return 1;
|
|
1352
|
+
return (idx + 1) / intensityScale.length;
|
|
1353
|
+
};
|
|
1354
|
+
// Color for intensity bar — use semantic severity or series palette
|
|
1355
|
+
const getBarColor = (fraction) => {
|
|
1356
|
+
const palette = tokens.primitives.series_palette;
|
|
1357
|
+
if (fraction >= 0.8) {
|
|
1358
|
+
const sem = getSemanticColor('severity', 'high');
|
|
1359
|
+
return sem?.bg || palette[0];
|
|
1360
|
+
}
|
|
1361
|
+
if (fraction >= 0.5) {
|
|
1362
|
+
const sem = getSemanticColor('severity', 'medium');
|
|
1363
|
+
return sem?.bg || palette[4];
|
|
1364
|
+
}
|
|
1365
|
+
const sem = getSemanticColor('severity', 'low');
|
|
1366
|
+
return sem?.bg || palette[2];
|
|
1367
|
+
};
|
|
1368
|
+
const getBarTextColor = (fraction) => {
|
|
1369
|
+
if (fraction >= 0.8) {
|
|
1370
|
+
const sem = getSemanticColor('severity', 'high');
|
|
1371
|
+
return sem?.text || tokens.surfaces.text_default;
|
|
1372
|
+
}
|
|
1373
|
+
if (fraction >= 0.5) {
|
|
1374
|
+
const sem = getSemanticColor('severity', 'medium');
|
|
1375
|
+
return sem?.text || tokens.surfaces.text_default;
|
|
1376
|
+
}
|
|
1377
|
+
const sem = getSemanticColor('severity', 'low');
|
|
1378
|
+
return sem?.text || tokens.surfaces.text_default;
|
|
1379
|
+
};
|
|
1380
|
+
// Prepare and optionally sort items
|
|
1381
|
+
const items = data
|
|
1382
|
+
.map((item, origIdx) => {
|
|
1383
|
+
if (typeof item !== 'object' || item === null)
|
|
1384
|
+
return null;
|
|
1385
|
+
const obj = item;
|
|
1386
|
+
const intensityVal = getField(obj, intensityField);
|
|
1387
|
+
const fraction = getIntensityFraction(intensityVal);
|
|
1388
|
+
return { obj, origIdx, intensityVal, fraction };
|
|
1389
|
+
})
|
|
1390
|
+
.filter(Boolean);
|
|
1391
|
+
if (sortByIntensity) {
|
|
1392
|
+
items.sort((a, b) => b.fraction - a.fraction);
|
|
1393
|
+
}
|
|
1394
|
+
const toggleExpand = (idx) => {
|
|
1395
|
+
setExpandedIdx(prev => {
|
|
1396
|
+
const next = new Set(prev);
|
|
1397
|
+
if (next.has(idx))
|
|
1398
|
+
next.delete(idx);
|
|
1399
|
+
else
|
|
1400
|
+
next.add(idx);
|
|
1401
|
+
return next;
|
|
1402
|
+
});
|
|
1403
|
+
};
|
|
1404
|
+
const DESC_LIMIT = Infinity; // Show full text — no truncation
|
|
1405
|
+
return (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: '2px' }, children: items.map((item, displayIdx) => {
|
|
1406
|
+
const { obj, origIdx, intensityVal, fraction } = item;
|
|
1407
|
+
const title = getField(obj, titleField);
|
|
1408
|
+
const subtitle = subtitleField ? getField(obj, subtitleField) : '';
|
|
1409
|
+
const desc = getField(obj, descField);
|
|
1410
|
+
const isExpanded = expandedIdx.has(displayIdx);
|
|
1411
|
+
const needsTruncation = desc.length > DESC_LIMIT;
|
|
1412
|
+
const barColor = getBarColor(fraction);
|
|
1413
|
+
const barTextColor = getBarTextColor(fraction);
|
|
1414
|
+
const barWidth = Math.max(fraction * 100, 8); // minimum 8% for visibility
|
|
1415
|
+
return (_jsxs("div", { style: {
|
|
1416
|
+
backgroundColor: 'var(--color-surface, #ffffff)',
|
|
1417
|
+
borderLeft: `3px solid ${barColor}`,
|
|
1418
|
+
padding: '0',
|
|
1419
|
+
borderRadius: '0 var(--radius-sm, 4px) var(--radius-sm, 4px) 0',
|
|
1420
|
+
}, children: [_jsxs("div", { style: {
|
|
1421
|
+
display: 'grid',
|
|
1422
|
+
gridTemplateColumns: '1fr auto minmax(80px, 160px)',
|
|
1423
|
+
gap: 'var(--space-sm, 0.5rem)',
|
|
1424
|
+
alignItems: 'center',
|
|
1425
|
+
padding: 'var(--space-xs, 0.375rem) var(--space-md, 1rem)',
|
|
1426
|
+
cursor: desc ? 'pointer' : 'default',
|
|
1427
|
+
}, onClick: () => desc && toggleExpand(displayIdx), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'baseline', gap: 'var(--space-sm, 0.5rem)', flexWrap: 'wrap', minWidth: 0 }, children: [_jsx("span", { style: {
|
|
1428
|
+
fontWeight: 600,
|
|
1429
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
1430
|
+
color: 'var(--color-text, #1a1d23)',
|
|
1431
|
+
lineHeight: 'var(--leading-snug, 1.4)',
|
|
1432
|
+
}, children: title }), subtitle && (_jsx("span", { style: {
|
|
1433
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1434
|
+
fontWeight: 500,
|
|
1435
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
1436
|
+
padding: '1px 8px',
|
|
1437
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
1438
|
+
backgroundColor: 'var(--color-surface-alt, #f8f9fa)',
|
|
1439
|
+
whiteSpace: 'nowrap',
|
|
1440
|
+
}, children: subtitle })), captureMode && onCapture && (_jsx("button", { title: "Capture this item", onClick: e => {
|
|
1441
|
+
e.stopPropagation();
|
|
1442
|
+
onCapture({
|
|
1443
|
+
source_view_key: captureViewKey || '',
|
|
1444
|
+
source_item_index: origIdx,
|
|
1445
|
+
source_renderer_type: 'intensity_matrix',
|
|
1446
|
+
content_type: 'item',
|
|
1447
|
+
selected_text: `${title} [${intensityVal}]: ${desc}`.slice(0, 500),
|
|
1448
|
+
structured_data: obj,
|
|
1449
|
+
context_title: parentSectionKey
|
|
1450
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > ${title}`
|
|
1451
|
+
: `${captureViewKey || 'Analysis'} > ${title}`,
|
|
1452
|
+
source_type: (captureSourceType || 'analysis'),
|
|
1453
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
1454
|
+
depth_level: parentSectionKey ? 'L2_element' : 'L1_section',
|
|
1455
|
+
parent_context: parentSectionKey ? {
|
|
1456
|
+
section_key: parentSectionKey,
|
|
1457
|
+
section_title: parentSectionTitle || '',
|
|
1458
|
+
} : undefined,
|
|
1459
|
+
});
|
|
1460
|
+
}, style: {
|
|
1461
|
+
background: 'none',
|
|
1462
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
1463
|
+
borderRadius: '4px',
|
|
1464
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
1465
|
+
cursor: 'pointer',
|
|
1466
|
+
padding: '2px 6px',
|
|
1467
|
+
fontSize: '0.7rem',
|
|
1468
|
+
lineHeight: 1,
|
|
1469
|
+
}, children: "\uD83D\uDCCC" }))] }), _jsx("span", { style: {
|
|
1470
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1471
|
+
fontWeight: 700,
|
|
1472
|
+
color: barTextColor,
|
|
1473
|
+
textTransform: 'uppercase',
|
|
1474
|
+
letterSpacing: '0.04em',
|
|
1475
|
+
whiteSpace: 'nowrap',
|
|
1476
|
+
}, children: intensityVal.replace(/_/g, ' ') }), _jsx("div", { style: {
|
|
1477
|
+
height: '8px',
|
|
1478
|
+
backgroundColor: 'var(--color-surface-alt, #f0f1f3)',
|
|
1479
|
+
borderRadius: '4px',
|
|
1480
|
+
overflow: 'hidden',
|
|
1481
|
+
}, children: _jsx("div", { style: {
|
|
1482
|
+
width: `${barWidth}%`,
|
|
1483
|
+
height: '100%',
|
|
1484
|
+
backgroundColor: barColor,
|
|
1485
|
+
borderRadius: '4px',
|
|
1486
|
+
transition: 'width 300ms ease',
|
|
1487
|
+
} }) })] }), desc && isExpanded && (_jsx("div", { style: {
|
|
1488
|
+
padding: '0 var(--space-md, 1rem) var(--space-sm, 0.5rem)',
|
|
1489
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1490
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
1491
|
+
lineHeight: 'var(--leading-relaxed, 1.65)',
|
|
1492
|
+
borderTop: '1px solid var(--color-border-light, #eef0f2)',
|
|
1493
|
+
marginTop: '2px',
|
|
1494
|
+
paddingTop: 'var(--space-xs, 0.375rem)',
|
|
1495
|
+
}, children: desc })), desc && !isExpanded && needsTruncation && (_jsxs("div", { style: {
|
|
1496
|
+
padding: '0 var(--space-md, 1rem) var(--space-xs, 0.25rem)',
|
|
1497
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1498
|
+
color: 'var(--color-text-faint, #9ca3af)',
|
|
1499
|
+
}, children: [desc.slice(0, DESC_LIMIT), "..."] }))] }, origIdx));
|
|
1500
|
+
}) }));
|
|
1501
|
+
}
|
|
1502
|
+
// ── MoveRepertoire → Grouped Intellectual Gestures ──────────
|
|
1503
|
+
//
|
|
1504
|
+
// Items grouped by a category field. Each group has a colored header with
|
|
1505
|
+
// count badge. Items within groups are compact rows. Groups are collapsible.
|
|
1506
|
+
//
|
|
1507
|
+
// Config:
|
|
1508
|
+
// title_field — item title
|
|
1509
|
+
// group_field — field to group by
|
|
1510
|
+
// description_field — item description
|
|
1511
|
+
// badge_field — optional extra badge per item
|
|
1512
|
+
// collapse_groups — start collapsed (default false)
|
|
1513
|
+
function MoveRepertoire({ data, config }) {
|
|
1514
|
+
const { tokens } = useDesignTokens();
|
|
1515
|
+
const [collapsedGroups, setCollapsedGroups] = React.useState(new Set());
|
|
1516
|
+
const [expandedDescs, setExpandedDescs] = React.useState(new Set());
|
|
1517
|
+
const [initialized, setInitialized] = React.useState(false);
|
|
1518
|
+
const captureMode = config._captureMode;
|
|
1519
|
+
const onCapture = config._onCapture;
|
|
1520
|
+
const captureJobId = config._captureJobId;
|
|
1521
|
+
const captureViewKey = config._captureViewKey;
|
|
1522
|
+
const captureSourceType = config._captureSourceType;
|
|
1523
|
+
const captureEntityId = config._captureEntityId;
|
|
1524
|
+
const parentSectionKey = config._parentSectionKey;
|
|
1525
|
+
const parentSectionTitle = config._parentSectionTitle;
|
|
1526
|
+
if (!data || !Array.isArray(data) || data.length === 0)
|
|
1527
|
+
return null;
|
|
1528
|
+
const titleField = config.title_field || 'name';
|
|
1529
|
+
const groupField = config.group_field || 'type';
|
|
1530
|
+
const descField = config.description_field || 'description';
|
|
1531
|
+
const badgeField = config.badge_field || undefined;
|
|
1532
|
+
const startCollapsed = config.collapse_groups === true;
|
|
1533
|
+
// Group items
|
|
1534
|
+
const groups = new Map();
|
|
1535
|
+
data.forEach((item, idx) => {
|
|
1536
|
+
if (typeof item !== 'object' || item === null)
|
|
1537
|
+
return;
|
|
1538
|
+
const obj = item;
|
|
1539
|
+
const groupVal = getField(obj, groupField) || 'Other';
|
|
1540
|
+
if (!groups.has(groupVal))
|
|
1541
|
+
groups.set(groupVal, []);
|
|
1542
|
+
groups.get(groupVal).push({ obj, origIdx: idx });
|
|
1543
|
+
});
|
|
1544
|
+
// Initialize collapsed state on first render with data
|
|
1545
|
+
if (!initialized && startCollapsed && groups.size > 0) {
|
|
1546
|
+
setCollapsedGroups(new Set(groups.keys()));
|
|
1547
|
+
setInitialized(true);
|
|
1548
|
+
}
|
|
1549
|
+
const toggleGroup = (group) => {
|
|
1550
|
+
setCollapsedGroups(prev => {
|
|
1551
|
+
const next = new Set(prev);
|
|
1552
|
+
if (next.has(group))
|
|
1553
|
+
next.delete(group);
|
|
1554
|
+
else
|
|
1555
|
+
next.add(group);
|
|
1556
|
+
return next;
|
|
1557
|
+
});
|
|
1558
|
+
};
|
|
1559
|
+
const toggleDesc = (key) => {
|
|
1560
|
+
setExpandedDescs(prev => {
|
|
1561
|
+
const next = new Set(prev);
|
|
1562
|
+
if (next.has(key))
|
|
1563
|
+
next.delete(key);
|
|
1564
|
+
else
|
|
1565
|
+
next.add(key);
|
|
1566
|
+
return next;
|
|
1567
|
+
});
|
|
1568
|
+
};
|
|
1569
|
+
const palette = tokens.primitives.series_palette;
|
|
1570
|
+
const DESC_LIMIT = Infinity; // Show full text — no truncation
|
|
1571
|
+
const groupEntries = Array.from(groups.entries());
|
|
1572
|
+
return (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--space-md, 1rem)' }, children: groupEntries.map(([groupName, items], groupIdx) => {
|
|
1573
|
+
const isCollapsed = collapsedGroups.has(groupName);
|
|
1574
|
+
const groupColor = palette[groupIdx % palette.length];
|
|
1575
|
+
return (_jsxs("div", { style: {
|
|
1576
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
1577
|
+
border: '1px solid var(--color-border, #e2e5e9)',
|
|
1578
|
+
overflow: 'hidden',
|
|
1579
|
+
}, children: [_jsxs("div", { onClick: () => toggleGroup(groupName), style: {
|
|
1580
|
+
display: 'flex',
|
|
1581
|
+
alignItems: 'center',
|
|
1582
|
+
gap: 'var(--space-sm, 0.5rem)',
|
|
1583
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
1584
|
+
backgroundColor: groupColor,
|
|
1585
|
+
color: tokens.surfaces.text_on_accent,
|
|
1586
|
+
cursor: 'pointer',
|
|
1587
|
+
userSelect: 'none',
|
|
1588
|
+
}, children: [_jsx("span", { style: {
|
|
1589
|
+
fontSize: '11px',
|
|
1590
|
+
transition: 'transform 150ms ease',
|
|
1591
|
+
transform: isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)',
|
|
1592
|
+
display: 'inline-block',
|
|
1593
|
+
}, children: "\u25BE" }), _jsx("span", { style: {
|
|
1594
|
+
fontWeight: 600,
|
|
1595
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
1596
|
+
textTransform: 'capitalize',
|
|
1597
|
+
}, children: groupName.replace(/_/g, ' ') }), _jsx("span", { style: {
|
|
1598
|
+
marginLeft: 'auto',
|
|
1599
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1600
|
+
fontWeight: 700,
|
|
1601
|
+
backgroundColor: 'rgba(255,255,255,0.25)',
|
|
1602
|
+
padding: '1px 8px',
|
|
1603
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
1604
|
+
}, children: items.length })] }), !isCollapsed && (_jsx("div", { style: { display: 'flex', flexDirection: 'column' }, children: items.map(({ obj, origIdx }, itemIdx) => {
|
|
1605
|
+
const title = getField(obj, titleField);
|
|
1606
|
+
const desc = getField(obj, descField);
|
|
1607
|
+
const badge = badgeField ? getField(obj, badgeField) : '';
|
|
1608
|
+
const descKey = `${groupName}-${itemIdx}`;
|
|
1609
|
+
const isDescExpanded = expandedDescs.has(descKey);
|
|
1610
|
+
const needsTruncation = desc.length > DESC_LIMIT;
|
|
1611
|
+
return (_jsxs("div", { style: {
|
|
1612
|
+
padding: 'var(--space-xs, 0.375rem) var(--space-md, 1rem)',
|
|
1613
|
+
borderBottom: itemIdx < items.length - 1 ? '1px solid var(--color-border-light, #eef0f2)' : 'none',
|
|
1614
|
+
backgroundColor: itemIdx % 2 === 0 ? 'var(--color-surface, #ffffff)' : 'var(--color-surface-alt, #fafbfc)',
|
|
1615
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'baseline', gap: 'var(--space-sm, 0.5rem)', flexWrap: 'wrap' }, children: [_jsx("span", { style: {
|
|
1616
|
+
fontWeight: 600,
|
|
1617
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
1618
|
+
color: 'var(--color-text, #1a1d23)',
|
|
1619
|
+
lineHeight: 'var(--leading-snug, 1.4)',
|
|
1620
|
+
}, children: title }), badge && (_jsx("span", { style: {
|
|
1621
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1622
|
+
fontWeight: 500,
|
|
1623
|
+
color: groupColor,
|
|
1624
|
+
padding: '1px 8px',
|
|
1625
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
1626
|
+
backgroundColor: 'var(--color-surface-alt, #f8f9fa)',
|
|
1627
|
+
}, children: badge })), captureMode && onCapture && (_jsx("button", { title: "Capture this item", onClick: e => {
|
|
1628
|
+
e.stopPropagation();
|
|
1629
|
+
onCapture({
|
|
1630
|
+
source_view_key: captureViewKey || '',
|
|
1631
|
+
source_item_index: origIdx,
|
|
1632
|
+
source_renderer_type: 'move_repertoire',
|
|
1633
|
+
content_type: 'item',
|
|
1634
|
+
selected_text: `[${groupName}] ${title}: ${desc}`.slice(0, 500),
|
|
1635
|
+
structured_data: obj,
|
|
1636
|
+
context_title: parentSectionKey
|
|
1637
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > ${groupName} > ${title}`
|
|
1638
|
+
: `${captureViewKey || 'Analysis'} > ${groupName} > ${title}`,
|
|
1639
|
+
source_type: (captureSourceType || 'analysis'),
|
|
1640
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
1641
|
+
depth_level: 'L2_element',
|
|
1642
|
+
parent_context: parentSectionKey ? {
|
|
1643
|
+
section_key: parentSectionKey,
|
|
1644
|
+
section_title: parentSectionTitle || '',
|
|
1645
|
+
} : undefined,
|
|
1646
|
+
});
|
|
1647
|
+
}, style: {
|
|
1648
|
+
background: 'none',
|
|
1649
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
1650
|
+
borderRadius: '4px',
|
|
1651
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
1652
|
+
cursor: 'pointer',
|
|
1653
|
+
padding: '2px 6px',
|
|
1654
|
+
fontSize: '0.7rem',
|
|
1655
|
+
lineHeight: 1,
|
|
1656
|
+
marginLeft: 'auto',
|
|
1657
|
+
}, children: "\uD83D\uDCCC" }))] }), desc && (_jsx("div", { style: {
|
|
1658
|
+
marginTop: '2px',
|
|
1659
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1660
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
1661
|
+
lineHeight: 'var(--leading-relaxed, 1.6)',
|
|
1662
|
+
cursor: needsTruncation ? 'pointer' : 'default',
|
|
1663
|
+
}, onClick: () => needsTruncation && toggleDesc(descKey), children: needsTruncation && !isDescExpanded
|
|
1664
|
+
? desc.slice(0, DESC_LIMIT) + '...'
|
|
1665
|
+
: desc }))] }, itemIdx));
|
|
1666
|
+
}) }))] }, groupName));
|
|
1667
|
+
}) }));
|
|
1668
|
+
}
|
|
1669
|
+
// ── DialecticalPair → Tension/Contrast Visualization ────────
|
|
1670
|
+
//
|
|
1671
|
+
// Two panels with a central tension indicator. Left = thesis/foregrounded,
|
|
1672
|
+
// right = antithesis/suppressed. Central node shows relationship type.
|
|
1673
|
+
//
|
|
1674
|
+
// Config:
|
|
1675
|
+
// left_key, right_key — field names or sub-section keys for left/right
|
|
1676
|
+
// left_label, right_label — panel headers
|
|
1677
|
+
// relationship_label — central node text (default "vs")
|
|
1678
|
+
// left_title_field, right_title_field — title fields within items
|
|
1679
|
+
// left_description_field, right_description_field — description fields
|
|
1680
|
+
function DialecticalPair({ data, config }) {
|
|
1681
|
+
const { tokens } = useDesignTokens();
|
|
1682
|
+
const [expandedLeft, setExpandedLeft] = React.useState(new Set());
|
|
1683
|
+
const [expandedRight, setExpandedRight] = React.useState(new Set());
|
|
1684
|
+
const captureMode = config._captureMode;
|
|
1685
|
+
const onCapture = config._onCapture;
|
|
1686
|
+
const captureJobId = config._captureJobId;
|
|
1687
|
+
const captureViewKey = config._captureViewKey;
|
|
1688
|
+
const captureSourceType = config._captureSourceType;
|
|
1689
|
+
const captureEntityId = config._captureEntityId;
|
|
1690
|
+
const parentSectionKey = config._parentSectionKey;
|
|
1691
|
+
const parentSectionTitle = config._parentSectionTitle;
|
|
1692
|
+
const leftKey = config.left_key || 'left';
|
|
1693
|
+
const rightKey = config.right_key || 'right';
|
|
1694
|
+
const leftLabel = config.left_label || leftKey.replace(/_/g, ' ');
|
|
1695
|
+
const rightLabel = config.right_label || rightKey.replace(/_/g, ' ');
|
|
1696
|
+
const relationshipLabel = config.relationship_label || 'vs';
|
|
1697
|
+
const leftTitleField = config.left_title_field || undefined;
|
|
1698
|
+
const rightTitleField = config.right_title_field || undefined;
|
|
1699
|
+
const leftDescField = config.left_description_field || undefined;
|
|
1700
|
+
const rightDescField = config.right_description_field || undefined;
|
|
1701
|
+
// Resolve data shape: could be {left_key: [...], right_key: [...]} or [{left_field, right_field}, ...]
|
|
1702
|
+
let leftItems = [];
|
|
1703
|
+
let rightItems = [];
|
|
1704
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
1705
|
+
// Object with left/right sub-arrays
|
|
1706
|
+
const obj = data;
|
|
1707
|
+
const rawLeft = obj[leftKey];
|
|
1708
|
+
const rawRight = obj[rightKey];
|
|
1709
|
+
if (Array.isArray(rawLeft))
|
|
1710
|
+
leftItems = rawLeft.filter(x => typeof x === 'object' && x !== null);
|
|
1711
|
+
if (Array.isArray(rawRight))
|
|
1712
|
+
rightItems = rawRight.filter(x => typeof x === 'object' && x !== null);
|
|
1713
|
+
}
|
|
1714
|
+
else if (Array.isArray(data)) {
|
|
1715
|
+
// Array of paired objects — split using left_key/right_key as field names
|
|
1716
|
+
data.forEach(item => {
|
|
1717
|
+
if (typeof item !== 'object' || item === null)
|
|
1718
|
+
return;
|
|
1719
|
+
const obj = item;
|
|
1720
|
+
// If fields exist, treat as paired data
|
|
1721
|
+
const leftVal = obj[leftKey];
|
|
1722
|
+
const rightVal = obj[rightKey];
|
|
1723
|
+
if (leftVal !== undefined || rightVal !== undefined) {
|
|
1724
|
+
leftItems.push({ text: leftVal, ...obj });
|
|
1725
|
+
rightItems.push({ text: rightVal, ...obj });
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
if (leftItems.length === 0 && rightItems.length === 0)
|
|
1730
|
+
return null;
|
|
1731
|
+
const palette = tokens.primitives.series_palette;
|
|
1732
|
+
const leftColor = palette[0];
|
|
1733
|
+
const rightColor = palette[1];
|
|
1734
|
+
const DESC_LIMIT = Infinity; // Show full text — no truncation
|
|
1735
|
+
const autoFindTitle = (obj, explicitField) => {
|
|
1736
|
+
if (explicitField && obj[explicitField])
|
|
1737
|
+
return String(obj[explicitField]);
|
|
1738
|
+
for (const k of TITLE_HINTS) {
|
|
1739
|
+
if (obj[k] && typeof obj[k] === 'string')
|
|
1740
|
+
return String(obj[k]);
|
|
1741
|
+
}
|
|
1742
|
+
// Fallback: first short string
|
|
1743
|
+
const firstShort = Object.entries(obj).find(([, v]) => typeof v === 'string' && v.length < 80);
|
|
1744
|
+
return firstShort ? String(firstShort[1]) : '';
|
|
1745
|
+
};
|
|
1746
|
+
const autoFindDesc = (obj, explicitField) => {
|
|
1747
|
+
if (explicitField && obj[explicitField])
|
|
1748
|
+
return String(obj[explicitField]);
|
|
1749
|
+
for (const k of DESC_HINTS) {
|
|
1750
|
+
if (obj[k] && typeof obj[k] === 'string')
|
|
1751
|
+
return String(obj[k]);
|
|
1752
|
+
}
|
|
1753
|
+
// Fallback: longest string
|
|
1754
|
+
let longest = '';
|
|
1755
|
+
Object.values(obj).forEach(v => {
|
|
1756
|
+
if (typeof v === 'string' && v.length > longest.length)
|
|
1757
|
+
longest = v;
|
|
1758
|
+
});
|
|
1759
|
+
return longest;
|
|
1760
|
+
};
|
|
1761
|
+
const toggleLeft = (idx) => {
|
|
1762
|
+
setExpandedLeft(prev => {
|
|
1763
|
+
const next = new Set(prev);
|
|
1764
|
+
if (next.has(idx))
|
|
1765
|
+
next.delete(idx);
|
|
1766
|
+
else
|
|
1767
|
+
next.add(idx);
|
|
1768
|
+
return next;
|
|
1769
|
+
});
|
|
1770
|
+
};
|
|
1771
|
+
const toggleRight = (idx) => {
|
|
1772
|
+
setExpandedRight(prev => {
|
|
1773
|
+
const next = new Set(prev);
|
|
1774
|
+
if (next.has(idx))
|
|
1775
|
+
next.delete(idx);
|
|
1776
|
+
else
|
|
1777
|
+
next.add(idx);
|
|
1778
|
+
return next;
|
|
1779
|
+
});
|
|
1780
|
+
};
|
|
1781
|
+
const renderPanel = (items, side, color, titleFieldOverride, descFieldOverride, expandedSet, toggleFn) => (_jsx("div", { style: {
|
|
1782
|
+
flex: 1,
|
|
1783
|
+
display: 'flex',
|
|
1784
|
+
flexDirection: 'column',
|
|
1785
|
+
maxHeight: '400px',
|
|
1786
|
+
overflowY: 'auto',
|
|
1787
|
+
}, children: items.map((obj, idx) => {
|
|
1788
|
+
const title = autoFindTitle(obj, titleFieldOverride);
|
|
1789
|
+
const desc = autoFindDesc(obj, descFieldOverride);
|
|
1790
|
+
const isExpanded = expandedSet.has(idx);
|
|
1791
|
+
const needsTruncation = desc.length > DESC_LIMIT;
|
|
1792
|
+
return (_jsxs("div", { style: {
|
|
1793
|
+
padding: 'var(--space-xs, 0.375rem) var(--space-sm, 0.5rem)',
|
|
1794
|
+
borderBottom: idx < items.length - 1 ? '1px solid var(--color-border-light, #eef0f2)' : 'none',
|
|
1795
|
+
cursor: needsTruncation ? 'pointer' : 'default',
|
|
1796
|
+
}, onClick: () => needsTruncation && toggleFn(idx), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'baseline', gap: 'var(--space-xs, 0.25rem)' }, children: [_jsx("span", { style: {
|
|
1797
|
+
fontWeight: 600,
|
|
1798
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1799
|
+
color: 'var(--color-text, #1a1d23)',
|
|
1800
|
+
lineHeight: 'var(--leading-snug, 1.4)',
|
|
1801
|
+
}, children: title }), captureMode && onCapture && (_jsx("button", { title: `Capture ${side} item`, onClick: e => {
|
|
1802
|
+
e.stopPropagation();
|
|
1803
|
+
onCapture({
|
|
1804
|
+
source_view_key: captureViewKey || '',
|
|
1805
|
+
source_item_index: idx,
|
|
1806
|
+
source_renderer_type: 'dialectical_pair',
|
|
1807
|
+
content_type: 'item',
|
|
1808
|
+
selected_text: `[${side === 'left' ? leftLabel : rightLabel}] ${title}: ${desc}`.slice(0, 500),
|
|
1809
|
+
structured_data: obj,
|
|
1810
|
+
context_title: parentSectionKey
|
|
1811
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > ${side === 'left' ? leftLabel : rightLabel} > ${title}`
|
|
1812
|
+
: `${captureViewKey || 'Analysis'} > ${side === 'left' ? leftLabel : rightLabel} > ${title}`,
|
|
1813
|
+
source_type: (captureSourceType || 'analysis'),
|
|
1814
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
1815
|
+
depth_level: 'L2_element',
|
|
1816
|
+
parent_context: parentSectionKey ? {
|
|
1817
|
+
section_key: parentSectionKey,
|
|
1818
|
+
section_title: parentSectionTitle || '',
|
|
1819
|
+
} : undefined,
|
|
1820
|
+
});
|
|
1821
|
+
}, style: {
|
|
1822
|
+
background: 'none',
|
|
1823
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
1824
|
+
borderRadius: '4px',
|
|
1825
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
1826
|
+
cursor: 'pointer',
|
|
1827
|
+
padding: '2px 6px',
|
|
1828
|
+
fontSize: '0.7rem',
|
|
1829
|
+
lineHeight: 1,
|
|
1830
|
+
marginLeft: 'auto',
|
|
1831
|
+
flexShrink: 0,
|
|
1832
|
+
}, children: "\uD83D\uDCCC" }))] }), desc && (_jsx("div", { style: {
|
|
1833
|
+
marginTop: '2px',
|
|
1834
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1835
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
1836
|
+
lineHeight: 'var(--leading-relaxed, 1.6)',
|
|
1837
|
+
}, children: needsTruncation && !isExpanded
|
|
1838
|
+
? desc.slice(0, DESC_LIMIT) + '...'
|
|
1839
|
+
: desc }))] }, idx));
|
|
1840
|
+
}) }));
|
|
1841
|
+
return (_jsxs("div", { style: {
|
|
1842
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
1843
|
+
border: '1px solid var(--color-border, #e2e5e9)',
|
|
1844
|
+
overflow: 'hidden',
|
|
1845
|
+
}, children: [_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr auto 1fr' }, children: [_jsx("div", { style: {
|
|
1846
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
1847
|
+
backgroundColor: leftColor,
|
|
1848
|
+
color: tokens.surfaces.text_on_accent,
|
|
1849
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1850
|
+
fontWeight: 600,
|
|
1851
|
+
textTransform: 'uppercase',
|
|
1852
|
+
letterSpacing: '0.06em',
|
|
1853
|
+
textAlign: 'center',
|
|
1854
|
+
}, children: leftLabel }), _jsx("div", { style: {
|
|
1855
|
+
display: 'flex',
|
|
1856
|
+
alignItems: 'center',
|
|
1857
|
+
justifyContent: 'center',
|
|
1858
|
+
padding: '0 var(--space-sm, 0.5rem)',
|
|
1859
|
+
backgroundColor: tokens.surfaces.surface_alt,
|
|
1860
|
+
position: 'relative',
|
|
1861
|
+
}, children: _jsx("span", { style: {
|
|
1862
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
1863
|
+
fontWeight: 800,
|
|
1864
|
+
color: tokens.surfaces.text_muted,
|
|
1865
|
+
textTransform: 'uppercase',
|
|
1866
|
+
letterSpacing: '0.1em',
|
|
1867
|
+
padding: '2px 10px',
|
|
1868
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
1869
|
+
border: `2px solid ${tokens.surfaces.border_accent}`,
|
|
1870
|
+
backgroundColor: tokens.surfaces.surface_default,
|
|
1871
|
+
whiteSpace: 'nowrap',
|
|
1872
|
+
}, children: relationshipLabel }) }), _jsx("div", { style: {
|
|
1873
|
+
padding: 'var(--space-sm, 0.5rem) var(--space-md, 1rem)',
|
|
1874
|
+
backgroundColor: rightColor,
|
|
1875
|
+
color: tokens.surfaces.text_on_accent,
|
|
1876
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1877
|
+
fontWeight: 600,
|
|
1878
|
+
textTransform: 'uppercase',
|
|
1879
|
+
letterSpacing: '0.06em',
|
|
1880
|
+
textAlign: 'center',
|
|
1881
|
+
}, children: rightLabel })] }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr auto 1fr' }, children: [renderPanel(leftItems, 'left', leftColor, leftTitleField, leftDescField, expandedLeft, toggleLeft), _jsx("div", { style: {
|
|
1882
|
+
width: '3px',
|
|
1883
|
+
background: `linear-gradient(to bottom, ${leftColor}, ${tokens.surfaces.border_accent}, ${rightColor})`,
|
|
1884
|
+
} }), renderPanel(rightItems, 'right', rightColor, rightTitleField, rightDescField, expandedRight, toggleRight)] })] }));
|
|
1885
|
+
}
|
|
1886
|
+
// ── OrderedFlow → Sequential Content Units ───────────────
|
|
1887
|
+
//
|
|
1888
|
+
// Generic renderer for any ordered sequence of content units:
|
|
1889
|
+
// chapters, argument steps, methodology phases, dialectical moves,
|
|
1890
|
+
// policy stages, evidence links, etc.
|
|
1891
|
+
//
|
|
1892
|
+
// Config:
|
|
1893
|
+
// title_field — primary label (auto-strips "Chapter N:" / "Step N:" prefixes)
|
|
1894
|
+
// subtitle_field — category/role badge (colored by design token semantic lookup)
|
|
1895
|
+
// description_field — expandable detail text
|
|
1896
|
+
// number_field — explicit numbering field (falls back to extracting from title or index)
|
|
1897
|
+
// strip_prefix — regex to strip from title (default: /^(Chapter|Step|Phase|Stage)\s+\d+[.:]\s*/i)
|
|
1898
|
+
function OrderedFlow({ data, config }) {
|
|
1899
|
+
const [expandedIdx, setExpandedIdx] = React.useState(null);
|
|
1900
|
+
const { tokens, getCategoryColor } = useDesignTokens();
|
|
1901
|
+
if (!data || !Array.isArray(data) || data.length === 0)
|
|
1902
|
+
return null;
|
|
1903
|
+
const titleField = config.title_field || 'title';
|
|
1904
|
+
const subtitleField = config.subtitle_field || 'type';
|
|
1905
|
+
const descriptionField = config.description_field || 'description';
|
|
1906
|
+
const numberField = config.number_field || undefined;
|
|
1907
|
+
const stripPrefix = config.strip_prefix;
|
|
1908
|
+
const prefixRegex = stripPrefix
|
|
1909
|
+
? new RegExp(stripPrefix, 'i')
|
|
1910
|
+
: /^(Chapter|Step|Phase|Stage|Part|Section|Move)\s+\d+[.:]\s*/i;
|
|
1911
|
+
const lineColor = tokens.surfaces?.border_default || '#e2e5e9';
|
|
1912
|
+
return (_jsxs("div", { style: { position: 'relative', paddingLeft: '36px' }, children: [_jsx("div", { style: {
|
|
1913
|
+
position: 'absolute',
|
|
1914
|
+
left: '11px',
|
|
1915
|
+
top: '12px',
|
|
1916
|
+
bottom: '12px',
|
|
1917
|
+
width: '2px',
|
|
1918
|
+
backgroundColor: lineColor,
|
|
1919
|
+
} }), data.map((item, idx) => {
|
|
1920
|
+
if (typeof item !== 'object' || item === null)
|
|
1921
|
+
return null;
|
|
1922
|
+
const obj = item;
|
|
1923
|
+
const title = getField(obj, titleField);
|
|
1924
|
+
const category = getField(obj, subtitleField);
|
|
1925
|
+
const categoryLower = category.toLowerCase();
|
|
1926
|
+
const desc = getField(obj, descriptionField);
|
|
1927
|
+
const isExpanded = expandedIdx === idx;
|
|
1928
|
+
const isLast = idx === data.length - 1;
|
|
1929
|
+
// Resolve step number: explicit field → extract from title → index
|
|
1930
|
+
let stepLabel;
|
|
1931
|
+
if (numberField && obj[numberField] !== undefined) {
|
|
1932
|
+
stepLabel = String(obj[numberField]);
|
|
1933
|
+
}
|
|
1934
|
+
else {
|
|
1935
|
+
const numMatch = title.match(/(\d+)/);
|
|
1936
|
+
stepLabel = numMatch ? numMatch[1] : String(idx + 1);
|
|
1937
|
+
}
|
|
1938
|
+
// Strip common prefixes for cleaner display
|
|
1939
|
+
const cleanTitle = title.replace(prefixRegex, '');
|
|
1940
|
+
// Color from design tokens: try categorical lookup, then series palette fallback
|
|
1941
|
+
const catColor = getCategoryColor?.(subtitleField, categoryLower);
|
|
1942
|
+
const dotColor = catColor?.text
|
|
1943
|
+
|| tokens.primitives.series_palette[idx % tokens.primitives.series_palette.length];
|
|
1944
|
+
const DESC_LIMIT = Infinity; // Show full text — no truncation
|
|
1945
|
+
const needsTruncation = desc.length > DESC_LIMIT;
|
|
1946
|
+
const displayDesc = isExpanded || !needsTruncation
|
|
1947
|
+
? desc
|
|
1948
|
+
: desc.slice(0, DESC_LIMIT) + '...';
|
|
1949
|
+
return (_jsxs("div", { onClick: () => needsTruncation && setExpandedIdx(isExpanded ? null : idx), style: {
|
|
1950
|
+
position: 'relative',
|
|
1951
|
+
paddingBottom: isLast ? 0 : 'var(--space-xs, 0.375rem)',
|
|
1952
|
+
marginBottom: isLast ? 0 : 'var(--space-xs, 0.375rem)',
|
|
1953
|
+
cursor: needsTruncation ? 'pointer' : 'default',
|
|
1954
|
+
}, children: [_jsx("div", { style: {
|
|
1955
|
+
position: 'absolute',
|
|
1956
|
+
left: '-36px',
|
|
1957
|
+
top: '2px',
|
|
1958
|
+
width: '24px',
|
|
1959
|
+
height: '24px',
|
|
1960
|
+
borderRadius: '50%',
|
|
1961
|
+
backgroundColor: dotColor,
|
|
1962
|
+
color: '#fff',
|
|
1963
|
+
display: 'flex',
|
|
1964
|
+
alignItems: 'center',
|
|
1965
|
+
justifyContent: 'center',
|
|
1966
|
+
fontSize: '11px',
|
|
1967
|
+
fontWeight: 700,
|
|
1968
|
+
zIndex: 1,
|
|
1969
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
1970
|
+
}, children: stepLabel }), _jsxs("div", { style: {
|
|
1971
|
+
display: 'flex',
|
|
1972
|
+
alignItems: 'baseline',
|
|
1973
|
+
gap: 'var(--space-sm, 0.5rem)',
|
|
1974
|
+
flexWrap: 'wrap',
|
|
1975
|
+
lineHeight: 'var(--leading-snug, 1.4)',
|
|
1976
|
+
}, children: [_jsx("span", { style: {
|
|
1977
|
+
fontWeight: 600,
|
|
1978
|
+
fontSize: 'var(--type-body, 0.9375rem)',
|
|
1979
|
+
color: 'var(--color-text, #1a1d23)',
|
|
1980
|
+
}, children: cleanTitle }), category && (_jsx("span", { style: {
|
|
1981
|
+
fontSize: '10px',
|
|
1982
|
+
fontWeight: 600,
|
|
1983
|
+
textTransform: 'uppercase',
|
|
1984
|
+
letterSpacing: '0.06em',
|
|
1985
|
+
color: dotColor,
|
|
1986
|
+
opacity: 0.85,
|
|
1987
|
+
}, children: category }))] }), desc && (_jsx("div", { style: {
|
|
1988
|
+
marginTop: '2px',
|
|
1989
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
1990
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
1991
|
+
lineHeight: 'var(--leading-relaxed, 1.6)',
|
|
1992
|
+
}, children: displayDesc }))] }, idx));
|
|
1993
|
+
})] }));
|
|
1994
|
+
}
|
|
1995
|
+
// ── RichDescriptionList → Readable Stacked Items ──────────
|
|
1996
|
+
//
|
|
1997
|
+
// Vertically-stacked list where each item gets room to breathe.
|
|
1998
|
+
// Handles BOTH string arrays ("Label: description") and object arrays.
|
|
1999
|
+
//
|
|
2000
|
+
// Config:
|
|
2001
|
+
// title_field — For object arrays: field name for label
|
|
2002
|
+
// description_field — For object arrays: field name for description
|
|
2003
|
+
// separator — For string arrays: split label from description (default ":")
|
|
2004
|
+
// max_visible_chars — Auto-collapse longer descriptions (default 200)
|
|
2005
|
+
// badge_fields — Optional extra fields to show as badges
|
|
2006
|
+
function RichDescriptionList({ data, config }) {
|
|
2007
|
+
const [expandedIdx, setExpandedIdx] = React.useState(new Set());
|
|
2008
|
+
const { tokens } = useDesignTokens();
|
|
2009
|
+
const captureMode = config._captureMode;
|
|
2010
|
+
const onCapture = config._onCapture;
|
|
2011
|
+
const captureJobId = config._captureJobId;
|
|
2012
|
+
const captureViewKey = config._captureViewKey;
|
|
2013
|
+
const captureSourceType = config._captureSourceType;
|
|
2014
|
+
const captureEntityId = config._captureEntityId;
|
|
2015
|
+
const parentSectionKey = config._parentSectionKey;
|
|
2016
|
+
const parentSectionTitle = config._parentSectionTitle;
|
|
2017
|
+
if (!data || !Array.isArray(data) || data.length === 0)
|
|
2018
|
+
return null;
|
|
2019
|
+
const titleField = config.title_field;
|
|
2020
|
+
const descriptionField = config.description_field;
|
|
2021
|
+
const separator = config.separator || ':';
|
|
2022
|
+
const maxChars = config.max_visible_chars || Infinity; // Show full text — no truncation
|
|
2023
|
+
const badgeFields = config.badge_fields || [];
|
|
2024
|
+
const palette = tokens.primitives.series_palette;
|
|
2025
|
+
// Parse items — handle string arrays and object arrays
|
|
2026
|
+
const items = data.map(item => {
|
|
2027
|
+
if (typeof item === 'string') {
|
|
2028
|
+
// Parse "Label: description..." format
|
|
2029
|
+
const sepIdx = item.indexOf(separator);
|
|
2030
|
+
if (sepIdx > 0 && sepIdx < 60) {
|
|
2031
|
+
return {
|
|
2032
|
+
label: item.slice(0, sepIdx).trim(),
|
|
2033
|
+
description: item.slice(sepIdx + separator.length).trim(),
|
|
2034
|
+
badges: [],
|
|
2035
|
+
raw: item,
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
// No separator found — use first ~40 chars as label
|
|
2039
|
+
const firstSpace = item.indexOf(' ', 30);
|
|
2040
|
+
if (firstSpace > 0) {
|
|
2041
|
+
return { label: item.slice(0, firstSpace), description: item.slice(firstSpace + 1), badges: [], raw: item };
|
|
2042
|
+
}
|
|
2043
|
+
return { label: item, description: '', badges: [], raw: item };
|
|
2044
|
+
}
|
|
2045
|
+
if (typeof item === 'object' && item !== null) {
|
|
2046
|
+
const obj = item;
|
|
2047
|
+
const label = titleField ? getField(obj, titleField) : '';
|
|
2048
|
+
const desc = descriptionField ? getField(obj, descriptionField) : '';
|
|
2049
|
+
const badges = badgeFields.map(f => getField(obj, f)).filter(Boolean);
|
|
2050
|
+
// If no explicit fields, auto-detect
|
|
2051
|
+
if (!label && !desc) {
|
|
2052
|
+
const auto = autoDetectFields(obj);
|
|
2053
|
+
return {
|
|
2054
|
+
label: auto.title ? getField(obj, auto.title) : '',
|
|
2055
|
+
description: auto.description ? getField(obj, auto.description) : '',
|
|
2056
|
+
badges,
|
|
2057
|
+
raw: item,
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
return { label, description: desc, badges, raw: item };
|
|
2061
|
+
}
|
|
2062
|
+
return { label: String(item), description: '', badges: [], raw: item };
|
|
2063
|
+
});
|
|
2064
|
+
const toggleExpand = (idx) => {
|
|
2065
|
+
setExpandedIdx(prev => {
|
|
2066
|
+
const next = new Set(prev);
|
|
2067
|
+
if (next.has(idx))
|
|
2068
|
+
next.delete(idx);
|
|
2069
|
+
else
|
|
2070
|
+
next.add(idx);
|
|
2071
|
+
return next;
|
|
2072
|
+
});
|
|
2073
|
+
};
|
|
2074
|
+
return (_jsx("div", { style: {
|
|
2075
|
+
display: 'flex',
|
|
2076
|
+
flexDirection: 'column',
|
|
2077
|
+
gap: 0,
|
|
2078
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
2079
|
+
border: '1px solid var(--color-border, #e2e5e9)',
|
|
2080
|
+
overflow: 'hidden',
|
|
2081
|
+
}, children: items.map((item, idx) => {
|
|
2082
|
+
const color = palette[idx % palette.length];
|
|
2083
|
+
const isExpanded = expandedIdx.has(idx);
|
|
2084
|
+
const needsTruncation = item.description.length > maxChars;
|
|
2085
|
+
const isLast = idx === items.length - 1;
|
|
2086
|
+
return (_jsxs("div", { style: {
|
|
2087
|
+
display: 'flex',
|
|
2088
|
+
borderBottom: isLast ? 'none' : '1px solid var(--color-border-light, #eef0f2)',
|
|
2089
|
+
}, children: [_jsx("div", { style: {
|
|
2090
|
+
width: '4px',
|
|
2091
|
+
backgroundColor: color,
|
|
2092
|
+
flexShrink: 0,
|
|
2093
|
+
} }), _jsxs("div", { style: {
|
|
2094
|
+
flex: 1,
|
|
2095
|
+
padding: 'var(--space-sm, 0.75rem) var(--space-md, 1rem)',
|
|
2096
|
+
}, children: [_jsxs("div", { style: {
|
|
2097
|
+
display: 'flex',
|
|
2098
|
+
alignItems: 'baseline',
|
|
2099
|
+
gap: 'var(--space-xs, 0.25rem)',
|
|
2100
|
+
}, children: [_jsx("span", { style: {
|
|
2101
|
+
color,
|
|
2102
|
+
fontSize: '14px',
|
|
2103
|
+
lineHeight: 1,
|
|
2104
|
+
flexShrink: 0,
|
|
2105
|
+
}, children: "\u25CF" }), _jsx("span", { style: {
|
|
2106
|
+
fontWeight: 700,
|
|
2107
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
2108
|
+
textTransform: 'uppercase',
|
|
2109
|
+
letterSpacing: '0.04em',
|
|
2110
|
+
color: 'var(--color-text, #1a1d23)',
|
|
2111
|
+
lineHeight: 'var(--leading-snug, 1.4)',
|
|
2112
|
+
}, children: item.label || `Item ${idx + 1}` }), item.badges.map((badge, bi) => (_jsx("span", { style: {
|
|
2113
|
+
fontSize: '10px',
|
|
2114
|
+
fontWeight: 600,
|
|
2115
|
+
textTransform: 'uppercase',
|
|
2116
|
+
letterSpacing: '0.05em',
|
|
2117
|
+
padding: '1px 6px',
|
|
2118
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
2119
|
+
backgroundColor: `${palette[(idx + bi + 1) % palette.length]}22`,
|
|
2120
|
+
color: palette[(idx + bi + 1) % palette.length],
|
|
2121
|
+
}, children: badge }, bi))), _jsxs("span", { style: {
|
|
2122
|
+
marginLeft: 'auto',
|
|
2123
|
+
fontSize: '10px',
|
|
2124
|
+
fontWeight: 500,
|
|
2125
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
2126
|
+
flexShrink: 0,
|
|
2127
|
+
}, children: ["[", idx + 1, "/", items.length, "]"] }), captureMode && onCapture && (_jsx("button", { title: "Capture this item", onClick: e => {
|
|
2128
|
+
e.stopPropagation();
|
|
2129
|
+
onCapture({
|
|
2130
|
+
source_view_key: captureViewKey || '',
|
|
2131
|
+
source_item_index: idx,
|
|
2132
|
+
source_renderer_type: 'rich_description_list',
|
|
2133
|
+
content_type: 'item',
|
|
2134
|
+
selected_text: `${item.label}: ${item.description}`.slice(0, 500),
|
|
2135
|
+
structured_data: item.raw,
|
|
2136
|
+
context_title: parentSectionKey
|
|
2137
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > ${item.label}`
|
|
2138
|
+
: `${captureViewKey || 'Analysis'} > ${item.label}`,
|
|
2139
|
+
source_type: (captureSourceType || 'analysis'),
|
|
2140
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
2141
|
+
depth_level: parentSectionKey ? 'L2_element' : 'L1_section',
|
|
2142
|
+
parent_context: parentSectionKey ? {
|
|
2143
|
+
section_key: parentSectionKey,
|
|
2144
|
+
section_title: parentSectionTitle || '',
|
|
2145
|
+
} : undefined,
|
|
2146
|
+
});
|
|
2147
|
+
}, style: {
|
|
2148
|
+
background: 'none',
|
|
2149
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
2150
|
+
borderRadius: '4px',
|
|
2151
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
2152
|
+
cursor: 'pointer',
|
|
2153
|
+
padding: '2px 6px',
|
|
2154
|
+
fontSize: '0.7rem',
|
|
2155
|
+
lineHeight: 1,
|
|
2156
|
+
flexShrink: 0,
|
|
2157
|
+
}, children: "\uD83D\uDCCC" }))] }), item.description && (_jsxs("div", { style: {
|
|
2158
|
+
marginTop: 'var(--space-2xs, 0.25rem)',
|
|
2159
|
+
fontSize: 'var(--type-caption, 0.8125rem)',
|
|
2160
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
2161
|
+
lineHeight: 'var(--leading-relaxed, 1.6)',
|
|
2162
|
+
}, children: [needsTruncation && !isExpanded
|
|
2163
|
+
? item.description.slice(0, maxChars) + '...'
|
|
2164
|
+
: item.description, needsTruncation && (_jsx("button", { onClick: (e) => { e.stopPropagation(); toggleExpand(idx); }, className: "gen-show-more-link", style: {
|
|
2165
|
+
background: 'none',
|
|
2166
|
+
border: 'none',
|
|
2167
|
+
color,
|
|
2168
|
+
cursor: 'pointer',
|
|
2169
|
+
fontSize: '0.75rem',
|
|
2170
|
+
padding: '0 4px',
|
|
2171
|
+
marginLeft: '4px',
|
|
2172
|
+
fontWeight: 600,
|
|
2173
|
+
}, children: isExpanded ? 'collapse ▴' : 'expand ▾' }))] }))] })] }, idx));
|
|
2174
|
+
}) }));
|
|
2175
|
+
}
|
|
2176
|
+
// ── PhaseTimeline → Connected Phase Nodes ──────────────────
|
|
2177
|
+
//
|
|
2178
|
+
// Horizontal connected timeline with prominent phase nodes. Designed for
|
|
2179
|
+
// temporal/sequential data stored as an OBJECT with a phases array and
|
|
2180
|
+
// optional mode badge.
|
|
2181
|
+
//
|
|
2182
|
+
// Config:
|
|
2183
|
+
// phases_field — key containing the phases array (default "phases")
|
|
2184
|
+
// label_field — field within each phase for the label (default "label")
|
|
2185
|
+
// description_field — field within each phase for the description (default "description")
|
|
2186
|
+
// mode_field — optional top-level field for a mode badge (default "mode")
|
|
2187
|
+
function PhaseTimeline({ data, config }) {
|
|
2188
|
+
const [expandedIdx, setExpandedIdx] = React.useState(new Set());
|
|
2189
|
+
const { tokens } = useDesignTokens();
|
|
2190
|
+
const captureMode = config._captureMode;
|
|
2191
|
+
const onCapture = config._onCapture;
|
|
2192
|
+
const captureJobId = config._captureJobId;
|
|
2193
|
+
const captureViewKey = config._captureViewKey;
|
|
2194
|
+
const captureSourceType = config._captureSourceType;
|
|
2195
|
+
const captureEntityId = config._captureEntityId;
|
|
2196
|
+
const parentSectionKey = config._parentSectionKey;
|
|
2197
|
+
const parentSectionTitle = config._parentSectionTitle;
|
|
2198
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
2199
|
+
return null;
|
|
2200
|
+
const obj = data;
|
|
2201
|
+
const phasesField = config.phases_field || 'phases';
|
|
2202
|
+
const labelField = config.label_field || 'label';
|
|
2203
|
+
const descField = config.description_field || 'description';
|
|
2204
|
+
const modeField = config.mode_field || 'mode';
|
|
2205
|
+
const rawPhases = obj[phasesField];
|
|
2206
|
+
if (!Array.isArray(rawPhases) || rawPhases.length === 0)
|
|
2207
|
+
return null;
|
|
2208
|
+
const mode = obj[modeField];
|
|
2209
|
+
const palette = tokens.primitives.series_palette;
|
|
2210
|
+
const DESC_LIMIT = Infinity; // Show full text — no truncation
|
|
2211
|
+
const phases = rawPhases
|
|
2212
|
+
.filter(p => typeof p === 'object' && p !== null)
|
|
2213
|
+
.map(p => p);
|
|
2214
|
+
const toggleExpand = (idx) => {
|
|
2215
|
+
setExpandedIdx(prev => {
|
|
2216
|
+
const next = new Set(prev);
|
|
2217
|
+
if (next.has(idx))
|
|
2218
|
+
next.delete(idx);
|
|
2219
|
+
else
|
|
2220
|
+
next.add(idx);
|
|
2221
|
+
return next;
|
|
2222
|
+
});
|
|
2223
|
+
};
|
|
2224
|
+
return (_jsxs("div", { children: [mode && (_jsx("div", { style: {
|
|
2225
|
+
marginBottom: 'var(--space-sm, 0.75rem)',
|
|
2226
|
+
display: 'flex',
|
|
2227
|
+
justifyContent: 'center',
|
|
2228
|
+
}, children: _jsxs("span", { style: {
|
|
2229
|
+
fontSize: '10px',
|
|
2230
|
+
fontWeight: 700,
|
|
2231
|
+
textTransform: 'uppercase',
|
|
2232
|
+
letterSpacing: '0.08em',
|
|
2233
|
+
padding: '3px 12px',
|
|
2234
|
+
borderRadius: 'var(--radius-pill, 9999px)',
|
|
2235
|
+
backgroundColor: `${palette[0]}18`,
|
|
2236
|
+
color: palette[0],
|
|
2237
|
+
border: `1px solid ${palette[0]}40`,
|
|
2238
|
+
}, children: [mode, " temporality"] }) })), _jsx("div", { style: {
|
|
2239
|
+
display: 'flex',
|
|
2240
|
+
alignItems: 'flex-start',
|
|
2241
|
+
gap: 0,
|
|
2242
|
+
overflow: 'hidden',
|
|
2243
|
+
}, children: phases.map((phase, idx) => {
|
|
2244
|
+
const label = getField(phase, labelField);
|
|
2245
|
+
const desc = getField(phase, descField);
|
|
2246
|
+
const color = palette[idx % palette.length];
|
|
2247
|
+
const isExpanded = expandedIdx.has(idx);
|
|
2248
|
+
const needsTruncation = desc.length > DESC_LIMIT;
|
|
2249
|
+
const isLast = idx === phases.length - 1;
|
|
2250
|
+
return (_jsxs(React.Fragment, { children: [_jsxs("div", { style: {
|
|
2251
|
+
flex: 1,
|
|
2252
|
+
display: 'flex',
|
|
2253
|
+
flexDirection: 'column',
|
|
2254
|
+
alignItems: 'center',
|
|
2255
|
+
minWidth: 0,
|
|
2256
|
+
}, children: [_jsx("div", { style: {
|
|
2257
|
+
width: '36px',
|
|
2258
|
+
height: '36px',
|
|
2259
|
+
borderRadius: '50%',
|
|
2260
|
+
backgroundColor: color,
|
|
2261
|
+
color: '#fff',
|
|
2262
|
+
display: 'flex',
|
|
2263
|
+
alignItems: 'center',
|
|
2264
|
+
justifyContent: 'center',
|
|
2265
|
+
fontSize: '14px',
|
|
2266
|
+
fontWeight: 700,
|
|
2267
|
+
flexShrink: 0,
|
|
2268
|
+
zIndex: 1,
|
|
2269
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
2270
|
+
boxShadow: `0 0 0 3px ${color}30`,
|
|
2271
|
+
}, children: idx + 1 }), _jsx("div", { style: {
|
|
2272
|
+
marginTop: 'var(--space-xs, 0.375rem)',
|
|
2273
|
+
textAlign: 'center',
|
|
2274
|
+
padding: 'var(--space-xs, 0.375rem) var(--space-sm, 0.5rem)',
|
|
2275
|
+
borderRadius: 'var(--radius-md, 8px)',
|
|
2276
|
+
border: `1px solid ${color}40`,
|
|
2277
|
+
backgroundColor: `${color}08`,
|
|
2278
|
+
width: '100%',
|
|
2279
|
+
minHeight: '44px',
|
|
2280
|
+
display: 'flex',
|
|
2281
|
+
flexDirection: 'column',
|
|
2282
|
+
alignItems: 'center',
|
|
2283
|
+
justifyContent: 'center',
|
|
2284
|
+
}, children: _jsx("span", { style: {
|
|
2285
|
+
fontWeight: 700,
|
|
2286
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
2287
|
+
textTransform: 'uppercase',
|
|
2288
|
+
letterSpacing: '0.04em',
|
|
2289
|
+
color: 'var(--color-text, #1a1d23)',
|
|
2290
|
+
lineHeight: 'var(--leading-snug, 1.3)',
|
|
2291
|
+
}, children: label || `Phase ${idx + 1}` }) }), desc && (_jsxs("div", { onClick: () => needsTruncation && toggleExpand(idx), style: {
|
|
2292
|
+
marginTop: 'var(--space-2xs, 0.25rem)',
|
|
2293
|
+
fontSize: 'var(--type-label, 0.6875rem)',
|
|
2294
|
+
color: 'var(--color-text-muted, #6b7280)',
|
|
2295
|
+
lineHeight: 'var(--leading-relaxed, 1.6)',
|
|
2296
|
+
textAlign: 'center',
|
|
2297
|
+
padding: '0 var(--space-2xs, 0.125rem)',
|
|
2298
|
+
cursor: needsTruncation ? 'pointer' : 'default',
|
|
2299
|
+
}, children: [needsTruncation && !isExpanded
|
|
2300
|
+
? desc.slice(0, DESC_LIMIT) + '...'
|
|
2301
|
+
: desc, needsTruncation && (_jsx("button", { onClick: e => { e.stopPropagation(); toggleExpand(idx); }, style: {
|
|
2302
|
+
background: 'none',
|
|
2303
|
+
border: 'none',
|
|
2304
|
+
color,
|
|
2305
|
+
cursor: 'pointer',
|
|
2306
|
+
fontSize: '0.7rem',
|
|
2307
|
+
padding: '0 3px',
|
|
2308
|
+
marginLeft: '2px',
|
|
2309
|
+
fontWeight: 600,
|
|
2310
|
+
}, children: isExpanded ? '▴' : '▾' }))] })), captureMode && onCapture && (_jsx("button", { title: "Capture this phase", onClick: e => {
|
|
2311
|
+
e.stopPropagation();
|
|
2312
|
+
onCapture({
|
|
2313
|
+
source_view_key: captureViewKey || '',
|
|
2314
|
+
source_item_index: idx,
|
|
2315
|
+
source_renderer_type: 'phase_timeline',
|
|
2316
|
+
content_type: 'item',
|
|
2317
|
+
selected_text: `[Phase ${idx + 1}] ${label}: ${desc}`.slice(0, 500),
|
|
2318
|
+
structured_data: phase,
|
|
2319
|
+
context_title: parentSectionKey
|
|
2320
|
+
? `${captureViewKey || 'Analysis'} > ${parentSectionTitle || ''} > Phase: ${label}`
|
|
2321
|
+
: `${captureViewKey || 'Analysis'} > Phase: ${label}`,
|
|
2322
|
+
source_type: (captureSourceType || 'analysis'),
|
|
2323
|
+
entity_id: captureEntityId || captureJobId || '',
|
|
2324
|
+
depth_level: parentSectionKey ? 'L2_element' : 'L1_section',
|
|
2325
|
+
parent_context: parentSectionKey ? {
|
|
2326
|
+
section_key: parentSectionKey,
|
|
2327
|
+
section_title: parentSectionTitle || '',
|
|
2328
|
+
} : undefined,
|
|
2329
|
+
});
|
|
2330
|
+
}, style: {
|
|
2331
|
+
marginTop: 'var(--space-2xs, 0.25rem)',
|
|
2332
|
+
background: 'none',
|
|
2333
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
2334
|
+
borderRadius: '4px',
|
|
2335
|
+
color: 'var(--dt-text-faint, #94a3b8)',
|
|
2336
|
+
cursor: 'pointer',
|
|
2337
|
+
padding: '2px 6px',
|
|
2338
|
+
fontSize: '0.7rem',
|
|
2339
|
+
lineHeight: 1,
|
|
2340
|
+
}, children: "\uD83D\uDCCC" }))] }), !isLast && (_jsxs("div", { style: {
|
|
2341
|
+
display: 'flex',
|
|
2342
|
+
alignItems: 'center',
|
|
2343
|
+
paddingTop: '18px',
|
|
2344
|
+
flexShrink: 0,
|
|
2345
|
+
}, children: [_jsx("div", { style: {
|
|
2346
|
+
width: '28px',
|
|
2347
|
+
height: '2px',
|
|
2348
|
+
background: `linear-gradient(to right, ${color}, ${palette[(idx + 1) % palette.length]})`,
|
|
2349
|
+
} }), _jsx("div", { style: {
|
|
2350
|
+
width: 0,
|
|
2351
|
+
height: 0,
|
|
2352
|
+
borderTop: '4px solid transparent',
|
|
2353
|
+
borderBottom: '4px solid transparent',
|
|
2354
|
+
borderLeft: `6px solid ${palette[(idx + 1) % palette.length]}`,
|
|
2355
|
+
} })] }))] }, idx));
|
|
2356
|
+
}) })] }));
|
|
2357
|
+
}
|
|
2358
|
+
// ── Distribution Summary ────────────────────────────────
|
|
2359
|
+
/**
|
|
2360
|
+
* Visual distribution bar chart with optional dominant pattern highlight
|
|
2361
|
+
* and collapsible narrative. Configurable via JSON view definitions.
|
|
2362
|
+
*
|
|
2363
|
+
* Data shape (object):
|
|
2364
|
+
* {
|
|
2365
|
+
* distribution: Record<string, number> | Array<{ key: string; count: number; major_count?: number }>
|
|
2366
|
+
* dominant?: string
|
|
2367
|
+
* narrative?: string
|
|
2368
|
+
* }
|
|
2369
|
+
*
|
|
2370
|
+
* Field mapping config (for data that doesn't use these exact field names):
|
|
2371
|
+
* distribution_field?: string -- field name for distribution data (default: "distribution")
|
|
2372
|
+
* dominant_field?: string -- field name for dominant item (default: "dominant")
|
|
2373
|
+
* narrative_field?: string -- field name for narrative text (default: "narrative")
|
|
2374
|
+
*
|
|
2375
|
+
* Display config:
|
|
2376
|
+
* category?: string -- design token category for colors (e.g., "tactic", "relationship")
|
|
2377
|
+
* dominant_label?: string -- label above dominant value (default: "Dominant")
|
|
2378
|
+
* count_noun?: string -- noun for total count (default: "items")
|
|
2379
|
+
* type_noun?: string -- noun for type count (default: "types")
|
|
2380
|
+
* severity_value?: string -- label for severity badges (default: "major")
|
|
2381
|
+
*
|
|
2382
|
+
* Interactive mode (passed by CardGridRenderer via _ prefix):
|
|
2383
|
+
* _onFilterClick?: (key: string) => void -- makes bars clickable
|
|
2384
|
+
* _activeFilter?: string | null -- highlights active bar
|
|
2385
|
+
* _groups?: Group[] -- live groups (overrides distribution field)
|
|
2386
|
+
*/
|
|
2387
|
+
export function DistributionSummary({ data, config }) {
|
|
2388
|
+
const { getCategoryColor, getLabel } = useDesignTokens();
|
|
2389
|
+
const [narrativeExpanded, setNarrativeExpanded] = useState(false);
|
|
2390
|
+
const obj = (data && typeof data === 'object' && !Array.isArray(data))
|
|
2391
|
+
? data
|
|
2392
|
+
: null;
|
|
2393
|
+
if (!obj)
|
|
2394
|
+
return null;
|
|
2395
|
+
// Config
|
|
2396
|
+
const category = config.category;
|
|
2397
|
+
const dominantLabel = config.dominant_label || 'Dominant';
|
|
2398
|
+
const countNoun = config.count_noun || 'items';
|
|
2399
|
+
const typeNoun = config.type_noun || 'types';
|
|
2400
|
+
const severityValue = config.severity_value || 'major';
|
|
2401
|
+
// Field mapping (allows data to use different field names)
|
|
2402
|
+
const distField = config.distribution_field || 'distribution';
|
|
2403
|
+
const domField = config.dominant_field || 'dominant';
|
|
2404
|
+
const narrField = config.narrative_field || 'narrative';
|
|
2405
|
+
// Interactive mode (injected by CardGridRenderer)
|
|
2406
|
+
const onFilterClick = config._onFilterClick;
|
|
2407
|
+
const activeFilter = config._activeFilter;
|
|
2408
|
+
const liveGroups = config._groups;
|
|
2409
|
+
// Build entries — prefer live groups (interactive) over static distribution
|
|
2410
|
+
const formatName = (key) => (category ? getLabel(category, key) : null) || key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
2411
|
+
let entries = [];
|
|
2412
|
+
const fallbackColors = { bg: '#f1f5f9', text: '#64748b', border: '#e2e8f0' };
|
|
2413
|
+
if (liveGroups && liveGroups.length > 0) {
|
|
2414
|
+
entries = liveGroups.map(g => ({
|
|
2415
|
+
key: g.key,
|
|
2416
|
+
label: g.label,
|
|
2417
|
+
count: g.items.length,
|
|
2418
|
+
majorCount: g.items.filter(i => String(i.severity || '').toLowerCase() === severityValue).length,
|
|
2419
|
+
colors: (category ? getCategoryColor(category, g.key) : null) || g.style || fallbackColors,
|
|
2420
|
+
}));
|
|
2421
|
+
}
|
|
2422
|
+
else {
|
|
2423
|
+
const rawDist = obj[distField];
|
|
2424
|
+
if (rawDist && typeof rawDist === 'object' && !Array.isArray(rawDist)) {
|
|
2425
|
+
entries = Object.entries(rawDist)
|
|
2426
|
+
.map(([key, count]) => ({
|
|
2427
|
+
key, count: Number(count) || 0, majorCount: 0,
|
|
2428
|
+
label: formatName(key),
|
|
2429
|
+
colors: (category ? getCategoryColor(category, key) : null) || fallbackColors,
|
|
2430
|
+
}))
|
|
2431
|
+
.sort((a, b) => b.count - a.count);
|
|
2432
|
+
}
|
|
2433
|
+
else if (Array.isArray(rawDist)) {
|
|
2434
|
+
entries = rawDist.map(item => {
|
|
2435
|
+
const key = String(item.key || '');
|
|
2436
|
+
return {
|
|
2437
|
+
key, count: Number(item.count || 0), majorCount: Number(item.major_count || 0),
|
|
2438
|
+
label: String(item.label || '') || formatName(key),
|
|
2439
|
+
colors: (category ? getCategoryColor(category, key) : null) || fallbackColors,
|
|
2440
|
+
};
|
|
2441
|
+
}).sort((a, b) => b.count - a.count);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
if (entries.length === 0)
|
|
2445
|
+
return null;
|
|
2446
|
+
const totalCount = entries.reduce((sum, e) => sum + e.count, 0);
|
|
2447
|
+
const maxCount = Math.max(...entries.map(e => e.count));
|
|
2448
|
+
const dominant = obj[domField];
|
|
2449
|
+
const narrative = obj[narrField];
|
|
2450
|
+
const isInteractive = Boolean(onFilterClick);
|
|
2451
|
+
return (_jsxs("div", { className: "ar-dist-summary", children: [_jsxs("div", { className: "gen-summary-header", children: [dominant && (_jsxs("div", { className: "gen-summary-stat", children: [_jsx("span", { className: "gen-stat-label", children: dominantLabel }), _jsx("span", { className: "gen-stat-value", children: formatName(dominant) })] })), _jsxs("div", { className: "gen-summary-counts", children: [_jsx("span", { className: "gen-summary-count-big", children: totalCount }), _jsxs("span", { className: "gen-summary-count-label", children: [countNoun, " across ", entries.length, " ", typeNoun] })] })] }), _jsxs("div", { className: "gen-dist-bars", children: [entries.map(entry => {
|
|
2452
|
+
const pct = Math.max(8, (entry.count / maxCount) * 100);
|
|
2453
|
+
const isActive = activeFilter === entry.key;
|
|
2454
|
+
const BarTag = isInteractive ? 'button' : 'div';
|
|
2455
|
+
return (_jsxs(BarTag, { type: isInteractive ? 'button' : undefined, className: `gen-dist-bar-row ${isActive ? 'gen-dist-bar-row--active' : ''}`, onClick: isInteractive ? () => onFilterClick(entry.key) : undefined, children: [_jsx("span", { className: "gen-dist-bar-label", children: entry.label }), _jsx("span", { className: "gen-dist-bar-track", children: _jsx("span", { className: "gen-dist-bar-fill", style: {
|
|
2456
|
+
width: `${pct}%`,
|
|
2457
|
+
backgroundColor: entry.colors.text,
|
|
2458
|
+
opacity: isActive ? 1 : 0.7,
|
|
2459
|
+
} }) }), _jsx("span", { className: "gen-dist-bar-count", style: { color: entry.colors.text }, children: entry.count }), entry.majorCount > 0 && (_jsxs("span", { className: "gen-dist-bar-severity", children: [entry.majorCount, " ", severityValue] }))] }, entry.key));
|
|
2460
|
+
}), isInteractive && activeFilter && (_jsx("button", { type: "button", className: "gen-dist-bar-clear", onClick: () => onFilterClick(''), children: "Clear filter" }))] }), narrative && (_jsxs(_Fragment, { children: [_jsx("div", { className: `gen-pattern-narrative-wrap ${narrativeExpanded ? 'gen-pattern-narrative-wrap--expanded' : ''}`, children: _jsx("p", { className: "gen-pattern-narrative", children: narrative }) }), _jsx("button", { type: "button", className: "gen-narrative-toggle", onClick: () => setNarrativeExpanded(!narrativeExpanded), children: narrativeExpanded ? 'Show less' : 'Read full analysis' })] }))] }));
|
|
2461
|
+
}
|
|
2462
|
+
//# sourceMappingURL=SubRenderers.js.map
|