flowsvg 0.0.0-stage → 0.1.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.
@@ -0,0 +1 @@
1
+ export * from "./core.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./core.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared node-content highlight paint for core timeline causes and browser-only hover.
3
+ * Paint consumes evaluated geometry and preserves explicit author colors; it never lays out text again.
4
+ */
5
+ import type { NodeContentLayout } from "./node-content.js";
6
+ /**
7
+ * Blend only default paint by the node highlight amount; preserve safe areas, metrics and author provenance.
8
+ * The evaluated fill selects readable ink during transitions: naive light-to-dark ink blending loses contrast midway.
9
+ */
10
+ export declare function paintContentHighlight(layout: NodeContentLayout, amount: number, fill: string): NodeContentLayout;
@@ -0,0 +1,54 @@
1
+ import { mixColor } from "./timeline.js";
2
+ const HIGHLIGHT_CONTENT_COLORS = {
3
+ title: "#172033", body: "#172033", marker: "#172033", auxiliary: "#354158", tag: "#164b9b", status: "#16466b"
4
+ };
5
+ const HIGHLIGHT_TAG_FILL = "#eaf1ff";
6
+ const MIN_CONTENT_CONTRAST = 4.5;
7
+ const CONTRAST_DARK_INK = "#000000";
8
+ const CONTRAST_LIGHT_INK = "#ffffff";
9
+ /**
10
+ * Blend only default paint by the node highlight amount; preserve safe areas, metrics and author provenance.
11
+ * The evaluated fill selects readable ink during transitions: naive light-to-dark ink blending loses contrast midway.
12
+ */
13
+ export function paintContentHighlight(layout, amount, fill) {
14
+ if (amount === 0)
15
+ return layout;
16
+ const tags = layout.tags.map((tag) => ({ ...tag, fill: tag.explicitFill ? tag.fill : mixColor(tag.fill, HIGHLIGHT_TAG_FILL, amount) }));
17
+ return {
18
+ ...layout, tags,
19
+ ...(layout.graphs ? { graphs: layout.graphs.map((graph) => ({ ...graph,
20
+ nodes: graph.nodes.map((node) => ({ ...node, fill: readableDefaultInk(node.fill, HIGHLIGHT_CONTENT_COLORS[node.highlighted ? "status" : "auxiliary"], amount, fill) })),
21
+ edges: graph.edges.map((edge) => ({ ...edge, color: readableDefaultInk(edge.color, HIGHLIGHT_CONTENT_COLORS[edge.highlighted ? "status" : "auxiliary"], amount, fill) }))
22
+ })) } : {}),
23
+ lines: layout.lines.map((line) => {
24
+ const tag = line.role === "tag" ? tags.find((candidate) => candidate.blockIndex === line.blockIndex
25
+ && candidate.itemIndex === line.itemIndex && candidate.tagIndex === line.tagIndex) : undefined;
26
+ const background = tag?.fill ?? fill;
27
+ const paint = (color) => readableDefaultInk(color, HIGHLIGHT_CONTENT_COLORS[line.role], amount, background);
28
+ const spans = line.spans.map((span) => ({ ...span, color: span.explicitColor ? span.color : paint(span.color) }));
29
+ return { ...line, spans, color: spans[0]?.color ?? paint(line.color) };
30
+ })
31
+ };
32
+ }
33
+ /** Retain semantic ink when it meets contrast; use opaque dark/light ink only across the transition's narrow middle. */
34
+ function readableDefaultInk(source, target, amount, background) {
35
+ const blended = mixColor(source, target, amount);
36
+ for (const candidate of [blended, source, target]) {
37
+ if (contrastRatio(candidate, background) >= MIN_CONTENT_CONTRAST)
38
+ return candidate;
39
+ }
40
+ return contrastRatio(CONTRAST_DARK_INK, background) >= contrastRatio(CONTRAST_LIGHT_INK, background)
41
+ ? CONTRAST_DARK_INK : CONTRAST_LIGHT_INK;
42
+ }
43
+ function contrastRatio(first, second) {
44
+ const a = relativeLuminance(first), b = relativeLuminance(second);
45
+ return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
46
+ }
47
+ /** Source colors are validated hexadecimal RGB/RGBA; only opaque default paints promise contrast. */
48
+ function relativeLuminance(color) {
49
+ const hex = color.slice(1);
50
+ const rgb = hex.length <= 4 ? [...hex.slice(0, 3)].map((digit) => digit + digit).join("") : hex;
51
+ const channels = [0, 2, 4].map((offset) => Number.parseInt(rgb.slice(offset, offset + 2), 16) / 255)
52
+ .map((value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
53
+ return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
54
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Deterministic node-content model and layout. The core supplies current absolute node geometry and theme colors.
3
+ * This module does not measure browser fonts or mutate authored dimensions; SVG consumes its evaluated lines and tag backgrounds.
4
+ */
5
+ import type { NodeKind, ValidationIssue } from "./core.js";
6
+ import type { ThemePalette } from "./theme.js";
7
+ /** Horizontal placement within the current content safe area. */
8
+ export type ContentAlignment = "left" | "center" | "right";
9
+ /** Controlled text appearance; omitted values use the block's semantic role. Colors are hexadecimal RGB/RGBA. */
10
+ export interface ContentTextStyle {
11
+ /** Positive finite font size in diagram units. */
12
+ fontSize?: number;
13
+ /** Integer weight from 100 through 900. */
14
+ fontWeight?: number;
15
+ color?: string;
16
+ align?: ContentAlignment;
17
+ }
18
+ /** Controlled inline typography. Alignment belongs to the containing block. */
19
+ export type ContentSpanStyle = Pick<ContentTextStyle, "fontSize" | "fontWeight" | "color">;
20
+ /** Inline source text with optional appearance; adjacent spans share wrapping and baseline placement. */
21
+ export interface ContentTextSpan {
22
+ text: string;
23
+ style?: ContentSpanStyle;
24
+ }
25
+ /** Plain text or an ordered series of inline styled spans. No span creates an independent target. */
26
+ export type ContentText = string | ContentTextSpan[];
27
+ /** Controlled tag typography and optional background; tags remain left-aligned within the item. */
28
+ export interface ContentTagStyle extends ContentSpanStyle {
29
+ fill?: string;
30
+ }
31
+ /** A decorative text tag, without an independent identity or interaction. */
32
+ export interface ContentTag {
33
+ text: ContentText;
34
+ style?: ContentTagStyle;
35
+ }
36
+ /** One list entry; absent parts consume no height. Explicit empty text preserves an authored line. */
37
+ export interface ContentListItem {
38
+ tags?: ContentTag[];
39
+ status?: ContentText;
40
+ /** Status is always right-aligned and defaults to 12-unit accent text. */
41
+ statusStyle?: ContentSpanStyle;
42
+ body?: ContentText;
43
+ auxiliary?: ContentText;
44
+ /** Body typography; overrides the containing list style. */
45
+ style?: ContentTextStyle;
46
+ /** Auxiliary typography; defaults to muted 12-unit text. */
47
+ auxiliaryStyle?: ContentTextStyle;
48
+ }
49
+ /** Controlled list typography with a nonnegative item gap, defaulting to 12 diagram units. */
50
+ export interface ContentListStyle extends ContentTextStyle {
51
+ itemGap?: number;
52
+ /** Space between tags and between wrapped tag rows; defaults to 6. */
53
+ tagGap?: number;
54
+ }
55
+ /** Decorative entity; x/y are center offsets from the graph block's top left, in diagram units. */
56
+ export interface ContentGraphNode {
57
+ id: string;
58
+ label: string;
59
+ x: number;
60
+ y: number;
61
+ highlighted?: boolean;
62
+ }
63
+ /** Undirected relation with IDs local to its graph block. */
64
+ export interface ContentGraphEdge {
65
+ from: string;
66
+ to: string;
67
+ }
68
+ /** Fixed-height entity graph with authored coordinates and no independent hit targets. */
69
+ export interface ContentGraphBlock {
70
+ type: "graph";
71
+ height: number;
72
+ nodes: ContentGraphNode[];
73
+ edges: ContentGraphEdge[];
74
+ }
75
+ /** Absolute graph primitives sharing the parent content clip. */
76
+ export interface ContentGraphLayout {
77
+ blockIndex: number;
78
+ nodes: {
79
+ x: number;
80
+ y: number;
81
+ radius: number;
82
+ fill: string;
83
+ highlighted: boolean;
84
+ }[];
85
+ edges: {
86
+ x1: number;
87
+ y1: number;
88
+ x2: number;
89
+ y2: number;
90
+ color: string;
91
+ highlighted: boolean;
92
+ }[];
93
+ }
94
+ /** One ordered title, paragraph, list or entity graph. */
95
+ export type ContentBlock = {
96
+ type: "title";
97
+ text: ContentText;
98
+ style?: ContentTextStyle;
99
+ } | {
100
+ type: "paragraph";
101
+ text: ContentText;
102
+ role?: "body" | "auxiliary";
103
+ style?: ContentTextStyle;
104
+ } | {
105
+ type: "list";
106
+ /** Omitted markers use unmarked records. */
107
+ marker?: "none" | "bullet" | "number";
108
+ items: ContentListItem[];
109
+ style?: ContentListStyle;
110
+ } | ContentGraphBlock;
111
+ /** Ordered fixed-node content. It replaces the simple label and never changes node dimensions. */
112
+ export interface NodeContent {
113
+ blocks: ContentBlock[];
114
+ style?: {
115
+ /** Uniform inset in diagram units; defaults to 16. */
116
+ padding?: number;
117
+ /** Vertical space between content blocks in diagram units; defaults to 14. */
118
+ blockGap?: number;
119
+ };
120
+ }
121
+ /** Absolute rectangular clip region; empty safe areas have zero width or height. */
122
+ export interface ContentSafeArea {
123
+ x: number;
124
+ y: number;
125
+ width: number;
126
+ height: number;
127
+ }
128
+ /** One evaluated inline span. x is its left edge and y is its shared line baseline. */
129
+ export interface ContentSpan {
130
+ text: string;
131
+ x: number;
132
+ y: number;
133
+ width: number;
134
+ fontSize: number;
135
+ fontWeight: number;
136
+ color: string;
137
+ /** True when a block or source span supplied this color; highlight paint preserves it. */
138
+ explicitColor: boolean;
139
+ }
140
+ /** One evaluated text line. x is the alignment anchor and y is the SVG baseline, in document units. */
141
+ export interface ContentLine {
142
+ spans: ContentSpan[];
143
+ text: string;
144
+ x: number;
145
+ y: number;
146
+ width: number;
147
+ fontSize: number;
148
+ fontWeight: number;
149
+ color: string;
150
+ align: ContentAlignment;
151
+ role: "title" | "body" | "auxiliary" | "marker" | "tag" | "status";
152
+ blockIndex: number;
153
+ /** Zero-based authored entry index; skipped empty entries do not renumber later entries. */
154
+ itemIndex?: number;
155
+ /** Zero-based authored tag index, present only for tag lines. */
156
+ tagIndex?: number;
157
+ }
158
+ /** Evaluated tag background, in absolute diagram units; paint preserves authored fills. */
159
+ export interface ContentTagBackground extends ContentSafeArea {
160
+ fill: string;
161
+ explicitFill: boolean;
162
+ blockIndex: number;
163
+ itemIndex: number;
164
+ tagIndex: number;
165
+ }
166
+ /** Complete layout before clipping; overflow flags report estimated width and height independently. */
167
+ export interface NodeContentLayout {
168
+ safeArea: ContentSafeArea;
169
+ lines: ContentLine[];
170
+ tags: ContentTagBackground[];
171
+ graphs?: ContentGraphLayout[];
172
+ widthOverflow: boolean;
173
+ heightOverflow: boolean;
174
+ }
175
+ /** Supported content shapes; node validators share this restriction. */
176
+ export declare function supportsNodeContent(kind: unknown): boolean;
177
+ /** Lay out current content from the top left, without DOM font measurement or authored-size changes. */
178
+ export declare function layoutNodeContent(content: NodeContent, bounds: ContentSafeArea, palette: ThemePalette, kind: NodeKind): NodeContentLayout;
179
+ /** Validate nested content separately from shape and label-mode checks at the document boundary. */
180
+ export declare function validateNodeContent(value: unknown, path: string, issues: ValidationIssue[]): void;
181
+ /** Validate inline source values before evaluation; unknown fields keep their exact source position. */
182
+ export declare function validateContentText(value: unknown, path: string, issues: ValidationIssue[]): void;
183
+ /** Validate the controlled style shared by content blocks; no CSS expression or external resource is accepted. */
184
+ export declare function validateContentTextStyle(value: unknown, path: string, issues: ValidationIssue[], inline?: boolean): void;