@nowline/layout 0.8.6 → 0.9.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/dist/axis-columns.d.ts +70 -0
- package/dist/axis-columns.d.ts.map +1 -0
- package/dist/axis-columns.js +104 -0
- package/dist/axis-columns.js.map +1 -0
- package/dist/calendar-resolver.d.ts +27 -0
- package/dist/calendar-resolver.d.ts.map +1 -0
- package/dist/calendar-resolver.js +132 -0
- package/dist/calendar-resolver.js.map +1 -0
- package/dist/calendar.d.ts +0 -19
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +4 -44
- package/dist/calendar.js.map +1 -1
- package/dist/canvas-width.d.ts +23 -0
- package/dist/canvas-width.d.ts.map +1 -0
- package/dist/canvas-width.js +26 -0
- package/dist/canvas-width.js.map +1 -0
- package/dist/capacity.d.ts +28 -0
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +64 -29
- package/dist/capacity.js.map +1 -1
- package/dist/chart-translation.d.ts +22 -0
- package/dist/chart-translation.d.ts.map +1 -0
- package/dist/chart-translation.js +83 -0
- package/dist/chart-translation.js.map +1 -0
- package/dist/container-header-geometry.d.ts +64 -0
- package/dist/container-header-geometry.d.ts.map +1 -0
- package/dist/container-header-geometry.js +89 -0
- package/dist/container-header-geometry.js.map +1 -0
- package/dist/dsl-utils.d.ts +3 -4
- package/dist/dsl-utils.d.ts.map +1 -1
- package/dist/dsl-utils.js +8 -25
- package/dist/dsl-utils.js.map +1 -1
- package/dist/edge-routing.d.ts +69 -78
- package/dist/edge-routing.d.ts.map +1 -1
- package/dist/edge-routing.js +271 -401
- package/dist/edge-routing.js.map +1 -1
- package/dist/frame-tab-geometry.d.ts +24 -14
- package/dist/frame-tab-geometry.d.ts.map +1 -1
- package/dist/frame-tab-geometry.js +69 -23
- package/dist/frame-tab-geometry.js.map +1 -1
- package/dist/header-card-geometry.d.ts +12 -0
- package/dist/header-card-geometry.d.ts.map +1 -1
- package/dist/header-card-geometry.js +14 -2
- package/dist/header-card-geometry.js.map +1 -1
- package/dist/header-logo.d.ts +28 -0
- package/dist/header-logo.d.ts.map +1 -0
- package/dist/header-logo.js +51 -0
- package/dist/header-logo.js.map +1 -0
- package/dist/i18n.d.ts +11 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +11 -1
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +20 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +1 -1
- package/dist/inline-date-pin-geometry.d.ts +19 -11
- package/dist/inline-date-pin-geometry.d.ts.map +1 -1
- package/dist/inline-date-pin-geometry.js +45 -46
- package/dist/inline-date-pin-geometry.js.map +1 -1
- package/dist/item-bar-geometry.d.ts +192 -4
- package/dist/item-bar-geometry.d.ts.map +1 -1
- package/dist/item-bar-geometry.js +258 -5
- package/dist/item-bar-geometry.js.map +1 -1
- package/dist/item-port-geometry.d.ts +13 -0
- package/dist/item-port-geometry.d.ts.map +1 -0
- package/dist/item-port-geometry.js +63 -0
- package/dist/item-port-geometry.js.map +1 -0
- package/dist/layout-context.d.ts +115 -51
- package/dist/layout-context.d.ts.map +1 -1
- package/dist/layout-context.js +96 -5
- package/dist/layout-context.js.map +1 -1
- package/dist/layout-insights.d.ts.map +1 -1
- package/dist/layout-insights.js +73 -1
- package/dist/layout-insights.js.map +1 -1
- package/dist/layout.d.ts +35 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +18 -1315
- package/dist/layout.js.map +1 -1
- package/dist/nodes/anchor-node.d.ts.map +1 -1
- package/dist/nodes/anchor-node.js +11 -4
- package/dist/nodes/anchor-node.js.map +1 -1
- package/dist/nodes/content-extent.d.ts +24 -0
- package/dist/nodes/content-extent.d.ts.map +1 -0
- package/dist/nodes/content-extent.js +100 -0
- package/dist/nodes/content-extent.js.map +1 -0
- package/dist/nodes/dependency-edges.d.ts +6 -0
- package/dist/nodes/dependency-edges.d.ts.map +1 -0
- package/dist/nodes/dependency-edges.js +148 -0
- package/dist/nodes/dependency-edges.js.map +1 -0
- package/dist/nodes/footnote-node.d.ts +24 -3
- package/dist/nodes/footnote-node.d.ts.map +1 -1
- package/dist/nodes/footnote-node.js +129 -25
- package/dist/nodes/footnote-node.js.map +1 -1
- package/dist/nodes/group-node.d.ts +10 -21
- package/dist/nodes/group-node.d.ts.map +1 -1
- package/dist/nodes/group-node.js +79 -152
- package/dist/nodes/group-node.js.map +1 -1
- package/dist/nodes/header-node.d.ts +56 -0
- package/dist/nodes/header-node.d.ts.map +1 -0
- package/dist/nodes/header-node.js +163 -0
- package/dist/nodes/header-node.js.map +1 -0
- package/dist/nodes/include-node.d.ts +22 -12
- package/dist/nodes/include-node.d.ts.map +1 -1
- package/dist/nodes/include-node.js +95 -62
- package/dist/nodes/include-node.js.map +1 -1
- package/dist/nodes/item-measure.d.ts +135 -0
- package/dist/nodes/item-measure.d.ts.map +1 -0
- package/dist/nodes/item-measure.js +514 -0
- package/dist/nodes/item-measure.js.map +1 -0
- package/dist/nodes/item-node.d.ts +162 -9
- package/dist/nodes/item-node.d.ts.map +1 -1
- package/dist/nodes/item-node.js +256 -69
- package/dist/nodes/item-node.js.map +1 -1
- package/dist/nodes/marker-band.d.ts +118 -0
- package/dist/nodes/marker-band.d.ts.map +1 -0
- package/dist/nodes/marker-band.js +303 -0
- package/dist/nodes/marker-band.js.map +1 -0
- package/dist/nodes/marker-geometry.js +1 -1
- package/dist/nodes/marker-geometry.js.map +1 -1
- package/dist/nodes/milestone-node.d.ts +112 -15
- package/dist/nodes/milestone-node.d.ts.map +1 -1
- package/dist/nodes/milestone-node.js +246 -95
- package/dist/nodes/milestone-node.js.map +1 -1
- package/dist/nodes/nowline-node.d.ts +8 -0
- package/dist/nodes/nowline-node.d.ts.map +1 -0
- package/dist/nodes/nowline-node.js +74 -0
- package/dist/nodes/nowline-node.js.map +1 -0
- package/dist/nodes/parallel-node.d.ts +7 -12
- package/dist/nodes/parallel-node.d.ts.map +1 -1
- package/dist/nodes/parallel-node.js +66 -30
- package/dist/nodes/parallel-node.js.map +1 -1
- package/dist/nodes/region-markers.d.ts +41 -0
- package/dist/nodes/region-markers.d.ts.map +1 -0
- package/dist/nodes/region-markers.js +130 -0
- package/dist/nodes/region-markers.js.map +1 -0
- package/dist/nodes/roadmap-node.d.ts +9 -72
- package/dist/nodes/roadmap-node.d.ts.map +1 -1
- package/dist/nodes/roadmap-node.js +38 -788
- package/dist/nodes/roadmap-node.js.map +1 -1
- package/dist/nodes/row-track.d.ts +33 -0
- package/dist/nodes/row-track.d.ts.map +1 -0
- package/dist/nodes/row-track.js +170 -0
- package/dist/nodes/row-track.js.map +1 -0
- package/dist/nodes/sequence-item.d.ts +25 -0
- package/dist/nodes/sequence-item.d.ts.map +1 -0
- package/dist/nodes/sequence-item.js +396 -0
- package/dist/nodes/sequence-item.js.map +1 -0
- package/dist/nodes/sequence-one.d.ts +5 -0
- package/dist/nodes/sequence-one.d.ts.map +1 -0
- package/dist/nodes/sequence-one.js +26 -0
- package/dist/nodes/sequence-one.js.map +1 -0
- package/dist/nodes/swimlane-node.d.ts +5 -17
- package/dist/nodes/swimlane-node.d.ts.map +1 -1
- package/dist/nodes/swimlane-node.js +62 -145
- package/dist/nodes/swimlane-node.js.map +1 -1
- package/dist/nodes/wave-node.d.ts +52 -0
- package/dist/nodes/wave-node.d.ts.map +1 -0
- package/dist/nodes/wave-node.js +483 -0
- package/dist/nodes/wave-node.js.map +1 -0
- package/dist/non-working-display.d.ts +14 -0
- package/dist/non-working-display.d.ts.map +1 -0
- package/dist/non-working-display.js +28 -0
- package/dist/non-working-display.js.map +1 -0
- package/dist/phases/date-window.d.ts +48 -0
- package/dist/phases/date-window.d.ts.map +1 -0
- package/dist/phases/date-window.js +130 -0
- package/dist/phases/date-window.js.map +1 -0
- package/dist/phases/finalize.d.ts +198 -0
- package/dist/phases/finalize.d.ts.map +1 -0
- package/dist/phases/finalize.js +1058 -0
- package/dist/phases/finalize.js.map +1 -0
- package/dist/phases/measure.d.ts +77 -0
- package/dist/phases/measure.d.ts.map +1 -0
- package/dist/phases/measure.js +222 -0
- package/dist/phases/measure.js.map +1 -0
- package/dist/phases/place.d.ts +28 -0
- package/dist/phases/place.d.ts.map +1 -0
- package/dist/phases/place.js +225 -0
- package/dist/phases/place.js.map +1 -0
- package/dist/renderable.d.ts.map +1 -1
- package/dist/renderable.js +23 -3
- package/dist/renderable.js.map +1 -1
- package/dist/resolve-today.d.ts +11 -1
- package/dist/resolve-today.d.ts.map +1 -1
- package/dist/resolve-today.js +26 -12
- package/dist/resolve-today.js.map +1 -1
- package/dist/row-packer.d.ts +42 -16
- package/dist/row-packer.d.ts.map +1 -1
- package/dist/row-packer.js +51 -78
- package/dist/row-packer.js.map +1 -1
- package/dist/schedule.d.ts +58 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +280 -106
- package/dist/schedule.js.map +1 -1
- package/dist/start-rules.d.ts +27 -0
- package/dist/start-rules.d.ts.map +1 -0
- package/dist/start-rules.js +91 -0
- package/dist/start-rules.js.map +1 -0
- package/dist/start-terms.d.ts +23 -0
- package/dist/start-terms.d.ts.map +1 -0
- package/dist/start-terms.js +27 -0
- package/dist/start-terms.js.map +1 -0
- package/dist/status-dot.d.ts +46 -0
- package/dist/status-dot.d.ts.map +1 -0
- package/dist/status-dot.js +78 -0
- package/dist/status-dot.js.map +1 -0
- package/dist/style-resolution.d.ts.map +1 -1
- package/dist/style-resolution.js +23 -43
- package/dist/style-resolution.js.map +1 -1
- package/dist/text-measure.d.ts +28 -0
- package/dist/text-measure.d.ts.map +1 -0
- package/dist/text-measure.js +70 -0
- package/dist/text-measure.js.map +1 -0
- package/dist/themes/contrast.d.ts +4 -0
- package/dist/themes/contrast.d.ts.map +1 -0
- package/dist/themes/contrast.js +32 -0
- package/dist/themes/contrast.js.map +1 -0
- package/dist/themes/dark.d.ts.map +1 -1
- package/dist/themes/dark.js +28 -1
- package/dist/themes/dark.js.map +1 -1
- package/dist/themes/grayscale.d.ts.map +1 -1
- package/dist/themes/grayscale.js +28 -1
- package/dist/themes/grayscale.js.map +1 -1
- package/dist/themes/index.d.ts +4 -11
- package/dist/themes/index.d.ts.map +1 -1
- package/dist/themes/index.js +6 -4
- package/dist/themes/index.js.map +1 -1
- package/dist/themes/light.d.ts.map +1 -1
- package/dist/themes/light.js +28 -1
- package/dist/themes/light.js.map +1 -1
- package/dist/themes/shape.d.ts +31 -0
- package/dist/themes/shape.d.ts.map +1 -1
- package/dist/themes/shared.d.ts +44 -50
- package/dist/themes/shared.d.ts.map +1 -1
- package/dist/themes/shared.js +79 -83
- package/dist/themes/shared.js.map +1 -1
- package/dist/time-scale.d.ts +42 -6
- package/dist/time-scale.d.ts.map +1 -1
- package/dist/time-scale.js +109 -17
- package/dist/time-scale.js.map +1 -1
- package/dist/types.d.ts +528 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/view-preset.d.ts +61 -6
- package/dist/view-preset.d.ts.map +1 -1
- package/dist/view-preset.js +439 -24
- package/dist/view-preset.js.map +1 -1
- package/dist/wave-barrier.d.ts +107 -0
- package/dist/wave-barrier.d.ts.map +1 -0
- package/dist/wave-barrier.js +174 -0
- package/dist/wave-barrier.js.map +1 -0
- package/dist/wave-hatch.d.ts +4 -0
- package/dist/wave-hatch.d.ts.map +1 -0
- package/dist/wave-hatch.js +21 -0
- package/dist/wave-hatch.js.map +1 -0
- package/dist/wave-layout.d.ts +99 -0
- package/dist/wave-layout.d.ts.map +1 -0
- package/dist/wave-layout.js +130 -0
- package/dist/wave-layout.js.map +1 -0
- package/dist/working-calendar.d.ts +128 -4
- package/dist/working-calendar.d.ts.map +1 -1
- package/dist/working-calendar.js +725 -51
- package/dist/working-calendar.js.map +1 -1
- package/dist/working-span.d.ts +11 -0
- package/dist/working-span.d.ts.map +1 -0
- package/dist/working-span.js +18 -0
- package/dist/working-span.js.map +1 -0
- package/package.json +4 -4
- package/src/axis-columns.ts +140 -0
- package/src/calendar-resolver.ts +161 -0
- package/src/calendar.ts +4 -45
- package/src/canvas-width.ts +36 -0
- package/src/capacity.ts +71 -30
- package/src/chart-translation.ts +88 -0
- package/src/container-header-geometry.ts +116 -0
- package/src/dsl-utils.ts +8 -26
- package/src/edge-routing.ts +347 -491
- package/src/frame-tab-geometry.ts +78 -25
- package/src/header-card-geometry.ts +17 -2
- package/src/header-logo.ts +67 -0
- package/src/i18n.ts +22 -1
- package/src/index.ts +97 -12
- package/src/inline-date-pin-geometry.ts +55 -58
- package/src/item-bar-geometry.ts +341 -5
- package/src/item-port-geometry.ts +69 -0
- package/src/layout-context.ts +210 -72
- package/src/layout-insights.ts +109 -2
- package/src/layout.ts +57 -1530
- package/src/nodes/anchor-node.ts +11 -4
- package/src/nodes/content-extent.ts +107 -0
- package/src/nodes/dependency-edges.ts +171 -0
- package/src/nodes/footnote-node.ts +171 -31
- package/src/nodes/group-node.ts +92 -214
- package/src/nodes/header-node.ts +255 -0
- package/src/nodes/include-node.ts +124 -110
- package/src/nodes/item-measure.ts +723 -0
- package/src/nodes/item-node.ts +358 -80
- package/src/nodes/marker-band.ts +406 -0
- package/src/nodes/marker-geometry.ts +1 -1
- package/src/nodes/milestone-node.ts +321 -136
- package/src/nodes/nowline-node.ts +87 -0
- package/src/nodes/parallel-node.ts +69 -38
- package/src/nodes/region-markers.ts +194 -0
- package/src/nodes/roadmap-node.ts +46 -950
- package/src/nodes/row-track.ts +217 -0
- package/src/nodes/sequence-item.ts +433 -0
- package/src/nodes/sequence-one.ts +40 -0
- package/src/nodes/swimlane-node.ts +66 -205
- package/src/nodes/wave-node.ts +609 -0
- package/src/non-working-display.ts +32 -0
- package/src/phases/date-window.ts +191 -0
- package/src/phases/finalize.ts +1303 -0
- package/src/phases/measure.ts +329 -0
- package/src/phases/place.ts +318 -0
- package/src/renderable.ts +23 -3
- package/src/resolve-today.ts +28 -25
- package/src/row-packer.ts +85 -84
- package/src/schedule.ts +362 -114
- package/src/start-rules.ts +113 -0
- package/src/start-terms.ts +38 -0
- package/src/status-dot.ts +81 -0
- package/src/style-resolution.ts +27 -57
- package/src/text-measure.ts +71 -0
- package/src/themes/contrast.ts +34 -0
- package/src/themes/dark.ts +28 -1
- package/src/themes/grayscale.ts +28 -1
- package/src/themes/index.ts +10 -7
- package/src/themes/light.ts +28 -1
- package/src/themes/shape.ts +38 -0
- package/src/themes/shared.ts +85 -93
- package/src/time-scale.ts +117 -19
- package/src/types.ts +529 -20
- package/src/view-preset.ts +520 -27
- package/src/wave-barrier.ts +242 -0
- package/src/wave-hatch.ts +25 -0
- package/src/wave-layout.ts +203 -0
- package/src/working-calendar.ts +875 -57
- package/src/working-span.ts +19 -0
package/src/nodes/group-node.ts
CHANGED
|
@@ -1,70 +1,30 @@
|
|
|
1
1
|
// GroupNode — Renderable for a `group { ... }` block. Sequences
|
|
2
|
-
// children inside the group's
|
|
3
|
-
//
|
|
4
|
-
// with a sibling's `rightEdge`, caption `spillX`, or a
|
|
5
|
-
// corridor bumps to a new row inside the group.
|
|
6
|
-
// `box.height` grows with the row stack so parent containers stack
|
|
7
|
-
// against the actual painted extent.
|
|
2
|
+
// children inside the group's body through `sequenceRowTrack`
|
|
3
|
+
// (row-track.ts), the loop `SwimlaneNode` uses, so an item whose desired
|
|
4
|
+
// start collides with a sibling's `rightEdge`, caption `spillX`, or a
|
|
5
|
+
// slack-arrow corridor bumps to a new row inside the group.
|
|
8
6
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { propValues } from '../dsl-utils.js';
|
|
7
|
+
// The group is drawn above and around its content, never beside it
|
|
8
|
+
// (specs/true-width-layout.md §3.3): a header band (title, inline-date
|
|
9
|
+
// glyphs, a bracket spanning exactly its dates) over a body padded above
|
|
10
|
+
// and below its rows. Its box spans exactly its children's earliest start
|
|
11
|
+
// and latest end; see `container-header-geometry.ts`.
|
|
12
|
+
|
|
13
|
+
import type { GroupBlock } from '@nowline/core';
|
|
14
|
+
import { containerChrome } from '../container-header-geometry.js';
|
|
15
|
+
import { propValue, propValues } from '../dsl-utils.js';
|
|
18
16
|
import { computeContainerInlineDatePins, pickInlineDate } from '../inline-date-pin-geometry.js';
|
|
19
|
-
import type { LayoutContext, TrackCursor } from '../layout-context.js';
|
|
20
|
-
import {
|
|
17
|
+
import type { LayoutContext, LayoutHelpers, TrackCursor } from '../layout-context.js';
|
|
18
|
+
import { resolvePinnedOrSequentialStart } from '../start-rules.js';
|
|
21
19
|
import { resolveStyle } from '../style-resolution.js';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
GROUP_TITLE_TAB_GUTTER_PX,
|
|
26
|
-
GROUP_TITLE_TAB_HEIGHT_PX,
|
|
27
|
-
ITEM_INSET_PX,
|
|
28
|
-
MIN_ITEM_WIDTH,
|
|
29
|
-
TRACK_BLOCK_TAIL_GUTTER_PX,
|
|
30
|
-
} from '../themes/shared.js';
|
|
31
|
-
import type {
|
|
32
|
-
BoundingBox,
|
|
33
|
-
PositionedGroup,
|
|
34
|
-
PositionedItem,
|
|
35
|
-
PositionedTrackChild,
|
|
36
|
-
} from '../types.js';
|
|
37
|
-
|
|
38
|
-
export interface GroupNodeDeps {
|
|
39
|
-
sequenceItem: (
|
|
40
|
-
child: ItemDeclaration,
|
|
41
|
-
cursor: TrackCursor,
|
|
42
|
-
ctx: LayoutContext,
|
|
43
|
-
ownerOverride?: string,
|
|
44
|
-
) => PositionedItem;
|
|
45
|
-
sequenceOne: (
|
|
46
|
-
child: ItemDeclaration | GroupBlock | ParallelBlock,
|
|
47
|
-
cursor: TrackCursor,
|
|
48
|
-
ctx: LayoutContext,
|
|
49
|
-
) => PositionedTrackChild;
|
|
50
|
-
resolveChildStart: (
|
|
51
|
-
props: EntityProperty[],
|
|
52
|
-
seqDefault: number,
|
|
53
|
-
laneLeftX: number,
|
|
54
|
-
ctx: LayoutContext,
|
|
55
|
-
) => number;
|
|
56
|
-
newCursor: (x: number, y: number) => TrackCursor;
|
|
57
|
-
estimateTextWidth: (text: string, fontSize: number) => number;
|
|
58
|
-
/** Predict the extra vertical height an item's wrapped label-chip
|
|
59
|
-
* rows will add to its bar; used to size the row-packer's row
|
|
60
|
-
* pitch ahead of the call to `sequenceItem`. */
|
|
61
|
-
predictItemChipExtraHeight: (item: ItemDeclaration, ctx: LayoutContext) => number;
|
|
62
|
-
}
|
|
20
|
+
import type { BoundingBox, PositionedGroup } from '../types.js';
|
|
21
|
+
import { waveFloorX } from '../wave-layout.js';
|
|
22
|
+
import { sequenceRowTrack } from './row-track.js';
|
|
63
23
|
|
|
64
24
|
export class GroupNode {
|
|
65
25
|
constructor(
|
|
66
26
|
public readonly node: GroupBlock,
|
|
67
|
-
private readonly
|
|
27
|
+
private readonly helpers: LayoutHelpers,
|
|
68
28
|
) {}
|
|
69
29
|
|
|
70
30
|
get id(): string {
|
|
@@ -72,18 +32,41 @@ export class GroupNode {
|
|
|
72
32
|
}
|
|
73
33
|
|
|
74
34
|
/**
|
|
75
|
-
* Sequence children inside the group's
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* `PositionedGroup` whose
|
|
79
|
-
*
|
|
35
|
+
* Sequence children inside the group's body, advance the parent
|
|
36
|
+
* `cursor` to the group's end in time (its children's duration ends,
|
|
37
|
+
* never a caption spill or a header title), and return a
|
|
38
|
+
* `PositionedGroup` whose box covers the header, the padded rows and
|
|
39
|
+
* the group's dates.
|
|
80
40
|
*/
|
|
81
41
|
place(cursor: TrackCursor, ctx: LayoutContext): PositionedGroup {
|
|
82
42
|
const { node } = this;
|
|
83
|
-
const {
|
|
43
|
+
const { helpers } = this;
|
|
84
44
|
const style = resolveStyle('group', node.properties, ctx.styleCtx);
|
|
85
|
-
|
|
86
|
-
|
|
45
|
+
// The group starts after every `after:` term, then its own wave and
|
|
46
|
+
// its lead wave floor it, so its box never opens in a column before
|
|
47
|
+
// its first piece of work (specs/waves.md §5.1). A lane child's
|
|
48
|
+
// cursor already sits there (`resolveChildStart`), so this is the
|
|
49
|
+
// identity for it; a parallel's group track starts at the parallel's
|
|
50
|
+
// start, so this is where its `after:` applies
|
|
51
|
+
// (specs/scheduler-unification.md §8 decision 5).
|
|
52
|
+
const startX = waveFloorX(
|
|
53
|
+
node,
|
|
54
|
+
resolvePinnedOrSequentialStart(node.properties, cursor.x, ctx),
|
|
55
|
+
ctx,
|
|
56
|
+
);
|
|
57
|
+
// In a roadmap with waves, a group with no title, no `style:` and
|
|
58
|
+
// no `labels:` that carries `wave:` only assigns membership: it
|
|
59
|
+
// draws nothing, which would otherwise sit exactly on the wave
|
|
60
|
+
// boundary its box opens at, and reserves nothing
|
|
61
|
+
// (specs/waves.md §9.7). An id alone does not make it titled
|
|
62
|
+
// (Example 4's `api-track`).
|
|
63
|
+
const waveOnly =
|
|
64
|
+
ctx.waves !== undefined &&
|
|
65
|
+
node.title === undefined &&
|
|
66
|
+
propValue(node.properties, 'wave') !== undefined &&
|
|
67
|
+
propValue(node.properties, 'style') === undefined &&
|
|
68
|
+
propValues(node.properties, 'labels').length === 0;
|
|
69
|
+
const title = waveOnly ? undefined : (node.title ?? node.name);
|
|
87
70
|
|
|
88
71
|
// Group children chain in time inside the group, so they
|
|
89
72
|
// form one sub-flow under the parent's flow path. See
|
|
@@ -91,170 +74,65 @@ export class GroupNode {
|
|
|
91
74
|
const previousFlowKey = ctx.currentFlowKey;
|
|
92
75
|
ctx.nextGroupId += 1;
|
|
93
76
|
ctx.currentFlowKey = `${previousFlowKey}/group:${node.name ?? `group-${ctx.nextGroupId}`}`;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// reservation, two bracket-titled groups stacked in a parallel
|
|
102
|
-
// collide: the previous sibling's bracket-foot ends at its
|
|
103
|
-
// box.bottom, and the next sibling's label visual top sits just
|
|
104
|
-
// above box.y — they touch in the gap. Shift `box.y` down by
|
|
105
|
-
// the overhang amount so the glyph lands in space we own.
|
|
106
|
-
const bracketLabelOverhang = !hasChiclet && title ? GROUP_BRACKET_LABEL_OVERHANG_PX : 0;
|
|
107
|
-
const startY = cursor.y + bracketLabelOverhang;
|
|
108
|
-
|
|
109
|
-
const step = ctx.bandScale.step();
|
|
110
|
-
const groupContentLeftX = startX;
|
|
111
|
-
const packer = new RowPacker({
|
|
112
|
-
laneLeftX: groupContentLeftX,
|
|
113
|
-
originY: startY + topPad,
|
|
114
|
-
minRowHeight: step,
|
|
115
|
-
slackCorridors: ctx.slackCorridors,
|
|
77
|
+
const afterDate = pickInlineDate(propValues(node.properties, 'after'));
|
|
78
|
+
const beforeDate = pickInlineDate(propValues(node.properties, 'before'));
|
|
79
|
+
const chrome = containerChrome({
|
|
80
|
+
drawn: !waveOnly,
|
|
81
|
+
hasTitle: Boolean(title),
|
|
82
|
+
hasPins: Boolean(afterDate || beforeDate),
|
|
83
|
+
hasBracket: style.bracket !== 'none',
|
|
116
84
|
});
|
|
117
|
-
let timeCursorX = groupContentLeftX;
|
|
118
|
-
|
|
119
|
-
const children: PositionedTrackChild[] = [];
|
|
120
|
-
for (const child of node.content) {
|
|
121
|
-
if (child.$type === 'DescriptionDirective') continue;
|
|
122
|
-
|
|
123
|
-
if (!isItemDeclaration(child)) {
|
|
124
|
-
const blockProps = (child as ParallelBlock | GroupBlock).properties ?? [];
|
|
125
|
-
const blockStart = deps.resolveChildStart(
|
|
126
|
-
blockProps,
|
|
127
|
-
timeCursorX,
|
|
128
|
-
groupContentLeftX,
|
|
129
|
-
ctx,
|
|
130
|
-
);
|
|
131
|
-
const { rowIndex, y: blockY } = packer.placeBlock();
|
|
132
|
-
const innerCursor = deps.newCursor(blockStart, blockY);
|
|
133
|
-
const positioned = deps.sequenceOne(
|
|
134
|
-
child as ItemDeclaration | GroupBlock | ParallelBlock,
|
|
135
|
-
innerCursor,
|
|
136
|
-
ctx,
|
|
137
|
-
);
|
|
138
|
-
children.push(positioned);
|
|
139
|
-
const blockEnd = positioned.box.x + positioned.box.width;
|
|
140
|
-
const blockHeight = Math.max(step, innerCursor.height);
|
|
141
|
-
packer.commitBlock({
|
|
142
|
-
rowIndex,
|
|
143
|
-
placed: positioned,
|
|
144
|
-
blockHeight,
|
|
145
|
-
blockEnd,
|
|
146
|
-
});
|
|
147
|
-
timeCursorX = Math.max(timeCursorX, blockEnd);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const props = (child as ItemDeclaration).properties;
|
|
152
|
-
const desiredStart = deps.resolveChildStart(props, timeCursorX, groupContentLeftX, ctx);
|
|
153
|
-
// Predict logical extent so the row-packer can bump on
|
|
154
|
-
// collision before we hand off to `sequenceItem`. Mirrors
|
|
155
|
-
// SwimlaneNode's pre-flight width math.
|
|
156
|
-
const durationDays = deriveItemDurationDays(props, ctx.sizes, ctx.cal);
|
|
157
|
-
const naturalWidth = Math.max(MIN_ITEM_WIDTH, durationDays * ctx.timeline.pixelsPerDay);
|
|
158
|
-
const desiredEnd = desiredStart + naturalWidth;
|
|
159
|
-
const childId = (child as ItemDeclaration).name ?? '';
|
|
160
|
-
|
|
161
|
-
const chipExtra = deps.predictItemChipExtraHeight(child as ItemDeclaration, ctx);
|
|
162
|
-
const predictedHeight = step + chipExtra;
|
|
163
|
-
const { rowIndex, y: rowY } = packer.placeItem({
|
|
164
|
-
childId,
|
|
165
|
-
desiredStart,
|
|
166
|
-
desiredEnd,
|
|
167
|
-
// Row pitch = `step()` + extra chip-row height. Keeps
|
|
168
|
-
// the inter-row visible gap (= step - bandwidth) intact
|
|
169
|
-
// when an item's labels wrap and grow the bar.
|
|
170
|
-
predictedHeight,
|
|
171
|
-
});
|
|
172
85
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
spillReservation = farRight + 6;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
packer.commitItem({
|
|
196
|
-
rowIndex,
|
|
197
|
-
placed: positioned,
|
|
198
|
-
logicalEnd: itemLogicalEnd,
|
|
199
|
-
spillReservation,
|
|
200
|
-
rowHeight: predictedHeight,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const innerHeight = Math.max(step, packer.usedHeight());
|
|
205
|
-
// The group's painted box hugs everything its children put on
|
|
206
|
-
// screen, including caption text that spills past a bar's right
|
|
207
|
-
// edge. `usedRightX` is the rightmost extent any row reached —
|
|
208
|
-
// either a bar's logical right or a caption's reserved spill —
|
|
209
|
-
// so the orange tint visually "owns" the spilled title/meta.
|
|
210
|
-
//
|
|
211
|
-
// The cursor channel (`cursor.x` / `cursor.maxX`) stays on the
|
|
212
|
-
// COMPACT `timeCursorX` (bar logical ends only). Bubbling the
|
|
213
|
-
// wide `visualRightX` upward would propagate through
|
|
214
|
-
// `ParallelNode.maxRight → parallel.box.width → swimlane
|
|
215
|
-
// blockEnd → timeCursorX`, pushing every subsequent sibling
|
|
216
|
-
// right by the spill width — including siblings on entirely
|
|
217
|
-
// different rows of the parent swimlane. The painted box and
|
|
218
|
-
// the logical cursor advance are intentionally decoupled here.
|
|
219
|
-
const visualRightX = Math.max(timeCursorX, packer.usedRightX());
|
|
86
|
+
const track = sequenceRowTrack(
|
|
87
|
+
node,
|
|
88
|
+
{ leftX: startX, firstRowY: cursor.y + chrome.header + chrome.pad },
|
|
89
|
+
ctx,
|
|
90
|
+
helpers,
|
|
91
|
+
);
|
|
92
|
+
const { children, timeCursorX, timeStartX } = track;
|
|
93
|
+
|
|
94
|
+
// Every row's height carries the inter-row gap below it; the body
|
|
95
|
+
// ends `pad` below the last child's painted bottom, not below that
|
|
96
|
+
// gap, and the cursor adds the gap back after the body so a sibling
|
|
97
|
+
// stacked below keeps the same rhythm as two items.
|
|
98
|
+
const step = ctx.bandScale.step();
|
|
99
|
+
const interRowGap = step - ctx.bandScale.bandwidth();
|
|
100
|
+
const contentHeight = Math.max(step, track.packer.usedHeight()) - interRowGap;
|
|
101
|
+
// The bracket and body span exactly the children's dates
|
|
102
|
+
// (specs/true-width-layout.md §3.3): what the group paints past them
|
|
103
|
+
// (a spilled caption, its title) is footprint the row packer keeps,
|
|
104
|
+
// not width.
|
|
220
105
|
const box: BoundingBox = {
|
|
221
|
-
x:
|
|
222
|
-
y:
|
|
223
|
-
width:
|
|
224
|
-
height:
|
|
106
|
+
x: timeStartX,
|
|
107
|
+
y: cursor.y,
|
|
108
|
+
width: timeCursorX - timeStartX,
|
|
109
|
+
height: chrome.header + chrome.pad + contentHeight + chrome.pad,
|
|
225
110
|
};
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// (`step - bandwidth`) so a sibling stacked below in a
|
|
232
|
-
// `parallel` (or any track-pack consumer) doesn't butt
|
|
233
|
-
// right up against the group's bottom edge. Items naturally
|
|
234
|
-
// include that gap because they report `cursor.height = step`
|
|
235
|
-
// (bandwidth + gap); groups need to add it explicitly since
|
|
236
|
-
// their painted height is gap-less.
|
|
237
|
-
const interRowGap = ctx.bandScale.step() - ctx.bandScale.bandwidth();
|
|
238
|
-
cursor.height = Math.max(cursor.height, bracketLabelOverhang + box.height + interRowGap);
|
|
111
|
+
// A parallel joins on its tracks' ends, as engine C does
|
|
112
|
+
// (specs/scheduler-unification.md §8 decision 8).
|
|
113
|
+
cursor.startX = box.x;
|
|
114
|
+
cursor.x = timeCursorX;
|
|
115
|
+
cursor.height = Math.max(cursor.height, box.height + interRowGap);
|
|
239
116
|
const id = node.name;
|
|
117
|
+
// `after:` the group is a time path: its last child's duration end.
|
|
240
118
|
if (id) {
|
|
241
119
|
ctx.entityLeftEdges.set(id, box.x);
|
|
242
|
-
ctx.entityRightEdges.set(id,
|
|
120
|
+
ctx.entityRightEdges.set(id, timeCursorX);
|
|
243
121
|
}
|
|
244
122
|
ctx.currentFlowKey = previousFlowKey;
|
|
245
|
-
const inlineDatePins =
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
beforeDate: pickInlineDate(propValues(node.properties, 'before')),
|
|
249
|
-
});
|
|
123
|
+
const inlineDatePins = waveOnly
|
|
124
|
+
? []
|
|
125
|
+
: computeContainerInlineDatePins({ box, afterDate, beforeDate, title });
|
|
250
126
|
return {
|
|
251
127
|
kind: 'group',
|
|
252
128
|
id,
|
|
253
129
|
title,
|
|
254
130
|
box,
|
|
131
|
+
headerHeight: chrome.header,
|
|
255
132
|
children,
|
|
256
133
|
style,
|
|
257
134
|
inlineDatePins: inlineDatePins.length > 0 ? inlineDatePins : undefined,
|
|
135
|
+
...(waveOnly ? { waveOnly: true as const } : {}),
|
|
258
136
|
};
|
|
259
137
|
}
|
|
260
138
|
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// HeaderNode — the roadmap header (title, author, company logo) in both
|
|
2
|
+
// positions, `beside` (a card left of the chart) and `above` (a band over
|
|
3
|
+
// it). `measure` sizes it before anything else is laid out, because the
|
|
4
|
+
// header decides where the chart starts: its left edge in beside mode,
|
|
5
|
+
// its top in above mode, and how low the header rows must end so the
|
|
6
|
+
// beside card fits (specs/layout-phases.md §3.7).
|
|
7
|
+
//
|
|
8
|
+
// HeaderNode does not implement `Renderable`: that signature is built
|
|
9
|
+
// around a time scale and a row band, which a header has no use for.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
HEADER_AUTHOR_FONT_SIZE_PX,
|
|
13
|
+
HEADER_AUTHOR_LINE_HEIGHT_PX,
|
|
14
|
+
HEADER_CARD_PADDING_BOTTOM,
|
|
15
|
+
HEADER_CARD_PADDING_TOP,
|
|
16
|
+
HEADER_CARD_PADDING_X,
|
|
17
|
+
HEADER_LOGO_GAP_PX,
|
|
18
|
+
HEADER_LOGO_PAD_Y_PX,
|
|
19
|
+
HEADER_TITLE_FONT_SIZE_PX,
|
|
20
|
+
HEADER_TITLE_LINE_HEIGHT_PX,
|
|
21
|
+
HEADER_TITLE_TO_AUTHOR_GAP_PX,
|
|
22
|
+
} from '../header-card-geometry.js';
|
|
23
|
+
import type { LogoDimensions, ScaledLogo } from '../header-logo.js';
|
|
24
|
+
import { estimateTextWidth, wrapText } from '../text-measure.js';
|
|
25
|
+
import {
|
|
26
|
+
HEADER_ABOVE_HEIGHT_PX,
|
|
27
|
+
HEADER_BESIDE_MAX_WIDTH_PX,
|
|
28
|
+
HEADER_BESIDE_MIN_WIDTH_PX,
|
|
29
|
+
} from '../themes/shared.js';
|
|
30
|
+
import type { BoundingBox, HeaderPosition, PositionedHeader, ResolvedStyle } from '../types.js';
|
|
31
|
+
|
|
32
|
+
// Left margin between the canvas's left edge and the visible card. The
|
|
33
|
+
// matching right-side breathing room is owned by `GUTTER_PX` (the canonical
|
|
34
|
+
// content gutter, applied between `chartLeftX` and `originX`), so the gap
|
|
35
|
+
// from the card's right edge to the timeline strip is the same as the gap
|
|
36
|
+
// between two adjacent items.
|
|
37
|
+
const HEADER_CARD_OUTER_PAD = 6;
|
|
38
|
+
|
|
39
|
+
/** Clearance (px) the beside card keeps from the canvas top. */
|
|
40
|
+
const HEADER_CARD_TOP_INSET = 4;
|
|
41
|
+
|
|
42
|
+
/** Gap (px) between the header (or the canvas top) and the chart. */
|
|
43
|
+
const CHART_TOP_GAP_PX = 8;
|
|
44
|
+
|
|
45
|
+
export interface MeasuredHeader {
|
|
46
|
+
position: HeaderPosition;
|
|
47
|
+
titleLines: string[];
|
|
48
|
+
authorLines: string[];
|
|
49
|
+
cardWidth: number;
|
|
50
|
+
/** Card height, grown when the logo is taller than the text. */
|
|
51
|
+
cardHeight: number;
|
|
52
|
+
/** Card height the title / author text alone needs. */
|
|
53
|
+
textCardHeight: number;
|
|
54
|
+
/** Width the logo takes left of the text, gap included; 0 without a logo. */
|
|
55
|
+
logoSlot: number;
|
|
56
|
+
logo?: ScaledLogo;
|
|
57
|
+
/** Beside: the card's box width, which is also `chartLeftX`. Above: 0. */
|
|
58
|
+
chartLeftX: number;
|
|
59
|
+
/** Above: the band height, bumped for an `xl` logo. Beside never reads it. */
|
|
60
|
+
aboveHeight: number;
|
|
61
|
+
/** Where the chart may start: below the canvas top, or below the above band. */
|
|
62
|
+
chartTopY: number;
|
|
63
|
+
/** Beside: the lowest the header rows may end so the card fits (card + 4 px inset). Above: 0. */
|
|
64
|
+
minRowsBottomY: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The positioned header without its attribution box, which finalize adds last. */
|
|
68
|
+
export type PositionedHeaderParts = Omit<PositionedHeader, 'attributionBox'>;
|
|
69
|
+
|
|
70
|
+
/** Where the chart ended up, as far as the header needs to know. */
|
|
71
|
+
export interface HeaderFrame {
|
|
72
|
+
/** Bottom of the header rows; the beside card's bottom hugs it. */
|
|
73
|
+
rowsBottomY: number;
|
|
74
|
+
/** Bottom of the chart; the beside header column runs down to it. */
|
|
75
|
+
chartBottomY: number;
|
|
76
|
+
/** The final canvas right edge; the above band spans to it. */
|
|
77
|
+
canvasRightX: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface HeaderNodeInput {
|
|
81
|
+
/** The roadmap's title (or name); an empty string when it has neither. */
|
|
82
|
+
title: string;
|
|
83
|
+
author?: string;
|
|
84
|
+
/** The resolved `default roadmap` style; `headerPosition` picks the mode. */
|
|
85
|
+
style: ResolvedStyle;
|
|
86
|
+
/** The drawn logo size, when the file declares a logo and the caller loaded one. */
|
|
87
|
+
logo?: ScaledLogo;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class HeaderNode {
|
|
91
|
+
constructor(private readonly input: HeaderNodeInput) {}
|
|
92
|
+
|
|
93
|
+
measure(): MeasuredHeader {
|
|
94
|
+
const { title, author, style, logo } = this.input;
|
|
95
|
+
const isBeside = style.headerPosition === 'beside';
|
|
96
|
+
// Pre-size the beside-mode header card. Width = max line width +
|
|
97
|
+
// padding, clamped to MIN..MAX with word-wrap once the title
|
|
98
|
+
// exceeds MAX. Above mode keeps the fixed-strip geometry (full
|
|
99
|
+
// canvas width, fixed height) but still reads the wrapped lines.
|
|
100
|
+
const sized = sizeBesideHeader(title, author, logo);
|
|
101
|
+
// Above-mode strip: fixed height, bumped only when the logo plus
|
|
102
|
+
// its clearance is taller (`logo-size:xl`).
|
|
103
|
+
const aboveHeight = logo
|
|
104
|
+
? Math.max(HEADER_ABOVE_HEIGHT_PX, logo.height + 2 * HEADER_LOGO_PAD_Y_PX)
|
|
105
|
+
: HEADER_ABOVE_HEIGHT_PX;
|
|
106
|
+
return {
|
|
107
|
+
position: style.headerPosition,
|
|
108
|
+
titleLines: sized.titleLines,
|
|
109
|
+
authorLines: sized.authorLines,
|
|
110
|
+
cardWidth: sized.cardWidth,
|
|
111
|
+
cardHeight: sized.cardHeight,
|
|
112
|
+
textCardHeight: sized.textCardHeight,
|
|
113
|
+
logoSlot: sized.logoSlot,
|
|
114
|
+
...(logo ? { logo } : {}),
|
|
115
|
+
chartLeftX: isBeside ? sized.boxWidth : 0,
|
|
116
|
+
aboveHeight,
|
|
117
|
+
chartTopY: isBeside ? CHART_TOP_GAP_PX : aboveHeight + CHART_TOP_GAP_PX,
|
|
118
|
+
// In beside mode the card's BOTTOM aligns with the bottom of the
|
|
119
|
+
// header rows so it visually anchors to the chart's top. The
|
|
120
|
+
// rows must end low enough for the card plus a 4 px top inset;
|
|
121
|
+
// the measure phase pushes the timeline down when they would not.
|
|
122
|
+
minRowsBottomY: isBeside ? sized.cardHeight + HEADER_CARD_TOP_INSET : 0,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The header box, the card, the logo box and the text inset, once the
|
|
128
|
+
* chart's extent is known. Beside: a column `chartLeftX` wide down to
|
|
129
|
+
* the chart bottom, with the card's bottom on `rowsBottomY`. Above: a
|
|
130
|
+
* band `aboveHeight` tall across the canvas, the card filling it.
|
|
131
|
+
*/
|
|
132
|
+
place(measured: MeasuredHeader, frame: HeaderFrame): PositionedHeaderParts {
|
|
133
|
+
const { title, author, style } = this.input;
|
|
134
|
+
const isBeside = measured.position === 'beside';
|
|
135
|
+
const box: BoundingBox = isBeside
|
|
136
|
+
? { x: 0, y: 0, width: measured.chartLeftX, height: frame.chartBottomY }
|
|
137
|
+
: { x: 0, y: 0, width: frame.canvasRightX, height: measured.aboveHeight };
|
|
138
|
+
// The visible card inside the box. Beside, its BOTTOM hugs the
|
|
139
|
+
// bottom of the header rows so the title block anchors to the
|
|
140
|
+
// chart's top edge whichever header rows are present; the measure
|
|
141
|
+
// phase already pushed the rows down far enough for the card to
|
|
142
|
+
// keep HEADER_CARD_TOP_INSET from the canvas top.
|
|
143
|
+
const cardBox: BoundingBox = isBeside
|
|
144
|
+
? {
|
|
145
|
+
x: HEADER_CARD_OUTER_PAD,
|
|
146
|
+
y: frame.rowsBottomY - measured.cardHeight,
|
|
147
|
+
width: measured.cardWidth,
|
|
148
|
+
height: measured.cardHeight,
|
|
149
|
+
}
|
|
150
|
+
: { x: 0, y: 0, width: frame.canvasRightX, height: measured.aboveHeight };
|
|
151
|
+
// Logo at the card's left padding, vertically centered; the text
|
|
152
|
+
// shifts right past it and centers in the (possibly taller) card.
|
|
153
|
+
let logo: PositionedHeader['logo'];
|
|
154
|
+
let textInset: PositionedHeader['textInset'];
|
|
155
|
+
if (measured.logo) {
|
|
156
|
+
logo = {
|
|
157
|
+
box: {
|
|
158
|
+
x: box.x + cardBox.x + HEADER_CARD_PADDING_X,
|
|
159
|
+
y: box.y + cardBox.y + (cardBox.height - measured.logo.height) / 2,
|
|
160
|
+
width: measured.logo.width,
|
|
161
|
+
height: measured.logo.height,
|
|
162
|
+
},
|
|
163
|
+
assetRef: measured.logo.assetRef,
|
|
164
|
+
intrinsic: measured.logo.intrinsic,
|
|
165
|
+
};
|
|
166
|
+
textInset = {
|
|
167
|
+
x: measured.logoSlot,
|
|
168
|
+
y: Math.max(0, (cardBox.height - measured.textCardHeight) / 2),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
box,
|
|
173
|
+
position: style.headerPosition,
|
|
174
|
+
title,
|
|
175
|
+
author,
|
|
176
|
+
titleLines: measured.titleLines,
|
|
177
|
+
authorLines: measured.authorLines,
|
|
178
|
+
cardBox,
|
|
179
|
+
logo,
|
|
180
|
+
...(textInset ? { textInset } : {}),
|
|
181
|
+
style,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Sized output from the beside-mode header word-wrap pass. */
|
|
187
|
+
interface SizedHeader {
|
|
188
|
+
titleLines: string[];
|
|
189
|
+
authorLines: string[];
|
|
190
|
+
cardWidth: number;
|
|
191
|
+
cardHeight: number;
|
|
192
|
+
textCardHeight: number;
|
|
193
|
+
logoSlot: number;
|
|
194
|
+
boxWidth: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sizeBesideHeader(
|
|
198
|
+
title: string,
|
|
199
|
+
author: string | undefined,
|
|
200
|
+
logo?: LogoDimensions,
|
|
201
|
+
): SizedHeader {
|
|
202
|
+
const maxContentWidth = HEADER_BESIDE_MAX_WIDTH_PX - 2 * HEADER_CARD_PADDING_X;
|
|
203
|
+
const titleLines = wrapText(title, maxContentWidth, HEADER_TITLE_FONT_SIZE_PX);
|
|
204
|
+
const authorLines = wrapText(author ?? '', maxContentWidth, HEADER_AUTHOR_FONT_SIZE_PX);
|
|
205
|
+
|
|
206
|
+
let widest = 0;
|
|
207
|
+
for (const line of titleLines)
|
|
208
|
+
widest = Math.max(widest, estimateTextWidth(line, HEADER_TITLE_FONT_SIZE_PX));
|
|
209
|
+
for (const line of authorLines)
|
|
210
|
+
widest = Math.max(widest, estimateTextWidth(line, HEADER_AUTHOR_FONT_SIZE_PX));
|
|
211
|
+
|
|
212
|
+
// A logo sits left of the text and widens the card past the text-only
|
|
213
|
+
// MAX clamp (which bounds the text column, not the logo slot).
|
|
214
|
+
const hasText = titleLines.length > 0 || authorLines.length > 0;
|
|
215
|
+
const logoSlot = logo ? logo.width + (hasText ? HEADER_LOGO_GAP_PX : 0) : 0;
|
|
216
|
+
|
|
217
|
+
const naturalCardWidth = widest + 2 * HEADER_CARD_PADDING_X;
|
|
218
|
+
// `HEADER_BESIDE_{MIN,MAX}_WIDTH_PX` bound the **boxWidth** (= cardWidth
|
|
219
|
+
// + left outer pad). Subtract one outer pad to derive the cardWidth
|
|
220
|
+
// bounds.
|
|
221
|
+
const cardWidth = Math.max(
|
|
222
|
+
HEADER_BESIDE_MIN_WIDTH_PX - HEADER_CARD_OUTER_PAD,
|
|
223
|
+
Math.min(HEADER_BESIDE_MAX_WIDTH_PX - HEADER_CARD_OUTER_PAD, naturalCardWidth) + logoSlot,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const titleBlockHeight =
|
|
227
|
+
titleLines.length > 0 ? (titleLines.length - 1) * HEADER_TITLE_LINE_HEIGHT_PX : 0;
|
|
228
|
+
const authorBlockHeight =
|
|
229
|
+
authorLines.length > 0
|
|
230
|
+
? HEADER_TITLE_TO_AUTHOR_GAP_PX +
|
|
231
|
+
(authorLines.length - 1) * HEADER_AUTHOR_LINE_HEIGHT_PX
|
|
232
|
+
: 0;
|
|
233
|
+
const textCardHeight =
|
|
234
|
+
HEADER_CARD_PADDING_TOP + titleBlockHeight + authorBlockHeight + HEADER_CARD_PADDING_BOTTOM;
|
|
235
|
+
// A logo taller than the text block grows the card; logo and text are
|
|
236
|
+
// then both centered vertically.
|
|
237
|
+
const cardHeight = logo
|
|
238
|
+
? Math.max(textCardHeight, logo.height + 2 * HEADER_LOGO_PAD_Y_PX)
|
|
239
|
+
: textCardHeight;
|
|
240
|
+
|
|
241
|
+
// `boxWidth` only includes the LEFT outer pad — the right-side breathing
|
|
242
|
+
// room between the card and the chart is owned by `GUTTER_PX` in the
|
|
243
|
+
// measure phase. So `boxWidth` doubles as `chartLeftX` (the card's right
|
|
244
|
+
// edge in canvas coordinates).
|
|
245
|
+
const boxWidth = cardWidth + HEADER_CARD_OUTER_PAD;
|
|
246
|
+
return {
|
|
247
|
+
titleLines,
|
|
248
|
+
authorLines,
|
|
249
|
+
cardWidth,
|
|
250
|
+
cardHeight,
|
|
251
|
+
textCardHeight,
|
|
252
|
+
logoSlot,
|
|
253
|
+
boxWidth,
|
|
254
|
+
};
|
|
255
|
+
}
|