@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.
@@ -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 { LegendConfig, LegendState } from '../utils/legend-types';
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
- _onNavigateToLine?: (line: number) => void,
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
- undefined,
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, undefined, {
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
+ }
@@ -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
  };
@@ -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
  };
@@ -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
  };
@@ -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
  };
@@ -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
  };
@@ -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
  };