@diagrammo/dgmo 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ import type { PaletteConfig } from './types';
2
+ import { registerPalette } from './registry';
3
+
4
+ // ============================================================
5
+ // Monokai Palette Definition
6
+ // Based on Monokai / Monokai Pro color scheme
7
+ // ============================================================
8
+
9
+ export const monokaiPalette: PaletteConfig = {
10
+ id: 'monokai',
11
+ name: 'Monokai',
12
+ light: {
13
+ bg: '#fafaf8',
14
+ surface: '#f0efe8',
15
+ overlay: '#e6e5de',
16
+ border: '#d4d3cc',
17
+ text: '#272822', // classic Monokai bg as text
18
+ textMuted: '#75715e', // comment
19
+ primary: '#49483e', // line highlight
20
+ secondary: '#f92672', // pink
21
+ accent: '#a6e22e', // green
22
+ destructive: '#f92672', // pink/red
23
+ colors: {
24
+ red: '#f92672', // Monokai pink-red
25
+ orange: '#fd971f',
26
+ yellow: '#e6db74',
27
+ green: '#a6e22e',
28
+ blue: '#5c7eab', // derived true blue
29
+ purple: '#ae81ff',
30
+ teal: '#4ea8a6', // muted from cyan
31
+ cyan: '#66d9ef',
32
+ gray: '#75715e', // comment
33
+ },
34
+ },
35
+ dark: {
36
+ bg: '#272822', // classic background
37
+ surface: '#2d2e27',
38
+ overlay: '#3e3d32', // line highlight
39
+ border: '#49483e',
40
+ text: '#f8f8f2', // foreground
41
+ textMuted: '#a6a28c', // brightened comment
42
+ primary: '#a6e22e', // green
43
+ secondary: '#66d9ef', // cyan
44
+ accent: '#f92672', // pink
45
+ destructive: '#f92672', // pink/red
46
+ colors: {
47
+ red: '#f92672',
48
+ orange: '#fd971f',
49
+ yellow: '#e6db74',
50
+ green: '#a6e22e',
51
+ blue: '#5c7eab', // derived true blue
52
+ purple: '#ae81ff',
53
+ teal: '#4ea8a6', // muted from cyan
54
+ cyan: '#66d9ef',
55
+ gray: '#75715e', // comment
56
+ },
57
+ },
58
+ };
59
+
60
+ registerPalette(monokaiPalette);
@@ -86,7 +86,9 @@ export function getPalette(id: string): PaletteConfig {
86
86
  return PALETTE_REGISTRY.get(id) ?? PALETTE_REGISTRY.get(DEFAULT_PALETTE_ID)!;
87
87
  }
88
88
 
89
- /** List all registered palettes (for the selector UI). */
89
+ /** List all registered palettes alphabetically (for the selector UI). */
90
90
  export function getAvailablePalettes(): PaletteConfig[] {
91
- return Array.from(PALETTE_REGISTRY.values());
91
+ return Array.from(PALETTE_REGISTRY.values()).sort((a, b) =>
92
+ a.name.localeCompare(b.name)
93
+ );
92
94
  }
@@ -1281,10 +1281,12 @@ export function renderSequenceDiagram(
1281
1281
 
1282
1282
  // Compute cumulative Y positions for each step, with section dividers as stable anchors
1283
1283
  const titleOffset = title ? TITLE_HEIGHT : 0;
1284
+ const LEGEND_FIXED_GAP = 8;
1285
+ const legendTopSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
1284
1286
  const groupOffset =
1285
1287
  groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
1286
1288
  const participantStartY =
1287
- TOP_MARGIN + titleOffset + PARTICIPANT_Y_OFFSET + groupOffset;
1289
+ TOP_MARGIN + titleOffset + legendTopSpace + PARTICIPANT_Y_OFFSET + groupOffset;
1288
1290
  const lifelineStartY0 = participantStartY + PARTICIPANT_BOX_HEIGHT;
1289
1291
  const hasActors = participants.some((p) => p.type === 'actor');
1290
1292
  const messageStartOffset = MESSAGE_START_OFFSET + (hasActors ? 20 : 0);
@@ -1390,8 +1392,7 @@ export function renderSequenceDiagram(
1390
1392
  PARTICIPANT_BOX_HEIGHT +
1391
1393
  Math.max(lifelineLength, 40) +
1392
1394
  40;
1393
- const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
1394
- const totalHeight = contentHeight + legendSpace;
1395
+ const totalHeight = contentHeight;
1395
1396
 
1396
1397
  const containerWidth = options?.exportWidth ?? container.getBoundingClientRect().width;
1397
1398
  const svgWidth = Math.max(totalWidth, containerWidth);
@@ -1570,7 +1571,7 @@ export function renderSequenceDiagram(
1570
1571
 
1571
1572
  // Render legend pills for tag groups
1572
1573
  if (parsed.tagGroups.length > 0) {
1573
- const legendY = contentHeight;
1574
+ const legendY = TOP_MARGIN + titleOffset;
1574
1575
  const groupBg = isDark
1575
1576
  ? mix(palette.surface, palette.bg, 50)
1576
1577
  : mix(palette.surface, palette.bg, 30);
package/src/sharing.ts CHANGED
@@ -10,6 +10,7 @@ export interface DiagramViewState {
10
10
  activeTagGroup?: string;
11
11
  collapsedGroups?: string[];
12
12
  swimlaneTagGroup?: string;
13
+ collapsedLanes?: string[];
13
14
  palette?: string;
14
15
  theme?: 'light' | 'dark';
15
16
  }
@@ -59,6 +60,10 @@ export function encodeDiagramUrl(
59
60
  hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
60
61
  }
61
62
 
63
+ if (options?.viewState?.collapsedLanes?.length) {
64
+ hash += `&cl=${encodeURIComponent(options.viewState.collapsedLanes.join(','))}`;
65
+ }
66
+
62
67
  if (options?.viewState?.palette && options.viewState.palette !== 'nord') {
63
68
  hash += `&pal=${encodeURIComponent(options.viewState.palette)}`;
64
69
  }
@@ -115,6 +120,9 @@ export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
115
120
  if (key === 'swim' && val) {
116
121
  viewState.swimlaneTagGroup = val;
117
122
  }
123
+ if (key === 'cl' && val) {
124
+ viewState.collapsedLanes = val.split(',').filter(Boolean);
125
+ }
118
126
  if (key === 'pal' && val) viewState.palette = val;
119
127
  if (key === 'th' && (val === 'light' || val === 'dark')) viewState.theme = val;
120
128
  }
@@ -127,9 +127,9 @@ export function renderSitemap(
127
127
  const fixedTitle = fixedLegend && !!parsed.title;
128
128
  const fixedTitleH = fixedTitle ? TITLE_HEIGHT : 0;
129
129
  const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
130
- // Space reserved above content (title only), and below content (legend)
131
- const fixedReserveTop = fixedTitleH;
132
- const fixedReserveBottom = legendReserveH;
130
+ // Space reserved above content (title + legend)
131
+ const fixedReserveTop = fixedTitleH + legendReserveH;
132
+ const fixedReserveBottom = 0;
133
133
  // Title inside scaled group only when legend is NOT fixed
134
134
  const titleOffset = !fixedTitle && parsed.title ? TITLE_HEIGHT : 0;
135
135
 
@@ -543,7 +543,7 @@ export function renderSitemap(
543
543
  const legendParent = svg
544
544
  .append('g')
545
545
  .attr('class', 'sitemap-legend-fixed')
546
- .attr('transform', `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})`);
546
+ .attr('transform', `translate(0, ${DIAGRAM_PADDING + fixedTitleH})`);
547
547
  if (activeTagGroup) {
548
548
  legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
549
549
  }
@@ -0,0 +1,212 @@
1
+ // ============================================================
2
+ // Duration & Business Day Arithmetic
3
+ // ============================================================
4
+
5
+ import type { Duration, DurationUnit, GanttHolidays, Offset, Weekday } from '../gantt/types';
6
+
7
+ // ── Weekday constants ─────────────────────────────────────
8
+
9
+ /** JS Date.getDay() → Weekday mapping (0=Sun, 1=Mon, ..., 6=Sat) */
10
+ const JS_DAY_TO_WEEKDAY: Weekday[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
11
+
12
+ /**
13
+ * Check if a date is a workday (not a weekend and not a holiday).
14
+ */
15
+ export function isWorkday(
16
+ date: Date,
17
+ workweek: Weekday[],
18
+ holidaySet: Set<string>,
19
+ ): boolean {
20
+ const dayName = JS_DAY_TO_WEEKDAY[date.getDay()];
21
+ if (!workweek.includes(dayName)) return false;
22
+ if (holidaySet.has(formatDateKey(date))) return false;
23
+ return true;
24
+ }
25
+
26
+ /**
27
+ * Format a Date as YYYY-MM-DD for holiday set lookups.
28
+ */
29
+ export function formatDateKey(date: Date): string {
30
+ const y = date.getFullYear();
31
+ const m = String(date.getMonth() + 1).padStart(2, '0');
32
+ const d = String(date.getDate()).padStart(2, '0');
33
+ return `${y}-${m}-${d}`;
34
+ }
35
+
36
+ /**
37
+ * Build a Set of holiday date strings for efficient lookup.
38
+ * Expands date ranges into individual dates.
39
+ */
40
+ export function buildHolidaySet(holidays: GanttHolidays): Set<string> {
41
+ const set = new Set<string>();
42
+
43
+ for (const h of holidays.dates) {
44
+ set.add(h.date);
45
+ }
46
+
47
+ for (const range of holidays.ranges) {
48
+ const start = new Date(range.startDate + 'T00:00:00');
49
+ const end = new Date(range.endDate + 'T00:00:00');
50
+ const current = new Date(start);
51
+ while (current <= end) {
52
+ set.add(formatDateKey(current));
53
+ current.setDate(current.getDate() + 1);
54
+ }
55
+ }
56
+
57
+ return set;
58
+ }
59
+
60
+ /**
61
+ * Add business days to a start date, skipping weekends and holidays.
62
+ *
63
+ * For fractional business days, rounds to the nearest whole day first.
64
+ * Handles both positive amounts (forward) and zero (returns start date).
65
+ */
66
+ export function addBusinessDays(
67
+ startDate: Date,
68
+ count: number,
69
+ workweek: Weekday[],
70
+ holidaySet: Set<string>,
71
+ direction: 1 | -1 = 1,
72
+ ): Date {
73
+ const days = Math.round(Math.abs(count));
74
+ if (days === 0) return new Date(startDate);
75
+
76
+ const current = new Date(startDate);
77
+ let remaining = days;
78
+
79
+ while (remaining > 0) {
80
+ current.setDate(current.getDate() + direction);
81
+ if (isWorkday(current, workweek, holidaySet)) {
82
+ remaining--;
83
+ }
84
+ }
85
+
86
+ return current;
87
+ }
88
+
89
+ /**
90
+ * Add a gantt duration to a start date, producing an end date.
91
+ *
92
+ * Calendar units (d, w, m, q, y) ignore holidays.
93
+ * Business day units (bd) skip weekends and holidays.
94
+ */
95
+ export function addGanttDuration(
96
+ startDate: Date,
97
+ duration: Duration,
98
+ holidays: GanttHolidays,
99
+ holidaySet: Set<string>,
100
+ direction: 1 | -1 = 1,
101
+ ): Date {
102
+ const { amount, unit } = duration;
103
+
104
+ switch (unit) {
105
+ case 'bd':
106
+ return addBusinessDays(startDate, amount, holidays.workweek, holidaySet, direction);
107
+
108
+ case 'd': {
109
+ const result = new Date(startDate);
110
+ result.setDate(result.getDate() + Math.round(amount) * direction);
111
+ return result;
112
+ }
113
+
114
+ case 'w': {
115
+ const result = new Date(startDate);
116
+ result.setDate(result.getDate() + Math.round(amount * 7) * direction);
117
+ return result;
118
+ }
119
+
120
+ case 'm': {
121
+ const result = new Date(startDate);
122
+ const wholeMonths = direction === -1 ? Math.round(amount) : Math.floor(amount);
123
+ const fractionalDays = direction === -1 ? 0 : Math.round((amount - wholeMonths) * 30);
124
+ result.setMonth(result.getMonth() + wholeMonths * direction);
125
+ if (fractionalDays > 0) {
126
+ result.setDate(result.getDate() + fractionalDays * direction);
127
+ }
128
+ return result;
129
+ }
130
+
131
+ case 'q': {
132
+ const result = new Date(startDate);
133
+ const totalMonths = amount * 3;
134
+ const wholeMonths = direction === -1 ? Math.round(totalMonths) : Math.floor(totalMonths);
135
+ const fractionalDays = direction === -1 ? 0 : Math.round((totalMonths - wholeMonths) * 30);
136
+ result.setMonth(result.getMonth() + wholeMonths * direction);
137
+ if (fractionalDays > 0) {
138
+ result.setDate(result.getDate() + fractionalDays * direction);
139
+ }
140
+ return result;
141
+ }
142
+
143
+ case 'y': {
144
+ const result = new Date(startDate);
145
+ const wholeYears = direction === -1 ? Math.round(amount) : Math.floor(amount);
146
+ const fractionalMonths = direction === -1 ? 0 : Math.round((amount - wholeYears) * 12);
147
+ result.setFullYear(result.getFullYear() + wholeYears * direction);
148
+ if (fractionalMonths > 0) {
149
+ result.setMonth(result.getMonth() + fractionalMonths * direction);
150
+ }
151
+ return result;
152
+ }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Parse a duration string like "3bd" or "5d".
158
+ */
159
+ export function parseDuration(s: string): Duration | null {
160
+ const match = s.trim().match(/^(\d+(?:\.\d+)?)(d|bd|w|m|q|y)$/);
161
+ if (!match) return null;
162
+ return { amount: parseFloat(match[1]), unit: match[2] as DurationUnit };
163
+ }
164
+
165
+ /**
166
+ * Parse an offset string like "5bd", "-3bd", or "0d".
167
+ * Returns null if the format is invalid.
168
+ * Explicit '+' prefix (e.g. "+5bd") returns null — caller should warn.
169
+ */
170
+ export function parseOffset(value: string): Offset | null {
171
+ const trimmed = value.trim();
172
+ let direction: 1 | -1 = 1;
173
+ let remainder = trimmed;
174
+
175
+ if (trimmed.startsWith('-')) {
176
+ direction = -1;
177
+ remainder = trimmed.slice(1);
178
+ } else if (trimmed.startsWith('+')) {
179
+ return null; // explicit + is not supported
180
+ }
181
+
182
+ const duration = parseDuration(remainder);
183
+ if (!duration) return null;
184
+ return { duration, direction };
185
+ }
186
+
187
+ /**
188
+ * Parse a date string (YYYY-MM-DD, YYYY-MM, or YYYY) into a Date object.
189
+ * Always returns midnight local time on the first available day.
190
+ */
191
+ export function parseGanttDate(s: string): Date {
192
+ const parts = s.split('-').map(p => parseInt(p, 10));
193
+ const year = parts[0];
194
+ const month = parts.length >= 2 ? parts[1] - 1 : 0; // JS months are 0-based
195
+ const day = parts.length >= 3 ? parts[2] : 1;
196
+ return new Date(year, month, day);
197
+ }
198
+
199
+ /**
200
+ * Format a Date as YYYY-MM-DD string.
201
+ */
202
+ export function formatGanttDate(date: Date): string {
203
+ return formatDateKey(date);
204
+ }
205
+
206
+ /**
207
+ * Calculate the difference in calendar days between two dates.
208
+ */
209
+ export function daysBetween(a: Date, b: Date): number {
210
+ const msPerDay = 86400000;
211
+ return Math.round((b.getTime() - a.getTime()) / msPerDay);
212
+ }
@@ -16,6 +16,7 @@ export const LEGEND_ENTRY_TRAIL = 8;
16
16
  export const LEGEND_GROUP_GAP = 12;
17
17
  export const LEGEND_EYE_SIZE = 14;
18
18
  export const LEGEND_EYE_GAP = 6;
19
+ export const LEGEND_ICON_W = 20;
19
20
 
20
21
  // Eye icon SVG paths (14×14 viewBox)
21
22
  // Present only in org and sitemap legends (metadata visibility toggle)