@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,180 @@
1
+ // ViewPreset — declarative configuration for the timeline header
2
+ // (tick stride, label thinning, label format). Replaces the
3
+ // imperative `for` loop and `formatTickLabel` switch in the legacy
4
+ // `timeline.ts`.
5
+ //
6
+ // `resolveScale` parses the DSL `scale:` property (and any nested
7
+ // `scale` block) into a `ViewPreset`. `buildHeaderTicks` produces
8
+ // the `PositionedTick[]` byte-stable with the legacy generator: same
9
+ // x positions, same labelX positions, same major/minor flags, same
10
+ // label text.
11
+
12
+ import type { NowlineFile, ScaleBlock } from '@nowline/core';
13
+ import { addDays } from './calendar.js';
14
+ import { DEFAULT_LOCALE, localeStrings } from './i18n.js';
15
+ import { DEFAULT_PIXELS_PER_DAY, LABEL_THINNING } from './themes/shared.js';
16
+ import type { TimeScale } from './time-scale.js';
17
+ import type { PositionedTick } from './types.js';
18
+ import type { WorkingCalendar } from './working-calendar.js';
19
+
20
+ export type ScaleUnit = 'days' | 'weeks' | 'months' | 'quarters' | 'years';
21
+
22
+ export interface ViewPreset {
23
+ /** Tick stride unit (each tick is one `unit` apart). */
24
+ unit: ScaleUnit;
25
+ /** Show a label every N ticks (1 = every tick gets a label). */
26
+ labelEvery: number;
27
+ /** Pixels per `1 unit` worth of working days. */
28
+ pixelsPerUnit: number;
29
+ }
30
+
31
+ // `ScaleConfig` is kept as an alias for source-compat with the few
32
+ // callers that still spell the old name; new code should use
33
+ // `ViewPreset`.
34
+ export type ScaleConfig = ViewPreset;
35
+
36
+ function stripColon(key: string): string {
37
+ return key.endsWith(':') ? key.slice(0, -1) : key;
38
+ }
39
+
40
+ export function resolveScale(file: NowlineFile, scaleBlock: ScaleBlock | undefined): ViewPreset {
41
+ const scaleProp = file.roadmapDecl?.properties.find((p) => stripColon(p.key) === 'scale');
42
+ // `scale:` accepts a unit name (`days`/`weeks`/`months`/`quarters`/`years`)
43
+ // OR a duration literal (`1w`, `2w`, `1m`, `1q`, `1y`). The literal form
44
+ // is the documented default in the DSL spec; it picks the unit and uses
45
+ // the literal's count to size the pixels-per-unit budget.
46
+ const rawScale = scaleProp?.value;
47
+ let unit: ScaleUnit = 'weeks';
48
+ let pixelsPerUnitOverride: number | undefined;
49
+ let labelEveryOverride: number | undefined;
50
+ if (rawScale) {
51
+ if (
52
+ rawScale === 'days' ||
53
+ rawScale === 'weeks' ||
54
+ rawScale === 'months' ||
55
+ rawScale === 'quarters' ||
56
+ rawScale === 'years'
57
+ ) {
58
+ unit = rawScale;
59
+ } else {
60
+ const dur = /^(\d+)([dwmqy])$/.exec(rawScale);
61
+ if (dur) {
62
+ const n = Math.max(1, parseInt(dur[1], 10));
63
+ switch (dur[2]) {
64
+ case 'd':
65
+ unit = 'days';
66
+ break;
67
+ case 'w':
68
+ unit = 'weeks';
69
+ break;
70
+ case 'm':
71
+ unit = 'months';
72
+ break;
73
+ case 'q':
74
+ unit = 'quarters';
75
+ break;
76
+ case 'y':
77
+ unit = 'years';
78
+ break;
79
+ }
80
+ pixelsPerUnitOverride = unitPx(unit) * n;
81
+ // A literal scale like `1w` says "I want exactly one label per
82
+ // unit." Override the default thinning so every tick is named.
83
+ labelEveryOverride = 1;
84
+ }
85
+ }
86
+ }
87
+ const defaultLabelEvery = labelEveryOverride ?? LABEL_THINNING[unit] ?? 4;
88
+
89
+ if (scaleBlock) {
90
+ const unitProp = scaleBlock.properties.find((p) => stripColon(p.key) === 'unit');
91
+ const resolvedUnit: ScaleUnit = (unitProp?.value as ScaleUnit) ?? unit;
92
+ const labelProp = scaleBlock.properties.find((p) => stripColon(p.key) === 'label-every');
93
+ const pxProp = scaleBlock.properties.find((p) => stripColon(p.key) === 'pixels-per-unit');
94
+ const labelEvery = labelProp
95
+ ? Math.max(1, parseInt(labelProp.value, 10) || defaultLabelEvery)
96
+ : defaultLabelEvery;
97
+ const pixelsPerUnit = pxProp
98
+ ? Math.max(1, parseInt(pxProp.value, 10) || unitPx(resolvedUnit))
99
+ : (pixelsPerUnitOverride ?? unitPx(resolvedUnit));
100
+ return { unit: resolvedUnit, labelEvery, pixelsPerUnit };
101
+ }
102
+
103
+ return {
104
+ unit,
105
+ labelEvery: defaultLabelEvery,
106
+ pixelsPerUnit: pixelsPerUnitOverride ?? unitPx(unit),
107
+ };
108
+ }
109
+
110
+ function unitPx(unit: ScaleUnit): number {
111
+ // Baseline pixel widths per one unit, tuned so ~6 month roadmaps fit
112
+ // comfortably in a 1200 px wide chart area.
113
+ switch (unit) {
114
+ case 'days':
115
+ return DEFAULT_PIXELS_PER_DAY;
116
+ case 'weeks':
117
+ return 40;
118
+ case 'months':
119
+ return 80;
120
+ case 'quarters':
121
+ return 160;
122
+ case 'years':
123
+ return 320;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Build header ticks for the chart. The ith tick sits at
129
+ * `originX + i * stridePx`. The last tick is rendered (so the chart
130
+ * has a closing edge) but its label is suppressed because there's no
131
+ * following column.
132
+ */
133
+ export function buildHeaderTicks(
134
+ scale: TimeScale,
135
+ preset: ViewPreset,
136
+ calendar: WorkingCalendar,
137
+ locale: string = DEFAULT_LOCALE,
138
+ ): PositionedTick[] {
139
+ const dayPerTick = calendar.daysPerUnit(preset.unit);
140
+ const stridePx = dayPerTick * scale.pixelsPerDay;
141
+ const totalDays = Math.max(1, Math.round(scale.widthPx / scale.pixelsPerDay));
142
+ const tickCount = Math.floor(totalDays / dayPerTick) + 1;
143
+ const ticks: PositionedTick[] = [];
144
+ for (let i = 0; i < tickCount; i++) {
145
+ const days = i * dayPerTick;
146
+ const x = scale.originX + days * scale.pixelsPerDay;
147
+ const isMajor = i % preset.labelEvery === 0;
148
+ const isLast = i === tickCount - 1;
149
+ ticks.push({
150
+ x,
151
+ labelX: isLast ? undefined : x + stridePx / 2,
152
+ major: isMajor,
153
+ label:
154
+ isMajor && !isLast
155
+ ? formatTickLabel(preset.unit, addDays(scale.domain[0], days), i, locale)
156
+ : undefined,
157
+ });
158
+ }
159
+ return ticks;
160
+ }
161
+
162
+ function formatTickLabel(unit: ScaleUnit, date: Date, _index: number, locale: string): string {
163
+ switch (unit) {
164
+ case 'days':
165
+ return `${date.getUTCMonth() + 1}/${date.getUTCDate()}`;
166
+ case 'weeks': {
167
+ const month = date.toLocaleString(locale, { month: 'short', timeZone: 'UTC' });
168
+ const day = date.getUTCDate().toString().padStart(2, '0');
169
+ return `${month} ${day}`;
170
+ }
171
+ case 'months':
172
+ return date.toLocaleString(locale, { month: 'short', timeZone: 'UTC' });
173
+ case 'quarters': {
174
+ const q = Math.floor(date.getUTCMonth() / 3) + 1;
175
+ return `${localeStrings(locale).quarterPrefix}${q} ${date.getUTCFullYear()}`;
176
+ }
177
+ case 'years':
178
+ return `${date.getUTCFullYear()}`;
179
+ }
180
+ }
@@ -0,0 +1,91 @@
1
+ // WorkingCalendar primitive — strategy interface for non-continuous
2
+ // time models (skip weekends, holidays, custom shutdowns). Today the
3
+ // rendering pipeline treats time as continuous; the existing
4
+ // `CalendarConfig` only changes how `1w` literals expand into days,
5
+ // not whether ticks skip weekends. WorkingCalendar establishes the
6
+ // API surface for the future weekend/holiday work without making
7
+ // `TimeScale` and `ViewPreset` aware of `CalendarConfig` directly.
8
+ //
9
+ // The default factories `continuousCalendar()` and
10
+ // `fromCalendarConfig(cal)` are pass-through: every calendar day is a
11
+ // working day, units expand using `CalendarConfig.daysPer*`. Future
12
+ // non-continuous calendars override `nextWorkingDay` and `addUnits`
13
+ // without changing the consumer surface.
14
+
15
+ import type { CalendarConfig } from './calendar.js';
16
+ import { addDays as addCalendarDays } from './calendar.js';
17
+ import type { ScaleUnit } from './view-preset.js';
18
+
19
+ export interface WorkingCalendar {
20
+ /** Days per `1<unit>` literal (e.g. `1w` → 5 for business). */
21
+ daysPerUnit(unit: ScaleUnit): number;
22
+ /** Move forward by N units (e.g. `addUnits(d, 2, 'weeks')`). */
23
+ addUnits(date: Date, count: number, unit: ScaleUnit): Date;
24
+ /** True when the given date is a working day in this calendar. */
25
+ isWorkingDay(date: Date): boolean;
26
+ }
27
+
28
+ export function fromCalendarConfig(cal: CalendarConfig): WorkingCalendar {
29
+ return {
30
+ daysPerUnit: (unit) => daysPerUnitForCalendar(unit, cal),
31
+ addUnits: (date, count, unit) =>
32
+ addCalendarDays(date, count * daysPerUnitForCalendar(unit, cal)),
33
+ // The continuous model used today treats every calendar day as a
34
+ // working day; non-continuous calendars will override this to
35
+ // implement weekend/holiday skipping when the work lands.
36
+ isWorkingDay: () => true,
37
+ };
38
+ }
39
+
40
+ export function continuousCalendar(): WorkingCalendar {
41
+ return {
42
+ daysPerUnit: (unit) => {
43
+ switch (unit) {
44
+ case 'days':
45
+ return 1;
46
+ case 'weeks':
47
+ return 7;
48
+ case 'months':
49
+ return 30;
50
+ case 'quarters':
51
+ return 91;
52
+ case 'years':
53
+ return 365;
54
+ }
55
+ },
56
+ addUnits: (date, count, unit) => {
57
+ const days =
58
+ count *
59
+ (unit === 'days'
60
+ ? 1
61
+ : unit === 'weeks'
62
+ ? 7
63
+ : unit === 'months'
64
+ ? 30
65
+ : unit === 'quarters'
66
+ ? 91
67
+ : 365);
68
+ return addCalendarDays(date, days);
69
+ },
70
+ isWorkingDay: () => true,
71
+ };
72
+ }
73
+
74
+ export function daysPerUnit(unit: ScaleUnit, cal: CalendarConfig): number {
75
+ return daysPerUnitForCalendar(unit, cal);
76
+ }
77
+
78
+ function daysPerUnitForCalendar(unit: ScaleUnit, cal: CalendarConfig): number {
79
+ switch (unit) {
80
+ case 'days':
81
+ return 1;
82
+ case 'weeks':
83
+ return cal.daysPerWeek;
84
+ case 'months':
85
+ return cal.daysPerMonth;
86
+ case 'quarters':
87
+ return cal.daysPerQuarter;
88
+ case 'years':
89
+ return cal.daysPerYear;
90
+ }
91
+ }