@diagrammo/dgmo 0.8.20 → 0.8.21
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/cli.cjs +92 -90
- package/dist/editor.cjs +13 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +13 -1
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +13 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +13 -1
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +4144 -940
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +318 -84
- package/dist/index.d.ts +318 -84
- package/dist/index.js +4132 -938
- 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-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/language-reference.md +137 -2
- package/package.json +1 -1
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +51 -9
- package/src/boxes-and-lines/parser.ts +8 -1
- package/src/boxes-and-lines/renderer.ts +121 -23
- package/src/boxes-and-lines/types.ts +1 -0
- package/src/completion.ts +26 -0
- package/src/d3.ts +153 -32
- package/src/dgmo-router.ts +6 -0
- package/src/editor/keywords.ts +12 -0
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +28 -0
- package/src/kanban/renderer.ts +303 -57
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +379 -0
- package/src/mindmap/renderer.ts +543 -0
- package/src/mindmap/text-wrap.ts +207 -0
- package/src/mindmap/types.ts +55 -0
- package/src/render.ts +18 -21
- package/src/sequence/renderer.ts +129 -18
- package/src/sharing.ts +2 -0
- package/src/sitemap/layout.ts +35 -12
- package/src/utils/export-container.ts +3 -2
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +2 -2
- package/src/utils/parsing.ts +2 -0
- 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
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)
|
|
@@ -432,7 +524,11 @@ export function renderKanban(
|
|
|
432
524
|
parsed.tagGroups,
|
|
433
525
|
activeTagGroup ?? null
|
|
434
526
|
);
|
|
435
|
-
const tagMeta = resolveCardTagMeta(
|
|
527
|
+
const tagMeta = resolveCardTagMeta(
|
|
528
|
+
card,
|
|
529
|
+
parsed.tagGroups,
|
|
530
|
+
hiddenMetaGroups
|
|
531
|
+
);
|
|
436
532
|
const hasMeta = tagMeta.length > 0 || card.details.length > 0;
|
|
437
533
|
|
|
438
534
|
// Org-chart-style fill: 15% blend of color into bg
|
|
@@ -674,8 +770,12 @@ interface SwimlaneBoardLayout {
|
|
|
674
770
|
startY: number;
|
|
675
771
|
}
|
|
676
772
|
|
|
677
|
-
function computeCardHeight(
|
|
678
|
-
|
|
773
|
+
function computeCardHeight(
|
|
774
|
+
card: KanbanCard,
|
|
775
|
+
tagGroups: KanbanTagGroup[],
|
|
776
|
+
hiddenMetaGroups?: string[]
|
|
777
|
+
) {
|
|
778
|
+
const tagMeta = resolveCardTagMeta(card, tagGroups, hiddenMetaGroups);
|
|
679
779
|
const metaCount = tagMeta.length + card.details.length;
|
|
680
780
|
const metaHeight =
|
|
681
781
|
metaCount > 0
|
|
@@ -690,7 +790,10 @@ function computeCardHeight(card: KanbanCard, tagGroups: KanbanTagGroup[]) {
|
|
|
690
790
|
function computeSwimlaneLayout(
|
|
691
791
|
parsed: ParsedKanban,
|
|
692
792
|
buckets: SwimlaneBucket[],
|
|
693
|
-
baseLayout: { columns: ColumnLayout[] }
|
|
793
|
+
baseLayout: { columns: ColumnLayout[] },
|
|
794
|
+
collapsedLanes?: Set<string>,
|
|
795
|
+
collapsedColumns?: Set<string>,
|
|
796
|
+
hiddenMetaGroups?: string[]
|
|
694
797
|
): SwimlaneBoardLayout {
|
|
695
798
|
const headerHeight = parsed.title ? TITLE_HEIGHT + 8 : 0;
|
|
696
799
|
const startY = DIAGRAM_PADDING + headerHeight;
|
|
@@ -699,8 +802,10 @@ function computeSwimlaneLayout(
|
|
|
699
802
|
const columnXs: SwimlaneBoardLayout['columnXs'] = [];
|
|
700
803
|
let currentX = DIAGRAM_PADDING + LANE_HEADER_WIDTH;
|
|
701
804
|
for (const col of baseLayout.columns) {
|
|
702
|
-
|
|
703
|
-
|
|
805
|
+
const isColCollapsed = collapsedColumns?.has(col.column.id) ?? false;
|
|
806
|
+
const w = isColCollapsed ? COLLAPSED_COLUMN_WIDTH : col.width;
|
|
807
|
+
columnXs.push({ column: col.column, x: currentX, width: w });
|
|
808
|
+
currentX += w + COLUMN_GAP;
|
|
704
809
|
}
|
|
705
810
|
const totalWidth = currentX - COLUMN_GAP + DIAGRAM_PADDING;
|
|
706
811
|
|
|
@@ -710,15 +815,36 @@ function computeSwimlaneLayout(
|
|
|
710
815
|
const minCellH = CARD_HEADER_HEIGHT + CARD_PADDING_Y + CARD_GAP;
|
|
711
816
|
|
|
712
817
|
for (const bucket of buckets) {
|
|
818
|
+
const isLaneCollapsed = collapsedLanes?.has(bucket.laneName) ?? false;
|
|
819
|
+
|
|
820
|
+
if (isLaneCollapsed) {
|
|
821
|
+
// Collapsed lane: minimal height, no card layouts
|
|
822
|
+
const cells: SwimlaneCellLayout[] = columnXs.map((colInfo) => ({
|
|
823
|
+
column: colInfo.column,
|
|
824
|
+
cards: bucket.cellsByColumn[colInfo.column.id] ?? [],
|
|
825
|
+
cardLayouts: [],
|
|
826
|
+
}));
|
|
827
|
+
lanes.push({ bucket, y: laneY, height: COLLAPSED_LANE_HEIGHT, cells });
|
|
828
|
+
laneY += COLLAPSED_LANE_HEIGHT + LANE_GAP;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
713
832
|
let maxCellH = minCellH;
|
|
714
833
|
const cellsTmp: { column: KanbanColumn; cards: KanbanCard[]; h: number }[] =
|
|
715
834
|
[];
|
|
716
835
|
|
|
717
836
|
for (const colInfo of columnXs) {
|
|
837
|
+
const isColCollapsed = collapsedColumns?.has(colInfo.column.id) ?? false;
|
|
718
838
|
const cards = bucket.cellsByColumn[colInfo.column.id] ?? [];
|
|
839
|
+
if (isColCollapsed) {
|
|
840
|
+
// Collapsed column cells get minimal height
|
|
841
|
+
cellsTmp.push({ column: colInfo.column, cards, h: 0 });
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
719
844
|
let h = 0;
|
|
720
845
|
for (const c of cards) {
|
|
721
|
-
h +=
|
|
846
|
+
h +=
|
|
847
|
+
computeCardHeight(c, parsed.tagGroups, hiddenMetaGroups) + CARD_GAP;
|
|
722
848
|
}
|
|
723
849
|
h = Math.max(h - (cards.length > 0 ? CARD_GAP : 0), 0);
|
|
724
850
|
cellsTmp.push({ column: colInfo.column, cards, h });
|
|
@@ -730,10 +856,14 @@ function computeSwimlaneLayout(
|
|
|
730
856
|
// Build cell layouts with card x/y offsets relative to (cell.x, laneY)
|
|
731
857
|
const cells: SwimlaneCellLayout[] = cellsTmp.map((tmp, i) => {
|
|
732
858
|
const colInfo = columnXs[i]!;
|
|
859
|
+
const isColCollapsed = collapsedColumns?.has(colInfo.column.id) ?? false;
|
|
860
|
+
if (isColCollapsed) {
|
|
861
|
+
return { column: tmp.column, cards: tmp.cards, cardLayouts: [] };
|
|
862
|
+
}
|
|
733
863
|
const cardLayouts: CardLayout[] = [];
|
|
734
864
|
let cy = 0;
|
|
735
865
|
for (const card of tmp.cards) {
|
|
736
|
-
const ch = computeCardHeight(card, parsed.tagGroups);
|
|
866
|
+
const ch = computeCardHeight(card, parsed.tagGroups, hiddenMetaGroups);
|
|
737
867
|
cardLayouts.push({
|
|
738
868
|
x: colInfo.x + COLUMN_PADDING,
|
|
739
869
|
y: laneY + cy,
|
|
@@ -766,11 +896,21 @@ function renderSwimlaneBoard(
|
|
|
766
896
|
swimlaneGroup: KanbanTagGroup,
|
|
767
897
|
palette: PaletteColors,
|
|
768
898
|
isDark: boolean,
|
|
769
|
-
activeTagGroup: string | null
|
|
899
|
+
activeTagGroup: string | null,
|
|
900
|
+
collapsedLanes?: Set<string>,
|
|
901
|
+
collapsedColumns?: Set<string>,
|
|
902
|
+
hiddenMetaGroups?: string[]
|
|
770
903
|
): void {
|
|
771
904
|
const visibleColumns = parsed.columns.filter((c) => !isArchiveColumn(c.name));
|
|
772
905
|
const buckets = bucketCardsBySwimlane(visibleColumns, swimlaneGroup);
|
|
773
|
-
const grid = computeSwimlaneLayout(
|
|
906
|
+
const grid = computeSwimlaneLayout(
|
|
907
|
+
parsed,
|
|
908
|
+
buckets,
|
|
909
|
+
baseLayout,
|
|
910
|
+
collapsedLanes,
|
|
911
|
+
collapsedColumns,
|
|
912
|
+
hiddenMetaGroups
|
|
913
|
+
);
|
|
774
914
|
|
|
775
915
|
// Resize the svg to fit the grid (only when not using exportDims).
|
|
776
916
|
const currentW = parseFloat(svg.attr('width') || '0');
|
|
@@ -789,6 +929,7 @@ function renderSwimlaneBoard(
|
|
|
789
929
|
// Column header row spanning all lanes
|
|
790
930
|
for (const colInfo of grid.columnXs) {
|
|
791
931
|
const col = colInfo.column;
|
|
932
|
+
const isColCollapsed = collapsedColumns?.has(col.id) ?? false;
|
|
792
933
|
const headerG = svg
|
|
793
934
|
.append('g')
|
|
794
935
|
.attr('class', 'kanban-column kanban-column-header')
|
|
@@ -808,21 +949,41 @@ function renderSwimlaneBoard(
|
|
|
808
949
|
.attr('rx', COLUMN_HEADER_RADIUS)
|
|
809
950
|
.attr('fill', colHeaderBg);
|
|
810
951
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
'
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
952
|
+
if (isColCollapsed) {
|
|
953
|
+
// Collapsed: show count + vertical name below header
|
|
954
|
+
headerG
|
|
955
|
+
.append('text')
|
|
956
|
+
.attr('x', colInfo.x + colInfo.width / 2)
|
|
957
|
+
.attr(
|
|
958
|
+
'y',
|
|
959
|
+
grid.startY + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
|
|
960
|
+
)
|
|
961
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
962
|
+
.attr('font-weight', 'bold')
|
|
963
|
+
.attr('fill', palette.textMuted)
|
|
964
|
+
.attr('text-anchor', 'middle')
|
|
965
|
+
.text(String(col.cards.length));
|
|
966
|
+
} else {
|
|
967
|
+
headerG
|
|
968
|
+
.append('text')
|
|
969
|
+
.attr('x', colInfo.x + COLUMN_PADDING)
|
|
970
|
+
.attr(
|
|
971
|
+
'y',
|
|
972
|
+
grid.startY +
|
|
973
|
+
COLUMN_HEADER_HEIGHT / 2 +
|
|
974
|
+
COLUMN_HEADER_FONT_SIZE / 2 -
|
|
975
|
+
2
|
|
976
|
+
)
|
|
977
|
+
.attr('font-size', COLUMN_HEADER_FONT_SIZE)
|
|
978
|
+
.attr('font-weight', 'bold')
|
|
979
|
+
.attr('fill', palette.text)
|
|
980
|
+
.text(col.name);
|
|
981
|
+
}
|
|
822
982
|
}
|
|
823
983
|
|
|
824
984
|
// Lanes
|
|
825
985
|
for (const lane of grid.lanes) {
|
|
986
|
+
const isLaneCollapsed = collapsedLanes?.has(lane.bucket.laneName) ?? false;
|
|
826
987
|
const laneG = svg
|
|
827
988
|
.append('g')
|
|
828
989
|
.attr('class', 'kanban-lane')
|
|
@@ -847,8 +1008,9 @@ function renderSwimlaneBoard(
|
|
|
847
1008
|
.attr('rx', COLUMN_RADIUS)
|
|
848
1009
|
.attr('fill', defaultColBg);
|
|
849
1010
|
|
|
850
|
-
// Lane label
|
|
1011
|
+
// Lane label + count
|
|
851
1012
|
let labelX = 10;
|
|
1013
|
+
const totalCards = lane.cells.reduce((s, c) => s + c.cards.length, 0);
|
|
852
1014
|
if (lane.bucket.laneColor) {
|
|
853
1015
|
headerG
|
|
854
1016
|
.append('circle')
|
|
@@ -858,24 +1020,74 @@ function renderSwimlaneBoard(
|
|
|
858
1020
|
.attr('fill', lane.bucket.laneColor);
|
|
859
1021
|
labelX += 14;
|
|
860
1022
|
}
|
|
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
1023
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1024
|
+
if (isLaneCollapsed) {
|
|
1025
|
+
// Collapsed: single line with name + count
|
|
1026
|
+
headerG
|
|
1027
|
+
.append('text')
|
|
1028
|
+
.attr('x', labelX)
|
|
1029
|
+
.attr('y', 20)
|
|
1030
|
+
.attr('font-size', 10)
|
|
1031
|
+
.attr('fill', palette.textMuted)
|
|
1032
|
+
.text(`${lane.bucket.laneName} (${totalCards})`);
|
|
1033
|
+
} else {
|
|
1034
|
+
// Expanded: name on first line, count on second
|
|
1035
|
+
headerG
|
|
1036
|
+
.append('text')
|
|
1037
|
+
.attr('x', labelX)
|
|
1038
|
+
.attr('y', 20)
|
|
1039
|
+
.attr('font-size', 12)
|
|
1040
|
+
.attr('font-weight', 'bold')
|
|
1041
|
+
.attr('fill', lane.bucket.isFallback ? palette.textMuted : palette.text)
|
|
1042
|
+
.text(lane.bucket.laneName);
|
|
1043
|
+
|
|
1044
|
+
headerG
|
|
1045
|
+
.append('text')
|
|
1046
|
+
.attr('x', labelX)
|
|
1047
|
+
.attr('y', 36)
|
|
1048
|
+
.attr('font-size', 10)
|
|
1049
|
+
.attr('fill', palette.textMuted)
|
|
1050
|
+
.text(`(${totalCards})`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (isLaneCollapsed) {
|
|
1054
|
+
// Render count placeholders in each cell
|
|
1055
|
+
for (const cell of lane.cells) {
|
|
1056
|
+
const isColCollapsed = collapsedColumns?.has(cell.column.id) ?? false;
|
|
1057
|
+
if (cell.cards.length > 0) {
|
|
1058
|
+
const colInfo = grid.columnXs.find(
|
|
1059
|
+
(c) => c.column.id === cell.column.id
|
|
1060
|
+
);
|
|
1061
|
+
if (!colInfo) continue;
|
|
1062
|
+
const placeholderBg = lane.bucket.laneColor
|
|
1063
|
+
? mix(lane.bucket.laneColor, palette.bg, isDark ? 40 : 28)
|
|
1064
|
+
: mix(palette.textMuted, palette.bg, isDark ? 28 : 22);
|
|
1065
|
+
const pw = isColCollapsed
|
|
1066
|
+
? COLLAPSED_COLUMN_WIDTH - 8
|
|
1067
|
+
: colInfo.width - COLUMN_PADDING * 2;
|
|
1068
|
+
laneG
|
|
1069
|
+
.append('rect')
|
|
1070
|
+
.attr('x', colInfo.x + (isColCollapsed ? 4 : COLUMN_PADDING))
|
|
1071
|
+
.attr('y', lane.y)
|
|
1072
|
+
.attr('width', pw)
|
|
1073
|
+
.attr('height', 18)
|
|
1074
|
+
.attr('rx', 4)
|
|
1075
|
+
.attr('fill', placeholderBg);
|
|
1076
|
+
laneG
|
|
1077
|
+
.append('text')
|
|
1078
|
+
.attr(
|
|
1079
|
+
'x',
|
|
1080
|
+
colInfo.x + (isColCollapsed ? 4 : COLUMN_PADDING) + pw / 2
|
|
1081
|
+
)
|
|
1082
|
+
.attr('y', lane.y + 13)
|
|
1083
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
1084
|
+
.attr('font-weight', 'bold')
|
|
1085
|
+
.attr('fill', palette.textMuted)
|
|
1086
|
+
.attr('text-anchor', 'middle')
|
|
1087
|
+
.text(String(cell.cards.length));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
879
1091
|
|
|
880
1092
|
// Lane divider line below the row
|
|
881
1093
|
laneG
|
|
@@ -888,17 +1100,50 @@ function renderSwimlaneBoard(
|
|
|
888
1100
|
.attr('stroke-opacity', 0.4)
|
|
889
1101
|
.attr('stroke-width', 1);
|
|
890
1102
|
|
|
891
|
-
// Cards inside cells
|
|
892
|
-
|
|
893
|
-
for (const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1103
|
+
// Cards inside cells (skip for collapsed lanes — they show count placeholders instead)
|
|
1104
|
+
if (!isLaneCollapsed) {
|
|
1105
|
+
for (const cell of lane.cells) {
|
|
1106
|
+
const isColCollapsed = collapsedColumns?.has(cell.column.id) ?? false;
|
|
1107
|
+
if (isColCollapsed && cell.cards.length > 0) {
|
|
1108
|
+
// Collapsed column in non-collapsed lane: show count placeholder
|
|
1109
|
+
const colInfo = grid.columnXs.find(
|
|
1110
|
+
(c) => c.column.id === cell.column.id
|
|
1111
|
+
);
|
|
1112
|
+
if (colInfo) {
|
|
1113
|
+
const placeholderBg = lane.bucket.laneColor
|
|
1114
|
+
? mix(lane.bucket.laneColor, palette.bg, isDark ? 40 : 28)
|
|
1115
|
+
: mix(palette.textMuted, palette.bg, isDark ? 28 : 22);
|
|
1116
|
+
laneG
|
|
1117
|
+
.append('rect')
|
|
1118
|
+
.attr('x', colInfo.x + 4)
|
|
1119
|
+
.attr('y', lane.y)
|
|
1120
|
+
.attr('width', COLLAPSED_COLUMN_WIDTH - 8)
|
|
1121
|
+
.attr('height', 22)
|
|
1122
|
+
.attr('rx', 4)
|
|
1123
|
+
.attr('fill', placeholderBg);
|
|
1124
|
+
laneG
|
|
1125
|
+
.append('text')
|
|
1126
|
+
.attr('x', colInfo.x + COLLAPSED_COLUMN_WIDTH / 2)
|
|
1127
|
+
.attr('y', lane.y + 16)
|
|
1128
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
1129
|
+
.attr('font-weight', 'bold')
|
|
1130
|
+
.attr('fill', palette.textMuted)
|
|
1131
|
+
.attr('text-anchor', 'middle')
|
|
1132
|
+
.text(String(cell.cards.length));
|
|
1133
|
+
}
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
for (const cardLayout of cell.cardLayouts) {
|
|
1137
|
+
renderSwimlaneCard(
|
|
1138
|
+
laneG,
|
|
1139
|
+
cardLayout,
|
|
1140
|
+
parsed.tagGroups,
|
|
1141
|
+
activeTagGroup,
|
|
1142
|
+
palette,
|
|
1143
|
+
cardBaseBg,
|
|
1144
|
+
hiddenMetaGroups
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
902
1147
|
}
|
|
903
1148
|
}
|
|
904
1149
|
}
|
|
@@ -910,11 +1155,12 @@ function renderSwimlaneCard(
|
|
|
910
1155
|
tagGroups: KanbanTagGroup[],
|
|
911
1156
|
activeTagGroup: string | null,
|
|
912
1157
|
palette: PaletteColors,
|
|
913
|
-
cardBaseBg: string
|
|
1158
|
+
cardBaseBg: string,
|
|
1159
|
+
hiddenMetaGroups?: string[]
|
|
914
1160
|
): void {
|
|
915
1161
|
const card = cardLayout.card;
|
|
916
1162
|
const resolvedColor = resolveCardTagColor(card, tagGroups, activeTagGroup);
|
|
917
|
-
const tagMeta = resolveCardTagMeta(card, tagGroups);
|
|
1163
|
+
const tagMeta = resolveCardTagMeta(card, tagGroups, hiddenMetaGroups);
|
|
918
1164
|
const hasMeta = tagMeta.length > 0 || card.details.length > 0;
|
|
919
1165
|
|
|
920
1166
|
const cardFill = resolvedColor
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Mindmap Collapse/Expand — prune subtrees of collapsed nodes
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import type { MindmapNode } from './types';
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
export interface CollapsedMindmapResult {
|
|
12
|
+
/** Roots with collapsed subtrees pruned (deep-cloned, never mutates original) */
|
|
13
|
+
roots: MindmapNode[];
|
|
14
|
+
/** nodeId → count of hidden descendants */
|
|
15
|
+
hiddenCounts: Map<string, number>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Helpers
|
|
20
|
+
// ============================================================
|
|
21
|
+
|
|
22
|
+
function cloneNode(node: MindmapNode): MindmapNode {
|
|
23
|
+
return {
|
|
24
|
+
id: node.id,
|
|
25
|
+
label: node.label,
|
|
26
|
+
description: node.description,
|
|
27
|
+
metadata: { ...node.metadata },
|
|
28
|
+
children: node.children.map(cloneNode),
|
|
29
|
+
parentId: node.parentId,
|
|
30
|
+
lineNumber: node.lineNumber,
|
|
31
|
+
color: node.color,
|
|
32
|
+
collapsed: node.collapsed,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function countDescendants(node: MindmapNode): number {
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (const child of node.children) {
|
|
39
|
+
count += 1 + countDescendants(child);
|
|
40
|
+
}
|
|
41
|
+
return count;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function computeHiddenCounts(
|
|
45
|
+
nodes: MindmapNode[],
|
|
46
|
+
collapsedIds: Set<string>,
|
|
47
|
+
hiddenCounts: Map<string, number>
|
|
48
|
+
): void {
|
|
49
|
+
for (const node of nodes) {
|
|
50
|
+
if (collapsedIds.has(node.id) && node.children.length > 0) {
|
|
51
|
+
hiddenCounts.set(node.id, countDescendants(node));
|
|
52
|
+
}
|
|
53
|
+
computeHiddenCounts(node.children, collapsedIds, hiddenCounts);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pruneCollapsed(node: MindmapNode, collapsedIds: Set<string>): void {
|
|
58
|
+
for (const child of node.children) {
|
|
59
|
+
pruneCollapsed(child, collapsedIds);
|
|
60
|
+
}
|
|
61
|
+
if (collapsedIds.has(node.id) && node.children.length > 0) {
|
|
62
|
+
node.children = [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================
|
|
67
|
+
// Main
|
|
68
|
+
// ============================================================
|
|
69
|
+
|
|
70
|
+
export function collapseMindmapTree(
|
|
71
|
+
roots: MindmapNode[],
|
|
72
|
+
collapsedIds: Set<string>
|
|
73
|
+
): CollapsedMindmapResult {
|
|
74
|
+
const hiddenCounts = new Map<string, number>();
|
|
75
|
+
|
|
76
|
+
if (collapsedIds.size === 0) {
|
|
77
|
+
return { roots, hiddenCounts };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
computeHiddenCounts(roots, collapsedIds, hiddenCounts);
|
|
81
|
+
|
|
82
|
+
const clonedRoots = roots.map(cloneNode);
|
|
83
|
+
for (const root of clonedRoots) {
|
|
84
|
+
pruneCollapsed(root, collapsedIds);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { roots: clonedRoots, hiddenCounts };
|
|
88
|
+
}
|