@diagrammo/dgmo 0.39.0 → 0.41.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/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import { palettes, getPalette } from './palettes';
20
20
  import type { PaletteConfig } from './palettes/types';
21
21
  import type { Theme } from './themes';
22
22
  import { formatDgmoError, type DgmoError } from './diagnostics';
23
+ import { renderErrorCard } from './error-card';
23
24
 
24
25
  export type { CompactViewState } from './sharing';
25
26
 
@@ -93,41 +94,17 @@ export async function render(
93
94
  return result;
94
95
  }
95
96
 
96
- // 'svg' (default): render an inline error SVG so the user sees the problem
97
+ // 'svg' (default): render an inline error card so the author sees what
98
+ // broke, on which line, and the offending source — see error-card.ts.
99
+ // We show the card whenever there are error-severity diagnostics, even if a
100
+ // parser produced a partial SVG: a misleading half-diagram is worse than a
101
+ // clear "here's what's wrong" for an embedded host (remark/astro/obsidian).
97
102
  return {
98
- svg: result.svg || renderErrorSvg(errors, palette, options?.theme),
103
+ svg: renderErrorCard(errors, text, palette, options?.theme),
99
104
  diagnostics: result.diagnostics,
100
105
  };
101
106
  }
102
107
 
103
- function renderErrorSvg(
104
- errors: DgmoError[],
105
- palette: PaletteConfig,
106
- theme?: Theme
107
- ): string {
108
- const colors = palette[theme === 'dark' ? 'dark' : 'light'];
109
- const bg = theme === 'transparent' ? 'transparent' : colors.bg;
110
- const fg = colors.destructive ?? colors.text;
111
- const muted = colors.textMuted ?? colors.text;
112
- const lines = errors.slice(0, 5).map((e) => formatDgmoError(e));
113
- const height = 60 + lines.length * 20;
114
- const body = lines
115
- .map(
116
- (line, i) =>
117
- `<text x="20" y="${60 + i * 20}" fill="${fg}" font-family="ui-monospace, monospace" font-size="12">${escapeXml(line)}</text>`
118
- )
119
- .join('');
120
- return `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="${height}" viewBox="0 0 640 ${height}"><rect width="100%" height="100%" fill="${bg}"/><text x="20" y="34" fill="${muted}" font-family="system-ui, sans-serif" font-size="13" font-weight="600">DGMO parse error</text>${body}</svg>`;
121
- }
122
-
123
- function escapeXml(s: string): string {
124
- return s
125
- .replace(/&/g, '&amp;')
126
- .replace(/</g, '&lt;')
127
- .replace(/>/g, '&gt;')
128
- .replace(/"/g, '&quot;');
129
- }
130
-
131
108
  // ============================================================
132
109
  // validate(text)
133
110
  // ============================================================
@@ -71,6 +71,10 @@ const FONT_SIZE_META = 10;
71
71
  const GRID_LINE_OPACITY = 0.15;
72
72
  const CURVE_STROKE_WIDTH = 2.5;
73
73
  const FACE_RADIUS = 14;
74
+ // Faces grow on hover/active; the thought bubble + connector must clear the
75
+ // enlarged outer edge (radius + ring halo), not the base radius.
76
+ const FACE_HOVER_SCALE = 1.5;
77
+ const FACE_HOVER_R = (FACE_RADIUS + 1) * FACE_HOVER_SCALE;
74
78
  const DIM_HOVER = 0.25;
75
79
  const TITLE_LINE_HEIGHT = 16;
76
80
 
@@ -450,6 +454,7 @@ export function renderJourneyMap(
450
454
  y,
451
455
  score,
452
456
  palette,
457
+ isDark,
453
458
  SCORE_LABEL_R
454
459
  );
455
460
 
@@ -517,7 +522,14 @@ export function renderJourneyMap(
517
522
 
518
523
  for (const pt of layout.curvePoints) {
519
524
  const step = allSteps[pt.stepIndex];
520
- const faceG = renderScoreFace(curveG, pt.x, pt.y, pt.score, palette);
525
+ const faceG = renderScoreFace(
526
+ curveG,
527
+ pt.x,
528
+ pt.y,
529
+ pt.score,
530
+ palette,
531
+ isDark
532
+ );
521
533
  addEmotionLabel(faceG, pt, palette);
522
534
  if (step) {
523
535
  faceG.attr('data-line-number', step.lineNumber);
@@ -549,7 +561,14 @@ export function renderJourneyMap(
549
561
  ? parsed.phases.flatMap((p) => p.steps)
550
562
  : parsed.steps;
551
563
  const step = allSteps[pt.stepIndex];
552
- const faceG = renderScoreFace(curveG, pt.x, pt.y, pt.score, palette);
564
+ const faceG = renderScoreFace(
565
+ curveG,
566
+ pt.x,
567
+ pt.y,
568
+ pt.score,
569
+ palette,
570
+ isDark
571
+ );
553
572
  addEmotionLabel(faceG, pt, palette);
554
573
  if (step) {
555
574
  faceG.attr('data-line-number', step.lineNumber);
@@ -723,6 +742,7 @@ export function renderJourneyMap(
723
742
  faceCy,
724
743
  step.score,
725
744
  palette,
745
+ isDark,
726
746
  COLLAPSED_FACE_R
727
747
  );
728
748
  faceG.attr('data-line-number', step.lineNumber);
@@ -856,7 +876,7 @@ export function renderJourneyMap(
856
876
  const fcy = parseFloat(el.attr('data-cy') ?? '0');
857
877
  el.attr(
858
878
  'transform',
859
- `translate(${fcx},${fcy}) scale(1.5) translate(${-fcx},${-fcy})`
879
+ `translate(${fcx},${fcy}) scale(${FACE_HOVER_SCALE}) translate(${-fcx},${-fcy})`
860
880
  );
861
881
  } else {
862
882
  el.attr('transform', null);
@@ -917,7 +937,7 @@ export function renderJourneyMap(
917
937
  PADDING,
918
938
  Math.min(fcx - bw / 2, layout.totalWidth - PADDING - bw)
919
939
  );
920
- const by = Math.max(PADDING, fcy - FACE_RADIUS - THOUGHT_GAP - bh);
940
+ const by = Math.max(PADDING, fcy - FACE_HOVER_R - THOUGHT_GAP - bh);
921
941
 
922
942
  const scoreColor = scoreToColor(score, palette);
923
943
  const tintedBg = mix(scoreColor, palette.surface, 20);
@@ -939,7 +959,7 @@ export function renderJourneyMap(
939
959
  .attr('x1', fcx)
940
960
  .attr('y1', by + bh)
941
961
  .attr('x2', fcx)
942
- .attr('y2', fcy - FACE_RADIUS - 1)
962
+ .attr('y2', fcy - FACE_HOVER_R - 1)
943
963
  .attr('stroke', scoreColor)
944
964
  .attr('stroke-width', CARD_STROKE_WIDTH);
945
965
 
@@ -1400,6 +1420,7 @@ function renderScoreFace(
1400
1420
  cy: number,
1401
1421
  score: number,
1402
1422
  palette: PaletteColors,
1423
+ isDark: boolean,
1403
1424
  radius?: number
1404
1425
  ): d3.Selection<SVGGElement, unknown, null, undefined> {
1405
1426
  const r = radius ?? FACE_RADIUS;
@@ -1410,21 +1431,28 @@ function renderScoreFace(
1410
1431
  .attr('data-cx', cx)
1411
1432
  .attr('data-cy', cy);
1412
1433
 
1413
- // Face circle
1434
+ // Face: a solid colored ring over the canonical tinted fill (the same
1435
+ // shapeFill() tint used for unsolid shapes elsewhere), with the eyes and
1436
+ // mouth drawn in the full score color.
1437
+ const faceFill = shapeFill(palette, color, isDark);
1438
+
1439
+ // Thin bg halo so the colored ring reads crisply where it crosses the line.
1440
+ g.append('circle')
1441
+ .attr('cx', cx)
1442
+ .attr('cy', cy)
1443
+ .attr('r', r + 1)
1444
+ .attr('fill', palette.bg);
1445
+
1414
1446
  g.append('circle')
1415
1447
  .attr('cx', cx)
1416
1448
  .attr('cy', cy)
1417
1449
  .attr('r', r)
1418
- .attr('fill', color)
1419
- .attr('stroke', palette.bg)
1420
- .attr('stroke-width', 1.5);
1450
+ .attr('fill', faceFill)
1451
+ .attr('stroke', color)
1452
+ .attr('stroke-width', 2);
1421
1453
 
1422
- // Eyes
1423
- const eyeColor = contrastText(
1424
- color,
1425
- palette.textOnFillLight,
1426
- palette.textOnFillDark
1427
- );
1454
+ // Eyes & mouth share the solid score color.
1455
+ const eyeColor = color;
1428
1456
  const eyeY = cy - r * 0.15;
1429
1457
  const eyeSpacing = r * 0.32;
1430
1458
  const eyeR = r * 0.12;
@@ -1441,10 +1469,14 @@ function renderScoreFace(
1441
1469
 
1442
1470
  // Mouth — arc curvature based on score
1443
1471
  // score 5: big smile, score 3: straight, score 1: deep frown
1444
- const mouthY = cy + r * 0.25;
1445
- const mouthW = r * 0.45;
1472
+ const mouthW = r * 0.46;
1446
1473
  // Curve amount: positive = smile, negative = frown
1447
- const curve = ((score - 3) / 2) * r * 0.35;
1474
+ const curve = ((score - 3) / 2) * r * 0.42;
1475
+ // Pin the mouth's visual apex (its mid-point) to a fixed Y and let the
1476
+ // curvature swing symmetrically about it. Anchoring the corners instead made
1477
+ // the frown's arch ride up into the eyes and read as a cramped grimace.
1478
+ const apexY = cy + r * 0.32;
1479
+ const mouthY = apexY - curve / 2;
1448
1480
 
1449
1481
  g.append('path')
1450
1482
  .attr(