@diagrammo/dgmo 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.claude/commands/dgmo.md +231 -13
  2. package/AGENTS.md +148 -0
  3. package/dist/cli.cjs +341 -165
  4. package/dist/index.cjs +4900 -1685
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +259 -18
  7. package/dist/index.d.ts +259 -18
  8. package/dist/index.js +4642 -1436
  9. package/dist/index.js.map +1 -1
  10. package/package.json +5 -3
  11. package/src/c4/layout.ts +0 -5
  12. package/src/c4/parser.ts +0 -16
  13. package/src/c4/renderer.ts +7 -11
  14. package/src/class/layout.ts +0 -1
  15. package/src/class/parser.ts +28 -0
  16. package/src/class/renderer.ts +189 -34
  17. package/src/cli.ts +566 -25
  18. package/src/colors.ts +3 -3
  19. package/src/completion.ts +58 -0
  20. package/src/d3.ts +179 -122
  21. package/src/dgmo-router.ts +3 -58
  22. package/src/echarts.ts +96 -55
  23. package/src/er/parser.ts +30 -1
  24. package/src/er/renderer.ts +12 -7
  25. package/src/gantt/calculator.ts +677 -0
  26. package/src/gantt/parser.ts +761 -0
  27. package/src/gantt/renderer.ts +2125 -0
  28. package/src/gantt/resolver.ts +144 -0
  29. package/src/gantt/types.ts +168 -0
  30. package/src/graph/flowchart-parser.ts +27 -4
  31. package/src/graph/flowchart-renderer.ts +1 -2
  32. package/src/graph/state-parser.ts +0 -1
  33. package/src/graph/state-renderer.ts +1 -3
  34. package/src/index.ts +37 -0
  35. package/src/infra/compute.ts +0 -7
  36. package/src/infra/layout.ts +0 -2
  37. package/src/infra/parser.ts +46 -4
  38. package/src/infra/renderer.ts +49 -27
  39. package/src/initiative-status/filter.ts +63 -0
  40. package/src/initiative-status/layout.ts +319 -67
  41. package/src/initiative-status/parser.ts +200 -25
  42. package/src/initiative-status/renderer.ts +298 -35
  43. package/src/initiative-status/types.ts +6 -0
  44. package/src/kanban/parser.ts +0 -2
  45. package/src/org/layout.ts +22 -59
  46. package/src/org/renderer.ts +11 -36
  47. package/src/palettes/dracula.ts +60 -0
  48. package/src/palettes/index.ts +8 -6
  49. package/src/palettes/monokai.ts +60 -0
  50. package/src/palettes/registry.ts +4 -2
  51. package/src/sequence/parser.ts +14 -11
  52. package/src/sequence/renderer.ts +5 -6
  53. package/src/sequence/tag-resolution.ts +0 -1
  54. package/src/sharing.ts +8 -0
  55. package/src/sitemap/layout.ts +1 -14
  56. package/src/sitemap/parser.ts +1 -2
  57. package/src/sitemap/renderer.ts +4 -7
  58. package/src/utils/arrows.ts +7 -7
  59. package/src/utils/duration.ts +212 -0
  60. package/src/utils/export-container.ts +40 -0
  61. package/src/utils/legend-constants.ts +1 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Diagram symbol extraction API.
3
+ *
4
+ * Provides DiagramSymbols interface + extractDiagramSymbols() dispatch.
5
+ * Each diagram type registers its own extractor via registerExtractor().
6
+ * All built-in extractors are registered at module init below.
7
+ */
8
+
9
+ import { extractSymbols as extractErSymbols } from './er/parser';
10
+ import { extractSymbols as extractFlowchartSymbols } from './graph/flowchart-parser';
11
+ import { extractSymbols as extractInfraSymbols } from './infra/parser';
12
+ import { extractSymbols as extractClassSymbols } from './class/parser';
13
+
14
+ // ChartType is just a string — alias here for documentation clarity.
15
+ export type ChartType = string;
16
+
17
+ export interface DiagramSymbols {
18
+ kind: ChartType;
19
+ entities: string[]; // table names, node IDs, class names, etc.
20
+ keywords: string[]; // diagram-specific reserved words
21
+ }
22
+
23
+ export type ExtractFn = (docText: string) => DiagramSymbols;
24
+
25
+ const registry = new Map<ChartType, ExtractFn>();
26
+
27
+ export function registerExtractor(kind: ChartType, fn: ExtractFn): void {
28
+ registry.set(kind, fn);
29
+ }
30
+
31
+ /**
32
+ * Extract diagram symbols from document text.
33
+ * Returns null if the chart type is unknown or has no registered extractor.
34
+ */
35
+ export function extractDiagramSymbols(docText: string): DiagramSymbols | null {
36
+ // Parse chartType from first `chart:` line — lightweight, no full parser.
37
+ let chartType: string | null = null;
38
+ for (const line of docText.split('\n')) {
39
+ const m = line.match(/^\s*chart\s*:\s*(.+)/i);
40
+ if (m) {
41
+ chartType = m[1]!.trim().toLowerCase();
42
+ break;
43
+ }
44
+ }
45
+ if (!chartType) return null;
46
+ const fn = registry.get(chartType);
47
+ if (!fn) return null;
48
+ return fn(docText);
49
+ }
50
+
51
+ // ============================================================
52
+ // Register built-in extractors
53
+ // ============================================================
54
+
55
+ registerExtractor('er', extractErSymbols);
56
+ registerExtractor('flowchart', extractFlowchartSymbols);
57
+ registerExtractor('infra', extractInfraSymbols);
58
+ registerExtractor('class', extractClassSymbols);
package/src/d3.ts CHANGED
@@ -1861,7 +1861,6 @@ export function renderArcDiagram(
1861
1861
  const positions = groupNodes.map((n) => yScale(n)!);
1862
1862
  const minY = Math.min(...positions) - bandPad;
1863
1863
  const maxY = Math.max(...positions) + bandPad;
1864
- const bandColor = group.color ?? mutedColor;
1865
1864
 
1866
1865
  g.append('rect')
1867
1866
  .attr('class', 'arc-group-band')
@@ -1996,7 +1995,6 @@ export function renderArcDiagram(
1996
1995
  const positions = groupNodes.map((n) => xScale(n)!);
1997
1996
  const minX = Math.min(...positions) - bandPad;
1998
1997
  const maxX = Math.max(...positions) + bandPad;
1999
- const bandColor = group.color ?? mutedColor;
2000
1998
 
2001
1999
  g.append('rect')
2002
2000
  .attr('class', 'arc-group-band')
@@ -2864,6 +2862,7 @@ export function renderTimeline(
2864
2862
  const textColor = palette.text;
2865
2863
  const mutedColor = palette.border;
2866
2864
  const bgColor = palette.bg;
2865
+ const bg = isDark ? palette.surface : palette.bg;
2867
2866
  const colors = getSeriesColors(palette);
2868
2867
 
2869
2868
  // Assign colors to groups
@@ -3060,7 +3059,7 @@ export function renderTimeline(
3060
3059
  }
3061
3060
  }
3062
3061
 
3063
- // Reserve space for tag legend at the bottom of chart content
3062
+ // Reserve space for tag legend at the top of chart content (below title/headers)
3064
3063
  const tagLegendReserve = parsed.timelineTagGroups.length > 0 ? 36 : 0;
3065
3064
 
3066
3065
  // ================================================================
@@ -3100,9 +3099,9 @@ export function renderTimeline(
3100
3099
  const scaleMargin = timelineScale ? 40 : 0;
3101
3100
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
3102
3101
  const margin = {
3103
- top: 104 + markerMargin,
3102
+ top: 104 + markerMargin + tagLegendReserve,
3104
3103
  right: 40 + scaleMargin,
3105
- bottom: 40 + tagLegendReserve,
3104
+ bottom: 40,
3106
3105
  left: 60 + scaleMargin,
3107
3106
  };
3108
3107
  const innerWidth = width - margin.left - margin.right;
@@ -3254,13 +3253,15 @@ export function renderTimeline(
3254
3253
  const y2 = yScale(parseTimelineDate(ev.endDate));
3255
3254
  const rectH = Math.max(y2 - y, 4);
3256
3255
 
3257
- let fill: string = evColor;
3256
+ let fill: string = mix(evColor, bg, 30);
3257
+ let stroke: string = evColor;
3258
3258
  if (ev.uncertain) {
3259
3259
  const gradientId = `uncertain-vg-${ev.lineNumber}`;
3260
+ const strokeGradientId = `uncertain-vg-s-${ev.lineNumber}`;
3260
3261
  const defs =
3261
3262
  svg.select('defs').node() || svg.append('defs').node();
3262
- d3Selection
3263
- .select(defs as Element)
3263
+ const defsEl = d3Selection.select(defs as Element);
3264
+ defsEl
3264
3265
  .append('linearGradient')
3265
3266
  .attr('id', gradientId)
3266
3267
  .attr('x1', '0%')
@@ -3276,9 +3277,28 @@ export function renderTimeline(
3276
3277
  .enter()
3277
3278
  .append('stop')
3278
3279
  .attr('offset', (d) => d.offset)
3279
- .attr('stop-color', laneColor)
3280
+ .attr('stop-color', mix(laneColor, bg, 30))
3281
+ .attr('stop-opacity', (d) => d.opacity);
3282
+ defsEl
3283
+ .append('linearGradient')
3284
+ .attr('id', strokeGradientId)
3285
+ .attr('x1', '0%')
3286
+ .attr('y1', '0%')
3287
+ .attr('x2', '0%')
3288
+ .attr('y2', '100%')
3289
+ .selectAll('stop')
3290
+ .data([
3291
+ { offset: '0%', opacity: 1 },
3292
+ { offset: '80%', opacity: 1 },
3293
+ { offset: '100%', opacity: 0 },
3294
+ ])
3295
+ .enter()
3296
+ .append('stop')
3297
+ .attr('offset', (d) => d.offset)
3298
+ .attr('stop-color', evColor)
3280
3299
  .attr('stop-opacity', (d) => d.opacity);
3281
3300
  fill = `url(#${gradientId})`;
3301
+ stroke = `url(#${strokeGradientId})`;
3282
3302
  }
3283
3303
 
3284
3304
  evG
@@ -3288,7 +3308,9 @@ export function renderTimeline(
3288
3308
  .attr('width', 12)
3289
3309
  .attr('height', rectH)
3290
3310
  .attr('rx', 4)
3291
- .attr('fill', fill);
3311
+ .attr('fill', fill)
3312
+ .attr('stroke', stroke)
3313
+ .attr('stroke-width', 2);
3292
3314
  evG
3293
3315
  .append('text')
3294
3316
  .attr('x', laneCenter + 14)
@@ -3303,9 +3325,9 @@ export function renderTimeline(
3303
3325
  .attr('cx', laneCenter)
3304
3326
  .attr('cy', y)
3305
3327
  .attr('r', 4)
3306
- .attr('fill', evColor)
3307
- .attr('stroke', bgColor)
3308
- .attr('stroke-width', 1.5);
3328
+ .attr('fill', mix(evColor, bg, 30))
3329
+ .attr('stroke', evColor)
3330
+ .attr('stroke-width', 2);
3309
3331
  evG
3310
3332
  .append('text')
3311
3333
  .attr('x', laneCenter + 10)
@@ -3322,9 +3344,9 @@ export function renderTimeline(
3322
3344
  const scaleMargin = timelineScale ? 40 : 0;
3323
3345
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
3324
3346
  const margin = {
3325
- top: 104 + markerMargin,
3347
+ top: 104 + markerMargin + tagLegendReserve,
3326
3348
  right: 200,
3327
- bottom: 40 + tagLegendReserve,
3349
+ bottom: 40,
3328
3350
  left: 60 + scaleMargin,
3329
3351
  };
3330
3352
  const innerWidth = width - margin.left - margin.right;
@@ -3472,13 +3494,15 @@ export function renderTimeline(
3472
3494
  const y2 = yScale(parseTimelineDate(ev.endDate));
3473
3495
  const rectH = Math.max(y2 - y, 4);
3474
3496
 
3475
- let fill: string = color;
3497
+ let fill: string = mix(color, bg, 30);
3498
+ let stroke: string = color;
3476
3499
  if (ev.uncertain) {
3477
3500
  const gradientId = `uncertain-v-${ev.lineNumber}`;
3501
+ const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
3478
3502
  const defs =
3479
3503
  svg.select('defs').node() || svg.append('defs').node();
3480
- d3Selection
3481
- .select(defs as Element)
3504
+ const defsEl = d3Selection.select(defs as Element);
3505
+ defsEl
3482
3506
  .append('linearGradient')
3483
3507
  .attr('id', gradientId)
3484
3508
  .attr('x1', '0%')
@@ -3494,9 +3518,28 @@ export function renderTimeline(
3494
3518
  .enter()
3495
3519
  .append('stop')
3496
3520
  .attr('offset', (d) => d.offset)
3521
+ .attr('stop-color', mix(color, bg, 30))
3522
+ .attr('stop-opacity', (d) => d.opacity);
3523
+ defsEl
3524
+ .append('linearGradient')
3525
+ .attr('id', strokeGradientId)
3526
+ .attr('x1', '0%')
3527
+ .attr('y1', '0%')
3528
+ .attr('x2', '0%')
3529
+ .attr('y2', '100%')
3530
+ .selectAll('stop')
3531
+ .data([
3532
+ { offset: '0%', opacity: 1 },
3533
+ { offset: '80%', opacity: 1 },
3534
+ { offset: '100%', opacity: 0 },
3535
+ ])
3536
+ .enter()
3537
+ .append('stop')
3538
+ .attr('offset', (d) => d.offset)
3497
3539
  .attr('stop-color', color)
3498
3540
  .attr('stop-opacity', (d) => d.opacity);
3499
3541
  fill = `url(#${gradientId})`;
3542
+ stroke = `url(#${strokeGradientId})`;
3500
3543
  }
3501
3544
 
3502
3545
  evG
@@ -3506,7 +3549,9 @@ export function renderTimeline(
3506
3549
  .attr('width', 12)
3507
3550
  .attr('height', rectH)
3508
3551
  .attr('rx', 4)
3509
- .attr('fill', fill);
3552
+ .attr('fill', fill)
3553
+ .attr('stroke', stroke)
3554
+ .attr('stroke-width', 2);
3510
3555
  evG
3511
3556
  .append('text')
3512
3557
  .attr('x', axisX + 16)
@@ -3521,9 +3566,9 @@ export function renderTimeline(
3521
3566
  .attr('cx', axisX)
3522
3567
  .attr('cy', y)
3523
3568
  .attr('r', 4)
3524
- .attr('fill', color)
3525
- .attr('stroke', bgColor)
3526
- .attr('stroke-width', 1.5);
3569
+ .attr('fill', mix(color, bg, 30))
3570
+ .attr('stroke', color)
3571
+ .attr('stroke-width', 2);
3527
3572
  evG
3528
3573
  .append('text')
3529
3574
  .attr('x', axisX + 16)
@@ -3603,9 +3648,9 @@ export function renderTimeline(
3603
3648
  // Group-sorted doesn't need legend space (group names shown on left)
3604
3649
  const baseTopMargin = title ? 50 : 20;
3605
3650
  const margin = {
3606
- top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin,
3651
+ top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
3607
3652
  right: 40,
3608
- bottom: 40 + scaleMargin + tagLegendReserve,
3653
+ bottom: 40 + scaleMargin,
3609
3654
  left: dynamicLeftMargin,
3610
3655
  };
3611
3656
  const innerWidth = width - margin.left - margin.right;
@@ -3781,13 +3826,15 @@ export function renderTimeline(
3781
3826
  const estLabelWidth = ev.label.length * 7 + 16;
3782
3827
  const labelFitsInside = rectW >= estLabelWidth;
3783
3828
 
3784
- let fill: string = evColor;
3829
+ let fill: string = mix(evColor, bg, 30);
3830
+ let stroke: string = evColor;
3785
3831
  if (ev.uncertain) {
3786
3832
  // Create gradient for uncertain end - fades last 20%
3787
3833
  const gradientId = `uncertain-${ev.lineNumber}`;
3834
+ const strokeGradientId = `uncertain-s-${ev.lineNumber}`;
3788
3835
  const defs = svg.select('defs').node() || svg.append('defs').node();
3789
- d3Selection
3790
- .select(defs as Element)
3836
+ const defsEl = d3Selection.select(defs as Element);
3837
+ defsEl
3791
3838
  .append('linearGradient')
3792
3839
  .attr('id', gradientId)
3793
3840
  .attr('x1', '0%')
@@ -3803,9 +3850,28 @@ export function renderTimeline(
3803
3850
  .enter()
3804
3851
  .append('stop')
3805
3852
  .attr('offset', (d) => d.offset)
3853
+ .attr('stop-color', mix(evColor, bg, 30))
3854
+ .attr('stop-opacity', (d) => d.opacity);
3855
+ defsEl
3856
+ .append('linearGradient')
3857
+ .attr('id', strokeGradientId)
3858
+ .attr('x1', '0%')
3859
+ .attr('y1', '0%')
3860
+ .attr('x2', '100%')
3861
+ .attr('y2', '0%')
3862
+ .selectAll('stop')
3863
+ .data([
3864
+ { offset: '0%', opacity: 1 },
3865
+ { offset: '80%', opacity: 1 },
3866
+ { offset: '100%', opacity: 0 },
3867
+ ])
3868
+ .enter()
3869
+ .append('stop')
3870
+ .attr('offset', (d) => d.offset)
3806
3871
  .attr('stop-color', evColor)
3807
3872
  .attr('stop-opacity', (d) => d.opacity);
3808
3873
  fill = `url(#${gradientId})`;
3874
+ stroke = `url(#${strokeGradientId})`;
3809
3875
  }
3810
3876
 
3811
3877
  evG
@@ -3815,17 +3881,19 @@ export function renderTimeline(
3815
3881
  .attr('width', rectW)
3816
3882
  .attr('height', BAR_H)
3817
3883
  .attr('rx', 4)
3818
- .attr('fill', fill);
3884
+ .attr('fill', fill)
3885
+ .attr('stroke', stroke)
3886
+ .attr('stroke-width', 2);
3819
3887
 
3820
3888
  if (labelFitsInside) {
3821
- // Text inside bar - always white for readability
3889
+ // Text inside bar - use textColor for readability on muted fill
3822
3890
  evG
3823
3891
  .append('text')
3824
3892
  .attr('x', x + 8)
3825
3893
  .attr('y', y)
3826
3894
  .attr('dy', '0.35em')
3827
3895
  .attr('text-anchor', 'start')
3828
- .attr('fill', '#ffffff')
3896
+ .attr('fill', textColor)
3829
3897
  .attr('font-size', '14px')
3830
3898
  .attr('font-weight', '700')
3831
3899
  .text(ev.label);
@@ -3856,9 +3924,9 @@ export function renderTimeline(
3856
3924
  .attr('cx', x)
3857
3925
  .attr('cy', y)
3858
3926
  .attr('r', 5)
3859
- .attr('fill', evColor)
3860
- .attr('stroke', bgColor)
3861
- .attr('stroke-width', 1.5);
3927
+ .attr('fill', mix(evColor, bg, 30))
3928
+ .attr('stroke', evColor)
3929
+ .attr('stroke-width', 2);
3862
3930
  evG
3863
3931
  .append('text')
3864
3932
  .attr('x', flipLeft ? x - 10 : x + 10)
@@ -3882,9 +3950,9 @@ export function renderTimeline(
3882
3950
  const scaleMargin = timelineScale ? 24 : 0;
3883
3951
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
3884
3952
  const margin = {
3885
- top: 104 + (timelineScale ? 40 : 0) + markerMargin,
3953
+ top: 104 + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
3886
3954
  right: 40,
3887
- bottom: 40 + scaleMargin + tagLegendReserve,
3955
+ bottom: 40 + scaleMargin,
3888
3956
  left: 60,
3889
3957
  };
3890
3958
  const innerWidth = width - margin.left - margin.right;
@@ -4041,13 +4109,15 @@ export function renderTimeline(
4041
4109
  const estLabelWidth = ev.label.length * 7 + 16;
4042
4110
  const labelFitsInside = rectW >= estLabelWidth;
4043
4111
 
4044
- let fill: string = color;
4112
+ let fill: string = mix(color, bg, 30);
4113
+ let stroke: string = color;
4045
4114
  if (ev.uncertain) {
4046
4115
  // Create gradient for uncertain end - fades last 20%
4047
4116
  const gradientId = `uncertain-ts-${ev.lineNumber}`;
4117
+ const strokeGradientId = `uncertain-ts-s-${ev.lineNumber}`;
4048
4118
  const defs = svg.select('defs').node() || svg.append('defs').node();
4049
- d3Selection
4050
- .select(defs as Element)
4119
+ const defsEl = d3Selection.select(defs as Element);
4120
+ defsEl
4051
4121
  .append('linearGradient')
4052
4122
  .attr('id', gradientId)
4053
4123
  .attr('x1', '0%')
@@ -4063,9 +4133,28 @@ export function renderTimeline(
4063
4133
  .enter()
4064
4134
  .append('stop')
4065
4135
  .attr('offset', (d) => d.offset)
4136
+ .attr('stop-color', mix(color, bg, 30))
4137
+ .attr('stop-opacity', (d) => d.opacity);
4138
+ defsEl
4139
+ .append('linearGradient')
4140
+ .attr('id', strokeGradientId)
4141
+ .attr('x1', '0%')
4142
+ .attr('y1', '0%')
4143
+ .attr('x2', '100%')
4144
+ .attr('y2', '0%')
4145
+ .selectAll('stop')
4146
+ .data([
4147
+ { offset: '0%', opacity: 1 },
4148
+ { offset: '80%', opacity: 1 },
4149
+ { offset: '100%', opacity: 0 },
4150
+ ])
4151
+ .enter()
4152
+ .append('stop')
4153
+ .attr('offset', (d) => d.offset)
4066
4154
  .attr('stop-color', color)
4067
4155
  .attr('stop-opacity', (d) => d.opacity);
4068
4156
  fill = `url(#${gradientId})`;
4157
+ stroke = `url(#${strokeGradientId})`;
4069
4158
  }
4070
4159
 
4071
4160
  evG
@@ -4075,17 +4164,19 @@ export function renderTimeline(
4075
4164
  .attr('width', rectW)
4076
4165
  .attr('height', BAR_H)
4077
4166
  .attr('rx', 4)
4078
- .attr('fill', fill);
4167
+ .attr('fill', fill)
4168
+ .attr('stroke', stroke)
4169
+ .attr('stroke-width', 2);
4079
4170
 
4080
4171
  if (labelFitsInside) {
4081
- // Text inside bar - always white for readability
4172
+ // Text inside bar - use textColor for readability on muted fill
4082
4173
  evG
4083
4174
  .append('text')
4084
4175
  .attr('x', x + 8)
4085
4176
  .attr('y', y)
4086
4177
  .attr('dy', '0.35em')
4087
4178
  .attr('text-anchor', 'start')
4088
- .attr('fill', '#ffffff')
4179
+ .attr('fill', textColor)
4089
4180
  .attr('font-size', '14px')
4090
4181
  .attr('font-weight', '700')
4091
4182
  .text(ev.label);
@@ -4116,9 +4207,9 @@ export function renderTimeline(
4116
4207
  .attr('cx', x)
4117
4208
  .attr('cy', y)
4118
4209
  .attr('r', 5)
4119
- .attr('fill', color)
4120
- .attr('stroke', bgColor)
4121
- .attr('stroke-width', 1.5);
4210
+ .attr('fill', mix(color, bg, 30))
4211
+ .attr('stroke', color)
4212
+ .attr('stroke-width', 2);
4122
4213
  evG
4123
4214
  .append('text')
4124
4215
  .attr('x', flipLeft ? x - 10 : x + 10)
@@ -4150,7 +4241,8 @@ export function renderTimeline(
4150
4241
  const mainSvg = d3Selection.select(container).select<SVGSVGElement>('svg');
4151
4242
  const mainG = mainSvg.select<SVGGElement>('g');
4152
4243
  if (!mainSvg.empty() && !mainG.empty()) {
4153
- const legendY = height - LG_HEIGHT - 4;
4244
+ // Position legend at top, below title
4245
+ const legendY = title ? 50 : 10;
4154
4246
 
4155
4247
  const groupBg = isDark
4156
4248
  ? mix(palette.surface, palette.bg, 50)
@@ -4438,8 +4530,8 @@ export function renderTimeline(
4438
4530
  color = ev.group && groupColorMap.has(ev.group)
4439
4531
  ? groupColorMap.get(ev.group)! : textColor;
4440
4532
  }
4441
- el.selectAll('rect').attr('fill', color);
4442
- el.selectAll('circle:not(.tl-event-point-outline)').attr('fill', color);
4533
+ el.selectAll('rect').attr('fill', mix(color, bg, 30)).attr('stroke', color);
4534
+ el.selectAll('circle:not(.tl-event-point-outline)').attr('fill', mix(color, bg, 30)).attr('stroke', color);
4443
4535
  });
4444
4536
  }
4445
4537
 
@@ -4635,46 +4727,6 @@ function renderWordCloudAsync(
4635
4727
  // Venn Diagram Math Helpers
4636
4728
  // ============================================================
4637
4729
 
4638
- function radiusFromArea(area: number): number {
4639
- return Math.sqrt(area / Math.PI);
4640
- }
4641
-
4642
- function circleOverlapArea(r1: number, r2: number, d: number): number {
4643
- // No overlap
4644
- if (d >= r1 + r2) return 0;
4645
- // Full containment
4646
- if (d + Math.min(r1, r2) <= Math.max(r1, r2)) {
4647
- return Math.PI * Math.min(r1, r2) ** 2;
4648
- }
4649
- const part1 = r1 * r1 * Math.acos((d * d + r1 * r1 - r2 * r2) / (2 * d * r1));
4650
- const part2 = r2 * r2 * Math.acos((d * d + r2 * r2 - r1 * r1) / (2 * d * r2));
4651
- const part3 =
4652
- 0.5 *
4653
- Math.sqrt((-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2));
4654
- return part1 + part2 - part3;
4655
- }
4656
-
4657
- function distanceForOverlap(
4658
- r1: number,
4659
- r2: number,
4660
- targetArea: number
4661
- ): number {
4662
- if (targetArea <= 0) return r1 + r2;
4663
- const minR = Math.min(r1, r2);
4664
- if (targetArea >= Math.PI * minR * minR) return Math.abs(r1 - r2);
4665
- let lo = Math.abs(r1 - r2);
4666
- let hi = r1 + r2;
4667
- for (let i = 0; i < 64; i++) {
4668
- const mid = (lo + hi) / 2;
4669
- if (circleOverlapArea(r1, r2, mid) > targetArea) {
4670
- lo = mid;
4671
- } else {
4672
- hi = mid;
4673
- }
4674
- }
4675
- return (lo + hi) / 2;
4676
- }
4677
-
4678
4730
  interface Point {
4679
4731
  x: number;
4680
4732
  y: number;
@@ -4686,29 +4738,6 @@ interface Circle {
4686
4738
  r: number;
4687
4739
  }
4688
4740
 
4689
- function thirdCirclePosition(
4690
- ax: number,
4691
- ay: number,
4692
- dAC: number,
4693
- bx: number,
4694
- by: number,
4695
- dBC: number
4696
- ): Point {
4697
- const dx = bx - ax;
4698
- const dy = by - ay;
4699
- const dAB = Math.sqrt(dx * dx + dy * dy);
4700
- if (dAB === 0) return { x: ax + dAC, y: ay };
4701
- const cosA = (dAB * dAB + dAC * dAC - dBC * dBC) / (2 * dAB * dAC);
4702
- const sinA = Math.sqrt(Math.max(0, 1 - cosA * cosA));
4703
- const ux = dx / dAB;
4704
- const uy = dy / dAB;
4705
- // Place C above the AB line
4706
- return {
4707
- x: ax + dAC * (cosA * ux - sinA * uy),
4708
- y: ay + dAC * (cosA * uy + sinA * ux),
4709
- };
4710
- }
4711
-
4712
4741
  function fitCirclesToContainerAsymmetric(
4713
4742
  circles: Circle[],
4714
4743
  w: number,
@@ -5255,7 +5284,6 @@ export function renderQuadrant(
5255
5284
  const init = initD3Chart(container, palette, exportDims);
5256
5285
  if (!init) return;
5257
5286
  const { svg, width, height, textColor } = init;
5258
- const mutedColor = palette.textMuted;
5259
5287
  const borderColor = palette.border;
5260
5288
 
5261
5289
  // Default quadrant colors with alpha
@@ -5300,14 +5328,24 @@ export function renderQuadrant(
5300
5328
  return `#${c(ar,br)}${c(ag,bg)}${c(ab,bb)}`;
5301
5329
  };
5302
5330
 
5303
- // Opaque quadrant fills using the assigned color directly
5304
- const getQuadrantFill = (
5331
+ const bg = isDark ? palette.surface : palette.bg;
5332
+
5333
+ // Full palette color for a quadrant (used for border and label tinting)
5334
+ const getQuadrantColor = (
5305
5335
  label: QuadrantLabel | null,
5306
5336
  defaultIdx: number
5307
5337
  ): string => {
5308
5338
  return label?.color ?? defaultColors[defaultIdx % defaultColors.length];
5309
5339
  };
5310
5340
 
5341
+ // Muted fill: palette color blended 30% toward bg — matches other chart fill style
5342
+ const getQuadrantFill = (
5343
+ label: QuadrantLabel | null,
5344
+ defaultIdx: number
5345
+ ): string => {
5346
+ return mixHex(getQuadrantColor(label, defaultIdx), bg, 30);
5347
+ };
5348
+
5311
5349
  // Quadrant definitions: position, rect bounds, label position
5312
5350
  const quadrantDefs: {
5313
5351
  position: QuadrantPosition;
@@ -5378,17 +5416,16 @@ export function renderQuadrant(
5378
5416
  .attr('width', (d) => d.w)
5379
5417
  .attr('height', (d) => d.h)
5380
5418
  .attr('fill', (d) => getQuadrantFill(d.label, d.colorIdx))
5381
- .attr('stroke', borderColor)
5382
- .attr('stroke-width', 0.5);
5419
+ .attr('stroke', (d) => getQuadrantColor(d.label, d.colorIdx))
5420
+ .attr('stroke-width', 2);
5383
5421
 
5384
5422
  // White text for points; quadrant labels use a darkened shade of their fill
5385
- const contrastColor = '#ffffff';
5386
5423
  const shadowColor = 'rgba(0,0,0,0.4)';
5387
5424
 
5388
- // Darken the quadrant fill to create a watermark-style label color
5425
+ // Darken the full palette color (not the muted fill) to create a watermark-style label
5389
5426
  const getQuadrantLabelColor = (d: (typeof quadrantDefs)[number]): string => {
5390
- const fill = getQuadrantFill(d.label, d.colorIdx);
5391
- return mixHex('#000000', fill, 40);
5427
+ const color = getQuadrantColor(d.label, d.colorIdx);
5428
+ return mixHex('#000000', color, 40);
5392
5429
  };
5393
5430
 
5394
5431
  // Scale label font size to fit within quadrant bounds, wrapping into multiple lines if needed
@@ -5666,13 +5703,13 @@ export function renderQuadrant(
5666
5703
  .attr('stroke', pointColor)
5667
5704
  .attr('stroke-width', 2);
5668
5705
 
5669
- // Label (contrasting color with shadow for readability)
5706
+ // Label (palette text color adapts to light/dark mode)
5670
5707
  pointG
5671
5708
  .append('text')
5672
5709
  .attr('x', cx)
5673
5710
  .attr('y', cy - 10)
5674
5711
  .attr('text-anchor', 'middle')
5675
- .attr('fill', contrastColor)
5712
+ .attr('fill', textColor)
5676
5713
  .attr('font-size', '12px')
5677
5714
  .attr('font-weight', '700')
5678
5715
  .style('text-shadow', `0 1px 2px ${shadowColor}`)
@@ -5962,7 +5999,7 @@ export async function renderForExport(
5962
5999
  const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
5963
6000
  const container = createExportContainer(exportWidth, exportHeight);
5964
6001
 
5965
- renderInitiativeStatus(container, isParsed, isLayout, effectivePalette, theme === 'dark', undefined, { width: exportWidth, height: exportHeight });
6002
+ renderInitiativeStatus(container, isParsed, isLayout, effectivePalette, theme === 'dark', { exportDims: { width: exportWidth, height: exportHeight } });
5966
6003
  return finalizeSvgExport(container, theme, effectivePalette, options);
5967
6004
  }
5968
6005
 
@@ -6051,6 +6088,26 @@ export async function renderForExport(
6051
6088
  return finalizeSvgExport(container, theme, effectivePalette, options);
6052
6089
  }
6053
6090
 
6091
+ if (detectedType === 'gantt') {
6092
+ const { parseGantt } = await import('./gantt/parser');
6093
+ const { calculateSchedule } = await import('./gantt/calculator');
6094
+ const { renderGantt } = await import('./gantt/renderer');
6095
+
6096
+ const effectivePalette = await resolveExportPalette(theme, palette);
6097
+ const ganttParsed = parseGantt(content, effectivePalette);
6098
+ if (ganttParsed.error) return '';
6099
+
6100
+ const resolved = calculateSchedule(ganttParsed);
6101
+ if (resolved.error || resolved.tasks.length === 0) return '';
6102
+
6103
+ const EXPORT_W = 1200;
6104
+ const EXPORT_H = 800;
6105
+ const container = createExportContainer(EXPORT_W, EXPORT_H);
6106
+
6107
+ renderGantt(container, resolved, effectivePalette, theme === 'dark', undefined, { width: EXPORT_W, height: EXPORT_H });
6108
+ return finalizeSvgExport(container, theme, effectivePalette, options);
6109
+ }
6110
+
6054
6111
  if (detectedType === 'state') {
6055
6112
  const { parseState } = await import('./graph/state-parser');
6056
6113
  const { layoutGraph } = await import('./graph/layout');