@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.
- package/LICENSE +190 -0
- package/README.md +103 -0
- package/dist/band-scale.d.ts +56 -0
- package/dist/band-scale.d.ts.map +1 -0
- package/dist/band-scale.js +86 -0
- package/dist/band-scale.js.map +1 -0
- package/dist/calendar.d.ts +79 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +210 -0
- package/dist/calendar.js.map +1 -0
- package/dist/capacity.d.ts +72 -0
- package/dist/capacity.d.ts.map +1 -0
- package/dist/capacity.js +163 -0
- package/dist/capacity.js.map +1 -0
- package/dist/dsl-utils.d.ts +5 -0
- package/dist/dsl-utils.d.ts.map +1 -0
- package/dist/dsl-utils.js +28 -0
- package/dist/dsl-utils.js.map +1 -0
- package/dist/edge-routing.d.ts +89 -0
- package/dist/edge-routing.d.ts.map +1 -0
- package/dist/edge-routing.js +435 -0
- package/dist/edge-routing.js.map +1 -0
- package/dist/frame-tab-geometry.d.ts +78 -0
- package/dist/frame-tab-geometry.d.ts.map +1 -0
- package/dist/frame-tab-geometry.js +115 -0
- package/dist/frame-tab-geometry.js.map +1 -0
- package/dist/header-card-geometry.d.ts +29 -0
- package/dist/header-card-geometry.d.ts.map +1 -0
- package/dist/header-card-geometry.js +41 -0
- package/dist/header-card-geometry.js.map +1 -0
- package/dist/i18n.d.ts +48 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +114 -0
- package/dist/i18n.js.map +1 -0
- package/dist/include-chrome-geometry.d.ts +86 -0
- package/dist/include-chrome-geometry.d.ts.map +1 -0
- package/dist/include-chrome-geometry.js +104 -0
- package/dist/include-chrome-geometry.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/item-bar-geometry.d.ts +127 -0
- package/dist/item-bar-geometry.d.ts.map +1 -0
- package/dist/item-bar-geometry.js +173 -0
- package/dist/item-bar-geometry.js.map +1 -0
- package/dist/lane-utilization.d.ts +90 -0
- package/dist/lane-utilization.d.ts.map +1 -0
- package/dist/lane-utilization.js +206 -0
- package/dist/lane-utilization.js.map +1 -0
- package/dist/layout-context.d.ts +143 -0
- package/dist/layout-context.d.ts.map +1 -0
- package/dist/layout-context.js +8 -0
- package/dist/layout-context.js.map +1 -0
- package/dist/layout.d.ts +18 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +1213 -0
- package/dist/layout.js.map +1 -0
- package/dist/nodes/anchor-node.d.ts +16 -0
- package/dist/nodes/anchor-node.d.ts.map +1 -0
- package/dist/nodes/anchor-node.js +68 -0
- package/dist/nodes/anchor-node.js.map +1 -0
- package/dist/nodes/footnote-node.d.ts +10 -0
- package/dist/nodes/footnote-node.d.ts.map +1 -0
- package/dist/nodes/footnote-node.js +41 -0
- package/dist/nodes/footnote-node.js.map +1 -0
- package/dist/nodes/group-node.d.ts +29 -0
- package/dist/nodes/group-node.d.ts.map +1 -0
- package/dist/nodes/group-node.js +187 -0
- package/dist/nodes/group-node.js.map +1 -0
- package/dist/nodes/include-node.d.ts +16 -0
- package/dist/nodes/include-node.d.ts.map +1 -0
- package/dist/nodes/include-node.js +117 -0
- package/dist/nodes/include-node.js.map +1 -0
- package/dist/nodes/item-node.d.ts +51 -0
- package/dist/nodes/item-node.d.ts.map +1 -0
- package/dist/nodes/item-node.js +108 -0
- package/dist/nodes/item-node.js.map +1 -0
- package/dist/nodes/marker-geometry.d.ts +22 -0
- package/dist/nodes/marker-geometry.d.ts.map +1 -0
- package/dist/nodes/marker-geometry.js +38 -0
- package/dist/nodes/marker-geometry.js.map +1 -0
- package/dist/nodes/milestone-node.d.ts +48 -0
- package/dist/nodes/milestone-node.d.ts.map +1 -0
- package/dist/nodes/milestone-node.js +210 -0
- package/dist/nodes/milestone-node.js.map +1 -0
- package/dist/nodes/parallel-node.d.ts +21 -0
- package/dist/nodes/parallel-node.d.ts.map +1 -0
- package/dist/nodes/parallel-node.js +80 -0
- package/dist/nodes/parallel-node.js.map +1 -0
- package/dist/nodes/roadmap-node.d.ts +76 -0
- package/dist/nodes/roadmap-node.d.ts.map +1 -0
- package/dist/nodes/roadmap-node.js +788 -0
- package/dist/nodes/roadmap-node.js.map +1 -0
- package/dist/nodes/swimlane-node.d.ts +38 -0
- package/dist/nodes/swimlane-node.d.ts.map +1 -0
- package/dist/nodes/swimlane-node.js +308 -0
- package/dist/nodes/swimlane-node.js.map +1 -0
- package/dist/renderable.d.ts +44 -0
- package/dist/renderable.d.ts.map +1 -0
- package/dist/renderable.js +21 -0
- package/dist/renderable.js.map +1 -0
- package/dist/row-packer.d.ts +125 -0
- package/dist/row-packer.d.ts.map +1 -0
- package/dist/row-packer.js +169 -0
- package/dist/row-packer.js.map +1 -0
- package/dist/style-resolution.d.ts +14 -0
- package/dist/style-resolution.d.ts.map +1 -0
- package/dist/style-resolution.js +191 -0
- package/dist/style-resolution.js.map +1 -0
- package/dist/themes/dark.d.ts +4 -0
- package/dist/themes/dark.d.ts.map +1 -0
- package/dist/themes/dark.js +241 -0
- package/dist/themes/dark.js.map +1 -0
- package/dist/themes/index.d.ts +15 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +30 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/themes/light.d.ts +4 -0
- package/dist/themes/light.d.ts.map +1 -0
- package/dist/themes/light.js +248 -0
- package/dist/themes/light.js.map +1 -0
- package/dist/themes/shape.d.ts +194 -0
- package/dist/themes/shape.d.ts.map +1 -0
- package/dist/themes/shape.js +6 -0
- package/dist/themes/shape.js.map +1 -0
- package/dist/themes/shared.d.ts +145 -0
- package/dist/themes/shared.d.ts.map +1 -0
- package/dist/themes/shared.js +310 -0
- package/dist/themes/shared.js.map +1 -0
- package/dist/time-scale.d.ts +39 -0
- package/dist/time-scale.d.ts.map +1 -0
- package/dist/time-scale.js +62 -0
- package/dist/time-scale.js.map +1 -0
- package/dist/types.d.ts +483 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/view-preset.d.ts +23 -0
- package/dist/view-preset.d.ts.map +1 -0
- package/dist/view-preset.js +146 -0
- package/dist/view-preset.js.map +1 -0
- package/dist/working-calendar.d.ts +14 -0
- package/dist/working-calendar.d.ts.map +1 -0
- package/dist/working-calendar.js +74 -0
- package/dist/working-calendar.js.map +1 -0
- package/package.json +37 -0
- package/src/band-scale.ts +115 -0
- package/src/calendar.ts +244 -0
- package/src/capacity.ts +191 -0
- package/src/dsl-utils.ts +30 -0
- package/src/edge-routing.ts +550 -0
- package/src/frame-tab-geometry.ts +165 -0
- package/src/header-card-geometry.ts +48 -0
- package/src/i18n.ts +124 -0
- package/src/include-chrome-geometry.ts +156 -0
- package/src/index.ts +116 -0
- package/src/item-bar-geometry.ts +222 -0
- package/src/lane-utilization.ts +259 -0
- package/src/layout-context.ts +182 -0
- package/src/layout.ts +1446 -0
- package/src/nodes/anchor-node.ts +77 -0
- package/src/nodes/footnote-node.ts +60 -0
- package/src/nodes/group-node.ts +252 -0
- package/src/nodes/include-node.ts +168 -0
- package/src/nodes/item-node.ts +171 -0
- package/src/nodes/marker-geometry.ts +43 -0
- package/src/nodes/milestone-node.ts +263 -0
- package/src/nodes/parallel-node.ts +101 -0
- package/src/nodes/roadmap-node.ts +957 -0
- package/src/nodes/swimlane-node.ts +423 -0
- package/src/renderable.ts +68 -0
- package/src/row-packer.ts +271 -0
- package/src/style-resolution.ts +243 -0
- package/src/themes/dark.ts +244 -0
- package/src/themes/index.ts +36 -0
- package/src/themes/light.ts +251 -0
- package/src/themes/shape.ts +230 -0
- package/src/themes/shared.ts +369 -0
- package/src/time-scale.ts +78 -0
- package/src/types.ts +607 -0
- package/src/view-preset.ts +180 -0
- package/src/working-calendar.ts +91 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-preset.js","sourceRoot":"","sources":["../src/view-preset.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CalendarConfig } from './calendar.js';
|
|
2
|
+
import type { ScaleUnit } from './view-preset.js';
|
|
3
|
+
export interface WorkingCalendar {
|
|
4
|
+
/** Days per `1<unit>` literal (e.g. `1w` → 5 for business). */
|
|
5
|
+
daysPerUnit(unit: ScaleUnit): number;
|
|
6
|
+
/** Move forward by N units (e.g. `addUnits(d, 2, 'weeks')`). */
|
|
7
|
+
addUnits(date: Date, count: number, unit: ScaleUnit): Date;
|
|
8
|
+
/** True when the given date is a working day in this calendar. */
|
|
9
|
+
isWorkingDay(date: Date): boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function fromCalendarConfig(cal: CalendarConfig): WorkingCalendar;
|
|
12
|
+
export declare function continuousCalendar(): WorkingCalendar;
|
|
13
|
+
export declare function daysPerUnit(unit: ScaleUnit, cal: CalendarConfig): number;
|
|
14
|
+
//# sourceMappingURL=working-calendar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"working-calendar.d.ts","sourceRoot":"","sources":["../src/working-calendar.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,WAAW,eAAe;IAC5B,+DAA+D;IAC/D,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACrC,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAC3D,kEAAkE;IAClE,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC;CACrC;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,eAAe,CAUvE;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAgCpD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,cAAc,GAAG,MAAM,CAExE"}
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import { addDays as addCalendarDays } from './calendar.js';
|
|
15
|
+
export function fromCalendarConfig(cal) {
|
|
16
|
+
return {
|
|
17
|
+
daysPerUnit: (unit) => daysPerUnitForCalendar(unit, cal),
|
|
18
|
+
addUnits: (date, count, unit) => addCalendarDays(date, count * daysPerUnitForCalendar(unit, cal)),
|
|
19
|
+
// The continuous model used today treats every calendar day as a
|
|
20
|
+
// working day; non-continuous calendars will override this to
|
|
21
|
+
// implement weekend/holiday skipping when the work lands.
|
|
22
|
+
isWorkingDay: () => true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function continuousCalendar() {
|
|
26
|
+
return {
|
|
27
|
+
daysPerUnit: (unit) => {
|
|
28
|
+
switch (unit) {
|
|
29
|
+
case 'days':
|
|
30
|
+
return 1;
|
|
31
|
+
case 'weeks':
|
|
32
|
+
return 7;
|
|
33
|
+
case 'months':
|
|
34
|
+
return 30;
|
|
35
|
+
case 'quarters':
|
|
36
|
+
return 91;
|
|
37
|
+
case 'years':
|
|
38
|
+
return 365;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
addUnits: (date, count, unit) => {
|
|
42
|
+
const days = count *
|
|
43
|
+
(unit === 'days'
|
|
44
|
+
? 1
|
|
45
|
+
: unit === 'weeks'
|
|
46
|
+
? 7
|
|
47
|
+
: unit === 'months'
|
|
48
|
+
? 30
|
|
49
|
+
: unit === 'quarters'
|
|
50
|
+
? 91
|
|
51
|
+
: 365);
|
|
52
|
+
return addCalendarDays(date, days);
|
|
53
|
+
},
|
|
54
|
+
isWorkingDay: () => true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function daysPerUnit(unit, cal) {
|
|
58
|
+
return daysPerUnitForCalendar(unit, cal);
|
|
59
|
+
}
|
|
60
|
+
function daysPerUnitForCalendar(unit, cal) {
|
|
61
|
+
switch (unit) {
|
|
62
|
+
case 'days':
|
|
63
|
+
return 1;
|
|
64
|
+
case 'weeks':
|
|
65
|
+
return cal.daysPerWeek;
|
|
66
|
+
case 'months':
|
|
67
|
+
return cal.daysPerMonth;
|
|
68
|
+
case 'quarters':
|
|
69
|
+
return cal.daysPerQuarter;
|
|
70
|
+
case 'years':
|
|
71
|
+
return cal.daysPerYear;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=working-calendar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"working-calendar.js","sourceRoot":"","sources":["../src/working-calendar.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,qEAAqE;AACrE,6DAA6D;AAC7D,oEAAoE;AACpE,mEAAmE;AACnE,iEAAiE;AACjE,mEAAmE;AACnE,EAAE;AACF,mDAAmD;AACnD,sEAAsE;AACtE,oEAAoE;AACpE,oEAAoE;AACpE,yCAAyC;AAGzC,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,eAAe,CAAC;AAY3D,MAAM,UAAU,kBAAkB,CAAC,GAAmB;IAClD,OAAO;QACH,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC;QACxD,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAC5B,eAAe,CAAC,IAAI,EAAE,KAAK,GAAG,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACpE,iEAAiE;QACjE,8DAA8D;QAC9D,0DAA0D;QAC1D,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI;KAC3B,CAAC;AACN,CAAC;AAED,MAAM,UAAU,kBAAkB;IAC9B,OAAO;QACH,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE;YAClB,QAAQ,IAAI,EAAE,CAAC;gBACX,KAAK,MAAM;oBACP,OAAO,CAAC,CAAC;gBACb,KAAK,OAAO;oBACR,OAAO,CAAC,CAAC;gBACb,KAAK,QAAQ;oBACT,OAAO,EAAE,CAAC;gBACd,KAAK,UAAU;oBACX,OAAO,EAAE,CAAC;gBACd,KAAK,OAAO;oBACR,OAAO,GAAG,CAAC;YACnB,CAAC;QACL,CAAC;QACD,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAC5B,MAAM,IAAI,GACN,KAAK;gBACL,CAAC,IAAI,KAAK,MAAM;oBACZ,CAAC,CAAC,CAAC;oBACH,CAAC,CAAC,IAAI,KAAK,OAAO;wBAChB,CAAC,CAAC,CAAC;wBACH,CAAC,CAAC,IAAI,KAAK,QAAQ;4BACjB,CAAC,CAAC,EAAE;4BACJ,CAAC,CAAC,IAAI,KAAK,UAAU;gCACnB,CAAC,CAAC,EAAE;gCACJ,CAAC,CAAC,GAAG,CAAC,CAAC;YACrB,OAAO,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACvC,CAAC;QACD,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI;KAC3B,CAAC;AACN,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAe,EAAE,GAAmB;IAC5D,OAAO,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,sBAAsB,CAAC,IAAe,EAAE,GAAmB;IAChE,QAAQ,IAAI,EAAE,CAAC;QACX,KAAK,MAAM;YACP,OAAO,CAAC,CAAC;QACb,KAAK,OAAO;YACR,OAAO,GAAG,CAAC,WAAW,CAAC;QAC3B,KAAK,QAAQ;YACT,OAAO,GAAG,CAAC,YAAY,CAAC;QAC5B,KAAK,UAAU;YACX,OAAO,GAAG,CAAC,cAAc,CAAC;QAC9B,KAAK,OAAO;YACR,OAAO,GAAG,CAAC,WAAW,CAAC;IAC/B,CAAC;AACL,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nowline/layout",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Nowline layout engine — AST → positioned model for rendering",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"src/"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"d3-scale": "^4.0.2",
|
|
21
|
+
"d3-time": "^3.1.0",
|
|
22
|
+
"@nowline/core": "0.2.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/d3-scale": "^4.0.8",
|
|
26
|
+
"@types/d3-time": "^3.0.3",
|
|
27
|
+
"langium": "~4.2.2",
|
|
28
|
+
"typescript": "~5.7.0",
|
|
29
|
+
"vitest": "^3.1.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -b tsconfig.json",
|
|
33
|
+
"watch": "tsc -b tsconfig.json --watch",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// BandScale — wraps `d3-scale.scaleBand` for the y-axis row stack.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the magic `ITEM_ROW_HEIGHT = 64` constant with a typed
|
|
4
|
+
// primitive that exposes `bandwidth()` (one row's visible height)
|
|
5
|
+
// and `step()` (row + inter-row gap). m2.5b's job is to land this
|
|
6
|
+
// surface; m2.5c will switch to the measure/place tree where each
|
|
7
|
+
// node returns its own intrinsic height and BandScale becomes the
|
|
8
|
+
// composition primitive parents stack with.
|
|
9
|
+
//
|
|
10
|
+
// `BandScale.forRows({ count, range })` matches the d3-scale.scaleBand
|
|
11
|
+
// constructor (domain = ['0', '1', ..., 'count-1']) but keeps the
|
|
12
|
+
// caller's step/bandwidth fixed when the row count is known up front.
|
|
13
|
+
// The current layout pipeline iterates rows lazily, so we also expose
|
|
14
|
+
// `BandScale.fixedRow({ bandwidth, step })` which doesn't anchor to a
|
|
15
|
+
// finite domain — the swimlane row loop uses this to know
|
|
16
|
+
// `bandwidth()` and `step()` without committing to a row count.
|
|
17
|
+
|
|
18
|
+
import { type ScaleBand, scaleBand } from 'd3-scale';
|
|
19
|
+
|
|
20
|
+
export interface FixedRowOptions {
|
|
21
|
+
/** Visible height of one row in pixels. */
|
|
22
|
+
bandwidth: number;
|
|
23
|
+
/** Pixels between the top of consecutive rows. Must be >= bandwidth. */
|
|
24
|
+
step: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BandedRangeOptions {
|
|
28
|
+
/** Number of bands. */
|
|
29
|
+
count: number;
|
|
30
|
+
/** [topY, bottomY]. */
|
|
31
|
+
range: [number, number];
|
|
32
|
+
/** Inner padding ratio (0..1) per d3-scale.scaleBand. Defaults to 0. */
|
|
33
|
+
paddingInner?: number;
|
|
34
|
+
/** Outer padding ratio (0..1) per d3-scale.scaleBand. Defaults to 0. */
|
|
35
|
+
paddingOuter?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class BandScale {
|
|
39
|
+
private readonly bandwidthPx: number;
|
|
40
|
+
private readonly stepPx: number;
|
|
41
|
+
private readonly d3?: ScaleBand<string>;
|
|
42
|
+
|
|
43
|
+
private constructor(bandwidth: number, step: number, d3?: ScaleBand<string>) {
|
|
44
|
+
this.bandwidthPx = bandwidth;
|
|
45
|
+
this.stepPx = step;
|
|
46
|
+
this.d3 = d3;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Open-ended row band with a fixed bandwidth + step. Used when
|
|
51
|
+
* the row count is determined by iteration (the v1 swimlane row
|
|
52
|
+
* packer). m2.5c will tighten this once the measure/place tree
|
|
53
|
+
* lets us know row counts ahead of time.
|
|
54
|
+
*/
|
|
55
|
+
static fixedRow(opts: FixedRowOptions): BandScale {
|
|
56
|
+
return new BandScale(opts.bandwidth, opts.step);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Closed-domain band that wraps d3-scale.scaleBand. Use when the
|
|
61
|
+
* full set of band ids is known at construction time (e.g. the
|
|
62
|
+
* top-level swimlane stack). The `forward(id)` API resolves an
|
|
63
|
+
* id to its top-y.
|
|
64
|
+
*/
|
|
65
|
+
static forRows(opts: BandedRangeOptions): BandScale {
|
|
66
|
+
const ids = Array.from({ length: opts.count }, (_, i) => String(i));
|
|
67
|
+
const d3 = scaleBand<string>()
|
|
68
|
+
.domain(ids)
|
|
69
|
+
.range(opts.range)
|
|
70
|
+
.paddingInner(opts.paddingInner ?? 0)
|
|
71
|
+
.paddingOuter(opts.paddingOuter ?? 0);
|
|
72
|
+
return new BandScale(d3.bandwidth(), d3.step(), d3);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Visible height of one row. */
|
|
76
|
+
bandwidth(): number {
|
|
77
|
+
return this.bandwidthPx;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Row + inter-row gap (top of row N to top of row N+1). */
|
|
81
|
+
step(): number {
|
|
82
|
+
return this.stepPx;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Top-y for a band id (only available on `forRows` instances). */
|
|
86
|
+
forward(id: string): number {
|
|
87
|
+
if (!this.d3) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'BandScale.forward is only available on closed-domain bands (BandScale.forRows)',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const y = this.d3(id);
|
|
93
|
+
if (y === undefined) {
|
|
94
|
+
throw new Error(`BandScale: unknown band id "${id}"`);
|
|
95
|
+
}
|
|
96
|
+
return y;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Default row band for the v1 layout: bandwidth = 56 px (the legacy
|
|
102
|
+
* bar height = ITEM_ROW_HEIGHT - 8), step = 64 px (the legacy row
|
|
103
|
+
* pitch = ITEM_ROW_HEIGHT). Keeps every existing sample byte-stable.
|
|
104
|
+
*
|
|
105
|
+
* m2.5c will derive these from item style + content measurement
|
|
106
|
+
* instead of pinning them to legacy constants.
|
|
107
|
+
*/
|
|
108
|
+
export const DEFAULT_ROW_BAND_PX = {
|
|
109
|
+
bandwidth: 56,
|
|
110
|
+
step: 64,
|
|
111
|
+
} as const;
|
|
112
|
+
|
|
113
|
+
export function defaultRowBand(): BandScale {
|
|
114
|
+
return BandScale.fixedRow(DEFAULT_ROW_BAND_PX);
|
|
115
|
+
}
|
package/src/calendar.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
|
|
7
|
+
import type { CalendarBlock, EntityProperty, NowlineFile, SizeDeclaration } from '@nowline/core';
|
|
8
|
+
|
|
9
|
+
import { parseCapacityValue } from './capacity.js';
|
|
10
|
+
import { propValue } from './dsl-utils.js';
|
|
11
|
+
import type { ResolvedSize } from './types.js';
|
|
12
|
+
|
|
13
|
+
export type CalendarMode = 'business' | 'full' | 'custom';
|
|
14
|
+
|
|
15
|
+
export interface CalendarConfig {
|
|
16
|
+
mode: CalendarMode;
|
|
17
|
+
daysPerWeek: number;
|
|
18
|
+
daysPerMonth: number;
|
|
19
|
+
daysPerQuarter: number;
|
|
20
|
+
daysPerYear: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const BUSINESS: CalendarConfig = {
|
|
24
|
+
mode: 'business',
|
|
25
|
+
daysPerWeek: 5,
|
|
26
|
+
daysPerMonth: 22,
|
|
27
|
+
daysPerQuarter: 65,
|
|
28
|
+
daysPerYear: 260,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const FULL: CalendarConfig = {
|
|
32
|
+
mode: 'full',
|
|
33
|
+
daysPerWeek: 7,
|
|
34
|
+
daysPerMonth: 30,
|
|
35
|
+
daysPerQuarter: 91,
|
|
36
|
+
daysPerYear: 365,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function resolveCalendar(
|
|
40
|
+
file: NowlineFile,
|
|
41
|
+
customBlock: CalendarBlock | undefined,
|
|
42
|
+
): CalendarConfig {
|
|
43
|
+
const calProp = file.roadmapDecl?.properties.find((p) => stripColon(p.key) === 'calendar');
|
|
44
|
+
const mode: CalendarMode = (calProp?.value as CalendarMode) ?? 'business';
|
|
45
|
+
if (mode === 'business') return { ...BUSINESS };
|
|
46
|
+
if (mode === 'full') return { ...FULL };
|
|
47
|
+
if (customBlock) {
|
|
48
|
+
const get = (key: string, fallback: number): number => {
|
|
49
|
+
const p = customBlock.properties.find((x) => stripColon(x.key) === key);
|
|
50
|
+
const n = p ? parseInt(p.value, 10) : NaN;
|
|
51
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
mode: 'custom',
|
|
55
|
+
daysPerWeek: get('days-per-week', BUSINESS.daysPerWeek),
|
|
56
|
+
daysPerMonth: get('days-per-month', BUSINESS.daysPerMonth),
|
|
57
|
+
daysPerQuarter: get('days-per-quarter', BUSINESS.daysPerQuarter),
|
|
58
|
+
daysPerYear: get('days-per-year', BUSINESS.daysPerYear),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { ...BUSINESS };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Decimal-aware so `size xs effort:0.5d` and `duration:1.5w` round-trip cleanly.
|
|
65
|
+
const DURATION_RE = /^(\d+(?:\.\d+)?)([dwmqy])$/;
|
|
66
|
+
|
|
67
|
+
export function literalToDays(literal: string, cal: CalendarConfig): number {
|
|
68
|
+
const m = DURATION_RE.exec(literal);
|
|
69
|
+
if (!m) return 0;
|
|
70
|
+
const n = parseFloat(m[1]);
|
|
71
|
+
switch (m[2]) {
|
|
72
|
+
case 'd':
|
|
73
|
+
return n;
|
|
74
|
+
case 'w':
|
|
75
|
+
return n * cal.daysPerWeek;
|
|
76
|
+
case 'm':
|
|
77
|
+
return n * cal.daysPerMonth;
|
|
78
|
+
case 'q':
|
|
79
|
+
return n * cal.daysPerQuarter;
|
|
80
|
+
case 'y':
|
|
81
|
+
return n * cal.daysPerYear;
|
|
82
|
+
default:
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert a `duration:` literal or a `size:NAME` reference into a calendar-
|
|
89
|
+
* resolved day count. Capacity-agnostic — used both for the duration
|
|
90
|
+
* literal lookup in `deriveItemDurationDays` and for the
|
|
91
|
+
* `remaining:` literal normalization in `sequenceItem`. Returns 0 for
|
|
92
|
+
* missing or unresolvable values; callers substitute their own minimum
|
|
93
|
+
* width when the result is zero.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveDuration(
|
|
96
|
+
value: string | undefined,
|
|
97
|
+
sizes: Map<string, ResolvedSize>,
|
|
98
|
+
cal: CalendarConfig,
|
|
99
|
+
): number {
|
|
100
|
+
if (!value) return 0;
|
|
101
|
+
if (DURATION_RE.test(value)) return literalToDays(value, cal);
|
|
102
|
+
return sizes.get(value)?.effortDays ?? 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Derive an item's calendar duration in days from its properties.
|
|
107
|
+
* Precedence (matches specs/dsl.md § "Sizing precedence"):
|
|
108
|
+
*
|
|
109
|
+
* 1. Explicit `duration:LITERAL` wins. The literal IS the calendar
|
|
110
|
+
* duration the bar paints; `size:NAME` (if also present) collapses
|
|
111
|
+
* to a pure annotation rendered as the size chip.
|
|
112
|
+
* 2. Otherwise, `size:NAME` resolves to its size declaration's
|
|
113
|
+
* `effort:` (single-engineer days) and we divide by the item's
|
|
114
|
+
* capacity (default 1) to get the team's calendar duration.
|
|
115
|
+
* 3. With neither, returns 0 — the validator already errors on items
|
|
116
|
+
* missing both `size:` and `duration:`, so this only happens in
|
|
117
|
+
* transient malformed inputs.
|
|
118
|
+
*/
|
|
119
|
+
export function deriveItemDurationDays(
|
|
120
|
+
props: EntityProperty[],
|
|
121
|
+
sizes: Map<string, ResolvedSize>,
|
|
122
|
+
cal: CalendarConfig,
|
|
123
|
+
): number {
|
|
124
|
+
const durationRaw = propValue(props, 'duration');
|
|
125
|
+
if (durationRaw && DURATION_RE.test(durationRaw)) {
|
|
126
|
+
return literalToDays(durationRaw, cal);
|
|
127
|
+
}
|
|
128
|
+
const sizeRef = propValue(props, 'size');
|
|
129
|
+
const size = sizeRef ? sizes.get(sizeRef) : undefined;
|
|
130
|
+
if (!size) return 0;
|
|
131
|
+
const capacity = parseCapacityValue(propValue(props, 'capacity')) ?? 1;
|
|
132
|
+
return size.effortDays / capacity;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Total work for the item in single-engineer days. Used to normalize a
|
|
137
|
+
* literal `remaining:` value (also single-engineer days per spec) into a
|
|
138
|
+
* 0..1 progress fraction.
|
|
139
|
+
*
|
|
140
|
+
* - sized: `size.effortDays` directly (already per-engineer).
|
|
141
|
+
* - duration-literal'd: `duration_days × capacity`. The literal sets
|
|
142
|
+
* calendar duration; multiplying by the engineer count recovers the
|
|
143
|
+
* equivalent single-engineer effort the lane is consuming.
|
|
144
|
+
*
|
|
145
|
+
* Returns 0 when neither `size:` nor `duration:` is set so callers can
|
|
146
|
+
* skip normalization safely.
|
|
147
|
+
*/
|
|
148
|
+
export function deriveTotalEffortDays(
|
|
149
|
+
props: EntityProperty[],
|
|
150
|
+
sizes: Map<string, ResolvedSize>,
|
|
151
|
+
cal: CalendarConfig,
|
|
152
|
+
): number {
|
|
153
|
+
const sizeRef = propValue(props, 'size');
|
|
154
|
+
const size = sizeRef ? sizes.get(sizeRef) : undefined;
|
|
155
|
+
if (size) return size.effortDays;
|
|
156
|
+
const durationRaw = propValue(props, 'duration');
|
|
157
|
+
if (durationRaw && DURATION_RE.test(durationRaw)) {
|
|
158
|
+
const capacity = parseCapacityValue(propValue(props, 'capacity')) ?? 1;
|
|
159
|
+
return literalToDays(durationRaw, cal) * capacity;
|
|
160
|
+
}
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Format a calendar day count back into the largest unit literal that
|
|
166
|
+
* divides cleanly under the active calendar. Used by the meta line for
|
|
167
|
+
* sized items so the displayed duration reflects the *derived* calendar
|
|
168
|
+
* duration rather than the raw effort literal — `size:m capacity:5`
|
|
169
|
+
* with `effort:1w` paints a 1d bar and the meta should read `M 1d`,
|
|
170
|
+
* not `M 1w`.
|
|
171
|
+
*
|
|
172
|
+
* - Whole-unit folds happen in descending order (`y` → `q` → `m` →
|
|
173
|
+
* `w`); the first one that yields an integer wins so `5d` →
|
|
174
|
+
* `"1w"` and `22d` (business calendar) → `"1m"`.
|
|
175
|
+
* - Anything left over collapses to a `Nd` literal with up to two
|
|
176
|
+
* decimal places, trailing zeros trimmed (`"3d"`, `"1.5d"`,
|
|
177
|
+
* `"1.67d"`). Matches the `DURATION_LITERAL` shape the parser
|
|
178
|
+
* accepts so round-tripping through the renderer reads naturally.
|
|
179
|
+
* - `0` (or anything sub-1d) returns `"0d"` rather than an empty
|
|
180
|
+
* string so callers always get a renderable token.
|
|
181
|
+
*/
|
|
182
|
+
export function formatDurationDays(days: number, cal: CalendarConfig): string {
|
|
183
|
+
const EPSILON = 1e-6;
|
|
184
|
+
const isWhole = (n: number) => n >= 1 && Math.abs(n - Math.round(n)) < EPSILON;
|
|
185
|
+
const folds: Array<[number, string]> = [
|
|
186
|
+
[cal.daysPerYear, 'y'],
|
|
187
|
+
[cal.daysPerQuarter, 'q'],
|
|
188
|
+
[cal.daysPerMonth, 'm'],
|
|
189
|
+
[cal.daysPerWeek, 'w'],
|
|
190
|
+
];
|
|
191
|
+
for (const [unitDays, suffix] of folds) {
|
|
192
|
+
const n = days / unitDays;
|
|
193
|
+
if (isWhole(n)) return `${Math.round(n)}${suffix}`;
|
|
194
|
+
}
|
|
195
|
+
if (Math.abs(days - Math.round(days)) < EPSILON) {
|
|
196
|
+
return `${Math.round(days)}d`;
|
|
197
|
+
}
|
|
198
|
+
// parseFloat strips trailing zeros: `1.50` → `1.5`, `1.67` → `1.67`.
|
|
199
|
+
return `${parseFloat(days.toFixed(2))}d`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build the layout's `Map<string, ResolvedSize>` once the calendar is
|
|
204
|
+
* known. Skips sizes whose `effort:` literal is missing or unparseable —
|
|
205
|
+
* the validator already errors on those, so layout silently drops them
|
|
206
|
+
* rather than emitting NaN-laden positions.
|
|
207
|
+
*/
|
|
208
|
+
export function resolveSizes(
|
|
209
|
+
decls: Map<string, SizeDeclaration>,
|
|
210
|
+
cal: CalendarConfig,
|
|
211
|
+
): Map<string, ResolvedSize> {
|
|
212
|
+
const out = new Map<string, ResolvedSize>();
|
|
213
|
+
for (const [name, decl] of decls) {
|
|
214
|
+
const effortProp = decl.properties.find((p) => stripColon(p.key) === 'effort');
|
|
215
|
+
const effortLiteral = effortProp?.value;
|
|
216
|
+
if (!effortLiteral) continue;
|
|
217
|
+
const effortDays = literalToDays(effortLiteral, cal);
|
|
218
|
+
if (effortDays <= 0) continue;
|
|
219
|
+
out.set(name, {
|
|
220
|
+
name,
|
|
221
|
+
title: decl.title || undefined,
|
|
222
|
+
effortDays,
|
|
223
|
+
effortLiteral,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function stripColon(key: string): string {
|
|
230
|
+
return key.endsWith(':') ? key.slice(0, -1) : key;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Date arithmetic in day units. All coordinates are relative to the roadmap
|
|
234
|
+
// start date; today is computed in days since start.
|
|
235
|
+
export function daysBetween(from: Date, to: Date): number {
|
|
236
|
+
const ONE_DAY = 86400 * 1000;
|
|
237
|
+
return Math.round((to.getTime() - from.getTime()) / ONE_DAY);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function addDays(base: Date, days: number): Date {
|
|
241
|
+
const out = new Date(base.getTime());
|
|
242
|
+
out.setUTCDate(out.getUTCDate() + days);
|
|
243
|
+
return out;
|
|
244
|
+
}
|