@nowline/layout 0.8.6 → 0.10.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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Start rules — engine A's (the pixel layout's) start rule for a lane or
|
|
2
|
+
// group child, built on the parsed terms of `start-terms.ts`: the latest of
|
|
3
|
+
// the sequential default and the `after:` terms (refs and inline dates),
|
|
4
|
+
// then the wave floor (specs/waves.md §8.4). Also the inline
|
|
5
|
+
// `after:` date that reports NL.I1008 when it set an item's start on a
|
|
6
|
+
// non-working day (specs/working-calendar.md §6).
|
|
7
|
+
|
|
8
|
+
import type { EntityProperty, GroupBlock, ItemDeclaration, ParallelBlock } from '@nowline/core';
|
|
9
|
+
import { formatIsoDate } from './dsl-utils.js';
|
|
10
|
+
import type { LayoutContext } from './layout-context.js';
|
|
11
|
+
import { startTermsOf } from './start-terms.js';
|
|
12
|
+
import type { PositionedItem } from './types.js';
|
|
13
|
+
import { dateAtX, WAVE_EDGE_TOLERANCE_PX, waveFloorX } from './wave-layout.js';
|
|
14
|
+
|
|
15
|
+
// Resolve the desired startX for a swimlane child: the latest of the
|
|
16
|
+
// sequential default (`seqDefault`, the lane's rightmost time cursor across
|
|
17
|
+
// all rows) and every `after:` term. Work has no fixed start pin, so
|
|
18
|
+
// `after:` only ever delays (specs/dsl.md § Item Properties).
|
|
19
|
+
//
|
|
20
|
+
// `after:` accepts both entity ids (looked up in `ctx.entityRightEdges`) and
|
|
21
|
+
// inline ISO date literals (looked up via `ctx.scale.forwardWithinDomain`).
|
|
22
|
+
// The validator already enforces "at most one inline date per direction" so
|
|
23
|
+
// at most one element in the list will hit the date path.
|
|
24
|
+
//
|
|
25
|
+
// In a roadmap with waves the result is floored by the child's wave and,
|
|
26
|
+
// for a container, its lead wave (specs/waves.md §8.4), so the row
|
|
27
|
+
// packer's predicted start matches the start `sequenceItem` / the
|
|
28
|
+
// container node places.
|
|
29
|
+
export function resolveChildStart(
|
|
30
|
+
child: ItemDeclaration | GroupBlock | ParallelBlock,
|
|
31
|
+
seqDefault: number,
|
|
32
|
+
ctx: LayoutContext,
|
|
33
|
+
): number {
|
|
34
|
+
return waveFloorX(
|
|
35
|
+
child,
|
|
36
|
+
resolvePinnedOrSequentialStart(child.properties, seqDefault, ctx),
|
|
37
|
+
ctx,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let scheduleWarmStart = true;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Turn the warm start from the day schedule on or off: the barrier seed of
|
|
45
|
+
* the place phase. Tests turn it off to compare against a cold start (same
|
|
46
|
+
* output, more barrier passes); layout never does.
|
|
47
|
+
*/
|
|
48
|
+
export function setScheduleWarmStart(enabled: boolean): void {
|
|
49
|
+
scheduleWarmStart = enabled;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True unless a test turned the warm start off (`setScheduleWarmStart`). */
|
|
53
|
+
export function isScheduleWarmStart(): boolean {
|
|
54
|
+
return scheduleWarmStart;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The start before the wave floor: the latest `after:` end (ids and inline
|
|
59
|
+
* dates inside the window) no earlier than `seqDefault`, else `seqDefault`.
|
|
60
|
+
*/
|
|
61
|
+
export function resolvePinnedOrSequentialStart(
|
|
62
|
+
props: EntityProperty[],
|
|
63
|
+
seqDefault: number,
|
|
64
|
+
ctx: LayoutContext,
|
|
65
|
+
): number {
|
|
66
|
+
const { after } = startTermsOf(props);
|
|
67
|
+
if (after.length > 0) {
|
|
68
|
+
let maxEnd = seqDefault;
|
|
69
|
+
for (const term of after) {
|
|
70
|
+
if ('date' in term) {
|
|
71
|
+
const xd = ctx.scale.forwardWithinDomain(term.date);
|
|
72
|
+
if (xd !== null) maxEnd = Math.max(maxEnd, xd);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const endX = ctx.entityRightEdges.get(term.ref);
|
|
76
|
+
if (endX !== undefined) maxEnd = Math.max(maxEnd, endX);
|
|
77
|
+
}
|
|
78
|
+
return maxEnd;
|
|
79
|
+
}
|
|
80
|
+
return seqDefault;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The inline `after:` date on a non-working day that set an item's start
|
|
85
|
+
* (NL.I1008, specs/working-calendar.md §6), for an item placed at `startX`.
|
|
86
|
+
* It reports only when it set the start, that is when the item starts at
|
|
87
|
+
* the date's x (within the edge tolerance): a later `after:` ref, the lane
|
|
88
|
+
* cursor or a wave floor that pushed the item past it never reports. A date
|
|
89
|
+
* on a working day or outside the date window reports nothing.
|
|
90
|
+
*/
|
|
91
|
+
export function nonWorkingPinOf(
|
|
92
|
+
props: EntityProperty[],
|
|
93
|
+
startX: number,
|
|
94
|
+
ctx: LayoutContext,
|
|
95
|
+
): PositionedItem['nonWorkingPin'] {
|
|
96
|
+
if (!ctx.calendar.hasNonWorkingDays) return undefined;
|
|
97
|
+
const pin = startPinOf(props);
|
|
98
|
+
if (!pin || ctx.calendar.isWorkingDay(pin.date)) return undefined;
|
|
99
|
+
const pinX = ctx.scale.forwardWithinDomain(pin.date);
|
|
100
|
+
if (pinX === null || Math.abs(startX - pinX) >= WAVE_EDGE_TOLERANCE_PX) return undefined;
|
|
101
|
+
return { key: pin.key, pin: pin.raw, start: formatIsoDate(dateAtX(startX, ctx)) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The inline `after:` date that can set an item's start. */
|
|
105
|
+
function startPinOf(
|
|
106
|
+
props: EntityProperty[],
|
|
107
|
+
): { key: 'after'; raw: string; date: Date } | undefined {
|
|
108
|
+
const { after } = startTermsOf(props);
|
|
109
|
+
for (const term of after) {
|
|
110
|
+
if ('date' in term) return { key: 'after', raw: term.raw, date: term.date };
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Start terms and wave floors, parsed once for every engine that places an
|
|
2
|
+
// item (specs/scheduler-unification.md §3.4): engine C's day walker, engine
|
|
3
|
+
// A's pixel walker and its non-working-pin helper, and the wave barrier's
|
|
4
|
+
// floor projections. Each caller keeps its own `max` rule and maps the
|
|
5
|
+
// parsed dates to its own unit (working-day index or x); only the parsing
|
|
6
|
+
// is shared. Pure: no calendar, no scale. Work has no fixed start pin:
|
|
7
|
+
// `date:` and `start:` on an item are unknown keys (specs/dsl.md § Item
|
|
8
|
+
// Properties, "Pinning is not supported"), so nothing here reads them.
|
|
9
|
+
|
|
10
|
+
import type { EntityProperty, WavePlan } from '@nowline/core';
|
|
11
|
+
import { parseIsoDate, propValues } from './dsl-utils.js';
|
|
12
|
+
|
|
13
|
+
/** One element of `after:`: an ISO date, or the id of an entity. */
|
|
14
|
+
export type AfterTerm = { raw: string; date: Date } | { raw: string; ref: string };
|
|
15
|
+
|
|
16
|
+
export interface StartTerms {
|
|
17
|
+
/** `after:` elements in source order: an ISO date or an id. */
|
|
18
|
+
after: ReadonlyArray<AfterTerm>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The start terms of an item, group or parallel from its properties. */
|
|
22
|
+
export function startTermsOf(props: EntityProperty[]): StartTerms {
|
|
23
|
+
const after = propValues(props, 'after').map((raw): AfterTerm => {
|
|
24
|
+
const date = parseIsoDate(raw);
|
|
25
|
+
return date ? { raw, date } : { raw, ref: raw };
|
|
26
|
+
});
|
|
27
|
+
return { after };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The declaration date of each wave's floor (`null` for a wave without a
|
|
32
|
+
* floor or whose date does not parse), at index `k - 1` for wave `k`. The
|
|
33
|
+
* dates are the plan's unclamped declarations: the day engine maps them with
|
|
34
|
+
* `workingIndexOf`, the pixel engine with `scale.forward`.
|
|
35
|
+
*/
|
|
36
|
+
export function waveFloorDates(plan: WavePlan): Array<Date | null> {
|
|
37
|
+
return plan.floors.map((f) => (f ? parseIsoDate(f.date) : null));
|
|
38
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Status-dot colour: the bar fill's luminance picks one of the theme's two
|
|
2
|
+
// status-dot palettes, and the item's status picks the colour in it. Moved
|
|
3
|
+
// from the SVG renderer (R5) so each positioned item carries its resolved
|
|
4
|
+
// dot colour and the renderer only draws the dot.
|
|
5
|
+
|
|
6
|
+
import type { Theme } from './themes/shape.js';
|
|
7
|
+
import type { StatusKind } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* sRGB → relative luminance per WCAG 2.x. Input may be `#rrggbb`,
|
|
11
|
+
* `#rgb`, or a non-hex token like `none` / `transparent`. Non-hex
|
|
12
|
+
* inputs return 1 (treated as light) so a transparent bar reuses
|
|
13
|
+
* the chart's light bg. Mostly used to choose between two
|
|
14
|
+
* status-dot palettes (`onLight` vs `onDark`) so the dot reads on
|
|
15
|
+
* any bar fill — see `pickStatusDotPalette` and
|
|
16
|
+
* `specs/rendering.md`'s status-dot section.
|
|
17
|
+
*
|
|
18
|
+
* This is the renderer's copy, kept with its semantics: unlike
|
|
19
|
+
* `relativeLuminance` in `themes/contrast.ts`, which throws on anything
|
|
20
|
+
* but `#rgb` / `#rrggbb`, it never throws, and a 6-character string that
|
|
21
|
+
* is not hex gives `NaN` (so it compares false against any threshold).
|
|
22
|
+
*/
|
|
23
|
+
export function fillLuminance(hex: string): number {
|
|
24
|
+
if (!hex || hex === 'none' || hex === 'transparent') return 1;
|
|
25
|
+
let h = hex.startsWith('#') ? hex.slice(1) : hex;
|
|
26
|
+
if (h.length === 3) {
|
|
27
|
+
h = h
|
|
28
|
+
.split('')
|
|
29
|
+
.map((c) => c + c)
|
|
30
|
+
.join('');
|
|
31
|
+
}
|
|
32
|
+
if (h.length !== 6) return 1;
|
|
33
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
34
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
35
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
36
|
+
const lin = (c: number): number => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
37
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Pick the status-dot palette whose tone contrasts best with the
|
|
42
|
+
* given bar bg.
|
|
43
|
+
*
|
|
44
|
+
* The crossover threshold is the bar luminance at which a deep dot
|
|
45
|
+
* (avg luminance ≈ 0.045 across `onLight` palette entries) and a
|
|
46
|
+
* pale dot (avg ≈ 0.85 across `onDark` entries) give equal WCAG
|
|
47
|
+
* contrast. Solving `(L_bar + 0.05)² ≈ 0.86 × 0.095` gives
|
|
48
|
+
* `L_bar ≈ 0.24`, so:
|
|
49
|
+
* - bars with luminance ≥ 0.24 (most label-driven mid-tones AND
|
|
50
|
+
* all pale status-tint bars) → `onLight` deep dot
|
|
51
|
+
* - bars with luminance < 0.24 (dark status-tint bars in dark
|
|
52
|
+
* theme, e.g. `#172554`) → `onDark` pale dot
|
|
53
|
+
*/
|
|
54
|
+
export function pickStatusDotPalette(bg: string, palette: Theme): Theme['statusDot']['onLight'] {
|
|
55
|
+
return fillLuminance(bg) >= 0.24 ? palette.statusDot.onLight : palette.statusDot.onDark;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The status dot's colour for a bar filled `bg` with status `status`.
|
|
60
|
+
*
|
|
61
|
+
* Status-dot color — the dot communicates status via hue, but
|
|
62
|
+
* the bar bg can range from pale status tints (`#eff6ff`) to
|
|
63
|
+
* saturated mid-tones (`#1e88e5` from `bg:blue` labels) to
|
|
64
|
+
* dark navies (`#172554` in dark theme), so a single palette
|
|
65
|
+
* can't keep contrast across all bars. Two palettes — `onLight`
|
|
66
|
+
* (deep tints, for pale bars) and `onDark` (pale tints, for
|
|
67
|
+
* saturated/dark bars) — are picked from based on the bar
|
|
68
|
+
* bg's relative luminance.
|
|
69
|
+
*/
|
|
70
|
+
export function statusDotColor(status: StatusKind, bg: string, palette: Theme): string {
|
|
71
|
+
const dotPalette = pickStatusDotPalette(bg, palette);
|
|
72
|
+
const statusColors: Record<string, string> = {
|
|
73
|
+
done: dotPalette.done,
|
|
74
|
+
'in-progress': dotPalette.inProgress,
|
|
75
|
+
'at-risk': dotPalette.atRisk,
|
|
76
|
+
blocked: dotPalette.blocked,
|
|
77
|
+
planned: dotPalette.planned,
|
|
78
|
+
neutral: dotPalette.neutral,
|
|
79
|
+
};
|
|
80
|
+
return statusColors[status] ?? statusColors.neutral;
|
|
81
|
+
}
|
package/src/style-resolution.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
StyleDeclaration,
|
|
6
6
|
StyleProperty,
|
|
7
7
|
} from '@nowline/core';
|
|
8
|
+
import { propKey, styleLayers } from '@nowline/core/internal';
|
|
9
|
+
import { isNonWorkingDisplay } from './non-working-display.js';
|
|
8
10
|
import { resolveColor } from './themes/index.js';
|
|
9
11
|
import type { EntityStyle, Theme } from './themes/shape.js';
|
|
10
12
|
import type { ResolvedStyle, SizeBucket } from './types.js';
|
|
@@ -31,10 +33,6 @@ export interface StyleContext {
|
|
|
31
33
|
labels: Map<string, LabelDeclaration>;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
function propKey(prop: { key: string }): string {
|
|
35
|
-
return prop.key.endsWith(':') ? prop.key.slice(0, -1) : prop.key;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
36
|
function entityStyleToResolved(e: EntityStyle, theme: Theme): ResolvedStyle {
|
|
39
37
|
return {
|
|
40
38
|
bg: resolveColor(e.bg, theme),
|
|
@@ -61,6 +59,7 @@ function entityStyleToResolved(e: EntityStyle, theme: Theme): ResolvedStyle {
|
|
|
61
59
|
// single-top-strip layout and keep the major-ticks-only grid.
|
|
62
60
|
timelinePosition: 'top',
|
|
63
61
|
minorGrid: false,
|
|
62
|
+
nonWorking: 'hide',
|
|
64
63
|
};
|
|
65
64
|
}
|
|
66
65
|
|
|
@@ -136,6 +135,9 @@ function applyProp(target: ResolvedStyle, key: string, value: string, theme: The
|
|
|
136
135
|
case 'minor-grid':
|
|
137
136
|
target.minorGrid = value === 'true';
|
|
138
137
|
break;
|
|
138
|
+
case 'non-working':
|
|
139
|
+
if (isNonWorkingDisplay(value)) target.nonWorking = value;
|
|
140
|
+
break;
|
|
139
141
|
default:
|
|
140
142
|
break;
|
|
141
143
|
}
|
|
@@ -161,7 +163,11 @@ function applyStyleDecl(
|
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
function applyProperties(
|
|
166
|
+
function applyProperties(
|
|
167
|
+
target: ResolvedStyle,
|
|
168
|
+
props: readonly EntityProperty[],
|
|
169
|
+
theme: Theme,
|
|
170
|
+
): void {
|
|
165
171
|
for (const p of props) {
|
|
166
172
|
const key = propKey(p);
|
|
167
173
|
if (p.value !== undefined) {
|
|
@@ -170,42 +176,14 @@ function applyProperties(target: ResolvedStyle, props: EntityProperty[], theme:
|
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
function applyLabelStyleRefs(
|
|
174
|
-
target: ResolvedStyle,
|
|
175
|
-
props: EntityProperty[],
|
|
176
|
-
ctx: StyleContext,
|
|
177
|
-
): void {
|
|
178
|
-
const labelsProp = props.find((p) => propKey(p) === 'labels');
|
|
179
|
-
if (!labelsProp) return;
|
|
180
|
-
const names: string[] = labelsProp.value ? [labelsProp.value] : labelsProp.values;
|
|
181
|
-
for (const name of names) {
|
|
182
|
-
const label = ctx.labels.get(name);
|
|
183
|
-
if (!label) continue;
|
|
184
|
-
// Label's `style:` ref gets applied.
|
|
185
|
-
const styleRef = label.properties.find((p) => propKey(p) === 'style');
|
|
186
|
-
if (styleRef?.value) {
|
|
187
|
-
applyStyleDecl(target, ctx.styles.get(styleRef.value), ctx.theme);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function applyEntityStyleRef(
|
|
193
|
-
target: ResolvedStyle,
|
|
194
|
-
props: EntityProperty[],
|
|
195
|
-
ctx: StyleContext,
|
|
196
|
-
): void {
|
|
197
|
-
const styleProp = props.find((p) => propKey(p) === 'style');
|
|
198
|
-
if (!styleProp?.value) return;
|
|
199
|
-
applyStyleDecl(target, ctx.styles.get(styleProp.value), ctx.theme);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
179
|
// Five-level precedence chain from specs/rendering.md § Style Precedence:
|
|
203
180
|
// 1. system default (theme's EntityStyle for this type)
|
|
204
|
-
// 2. config `default <entity>`
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
181
|
+
// 2-5. core's `styleLayers`: the config `default <entity>` line (its `style:`
|
|
182
|
+
// ref, then its raw properties), label `style:` refs (the first label wins
|
|
183
|
+
// a shared key), the entity's own `style:` ref, then its inline properties
|
|
184
|
+
// (banned by the validator for roadmap entities; still supported for
|
|
185
|
+
// declared styles / defaults). A style or label this file does not declare
|
|
186
|
+
// changes nothing.
|
|
209
187
|
export function resolveStyle(
|
|
210
188
|
entityType: EntityTypeKey,
|
|
211
189
|
props: EntityProperty[],
|
|
@@ -213,26 +191,18 @@ export function resolveStyle(
|
|
|
213
191
|
): ResolvedStyle {
|
|
214
192
|
const baseEntity = ctx.theme.entities[entityType];
|
|
215
193
|
const out = entityStyleToResolved(baseEntity, ctx.theme);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
194
|
+
for (const layer of styleLayers(entityType, props, ctx)) {
|
|
195
|
+
switch (layer.level) {
|
|
196
|
+
case 'default':
|
|
197
|
+
case 'inline':
|
|
198
|
+
applyProperties(out, layer.props, ctx.theme);
|
|
199
|
+
break;
|
|
200
|
+
case 'label':
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
applyStyleDecl(out, layer.decl, ctx.theme);
|
|
224
204
|
}
|
|
225
205
|
}
|
|
226
|
-
|
|
227
|
-
// Level 3: label styles (labels on this entity, each label's `style:` ref)
|
|
228
|
-
applyLabelStyleRefs(out, props, ctx);
|
|
229
|
-
|
|
230
|
-
// Level 4: entity's own `style:` ref
|
|
231
|
-
applyEntityStyleRef(out, props, ctx);
|
|
232
|
-
|
|
233
|
-
// Level 5: inline props on the entity
|
|
234
|
-
applyProperties(out, props, ctx.theme);
|
|
235
|
-
|
|
236
206
|
return out;
|
|
237
207
|
}
|
|
238
208
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Shared text-measurement heuristics for layout.
|
|
2
|
+
//
|
|
3
|
+
// Layout has no font metrics, so every caption / header / marker fit
|
|
4
|
+
// decision runs on one pessimistic per-character estimate. Keeping the
|
|
5
|
+
// estimate and the whitespace word-wrap built on it in a single module
|
|
6
|
+
// means the item-title wrap (`measureItem`) and the header-card wrap in
|
|
7
|
+
// `layout.ts` can never disagree about how wide a string is.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rough px-width estimate for sans-serif text. Intentionally pessimistic
|
|
11
|
+
* (~0.58 em per char) so we err toward "doesn't fit" and trigger a
|
|
12
|
+
* spill or row bump rather than draw a title that clips.
|
|
13
|
+
*/
|
|
14
|
+
export function estimateTextWidth(text: string, fontSize: number): number {
|
|
15
|
+
return text.length * fontSize * 0.58;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `text` when it fits `maxWidth` (in px), else its longest prefix that fits
|
|
20
|
+
* with a trailing `…`, trailing whitespace dropped before the ellipsis. It
|
|
21
|
+
* always keeps the first character, so the result can overrun `maxWidth`
|
|
22
|
+
* when even one character and the ellipsis do not fit. Counts code points,
|
|
23
|
+
* so a surrogate pair is never split.
|
|
24
|
+
*/
|
|
25
|
+
export function ellipsizeToWidth(text: string, maxWidth: number, fontSize: number): string {
|
|
26
|
+
if (estimateTextWidth(text, fontSize) <= maxWidth) return text;
|
|
27
|
+
const chars = Array.from(text.trimStart());
|
|
28
|
+
for (let n = chars.length - 1; n >= 1; n--) {
|
|
29
|
+
const prefix = chars.slice(0, n).join('').trimEnd();
|
|
30
|
+
const candidate = `${prefix}…`;
|
|
31
|
+
if (estimateTextWidth(candidate, fontSize) <= maxWidth) return candidate;
|
|
32
|
+
}
|
|
33
|
+
return `${chars[0] ?? ''}…`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Word-wrap `text` so that no line is wider than `maxWidth` (in px).
|
|
38
|
+
* Wrap points are whitespace only: a long single word is kept on its
|
|
39
|
+
* own line even if it overflows `maxWidth` — we never split a word in
|
|
40
|
+
* the middle. The wrap is greedy (fill each line before starting the
|
|
41
|
+
* next), which yields the minimum possible line count for a given
|
|
42
|
+
* width.
|
|
43
|
+
*
|
|
44
|
+
* `firstLineMaxWidth` (default `maxWidth`) narrows only the FIRST line,
|
|
45
|
+
* for callers whose first line has to stay clear of something the later
|
|
46
|
+
* lines are below (the item bar's top-right decorations).
|
|
47
|
+
*/
|
|
48
|
+
export function wrapText(
|
|
49
|
+
text: string,
|
|
50
|
+
maxWidth: number,
|
|
51
|
+
fontSize: number,
|
|
52
|
+
firstLineMaxWidth: number = maxWidth,
|
|
53
|
+
): string[] {
|
|
54
|
+
if (!text) return [];
|
|
55
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
56
|
+
if (words.length === 0) return [];
|
|
57
|
+
const lines: string[] = [];
|
|
58
|
+
let cur = '';
|
|
59
|
+
for (const word of words) {
|
|
60
|
+
const trial = cur ? `${cur} ${word}` : word;
|
|
61
|
+
const limit = lines.length === 0 ? firstLineMaxWidth : maxWidth;
|
|
62
|
+
if (cur && estimateTextWidth(trial, fontSize) > limit) {
|
|
63
|
+
lines.push(cur);
|
|
64
|
+
cur = word;
|
|
65
|
+
} else {
|
|
66
|
+
cur = trial;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (cur) lines.push(cur);
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// WCAG 2.x contrast helpers over `#rgb` / `#rrggbb` hex colours. Used by the
|
|
2
|
+
// theme contrast-floor tests and by wave strip label colour selection.
|
|
3
|
+
|
|
4
|
+
export function hexToRgb(hex: string): [number, number, number] {
|
|
5
|
+
let h = hex.trim().replace(/^#/, '');
|
|
6
|
+
if (h.length === 3) {
|
|
7
|
+
h = h
|
|
8
|
+
.split('')
|
|
9
|
+
.map((c) => c + c)
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) {
|
|
13
|
+
throw new Error(`Unsupported hex colour: ${hex}`);
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
Number.parseInt(h.slice(0, 2), 16),
|
|
17
|
+
Number.parseInt(h.slice(2, 4), 16),
|
|
18
|
+
Number.parseInt(h.slice(4, 6), 16),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function relativeLuminance(hex: string): number {
|
|
23
|
+
const [r, g, b] = hexToRgb(hex).map((v) => {
|
|
24
|
+
const c = v / 255;
|
|
25
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
26
|
+
}) as [number, number, number];
|
|
27
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function contrastRatio(a: string, b: string): number {
|
|
31
|
+
const la = relativeLuminance(a);
|
|
32
|
+
const lb = relativeLuminance(b);
|
|
33
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
34
|
+
}
|
package/src/themes/dark.ts
CHANGED
|
@@ -62,13 +62,19 @@ export const darkTheme: Theme = {
|
|
|
62
62
|
shadow: 'subtle',
|
|
63
63
|
cornerRadius: 'sm',
|
|
64
64
|
},
|
|
65
|
+
// Containers shade their body with `bg` as a light tint
|
|
66
|
+
// (CONTAINER_BODY_FILL_OPACITY); groups and parallels get distinct
|
|
67
|
+
// ones, and a parallel's header bracket is dashed, a group's solid
|
|
68
|
+
// (specs/true-width-layout.md §3.3).
|
|
65
69
|
parallel: {
|
|
66
70
|
...baseEntity,
|
|
67
|
-
|
|
71
|
+
bg: '#8c84ff',
|
|
72
|
+
bracket: 'dashed',
|
|
68
73
|
padding: 'xs',
|
|
69
74
|
},
|
|
70
75
|
group: {
|
|
71
76
|
...baseEntity,
|
|
77
|
+
bg: '#f0a832',
|
|
72
78
|
bracket: 'solid',
|
|
73
79
|
padding: 'xs',
|
|
74
80
|
fg: '#94a3b8',
|
|
@@ -125,6 +131,8 @@ export const darkTheme: Theme = {
|
|
|
125
131
|
timeline: {
|
|
126
132
|
gridLine: '#475569',
|
|
127
133
|
minorGridLine: '#334155',
|
|
134
|
+
nonWorkingSeam: '#6b7a90',
|
|
135
|
+
nonWorkingFill: '#94a3b8',
|
|
128
136
|
tickMark: '#334155',
|
|
129
137
|
labelText: '#cbd5e1',
|
|
130
138
|
panelFill: '#0f172a',
|
|
@@ -197,6 +205,15 @@ export const darkTheme: Theme = {
|
|
|
197
205
|
badgeStroke: '#475569',
|
|
198
206
|
badgeText: '#94a3b8',
|
|
199
207
|
},
|
|
208
|
+
wave: {
|
|
209
|
+
stripFill: '#042f2e',
|
|
210
|
+
stripFillAlt: '#134e4a',
|
|
211
|
+
labelText: '#99f6e4',
|
|
212
|
+
labelMuted: '#5eead4',
|
|
213
|
+
boundary: '#2dd4bf',
|
|
214
|
+
hatch: '#0f172a',
|
|
215
|
+
hatchOnDark: '#ffffff',
|
|
216
|
+
},
|
|
200
217
|
arrowhead: {
|
|
201
218
|
neutral: '#94a3b8',
|
|
202
219
|
light: '#64748b',
|
|
@@ -210,6 +227,16 @@ export const darkTheme: Theme = {
|
|
|
210
227
|
planned: '#94a3b8',
|
|
211
228
|
neutral: '#94a3b8',
|
|
212
229
|
},
|
|
230
|
+
// Item bar fill per status, applied by layout when the item's resolved
|
|
231
|
+
// `bg` is still `entities.item.bg` (m2d Resolution 3).
|
|
232
|
+
statusTint: {
|
|
233
|
+
done: '#052e16',
|
|
234
|
+
inProgress: '#172554',
|
|
235
|
+
atRisk: '#422006',
|
|
236
|
+
blocked: '#7f1d1d',
|
|
237
|
+
planned: '#1e293b',
|
|
238
|
+
neutral: '#1e293b',
|
|
239
|
+
},
|
|
213
240
|
// Status-dot palettes — see `light.ts` for the contract. Same
|
|
214
241
|
// two palettes both themes; dark-theme bars come in BOTH dark
|
|
215
242
|
// (status-tint) and bright (label-driven) flavors, so a single
|
package/src/themes/grayscale.ts
CHANGED
|
@@ -63,13 +63,19 @@ export const grayscaleTheme: Theme = {
|
|
|
63
63
|
shadow: 'subtle',
|
|
64
64
|
cornerRadius: 'sm',
|
|
65
65
|
},
|
|
66
|
+
// Containers shade their body with `bg` as a light tint
|
|
67
|
+
// (CONTAINER_BODY_FILL_OPACITY); groups and parallels get distinct
|
|
68
|
+
// ones, and a parallel's header bracket is dashed, a group's solid
|
|
69
|
+
// (specs/true-width-layout.md §3.3).
|
|
66
70
|
parallel: {
|
|
67
71
|
...baseEntity,
|
|
68
|
-
|
|
72
|
+
bg: '#9e9e9e',
|
|
73
|
+
bracket: 'dashed',
|
|
69
74
|
padding: 'xs',
|
|
70
75
|
},
|
|
71
76
|
group: {
|
|
72
77
|
...baseEntity,
|
|
78
|
+
bg: '#595959',
|
|
73
79
|
bracket: 'solid',
|
|
74
80
|
padding: 'xs',
|
|
75
81
|
fg: '#595959',
|
|
@@ -126,6 +132,8 @@ export const grayscaleTheme: Theme = {
|
|
|
126
132
|
timeline: {
|
|
127
133
|
gridLine: '#bdbdbd',
|
|
128
134
|
minorGridLine: '#d4d4d4',
|
|
135
|
+
nonWorkingSeam: '#9a9a9a',
|
|
136
|
+
nonWorkingFill: '#737373',
|
|
129
137
|
tickMark: '#bdbdbd',
|
|
130
138
|
labelText: '#737373',
|
|
131
139
|
panelFill: '#ffffff',
|
|
@@ -198,6 +206,15 @@ export const grayscaleTheme: Theme = {
|
|
|
198
206
|
badgeStroke: '#bdbdbd',
|
|
199
207
|
badgeText: '#595959',
|
|
200
208
|
},
|
|
209
|
+
wave: {
|
|
210
|
+
stripFill: '#fafafa',
|
|
211
|
+
stripFillAlt: '#e0e0e0',
|
|
212
|
+
labelText: '#212121',
|
|
213
|
+
labelMuted: '#616161',
|
|
214
|
+
boundary: '#616161',
|
|
215
|
+
hatch: '#000000',
|
|
216
|
+
hatchOnDark: '#ffffff',
|
|
217
|
+
},
|
|
201
218
|
arrowhead: {
|
|
202
219
|
neutral: '#595959',
|
|
203
220
|
light: '#9e9e9e',
|
|
@@ -211,6 +228,16 @@ export const grayscaleTheme: Theme = {
|
|
|
211
228
|
planned: '#bdbdbd',
|
|
212
229
|
neutral: '#bdbdbd',
|
|
213
230
|
},
|
|
231
|
+
// Item bar fill per status, applied by layout when the item's resolved
|
|
232
|
+
// `bg` is still `entities.item.bg` (m2d Resolution 3).
|
|
233
|
+
statusTint: {
|
|
234
|
+
done: '#ebebeb',
|
|
235
|
+
inProgress: '#e4e4e4',
|
|
236
|
+
atRisk: '#eeeeee',
|
|
237
|
+
blocked: '#dcdcdc',
|
|
238
|
+
planned: '#f5f5f5',
|
|
239
|
+
neutral: '#f5f5f5',
|
|
240
|
+
},
|
|
214
241
|
statusDot: {
|
|
215
242
|
onLight: {
|
|
216
243
|
done: '#3a3a3a',
|
package/src/themes/index.ts
CHANGED
|
@@ -8,15 +8,21 @@ import { grayscaleNamed, grayscaleTheme } from './grayscale.js';
|
|
|
8
8
|
import { lightNamed, lightTheme } from './light.js';
|
|
9
9
|
import type { NamedColors, Theme } from './shape.js';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// The one list of theme names. `ThemeName` derives from it and `themes` /
|
|
12
|
+
// `namedColors` are keyed by it, so a new theme is added here and tsc then
|
|
13
|
+
// demands its palette. Every surface that validates a theme (CLI `render` /
|
|
14
|
+
// `serve`, the GitHub Action) goes through `normalizeThemeName` below.
|
|
15
|
+
export const THEME_NAMES = ['light', 'dark', 'grayscale'] as const;
|
|
12
16
|
|
|
13
|
-
export
|
|
17
|
+
export type ThemeName = (typeof THEME_NAMES)[number];
|
|
18
|
+
|
|
19
|
+
export const themes: Record<ThemeName, Theme> = {
|
|
14
20
|
light: lightTheme,
|
|
15
21
|
dark: darkTheme,
|
|
16
22
|
grayscale: grayscaleTheme,
|
|
17
23
|
};
|
|
18
24
|
|
|
19
|
-
export const namedColors:
|
|
25
|
+
export const namedColors: Record<ThemeName, NamedColors> = {
|
|
20
26
|
light: lightNamed,
|
|
21
27
|
dark: darkNamed,
|
|
22
28
|
grayscale: grayscaleNamed,
|
|
@@ -37,10 +43,7 @@ const THEME_ALIASES: Record<string, ThemeName> = {
|
|
|
37
43
|
export function normalizeThemeName(raw: string): ThemeName | undefined {
|
|
38
44
|
const lower = raw.toLowerCase();
|
|
39
45
|
const canonical = THEME_ALIASES[lower] ?? lower;
|
|
40
|
-
|
|
41
|
-
return canonical;
|
|
42
|
-
}
|
|
43
|
-
return undefined;
|
|
46
|
+
return THEME_NAMES.find((name) => name === canonical);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
// Aliases collapse internationally-friendlier spellings onto the canonical
|