@diagrammo/dgmo 0.8.18 → 0.8.19
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 +521 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -12
- package/dist/index.d.ts +107 -12
- package/dist/index.js +518 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/gantt/renderer.ts +151 -89
- package/src/index.ts +10 -2
- package/src/sequence/collapse.ts +169 -0
- package/src/sequence/parser.ts +14 -2
- package/src/sequence/renderer.ts +186 -49
- package/src/sharing.ts +86 -49
- package/src/utils/legend-constants.ts +11 -0
- package/src/utils/legend-d3.ts +171 -0
- package/src/utils/legend-layout.ts +140 -13
- package/src/utils/legend-types.ts +45 -0
package/src/sequence/renderer.ts
CHANGED
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
SequenceParticipant,
|
|
22
22
|
} from './parser';
|
|
23
23
|
import { isSequenceBlock, isSequenceSection, isSequenceNote } from './parser';
|
|
24
|
+
import { applyCollapseProjection } from './collapse';
|
|
25
|
+
import type { CollapsedView } from './collapse';
|
|
24
26
|
import { resolveSequenceTags } from './tag-resolution';
|
|
25
27
|
import type { ResolvedTagMap } from './tag-resolution';
|
|
26
28
|
import { resolveActiveTagGroup } from '../utils/tag-groups';
|
|
@@ -533,6 +535,7 @@ export interface SectionMessageGroup {
|
|
|
533
535
|
|
|
534
536
|
export interface SequenceRenderOptions {
|
|
535
537
|
collapsedSections?: Set<number>; // keyed by section lineNumber
|
|
538
|
+
collapsedGroups?: Set<number>; // keyed by group lineNumber
|
|
536
539
|
expandedNoteLines?: Set<number>; // keyed by note lineNumber; undefined = all expanded (CLI default)
|
|
537
540
|
exportWidth?: number; // Explicit width for CLI/export rendering (bypasses getBoundingClientRect)
|
|
538
541
|
activeTagGroup?: string | null; // Active tag group name for tag-driven recoloring; null = explicitly none
|
|
@@ -900,7 +903,37 @@ export function renderSequenceDiagram(
|
|
|
900
903
|
// Clear previous content
|
|
901
904
|
d3Selection.select(container).selectAll('*').remove();
|
|
902
905
|
|
|
903
|
-
const { title,
|
|
906
|
+
const { title, options: parsedOptions } = parsed;
|
|
907
|
+
|
|
908
|
+
// Compute effective collapsed groups: union of syntax-declared and runtime-toggled
|
|
909
|
+
const effectiveCollapsedGroups = new Set<number>();
|
|
910
|
+
for (const group of parsed.groups) {
|
|
911
|
+
if (group.collapsed) effectiveCollapsedGroups.add(group.lineNumber);
|
|
912
|
+
}
|
|
913
|
+
if (options?.collapsedGroups) {
|
|
914
|
+
for (const ln of options.collapsedGroups) {
|
|
915
|
+
// Toggle: if already in the set (from syntax), remove it (user expanded);
|
|
916
|
+
// if not in the set, add it (user collapsed)
|
|
917
|
+
if (effectiveCollapsedGroups.has(ln)) {
|
|
918
|
+
effectiveCollapsedGroups.delete(ln);
|
|
919
|
+
} else {
|
|
920
|
+
effectiveCollapsedGroups.add(ln);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Apply collapse projection before participant ordering
|
|
926
|
+
const collapsed: CollapsedView | null =
|
|
927
|
+
effectiveCollapsedGroups.size > 0
|
|
928
|
+
? applyCollapseProjection(parsed, effectiveCollapsedGroups)
|
|
929
|
+
: null;
|
|
930
|
+
|
|
931
|
+
const messages = collapsed ? collapsed.messages : parsed.messages;
|
|
932
|
+
const elements = collapsed ? collapsed.elements : parsed.elements;
|
|
933
|
+
const groups = collapsed ? collapsed.groups : parsed.groups;
|
|
934
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
935
|
+
const collapsedGroupIds = collapsed?.collapsedGroupIds ?? new Map();
|
|
936
|
+
|
|
904
937
|
const collapsedSections = options?.collapsedSections;
|
|
905
938
|
const expandedNoteLines = options?.expandedNoteLines;
|
|
906
939
|
const collapseNotesDisabled =
|
|
@@ -911,8 +944,12 @@ export function renderSequenceDiagram(
|
|
|
911
944
|
expandedNoteLines === undefined ||
|
|
912
945
|
collapseNotesDisabled ||
|
|
913
946
|
expandedNoteLines.has(note.lineNumber);
|
|
947
|
+
|
|
948
|
+
const sourceParticipants = collapsed
|
|
949
|
+
? collapsed.participants
|
|
950
|
+
: parsed.participants;
|
|
914
951
|
const participants = applyPositionOverrides(
|
|
915
|
-
applyGroupOrdering(
|
|
952
|
+
applyGroupOrdering(sourceParticipants, groups, messages)
|
|
916
953
|
);
|
|
917
954
|
if (participants.length === 0) return;
|
|
918
955
|
|
|
@@ -1158,6 +1195,15 @@ export function renderSequenceDiagram(
|
|
|
1158
1195
|
const preSectionMsgIndices: number[] = [];
|
|
1159
1196
|
const sectionRegions: SectionRegion[] = [];
|
|
1160
1197
|
{
|
|
1198
|
+
// Build lineNumber → message index lookup. This is used instead of
|
|
1199
|
+
// messages.indexOf() because collapse projection creates spread copies
|
|
1200
|
+
// of messages, breaking reference equality.
|
|
1201
|
+
const msgLineToIndex = new Map<number, number>();
|
|
1202
|
+
messages.forEach((m, i) => msgLineToIndex.set(m.lineNumber, i));
|
|
1203
|
+
|
|
1204
|
+
const findMsgIndex = (child: SequenceElement): number =>
|
|
1205
|
+
msgLineToIndex.get(child.lineNumber) ?? -1;
|
|
1206
|
+
|
|
1161
1207
|
const collectMsgIndicesFromBlock = (
|
|
1162
1208
|
block: import('./parser').SequenceBlock
|
|
1163
1209
|
): number[] => {
|
|
@@ -1166,7 +1212,7 @@ export function renderSequenceDiagram(
|
|
|
1166
1212
|
if (isSequenceBlock(child)) {
|
|
1167
1213
|
indices.push(...collectMsgIndicesFromBlock(child));
|
|
1168
1214
|
} else if (!isSequenceSection(child) && !isSequenceNote(child)) {
|
|
1169
|
-
const idx =
|
|
1215
|
+
const idx = findMsgIndex(child);
|
|
1170
1216
|
if (idx >= 0) indices.push(idx);
|
|
1171
1217
|
}
|
|
1172
1218
|
}
|
|
@@ -1176,7 +1222,7 @@ export function renderSequenceDiagram(
|
|
|
1176
1222
|
if (isSequenceBlock(child)) {
|
|
1177
1223
|
indices.push(...collectMsgIndicesFromBlock(child));
|
|
1178
1224
|
} else if (!isSequenceSection(child) && !isSequenceNote(child)) {
|
|
1179
|
-
const idx =
|
|
1225
|
+
const idx = findMsgIndex(child);
|
|
1180
1226
|
if (idx >= 0) indices.push(idx);
|
|
1181
1227
|
}
|
|
1182
1228
|
}
|
|
@@ -1186,7 +1232,7 @@ export function renderSequenceDiagram(
|
|
|
1186
1232
|
if (isSequenceBlock(child)) {
|
|
1187
1233
|
indices.push(...collectMsgIndicesFromBlock(child));
|
|
1188
1234
|
} else if (!isSequenceSection(child) && !isSequenceNote(child)) {
|
|
1189
|
-
const idx =
|
|
1235
|
+
const idx = findMsgIndex(child);
|
|
1190
1236
|
if (idx >= 0) indices.push(idx);
|
|
1191
1237
|
}
|
|
1192
1238
|
}
|
|
@@ -1202,7 +1248,7 @@ export function renderSequenceDiagram(
|
|
|
1202
1248
|
} else if (isSequenceBlock(el)) {
|
|
1203
1249
|
currentTarget.push(...collectMsgIndicesFromBlock(el));
|
|
1204
1250
|
} else {
|
|
1205
|
-
const idx =
|
|
1251
|
+
const idx = findMsgIndex(el);
|
|
1206
1252
|
if (idx >= 0) currentTarget.push(idx);
|
|
1207
1253
|
}
|
|
1208
1254
|
}
|
|
@@ -1285,8 +1331,10 @@ export function renderSequenceDiagram(
|
|
|
1285
1331
|
const LEGEND_FIXED_GAP = 8;
|
|
1286
1332
|
const legendTopSpace =
|
|
1287
1333
|
parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
1334
|
+
// Use parsed.groups (not projected groups) to keep vertical space consistent
|
|
1335
|
+
// even when all groups are collapsed into virtual participants
|
|
1288
1336
|
const groupOffset =
|
|
1289
|
-
groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
|
|
1337
|
+
parsed.groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
|
|
1290
1338
|
const participantStartY =
|
|
1291
1339
|
TOP_MARGIN +
|
|
1292
1340
|
titleOffset +
|
|
@@ -1618,7 +1666,23 @@ export function renderSequenceDiagram(
|
|
|
1618
1666
|
);
|
|
1619
1667
|
}
|
|
1620
1668
|
|
|
1621
|
-
//
|
|
1669
|
+
// Build set of collapsed group names for drill-bar rendering
|
|
1670
|
+
const collapsedGroupNames = new Set<string>();
|
|
1671
|
+
const collapsedGroupMeta = new Map<
|
|
1672
|
+
string,
|
|
1673
|
+
{ lineNumber: number; metadata?: Record<string, string> }
|
|
1674
|
+
>();
|
|
1675
|
+
for (const group of parsed.groups) {
|
|
1676
|
+
if (effectiveCollapsedGroups.has(group.lineNumber)) {
|
|
1677
|
+
collapsedGroupNames.add(group.name);
|
|
1678
|
+
collapsedGroupMeta.set(group.name, {
|
|
1679
|
+
lineNumber: group.lineNumber,
|
|
1680
|
+
metadata: group.metadata,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Render group boxes (behind participant shapes) — skip collapsed groups
|
|
1622
1686
|
for (const group of groups) {
|
|
1623
1687
|
if (group.participantIds.length === 0) continue;
|
|
1624
1688
|
|
|
@@ -1650,7 +1714,15 @@ export function renderSequenceDiagram(
|
|
|
1650
1714
|
: palette.bg;
|
|
1651
1715
|
const strokeColor = groupTagColor || palette.textMuted;
|
|
1652
1716
|
|
|
1653
|
-
svg
|
|
1717
|
+
const groupG = svg
|
|
1718
|
+
.append('g')
|
|
1719
|
+
.attr('class', 'group-box-wrapper')
|
|
1720
|
+
.attr('data-group-toggle', '')
|
|
1721
|
+
.attr('data-group-line', String(group.lineNumber))
|
|
1722
|
+
.attr('cursor', 'pointer');
|
|
1723
|
+
groupG.append('title').text('Click to collapse');
|
|
1724
|
+
|
|
1725
|
+
groupG
|
|
1654
1726
|
.append('rect')
|
|
1655
1727
|
.attr('x', minX)
|
|
1656
1728
|
.attr('y', boxY)
|
|
@@ -1661,11 +1733,10 @@ export function renderSequenceDiagram(
|
|
|
1661
1733
|
.attr('stroke', strokeColor)
|
|
1662
1734
|
.attr('stroke-width', 1)
|
|
1663
1735
|
.attr('stroke-opacity', 0.5)
|
|
1664
|
-
.attr('class', 'group-box')
|
|
1665
|
-
.attr('data-group-line', String(group.lineNumber));
|
|
1736
|
+
.attr('class', 'group-box');
|
|
1666
1737
|
|
|
1667
1738
|
// Group label
|
|
1668
|
-
|
|
1739
|
+
groupG
|
|
1669
1740
|
.append('text')
|
|
1670
1741
|
.attr('x', minX + 8)
|
|
1671
1742
|
.attr('y', boxY + GROUP_LABEL_SIZE + 4)
|
|
@@ -1674,7 +1745,6 @@ export function renderSequenceDiagram(
|
|
|
1674
1745
|
.attr('font-weight', 'bold')
|
|
1675
1746
|
.attr('opacity', 0.7)
|
|
1676
1747
|
.attr('class', 'group-label')
|
|
1677
|
-
.attr('data-group-line', String(group.lineNumber))
|
|
1678
1748
|
.text(group.name);
|
|
1679
1749
|
}
|
|
1680
1750
|
|
|
@@ -1690,6 +1760,16 @@ export function renderSequenceDiagram(
|
|
|
1690
1760
|
tagKey && pTagValue
|
|
1691
1761
|
? { key: tagKey, value: pTagValue.toLowerCase() }
|
|
1692
1762
|
: undefined;
|
|
1763
|
+
// For collapsed group participants, resolve tag color from group metadata
|
|
1764
|
+
const isCollapsedGroup = collapsedGroupNames.has(participant.id);
|
|
1765
|
+
let effectiveTagColor = pTagColor;
|
|
1766
|
+
if (isCollapsedGroup && !effectiveTagColor) {
|
|
1767
|
+
const meta = collapsedGroupMeta.get(participant.id);
|
|
1768
|
+
if (meta?.metadata && tagKey) {
|
|
1769
|
+
effectiveTagColor = getTagColor(meta.metadata[tagKey]);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1693
1773
|
renderParticipant(
|
|
1694
1774
|
svg,
|
|
1695
1775
|
participant,
|
|
@@ -1697,18 +1777,104 @@ export function renderSequenceDiagram(
|
|
|
1697
1777
|
cy,
|
|
1698
1778
|
palette,
|
|
1699
1779
|
isDark,
|
|
1700
|
-
|
|
1780
|
+
effectiveTagColor,
|
|
1701
1781
|
pTagAttr
|
|
1702
1782
|
);
|
|
1703
1783
|
|
|
1704
|
-
//
|
|
1784
|
+
// Collapsed group: re-render participant box at full group height + drill-bar
|
|
1785
|
+
if (isCollapsedGroup) {
|
|
1786
|
+
const meta = collapsedGroupMeta.get(participant.id)!;
|
|
1787
|
+
const drillColor = effectiveTagColor || palette.textMuted;
|
|
1788
|
+
const drillBarH = 6;
|
|
1789
|
+
const boxW = PARTICIPANT_BOX_WIDTH;
|
|
1790
|
+
// Match the group box dimensions
|
|
1791
|
+
const fullH =
|
|
1792
|
+
PARTICIPANT_BOX_HEIGHT + GROUP_PADDING_TOP + GROUP_PADDING_BOTTOM;
|
|
1793
|
+
const clipId = `clip-drill-group-${participant.id.replace(/[^a-zA-Z0-9-]/g, '-')}`;
|
|
1794
|
+
|
|
1795
|
+
// Add toggle attributes to the participant <g> so any click on it
|
|
1796
|
+
// (overlay rect, label, drill-bar) walks up and triggers the toggle
|
|
1797
|
+
const participantG = svg.select<SVGGElement>(
|
|
1798
|
+
`.participant[data-participant-id="${participant.id}"]`
|
|
1799
|
+
);
|
|
1800
|
+
participantG
|
|
1801
|
+
.attr('data-group-toggle', '')
|
|
1802
|
+
.attr('data-group-line', String(meta.lineNumber))
|
|
1803
|
+
.attr('cursor', 'pointer');
|
|
1804
|
+
participantG.append('title').text('Click to expand');
|
|
1805
|
+
|
|
1806
|
+
// Overlay a taller rect to replace the standard participant box
|
|
1807
|
+
const pFill = effectiveTagColor
|
|
1808
|
+
? mix(
|
|
1809
|
+
effectiveTagColor,
|
|
1810
|
+
isDark ? palette.surface : palette.bg,
|
|
1811
|
+
isDark ? 30 : 40
|
|
1812
|
+
)
|
|
1813
|
+
: isDark
|
|
1814
|
+
? mix(palette.overlay, palette.surface, 50)
|
|
1815
|
+
: mix(palette.bg, palette.surface, 50);
|
|
1816
|
+
const pStroke = effectiveTagColor || palette.border;
|
|
1817
|
+
|
|
1818
|
+
// Taller box inside the participant <g> (local coords, y=0 is participant cy)
|
|
1819
|
+
participantG
|
|
1820
|
+
.append('rect')
|
|
1821
|
+
.attr('x', -boxW / 2)
|
|
1822
|
+
.attr('y', -GROUP_PADDING_TOP)
|
|
1823
|
+
.attr('width', boxW)
|
|
1824
|
+
.attr('height', fullH)
|
|
1825
|
+
.attr('rx', 6)
|
|
1826
|
+
.attr('fill', pFill)
|
|
1827
|
+
.attr('stroke', pStroke)
|
|
1828
|
+
.attr('stroke-width', 1.5);
|
|
1829
|
+
|
|
1830
|
+
// Re-render label centered in the taller box (local coords)
|
|
1831
|
+
participantG
|
|
1832
|
+
.append('text')
|
|
1833
|
+
.attr('x', 0)
|
|
1834
|
+
.attr('y', -GROUP_PADDING_TOP + fullH / 2)
|
|
1835
|
+
.attr('text-anchor', 'middle')
|
|
1836
|
+
.attr('dominant-baseline', 'central')
|
|
1837
|
+
.attr('fill', palette.text)
|
|
1838
|
+
.attr('font-size', 13)
|
|
1839
|
+
.attr('font-weight', 500)
|
|
1840
|
+
.text(participant.label);
|
|
1841
|
+
|
|
1842
|
+
// Drill-bar at bottom (local coords)
|
|
1843
|
+
participantG
|
|
1844
|
+
.append('clipPath')
|
|
1845
|
+
.attr('id', clipId)
|
|
1846
|
+
.append('rect')
|
|
1847
|
+
.attr('x', -boxW / 2)
|
|
1848
|
+
.attr('y', -GROUP_PADDING_TOP)
|
|
1849
|
+
.attr('width', boxW)
|
|
1850
|
+
.attr('height', fullH)
|
|
1851
|
+
.attr('rx', 6);
|
|
1852
|
+
|
|
1853
|
+
participantG
|
|
1854
|
+
.append('rect')
|
|
1855
|
+
.attr('class', 'sequence-drill-bar')
|
|
1856
|
+
.attr('x', -boxW / 2)
|
|
1857
|
+
.attr('y', -GROUP_PADDING_TOP + fullH - drillBarH)
|
|
1858
|
+
.attr('width', boxW)
|
|
1859
|
+
.attr('height', drillBarH)
|
|
1860
|
+
.attr('fill', drillColor)
|
|
1861
|
+
.attr('clip-path', `url(#${clipId})`);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// Render lifeline — collapsed groups start below the taller box
|
|
1865
|
+
const llY = isCollapsedGroup
|
|
1866
|
+
? lifelineStartY + GROUP_PADDING_BOTTOM
|
|
1867
|
+
: lifelineStartY;
|
|
1868
|
+
const llColor = isCollapsedGroup
|
|
1869
|
+
? effectiveTagColor || palette.textMuted
|
|
1870
|
+
: pTagColor || palette.textMuted;
|
|
1705
1871
|
const lifelineEl = svg
|
|
1706
1872
|
.append('line')
|
|
1707
1873
|
.attr('x1', cx)
|
|
1708
|
-
.attr('y1',
|
|
1874
|
+
.attr('y1', llY)
|
|
1709
1875
|
.attr('x2', cx)
|
|
1710
1876
|
.attr('y2', lifelineStartY + lifelineLength)
|
|
1711
|
-
.attr('stroke',
|
|
1877
|
+
.attr('stroke', llColor)
|
|
1712
1878
|
.attr('stroke-width', 1)
|
|
1713
1879
|
.attr('stroke-dasharray', '6 4')
|
|
1714
1880
|
.attr('class', 'lifeline')
|
|
@@ -2104,43 +2270,14 @@ export function renderSequenceDiagram(
|
|
|
2104
2270
|
? `${sec.label} (${msgCount} ${msgCount === 1 ? 'message' : 'messages'})`
|
|
2105
2271
|
: sec.label;
|
|
2106
2272
|
|
|
2107
|
-
// Collapsed sections use white text for contrast against the darker band
|
|
2108
|
-
const labelColor = isCollapsed ? '#ffffff' : lineColor;
|
|
2109
|
-
|
|
2110
|
-
// Chevron indicator
|
|
2111
|
-
const chevronSpace = 14;
|
|
2112
|
-
const labelX = (sectionLineX1 + sectionLineX2) / 2;
|
|
2113
|
-
const chevronX = labelX - (labelText.length * 3.5 + 8 + chevronSpace / 2);
|
|
2114
|
-
const chevronY = secY;
|
|
2115
|
-
if (isCollapsed) {
|
|
2116
|
-
// Right-pointing triangle ▶
|
|
2117
|
-
sectionG
|
|
2118
|
-
.append('path')
|
|
2119
|
-
.attr(
|
|
2120
|
-
'd',
|
|
2121
|
-
`M ${chevronX} ${chevronY - 4} L ${chevronX + 6} ${chevronY} L ${chevronX} ${chevronY + 4} Z`
|
|
2122
|
-
)
|
|
2123
|
-
.attr('fill', labelColor)
|
|
2124
|
-
.attr('class', 'section-chevron');
|
|
2125
|
-
} else {
|
|
2126
|
-
// Down-pointing triangle ▼
|
|
2127
|
-
sectionG
|
|
2128
|
-
.append('path')
|
|
2129
|
-
.attr(
|
|
2130
|
-
'd',
|
|
2131
|
-
`M ${chevronX - 1} ${chevronY - 3} L ${chevronX + 7} ${chevronY - 3} L ${chevronX + 3} ${chevronY + 3} Z`
|
|
2132
|
-
)
|
|
2133
|
-
.attr('fill', labelColor)
|
|
2134
|
-
.attr('class', 'section-chevron');
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
2273
|
// Centered label text
|
|
2274
|
+
const labelX = (sectionLineX1 + sectionLineX2) / 2;
|
|
2138
2275
|
sectionG
|
|
2139
2276
|
.append('text')
|
|
2140
|
-
.attr('x', labelX
|
|
2277
|
+
.attr('x', labelX)
|
|
2141
2278
|
.attr('y', secY + 4)
|
|
2142
2279
|
.attr('text-anchor', 'middle')
|
|
2143
|
-
.attr('fill',
|
|
2280
|
+
.attr('fill', lineColor)
|
|
2144
2281
|
.attr('font-size', 11)
|
|
2145
2282
|
.attr('font-weight', 'bold')
|
|
2146
2283
|
.attr('class', 'section-label')
|
package/src/sharing.ts
CHANGED
|
@@ -6,24 +6,45 @@ import {
|
|
|
6
6
|
const DEFAULT_BASE_URL = 'https://online.diagrammo.app';
|
|
7
7
|
const COMPRESSED_SIZE_LIMIT = 8192; // 8 KB
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Compact view state schema (ADR-6).
|
|
11
|
+
* All fields optional. Only non-default values are encoded.
|
|
12
|
+
* `tag: null` means "user chose none"; absent `tag` means "use DSL default" (ADR-5).
|
|
13
|
+
*/
|
|
14
|
+
export interface CompactViewState {
|
|
15
|
+
tag?: string | null; // active tag override (null = "none")
|
|
16
|
+
cs?: number[]; // collapsed sections (sequence line numbers)
|
|
17
|
+
cg?: string[]; // collapsed groups/nodes (IDs or names)
|
|
18
|
+
swim?: string | null; // swimlane tag group
|
|
19
|
+
cl?: string[]; // collapsed lanes
|
|
20
|
+
cc?: string[]; // collapsed columns (kanban)
|
|
21
|
+
rm?: string; // render mode override
|
|
22
|
+
htv?: Record<string, string[]>; // hidden tag values
|
|
23
|
+
ha?: string[]; // hidden attributes
|
|
24
|
+
enl?: number[]; // expanded note lines (sequence)
|
|
25
|
+
sem?: boolean; // semantic colors (ER)
|
|
26
|
+
cm?: boolean; // compact meta (kanban)
|
|
27
|
+
c4l?: string; // C4 level
|
|
28
|
+
c4s?: string; // C4 system
|
|
29
|
+
c4c?: string; // C4 container
|
|
30
|
+
rps?: number; // RPS multiplier (infra)
|
|
31
|
+
spd?: number; // playback speed (infra)
|
|
32
|
+
io?: Record<string, number>; // instance overrides (infra)
|
|
16
33
|
}
|
|
17
34
|
|
|
18
35
|
export interface DecodedDiagramUrl {
|
|
19
36
|
dsl: string;
|
|
20
|
-
viewState:
|
|
37
|
+
viewState: CompactViewState;
|
|
38
|
+
palette?: string;
|
|
39
|
+
theme?: 'light' | 'dark';
|
|
21
40
|
filename?: string;
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
export interface EncodeDiagramUrlOptions {
|
|
25
44
|
baseUrl?: string;
|
|
26
|
-
viewState?:
|
|
45
|
+
viewState?: CompactViewState;
|
|
46
|
+
palette?: string;
|
|
47
|
+
theme?: 'light' | 'dark';
|
|
27
48
|
filename?: string;
|
|
28
49
|
}
|
|
29
50
|
|
|
@@ -36,6 +57,34 @@ export type EncodeDiagramUrlResult =
|
|
|
36
57
|
limit: number;
|
|
37
58
|
};
|
|
38
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Encode a CompactViewState to a compressed string for URL embedding.
|
|
62
|
+
* Returns empty string if state has no keys (ADR-4).
|
|
63
|
+
*/
|
|
64
|
+
export function encodeViewState(state: CompactViewState): string {
|
|
65
|
+
const keys = Object.keys(state);
|
|
66
|
+
if (keys.length === 0) return '';
|
|
67
|
+
return compressToEncodedURIComponent(JSON.stringify(state));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Decode a compressed view state string back to CompactViewState.
|
|
72
|
+
* Returns empty object on failure (no crash).
|
|
73
|
+
*/
|
|
74
|
+
export function decodeViewState(encoded: string): CompactViewState {
|
|
75
|
+
if (!encoded) return {};
|
|
76
|
+
try {
|
|
77
|
+
const json = decompressFromEncodedURIComponent(encoded);
|
|
78
|
+
if (!json) return {};
|
|
79
|
+
const parsed = JSON.parse(json);
|
|
80
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
81
|
+
return {};
|
|
82
|
+
return parsed as CompactViewState;
|
|
83
|
+
} catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
39
88
|
/**
|
|
40
89
|
* Compress a DGMO DSL string into a shareable URL.
|
|
41
90
|
* Returns `{ url }` on success, or `{ error: 'too-large', compressedSize, limit }` if the
|
|
@@ -59,28 +108,21 @@ export function encodeDiagramUrl(
|
|
|
59
108
|
|
|
60
109
|
let hash = `dgmo=${compressed}`;
|
|
61
110
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (options?.viewState?.swimlaneTagGroup) {
|
|
71
|
-
hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (options?.viewState?.collapsedLanes?.length) {
|
|
75
|
-
hash += `&cl=${encodeURIComponent(options.viewState.collapsedLanes.join(','))}`;
|
|
111
|
+
// View state as single compressed blob (ADR-1)
|
|
112
|
+
if (options?.viewState) {
|
|
113
|
+
const vsEncoded = encodeViewState(options.viewState);
|
|
114
|
+
if (vsEncoded) {
|
|
115
|
+
hash += `&vs=${vsEncoded}`;
|
|
116
|
+
}
|
|
76
117
|
}
|
|
77
118
|
|
|
78
|
-
|
|
79
|
-
|
|
119
|
+
// Palette and theme are app-level, kept as separate params
|
|
120
|
+
if (options?.palette && options.palette !== 'nord') {
|
|
121
|
+
hash += `&pal=${encodeURIComponent(options.palette)}`;
|
|
80
122
|
}
|
|
81
123
|
|
|
82
|
-
if (options?.
|
|
83
|
-
hash += `&th=${encodeURIComponent(options.
|
|
124
|
+
if (options?.theme && options.theme !== 'dark') {
|
|
125
|
+
hash += `&th=${encodeURIComponent(options.theme)}`;
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
if (options?.filename) {
|
|
@@ -95,8 +137,8 @@ export function encodeDiagramUrl(
|
|
|
95
137
|
/**
|
|
96
138
|
* Decode a DGMO DSL string and view state from a URL query string or hash.
|
|
97
139
|
* Accepts any of:
|
|
98
|
-
* - `?dgmo=<payload>&
|
|
99
|
-
* - `#dgmo=<payload>&
|
|
140
|
+
* - `?dgmo=<payload>&vs=<state>`
|
|
141
|
+
* - `#dgmo=<payload>&vs=<state>` (backwards compat)
|
|
100
142
|
* - `dgmo=<payload>`
|
|
101
143
|
* - `<bare payload>`
|
|
102
144
|
*
|
|
@@ -105,6 +147,8 @@ export function encodeDiagramUrl(
|
|
|
105
147
|
export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
|
|
106
148
|
const empty: DecodedDiagramUrl = { dsl: '', viewState: {} };
|
|
107
149
|
let filename: string | undefined;
|
|
150
|
+
let palette: string | undefined;
|
|
151
|
+
let theme: 'light' | 'dark' | undefined;
|
|
108
152
|
if (!hash) return empty;
|
|
109
153
|
|
|
110
154
|
let raw = hash;
|
|
@@ -120,29 +164,22 @@ export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
|
|
|
120
164
|
const parts = raw.split('&');
|
|
121
165
|
let payload = parts[0];
|
|
122
166
|
|
|
123
|
-
// Parse extra params
|
|
124
|
-
|
|
167
|
+
// Parse extra params
|
|
168
|
+
let viewState: CompactViewState = {};
|
|
125
169
|
for (let i = 1; i < parts.length; i++) {
|
|
126
170
|
const eq = parts[i].indexOf('=');
|
|
127
171
|
if (eq === -1) continue;
|
|
128
172
|
const key = parts[i].slice(0, eq);
|
|
129
|
-
const val =
|
|
130
|
-
if (key === '
|
|
131
|
-
viewState
|
|
132
|
-
}
|
|
133
|
-
if (key === 'cg' && val) {
|
|
134
|
-
viewState.collapsedGroups = val.split(',').filter(Boolean);
|
|
135
|
-
}
|
|
136
|
-
if (key === 'swim' && val) {
|
|
137
|
-
viewState.swimlaneTagGroup = val;
|
|
173
|
+
const val = parts[i].slice(eq + 1);
|
|
174
|
+
if (key === 'vs' && val) {
|
|
175
|
+
viewState = decodeViewState(val);
|
|
138
176
|
}
|
|
139
|
-
if (key === '
|
|
140
|
-
|
|
177
|
+
if (key === 'pal' && val) palette = decodeURIComponent(val);
|
|
178
|
+
if (key === 'th') {
|
|
179
|
+
const decoded = decodeURIComponent(val);
|
|
180
|
+
if (decoded === 'light' || decoded === 'dark') theme = decoded;
|
|
141
181
|
}
|
|
142
|
-
if (key === '
|
|
143
|
-
if (key === 'th' && (val === 'light' || val === 'dark'))
|
|
144
|
-
viewState.theme = val;
|
|
145
|
-
if (key === 'fn' && val) filename = val;
|
|
182
|
+
if (key === 'fn' && val) filename = decodeURIComponent(val);
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
// Strip 'dgmo=' prefix
|
|
@@ -150,12 +187,12 @@ export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
|
|
|
150
187
|
payload = payload.slice(5);
|
|
151
188
|
}
|
|
152
189
|
|
|
153
|
-
if (!payload) return { dsl: '', viewState, filename };
|
|
190
|
+
if (!payload) return { dsl: '', viewState, palette, theme, filename };
|
|
154
191
|
|
|
155
192
|
try {
|
|
156
193
|
const result = decompressFromEncodedURIComponent(payload);
|
|
157
|
-
return { dsl: result ?? '', viewState, filename };
|
|
194
|
+
return { dsl: result ?? '', viewState, palette, theme, filename };
|
|
158
195
|
} catch {
|
|
159
|
-
return { dsl: '', viewState, filename };
|
|
196
|
+
return { dsl: '', viewState, palette, theme, filename };
|
|
160
197
|
}
|
|
161
198
|
}
|
|
@@ -54,3 +54,14 @@ export const EYE_OPEN_PATH =
|
|
|
54
54
|
'M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z';
|
|
55
55
|
export const EYE_CLOSED_PATH =
|
|
56
56
|
'M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1';
|
|
57
|
+
|
|
58
|
+
// ── Controls group constants ────────────────────────────────
|
|
59
|
+
// Gear/cog icon (14×14 viewBox) — 6 flat teeth with center hole
|
|
60
|
+
// Computed from polar coordinates: outerR=5.5, innerR=3.5, holeR=2, center=(7,7)
|
|
61
|
+
// Uses evenodd fill-rule for the center hole
|
|
62
|
+
export const CONTROLS_ICON_PATH =
|
|
63
|
+
'M5.6 1.7L8.4 1.7L7.9 3.6L9.5 4.5L10.9 3.1L12.3 5.6L10.4 6.1L10.4 7.9L12.3 8.4L10.9 10.9L9.5 9.5L7.9 10.4L8.4 12.3L5.6 12.3L6.1 10.4L4.5 9.5L3.1 10.9L1.7 8.4L3.6 7.9L3.6 6.1L1.7 5.6L3.1 3.1L4.5 4.5L6.1 3.6Z' +
|
|
64
|
+
'M5 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0Z';
|
|
65
|
+
export const LEGEND_TOGGLE_DOT_R = LEGEND_DOT_R;
|
|
66
|
+
export const LEGEND_TOGGLE_OFF_OPACITY = 0.4;
|
|
67
|
+
export const LEGEND_GEAR_PILL_W = 14 + LEGEND_PILL_PAD; // gear icon (14) + padding
|