@diagrammo/dgmo 0.8.19 → 0.8.20

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.
@@ -114,6 +114,79 @@ no-option-name // off
114
114
  - Bare keyword = on; `no-` prefix = off
115
115
  - Must appear before diagram content
116
116
 
117
+ ### 1.9 In-Arrow Message Labels
118
+
119
+ An **in-arrow label** is the text embedded inside an arrow between the opening delimiter and the arrow token, as in `A -label-> B`.
120
+
121
+ ```
122
+ A -label-> B
123
+ ^ ^---^ ^^
124
+ | | ||
125
+ | | |+- destination id
126
+ | | +- arrow token
127
+ | +- label text (plain, no markdown)
128
+ +- opening delimiter (matches arrow type)
129
+ ```
130
+
131
+ **Chart types that support in-arrow labels**: sequence, flowchart, state, infra, c4, er, class, boxes-and-lines.
132
+
133
+ #### Cheat sheet
134
+
135
+ ```
136
+ // happy-path: labels are plain text with punctuation allowed
137
+ A -location[]-> B // label = "location[]"
138
+ A -a[b]c-> B // label = "a[b]c"
139
+ A -{json}-> B // label = "{json}"
140
+
141
+ // unicode: all scripts and emoji preserved verbatim
142
+ A -café-> B
143
+ A -日本語-> B
144
+ A -🎉-> B
145
+
146
+ // punctuation is literal — no markdown interpretation
147
+ A -(parenthetical)-> B // label = "(parenthetical)" (NOT a color)
148
+ A -*emphasis*-> B // label = "*emphasis*" (NOT bold)
149
+ A -`code`-> B // label = "`code`" (NOT a code span)
150
+
151
+ // forbidden: -> and ~> substrings inside a label
152
+ A -uses -> chain-> B // ERROR (E_ARROW_SUBSTRING_IN_LABEL)
153
+ // migration: move the label to the post-colon form
154
+ A -> B: uses -> chain // works for charts that accept post-colon labels
155
+
156
+ // migration from pre-gauntlet (legacy) syntax
157
+ A -Makes calls [HTTP]-> B // label is now the FULL "Makes calls [HTTP]"
158
+ A -Makes calls-> B | tech: HTTP // preferred: technology on target metadata
159
+ ```
160
+
161
+ #### Character-set contract
162
+
163
+ - **Allowed**: any Unicode codepoint except the forbidden list below. Brackets `[] {} ()`, pipes `|`, quotes `"' `, backticks, punctuation, digits, emoji, ZWJ sequences, combining marks — all pass through as literal characters.
164
+ - **Forbidden substrings**: `->` and `~>`. These terminate the arrow. If you need them inside a label, use the post-colon form (`A -> B: uses -> to chain`) on chart types that support it; there is no escape mechanism.
165
+ - **Forbidden characters**: C0 control characters U+0000–U+001F except U+0009 (tab), and U+007F (DEL). Silent renderer breakage and log-injection surface — no legitimate use case.
166
+ - **Whitespace**: leading and trailing whitespace is trimmed; internal whitespace runs (including tabs, non-breaking spaces, and zero-width spaces) are **preserved**, never collapsed.
167
+ - **Plain text only**: no markdown interpretation. `*foo*` renders as `*foo*`, not italicized. `[label](url)` renders as literal `[label](url)`, not a hyperlink. Clickable URLs belong in notes, not in in-arrow labels.
168
+ - **HTML-safe**: all renderers emit label text as a DOM text node. `<script>alert(1)</script>` renders as literal text — the entire label is a sequence of codepoints, not a markup fragment.
169
+
170
+ #### Color suffix (flowchart and state only)
171
+
172
+ ```
173
+ A -(red)-> B // colored edge, no label
174
+ A -(notacolor)-> B // label = "(notacolor)" (fall-through)
175
+ A -(red) uses-> B // label = "(red) uses" (combined form not supported)
176
+ A -red-> B // label = "red" (bare word is always a label)
177
+ ```
178
+
179
+ A parenthesized palette color is only recognized when the entire label between the opening `-` and the arrow token is exactly `(colorName)` and `colorName` is one of the 11 names in §1.5. Any other content falls through to the label. To combine a color and a label, use the post-colon or pipe-metadata form instead.
180
+
181
+ #### Migrating from pre-gauntlet syntax
182
+
183
+ Two legacy forms changed with this spec:
184
+
185
+ 1. **C4 trailing `[technology]` sugar is removed.** A C4 arrow like `-Makes calls [HTTPS]-> API` used to extract `HTTPS` as the technology annotation. The full `Makes calls [HTTPS]` is now the label. Use the post-colon or pipe form for technology: `-Makes calls-> API | tech: HTTPS`.
186
+ 2. **Bare palette color suffixes are a literal label.** `A -red-> B` on flowchart/state used to be accepted as a bare color suffix in some surfaces. It is now always a label with text `red`. To color an edge, use the `-(red)->` parens form.
187
+
188
+ No code migration is required for in-arrow label character escaping — any label that was valid before remains valid, with one exception: if your label happened to contain the literal substring `->` or `~>`, the parser now rejects it with `E_ARROW_SUBSTRING_IN_LABEL`. Move those labels to the post-colon form.
189
+
117
190
  ---
118
191
 
119
192
  ## 2. Sequence Diagrams
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.8.19",
3
+ "version": "0.8.20",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -55,10 +55,12 @@
55
55
  ],
56
56
  "sideEffects": false,
57
57
  "scripts": {
58
- "prebuild": "rm -rf dist && lezer-generator src/editor/dgmo.grammar -o src/editor/dgmo.grammar.js",
58
+ "codegen": "lezer-generator src/editor/dgmo.grammar -o src/editor/dgmo.grammar.js",
59
+ "prebuild": "rm -rf dist && pnpm codegen",
59
60
  "build": "tsup",
60
61
  "typecheck": "tsc --noEmit",
61
62
  "dev": "tsup --watch",
63
+ "pretest": "pnpm codegen",
62
64
  "test": "vitest run",
63
65
  "test:watch": "vitest",
64
66
  "gallery": "pnpm build && node scripts/generate-gallery.mjs",
@@ -69,7 +71,13 @@
69
71
  "check:duplication": "jscpd ./src",
70
72
  "check:deadcode": "knip",
71
73
  "check:spelling": "cspell \"src/**/*.ts\" \"tests/**/*.ts\"",
74
+ "check:all": "pnpm check:deadcode && pnpm check:spelling && pnpm check:duplication && pnpm check:circular && pnpm check:deps && pnpm check:security && pnpm build && pnpm check:publish && pnpm check:types",
75
+ "check:circular": "madge --circular --extensions ts src/ --json | node -e \"const c=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); const n=c.length; if(n>7){console.error('New circular deps found ('+n+' > 7 known type-only cycles)');process.exit(1)}else if(n>0){console.log(n+' known type-only/dynamic cycles (safe)')}else{console.log('No circular dependencies')}\"",
76
+ "check:deps": "depcheck --ignores='@codemirror/language,@lezer/*,husky,lint-staged,tsup'",
77
+ "check:security": "pnpm audit --prod",
78
+ "check:publish": "publint",
72
79
  "check:size": "pnpm build && du -sh dist/ && echo '---' && ls -lh dist/*.js dist/*.cjs",
80
+ "check:types": "attw --pack . --ignore-rules no-resolution",
73
81
  "postinstall": "node -e \"console.log('\\n💡 Claude Code user? Run: dgmo --install-claude-skill\\n')\"",
74
82
  "prepare": "husky"
75
83
  },
@@ -83,10 +91,11 @@
83
91
  "d3-selection": "^3.0.0",
84
92
  "d3-shape": "^3.2.0",
85
93
  "echarts": "^6.0.0",
86
- "jsdom": "^29.0.1",
94
+ "jsdom": "^29.0.2",
87
95
  "lz-string": "^1.5.0"
88
96
  },
89
97
  "devDependencies": {
98
+ "@arethetypeswrong/cli": "^0.18.2",
90
99
  "@codemirror/language": "^6.12.3",
91
100
  "@eslint/js": "^10.0.1",
92
101
  "@lezer/generator": "^1.8.0",
@@ -97,18 +106,22 @@
97
106
  "@types/d3-selection": "^3.0.11",
98
107
  "@types/d3-shape": "^3.1.8",
99
108
  "@types/jsdom": "^28.0.1",
100
- "cspell": "^9.7.0",
109
+ "cspell": "^10.0.0",
110
+ "depcheck": "^1.4.7",
101
111
  "esbuild": "^0.28.0",
102
112
  "eslint": "^10.2.0",
113
+ "eslint-plugin-security": "^4.0.0",
103
114
  "husky": "^9.1.7",
104
- "jscpd": "^4.0.8",
105
- "knip": "^6.3.0",
115
+ "jscpd": "^4.0.9",
116
+ "knip": "^6.3.1",
106
117
  "lint-staged": "^16.4.0",
107
- "prettier": "^3.8.1",
118
+ "madge": "^8.0.0",
119
+ "prettier": "^3.8.2",
120
+ "publint": "^0.3.18",
108
121
  "tsup": "^8.5.1",
109
122
  "typescript": "^6.0.2",
110
- "typescript-eslint": "^8.58.0",
111
- "vitest": "^4.1.2"
123
+ "typescript-eslint": "^8.58.1",
124
+ "vitest": "^4.1.4"
112
125
  },
113
126
  "lint-staged": {
114
127
  "*.ts": [
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { makeDgmoError, suggest } from '../diagnostics';
6
6
  import type { DgmoError } from '../diagnostics';
7
+ import { parseInArrowLabel } from '../utils/arrows';
7
8
  import type { ParsedBoxesAndLines, BLNode, BLEdge, BLGroup } from './types';
8
9
  import {
9
10
  matchTagBlockHeading,
@@ -607,7 +608,9 @@ function parseEdgeLine(
607
608
  const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
608
609
  if (biLabeledMatch) {
609
610
  const source = resolveEndpoint(biLabeledMatch[1].trim());
610
- const label = biLabeledMatch[2].trim();
611
+ const labelResult = parseInArrowLabel(biLabeledMatch[2], lineNum);
612
+ diagnostics.push(...labelResult.diagnostics);
613
+ const label = labelResult.label;
611
614
  let rest = biLabeledMatch[3].trim();
612
615
 
613
616
  let metadata: Record<string, string> = {};
@@ -631,7 +634,7 @@ function parseEdgeLine(
631
634
  return {
632
635
  source,
633
636
  target: resolveEndpoint(rest),
634
- label: label || undefined,
637
+ label,
635
638
  bidirectional: true,
636
639
  lineNumber: lineNum,
637
640
  metadata,
@@ -675,7 +678,9 @@ function parseEdgeLine(
675
678
  const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
676
679
  if (labeledMatch) {
677
680
  const source = resolveEndpoint(labeledMatch[1].trim());
678
- const label = labeledMatch[2].trim();
681
+ const labelResult = parseInArrowLabel(labeledMatch[2], lineNum);
682
+ diagnostics.push(...labelResult.diagnostics);
683
+ const label = labelResult.label;
679
684
  let rest = labeledMatch[3].trim();
680
685
 
681
686
  if (label) {
package/src/c4/parser.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { PaletteColors } from '../palettes';
6
6
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
7
+ import { parseInArrowLabel } from '../utils/arrows';
7
8
  import type { TagGroup } from '../utils/tag-groups';
8
9
  import {
9
10
  matchTagBlockHeading,
@@ -519,14 +520,14 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
519
520
  break;
520
521
  }
521
522
 
522
- // Extract [technology] from end of label
523
- let label: string | undefined = rawLabel;
523
+ // TD-5: the trailing `[tech]` sugar is no longer extracted from the
524
+ // in-arrow label. The entire label stays as the label. Technology
525
+ // metadata comes from post-colon or pipe metadata on the target.
526
+ // Also run TD-13/TD-14 validation on the label characters.
527
+ const labelResult = parseInArrowLabel(rawLabel, lineNumber);
528
+ labelResult.diagnostics.forEach((d) => result.diagnostics.push(d));
529
+ const label: string | undefined = labelResult.label;
524
530
  let technology: string | undefined;
525
- const techMatch = rawLabel.match(/\[([^\]]+)\]\s*$/);
526
- if (techMatch) {
527
- label = rawLabel.substring(0, techMatch.index!).trim() || undefined;
528
- technology = techMatch[1].trim();
529
- }
530
531
 
531
532
  // Extract pipe metadata from target body (e.g. "Database | tech: SQL")
532
533
  let target = targetBody;
@@ -1,6 +1,7 @@
1
1
  import { resolveColorWithDiagnostic } from '../colors';
2
2
  import type { PaletteColors } from '../palettes';
3
3
  import { makeDgmoError, formatDgmoError } from '../diagnostics';
4
+ import { validateLabelCharacters } from '../utils/arrows';
4
5
  import {
5
6
  measureIndent,
6
7
  parseFirstLine,
@@ -251,6 +252,11 @@ export function parseClassDiagram(
251
252
 
252
253
  getOrCreateClass(targetName, lineNumber);
253
254
 
255
+ if (label) {
256
+ result.diagnostics.push(
257
+ ...validateLabelCharacters(label, lineNumber)
258
+ );
259
+ }
254
260
  result.relationships.push({
255
261
  source: currentClass.id,
256
262
  target: classId(targetName),
package/src/cli.ts CHANGED
@@ -160,7 +160,6 @@ Key options:
160
160
  - \`--theme <theme>\` — \`light\` (default), \`dark\`, \`transparent\`
161
161
  - \`--palette <name>\` — \`nord\` (default), \`solarized\`, \`catppuccin\`, \`rose-pine\`, \`gruvbox\`, \`tokyo-night\`, \`one-dark\`, \`bold\`
162
162
  - \`--copy\` — copy the URL to clipboard (use with \`-o url\`)
163
- - \`--branding\` — add diagrammo.app branding to exports
164
163
  - \`--chart-types\` — list all supported chart types
165
164
 
166
165
  ## Supported Chart Types
@@ -502,7 +501,6 @@ Options:
502
501
  --c4-system <name> System to drill into (with --c4-level containers or components)
503
502
  --c4-container <name> Container to drill into (with --c4-level components)
504
503
  --tag-group <name> Pre-select a tag group for static export coloring
505
- --branding Add diagrammo.app branding to exports
506
504
  --copy Copy URL to clipboard (only with -o url)
507
505
  --json Output structured JSON to stdout
508
506
  --chart-types List all supported chart types
@@ -532,7 +530,6 @@ function parseArgs(argv: string[]): {
532
530
  palette: string;
533
531
  help: boolean;
534
532
  version: boolean;
535
- branding: boolean;
536
533
  copy: boolean;
537
534
  json: boolean;
538
535
  chartTypes: boolean;
@@ -553,7 +550,6 @@ function parseArgs(argv: string[]): {
553
550
  palette: 'nord',
554
551
  help: false,
555
552
  version: false,
556
- branding: false,
557
553
  copy: false,
558
554
  json: false,
559
555
  chartTypes: false,
@@ -637,9 +633,6 @@ function parseArgs(argv: string[]): {
637
633
  } else if (arg === '--tag-group') {
638
634
  result.tagGroup = args[++i];
639
635
  i++;
640
- } else if (arg === '--branding') {
641
- result.branding = true;
642
- i++;
643
636
  } else if (arg === '--json') {
644
637
  result.json = true;
645
638
  i++;
@@ -1251,10 +1244,9 @@ async function main(): Promise<void> {
1251
1244
  }
1252
1245
  }
1253
1246
 
1254
- const svg = await render(content, {
1247
+ const { svg } = await render(content, {
1255
1248
  theme: opts.theme,
1256
1249
  palette: opts.palette,
1257
- branding: opts.branding,
1258
1250
  c4Level: opts.c4Level,
1259
1251
  c4System: opts.c4System,
1260
1252
  c4Container: opts.c4Container,
package/src/d3.ts CHANGED
@@ -4,8 +4,9 @@ import * as d3Shape from 'd3-shape';
4
4
  import * as d3Array from 'd3-array';
5
5
  import cloud from 'd3-cloud';
6
6
  import { FONT_FAMILY } from './fonts';
7
- import { injectBranding } from './branding';
8
7
  import { computeQuadrantPointLabels, type LabelRect } from './label-layout';
8
+ import { MONTH_ABBR, computeTimeTicks } from './utils/time-ticks';
9
+ import type { D3ExportDimensions } from './utils/d3-types';
9
10
 
10
11
  // ============================================================
11
12
  // Types
@@ -133,10 +134,7 @@ interface QuadrantLabels {
133
134
  }
134
135
 
135
136
  /** Optional explicit dimensions for CLI/export rendering (bypasses DOM layout). */
136
- export interface D3ExportDimensions {
137
- width?: number;
138
- height?: number;
139
- }
137
+ export type { D3ExportDimensions } from './utils/d3-types';
140
138
 
141
139
  export interface ParsedVisualization {
142
140
  type: VisualizationType | null;
@@ -331,34 +329,6 @@ export function parseTimelineDate(s: string): number {
331
329
  );
332
330
  }
333
331
 
334
- /** Convert a fractional year number back to a Date (inverse of parseTimelineDate). */
335
- function fractionalYearToDate(frac: number): Date {
336
- const year = Math.floor(frac);
337
- const remainder = frac - year;
338
- // Inverse of: (month-1)/12 + (day-1)/365 + hour/8760 + minute/525600
339
- const monthFrac = remainder * 12;
340
- const month = Math.floor(monthFrac); // 0-based
341
- const monthRemainder = remainder - month / 12;
342
- const dayFrac = monthRemainder * 365; // fractional day-of-year offset
343
- const day = Math.floor(dayFrac) + 1;
344
- const dayRemainder = dayFrac - Math.floor(dayFrac);
345
- const hourFrac = dayRemainder * 24;
346
- const hour = Math.floor(hourFrac);
347
- const minute = Math.round((hourFrac - hour) * 60);
348
- return new Date(year, month, day, hour, minute);
349
- }
350
-
351
- /** Convert a Date to a fractional year number. */
352
- function dateToFractionalYear(d: Date): number {
353
- return (
354
- d.getFullYear() +
355
- d.getMonth() / 12 +
356
- (d.getDate() - 1) / 365 +
357
- d.getHours() / 8760 +
358
- d.getMinutes() / 525600
359
- );
360
- }
361
-
362
332
  /**
363
333
  * Adds a duration to a date string and returns the resulting date string.
364
334
  * Supports: d (days), w (weeks), m (months), y (years), h (hours), min (minutes)
@@ -2885,21 +2855,6 @@ function renderMarkers(
2885
2855
  // Timeline Time Scale
2886
2856
  // ============================================================
2887
2857
 
2888
- const MONTH_ABBR = [
2889
- 'Jan',
2890
- 'Feb',
2891
- 'Mar',
2892
- 'Apr',
2893
- 'May',
2894
- 'Jun',
2895
- 'Jul',
2896
- 'Aug',
2897
- 'Sep',
2898
- 'Oct',
2899
- 'Nov',
2900
- 'Dec',
2901
- ];
2902
-
2903
2858
  /**
2904
2859
  * Converts a DSL date string (YYYY, YYYY-MM, YYYY-MM-DD, or YYYY-MM-DD HH:MM) to a human-readable label.
2905
2860
  * '1718' → '1718'
@@ -2947,173 +2902,6 @@ function formatBoundaryLabel(dateStr: string, otherDateStr: string): string {
2947
2902
  return formatDateLabel(dateStr);
2948
2903
  }
2949
2904
 
2950
- /**
2951
- * Computes adaptive tick marks for a timeline scale.
2952
- * - Multi-year spans → year ticks
2953
- * - Within ~1 year → month ticks
2954
- * - Within ~3 months → week ticks (1st, 8th, 15th, 22nd)
2955
- *
2956
- * Optional boundary parameters add ticks at exact data start/end:
2957
- * - boundaryStart/boundaryEnd: numeric date values
2958
- * - boundaryStartLabel/boundaryEndLabel: formatted labels for those dates
2959
- */
2960
- export function computeTimeTicks(
2961
- domainMin: number,
2962
- domainMax: number,
2963
- scale: d3Scale.ScaleLinear<number, number>,
2964
- boundaryStart?: number,
2965
- boundaryEnd?: number,
2966
- boundaryStartLabel?: string,
2967
- boundaryEndLabel?: string
2968
- ): { pos: number; label: string }[] {
2969
- const minYear = Math.floor(domainMin);
2970
- const maxYear = Math.floor(domainMax);
2971
- const span = domainMax - domainMin;
2972
-
2973
- let ticks: { pos: number; label: string }[] = [];
2974
-
2975
- // Year ticks for multi-year spans (need at least 2 boundaries)
2976
- const firstYear = Math.ceil(domainMin);
2977
- const lastYear = Math.floor(domainMax);
2978
- if (lastYear >= firstYear + 1) {
2979
- // Decimate ticks for long spans so labels don't overlap
2980
- const yearSpan = lastYear - firstYear;
2981
- let step = 1;
2982
- if (yearSpan > 80) step = 20;
2983
- else if (yearSpan > 40) step = 10;
2984
- else if (yearSpan > 20) step = 5;
2985
- else if (yearSpan > 10) step = 2;
2986
-
2987
- // Align to step boundary so ticks land on round years (1700, 1710, …)
2988
- const alignedFirst = Math.ceil(firstYear / step) * step;
2989
- for (let y = alignedFirst; y <= lastYear; y += step) {
2990
- ticks.push({ pos: scale(y), label: String(y) });
2991
- }
2992
- } else if (span > 0.25) {
2993
- // Month ticks for spans > ~3 months
2994
- const crossesYear = maxYear > minYear;
2995
- for (let y = minYear; y <= maxYear + 1; y++) {
2996
- for (let m = 1; m <= 12; m++) {
2997
- const val = y + (m - 1) / 12;
2998
- if (val > domainMax) break;
2999
- if (val >= domainMin) {
3000
- ticks.push({
3001
- pos: scale(val),
3002
- label: crossesYear
3003
- ? `${MONTH_ABBR[m - 1]} '${String(y).slice(-2)}`
3004
- : MONTH_ABBR[m - 1],
3005
- });
3006
- }
3007
- }
3008
- }
3009
- } else if (span <= 0.000685) {
3010
- // Minute ticks for spans ≤ ~6 hours
3011
- // Adaptive step: >3h → 30min, >1h → 15min, >30min → 10min, else 5min
3012
- let stepMin = 5;
3013
- const spanHours = span * 8760;
3014
- if (spanHours > 3) stepMin = 30;
3015
- else if (spanHours > 1) stepMin = 15;
3016
- else if (spanHours > 0.5) stepMin = 10;
3017
-
3018
- // Iterate from the start hour boundary
3019
- const startDate = fractionalYearToDate(domainMin);
3020
- // Round down to nearest step boundary
3021
- startDate.setMinutes(
3022
- Math.floor(startDate.getMinutes() / stepMin) * stepMin,
3023
- 0,
3024
- 0
3025
- );
3026
-
3027
- while (true) {
3028
- const val = dateToFractionalYear(startDate);
3029
- if (val > domainMax) break;
3030
- if (val >= domainMin) {
3031
- const hh = String(startDate.getHours()).padStart(2, '0');
3032
- const mm = String(startDate.getMinutes()).padStart(2, '0');
3033
- ticks.push({ pos: scale(val), label: `${hh}:${mm}` });
3034
- }
3035
- startDate.setMinutes(startDate.getMinutes() + stepMin);
3036
- }
3037
- } else if (span <= 0.00822) {
3038
- // Hour ticks for spans ≤ ~3 days
3039
- // Adaptive step: >2d → 6h, >1d → 3h, >12h → 2h, else 1h
3040
- let stepHour = 1;
3041
- const spanHours = span * 8760;
3042
- if (spanHours > 48) stepHour = 6;
3043
- else if (spanHours > 24) stepHour = 3;
3044
- else if (spanHours > 12) stepHour = 2;
3045
-
3046
- // For single-day spans, just show HH:MM without the date prefix
3047
- const singleDay = spanHours <= 24;
3048
-
3049
- const startDate = fractionalYearToDate(domainMin);
3050
- // Round down to nearest step boundary
3051
- startDate.setHours(
3052
- Math.floor(startDate.getHours() / stepHour) * stepHour,
3053
- 0,
3054
- 0,
3055
- 0
3056
- );
3057
-
3058
- while (true) {
3059
- const val = dateToFractionalYear(startDate);
3060
- if (val > domainMax) break;
3061
- if (val >= domainMin) {
3062
- const hh = String(startDate.getHours()).padStart(2, '0');
3063
- const mm = String(startDate.getMinutes()).padStart(2, '0');
3064
- if (singleDay) {
3065
- ticks.push({ pos: scale(val), label: `${hh}:${mm}` });
3066
- } else {
3067
- const mon = MONTH_ABBR[startDate.getMonth()];
3068
- const d = startDate.getDate();
3069
- ticks.push({ pos: scale(val), label: `${mon} ${d} ${hh}:${mm}` });
3070
- }
3071
- }
3072
- startDate.setHours(startDate.getHours() + stepHour);
3073
- }
3074
- } else {
3075
- // Week ticks for spans ≤ ~3 months (1st, 8th, 15th, 22nd of each month)
3076
- for (let y = minYear; y <= maxYear + 1; y++) {
3077
- for (let m = 1; m <= 12; m++) {
3078
- for (const d of [1, 8, 15, 22]) {
3079
- const val = y + (m - 1) / 12 + (d - 1) / 365;
3080
- if (val > domainMax) break;
3081
- if (val >= domainMin) {
3082
- ticks.push({
3083
- pos: scale(val),
3084
- label: `${MONTH_ABBR[m - 1]} ${d}`,
3085
- });
3086
- }
3087
- }
3088
- }
3089
- }
3090
- }
3091
-
3092
- // Add boundary ticks at exact data start/end if provided
3093
- // When a boundary tick collides with a standard tick, replace the standard tick
3094
- const collisionThreshold = 40; // pixels
3095
-
3096
- if (boundaryStart !== undefined && boundaryStartLabel) {
3097
- const boundaryPos = scale(boundaryStart);
3098
- // Remove any standard ticks that would collide with the start boundary
3099
- ticks = ticks.filter(
3100
- (t) => Math.abs(t.pos - boundaryPos) >= collisionThreshold
3101
- );
3102
- ticks.unshift({ pos: boundaryPos, label: boundaryStartLabel });
3103
- }
3104
-
3105
- if (boundaryEnd !== undefined && boundaryEndLabel) {
3106
- const boundaryPos = scale(boundaryEnd);
3107
- // Remove any standard ticks that would collide with the end boundary
3108
- ticks = ticks.filter(
3109
- (t) => Math.abs(t.pos - boundaryPos) >= collisionThreshold
3110
- );
3111
- ticks.push({ pos: boundaryPos, label: boundaryEndLabel });
3112
- }
3113
-
3114
- return ticks;
3115
- }
3116
-
3117
2905
  /**
3118
2906
  * Renders adaptive tick marks along the time axis.
3119
2907
  * Optional boundary parameters add ticks at exact data start/end.
@@ -6678,8 +6466,7 @@ function createExportContainer(width: number, height: number): HTMLDivElement {
6678
6466
  function finalizeSvgExport(
6679
6467
  container: HTMLDivElement,
6680
6468
  theme: string,
6681
- palette: PaletteColors,
6682
- options?: { branding?: boolean }
6469
+ palette: PaletteColors
6683
6470
  ): string {
6684
6471
  const svgEl = container.querySelector('svg');
6685
6472
  if (!svgEl) return '';
@@ -6694,10 +6481,6 @@ function finalizeSvgExport(
6694
6481
  svgEl.querySelectorAll('[data-export-ignore]').forEach((el) => el.remove());
6695
6482
  const svgHtml = svgEl.outerHTML;
6696
6483
  document.body.removeChild(container);
6697
- if (options?.branding !== false) {
6698
- const brandColor = theme === 'transparent' ? '#888' : palette.textMuted;
6699
- return injectBranding(svgHtml, brandColor);
6700
- }
6701
6484
  return svgHtml;
6702
6485
  }
6703
6486
 
@@ -6716,7 +6499,6 @@ export async function renderForExport(
6716
6499
  swimlaneTagGroup?: string | null;
6717
6500
  },
6718
6501
  options?: {
6719
- branding?: boolean;
6720
6502
  c4Level?: 'context' | 'containers' | 'components' | 'deployment';
6721
6503
  c4System?: string;
6722
6504
  c4Container?: string;
@@ -6778,7 +6560,7 @@ export async function renderForExport(
6778
6560
  activeTagGroup,
6779
6561
  hiddenAttributes
6780
6562
  );
6781
- return finalizeSvgExport(container, theme, effectivePalette, options);
6563
+ return finalizeSvgExport(container, theme, effectivePalette);
6782
6564
  }
6783
6565
 
6784
6566
  if (detectedType === 'sitemap') {
@@ -6832,7 +6614,7 @@ export async function renderForExport(
6832
6614
  activeTagGroup,
6833
6615
  hiddenAttributes
6834
6616
  );
6835
- return finalizeSvgExport(container, theme, effectivePalette, options);
6617
+ return finalizeSvgExport(container, theme, effectivePalette);
6836
6618
  }
6837
6619
 
6838
6620
  if (detectedType === 'kanban') {
@@ -6856,7 +6638,7 @@ export async function renderForExport(
6856
6638
  options?.tagGroup
6857
6639
  ),
6858
6640
  });
6859
- return finalizeSvgExport(container, theme, effectivePalette, options);
6641
+ return finalizeSvgExport(container, theme, effectivePalette);
6860
6642
  }
6861
6643
 
6862
6644
  if (detectedType === 'class') {
@@ -6884,7 +6666,7 @@ export async function renderForExport(
6884
6666
  undefined,
6885
6667
  { width: exportWidth, height: exportHeight }
6886
6668
  );
6887
- return finalizeSvgExport(container, theme, effectivePalette, options);
6669
+ return finalizeSvgExport(container, theme, effectivePalette);
6888
6670
  }
6889
6671
 
6890
6672
  if (detectedType === 'er') {
@@ -6917,7 +6699,7 @@ export async function renderForExport(
6917
6699
  options?.tagGroup
6918
6700
  )
6919
6701
  );
6920
- return finalizeSvgExport(container, theme, effectivePalette, options);
6702
+ return finalizeSvgExport(container, theme, effectivePalette);
6921
6703
  }
6922
6704
 
6923
6705
  if (detectedType === 'boxes-and-lines') {
@@ -6948,7 +6730,7 @@ export async function renderForExport(
6948
6730
  activeTagGroup: options?.tagGroup,
6949
6731
  }
6950
6732
  );
6951
- return finalizeSvgExport(container, theme, effectivePalette, options);
6733
+ return finalizeSvgExport(container, theme, effectivePalette);
6952
6734
  }
6953
6735
 
6954
6736
  if (detectedType === 'c4') {
@@ -7009,7 +6791,7 @@ export async function renderForExport(
7009
6791
  options?.tagGroup
7010
6792
  )
7011
6793
  );
7012
- return finalizeSvgExport(container, theme, effectivePalette, options);
6794
+ return finalizeSvgExport(container, theme, effectivePalette);
7013
6795
  }
7014
6796
 
7015
6797
  if (detectedType === 'flowchart') {
@@ -7033,7 +6815,7 @@ export async function renderForExport(
7033
6815
  undefined,
7034
6816
  { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
7035
6817
  );
7036
- return finalizeSvgExport(container, theme, effectivePalette, options);
6818
+ return finalizeSvgExport(container, theme, effectivePalette);
7037
6819
  }
7038
6820
 
7039
6821
  if (detectedType === 'infra') {
@@ -7086,7 +6868,7 @@ export async function renderForExport(
7086
6868
  infraSvg.setAttribute('width', String(exportWidth));
7087
6869
  infraSvg.setAttribute('height', String(exportHeight));
7088
6870
  }
7089
- return finalizeSvgExport(container, theme, effectivePalette, options);
6871
+ return finalizeSvgExport(container, theme, effectivePalette);
7090
6872
  }
7091
6873
 
7092
6874
  if (detectedType === 'gantt') {
@@ -7111,7 +6893,7 @@ export async function renderForExport(
7111
6893
  undefined,
7112
6894
  { width: EXPORT_W, height: EXPORT_H }
7113
6895
  );
7114
- return finalizeSvgExport(container, theme, effectivePalette, options);
6896
+ return finalizeSvgExport(container, theme, effectivePalette);
7115
6897
  }
7116
6898
 
7117
6899
  if (detectedType === 'state') {
@@ -7135,7 +6917,7 @@ export async function renderForExport(
7135
6917
  undefined,
7136
6918
  { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
7137
6919
  );
7138
- return finalizeSvgExport(container, theme, effectivePalette, options);
6920
+ return finalizeSvgExport(container, theme, effectivePalette);
7139
6921
  }
7140
6922
 
7141
6923
  const parsed = parseVisualization(content, palette);
@@ -7235,5 +7017,5 @@ export async function renderForExport(
7235
7017
  );
7236
7018
  }
7237
7019
 
7238
- return finalizeSvgExport(container, theme, effectivePalette, options);
7020
+ return finalizeSvgExport(container, theme, effectivePalette);
7239
7021
  }