@diagrammo/dgmo 0.8.21 → 0.8.23
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/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +145 -93
- package/dist/editor.cjs +20 -3
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +20 -3
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +15 -2
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +15 -2
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +20843 -14937
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +426 -17
- package/dist/index.d.ts +426 -17
- package/dist/index.js +20795 -14912
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +380 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +179 -0
- package/dist/internal.d.ts +179 -0
- package/dist/internal.js +337 -0
- package/dist/internal.js.map +1 -0
- package/docs/guide/chart-cycle.md +156 -0
- package/docs/guide/chart-journey-map.md +179 -0
- package/docs/guide/chart-pyramid.md +111 -0
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/registry.json +6 -0
- package/docs/language-reference.md +177 -6
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
- package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
- package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +11 -1
- package/src/boxes-and-lines/layout.ts +309 -33
- package/src/boxes-and-lines/parser.ts +86 -10
- package/src/boxes-and-lines/renderer.ts +250 -91
- package/src/boxes-and-lines/types.ts +1 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/cli.ts +5 -35
- package/src/completion.ts +233 -41
- package/src/cycle/layout.ts +723 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +566 -0
- package/src/cycle/types.ts +98 -0
- package/src/d3.ts +107 -8
- package/src/dgmo-router.ts +82 -3
- package/src/echarts.ts +8 -5
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +17 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/state-parser.ts +5 -10
- package/src/index.ts +63 -2
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +32 -8
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/internal.ts +16 -0
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1521 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +31 -15
- package/src/mindmap/parser.ts +12 -18
- package/src/mindmap/renderer.ts +14 -13
- package/src/mindmap/text-wrap.ts +22 -12
- package/src/mindmap/types.ts +2 -2
- package/src/org/collapse.ts +81 -0
- package/src/org/parser.ts +2 -6
- package/src/org/renderer.ts +212 -4
- package/src/pyramid/parser.ts +172 -0
- package/src/pyramid/renderer.ts +684 -0
- package/src/pyramid/types.ts +28 -0
- package/src/render.ts +2 -8
- package/src/sequence/parser.ts +62 -20
- package/src/sequence/renderer.ts +146 -40
- package/src/sharing.ts +1 -0
- package/src/sitemap/layout.ts +21 -6
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1112 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/legend-layout.ts +3 -1
- package/src/utils/parsing.ts +47 -7
- package/src/utils/tag-groups.ts +46 -60
package/src/infra/renderer.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type { PaletteColors } from '../palettes';
|
|
|
9
9
|
import { mix } from '../palettes/color-utils';
|
|
10
10
|
import type { InfraTagGroup } from './types';
|
|
11
11
|
import { resolveColor } from '../colors';
|
|
12
|
+
import { renderInlineText } from '../utils/inline-markdown';
|
|
13
|
+
import { preprocessDescriptionLine } from '../utils/description-helpers';
|
|
12
14
|
import type {
|
|
13
15
|
InfraLayoutResult,
|
|
14
16
|
InfraLayoutNode,
|
|
@@ -1432,11 +1434,14 @@ function renderNodes(
|
|
|
1432
1434
|
const expanded = expandedNodeIds?.has(node.id) ?? false;
|
|
1433
1435
|
|
|
1434
1436
|
// Description subtitle — shown below label only when node is selected
|
|
1435
|
-
const
|
|
1436
|
-
expanded && node.description && !node.isEdge ?
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
const
|
|
1437
|
+
const descLines =
|
|
1438
|
+
expanded && node.description && !node.isEdge ? node.description : [];
|
|
1439
|
+
const descH = descLines.length * META_LINE_HEIGHT;
|
|
1440
|
+
for (let di = 0; di < descLines.length; di++) {
|
|
1441
|
+
const rawLine = descLines[di];
|
|
1442
|
+
const processed = preprocessDescriptionLine(rawLine);
|
|
1443
|
+
const descTruncated = truncateDesc(processed);
|
|
1444
|
+
const isTruncated = descTruncated !== processed;
|
|
1440
1445
|
const textEl = g
|
|
1441
1446
|
.append('text')
|
|
1442
1447
|
.attr('x', node.x)
|
|
@@ -1444,15 +1449,16 @@ function renderNodes(
|
|
|
1444
1449
|
'y',
|
|
1445
1450
|
y +
|
|
1446
1451
|
NODE_HEADER_HEIGHT +
|
|
1452
|
+
di * META_LINE_HEIGHT +
|
|
1447
1453
|
META_LINE_HEIGHT / 2 +
|
|
1448
1454
|
META_FONT_SIZE * 0.35
|
|
1449
1455
|
)
|
|
1450
1456
|
.attr('text-anchor', 'middle')
|
|
1451
1457
|
.attr('font-family', FONT_FAMILY)
|
|
1452
1458
|
.attr('font-size', META_FONT_SIZE)
|
|
1453
|
-
.attr('fill', mutedColor)
|
|
1454
|
-
|
|
1455
|
-
if (isTruncated) textEl.append('title').text(
|
|
1459
|
+
.attr('fill', mutedColor);
|
|
1460
|
+
renderInlineText(textEl, descTruncated, palette, META_FONT_SIZE);
|
|
1461
|
+
if (isTruncated) textEl.append('title').text(rawLine);
|
|
1456
1462
|
}
|
|
1457
1463
|
|
|
1458
1464
|
// Declared properties only shown when node is selected (expanded)
|
package/src/infra/types.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface InfraNode {
|
|
|
62
62
|
groupId: string | null;
|
|
63
63
|
tags: Record<string, string>; // tagGroup -> tagValue
|
|
64
64
|
isEdge: boolean; // true for the `edge` entry-point component
|
|
65
|
-
description?: string;
|
|
65
|
+
description?: string[];
|
|
66
66
|
lineNumber: number;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -170,7 +170,7 @@ export interface ComputedInfraNode {
|
|
|
170
170
|
};
|
|
171
171
|
properties: InfraProperty[];
|
|
172
172
|
tags: Record<string, string>;
|
|
173
|
-
description?: string;
|
|
173
|
+
description?: string[];
|
|
174
174
|
lineNumber: number;
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -186,7 +186,14 @@ export interface ComputedInfraEdge {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
export interface InfraDiagnostic {
|
|
189
|
-
type:
|
|
189
|
+
type:
|
|
190
|
+
| 'SPLIT_SUM'
|
|
191
|
+
| 'CYCLE'
|
|
192
|
+
| 'OVERLOAD'
|
|
193
|
+
| 'RATE_LIMITED'
|
|
194
|
+
| 'ORPHAN'
|
|
195
|
+
| 'SYNTAX'
|
|
196
|
+
| 'UPTIME';
|
|
190
197
|
line: number;
|
|
191
198
|
message: string;
|
|
192
199
|
}
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// @diagrammo/dgmo/internal — internal helpers for app consumers.
|
|
3
|
+
// Not part of the public API; may change between versions.
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
export { parseDataRowValues } from './chart';
|
|
7
|
+
export {
|
|
8
|
+
computeCardArchive,
|
|
9
|
+
computeCardMove,
|
|
10
|
+
isArchiveColumn,
|
|
11
|
+
} from './kanban/mutations';
|
|
12
|
+
export {
|
|
13
|
+
groupMessagesBySection,
|
|
14
|
+
buildNoteMessageMap,
|
|
15
|
+
collectNoteLineNumbers,
|
|
16
|
+
} from './sequence/renderer';
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import type { PaletteColors } from '../palettes';
|
|
2
|
+
import { mix } from '../palettes/color-utils';
|
|
3
|
+
import type {
|
|
4
|
+
ParsedJourneyMap,
|
|
5
|
+
JourneyMapPhase,
|
|
6
|
+
JourneyMapStep,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
// ============================================================
|
|
10
|
+
// Layout Types
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
export interface CurvePoint {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
score: number;
|
|
17
|
+
emotionLabel?: string;
|
|
18
|
+
stepIndex: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StepLayout {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
step: JourneyMapStep;
|
|
27
|
+
color: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PhaseLayout {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
phase: JourneyMapPhase;
|
|
36
|
+
headerColor: string;
|
|
37
|
+
stepLayouts: StepLayout[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface JourneyMapLayout {
|
|
41
|
+
phases: PhaseLayout[];
|
|
42
|
+
flatStepLayouts: StepLayout[];
|
|
43
|
+
curvePoints: CurvePoint[];
|
|
44
|
+
totalWidth: number;
|
|
45
|
+
totalHeight: number;
|
|
46
|
+
curveAreaTop: number;
|
|
47
|
+
curveAreaBottom: number;
|
|
48
|
+
cardAreaTop: number;
|
|
49
|
+
personaHeight: number;
|
|
50
|
+
titleHeight: number;
|
|
51
|
+
/** Whether any step has thought annotations */
|
|
52
|
+
hasThoughts: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================
|
|
56
|
+
// Constants
|
|
57
|
+
// ============================================================
|
|
58
|
+
|
|
59
|
+
const PADDING = 24;
|
|
60
|
+
const TITLE_HEIGHT = 36;
|
|
61
|
+
const PERSONA_HEIGHT = 48;
|
|
62
|
+
const CURVE_AREA_HEIGHT = 260;
|
|
63
|
+
const CARD_GAP = 8;
|
|
64
|
+
const STEP_CARD_WIDTH = 190;
|
|
65
|
+
const CARD_HEADER_HEIGHT = 24;
|
|
66
|
+
const CARD_META_LINE_HEIGHT = 14;
|
|
67
|
+
const PHASE_HEADER_HEIGHT = 36;
|
|
68
|
+
const CARD_PADDING_X = 10;
|
|
69
|
+
const CARD_PADDING_Y = 6;
|
|
70
|
+
const ANNO_ICON_SIZE = 10;
|
|
71
|
+
const ANNO_ICON_GAP = 4;
|
|
72
|
+
export const TAG_STRIP_HEIGHT = 18;
|
|
73
|
+
const PHASE_GAP = 16;
|
|
74
|
+
const COLUMN_PADDING = 12;
|
|
75
|
+
const FACE_ICON_SIZE = 20;
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// Score-to-color
|
|
79
|
+
// ============================================================
|
|
80
|
+
|
|
81
|
+
export function scoreToColor(score: number, palette: PaletteColors): string {
|
|
82
|
+
// 5=green, 1=red — interpolate
|
|
83
|
+
const t = ((5 - score) / 4) * 100;
|
|
84
|
+
return mix(palette.colors.red, palette.colors.green, t);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================
|
|
88
|
+
// Layout Engine
|
|
89
|
+
// ============================================================
|
|
90
|
+
|
|
91
|
+
export const COLLAPSED_PHASE_WIDTH = 60;
|
|
92
|
+
|
|
93
|
+
export function layoutJourneyMap(
|
|
94
|
+
parsed: ParsedJourneyMap,
|
|
95
|
+
palette: PaletteColors,
|
|
96
|
+
options?: {
|
|
97
|
+
exportDims?: { width: number; height: number };
|
|
98
|
+
collapsedPhases?: Set<string>;
|
|
99
|
+
}
|
|
100
|
+
): JourneyMapLayout {
|
|
101
|
+
const hasTitle = !!parsed.title;
|
|
102
|
+
const hasPersona = !!parsed.persona;
|
|
103
|
+
const hasPhases = parsed.phases.length > 0;
|
|
104
|
+
|
|
105
|
+
const titleHeight = hasTitle ? TITLE_HEIGHT : 0;
|
|
106
|
+
const personaHeight = hasPersona ? PERSONA_HEIGHT : 0;
|
|
107
|
+
|
|
108
|
+
// Thought bubbles render as overlays on hover — no reserved vertical space
|
|
109
|
+
const allStepsForThoughts = hasPhases
|
|
110
|
+
? parsed.phases.flatMap((p) => p.steps)
|
|
111
|
+
: parsed.steps;
|
|
112
|
+
const hasThoughts = allStepsForThoughts.some((s) =>
|
|
113
|
+
s.annotations.some((a) => a.type === 'thought')
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const curveAreaTop = PADDING + titleHeight + personaHeight;
|
|
117
|
+
const curveAreaBottom = curveAreaTop + CURVE_AREA_HEIGHT;
|
|
118
|
+
const cardAreaTop = curveAreaBottom + PADDING;
|
|
119
|
+
|
|
120
|
+
const allSteps = hasPhases
|
|
121
|
+
? parsed.phases.flatMap((p) => p.steps)
|
|
122
|
+
: parsed.steps;
|
|
123
|
+
|
|
124
|
+
// Compute step card heights based on content (matches kanban card sizing)
|
|
125
|
+
const annoIconIndent = ANNO_ICON_SIZE + ANNO_ICON_GAP;
|
|
126
|
+
const annoTextW = STEP_CARD_WIDTH - CARD_PADDING_X * 2 - annoIconIndent;
|
|
127
|
+
const descTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
|
|
128
|
+
const charWidth = 4.8; // average char width at FONT_SIZE_META (10px)
|
|
129
|
+
|
|
130
|
+
const titleTextWidth = STEP_CARD_WIDTH - CARD_PADDING_X * 2;
|
|
131
|
+
const titleCharWidth = 6.5; // average char width at FONT_SIZE_STEP (12px)
|
|
132
|
+
const TITLE_LINE_HEIGHT = 16;
|
|
133
|
+
|
|
134
|
+
const stepHeights = allSteps.map((step) => {
|
|
135
|
+
const titleLines = wrapLineCount(
|
|
136
|
+
step.title,
|
|
137
|
+
titleTextWidth,
|
|
138
|
+
titleCharWidth
|
|
139
|
+
);
|
|
140
|
+
let h = CARD_PADDING_Y + titleLines * TITLE_LINE_HEIGHT + CARD_PADDING_Y;
|
|
141
|
+
const cardAnnos = step.annotations;
|
|
142
|
+
let contentLines = 0;
|
|
143
|
+
// Description may wrap
|
|
144
|
+
if (step.description) {
|
|
145
|
+
contentLines += wrapLineCount(step.description, descTextWidth, charWidth);
|
|
146
|
+
}
|
|
147
|
+
// Annotations: all lines indented past icon
|
|
148
|
+
for (const anno of cardAnnos) {
|
|
149
|
+
contentLines += wrapLineCount(anno.text, annoTextW, charWidth);
|
|
150
|
+
}
|
|
151
|
+
if (contentLines > 0) {
|
|
152
|
+
h += contentLines * CARD_META_LINE_HEIGHT + 4; // 4px bottom padding
|
|
153
|
+
}
|
|
154
|
+
return h;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const minCardHeight = CARD_HEADER_HEIGHT + CARD_META_LINE_HEIGHT;
|
|
158
|
+
const maxCardHeight = Math.max(minCardHeight, ...stepHeights);
|
|
159
|
+
|
|
160
|
+
// Check if any step has tags — if so, reserve space for the tag strip above cards
|
|
161
|
+
const hasTags = allSteps.some((s) => Object.keys(s.tags).length > 0);
|
|
162
|
+
const tagStripOffset = hasTags ? TAG_STRIP_HEIGHT + 6 : 0;
|
|
163
|
+
|
|
164
|
+
// Layout phases or flat steps
|
|
165
|
+
const phaseLayouts: PhaseLayout[] = [];
|
|
166
|
+
const flatStepLayouts: StepLayout[] = [];
|
|
167
|
+
const curvePoints: CurvePoint[] = [];
|
|
168
|
+
|
|
169
|
+
let globalStepIndex = 0;
|
|
170
|
+
|
|
171
|
+
const collapsed = options?.collapsedPhases ?? new Set<string>();
|
|
172
|
+
|
|
173
|
+
if (hasPhases) {
|
|
174
|
+
let phaseX = PADDING;
|
|
175
|
+
|
|
176
|
+
for (const phase of parsed.phases) {
|
|
177
|
+
const isCollapsed = collapsed.has(phase.name);
|
|
178
|
+
const stepCount = Math.max(phase.steps.length, 1);
|
|
179
|
+
const phaseWidth = isCollapsed
|
|
180
|
+
? STEP_CARD_WIDTH + COLUMN_PADDING * 2
|
|
181
|
+
: stepCount * STEP_CARD_WIDTH +
|
|
182
|
+
(stepCount - 1) * CARD_GAP +
|
|
183
|
+
COLUMN_PADDING * 2;
|
|
184
|
+
|
|
185
|
+
const stepLayouts: StepLayout[] = [];
|
|
186
|
+
|
|
187
|
+
if (!isCollapsed) {
|
|
188
|
+
let stepX = phaseX + COLUMN_PADDING;
|
|
189
|
+
|
|
190
|
+
for (let si = 0; si < phase.steps.length; si++) {
|
|
191
|
+
const step = phase.steps[si];
|
|
192
|
+
const color =
|
|
193
|
+
step.score !== undefined
|
|
194
|
+
? scoreToColor(step.score, palette)
|
|
195
|
+
: palette.surface;
|
|
196
|
+
|
|
197
|
+
const sl: StepLayout = {
|
|
198
|
+
x: stepX,
|
|
199
|
+
y: cardAreaTop + PHASE_HEADER_HEIGHT + CARD_GAP + tagStripOffset,
|
|
200
|
+
width: STEP_CARD_WIDTH,
|
|
201
|
+
height: maxCardHeight,
|
|
202
|
+
step,
|
|
203
|
+
color,
|
|
204
|
+
};
|
|
205
|
+
stepLayouts.push(sl);
|
|
206
|
+
|
|
207
|
+
// Curve point
|
|
208
|
+
if (step.score !== undefined) {
|
|
209
|
+
const curveX = stepX + STEP_CARD_WIDTH / 2;
|
|
210
|
+
const curveY =
|
|
211
|
+
curveAreaBottom -
|
|
212
|
+
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 120) -
|
|
213
|
+
10;
|
|
214
|
+
curvePoints.push({
|
|
215
|
+
x: curveX,
|
|
216
|
+
y: curveY,
|
|
217
|
+
score: step.score,
|
|
218
|
+
emotionLabel: step.emotionLabel,
|
|
219
|
+
stepIndex: globalStepIndex,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
stepX += STEP_CARD_WIDTH + CARD_GAP;
|
|
224
|
+
globalStepIndex++;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// Collapsed: spread curve points across the compressed column width
|
|
228
|
+
const stepCount = phase.steps.length;
|
|
229
|
+
const padX = COLUMN_PADDING + FACE_ICON_SIZE;
|
|
230
|
+
const availW = phaseWidth - padX * 2;
|
|
231
|
+
for (let si = 0; si < stepCount; si++) {
|
|
232
|
+
const step = phase.steps[si];
|
|
233
|
+
if (step.score !== undefined) {
|
|
234
|
+
const curveX =
|
|
235
|
+
stepCount === 1
|
|
236
|
+
? phaseX + phaseWidth / 2
|
|
237
|
+
: phaseX + padX + (si / (stepCount - 1)) * availW;
|
|
238
|
+
const curveY =
|
|
239
|
+
curveAreaBottom -
|
|
240
|
+
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 120) -
|
|
241
|
+
10;
|
|
242
|
+
curvePoints.push({
|
|
243
|
+
x: curveX,
|
|
244
|
+
y: curveY,
|
|
245
|
+
score: step.score,
|
|
246
|
+
emotionLabel: step.emotionLabel,
|
|
247
|
+
stepIndex: globalStepIndex,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
globalStepIndex++;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Phase header color from average score
|
|
255
|
+
const scoredSteps = phase.steps.filter((s) => s.score !== undefined);
|
|
256
|
+
const avgScore =
|
|
257
|
+
scoredSteps.length > 0
|
|
258
|
+
? scoredSteps.reduce((sum, s) => sum + s.score!, 0) /
|
|
259
|
+
scoredSteps.length
|
|
260
|
+
: 3;
|
|
261
|
+
const headerColor = mix(scoreToColor(avgScore, palette), palette.bg, 25);
|
|
262
|
+
|
|
263
|
+
const COLLAPSED_CARD_H = 26;
|
|
264
|
+
const COLLAPSED_GAP = 6;
|
|
265
|
+
const phaseHeight = isCollapsed
|
|
266
|
+
? PHASE_HEADER_HEIGHT +
|
|
267
|
+
CARD_GAP +
|
|
268
|
+
phase.steps.length * (COLLAPSED_CARD_H + COLLAPSED_GAP) +
|
|
269
|
+
CARD_GAP
|
|
270
|
+
: PHASE_HEADER_HEIGHT +
|
|
271
|
+
CARD_GAP +
|
|
272
|
+
tagStripOffset +
|
|
273
|
+
maxCardHeight +
|
|
274
|
+
CARD_GAP;
|
|
275
|
+
|
|
276
|
+
phaseLayouts.push({
|
|
277
|
+
x: phaseX,
|
|
278
|
+
y: cardAreaTop,
|
|
279
|
+
width: phaseWidth,
|
|
280
|
+
height: phaseHeight,
|
|
281
|
+
phase,
|
|
282
|
+
headerColor,
|
|
283
|
+
stepLayouts,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
phaseX += phaseWidth + PHASE_GAP;
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// Flat mode
|
|
290
|
+
let stepX = PADDING;
|
|
291
|
+
|
|
292
|
+
for (let si = 0; si < parsed.steps.length; si++) {
|
|
293
|
+
const step = parsed.steps[si];
|
|
294
|
+
const color =
|
|
295
|
+
step.score !== undefined
|
|
296
|
+
? scoreToColor(step.score, palette)
|
|
297
|
+
: palette.surface;
|
|
298
|
+
|
|
299
|
+
flatStepLayouts.push({
|
|
300
|
+
x: stepX,
|
|
301
|
+
y: cardAreaTop + CARD_GAP + tagStripOffset,
|
|
302
|
+
width: STEP_CARD_WIDTH,
|
|
303
|
+
height: maxCardHeight,
|
|
304
|
+
step,
|
|
305
|
+
color,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (step.score !== undefined) {
|
|
309
|
+
const curveX = stepX + STEP_CARD_WIDTH / 2;
|
|
310
|
+
const curveY =
|
|
311
|
+
curveAreaBottom -
|
|
312
|
+
((step.score - 1) / 4) * (CURVE_AREA_HEIGHT - 20) -
|
|
313
|
+
10;
|
|
314
|
+
curvePoints.push({
|
|
315
|
+
x: curveX,
|
|
316
|
+
y: curveY,
|
|
317
|
+
score: step.score,
|
|
318
|
+
emotionLabel: step.emotionLabel,
|
|
319
|
+
stepIndex: si,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
stepX += STEP_CARD_WIDTH + CARD_GAP;
|
|
324
|
+
globalStepIndex++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Compute total dimensions
|
|
329
|
+
const rightEdge = hasPhases
|
|
330
|
+
? phaseLayouts.length > 0
|
|
331
|
+
? phaseLayouts[phaseLayouts.length - 1].x +
|
|
332
|
+
phaseLayouts[phaseLayouts.length - 1].width +
|
|
333
|
+
PADDING
|
|
334
|
+
: PADDING * 2
|
|
335
|
+
: flatStepLayouts.length > 0
|
|
336
|
+
? flatStepLayouts[flatStepLayouts.length - 1].x +
|
|
337
|
+
STEP_CARD_WIDTH +
|
|
338
|
+
PADDING
|
|
339
|
+
: PADDING * 2;
|
|
340
|
+
|
|
341
|
+
const bottomEdge = hasPhases
|
|
342
|
+
? phaseLayouts.length > 0
|
|
343
|
+
? phaseLayouts[0].y + phaseLayouts[0].height + PADDING + 40
|
|
344
|
+
: cardAreaTop + PADDING
|
|
345
|
+
: cardAreaTop + CARD_GAP + tagStripOffset + maxCardHeight + PADDING + 40;
|
|
346
|
+
|
|
347
|
+
// Add space for score legend at bottom
|
|
348
|
+
const totalWidth = Math.max(rightEdge, 400);
|
|
349
|
+
const totalHeight = bottomEdge;
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
phases: phaseLayouts,
|
|
353
|
+
flatStepLayouts,
|
|
354
|
+
curvePoints,
|
|
355
|
+
totalWidth,
|
|
356
|
+
totalHeight,
|
|
357
|
+
curveAreaTop,
|
|
358
|
+
curveAreaBottom,
|
|
359
|
+
cardAreaTop,
|
|
360
|
+
personaHeight,
|
|
361
|
+
titleHeight,
|
|
362
|
+
hasThoughts,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Count how many visual lines a text string will occupy when wrapped. */
|
|
367
|
+
function wrapLineCount(
|
|
368
|
+
text: string,
|
|
369
|
+
maxWidth: number,
|
|
370
|
+
charWidth: number
|
|
371
|
+
): number {
|
|
372
|
+
const maxChars = Math.max(1, Math.floor(maxWidth / charWidth));
|
|
373
|
+
const words = text.split(/\s+/);
|
|
374
|
+
let lines = 1;
|
|
375
|
+
let currentLen = 0;
|
|
376
|
+
for (const word of words) {
|
|
377
|
+
const needed = currentLen > 0 ? word.length + 1 : word.length;
|
|
378
|
+
if (currentLen + needed > maxChars && currentLen > 0) {
|
|
379
|
+
lines++;
|
|
380
|
+
currentLen = word.length;
|
|
381
|
+
} else {
|
|
382
|
+
currentLen += needed;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return lines;
|
|
386
|
+
}
|