@nowline/layout 0.2.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.
Files changed (183) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +103 -0
  3. package/dist/band-scale.d.ts +56 -0
  4. package/dist/band-scale.d.ts.map +1 -0
  5. package/dist/band-scale.js +86 -0
  6. package/dist/band-scale.js.map +1 -0
  7. package/dist/calendar.d.ts +79 -0
  8. package/dist/calendar.d.ts.map +1 -0
  9. package/dist/calendar.js +210 -0
  10. package/dist/calendar.js.map +1 -0
  11. package/dist/capacity.d.ts +72 -0
  12. package/dist/capacity.d.ts.map +1 -0
  13. package/dist/capacity.js +163 -0
  14. package/dist/capacity.js.map +1 -0
  15. package/dist/dsl-utils.d.ts +5 -0
  16. package/dist/dsl-utils.d.ts.map +1 -0
  17. package/dist/dsl-utils.js +28 -0
  18. package/dist/dsl-utils.js.map +1 -0
  19. package/dist/edge-routing.d.ts +89 -0
  20. package/dist/edge-routing.d.ts.map +1 -0
  21. package/dist/edge-routing.js +435 -0
  22. package/dist/edge-routing.js.map +1 -0
  23. package/dist/frame-tab-geometry.d.ts +78 -0
  24. package/dist/frame-tab-geometry.d.ts.map +1 -0
  25. package/dist/frame-tab-geometry.js +115 -0
  26. package/dist/frame-tab-geometry.js.map +1 -0
  27. package/dist/header-card-geometry.d.ts +29 -0
  28. package/dist/header-card-geometry.d.ts.map +1 -0
  29. package/dist/header-card-geometry.js +41 -0
  30. package/dist/header-card-geometry.js.map +1 -0
  31. package/dist/i18n.d.ts +48 -0
  32. package/dist/i18n.d.ts.map +1 -0
  33. package/dist/i18n.js +114 -0
  34. package/dist/i18n.js.map +1 -0
  35. package/dist/include-chrome-geometry.d.ts +86 -0
  36. package/dist/include-chrome-geometry.d.ts.map +1 -0
  37. package/dist/include-chrome-geometry.js +104 -0
  38. package/dist/include-chrome-geometry.js.map +1 -0
  39. package/dist/index.d.ts +11 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +10 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/item-bar-geometry.d.ts +127 -0
  44. package/dist/item-bar-geometry.d.ts.map +1 -0
  45. package/dist/item-bar-geometry.js +173 -0
  46. package/dist/item-bar-geometry.js.map +1 -0
  47. package/dist/lane-utilization.d.ts +90 -0
  48. package/dist/lane-utilization.d.ts.map +1 -0
  49. package/dist/lane-utilization.js +206 -0
  50. package/dist/lane-utilization.js.map +1 -0
  51. package/dist/layout-context.d.ts +143 -0
  52. package/dist/layout-context.d.ts.map +1 -0
  53. package/dist/layout-context.js +8 -0
  54. package/dist/layout-context.js.map +1 -0
  55. package/dist/layout.d.ts +18 -0
  56. package/dist/layout.d.ts.map +1 -0
  57. package/dist/layout.js +1213 -0
  58. package/dist/layout.js.map +1 -0
  59. package/dist/nodes/anchor-node.d.ts +16 -0
  60. package/dist/nodes/anchor-node.d.ts.map +1 -0
  61. package/dist/nodes/anchor-node.js +68 -0
  62. package/dist/nodes/anchor-node.js.map +1 -0
  63. package/dist/nodes/footnote-node.d.ts +10 -0
  64. package/dist/nodes/footnote-node.d.ts.map +1 -0
  65. package/dist/nodes/footnote-node.js +41 -0
  66. package/dist/nodes/footnote-node.js.map +1 -0
  67. package/dist/nodes/group-node.d.ts +29 -0
  68. package/dist/nodes/group-node.d.ts.map +1 -0
  69. package/dist/nodes/group-node.js +187 -0
  70. package/dist/nodes/group-node.js.map +1 -0
  71. package/dist/nodes/include-node.d.ts +16 -0
  72. package/dist/nodes/include-node.d.ts.map +1 -0
  73. package/dist/nodes/include-node.js +117 -0
  74. package/dist/nodes/include-node.js.map +1 -0
  75. package/dist/nodes/item-node.d.ts +51 -0
  76. package/dist/nodes/item-node.d.ts.map +1 -0
  77. package/dist/nodes/item-node.js +108 -0
  78. package/dist/nodes/item-node.js.map +1 -0
  79. package/dist/nodes/marker-geometry.d.ts +22 -0
  80. package/dist/nodes/marker-geometry.d.ts.map +1 -0
  81. package/dist/nodes/marker-geometry.js +38 -0
  82. package/dist/nodes/marker-geometry.js.map +1 -0
  83. package/dist/nodes/milestone-node.d.ts +48 -0
  84. package/dist/nodes/milestone-node.d.ts.map +1 -0
  85. package/dist/nodes/milestone-node.js +210 -0
  86. package/dist/nodes/milestone-node.js.map +1 -0
  87. package/dist/nodes/parallel-node.d.ts +21 -0
  88. package/dist/nodes/parallel-node.d.ts.map +1 -0
  89. package/dist/nodes/parallel-node.js +80 -0
  90. package/dist/nodes/parallel-node.js.map +1 -0
  91. package/dist/nodes/roadmap-node.d.ts +76 -0
  92. package/dist/nodes/roadmap-node.d.ts.map +1 -0
  93. package/dist/nodes/roadmap-node.js +788 -0
  94. package/dist/nodes/roadmap-node.js.map +1 -0
  95. package/dist/nodes/swimlane-node.d.ts +38 -0
  96. package/dist/nodes/swimlane-node.d.ts.map +1 -0
  97. package/dist/nodes/swimlane-node.js +308 -0
  98. package/dist/nodes/swimlane-node.js.map +1 -0
  99. package/dist/renderable.d.ts +44 -0
  100. package/dist/renderable.d.ts.map +1 -0
  101. package/dist/renderable.js +21 -0
  102. package/dist/renderable.js.map +1 -0
  103. package/dist/row-packer.d.ts +125 -0
  104. package/dist/row-packer.d.ts.map +1 -0
  105. package/dist/row-packer.js +169 -0
  106. package/dist/row-packer.js.map +1 -0
  107. package/dist/style-resolution.d.ts +14 -0
  108. package/dist/style-resolution.d.ts.map +1 -0
  109. package/dist/style-resolution.js +191 -0
  110. package/dist/style-resolution.js.map +1 -0
  111. package/dist/themes/dark.d.ts +4 -0
  112. package/dist/themes/dark.d.ts.map +1 -0
  113. package/dist/themes/dark.js +241 -0
  114. package/dist/themes/dark.js.map +1 -0
  115. package/dist/themes/index.d.ts +15 -0
  116. package/dist/themes/index.d.ts.map +1 -0
  117. package/dist/themes/index.js +30 -0
  118. package/dist/themes/index.js.map +1 -0
  119. package/dist/themes/light.d.ts +4 -0
  120. package/dist/themes/light.d.ts.map +1 -0
  121. package/dist/themes/light.js +248 -0
  122. package/dist/themes/light.js.map +1 -0
  123. package/dist/themes/shape.d.ts +194 -0
  124. package/dist/themes/shape.d.ts.map +1 -0
  125. package/dist/themes/shape.js +6 -0
  126. package/dist/themes/shape.js.map +1 -0
  127. package/dist/themes/shared.d.ts +145 -0
  128. package/dist/themes/shared.d.ts.map +1 -0
  129. package/dist/themes/shared.js +310 -0
  130. package/dist/themes/shared.js.map +1 -0
  131. package/dist/time-scale.d.ts +39 -0
  132. package/dist/time-scale.d.ts.map +1 -0
  133. package/dist/time-scale.js +62 -0
  134. package/dist/time-scale.js.map +1 -0
  135. package/dist/types.d.ts +483 -0
  136. package/dist/types.d.ts.map +1 -0
  137. package/dist/types.js +6 -0
  138. package/dist/types.js.map +1 -0
  139. package/dist/view-preset.d.ts +23 -0
  140. package/dist/view-preset.d.ts.map +1 -0
  141. package/dist/view-preset.js +146 -0
  142. package/dist/view-preset.js.map +1 -0
  143. package/dist/working-calendar.d.ts +14 -0
  144. package/dist/working-calendar.d.ts.map +1 -0
  145. package/dist/working-calendar.js +74 -0
  146. package/dist/working-calendar.js.map +1 -0
  147. package/package.json +37 -0
  148. package/src/band-scale.ts +115 -0
  149. package/src/calendar.ts +244 -0
  150. package/src/capacity.ts +191 -0
  151. package/src/dsl-utils.ts +30 -0
  152. package/src/edge-routing.ts +550 -0
  153. package/src/frame-tab-geometry.ts +165 -0
  154. package/src/header-card-geometry.ts +48 -0
  155. package/src/i18n.ts +124 -0
  156. package/src/include-chrome-geometry.ts +156 -0
  157. package/src/index.ts +116 -0
  158. package/src/item-bar-geometry.ts +222 -0
  159. package/src/lane-utilization.ts +259 -0
  160. package/src/layout-context.ts +182 -0
  161. package/src/layout.ts +1446 -0
  162. package/src/nodes/anchor-node.ts +77 -0
  163. package/src/nodes/footnote-node.ts +60 -0
  164. package/src/nodes/group-node.ts +252 -0
  165. package/src/nodes/include-node.ts +168 -0
  166. package/src/nodes/item-node.ts +171 -0
  167. package/src/nodes/marker-geometry.ts +43 -0
  168. package/src/nodes/milestone-node.ts +263 -0
  169. package/src/nodes/parallel-node.ts +101 -0
  170. package/src/nodes/roadmap-node.ts +957 -0
  171. package/src/nodes/swimlane-node.ts +423 -0
  172. package/src/renderable.ts +68 -0
  173. package/src/row-packer.ts +271 -0
  174. package/src/style-resolution.ts +243 -0
  175. package/src/themes/dark.ts +244 -0
  176. package/src/themes/index.ts +36 -0
  177. package/src/themes/light.ts +251 -0
  178. package/src/themes/shape.ts +230 -0
  179. package/src/themes/shared.ts +369 -0
  180. package/src/time-scale.ts +78 -0
  181. package/src/types.ts +607 -0
  182. package/src/view-preset.ts +180 -0
  183. package/src/working-calendar.ts +91 -0
@@ -0,0 +1,210 @@
1
+ // Calendar and duration resolution. An item's `duration:` is a raw literal
2
+ // (`1d`, `2w`, `3m`, `1q`, `1y`); `size:NAME` references a `size NAME
3
+ // effort:<literal>` declaration whose effort literal is calendar-resolved
4
+ // once into a `ResolvedSize`. The calendar block controls how literal units
5
+ // translate into absolute days.
6
+ import { parseCapacityValue } from './capacity.js';
7
+ import { propValue } from './dsl-utils.js';
8
+ const BUSINESS = {
9
+ mode: 'business',
10
+ daysPerWeek: 5,
11
+ daysPerMonth: 22,
12
+ daysPerQuarter: 65,
13
+ daysPerYear: 260,
14
+ };
15
+ const FULL = {
16
+ mode: 'full',
17
+ daysPerWeek: 7,
18
+ daysPerMonth: 30,
19
+ daysPerQuarter: 91,
20
+ daysPerYear: 365,
21
+ };
22
+ export function resolveCalendar(file, customBlock) {
23
+ const calProp = file.roadmapDecl?.properties.find((p) => stripColon(p.key) === 'calendar');
24
+ const mode = calProp?.value ?? 'business';
25
+ if (mode === 'business')
26
+ return { ...BUSINESS };
27
+ if (mode === 'full')
28
+ return { ...FULL };
29
+ if (customBlock) {
30
+ const get = (key, fallback) => {
31
+ const p = customBlock.properties.find((x) => stripColon(x.key) === key);
32
+ const n = p ? parseInt(p.value, 10) : NaN;
33
+ return Number.isFinite(n) && n > 0 ? n : fallback;
34
+ };
35
+ return {
36
+ mode: 'custom',
37
+ daysPerWeek: get('days-per-week', BUSINESS.daysPerWeek),
38
+ daysPerMonth: get('days-per-month', BUSINESS.daysPerMonth),
39
+ daysPerQuarter: get('days-per-quarter', BUSINESS.daysPerQuarter),
40
+ daysPerYear: get('days-per-year', BUSINESS.daysPerYear),
41
+ };
42
+ }
43
+ return { ...BUSINESS };
44
+ }
45
+ // Decimal-aware so `size xs effort:0.5d` and `duration:1.5w` round-trip cleanly.
46
+ const DURATION_RE = /^(\d+(?:\.\d+)?)([dwmqy])$/;
47
+ export function literalToDays(literal, cal) {
48
+ const m = DURATION_RE.exec(literal);
49
+ if (!m)
50
+ return 0;
51
+ const n = parseFloat(m[1]);
52
+ switch (m[2]) {
53
+ case 'd':
54
+ return n;
55
+ case 'w':
56
+ return n * cal.daysPerWeek;
57
+ case 'm':
58
+ return n * cal.daysPerMonth;
59
+ case 'q':
60
+ return n * cal.daysPerQuarter;
61
+ case 'y':
62
+ return n * cal.daysPerYear;
63
+ default:
64
+ return 0;
65
+ }
66
+ }
67
+ /**
68
+ * Convert a `duration:` literal or a `size:NAME` reference into a calendar-
69
+ * resolved day count. Capacity-agnostic — used both for the duration
70
+ * literal lookup in `deriveItemDurationDays` and for the
71
+ * `remaining:` literal normalization in `sequenceItem`. Returns 0 for
72
+ * missing or unresolvable values; callers substitute their own minimum
73
+ * width when the result is zero.
74
+ */
75
+ export function resolveDuration(value, sizes, cal) {
76
+ if (!value)
77
+ return 0;
78
+ if (DURATION_RE.test(value))
79
+ return literalToDays(value, cal);
80
+ return sizes.get(value)?.effortDays ?? 0;
81
+ }
82
+ /**
83
+ * Derive an item's calendar duration in days from its properties.
84
+ * Precedence (matches specs/dsl.md § "Sizing precedence"):
85
+ *
86
+ * 1. Explicit `duration:LITERAL` wins. The literal IS the calendar
87
+ * duration the bar paints; `size:NAME` (if also present) collapses
88
+ * to a pure annotation rendered as the size chip.
89
+ * 2. Otherwise, `size:NAME` resolves to its size declaration's
90
+ * `effort:` (single-engineer days) and we divide by the item's
91
+ * capacity (default 1) to get the team's calendar duration.
92
+ * 3. With neither, returns 0 — the validator already errors on items
93
+ * missing both `size:` and `duration:`, so this only happens in
94
+ * transient malformed inputs.
95
+ */
96
+ export function deriveItemDurationDays(props, sizes, cal) {
97
+ const durationRaw = propValue(props, 'duration');
98
+ if (durationRaw && DURATION_RE.test(durationRaw)) {
99
+ return literalToDays(durationRaw, cal);
100
+ }
101
+ const sizeRef = propValue(props, 'size');
102
+ const size = sizeRef ? sizes.get(sizeRef) : undefined;
103
+ if (!size)
104
+ return 0;
105
+ const capacity = parseCapacityValue(propValue(props, 'capacity')) ?? 1;
106
+ return size.effortDays / capacity;
107
+ }
108
+ /**
109
+ * Total work for the item in single-engineer days. Used to normalize a
110
+ * literal `remaining:` value (also single-engineer days per spec) into a
111
+ * 0..1 progress fraction.
112
+ *
113
+ * - sized: `size.effortDays` directly (already per-engineer).
114
+ * - duration-literal'd: `duration_days × capacity`. The literal sets
115
+ * calendar duration; multiplying by the engineer count recovers the
116
+ * equivalent single-engineer effort the lane is consuming.
117
+ *
118
+ * Returns 0 when neither `size:` nor `duration:` is set so callers can
119
+ * skip normalization safely.
120
+ */
121
+ export function deriveTotalEffortDays(props, sizes, cal) {
122
+ const sizeRef = propValue(props, 'size');
123
+ const size = sizeRef ? sizes.get(sizeRef) : undefined;
124
+ if (size)
125
+ return size.effortDays;
126
+ const durationRaw = propValue(props, 'duration');
127
+ if (durationRaw && DURATION_RE.test(durationRaw)) {
128
+ const capacity = parseCapacityValue(propValue(props, 'capacity')) ?? 1;
129
+ return literalToDays(durationRaw, cal) * capacity;
130
+ }
131
+ return 0;
132
+ }
133
+ /**
134
+ * Format a calendar day count back into the largest unit literal that
135
+ * divides cleanly under the active calendar. Used by the meta line for
136
+ * sized items so the displayed duration reflects the *derived* calendar
137
+ * duration rather than the raw effort literal — `size:m capacity:5`
138
+ * with `effort:1w` paints a 1d bar and the meta should read `M 1d`,
139
+ * not `M 1w`.
140
+ *
141
+ * - Whole-unit folds happen in descending order (`y` → `q` → `m` →
142
+ * `w`); the first one that yields an integer wins so `5d` →
143
+ * `"1w"` and `22d` (business calendar) → `"1m"`.
144
+ * - Anything left over collapses to a `Nd` literal with up to two
145
+ * decimal places, trailing zeros trimmed (`"3d"`, `"1.5d"`,
146
+ * `"1.67d"`). Matches the `DURATION_LITERAL` shape the parser
147
+ * accepts so round-tripping through the renderer reads naturally.
148
+ * - `0` (or anything sub-1d) returns `"0d"` rather than an empty
149
+ * string so callers always get a renderable token.
150
+ */
151
+ export function formatDurationDays(days, cal) {
152
+ const EPSILON = 1e-6;
153
+ const isWhole = (n) => n >= 1 && Math.abs(n - Math.round(n)) < EPSILON;
154
+ const folds = [
155
+ [cal.daysPerYear, 'y'],
156
+ [cal.daysPerQuarter, 'q'],
157
+ [cal.daysPerMonth, 'm'],
158
+ [cal.daysPerWeek, 'w'],
159
+ ];
160
+ for (const [unitDays, suffix] of folds) {
161
+ const n = days / unitDays;
162
+ if (isWhole(n))
163
+ return `${Math.round(n)}${suffix}`;
164
+ }
165
+ if (Math.abs(days - Math.round(days)) < EPSILON) {
166
+ return `${Math.round(days)}d`;
167
+ }
168
+ // parseFloat strips trailing zeros: `1.50` → `1.5`, `1.67` → `1.67`.
169
+ return `${parseFloat(days.toFixed(2))}d`;
170
+ }
171
+ /**
172
+ * Build the layout's `Map<string, ResolvedSize>` once the calendar is
173
+ * known. Skips sizes whose `effort:` literal is missing or unparseable —
174
+ * the validator already errors on those, so layout silently drops them
175
+ * rather than emitting NaN-laden positions.
176
+ */
177
+ export function resolveSizes(decls, cal) {
178
+ const out = new Map();
179
+ for (const [name, decl] of decls) {
180
+ const effortProp = decl.properties.find((p) => stripColon(p.key) === 'effort');
181
+ const effortLiteral = effortProp?.value;
182
+ if (!effortLiteral)
183
+ continue;
184
+ const effortDays = literalToDays(effortLiteral, cal);
185
+ if (effortDays <= 0)
186
+ continue;
187
+ out.set(name, {
188
+ name,
189
+ title: decl.title || undefined,
190
+ effortDays,
191
+ effortLiteral,
192
+ });
193
+ }
194
+ return out;
195
+ }
196
+ function stripColon(key) {
197
+ return key.endsWith(':') ? key.slice(0, -1) : key;
198
+ }
199
+ // Date arithmetic in day units. All coordinates are relative to the roadmap
200
+ // start date; today is computed in days since start.
201
+ export function daysBetween(from, to) {
202
+ const ONE_DAY = 86400 * 1000;
203
+ return Math.round((to.getTime() - from.getTime()) / ONE_DAY);
204
+ }
205
+ export function addDays(base, days) {
206
+ const out = new Date(base.getTime());
207
+ out.setUTCDate(out.getUTCDate() + days);
208
+ return out;
209
+ }
210
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../src/calendar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { SymbolDeclaration } from '@nowline/core';
2
+ import type { ResolvedCapacityIconRef } from './types.js';
3
+ /**
4
+ * Parse a `capacity:` value. Accepts the three forms the validator allows on
5
+ * items (positive int, positive decimal, positive percent) — swimlanes only
6
+ * allow int/decimal but the validator already rejects percent on lanes, so a
7
+ * single parser is fine here.
8
+ *
9
+ * Returns `null` when the value is missing, malformed, or non-positive. The
10
+ * renderer must not draw a capacity suffix / badge in that case (per spec
11
+ * "the suffix appears only when the resolved capacity is `> 0`").
12
+ */
13
+ export declare function parseCapacityValue(raw: string | undefined): number | null;
14
+ /**
15
+ * Format a parsed capacity number for display per specs/rendering.md §
16
+ * "Number formatting": integers render as integers (`5`, not `5.0`); decimals
17
+ * render with trailing zeros trimmed (`0.5`, `1.25`).
18
+ *
19
+ * The `toFixed(6)` cap guards against `0.1 + 0.2`-style float noise creeping
20
+ * into the rendered string. Six is more than the DSL grammar admits anyway —
21
+ * `1.234567%` lexes but is far below the granularity any roadmap author cares
22
+ * about, so trimming there is safe.
23
+ */
24
+ export declare function formatCapacityNumber(value: number): string;
25
+ /**
26
+ * Resolved capacity-icon ready for the renderer. Re-exported from
27
+ * `./types.js` so callers can `import { ResolvedCapacityIcon } from
28
+ * './capacity.js'` without reaching into the positioned-model module.
29
+ *
30
+ * Two flavors:
31
+ *
32
+ * - `kind: 'builtin'` — the renderer looks up its SVG (person/people/
33
+ * points/time) or text representation (multiplier) via its icon library.
34
+ * `'none'` is collapsed to `null` upstream (no glyph rendered).
35
+ * - `kind: 'literal'` — the renderer paints `text` as a `<text>` node.
36
+ * Covers inline Unicode literals (`capacity-icon:"💰"`) and dereferenced
37
+ * custom `symbol` declarations.
38
+ */
39
+ export type ResolvedCapacityIcon = ResolvedCapacityIconRef;
40
+ /**
41
+ * Resolve a `capacity-icon:` style value into a `ResolvedCapacityIcon` (or
42
+ * `null` for `'none'`).
43
+ *
44
+ * Resolution order matches specs/dsl.md § Style Properties for `icon:` and
45
+ * `capacity-icon:`:
46
+ *
47
+ * 1. `'none'` → `null` (renderer emits no glyph).
48
+ * 2. Built-in name → `{ kind: 'builtin', name }`.
49
+ * 3. Custom symbol id present in `symbols` → `{ kind: 'literal', text:
50
+ * <unicode:> }`. The author wrote an identifier; we hand the
51
+ * renderer the underlying Unicode payload.
52
+ * 4. Anything else → `{ kind: 'literal', text: icon }`. This is the inline
53
+ * Unicode literal form (`capacity-icon:"💰"`) — Langium's
54
+ * ValueConverter has already stripped the surrounding quotes, so the
55
+ * raw payload arrives here.
56
+ *
57
+ * Validator rule 17 already rejects malformed combinations (unknown built-in
58
+ * with no matching symbol, symbol id collision with built-ins, etc.), so this
59
+ * function trusts its input shape.
60
+ */
61
+ export declare function resolveCapacityIcon(icon: string, symbols: Map<string, SymbolDeclaration>): ResolvedCapacityIcon | null;
62
+ /**
63
+ * Estimate the on-screen width (px) the capacity suffix will occupy at the
64
+ * given font size, including the leading separator gap before the glyph.
65
+ *
66
+ * The renderer paints the suffix as `<num>{gap}<glyph>` (no leading space
67
+ * before the number — callers handle that as an outer separator). Width
68
+ * estimates are intentionally pessimistic so borderline-fitting captions
69
+ * trigger spill rather than clip.
70
+ */
71
+ export declare function estimateCapacitySuffixWidth(text: string, icon: ResolvedCapacityIcon | null, fontSizePx: number): number;
72
+ //# sourceMappingURL=capacity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capacity.d.ts","sourceRoot":"","sources":["../src/capacity.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAuB1D;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAezE;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAmB3D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,GACxC,oBAAoB,GAAG,IAAI,CAc7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACvC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,oBAAoB,GAAG,IAAI,EACjC,UAAU,EAAE,MAAM,GACnB,MAAM,CAcR"}
@@ -0,0 +1,163 @@
1
+ // Capacity parsing, number formatting, and capacity-icon resolution helpers.
2
+ //
3
+ // Layout owns the *contract* the renderer reads:
4
+ //
5
+ // - `parseCapacityValue` turns the DSL's three numeric forms (`5`, `0.5`,
6
+ // `50%`) into a single positive number — percent literals are syntactic
7
+ // sugar for decimals (`50%` → `0.5`) per specs/dsl.md § Capacity.
8
+ // - `formatCapacityNumber` produces the spec's display string: integers
9
+ // stay integers (`5`, not `5.0`); decimals trim trailing zeros (`0.5`,
10
+ // `1.25`).
11
+ // - `resolveCapacityIcon` collapses the three syntactic forms of the
12
+ // `capacity-icon:` style property (built-in name, custom `symbol` id,
13
+ // inline Unicode literal) into either a built-in name the renderer
14
+ // recognizes OR a literal string the renderer paints as text. Custom
15
+ // symbol ids are dereferenced via `ResolvedConfig.symbols` here so the
16
+ // renderer never has to walk the config map.
17
+ //
18
+ // All three helpers are pure — no AST, no theme, no side effects — so they
19
+ // can be tested in isolation and reused by future capacity consumers (e.g.
20
+ // the lane badge in m7 and the overload sweep in m8).
21
+ /**
22
+ * Built-in `capacity-icon:` names the renderer understands directly. Stays in
23
+ * sync with `BUILTIN_CAPACITY_ICONS` in `packages/core/.../nowline-validator.ts`
24
+ * — the validator uses this set to decide whether a value is a known built-in;
25
+ * the layout uses it to decide whether to forward the value as-is or
26
+ * dereference it through the glyph map. Layout-side and validator-side can
27
+ * diverge briefly during refactors but should converge before each release.
28
+ */
29
+ const BUILTIN_CAPACITY_ICONS = new Set([
30
+ 'none',
31
+ 'multiplier',
32
+ 'person',
33
+ 'people',
34
+ 'points',
35
+ 'time',
36
+ ]);
37
+ const POSITIVE_INT_RE = /^\d+$/;
38
+ const POSITIVE_DECIMAL_RE = /^\d+\.\d+$/;
39
+ const POSITIVE_PERCENT_RE = /^\d+(?:\.\d+)?%$/;
40
+ /**
41
+ * Parse a `capacity:` value. Accepts the three forms the validator allows on
42
+ * items (positive int, positive decimal, positive percent) — swimlanes only
43
+ * allow int/decimal but the validator already rejects percent on lanes, so a
44
+ * single parser is fine here.
45
+ *
46
+ * Returns `null` when the value is missing, malformed, or non-positive. The
47
+ * renderer must not draw a capacity suffix / badge in that case (per spec
48
+ * "the suffix appears only when the resolved capacity is `> 0`").
49
+ */
50
+ export function parseCapacityValue(raw) {
51
+ if (!raw)
52
+ return null;
53
+ if (POSITIVE_INT_RE.test(raw)) {
54
+ const n = parseInt(raw, 10);
55
+ return n > 0 ? n : null;
56
+ }
57
+ if (POSITIVE_DECIMAL_RE.test(raw)) {
58
+ const n = parseFloat(raw);
59
+ return Number.isFinite(n) && n > 0 ? n : null;
60
+ }
61
+ if (POSITIVE_PERCENT_RE.test(raw)) {
62
+ const n = parseFloat(raw.slice(0, -1)) / 100;
63
+ return Number.isFinite(n) && n > 0 ? n : null;
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * Format a parsed capacity number for display per specs/rendering.md §
69
+ * "Number formatting": integers render as integers (`5`, not `5.0`); decimals
70
+ * render with trailing zeros trimmed (`0.5`, `1.25`).
71
+ *
72
+ * The `toFixed(6)` cap guards against `0.1 + 0.2`-style float noise creeping
73
+ * into the rendered string. Six is more than the DSL grammar admits anyway —
74
+ * `1.234567%` lexes but is far below the granularity any roadmap author cares
75
+ * about, so trimming there is safe.
76
+ */
77
+ export function formatCapacityNumber(value) {
78
+ if (Number.isInteger(value))
79
+ return String(value);
80
+ let s = value.toFixed(6);
81
+ s = s.replace(/0+$/, '');
82
+ s = s.replace(/\.$/, '');
83
+ return s;
84
+ }
85
+ /**
86
+ * Read a property value off a Langium-shaped EntityProperty, normalizing the
87
+ * trailing-colon form. Validator-side uses the same trick — the grammar
88
+ * stores `key` as the *raw* token, including the colon for `unicode:`-style
89
+ * property keys.
90
+ */
91
+ function propKey(prop) {
92
+ return prop.key.endsWith(':') ? prop.key.slice(0, -1) : prop.key;
93
+ }
94
+ function symbolUnicode(decl) {
95
+ for (const p of decl.properties) {
96
+ if (propKey(p) === 'unicode' && p.value)
97
+ return p.value;
98
+ }
99
+ return undefined;
100
+ }
101
+ /**
102
+ * Resolve a `capacity-icon:` style value into a `ResolvedCapacityIcon` (or
103
+ * `null` for `'none'`).
104
+ *
105
+ * Resolution order matches specs/dsl.md § Style Properties for `icon:` and
106
+ * `capacity-icon:`:
107
+ *
108
+ * 1. `'none'` → `null` (renderer emits no glyph).
109
+ * 2. Built-in name → `{ kind: 'builtin', name }`.
110
+ * 3. Custom symbol id present in `symbols` → `{ kind: 'literal', text:
111
+ * <unicode:> }`. The author wrote an identifier; we hand the
112
+ * renderer the underlying Unicode payload.
113
+ * 4. Anything else → `{ kind: 'literal', text: icon }`. This is the inline
114
+ * Unicode literal form (`capacity-icon:"💰"`) — Langium's
115
+ * ValueConverter has already stripped the surrounding quotes, so the
116
+ * raw payload arrives here.
117
+ *
118
+ * Validator rule 17 already rejects malformed combinations (unknown built-in
119
+ * with no matching symbol, symbol id collision with built-ins, etc.), so this
120
+ * function trusts its input shape.
121
+ */
122
+ export function resolveCapacityIcon(icon, symbols) {
123
+ if (icon === 'none')
124
+ return null;
125
+ if (BUILTIN_CAPACITY_ICONS.has(icon)) {
126
+ return {
127
+ kind: 'builtin',
128
+ name: icon,
129
+ };
130
+ }
131
+ const custom = symbols.get(icon);
132
+ if (custom) {
133
+ const unicode = symbolUnicode(custom);
134
+ return { kind: 'literal', text: unicode ?? icon };
135
+ }
136
+ return { kind: 'literal', text: icon };
137
+ }
138
+ /**
139
+ * Estimate the on-screen width (px) the capacity suffix will occupy at the
140
+ * given font size, including the leading separator gap before the glyph.
141
+ *
142
+ * The renderer paints the suffix as `<num>{gap}<glyph>` (no leading space
143
+ * before the number — callers handle that as an outer separator). Width
144
+ * estimates are intentionally pessimistic so borderline-fitting captions
145
+ * trigger spill rather than clip.
146
+ */
147
+ export function estimateCapacitySuffixWidth(text, icon, fontSizePx) {
148
+ if (!icon)
149
+ return text.length * fontSizePx * 0.58;
150
+ if (icon.kind === 'builtin' && icon.name === 'multiplier') {
151
+ // Multiplier is a typographic operator with built-in side bearing —
152
+ // no separator gap, glyph width approx. one character.
153
+ return (text.length + 1) * fontSizePx * 0.58;
154
+ }
155
+ // 0.1em separator + 1em glyph. Estimating the glyph as 1em (rather than
156
+ // 0.58em, the per-character width) is intentional: SVG icons render at
157
+ // their full font-size square, and most Unicode literals authors use for
158
+ // capacity (★, 💰, ⚙) similarly read at near-em widths.
159
+ const glyphWidthEm = 1.0;
160
+ const gapEm = 0.1;
161
+ return text.length * fontSizePx * 0.58 + (gapEm + glyphWidthEm) * fontSizePx;
162
+ }
163
+ //# sourceMappingURL=capacity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capacity.js","sourceRoot":"","sources":["../src/capacity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { EntityProperty } from '@nowline/core';
2
+ export declare function propValue(props: EntityProperty[], key: string): string | undefined;
3
+ export declare function propValues(props: EntityProperty[], key: string): string[];
4
+ export declare function parseDate(raw: string | undefined): Date | null;
5
+ //# sourceMappingURL=dsl-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dsl-utils.d.ts","sourceRoot":"","sources":["../src/dsl-utils.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAMpD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAElF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAIzE;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,CAM9D"}
@@ -0,0 +1,28 @@
1
+ // Tiny DSL utilities shared between `layout.ts` and the per-entity nodes
2
+ // under `nodes/`. Kept intentionally trivial and pure: a `:`-trim,
3
+ // property lookup helpers against the AST's `EntityProperty[]`, and an
4
+ // ISO-date parser. Anything that needs configuration or non-trivial
5
+ // resolution (durations, calendars, styles) stays in the modules that
6
+ // own those concerns.
7
+ function stripColon(key) {
8
+ return key.endsWith(':') ? key.slice(0, -1) : key;
9
+ }
10
+ export function propValue(props, key) {
11
+ return props.find((p) => stripColon(p.key) === key)?.value;
12
+ }
13
+ export function propValues(props, key) {
14
+ const p = props.find((x) => stripColon(x.key) === key);
15
+ if (!p)
16
+ return [];
17
+ return p.value !== undefined ? [p.value] : [...p.values];
18
+ }
19
+ export function parseDate(raw) {
20
+ if (!raw)
21
+ return null;
22
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
23
+ if (!m)
24
+ return null;
25
+ const d = new Date(Date.UTC(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10)));
26
+ return Number.isNaN(d.getTime()) ? null : d;
27
+ }
28
+ //# sourceMappingURL=dsl-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dsl-utils.js","sourceRoot":"","sources":["../src/dsl-utils.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,mEAAmE;AACnE,uEAAuE;AACvE,oEAAoE;AACpE,sEAAsE;AACtE,sBAAsB;AAItB,SAAS,UAAU,CAAC,GAAW;IAC3B,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAuB,EAAE,GAAW;IAC1D,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAuB,EAAE,GAAW;IAC3D,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC;IACvD,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAClB,OAAO,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAuB;IAC7C,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,CAAC,GAAG,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChD,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7F,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC"}
@@ -0,0 +1,89 @@
1
+ import type { BoundingBox, Point, PositionedIncludeRegion, PositionedSwimlane } from './types.js';
2
+ /** A visible parallel or group bracket stroke. Used by the
3
+ * bracket-clearance nudge: arrows whose chosen elbow X falls within
4
+ * `BRACKET_NUDGE_PX` of one of these lines are shifted away. Brackets
5
+ * are NOT obstacles — they're aesthetic preferences, not collisions. */
6
+ export interface BracketLine {
7
+ x: number;
8
+ yTop: number;
9
+ yBottom: number;
10
+ }
11
+ /** Per-edge routing request. `from` and `to` are the (visualEdge, midY)
12
+ * attach points the rest of the layout already computed; the router
13
+ * produces the orthogonal polyline that connects them. */
14
+ export interface EdgeRouteRequest {
15
+ fromId: string;
16
+ toId: string;
17
+ from: Point;
18
+ to: Point;
19
+ /** Marker → item edges (anchor / milestone source) skip the channel
20
+ * router entirely — the cut line is the visible stem, the path is
21
+ * always a short horizontal stub. Set true to bypass routing. */
22
+ isMarkerSource: boolean;
23
+ }
24
+ export interface EdgeRouteResult {
25
+ fromId: string;
26
+ toId: string;
27
+ waypoints: Point[];
28
+ /** True when the chosen channel intersected an item bar. The
29
+ * renderer paints these edges BEFORE bar fills with a thinner
30
+ * stroke so the bar still reads as the foreground. */
31
+ underBar: boolean;
32
+ }
33
+ /** Distance (px) the elbow X must keep from any visible bracket. Picked
34
+ * to leave a small visible gap between the arrow's vertical leg and
35
+ * the bracket stroke without forcing the arrow far from its natural
36
+ * mid-gutter line. */
37
+ export declare const BRACKET_NUDGE_PX = 4;
38
+ /** Minimum horizontal source stub (px) — distance from the source's
39
+ * exit point to the vertical-leg elbow. Below this, the source-side
40
+ * arrow body collapses into the bar's right edge with no visible
41
+ * horizontal segment. Treated as a hard constraint when picking
42
+ * the channel X for left-to-right edges. */
43
+ export declare const MIN_SOURCE_STUB_PX = 6;
44
+ /** Minimum horizontal target stub (px) — distance from the vertical-leg
45
+ * elbow to the target's left edge. Below this, the arrowhead has no
46
+ * horizontal lead-in: the leg appears to plunge directly into the bar
47
+ * without a visible target-side stub. Treated as a hard constraint
48
+ * when picking the channel X for left-to-right edges; conflicts with
49
+ * obstacle / bracket clearance trigger the under-bar fallback. */
50
+ export declare const MIN_TARGET_STUB_PX = 6;
51
+ export interface ChannelGridInput {
52
+ /** Every painted item box (visual edges, including chip-spill
53
+ * growth). Treated as hard obstacles: a vertical leg crossing one
54
+ * triggers under-bar fallback. */
55
+ itemBars: BoundingBox[];
56
+ /** Every visible bracket stroke (parallel `bracket:solid|dashed`,
57
+ * filled-style group chiclets are NOT brackets). Used by the
58
+ * clearance nudge only — not obstacles. */
59
+ brackets: BracketLine[];
60
+ }
61
+ export declare class ChannelGrid {
62
+ private readonly items;
63
+ private readonly brackets;
64
+ constructor(input: ChannelGridInput);
65
+ /** True when a vertical line at `x` intersects any item bar within
66
+ * the Y span `[yMin, yMax]`. */
67
+ hasObstacle(x: number, yMin: number, yMax: number): boolean;
68
+ /** Visible brackets within `radius` px of `x` whose Y span overlaps
69
+ * `[yMin, yMax]`. The clearance nudge uses these to shift the
70
+ * elbow X away from the bracket. */
71
+ bracketsNear(x: number, yMin: number, yMax: number, radius: number): BracketLine[];
72
+ }
73
+ /**
74
+ * Walk a positioned swimlane tree and collect every painted item bar
75
+ * (visual edges) AND every visible bracket stroke. Output feeds
76
+ * `ChannelGrid` so the router knows what to avoid.
77
+ *
78
+ * Swimlanes contained in include regions count too — items inside an
79
+ * isolated region share the parent timeline so cross-region arrows
80
+ * still need the obstacle/bracket data to route cleanly.
81
+ */
82
+ export declare function collectRoutingObstacles(swimlanes: PositionedSwimlane[], includes: PositionedIncludeRegion[]): ChannelGridInput;
83
+ /**
84
+ * Route every dependency edge in one batch so slot assignment can
85
+ * coordinate across edges that share a channel. Marker → item edges
86
+ * route as direct stubs and bypass the channel router.
87
+ */
88
+ export declare function routeChannelEdges(requests: EdgeRouteRequest[], grid: ChannelGrid): EdgeRouteResult[];
89
+ //# sourceMappingURL=edge-routing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-routing.d.ts","sourceRoot":"","sources":["../src/edge-routing.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACR,WAAW,EACX,KAAK,EACL,uBAAuB,EACvB,kBAAkB,EAErB,MAAM,YAAY,CAAC;AAEpB;;;yEAGyE;AACzE,MAAM,WAAW,WAAW;IACxB,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;2DAE2D;AAC3D,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,CAAC;IACZ,EAAE,EAAE,KAAK,CAAC;IACV;;sEAEkE;IAClE,cAAc,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,EAAE,CAAC;IACnB;;2DAEuD;IACvD,QAAQ,EAAE,OAAO,CAAC;CACrB;AAED;;;uBAGuB;AACvB,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC;;;;6CAI6C;AAC7C,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;;;mEAKmE;AACnE,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAmBpC,MAAM,WAAW,gBAAgB;IAC7B;;uCAEmC;IACnC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB;;gDAE4C;IAC5C,QAAQ,EAAE,WAAW,EAAE,CAAC;CAC3B;AAED,qBAAa,WAAW;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;gBAE7B,KAAK,EAAE,gBAAgB;IAKnC;qCACiC;IACjC,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAW3D;;yCAEqC;IACrC,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE;CAWrF;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACnC,SAAS,EAAE,kBAAkB,EAAE,EAC/B,QAAQ,EAAE,uBAAuB,EAAE,GACpC,gBAAgB,CAqElB;AAyPD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,IAAI,EAAE,WAAW,GAClB,eAAe,EAAE,CAkDnB"}