@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,191 @@
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
+ import type { SymbolDeclaration } from '@nowline/core';
23
+ import type { ResolvedCapacityIconRef } from './types.js';
24
+
25
+ /**
26
+ * Built-in `capacity-icon:` names the renderer understands directly. Stays in
27
+ * sync with `BUILTIN_CAPACITY_ICONS` in `packages/core/.../nowline-validator.ts`
28
+ * โ€” the validator uses this set to decide whether a value is a known built-in;
29
+ * the layout uses it to decide whether to forward the value as-is or
30
+ * dereference it through the glyph map. Layout-side and validator-side can
31
+ * diverge briefly during refactors but should converge before each release.
32
+ */
33
+ const BUILTIN_CAPACITY_ICONS = new Set<string>([
34
+ 'none',
35
+ 'multiplier',
36
+ 'person',
37
+ 'people',
38
+ 'points',
39
+ 'time',
40
+ ]);
41
+
42
+ const POSITIVE_INT_RE = /^\d+$/;
43
+ const POSITIVE_DECIMAL_RE = /^\d+\.\d+$/;
44
+ const POSITIVE_PERCENT_RE = /^\d+(?:\.\d+)?%$/;
45
+
46
+ /**
47
+ * Parse a `capacity:` value. Accepts the three forms the validator allows on
48
+ * items (positive int, positive decimal, positive percent) โ€” swimlanes only
49
+ * allow int/decimal but the validator already rejects percent on lanes, so a
50
+ * single parser is fine here.
51
+ *
52
+ * Returns `null` when the value is missing, malformed, or non-positive. The
53
+ * renderer must not draw a capacity suffix / badge in that case (per spec
54
+ * "the suffix appears only when the resolved capacity is `> 0`").
55
+ */
56
+ export function parseCapacityValue(raw: string | undefined): number | null {
57
+ if (!raw) return null;
58
+ if (POSITIVE_INT_RE.test(raw)) {
59
+ const n = parseInt(raw, 10);
60
+ return n > 0 ? n : null;
61
+ }
62
+ if (POSITIVE_DECIMAL_RE.test(raw)) {
63
+ const n = parseFloat(raw);
64
+ return Number.isFinite(n) && n > 0 ? n : null;
65
+ }
66
+ if (POSITIVE_PERCENT_RE.test(raw)) {
67
+ const n = parseFloat(raw.slice(0, -1)) / 100;
68
+ return Number.isFinite(n) && n > 0 ? n : null;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Format a parsed capacity number for display per specs/rendering.md ยง
75
+ * "Number formatting": integers render as integers (`5`, not `5.0`); decimals
76
+ * render with trailing zeros trimmed (`0.5`, `1.25`).
77
+ *
78
+ * The `toFixed(6)` cap guards against `0.1 + 0.2`-style float noise creeping
79
+ * into the rendered string. Six is more than the DSL grammar admits anyway โ€”
80
+ * `1.234567%` lexes but is far below the granularity any roadmap author cares
81
+ * about, so trimming there is safe.
82
+ */
83
+ export function formatCapacityNumber(value: number): string {
84
+ if (Number.isInteger(value)) return String(value);
85
+ let s = value.toFixed(6);
86
+ s = s.replace(/0+$/, '');
87
+ s = s.replace(/\.$/, '');
88
+ return s;
89
+ }
90
+
91
+ /**
92
+ * Resolved capacity-icon ready for the renderer. Re-exported from
93
+ * `./types.js` so callers can `import { ResolvedCapacityIcon } from
94
+ * './capacity.js'` without reaching into the positioned-model module.
95
+ *
96
+ * Two flavors:
97
+ *
98
+ * - `kind: 'builtin'` โ€” the renderer looks up its SVG (person/people/
99
+ * points/time) or text representation (multiplier) via its icon library.
100
+ * `'none'` is collapsed to `null` upstream (no glyph rendered).
101
+ * - `kind: 'literal'` โ€” the renderer paints `text` as a `<text>` node.
102
+ * Covers inline Unicode literals (`capacity-icon:"๐Ÿ’ฐ"`) and dereferenced
103
+ * custom `symbol` declarations.
104
+ */
105
+ export type ResolvedCapacityIcon = ResolvedCapacityIconRef;
106
+
107
+ /**
108
+ * Read a property value off a Langium-shaped EntityProperty, normalizing the
109
+ * trailing-colon form. Validator-side uses the same trick โ€” the grammar
110
+ * stores `key` as the *raw* token, including the colon for `unicode:`-style
111
+ * property keys.
112
+ */
113
+ function propKey(prop: { key: string }): string {
114
+ return prop.key.endsWith(':') ? prop.key.slice(0, -1) : prop.key;
115
+ }
116
+
117
+ function symbolUnicode(decl: SymbolDeclaration): string | undefined {
118
+ for (const p of decl.properties) {
119
+ if (propKey(p) === 'unicode' && p.value) return p.value;
120
+ }
121
+ return undefined;
122
+ }
123
+
124
+ /**
125
+ * Resolve a `capacity-icon:` style value into a `ResolvedCapacityIcon` (or
126
+ * `null` for `'none'`).
127
+ *
128
+ * Resolution order matches specs/dsl.md ยง Style Properties for `icon:` and
129
+ * `capacity-icon:`:
130
+ *
131
+ * 1. `'none'` โ†’ `null` (renderer emits no glyph).
132
+ * 2. Built-in name โ†’ `{ kind: 'builtin', name }`.
133
+ * 3. Custom symbol id present in `symbols` โ†’ `{ kind: 'literal', text:
134
+ * <unicode:> }`. The author wrote an identifier; we hand the
135
+ * renderer the underlying Unicode payload.
136
+ * 4. Anything else โ†’ `{ kind: 'literal', text: icon }`. This is the inline
137
+ * Unicode literal form (`capacity-icon:"๐Ÿ’ฐ"`) โ€” Langium's
138
+ * ValueConverter has already stripped the surrounding quotes, so the
139
+ * raw payload arrives here.
140
+ *
141
+ * Validator rule 17 already rejects malformed combinations (unknown built-in
142
+ * with no matching symbol, symbol id collision with built-ins, etc.), so this
143
+ * function trusts its input shape.
144
+ */
145
+ export function resolveCapacityIcon(
146
+ icon: string,
147
+ symbols: Map<string, SymbolDeclaration>,
148
+ ): ResolvedCapacityIcon | null {
149
+ if (icon === 'none') return null;
150
+ if (BUILTIN_CAPACITY_ICONS.has(icon)) {
151
+ return {
152
+ kind: 'builtin',
153
+ name: icon as 'multiplier' | 'person' | 'people' | 'points' | 'time',
154
+ };
155
+ }
156
+ const custom = symbols.get(icon);
157
+ if (custom) {
158
+ const unicode = symbolUnicode(custom);
159
+ return { kind: 'literal', text: unicode ?? icon };
160
+ }
161
+ return { kind: 'literal', text: icon };
162
+ }
163
+
164
+ /**
165
+ * Estimate the on-screen width (px) the capacity suffix will occupy at the
166
+ * given font size, including the leading separator gap before the glyph.
167
+ *
168
+ * The renderer paints the suffix as `<num>{gap}<glyph>` (no leading space
169
+ * before the number โ€” callers handle that as an outer separator). Width
170
+ * estimates are intentionally pessimistic so borderline-fitting captions
171
+ * trigger spill rather than clip.
172
+ */
173
+ export function estimateCapacitySuffixWidth(
174
+ text: string,
175
+ icon: ResolvedCapacityIcon | null,
176
+ fontSizePx: number,
177
+ ): number {
178
+ if (!icon) return text.length * fontSizePx * 0.58;
179
+ if (icon.kind === 'builtin' && icon.name === 'multiplier') {
180
+ // Multiplier is a typographic operator with built-in side bearing โ€”
181
+ // no separator gap, glyph width approx. one character.
182
+ return (text.length + 1) * fontSizePx * 0.58;
183
+ }
184
+ // 0.1em separator + 1em glyph. Estimating the glyph as 1em (rather than
185
+ // 0.58em, the per-character width) is intentional: SVG icons render at
186
+ // their full font-size square, and most Unicode literals authors use for
187
+ // capacity (โ˜…, ๐Ÿ’ฐ, โš™) similarly read at near-em widths.
188
+ const glyphWidthEm = 1.0;
189
+ const gapEm = 0.1;
190
+ return text.length * fontSizePx * 0.58 + (gapEm + glyphWidthEm) * fontSizePx;
191
+ }
@@ -0,0 +1,30 @@
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
+
8
+ import type { EntityProperty } from '@nowline/core';
9
+
10
+ function stripColon(key: string): string {
11
+ return key.endsWith(':') ? key.slice(0, -1) : key;
12
+ }
13
+
14
+ export function propValue(props: EntityProperty[], key: string): string | undefined {
15
+ return props.find((p) => stripColon(p.key) === key)?.value;
16
+ }
17
+
18
+ export function propValues(props: EntityProperty[], key: string): string[] {
19
+ const p = props.find((x) => stripColon(x.key) === key);
20
+ if (!p) return [];
21
+ return p.value !== undefined ? [p.value] : [...p.values];
22
+ }
23
+
24
+ export function parseDate(raw: string | undefined): Date | null {
25
+ if (!raw) return null;
26
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
27
+ if (!m) return null;
28
+ const d = new Date(Date.UTC(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10)));
29
+ return Number.isNaN(d.getTime()) ? null : d;
30
+ }