@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.
- package/dist/cli.cjs +180 -178
- package/dist/index.cjs +5296 -2209
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +236 -16
- package/dist/index.d.ts +236 -16
- package/dist/index.js +12423 -9343
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/c4/renderer.ts +6 -6
- package/src/class/renderer.ts +183 -7
- package/src/cli.ts +3 -11
- package/src/colors.ts +3 -3
- package/src/d3.ts +128 -23
- package/src/dgmo-router.ts +3 -1
- package/src/er/renderer.ts +11 -5
- package/src/gantt/calculator.ts +677 -0
- package/src/gantt/parser.ts +761 -0
- package/src/gantt/renderer.ts +2125 -0
- package/src/gantt/resolver.ts +144 -0
- package/src/gantt/types.ts +168 -0
- package/src/index.ts +27 -0
- package/src/infra/renderer.ts +48 -12
- package/src/initiative-status/filter.ts +63 -0
- package/src/initiative-status/layout.ts +319 -67
- package/src/initiative-status/parser.ts +200 -25
- package/src/initiative-status/renderer.ts +293 -10
- package/src/initiative-status/types.ts +6 -0
- package/src/org/layout.ts +22 -55
- package/src/org/renderer.ts +4 -8
- package/src/palettes/dracula.ts +60 -0
- package/src/palettes/index.ts +8 -6
- package/src/palettes/monokai.ts +60 -0
- package/src/palettes/registry.ts +4 -2
- package/src/sequence/renderer.ts +5 -4
- package/src/sharing.ts +8 -0
- package/src/sitemap/renderer.ts +4 -4
- package/src/utils/duration.ts +212 -0
- package/src/utils/legend-constants.ts +1 -0
|
@@ -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);
|
package/src/palettes/registry.ts
CHANGED
|
@@ -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
|
}
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
}
|
package/src/sitemap/renderer.ts
CHANGED
|
@@ -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
|
|
131
|
-
const fixedReserveTop = fixedTitleH;
|
|
132
|
-
const fixedReserveBottom =
|
|
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, ${
|
|
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)
|