@diagrammo/dgmo 0.8.20 → 0.8.22
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 +142 -90
- package/dist/editor.cjs +30 -4
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +30 -4
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +25 -3
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +25 -3
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +21201 -12886
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +646 -89
- package/dist/index.d.ts +646 -89
- package/dist/index.js +21178 -12889
- package/dist/index.js.map +1 -1
- package/docs/guide/chart-mindmap.md +198 -0
- package/docs/guide/chart-sequence.md +23 -1
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/chart-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/guide/registry.json +1 -0
- package/docs/language-reference.md +249 -4
- 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/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 +1 -1
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +360 -42
- package/src/boxes-and-lines/parser.ts +94 -11
- package/src/boxes-and-lines/renderer.ts +371 -114
- package/src/boxes-and-lines/types.ts +2 -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/completion.ts +253 -0
- package/src/cycle/layout.ts +732 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +539 -0
- package/src/cycle/types.ts +77 -0
- package/src/d3.ts +240 -40
- package/src/dgmo-router.ts +15 -0
- package/src/echarts.ts +7 -4
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +26 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-parser.ts +5 -10
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +78 -0
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +30 -6
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1456 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +325 -63
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +373 -0
- package/src/mindmap/renderer.ts +544 -0
- package/src/mindmap/text-wrap.ts +217 -0
- package/src/mindmap/types.ts +55 -0
- package/src/org/parser.ts +2 -6
- package/src/render.ts +18 -21
- package/src/sequence/renderer.ts +273 -56
- package/src/sharing.ts +3 -0
- package/src/sitemap/layout.ts +56 -18
- 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 +1058 -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/export-container.ts +3 -2
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +5 -3
- package/src/utils/parsing.ts +48 -7
- package/src/utils/tag-groups.ts +46 -60
- package/src/wireframe/layout.ts +460 -0
- package/src/wireframe/parser.ts +956 -0
- package/src/wireframe/renderer.ts +1293 -0
- package/src/wireframe/types.ts +110 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DgmoError } from '../diagnostics';
|
|
2
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
3
|
+
|
|
4
|
+
export interface JourneyMapAnnotation {
|
|
5
|
+
type: 'pain' | 'opportunity' | 'thought';
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface JourneyMapStep {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
score?: number;
|
|
13
|
+
emotionLabel?: string;
|
|
14
|
+
tags: Record<string, string>;
|
|
15
|
+
annotations: JourneyMapAnnotation[];
|
|
16
|
+
description?: string;
|
|
17
|
+
lineNumber: number;
|
|
18
|
+
endLineNumber: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JourneyMapPhase {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
steps: JourneyMapStep[];
|
|
25
|
+
lineNumber: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface JourneyMapPersona {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
color?: string;
|
|
32
|
+
lineNumber: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParsedJourneyMap {
|
|
36
|
+
type: 'journey-map';
|
|
37
|
+
title?: string;
|
|
38
|
+
titleLineNumber?: number;
|
|
39
|
+
persona?: JourneyMapPersona;
|
|
40
|
+
phases: JourneyMapPhase[];
|
|
41
|
+
/** Flat-mode steps (not inside any phase) */
|
|
42
|
+
steps: JourneyMapStep[];
|
|
43
|
+
tagGroups: TagGroup[];
|
|
44
|
+
options: Record<string, string>;
|
|
45
|
+
diagnostics: DgmoError[];
|
|
46
|
+
error: string | null;
|
|
47
|
+
}
|
package/src/kanban/parser.ts
CHANGED
|
@@ -387,8 +387,8 @@ function parseCardLine(
|
|
|
387
387
|
lineNumber: number,
|
|
388
388
|
counter: number,
|
|
389
389
|
aliasMap: Map<string, string>,
|
|
390
|
-
|
|
391
|
-
|
|
390
|
+
_palette?: PaletteColors,
|
|
391
|
+
_diagnostics?: import('../diagnostics').DgmoError[]
|
|
392
392
|
): KanbanCard {
|
|
393
393
|
// Split on first pipe: Title | tag: value, tag: value
|
|
394
394
|
const pipeIdx = trimmed.indexOf('|');
|
|
@@ -402,13 +402,7 @@ function parseCardLine(
|
|
|
402
402
|
rawTitle = trimmed;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
|
|
406
|
-
const { label: title, color } = extractColor(
|
|
407
|
-
rawTitle,
|
|
408
|
-
palette,
|
|
409
|
-
diagnostics,
|
|
410
|
-
lineNumber
|
|
411
|
-
);
|
|
405
|
+
const title = rawTitle;
|
|
412
406
|
|
|
413
407
|
// Parse tags: comma-separated key: value pairs
|
|
414
408
|
const tags: Record<string, string> = {};
|
|
@@ -431,6 +425,5 @@ function parseCardLine(
|
|
|
431
425
|
details: [],
|
|
432
426
|
lineNumber,
|
|
433
427
|
endLineNumber: lineNumber,
|
|
434
|
-
color,
|
|
435
428
|
};
|
|
436
429
|
}
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -34,6 +34,9 @@ interface KanbanInteractiveOptions {
|
|
|
34
34
|
activeTagGroup?: string | null;
|
|
35
35
|
currentSwimlaneGroup?: string | null;
|
|
36
36
|
onSwimlaneChange?: (group: string | null) => void;
|
|
37
|
+
collapsedLanes?: Set<string>;
|
|
38
|
+
collapsedColumns?: Set<string>;
|
|
39
|
+
compactMeta?: boolean;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// ============================================================
|
|
@@ -61,6 +64,8 @@ const CARD_META_FONT_SIZE = 10;
|
|
|
61
64
|
const WIP_FONT_SIZE = 10;
|
|
62
65
|
const COLUMN_RADIUS = 8;
|
|
63
66
|
const COLUMN_HEADER_RADIUS = 8;
|
|
67
|
+
const COLLAPSED_COLUMN_WIDTH = 40;
|
|
68
|
+
const COLLAPSED_LANE_HEIGHT = 26;
|
|
64
69
|
|
|
65
70
|
// ============================================================
|
|
66
71
|
// Tag color resolution
|
|
@@ -68,10 +73,12 @@ const COLUMN_HEADER_RADIUS = 8;
|
|
|
68
73
|
|
|
69
74
|
function resolveCardTagMeta(
|
|
70
75
|
card: KanbanCard,
|
|
71
|
-
tagGroups: KanbanTagGroup[]
|
|
76
|
+
tagGroups: KanbanTagGroup[],
|
|
77
|
+
hiddenMetaGroups?: string[]
|
|
72
78
|
): { label: string; value: string; color?: string }[] {
|
|
73
79
|
const meta: { label: string; value: string; color?: string }[] = [];
|
|
74
80
|
for (const group of tagGroups) {
|
|
81
|
+
if (hiddenMetaGroups?.includes(group.name.toLowerCase())) continue;
|
|
75
82
|
const tagValue = card.tags[group.name.toLowerCase()];
|
|
76
83
|
const value = tagValue ?? group.defaultValue;
|
|
77
84
|
if (!value) continue;
|
|
@@ -125,7 +132,9 @@ interface CardLayout {
|
|
|
125
132
|
|
|
126
133
|
function computeLayout(
|
|
127
134
|
parsed: ParsedKanban,
|
|
128
|
-
_palette: PaletteColors
|
|
135
|
+
_palette: PaletteColors,
|
|
136
|
+
collapsedColumns?: Set<string>,
|
|
137
|
+
hiddenMetaGroups?: string[]
|
|
129
138
|
): { columns: ColumnLayout[]; totalWidth: number; totalHeight: number } {
|
|
130
139
|
// Title row
|
|
131
140
|
const headerHeight = parsed.title ? TITLE_HEIGHT + 8 : 0;
|
|
@@ -141,6 +150,20 @@ function computeLayout(
|
|
|
141
150
|
const visibleColumns = parsed.columns.filter((c) => !isArchiveColumn(c.name));
|
|
142
151
|
|
|
143
152
|
for (const col of visibleColumns) {
|
|
153
|
+
const isCollapsed = collapsedColumns?.has(col.id) ?? false;
|
|
154
|
+
|
|
155
|
+
if (isCollapsed) {
|
|
156
|
+
columnLayouts.push({
|
|
157
|
+
x: 0,
|
|
158
|
+
y: startY,
|
|
159
|
+
width: COLLAPSED_COLUMN_WIDTH,
|
|
160
|
+
height: 0, // normalized below
|
|
161
|
+
column: col,
|
|
162
|
+
cardLayouts: [],
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
144
167
|
// Compute card heights and column width
|
|
145
168
|
let maxCardTextWidth = col.name.length * (COLUMN_HEADER_FONT_SIZE * 0.65);
|
|
146
169
|
|
|
@@ -155,7 +178,11 @@ function computeLayout(
|
|
|
155
178
|
);
|
|
156
179
|
|
|
157
180
|
// Count metadata rows (tag groups + detail lines)
|
|
158
|
-
const tagMeta = resolveCardTagMeta(
|
|
181
|
+
const tagMeta = resolveCardTagMeta(
|
|
182
|
+
card,
|
|
183
|
+
parsed.tagGroups,
|
|
184
|
+
hiddenMetaGroups
|
|
185
|
+
);
|
|
159
186
|
const metaCount = tagMeta.length + card.details.length;
|
|
160
187
|
const metaHeight =
|
|
161
188
|
metaCount > 0
|
|
@@ -236,6 +263,9 @@ export function renderKanban(
|
|
|
236
263
|
const exportDims = options?.exportDims;
|
|
237
264
|
const activeTagGroup = options?.activeTagGroup ?? null;
|
|
238
265
|
const onSwimlaneChange = options?.onSwimlaneChange;
|
|
266
|
+
const collapsedLanes = options?.collapsedLanes;
|
|
267
|
+
const collapsedColumns = options?.collapsedColumns;
|
|
268
|
+
const compactMeta = options?.compactMeta ?? false;
|
|
239
269
|
// Resolve current swimlane group: must match an existing tag group, else ignore.
|
|
240
270
|
const requestedSwimlane = options?.currentSwimlaneGroup ?? null;
|
|
241
271
|
const swimlaneGroup = requestedSwimlane
|
|
@@ -244,7 +274,20 @@ export function renderKanban(
|
|
|
244
274
|
) ?? null)
|
|
245
275
|
: null;
|
|
246
276
|
|
|
247
|
-
|
|
277
|
+
// Compute hidden meta groups for compact mode
|
|
278
|
+
const hiddenMetaGroups: string[] = [];
|
|
279
|
+
if (compactMeta) {
|
|
280
|
+
if (activeTagGroup) hiddenMetaGroups.push(activeTagGroup.toLowerCase());
|
|
281
|
+
if (requestedSwimlane)
|
|
282
|
+
hiddenMetaGroups.push(requestedSwimlane.toLowerCase());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const layout = computeLayout(
|
|
286
|
+
parsed,
|
|
287
|
+
palette,
|
|
288
|
+
collapsedColumns,
|
|
289
|
+
hiddenMetaGroups
|
|
290
|
+
);
|
|
248
291
|
|
|
249
292
|
const width = exportDims?.width ?? layout.totalWidth;
|
|
250
293
|
const height = exportDims?.height ?? layout.totalHeight;
|
|
@@ -347,7 +390,10 @@ export function renderKanban(
|
|
|
347
390
|
swimlaneGroup,
|
|
348
391
|
palette,
|
|
349
392
|
isDark,
|
|
350
|
-
activeTagGroup
|
|
393
|
+
activeTagGroup,
|
|
394
|
+
collapsedLanes,
|
|
395
|
+
collapsedColumns,
|
|
396
|
+
hiddenMetaGroups
|
|
351
397
|
);
|
|
352
398
|
return;
|
|
353
399
|
}
|
|
@@ -364,6 +410,7 @@ export function renderKanban(
|
|
|
364
410
|
|
|
365
411
|
for (const colLayout of layout.columns) {
|
|
366
412
|
const col = colLayout.column;
|
|
413
|
+
const isColCollapsed = collapsedColumns?.has(col.id) ?? false;
|
|
367
414
|
const g = svg
|
|
368
415
|
.append('g')
|
|
369
416
|
.attr('class', 'kanban-column')
|
|
@@ -377,6 +424,51 @@ export function renderKanban(
|
|
|
377
424
|
? mix(col.color, palette.bg, 25)
|
|
378
425
|
: defaultColHeaderBg;
|
|
379
426
|
|
|
427
|
+
if (isColCollapsed) {
|
|
428
|
+
// Collapsed column: narrow strip with count + vertical name
|
|
429
|
+
g.append('rect')
|
|
430
|
+
.attr('x', colLayout.x)
|
|
431
|
+
.attr('y', colLayout.y)
|
|
432
|
+
.attr('width', COLLAPSED_COLUMN_WIDTH)
|
|
433
|
+
.attr('height', colLayout.height)
|
|
434
|
+
.attr('rx', COLUMN_RADIUS)
|
|
435
|
+
.attr('fill', thisColBg);
|
|
436
|
+
|
|
437
|
+
g.append('rect')
|
|
438
|
+
.attr('x', colLayout.x)
|
|
439
|
+
.attr('y', colLayout.y)
|
|
440
|
+
.attr('width', COLLAPSED_COLUMN_WIDTH)
|
|
441
|
+
.attr('height', COLUMN_HEADER_HEIGHT)
|
|
442
|
+
.attr('rx', COLUMN_HEADER_RADIUS)
|
|
443
|
+
.attr('fill', thisColHeaderBg);
|
|
444
|
+
|
|
445
|
+
// Card count
|
|
446
|
+
g.append('text')
|
|
447
|
+
.attr('x', colLayout.x + COLLAPSED_COLUMN_WIDTH / 2)
|
|
448
|
+
.attr(
|
|
449
|
+
'y',
|
|
450
|
+
colLayout.y + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
|
|
451
|
+
)
|
|
452
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
453
|
+
.attr('font-weight', 'bold')
|
|
454
|
+
.attr('fill', palette.textMuted)
|
|
455
|
+
.attr('text-anchor', 'middle')
|
|
456
|
+
.text(String(col.cards.length));
|
|
457
|
+
|
|
458
|
+
// Vertical column name
|
|
459
|
+
g.append('text')
|
|
460
|
+
.attr('x', colLayout.x + COLLAPSED_COLUMN_WIDTH / 2)
|
|
461
|
+
.attr('y', colLayout.y + COLUMN_HEADER_HEIGHT + COLUMN_PADDING)
|
|
462
|
+
.attr('font-size', CARD_TITLE_FONT_SIZE)
|
|
463
|
+
.attr('font-weight', 'bold')
|
|
464
|
+
.attr('fill', palette.text)
|
|
465
|
+
.attr('text-anchor', 'middle')
|
|
466
|
+
.attr('writing-mode', 'tb')
|
|
467
|
+
.text(col.name);
|
|
468
|
+
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
|
|
380
472
|
// Column background
|
|
381
473
|
g.append('rect')
|
|
382
474
|
.attr('x', colLayout.x)
|
|
@@ -407,17 +499,21 @@ export function renderKanban(
|
|
|
407
499
|
.attr('fill', palette.text)
|
|
408
500
|
.text(col.name);
|
|
409
501
|
|
|
410
|
-
// WIP limit badge
|
|
411
|
-
|
|
412
|
-
const wipExceeded =
|
|
413
|
-
|
|
414
|
-
const
|
|
502
|
+
// Card count / WIP limit badge (right-aligned)
|
|
503
|
+
{
|
|
504
|
+
const wipExceeded =
|
|
505
|
+
col.wipLimit != null && col.cards.length > col.wipLimit;
|
|
506
|
+
const badgeText =
|
|
507
|
+
col.wipLimit != null
|
|
508
|
+
? `${col.cards.length}/${col.wipLimit}`
|
|
509
|
+
: String(col.cards.length);
|
|
415
510
|
g.append('text')
|
|
416
|
-
.attr('x', colLayout.x +
|
|
511
|
+
.attr('x', colLayout.x + colLayout.width - COLUMN_PADDING)
|
|
417
512
|
.attr(
|
|
418
513
|
'y',
|
|
419
514
|
colLayout.y + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
|
|
420
515
|
)
|
|
516
|
+
.attr('text-anchor', 'end')
|
|
421
517
|
.attr('font-size', WIP_FONT_SIZE)
|
|
422
518
|
.attr('fill', wipExceeded ? palette.colors.red : palette.textMuted)
|
|
423
519
|
.attr('font-weight', wipExceeded ? 'bold' : 'normal')
|
|
@@ -432,7 +528,11 @@ export function renderKanban(
|
|
|
432
528
|
parsed.tagGroups,
|
|
433
529
|
activeTagGroup ?? null
|
|
434
530
|
);
|
|
435
|
-
const tagMeta = resolveCardTagMeta(
|
|
531
|
+
const tagMeta = resolveCardTagMeta(
|
|
532
|
+
card,
|
|
533
|
+
parsed.tagGroups,
|
|
534
|
+
hiddenMetaGroups
|
|
535
|
+
);
|
|
436
536
|
const hasMeta = tagMeta.length > 0 || card.details.length > 0;
|
|
437
537
|
|
|
438
538
|
// Org-chart-style fill: 15% blend of color into bg
|
|
@@ -674,8 +774,12 @@ interface SwimlaneBoardLayout {
|
|
|
674
774
|
startY: number;
|
|
675
775
|
}
|
|
676
776
|
|
|
677
|
-
function computeCardHeight(
|
|
678
|
-
|
|
777
|
+
function computeCardHeight(
|
|
778
|
+
card: KanbanCard,
|
|
779
|
+
tagGroups: KanbanTagGroup[],
|
|
780
|
+
hiddenMetaGroups?: string[]
|
|
781
|
+
) {
|
|
782
|
+
const tagMeta = resolveCardTagMeta(card, tagGroups, hiddenMetaGroups);
|
|
679
783
|
const metaCount = tagMeta.length + card.details.length;
|
|
680
784
|
const metaHeight =
|
|
681
785
|
metaCount > 0
|
|
@@ -690,7 +794,10 @@ function computeCardHeight(card: KanbanCard, tagGroups: KanbanTagGroup[]) {
|
|
|
690
794
|
function computeSwimlaneLayout(
|
|
691
795
|
parsed: ParsedKanban,
|
|
692
796
|
buckets: SwimlaneBucket[],
|
|
693
|
-
baseLayout: { columns: ColumnLayout[] }
|
|
797
|
+
baseLayout: { columns: ColumnLayout[] },
|
|
798
|
+
collapsedLanes?: Set<string>,
|
|
799
|
+
collapsedColumns?: Set<string>,
|
|
800
|
+
hiddenMetaGroups?: string[]
|
|
694
801
|
): SwimlaneBoardLayout {
|
|
695
802
|
const headerHeight = parsed.title ? TITLE_HEIGHT + 8 : 0;
|
|
696
803
|
const startY = DIAGRAM_PADDING + headerHeight;
|
|
@@ -699,8 +806,10 @@ function computeSwimlaneLayout(
|
|
|
699
806
|
const columnXs: SwimlaneBoardLayout['columnXs'] = [];
|
|
700
807
|
let currentX = DIAGRAM_PADDING + LANE_HEADER_WIDTH;
|
|
701
808
|
for (const col of baseLayout.columns) {
|
|
702
|
-
|
|
703
|
-
|
|
809
|
+
const isColCollapsed = collapsedColumns?.has(col.column.id) ?? false;
|
|
810
|
+
const w = isColCollapsed ? COLLAPSED_COLUMN_WIDTH : col.width;
|
|
811
|
+
columnXs.push({ column: col.column, x: currentX, width: w });
|
|
812
|
+
currentX += w + COLUMN_GAP;
|
|
704
813
|
}
|
|
705
814
|
const totalWidth = currentX - COLUMN_GAP + DIAGRAM_PADDING;
|
|
706
815
|
|
|
@@ -710,15 +819,36 @@ function computeSwimlaneLayout(
|
|
|
710
819
|
const minCellH = CARD_HEADER_HEIGHT + CARD_PADDING_Y + CARD_GAP;
|
|
711
820
|
|
|
712
821
|
for (const bucket of buckets) {
|
|
822
|
+
const isLaneCollapsed = collapsedLanes?.has(bucket.laneName) ?? false;
|
|
823
|
+
|
|
824
|
+
if (isLaneCollapsed) {
|
|
825
|
+
// Collapsed lane: minimal height, no card layouts
|
|
826
|
+
const cells: SwimlaneCellLayout[] = columnXs.map((colInfo) => ({
|
|
827
|
+
column: colInfo.column,
|
|
828
|
+
cards: bucket.cellsByColumn[colInfo.column.id] ?? [],
|
|
829
|
+
cardLayouts: [],
|
|
830
|
+
}));
|
|
831
|
+
lanes.push({ bucket, y: laneY, height: COLLAPSED_LANE_HEIGHT, cells });
|
|
832
|
+
laneY += COLLAPSED_LANE_HEIGHT + LANE_GAP;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
|
|
713
836
|
let maxCellH = minCellH;
|
|
714
837
|
const cellsTmp: { column: KanbanColumn; cards: KanbanCard[]; h: number }[] =
|
|
715
838
|
[];
|
|
716
839
|
|
|
717
840
|
for (const colInfo of columnXs) {
|
|
841
|
+
const isColCollapsed = collapsedColumns?.has(colInfo.column.id) ?? false;
|
|
718
842
|
const cards = bucket.cellsByColumn[colInfo.column.id] ?? [];
|
|
843
|
+
if (isColCollapsed) {
|
|
844
|
+
// Collapsed column cells get minimal height
|
|
845
|
+
cellsTmp.push({ column: colInfo.column, cards, h: 0 });
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
719
848
|
let h = 0;
|
|
720
849
|
for (const c of cards) {
|
|
721
|
-
h +=
|
|
850
|
+
h +=
|
|
851
|
+
computeCardHeight(c, parsed.tagGroups, hiddenMetaGroups) + CARD_GAP;
|
|
722
852
|
}
|
|
723
853
|
h = Math.max(h - (cards.length > 0 ? CARD_GAP : 0), 0);
|
|
724
854
|
cellsTmp.push({ column: colInfo.column, cards, h });
|
|
@@ -730,10 +860,14 @@ function computeSwimlaneLayout(
|
|
|
730
860
|
// Build cell layouts with card x/y offsets relative to (cell.x, laneY)
|
|
731
861
|
const cells: SwimlaneCellLayout[] = cellsTmp.map((tmp, i) => {
|
|
732
862
|
const colInfo = columnXs[i]!;
|
|
863
|
+
const isColCollapsed = collapsedColumns?.has(colInfo.column.id) ?? false;
|
|
864
|
+
if (isColCollapsed) {
|
|
865
|
+
return { column: tmp.column, cards: tmp.cards, cardLayouts: [] };
|
|
866
|
+
}
|
|
733
867
|
const cardLayouts: CardLayout[] = [];
|
|
734
868
|
let cy = 0;
|
|
735
869
|
for (const card of tmp.cards) {
|
|
736
|
-
const ch = computeCardHeight(card, parsed.tagGroups);
|
|
870
|
+
const ch = computeCardHeight(card, parsed.tagGroups, hiddenMetaGroups);
|
|
737
871
|
cardLayouts.push({
|
|
738
872
|
x: colInfo.x + COLUMN_PADDING,
|
|
739
873
|
y: laneY + cy,
|
|
@@ -766,11 +900,21 @@ function renderSwimlaneBoard(
|
|
|
766
900
|
swimlaneGroup: KanbanTagGroup,
|
|
767
901
|
palette: PaletteColors,
|
|
768
902
|
isDark: boolean,
|
|
769
|
-
activeTagGroup: string | null
|
|
903
|
+
activeTagGroup: string | null,
|
|
904
|
+
collapsedLanes?: Set<string>,
|
|
905
|
+
collapsedColumns?: Set<string>,
|
|
906
|
+
hiddenMetaGroups?: string[]
|
|
770
907
|
): void {
|
|
771
908
|
const visibleColumns = parsed.columns.filter((c) => !isArchiveColumn(c.name));
|
|
772
909
|
const buckets = bucketCardsBySwimlane(visibleColumns, swimlaneGroup);
|
|
773
|
-
const grid = computeSwimlaneLayout(
|
|
910
|
+
const grid = computeSwimlaneLayout(
|
|
911
|
+
parsed,
|
|
912
|
+
buckets,
|
|
913
|
+
baseLayout,
|
|
914
|
+
collapsedLanes,
|
|
915
|
+
collapsedColumns,
|
|
916
|
+
hiddenMetaGroups
|
|
917
|
+
);
|
|
774
918
|
|
|
775
919
|
// Resize the svg to fit the grid (only when not using exportDims).
|
|
776
920
|
const currentW = parseFloat(svg.attr('width') || '0');
|
|
@@ -789,6 +933,7 @@ function renderSwimlaneBoard(
|
|
|
789
933
|
// Column header row spanning all lanes
|
|
790
934
|
for (const colInfo of grid.columnXs) {
|
|
791
935
|
const col = colInfo.column;
|
|
936
|
+
const isColCollapsed = collapsedColumns?.has(col.id) ?? false;
|
|
792
937
|
const headerG = svg
|
|
793
938
|
.append('g')
|
|
794
939
|
.attr('class', 'kanban-column kanban-column-header')
|
|
@@ -808,21 +953,61 @@ function renderSwimlaneBoard(
|
|
|
808
953
|
.attr('rx', COLUMN_HEADER_RADIUS)
|
|
809
954
|
.attr('fill', colHeaderBg);
|
|
810
955
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
'
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
956
|
+
if (isColCollapsed) {
|
|
957
|
+
// Collapsed: show count + vertical name below header
|
|
958
|
+
headerG
|
|
959
|
+
.append('text')
|
|
960
|
+
.attr('x', colInfo.x + colInfo.width / 2)
|
|
961
|
+
.attr(
|
|
962
|
+
'y',
|
|
963
|
+
grid.startY + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
|
|
964
|
+
)
|
|
965
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
966
|
+
.attr('font-weight', 'bold')
|
|
967
|
+
.attr('fill', palette.textMuted)
|
|
968
|
+
.attr('text-anchor', 'middle')
|
|
969
|
+
.text(String(col.cards.length));
|
|
970
|
+
} else {
|
|
971
|
+
headerG
|
|
972
|
+
.append('text')
|
|
973
|
+
.attr('x', colInfo.x + COLUMN_PADDING)
|
|
974
|
+
.attr(
|
|
975
|
+
'y',
|
|
976
|
+
grid.startY +
|
|
977
|
+
COLUMN_HEADER_HEIGHT / 2 +
|
|
978
|
+
COLUMN_HEADER_FONT_SIZE / 2 -
|
|
979
|
+
2
|
|
980
|
+
)
|
|
981
|
+
.attr('font-size', COLUMN_HEADER_FONT_SIZE)
|
|
982
|
+
.attr('font-weight', 'bold')
|
|
983
|
+
.attr('fill', palette.text)
|
|
984
|
+
.text(col.name);
|
|
985
|
+
|
|
986
|
+
// Card count (right-aligned)
|
|
987
|
+
const wipExceeded =
|
|
988
|
+
col.wipLimit != null && col.cards.length > col.wipLimit;
|
|
989
|
+
const badgeText =
|
|
990
|
+
col.wipLimit != null
|
|
991
|
+
? `${col.cards.length}/${col.wipLimit}`
|
|
992
|
+
: String(col.cards.length);
|
|
993
|
+
headerG
|
|
994
|
+
.append('text')
|
|
995
|
+
.attr('x', colInfo.x + colInfo.width - COLUMN_PADDING)
|
|
996
|
+
.attr(
|
|
997
|
+
'y',
|
|
998
|
+
grid.startY + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
|
|
999
|
+
)
|
|
1000
|
+
.attr('text-anchor', 'end')
|
|
1001
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
1002
|
+
.attr('fill', wipExceeded ? palette.colors.red : palette.textMuted)
|
|
1003
|
+
.attr('font-weight', wipExceeded ? 'bold' : 'normal')
|
|
1004
|
+
.text(badgeText);
|
|
1005
|
+
}
|
|
822
1006
|
}
|
|
823
1007
|
|
|
824
1008
|
// Lanes
|
|
825
1009
|
for (const lane of grid.lanes) {
|
|
1010
|
+
const isLaneCollapsed = collapsedLanes?.has(lane.bucket.laneName) ?? false;
|
|
826
1011
|
const laneG = svg
|
|
827
1012
|
.append('g')
|
|
828
1013
|
.attr('class', 'kanban-lane')
|
|
@@ -847,8 +1032,9 @@ function renderSwimlaneBoard(
|
|
|
847
1032
|
.attr('rx', COLUMN_RADIUS)
|
|
848
1033
|
.attr('fill', defaultColBg);
|
|
849
1034
|
|
|
850
|
-
// Lane label
|
|
1035
|
+
// Lane label + count
|
|
851
1036
|
let labelX = 10;
|
|
1037
|
+
const totalCards = lane.cells.reduce((s, c) => s + c.cards.length, 0);
|
|
852
1038
|
if (lane.bucket.laneColor) {
|
|
853
1039
|
headerG
|
|
854
1040
|
.append('circle')
|
|
@@ -858,24 +1044,66 @@ function renderSwimlaneBoard(
|
|
|
858
1044
|
.attr('fill', lane.bucket.laneColor);
|
|
859
1045
|
labelX += 14;
|
|
860
1046
|
}
|
|
861
|
-
headerG
|
|
862
|
-
.append('text')
|
|
863
|
-
.attr('x', labelX)
|
|
864
|
-
.attr('y', 20)
|
|
865
|
-
.attr('font-size', 12)
|
|
866
|
-
.attr('font-weight', 'bold')
|
|
867
|
-
.attr('fill', lane.bucket.isFallback ? palette.textMuted : palette.text)
|
|
868
|
-
.text(lane.bucket.laneName);
|
|
869
1047
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1048
|
+
if (isLaneCollapsed) {
|
|
1049
|
+
// Collapsed: single line with name + count
|
|
1050
|
+
headerG
|
|
1051
|
+
.append('text')
|
|
1052
|
+
.attr('x', labelX)
|
|
1053
|
+
.attr('y', 20)
|
|
1054
|
+
.attr('font-size', 10)
|
|
1055
|
+
.attr('fill', palette.textMuted)
|
|
1056
|
+
.text(`${lane.bucket.laneName} (${totalCards})`);
|
|
1057
|
+
} else {
|
|
1058
|
+
// Expanded: name only (count omitted to match app view)
|
|
1059
|
+
headerG
|
|
1060
|
+
.append('text')
|
|
1061
|
+
.attr('x', labelX)
|
|
1062
|
+
.attr('y', 20)
|
|
1063
|
+
.attr('font-size', 12)
|
|
1064
|
+
.attr('font-weight', 'bold')
|
|
1065
|
+
.attr('fill', lane.bucket.isFallback ? palette.textMuted : palette.text)
|
|
1066
|
+
.text(lane.bucket.laneName);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (isLaneCollapsed) {
|
|
1070
|
+
// Render count placeholders in each cell
|
|
1071
|
+
for (const cell of lane.cells) {
|
|
1072
|
+
const isColCollapsed = collapsedColumns?.has(cell.column.id) ?? false;
|
|
1073
|
+
if (cell.cards.length > 0) {
|
|
1074
|
+
const colInfo = grid.columnXs.find(
|
|
1075
|
+
(c) => c.column.id === cell.column.id
|
|
1076
|
+
);
|
|
1077
|
+
if (!colInfo) continue;
|
|
1078
|
+
const placeholderBg = lane.bucket.laneColor
|
|
1079
|
+
? mix(lane.bucket.laneColor, palette.bg, isDark ? 40 : 28)
|
|
1080
|
+
: mix(palette.textMuted, palette.bg, isDark ? 28 : 22);
|
|
1081
|
+
const pw = isColCollapsed
|
|
1082
|
+
? COLLAPSED_COLUMN_WIDTH - 8
|
|
1083
|
+
: colInfo.width - COLUMN_PADDING * 2;
|
|
1084
|
+
laneG
|
|
1085
|
+
.append('rect')
|
|
1086
|
+
.attr('x', colInfo.x + (isColCollapsed ? 4 : COLUMN_PADDING))
|
|
1087
|
+
.attr('y', lane.y)
|
|
1088
|
+
.attr('width', pw)
|
|
1089
|
+
.attr('height', 18)
|
|
1090
|
+
.attr('rx', 4)
|
|
1091
|
+
.attr('fill', placeholderBg);
|
|
1092
|
+
laneG
|
|
1093
|
+
.append('text')
|
|
1094
|
+
.attr(
|
|
1095
|
+
'x',
|
|
1096
|
+
colInfo.x + (isColCollapsed ? 4 : COLUMN_PADDING) + pw / 2
|
|
1097
|
+
)
|
|
1098
|
+
.attr('y', lane.y + 13)
|
|
1099
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
1100
|
+
.attr('font-weight', 'bold')
|
|
1101
|
+
.attr('fill', palette.textMuted)
|
|
1102
|
+
.attr('text-anchor', 'middle')
|
|
1103
|
+
.text(String(cell.cards.length));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
879
1107
|
|
|
880
1108
|
// Lane divider line below the row
|
|
881
1109
|
laneG
|
|
@@ -888,17 +1116,50 @@ function renderSwimlaneBoard(
|
|
|
888
1116
|
.attr('stroke-opacity', 0.4)
|
|
889
1117
|
.attr('stroke-width', 1);
|
|
890
1118
|
|
|
891
|
-
// Cards inside cells
|
|
892
|
-
|
|
893
|
-
for (const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1119
|
+
// Cards inside cells (skip for collapsed lanes — they show count placeholders instead)
|
|
1120
|
+
if (!isLaneCollapsed) {
|
|
1121
|
+
for (const cell of lane.cells) {
|
|
1122
|
+
const isColCollapsed = collapsedColumns?.has(cell.column.id) ?? false;
|
|
1123
|
+
if (isColCollapsed && cell.cards.length > 0) {
|
|
1124
|
+
// Collapsed column in non-collapsed lane: show count placeholder
|
|
1125
|
+
const colInfo = grid.columnXs.find(
|
|
1126
|
+
(c) => c.column.id === cell.column.id
|
|
1127
|
+
);
|
|
1128
|
+
if (colInfo) {
|
|
1129
|
+
const placeholderBg = lane.bucket.laneColor
|
|
1130
|
+
? mix(lane.bucket.laneColor, palette.bg, isDark ? 40 : 28)
|
|
1131
|
+
: mix(palette.textMuted, palette.bg, isDark ? 28 : 22);
|
|
1132
|
+
laneG
|
|
1133
|
+
.append('rect')
|
|
1134
|
+
.attr('x', colInfo.x + 4)
|
|
1135
|
+
.attr('y', lane.y)
|
|
1136
|
+
.attr('width', COLLAPSED_COLUMN_WIDTH - 8)
|
|
1137
|
+
.attr('height', 22)
|
|
1138
|
+
.attr('rx', 4)
|
|
1139
|
+
.attr('fill', placeholderBg);
|
|
1140
|
+
laneG
|
|
1141
|
+
.append('text')
|
|
1142
|
+
.attr('x', colInfo.x + COLLAPSED_COLUMN_WIDTH / 2)
|
|
1143
|
+
.attr('y', lane.y + 16)
|
|
1144
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
1145
|
+
.attr('font-weight', 'bold')
|
|
1146
|
+
.attr('fill', palette.textMuted)
|
|
1147
|
+
.attr('text-anchor', 'middle')
|
|
1148
|
+
.text(String(cell.cards.length));
|
|
1149
|
+
}
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
for (const cardLayout of cell.cardLayouts) {
|
|
1153
|
+
renderSwimlaneCard(
|
|
1154
|
+
laneG,
|
|
1155
|
+
cardLayout,
|
|
1156
|
+
parsed.tagGroups,
|
|
1157
|
+
activeTagGroup,
|
|
1158
|
+
palette,
|
|
1159
|
+
cardBaseBg,
|
|
1160
|
+
hiddenMetaGroups
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
902
1163
|
}
|
|
903
1164
|
}
|
|
904
1165
|
}
|
|
@@ -910,11 +1171,12 @@ function renderSwimlaneCard(
|
|
|
910
1171
|
tagGroups: KanbanTagGroup[],
|
|
911
1172
|
activeTagGroup: string | null,
|
|
912
1173
|
palette: PaletteColors,
|
|
913
|
-
cardBaseBg: string
|
|
1174
|
+
cardBaseBg: string,
|
|
1175
|
+
hiddenMetaGroups?: string[]
|
|
914
1176
|
): void {
|
|
915
1177
|
const card = cardLayout.card;
|
|
916
1178
|
const resolvedColor = resolveCardTagColor(card, tagGroups, activeTagGroup);
|
|
917
|
-
const tagMeta = resolveCardTagMeta(card, tagGroups);
|
|
1179
|
+
const tagMeta = resolveCardTagMeta(card, tagGroups, hiddenMetaGroups);
|
|
918
1180
|
const hasMeta = tagMeta.length > 0 || card.details.length > 0;
|
|
919
1181
|
|
|
920
1182
|
const cardFill = resolvedColor
|