@diagrammo/dgmo 0.8.14 → 0.8.16
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 +101 -101
- package/dist/index.cjs +623 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -9
- package/dist/index.d.ts +41 -9
- package/dist/index.js +620 -119
- package/dist/index.js.map +1 -1
- package/docs/guide/colors.md +26 -22
- package/docs/language-reference.md +3 -1
- package/package.json +1 -1
- package/src/chart.ts +22 -4
- package/src/class/parser.ts +9 -2
- package/src/colors.ts +64 -8
- package/src/d3.ts +71 -34
- package/src/dgmo-mermaid.ts +8 -2
- package/src/echarts.ts +19 -4
- package/src/er/parser.ts +12 -4
- package/src/graph/flowchart-parser.ts +36 -6
- package/src/graph/state-parser.ts +34 -6
- package/src/index.ts +9 -1
- package/src/infra/parser.ts +7 -1
- package/src/kanban/parser.ts +17 -5
- package/src/kanban/renderer.ts +524 -8
- package/src/palettes/bold.ts +4 -0
- package/src/palettes/catppuccin.ts +4 -0
- package/src/palettes/dracula.ts +4 -0
- package/src/palettes/gruvbox.ts +4 -0
- package/src/palettes/monokai.ts +4 -0
- package/src/palettes/nord.ts +4 -0
- package/src/palettes/one-dark.ts +4 -0
- package/src/palettes/rose-pine.ts +4 -0
- package/src/palettes/solarized.ts +4 -0
- package/src/palettes/tokyo-night.ts +4 -0
- package/src/palettes/types.ts +2 -0
- package/src/sequence/parser.ts +6 -1
- package/src/sequence/renderer.ts +2 -6
- package/src/sharing.ts +11 -3
- package/src/sitemap/parser.ts +43 -10
- package/src/utils/duration.ts +10 -1
- package/src/utils/parsing.ts +22 -6
package/src/kanban/renderer.ts
CHANGED
|
@@ -17,7 +17,24 @@ import { parseKanban } from './parser';
|
|
|
17
17
|
import { isArchiveColumn } from './mutations';
|
|
18
18
|
import { LEGEND_HEIGHT, measureLegendText } from '../utils/legend-constants';
|
|
19
19
|
import { renderLegendD3 } from '../utils/legend-d3';
|
|
20
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
LegendConfig,
|
|
22
|
+
LegendState,
|
|
23
|
+
LegendCallbacks,
|
|
24
|
+
D3Sel,
|
|
25
|
+
} from '../utils/legend-types';
|
|
26
|
+
|
|
27
|
+
// ============================================================
|
|
28
|
+
// Public options object
|
|
29
|
+
// ============================================================
|
|
30
|
+
|
|
31
|
+
export interface KanbanInteractiveOptions {
|
|
32
|
+
onNavigateToLine?: (line: number) => void;
|
|
33
|
+
exportDims?: { width: number; height: number };
|
|
34
|
+
activeTagGroup?: string | null;
|
|
35
|
+
currentSwimlaneGroup?: string | null;
|
|
36
|
+
onSwimlaneChange?: (group: string | null) => void;
|
|
37
|
+
}
|
|
21
38
|
|
|
22
39
|
// ============================================================
|
|
23
40
|
// Constants
|
|
@@ -214,10 +231,19 @@ export function renderKanban(
|
|
|
214
231
|
parsed: ParsedKanban,
|
|
215
232
|
palette: PaletteColors,
|
|
216
233
|
isDark: boolean,
|
|
217
|
-
|
|
218
|
-
exportDims?: { width: number; height: number },
|
|
219
|
-
activeTagGroup?: string | null
|
|
234
|
+
options?: KanbanInteractiveOptions
|
|
220
235
|
): void {
|
|
236
|
+
const exportDims = options?.exportDims;
|
|
237
|
+
const activeTagGroup = options?.activeTagGroup ?? null;
|
|
238
|
+
const onSwimlaneChange = options?.onSwimlaneChange;
|
|
239
|
+
// Resolve current swimlane group: must match an existing tag group, else ignore.
|
|
240
|
+
const requestedSwimlane = options?.currentSwimlaneGroup ?? null;
|
|
241
|
+
const swimlaneGroup = requestedSwimlane
|
|
242
|
+
? (parsed.tagGroups.find(
|
|
243
|
+
(g) => g.name.toLowerCase() === requestedSwimlane.toLowerCase()
|
|
244
|
+
) ?? null)
|
|
245
|
+
: null;
|
|
246
|
+
|
|
221
247
|
const layout = computeLayout(parsed, palette);
|
|
222
248
|
|
|
223
249
|
const width = exportDims?.width ?? layout.totalWidth;
|
|
@@ -265,17 +291,67 @@ export function renderKanban(
|
|
|
265
291
|
.append('g')
|
|
266
292
|
.attr('class', 'kanban-legend')
|
|
267
293
|
.attr('transform', `translate(${legendX},${legendY})`);
|
|
294
|
+
|
|
295
|
+
// Only show ≡ swimlane icons in interactive contexts (callback present
|
|
296
|
+
// and not exporting). Static SVG/PNG exports get no icon.
|
|
297
|
+
const showSwimlaneIcon = !!onSwimlaneChange && !exportDims;
|
|
298
|
+
|
|
299
|
+
const legendCallbacks: LegendCallbacks | undefined = showSwimlaneIcon
|
|
300
|
+
? {
|
|
301
|
+
onGroupRendered: (groupName, groupEl, isActive) => {
|
|
302
|
+
const isCurrent =
|
|
303
|
+
swimlaneGroup?.name.toLowerCase() === groupName.toLowerCase();
|
|
304
|
+
// Position icon at the trailing edge of the pill text.
|
|
305
|
+
// For collapsed pills, the groupEl is at (pill.x, pill.y) and the
|
|
306
|
+
// pill rect spans pill.width. For active capsule, the groupEl is at
|
|
307
|
+
// (capsule.x, capsule.y) and the inner pill is offset.
|
|
308
|
+
// We measure text and place the icon just to the right of it.
|
|
309
|
+
const textW = measureLegendText(groupName, 11);
|
|
310
|
+
const iconX = isActive
|
|
311
|
+
? 4 + 6 + textW + 6 // capsule pad + pill pad/2 + text + gap
|
|
312
|
+
: 8 + textW + 4; // pill pad/2 + text + gap
|
|
313
|
+
const iconY = (LEGEND_HEIGHT - 7) / 2;
|
|
314
|
+
const iconEl = drawSwimlaneIcon(
|
|
315
|
+
groupEl,
|
|
316
|
+
iconX,
|
|
317
|
+
iconY,
|
|
318
|
+
isCurrent,
|
|
319
|
+
palette
|
|
320
|
+
);
|
|
321
|
+
iconEl.append('title').text(`Group by ${groupName}`);
|
|
322
|
+
iconEl.style('cursor', 'pointer').on('click', (event: Event) => {
|
|
323
|
+
event.stopPropagation();
|
|
324
|
+
onSwimlaneChange?.(isCurrent ? null : groupName);
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
: undefined;
|
|
329
|
+
|
|
268
330
|
renderLegendD3(
|
|
269
331
|
legendG,
|
|
270
332
|
legendConfig,
|
|
271
333
|
legendState,
|
|
272
334
|
palette,
|
|
273
335
|
isDark,
|
|
274
|
-
|
|
336
|
+
legendCallbacks,
|
|
275
337
|
width - legendX - DIAGRAM_PADDING
|
|
276
338
|
);
|
|
277
339
|
}
|
|
278
340
|
|
|
341
|
+
// Swimlane mode: bucket cards into (column, lane) cells and render grid
|
|
342
|
+
if (swimlaneGroup) {
|
|
343
|
+
renderSwimlaneBoard(
|
|
344
|
+
svg,
|
|
345
|
+
parsed,
|
|
346
|
+
layout,
|
|
347
|
+
swimlaneGroup,
|
|
348
|
+
palette,
|
|
349
|
+
isDark,
|
|
350
|
+
activeTagGroup
|
|
351
|
+
);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
279
355
|
// Columns
|
|
280
356
|
const defaultColBg = isDark
|
|
281
357
|
? mix(palette.surface, palette.bg, 50)
|
|
@@ -477,11 +553,451 @@ export function renderKanbanForExport(
|
|
|
477
553
|
const layout = computeLayout(parsed, palette);
|
|
478
554
|
|
|
479
555
|
const container = document.createElement('div');
|
|
480
|
-
renderKanban(container, parsed, palette, isDark,
|
|
481
|
-
width: layout.totalWidth,
|
|
482
|
-
height: layout.totalHeight,
|
|
556
|
+
renderKanban(container, parsed, palette, isDark, {
|
|
557
|
+
exportDims: { width: layout.totalWidth, height: layout.totalHeight },
|
|
483
558
|
});
|
|
484
559
|
|
|
485
560
|
const svgEl = container.querySelector('svg');
|
|
486
561
|
return svgEl?.outerHTML ?? '';
|
|
487
562
|
}
|
|
563
|
+
|
|
564
|
+
// ============================================================
|
|
565
|
+
// Swimlane: icon, bucketing, layout, render
|
|
566
|
+
// ============================================================
|
|
567
|
+
|
|
568
|
+
const LANE_HEADER_WIDTH = 140;
|
|
569
|
+
const LANE_GAP = 14;
|
|
570
|
+
|
|
571
|
+
function drawSwimlaneIcon(
|
|
572
|
+
parent: D3Sel,
|
|
573
|
+
x: number,
|
|
574
|
+
y: number,
|
|
575
|
+
isActive: boolean,
|
|
576
|
+
palette: PaletteColors
|
|
577
|
+
): D3Sel {
|
|
578
|
+
const iconG = parent
|
|
579
|
+
.append('g')
|
|
580
|
+
.attr('class', 'kanban-swimlane-icon')
|
|
581
|
+
.attr('transform', `translate(${x}, ${y})`);
|
|
582
|
+
|
|
583
|
+
const color = isActive ? palette.primary : palette.textMuted;
|
|
584
|
+
const opacity = isActive ? 1 : 0.35;
|
|
585
|
+
const barWidths = [8, 12, 6];
|
|
586
|
+
const barH = 2;
|
|
587
|
+
const gap = 3;
|
|
588
|
+
|
|
589
|
+
for (let i = 0; i < barWidths.length; i++) {
|
|
590
|
+
iconG
|
|
591
|
+
.append('rect')
|
|
592
|
+
.attr('x', 0)
|
|
593
|
+
.attr('y', i * gap)
|
|
594
|
+
.attr('width', barWidths[i]!)
|
|
595
|
+
.attr('height', barH)
|
|
596
|
+
.attr('rx', 1)
|
|
597
|
+
.attr('fill', color)
|
|
598
|
+
.attr('opacity', opacity);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return iconG;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
interface SwimlaneBucket {
|
|
605
|
+
laneName: string;
|
|
606
|
+
laneColor?: string;
|
|
607
|
+
isFallback: boolean;
|
|
608
|
+
cellsByColumn: Record<string, KanbanCard[]>;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function bucketCardsBySwimlane(
|
|
612
|
+
columns: KanbanColumn[],
|
|
613
|
+
swimlaneGroup: KanbanTagGroup
|
|
614
|
+
): SwimlaneBucket[] {
|
|
615
|
+
const tagKey = swimlaneGroup.name.toLowerCase();
|
|
616
|
+
const buckets = new Map<string, SwimlaneBucket>();
|
|
617
|
+
|
|
618
|
+
// Seed lanes from declaration order
|
|
619
|
+
for (const entry of swimlaneGroup.entries) {
|
|
620
|
+
buckets.set(entry.value.toLowerCase(), {
|
|
621
|
+
laneName: entry.value,
|
|
622
|
+
laneColor: entry.color,
|
|
623
|
+
isFallback: false,
|
|
624
|
+
cellsByColumn: {},
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const fallbackKey = `__no_${tagKey}__`;
|
|
629
|
+
|
|
630
|
+
for (const col of columns) {
|
|
631
|
+
for (const card of col.cards) {
|
|
632
|
+
const raw = card.tags[tagKey] ?? swimlaneGroup.defaultValue;
|
|
633
|
+
let bucketKey: string;
|
|
634
|
+
if (raw && buckets.has(raw.toLowerCase())) {
|
|
635
|
+
bucketKey = raw.toLowerCase();
|
|
636
|
+
} else {
|
|
637
|
+
if (!buckets.has(fallbackKey)) {
|
|
638
|
+
buckets.set(fallbackKey, {
|
|
639
|
+
laneName: `No ${swimlaneGroup.name}`,
|
|
640
|
+
isFallback: true,
|
|
641
|
+
cellsByColumn: {},
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
bucketKey = fallbackKey;
|
|
645
|
+
}
|
|
646
|
+
const bucket = buckets.get(bucketKey)!;
|
|
647
|
+
(bucket.cellsByColumn[col.id] ??= []).push(card);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Drop empty seeded lanes that have no cards (and aren't fallback).
|
|
652
|
+
// Phase A choice: keep entries even if empty so users see the structure.
|
|
653
|
+
return Array.from(buckets.values());
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
interface SwimlaneCellLayout {
|
|
657
|
+
column: KanbanColumn;
|
|
658
|
+
cards: KanbanCard[];
|
|
659
|
+
cardLayouts: CardLayout[];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
interface SwimlaneLaneLayout {
|
|
663
|
+
bucket: SwimlaneBucket;
|
|
664
|
+
y: number;
|
|
665
|
+
height: number;
|
|
666
|
+
cells: SwimlaneCellLayout[];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
interface SwimlaneBoardLayout {
|
|
670
|
+
columnXs: { column: KanbanColumn; x: number; width: number }[];
|
|
671
|
+
lanes: SwimlaneLaneLayout[];
|
|
672
|
+
totalWidth: number;
|
|
673
|
+
totalHeight: number;
|
|
674
|
+
startY: number;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function computeCardHeight(card: KanbanCard, tagGroups: KanbanTagGroup[]) {
|
|
678
|
+
const tagMeta = resolveCardTagMeta(card, tagGroups);
|
|
679
|
+
const metaCount = tagMeta.length + card.details.length;
|
|
680
|
+
const metaHeight =
|
|
681
|
+
metaCount > 0
|
|
682
|
+
? CARD_SEPARATOR_GAP +
|
|
683
|
+
1 +
|
|
684
|
+
CARD_PADDING_Y +
|
|
685
|
+
metaCount * CARD_META_LINE_HEIGHT
|
|
686
|
+
: 0;
|
|
687
|
+
return CARD_HEADER_HEIGHT + CARD_PADDING_Y + metaHeight;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function computeSwimlaneLayout(
|
|
691
|
+
parsed: ParsedKanban,
|
|
692
|
+
buckets: SwimlaneBucket[],
|
|
693
|
+
baseLayout: { columns: ColumnLayout[] }
|
|
694
|
+
): SwimlaneBoardLayout {
|
|
695
|
+
const headerHeight = parsed.title ? TITLE_HEIGHT + 8 : 0;
|
|
696
|
+
const startY = DIAGRAM_PADDING + headerHeight;
|
|
697
|
+
|
|
698
|
+
// Column x positions: shift right by LANE_HEADER_WIDTH, reuse widths from base layout.
|
|
699
|
+
const columnXs: SwimlaneBoardLayout['columnXs'] = [];
|
|
700
|
+
let currentX = DIAGRAM_PADDING + LANE_HEADER_WIDTH;
|
|
701
|
+
for (const col of baseLayout.columns) {
|
|
702
|
+
columnXs.push({ column: col.column, x: currentX, width: col.width });
|
|
703
|
+
currentX += col.width + COLUMN_GAP;
|
|
704
|
+
}
|
|
705
|
+
const totalWidth = currentX - COLUMN_GAP + DIAGRAM_PADDING;
|
|
706
|
+
|
|
707
|
+
// For each lane, compute card stacks per column and normalize the lane height.
|
|
708
|
+
const lanes: SwimlaneLaneLayout[] = [];
|
|
709
|
+
let laneY = startY + COLUMN_HEADER_HEIGHT + COLUMN_PADDING;
|
|
710
|
+
const minCellH = CARD_HEADER_HEIGHT + CARD_PADDING_Y + CARD_GAP;
|
|
711
|
+
|
|
712
|
+
for (const bucket of buckets) {
|
|
713
|
+
let maxCellH = minCellH;
|
|
714
|
+
const cellsTmp: { column: KanbanColumn; cards: KanbanCard[]; h: number }[] =
|
|
715
|
+
[];
|
|
716
|
+
|
|
717
|
+
for (const colInfo of columnXs) {
|
|
718
|
+
const cards = bucket.cellsByColumn[colInfo.column.id] ?? [];
|
|
719
|
+
let h = 0;
|
|
720
|
+
for (const c of cards) {
|
|
721
|
+
h += computeCardHeight(c, parsed.tagGroups) + CARD_GAP;
|
|
722
|
+
}
|
|
723
|
+
h = Math.max(h - (cards.length > 0 ? CARD_GAP : 0), 0);
|
|
724
|
+
cellsTmp.push({ column: colInfo.column, cards, h });
|
|
725
|
+
if (h > maxCellH) maxCellH = h;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const laneHeight = Math.max(maxCellH, minCellH);
|
|
729
|
+
|
|
730
|
+
// Build cell layouts with card x/y offsets relative to (cell.x, laneY)
|
|
731
|
+
const cells: SwimlaneCellLayout[] = cellsTmp.map((tmp, i) => {
|
|
732
|
+
const colInfo = columnXs[i]!;
|
|
733
|
+
const cardLayouts: CardLayout[] = [];
|
|
734
|
+
let cy = 0;
|
|
735
|
+
for (const card of tmp.cards) {
|
|
736
|
+
const ch = computeCardHeight(card, parsed.tagGroups);
|
|
737
|
+
cardLayouts.push({
|
|
738
|
+
x: colInfo.x + COLUMN_PADDING,
|
|
739
|
+
y: laneY + cy,
|
|
740
|
+
width: colInfo.width - COLUMN_PADDING * 2,
|
|
741
|
+
height: ch,
|
|
742
|
+
card,
|
|
743
|
+
});
|
|
744
|
+
cy += ch + CARD_GAP;
|
|
745
|
+
}
|
|
746
|
+
return { column: tmp.column, cards: tmp.cards, cardLayouts };
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
lanes.push({ bucket, y: laneY, height: laneHeight, cells });
|
|
750
|
+
laneY += laneHeight + LANE_GAP;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const totalHeight = laneY - LANE_GAP + COLUMN_PADDING + DIAGRAM_PADDING;
|
|
754
|
+
|
|
755
|
+
return { columnXs, lanes, totalWidth, totalHeight, startY };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function renderSwimlaneBoard(
|
|
759
|
+
svg: D3Sel,
|
|
760
|
+
parsed: ParsedKanban,
|
|
761
|
+
baseLayout: {
|
|
762
|
+
columns: ColumnLayout[];
|
|
763
|
+
totalWidth: number;
|
|
764
|
+
totalHeight: number;
|
|
765
|
+
},
|
|
766
|
+
swimlaneGroup: KanbanTagGroup,
|
|
767
|
+
palette: PaletteColors,
|
|
768
|
+
isDark: boolean,
|
|
769
|
+
activeTagGroup: string | null
|
|
770
|
+
): void {
|
|
771
|
+
const visibleColumns = parsed.columns.filter((c) => !isArchiveColumn(c.name));
|
|
772
|
+
const buckets = bucketCardsBySwimlane(visibleColumns, swimlaneGroup);
|
|
773
|
+
const grid = computeSwimlaneLayout(parsed, buckets, baseLayout);
|
|
774
|
+
|
|
775
|
+
// Resize the svg to fit the grid (only when not using exportDims).
|
|
776
|
+
const currentW = parseFloat(svg.attr('width') || '0');
|
|
777
|
+
const currentH = parseFloat(svg.attr('height') || '0');
|
|
778
|
+
if (grid.totalWidth > currentW) svg.attr('width', grid.totalWidth);
|
|
779
|
+
if (grid.totalHeight > currentH) svg.attr('height', grid.totalHeight);
|
|
780
|
+
|
|
781
|
+
const defaultColBg = isDark
|
|
782
|
+
? mix(palette.surface, palette.bg, 50)
|
|
783
|
+
: mix(palette.surface, palette.bg, 30);
|
|
784
|
+
const defaultColHeaderBg = isDark
|
|
785
|
+
? mix(palette.surface, palette.bg, 70)
|
|
786
|
+
: mix(palette.surface, palette.bg, 50);
|
|
787
|
+
const cardBaseBg = isDark ? palette.surface : palette.bg;
|
|
788
|
+
|
|
789
|
+
// Column header row spanning all lanes
|
|
790
|
+
for (const colInfo of grid.columnXs) {
|
|
791
|
+
const col = colInfo.column;
|
|
792
|
+
const headerG = svg
|
|
793
|
+
.append('g')
|
|
794
|
+
.attr('class', 'kanban-column kanban-column-header')
|
|
795
|
+
.attr('data-column-id', col.id)
|
|
796
|
+
.attr('data-line-number', col.lineNumber);
|
|
797
|
+
|
|
798
|
+
const colHeaderBg = col.color
|
|
799
|
+
? mix(col.color, palette.bg, 25)
|
|
800
|
+
: defaultColHeaderBg;
|
|
801
|
+
|
|
802
|
+
headerG
|
|
803
|
+
.append('rect')
|
|
804
|
+
.attr('x', colInfo.x)
|
|
805
|
+
.attr('y', grid.startY)
|
|
806
|
+
.attr('width', colInfo.width)
|
|
807
|
+
.attr('height', COLUMN_HEADER_HEIGHT)
|
|
808
|
+
.attr('rx', COLUMN_HEADER_RADIUS)
|
|
809
|
+
.attr('fill', colHeaderBg);
|
|
810
|
+
|
|
811
|
+
headerG
|
|
812
|
+
.append('text')
|
|
813
|
+
.attr('x', colInfo.x + COLUMN_PADDING)
|
|
814
|
+
.attr(
|
|
815
|
+
'y',
|
|
816
|
+
grid.startY + COLUMN_HEADER_HEIGHT / 2 + COLUMN_HEADER_FONT_SIZE / 2 - 2
|
|
817
|
+
)
|
|
818
|
+
.attr('font-size', COLUMN_HEADER_FONT_SIZE)
|
|
819
|
+
.attr('font-weight', 'bold')
|
|
820
|
+
.attr('fill', palette.text)
|
|
821
|
+
.text(col.name);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Lanes
|
|
825
|
+
for (const lane of grid.lanes) {
|
|
826
|
+
const laneG = svg
|
|
827
|
+
.append('g')
|
|
828
|
+
.attr('class', 'kanban-lane')
|
|
829
|
+
.attr('data-lane-name', lane.bucket.laneName);
|
|
830
|
+
|
|
831
|
+
// Lane header on the left edge
|
|
832
|
+
const headerG = laneG
|
|
833
|
+
.append('g')
|
|
834
|
+
.attr('class', 'kanban-lane-header')
|
|
835
|
+
.attr(
|
|
836
|
+
'transform',
|
|
837
|
+
`translate(${DIAGRAM_PADDING}, ${lane.y - COLUMN_PADDING})`
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
// Background band spanning the row
|
|
841
|
+
headerG
|
|
842
|
+
.append('rect')
|
|
843
|
+
.attr('x', 0)
|
|
844
|
+
.attr('y', 0)
|
|
845
|
+
.attr('width', LANE_HEADER_WIDTH - 8)
|
|
846
|
+
.attr('height', lane.height + COLUMN_PADDING * 2)
|
|
847
|
+
.attr('rx', COLUMN_RADIUS)
|
|
848
|
+
.attr('fill', defaultColBg);
|
|
849
|
+
|
|
850
|
+
// Lane label
|
|
851
|
+
let labelX = 10;
|
|
852
|
+
if (lane.bucket.laneColor) {
|
|
853
|
+
headerG
|
|
854
|
+
.append('circle')
|
|
855
|
+
.attr('cx', labelX + 4)
|
|
856
|
+
.attr('cy', 16)
|
|
857
|
+
.attr('r', 4)
|
|
858
|
+
.attr('fill', lane.bucket.laneColor);
|
|
859
|
+
labelX += 14;
|
|
860
|
+
}
|
|
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
|
+
|
|
870
|
+
// Card count
|
|
871
|
+
const totalCards = lane.cells.reduce((s, c) => s + c.cards.length, 0);
|
|
872
|
+
headerG
|
|
873
|
+
.append('text')
|
|
874
|
+
.attr('x', labelX)
|
|
875
|
+
.attr('y', 36)
|
|
876
|
+
.attr('font-size', 10)
|
|
877
|
+
.attr('fill', palette.textMuted)
|
|
878
|
+
.text(`(${totalCards})`);
|
|
879
|
+
|
|
880
|
+
// Lane divider line below the row
|
|
881
|
+
laneG
|
|
882
|
+
.append('line')
|
|
883
|
+
.attr('x1', DIAGRAM_PADDING + LANE_HEADER_WIDTH)
|
|
884
|
+
.attr('x2', grid.totalWidth - DIAGRAM_PADDING)
|
|
885
|
+
.attr('y1', lane.y + lane.height + COLUMN_PADDING)
|
|
886
|
+
.attr('y2', lane.y + lane.height + COLUMN_PADDING)
|
|
887
|
+
.attr('stroke', palette.border ?? palette.textMuted)
|
|
888
|
+
.attr('stroke-opacity', 0.4)
|
|
889
|
+
.attr('stroke-width', 1);
|
|
890
|
+
|
|
891
|
+
// Cards inside cells
|
|
892
|
+
for (const cell of lane.cells) {
|
|
893
|
+
for (const cardLayout of cell.cardLayouts) {
|
|
894
|
+
renderSwimlaneCard(
|
|
895
|
+
laneG,
|
|
896
|
+
cardLayout,
|
|
897
|
+
parsed.tagGroups,
|
|
898
|
+
activeTagGroup,
|
|
899
|
+
palette,
|
|
900
|
+
cardBaseBg
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function renderSwimlaneCard(
|
|
908
|
+
parent: D3Sel,
|
|
909
|
+
cardLayout: CardLayout,
|
|
910
|
+
tagGroups: KanbanTagGroup[],
|
|
911
|
+
activeTagGroup: string | null,
|
|
912
|
+
palette: PaletteColors,
|
|
913
|
+
cardBaseBg: string
|
|
914
|
+
): void {
|
|
915
|
+
const card = cardLayout.card;
|
|
916
|
+
const resolvedColor = resolveCardTagColor(card, tagGroups, activeTagGroup);
|
|
917
|
+
const tagMeta = resolveCardTagMeta(card, tagGroups);
|
|
918
|
+
const hasMeta = tagMeta.length > 0 || card.details.length > 0;
|
|
919
|
+
|
|
920
|
+
const cardFill = resolvedColor
|
|
921
|
+
? mix(resolvedColor, cardBaseBg, 15)
|
|
922
|
+
: mix(palette.primary, cardBaseBg, 15);
|
|
923
|
+
const cardStroke = resolvedColor ?? palette.textMuted;
|
|
924
|
+
|
|
925
|
+
const cg = parent
|
|
926
|
+
.append('g')
|
|
927
|
+
.attr('class', 'kanban-card')
|
|
928
|
+
.attr('data-card-id', card.id)
|
|
929
|
+
.attr('data-line-number', card.lineNumber);
|
|
930
|
+
|
|
931
|
+
if (activeTagGroup) {
|
|
932
|
+
const tagKey = activeTagGroup.toLowerCase();
|
|
933
|
+
const group = tagGroups.find((tg) => tg.name.toLowerCase() === tagKey);
|
|
934
|
+
const value = card.tags[tagKey] ?? group?.defaultValue;
|
|
935
|
+
if (value) {
|
|
936
|
+
cg.attr(`data-tag-${tagKey}`, value.toLowerCase());
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const cx = cardLayout.x;
|
|
941
|
+
const cy = cardLayout.y;
|
|
942
|
+
|
|
943
|
+
cg.append('rect')
|
|
944
|
+
.attr('x', cx)
|
|
945
|
+
.attr('y', cy)
|
|
946
|
+
.attr('width', cardLayout.width)
|
|
947
|
+
.attr('height', cardLayout.height)
|
|
948
|
+
.attr('rx', CARD_RADIUS)
|
|
949
|
+
.attr('fill', cardFill)
|
|
950
|
+
.attr('stroke', cardStroke)
|
|
951
|
+
.attr('stroke-width', CARD_STROKE_WIDTH);
|
|
952
|
+
|
|
953
|
+
const titleEl = cg
|
|
954
|
+
.append('text')
|
|
955
|
+
.attr('x', cx + CARD_PADDING_X)
|
|
956
|
+
.attr('y', cy + CARD_PADDING_Y + CARD_TITLE_FONT_SIZE)
|
|
957
|
+
.attr('font-size', CARD_TITLE_FONT_SIZE)
|
|
958
|
+
.attr('font-weight', '500')
|
|
959
|
+
.attr('fill', palette.text);
|
|
960
|
+
renderInlineText(titleEl, card.title, palette, CARD_TITLE_FONT_SIZE);
|
|
961
|
+
|
|
962
|
+
if (hasMeta) {
|
|
963
|
+
const separatorY = cy + CARD_HEADER_HEIGHT;
|
|
964
|
+
cg.append('line')
|
|
965
|
+
.attr('x1', cx)
|
|
966
|
+
.attr('y1', separatorY)
|
|
967
|
+
.attr('x2', cx + cardLayout.width)
|
|
968
|
+
.attr('y2', separatorY)
|
|
969
|
+
.attr('stroke', cardStroke)
|
|
970
|
+
.attr('stroke-opacity', 0.3)
|
|
971
|
+
.attr('stroke-width', 1);
|
|
972
|
+
|
|
973
|
+
let metaY = separatorY + CARD_SEPARATOR_GAP + CARD_META_FONT_SIZE;
|
|
974
|
+
|
|
975
|
+
for (const meta of tagMeta) {
|
|
976
|
+
cg.append('text')
|
|
977
|
+
.attr('x', cx + CARD_PADDING_X)
|
|
978
|
+
.attr('y', metaY)
|
|
979
|
+
.attr('font-size', CARD_META_FONT_SIZE)
|
|
980
|
+
.attr('fill', palette.textMuted)
|
|
981
|
+
.text(`${meta.label}: `);
|
|
982
|
+
const labelWidth = (meta.label.length + 2) * CARD_META_FONT_SIZE * 0.6;
|
|
983
|
+
cg.append('text')
|
|
984
|
+
.attr('x', cx + CARD_PADDING_X + labelWidth)
|
|
985
|
+
.attr('y', metaY)
|
|
986
|
+
.attr('font-size', CARD_META_FONT_SIZE)
|
|
987
|
+
.attr('fill', palette.text)
|
|
988
|
+
.text(meta.value);
|
|
989
|
+
metaY += CARD_META_LINE_HEIGHT;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
for (const detail of card.details) {
|
|
993
|
+
const detailEl = cg
|
|
994
|
+
.append('text')
|
|
995
|
+
.attr('x', cx + CARD_PADDING_X)
|
|
996
|
+
.attr('y', metaY)
|
|
997
|
+
.attr('font-size', CARD_META_FONT_SIZE)
|
|
998
|
+
.attr('fill', palette.textMuted);
|
|
999
|
+
renderInlineText(detailEl, detail, palette, CARD_META_FONT_SIZE);
|
|
1000
|
+
metaY += CARD_META_LINE_HEIGHT;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
package/src/palettes/bold.ts
CHANGED
|
@@ -29,6 +29,8 @@ export const boldPalette: PaletteConfig = {
|
|
|
29
29
|
teal: '#008080',
|
|
30
30
|
cyan: '#00cccc',
|
|
31
31
|
gray: '#808080',
|
|
32
|
+
black: '#000000',
|
|
33
|
+
white: '#f0f0f0',
|
|
32
34
|
},
|
|
33
35
|
},
|
|
34
36
|
dark: {
|
|
@@ -52,6 +54,8 @@ export const boldPalette: PaletteConfig = {
|
|
|
52
54
|
teal: '#00cccc',
|
|
53
55
|
cyan: '#00ffff',
|
|
54
56
|
gray: '#808080',
|
|
57
|
+
black: '#111111',
|
|
58
|
+
white: '#ffffff',
|
|
55
59
|
},
|
|
56
60
|
},
|
|
57
61
|
};
|
|
@@ -46,6 +46,8 @@ export const catppuccinPalette: PaletteConfig = {
|
|
|
46
46
|
teal: '#179299',
|
|
47
47
|
cyan: '#209fb5',
|
|
48
48
|
gray: '#9ca0b0', // Latte Overlay0
|
|
49
|
+
black: '#4c4f69',
|
|
50
|
+
white: '#e6e9ef',
|
|
49
51
|
},
|
|
50
52
|
},
|
|
51
53
|
dark: {
|
|
@@ -69,6 +71,8 @@ export const catppuccinPalette: PaletteConfig = {
|
|
|
69
71
|
teal: '#94e2d5',
|
|
70
72
|
cyan: '#74c7ec',
|
|
71
73
|
gray: '#6c7086', // Mocha Overlay0
|
|
74
|
+
black: '#313244',
|
|
75
|
+
white: '#cdd6f4',
|
|
72
76
|
},
|
|
73
77
|
},
|
|
74
78
|
};
|
package/src/palettes/dracula.ts
CHANGED
|
@@ -30,6 +30,8 @@ export const draculaPalette: PaletteConfig = {
|
|
|
30
30
|
teal: '#5ac8b8', // muted cyan toward green
|
|
31
31
|
cyan: '#8be9fd',
|
|
32
32
|
gray: '#6272a4',
|
|
33
|
+
black: '#282a36',
|
|
34
|
+
white: '#f0f0ec',
|
|
33
35
|
},
|
|
34
36
|
},
|
|
35
37
|
dark: {
|
|
@@ -53,6 +55,8 @@ export const draculaPalette: PaletteConfig = {
|
|
|
53
55
|
teal: '#5ac8b8', // muted cyan toward green
|
|
54
56
|
cyan: '#8be9fd',
|
|
55
57
|
gray: '#6272a4',
|
|
58
|
+
black: '#343746',
|
|
59
|
+
white: '#f8f8f2',
|
|
56
60
|
},
|
|
57
61
|
},
|
|
58
62
|
};
|
package/src/palettes/gruvbox.ts
CHANGED
|
@@ -47,6 +47,8 @@ export const gruvboxPalette: PaletteConfig = {
|
|
|
47
47
|
teal: '#427b58', // faded aqua
|
|
48
48
|
cyan: '#427b58', // faded aqua
|
|
49
49
|
gray: '#928374',
|
|
50
|
+
black: '#3c3836',
|
|
51
|
+
white: '#ebdbb2',
|
|
50
52
|
},
|
|
51
53
|
},
|
|
52
54
|
dark: {
|
|
@@ -70,6 +72,8 @@ export const gruvboxPalette: PaletteConfig = {
|
|
|
70
72
|
teal: '#8ec07c', // bright aqua
|
|
71
73
|
cyan: '#8ec07c', // bright aqua
|
|
72
74
|
gray: '#928374',
|
|
75
|
+
black: '#3c3836',
|
|
76
|
+
white: '#ebdbb2',
|
|
73
77
|
},
|
|
74
78
|
},
|
|
75
79
|
};
|
package/src/palettes/monokai.ts
CHANGED
|
@@ -30,6 +30,8 @@ export const monokaiPalette: PaletteConfig = {
|
|
|
30
30
|
teal: '#4ea8a6', // muted from cyan
|
|
31
31
|
cyan: '#66d9ef',
|
|
32
32
|
gray: '#75715e', // comment
|
|
33
|
+
black: '#272822',
|
|
34
|
+
white: '#f0efe8',
|
|
33
35
|
},
|
|
34
36
|
},
|
|
35
37
|
dark: {
|
|
@@ -53,6 +55,8 @@ export const monokaiPalette: PaletteConfig = {
|
|
|
53
55
|
teal: '#4ea8a6', // muted from cyan
|
|
54
56
|
cyan: '#66d9ef',
|
|
55
57
|
gray: '#75715e', // comment
|
|
58
|
+
black: '#2d2e27',
|
|
59
|
+
white: '#f8f8f2',
|
|
56
60
|
},
|
|
57
61
|
},
|
|
58
62
|
};
|
package/src/palettes/nord.ts
CHANGED
|
@@ -29,6 +29,8 @@ export const nordPalette: PaletteConfig = {
|
|
|
29
29
|
teal: '#8fbcbb', // nord7
|
|
30
30
|
cyan: '#88c0d0', // nord8
|
|
31
31
|
gray: '#4c566a', // nord3
|
|
32
|
+
black: '#2e3440',
|
|
33
|
+
white: '#e5e9f0',
|
|
32
34
|
},
|
|
33
35
|
},
|
|
34
36
|
dark: {
|
|
@@ -52,6 +54,8 @@ export const nordPalette: PaletteConfig = {
|
|
|
52
54
|
teal: '#8fbcbb', // nord7
|
|
53
55
|
cyan: '#88c0d0', // nord8
|
|
54
56
|
gray: '#4c566a', // nord3
|
|
57
|
+
black: '#3b4252',
|
|
58
|
+
white: '#eceff4',
|
|
55
59
|
},
|
|
56
60
|
},
|
|
57
61
|
};
|
package/src/palettes/one-dark.ts
CHANGED
|
@@ -31,6 +31,8 @@ export const oneDarkPalette: PaletteConfig = {
|
|
|
31
31
|
teal: '#0184bc',
|
|
32
32
|
cyan: '#0997b3',
|
|
33
33
|
gray: '#696c77',
|
|
34
|
+
black: '#383a42',
|
|
35
|
+
white: '#f0f0f0',
|
|
34
36
|
},
|
|
35
37
|
},
|
|
36
38
|
dark: {
|
|
@@ -55,6 +57,8 @@ export const oneDarkPalette: PaletteConfig = {
|
|
|
55
57
|
teal: '#56b6c2',
|
|
56
58
|
cyan: '#56b6c2',
|
|
57
59
|
gray: '#5c6370',
|
|
60
|
+
black: '#21252b',
|
|
61
|
+
white: '#abb2bf',
|
|
58
62
|
},
|
|
59
63
|
},
|
|
60
64
|
};
|