@rankonelabs/livid-svg 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steven Ciraolo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @rankonelabs/livid-svg
2
+
3
+ `LaidOutDiagram` to an SVG string, for [livid](https://github.com/RankOneLabs/livid).
4
+
5
+ Build-time only. No DOM, no client JS, and nothing measured — geometry arrives
6
+ from [`@rankonelabs/livid-core`](https://www.npmjs.com/package/@rankonelabs/livid-core)
7
+ already computed, so this package draws and does not lay out. That is what makes
8
+ the SVG in a post and the React canvas in an app the *same map* rather than two
9
+ drawings that drifted.
10
+
11
+ ## Install
12
+
13
+ ```
14
+ npm install @rankonelabs/livid-svg @rankonelabs/livid-core
15
+ ```
16
+
17
+ Core is a peer dependency, and the only one. Nothing is imported at runtime —
18
+ this package emits zero dependencies of its own.
19
+
20
+ ## Use
21
+
22
+ ```ts
23
+ import { layout, validateDiagram } from '@rankonelabs/livid-core'
24
+ import { renderSvg } from '@rankonelabs/livid-svg'
25
+
26
+ const valid = validateDiagram(registry, spec)
27
+ if (!valid.ok) return valid.error
28
+
29
+ const laid = await layout(valid.value)
30
+ if (!laid.ok) return laid.error
31
+
32
+ const svg = renderSvg(laid.value, {
33
+ title: 'Refund approval at HITL',
34
+ theme: { palette: { lines: { 'line-critical': '#D64500', 'line-parallel': '#1B6CA8' } } },
35
+ })
36
+ ```
37
+
38
+ ## Labels
39
+
40
+ Boxes carry their own text. Circles and diamonds cannot — core holds them to a
41
+ fixed width because growing one distorts it past recognition, and shape carries
42
+ type — so their labels sit alongside with a leader tick, which is also how a
43
+ transit map names a junction. Set `metrics.labelSide` to `right` for a
44
+ top-to-bottom layout.
45
+
46
+ Text is estimated rather than measured (there is no DOM at build time) and the
47
+ estimate errs wide: a label with room to spare reads fine, a clipped one does
48
+ not. The viewport always includes outside labels, so nothing is cut off.
49
+
50
+ ## Colour
51
+
52
+ `LineSpec.color` is a token, not a colour — core has no palette. Configure
53
+ tokens through `palette.lines`; any token left unconfigured takes a colour from
54
+ `palette.ramp` by line order, so an unthemed diagram still renders as a map
55
+ rather than one grey tangle.
56
+
57
+ An edge takes the colour of where it is *going*. Track between two stations on
58
+ one line is that line; track leaving a router onto another line already belongs
59
+ to the new one, which is what makes an interchange read as a change. Edges that
60
+ cross lines are drawn at `branchWeight`, lighter than the track they leave.
61
+
62
+ ## Drill-down
63
+
64
+ Levels are stacked, not made interactive: a static file has to contain every
65
+ level it can reveal, and stacking reveals them without script. Pass
66
+ `levels: 'root'` to draw only the top. Descending on demand belongs to the React
67
+ renderer.
68
+
69
+ Feed it `layoutDeep()` output if you want nested levels to have geometry.
70
+
71
+ ## Configuration
72
+
73
+ Everything visual is config — `palette`, `typography`, `metrics` — and nothing
74
+ is component injection. Both renderers are closed, which is what guarantees they
75
+ agree.
76
+
77
+ MIT.
@@ -0,0 +1 @@
1
+ {"root":["../src/geometry.ts","../src/index.ts","../src/render.ts","../src/shapes.ts","../src/theme.ts"],"version":"5.9.3"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Where things sit. Core lays out the graph; this works out what the graph's
3
+ * *labels* need on top of that, which core cannot know because it has no
4
+ * opinion about type.
5
+ */
6
+ import type { NodeShape, Point, Size } from '@rankonelabs/livid-core';
7
+ import type { Metrics, Typography } from './theme.js';
8
+ export interface Box {
9
+ readonly minX: number;
10
+ readonly minY: number;
11
+ readonly maxX: number;
12
+ readonly maxY: number;
13
+ }
14
+ export declare function boxOf(position: Point, size: Size): Box;
15
+ export declare const EMPTY_BOX: Box;
16
+ export declare function unionOf(boxes: readonly Box[]): Box;
17
+ export declare function boxAround(points: readonly Point[], slack: number): Box;
18
+ /**
19
+ * Text is estimated, not measured: rendering happens at build time with no DOM
20
+ * and no font metrics. The estimate only has to be good enough to reserve
21
+ * space, and it errs wide — a label with room to spare reads fine, a clipped
22
+ * one does not.
23
+ */
24
+ export declare function estimateTextWidth(text: string, fontSize: number, advanceRatio: number): number;
25
+ /**
26
+ * Whether a shape can carry its own label.
27
+ *
28
+ * Circles and diamonds cannot: core holds them to a fixed width because
29
+ * growing one distorts it past recognition, and shape carries type. So their
30
+ * names sit alongside, which is also how a transit map names a junction.
31
+ */
32
+ export type LabelPlacement = 'inside' | 'beside';
33
+ export declare function labelPlacementOf(shape: NodeShape): LabelPlacement;
34
+ export interface PlacedLabel {
35
+ readonly text: string;
36
+ readonly x: number;
37
+ readonly y: number;
38
+ readonly anchor: 'middle' | 'start';
39
+ /** Space the label occupies, which the viewport has to include. */
40
+ readonly box: Box;
41
+ /** Tick from the shape out to the label; absent when the label is inside. */
42
+ readonly leader: readonly [Point, Point] | null;
43
+ }
44
+ export declare function placeLabel(text: string, shape: NodeShape, position: Point, size: Size, typography: Typography, metrics: Metrics): PlacedLabel;
45
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEtE,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,GAAG,CAOtD;AAED,eAAO,MAAM,SAAS,EAAE,GAA0E,CAAC;AAEnG,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,EAAE,GAAG,GAAG,CAUlD;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,CAStE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAE9F;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAIjD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,GAAG,cAAc,CAEjE;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC;IACpC,mEAAmE;IACnE,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC;CACjD;AAED,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,KAAK,EACf,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,OAAO,GACf,WAAW,CAoDb"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Where things sit. Core lays out the graph; this works out what the graph's
3
+ * *labels* need on top of that, which core cannot know because it has no
4
+ * opinion about type.
5
+ */
6
+ export function boxOf(position, size) {
7
+ return {
8
+ minX: position.x,
9
+ minY: position.y,
10
+ maxX: position.x + size.width,
11
+ maxY: position.y + size.height,
12
+ };
13
+ }
14
+ export const EMPTY_BOX = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
15
+ export function unionOf(boxes) {
16
+ return boxes.reduce((left, right) => ({
17
+ minX: Math.min(left.minX, right.minX),
18
+ minY: Math.min(left.minY, right.minY),
19
+ maxX: Math.max(left.maxX, right.maxX),
20
+ maxY: Math.max(left.maxY, right.maxY),
21
+ }), EMPTY_BOX);
22
+ }
23
+ export function boxAround(points, slack) {
24
+ return unionOf(points.map((point) => ({
25
+ minX: point.x - slack,
26
+ minY: point.y - slack,
27
+ maxX: point.x + slack,
28
+ maxY: point.y + slack,
29
+ })));
30
+ }
31
+ /**
32
+ * Text is estimated, not measured: rendering happens at build time with no DOM
33
+ * and no font metrics. The estimate only has to be good enough to reserve
34
+ * space, and it errs wide — a label with room to spare reads fine, a clipped
35
+ * one does not.
36
+ */
37
+ export function estimateTextWidth(text, fontSize, advanceRatio) {
38
+ return text.length * fontSize * advanceRatio;
39
+ }
40
+ const POINT_SHAPES = new Set(['circle', 'diamond']);
41
+ export function labelPlacementOf(shape) {
42
+ return POINT_SHAPES.has(shape) ? 'beside' : 'inside';
43
+ }
44
+ export function placeLabel(text, shape, position, size, typography, metrics) {
45
+ const width = estimateTextWidth(text, typography.labelSize, typography.advanceRatio);
46
+ const height = typography.labelSize;
47
+ const centreX = position.x + size.width / 2;
48
+ const centreY = position.y + size.height / 2;
49
+ if (labelPlacementOf(shape) === 'inside') {
50
+ // Dominant-baseline is unreliable across renderers, so the baseline is
51
+ // nudged by a third of the cap height instead.
52
+ const y = centreY + height / 3;
53
+ return {
54
+ text,
55
+ x: centreX,
56
+ y,
57
+ anchor: 'middle',
58
+ box: boxOf(position, size),
59
+ leader: null,
60
+ };
61
+ }
62
+ if (metrics.labelSide === 'right') {
63
+ const startX = position.x + size.width + metrics.leader + metrics.labelGap;
64
+ return {
65
+ text,
66
+ x: startX,
67
+ y: centreY + height / 3,
68
+ anchor: 'start',
69
+ box: { minX: startX, minY: centreY - height, maxX: startX + width, maxY: centreY + height },
70
+ leader: [
71
+ { x: position.x + size.width, y: centreY },
72
+ { x: position.x + size.width + metrics.leader, y: centreY },
73
+ ],
74
+ };
75
+ }
76
+ const baseline = position.y + size.height + metrics.leader + metrics.labelGap + height;
77
+ return {
78
+ text,
79
+ x: centreX,
80
+ y: baseline,
81
+ anchor: 'middle',
82
+ box: {
83
+ minX: centreX - width / 2,
84
+ minY: position.y + size.height,
85
+ maxX: centreX + width / 2,
86
+ maxY: baseline + metrics.labelGap,
87
+ },
88
+ leader: [
89
+ { x: centreX, y: position.y + size.height },
90
+ { x: centreX, y: position.y + size.height + metrics.leader },
91
+ ],
92
+ };
93
+ }
94
+ //# sourceMappingURL=geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.js","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @rankonelabs/livid-svg — `LaidOutDiagram` to an SVG string.
3
+ *
4
+ * Build-time, zero client JS. Geometry comes from core already computed, so
5
+ * this package measures nothing and lays out nothing; it draws.
6
+ */
7
+ export { type SvgOptions, renderSvg, escapeText, escapeAttr } from './render.js';
8
+ export { type LabelSide, type Palette, type Typography, type Metrics, type SvgTheme, type SvgThemeOverrides, DEFAULT_PALETTE, DEFAULT_TYPOGRAPHY, DEFAULT_METRICS, DEFAULT_THEME, resolveTheme, lineColour, } from './theme.js';
9
+ export { type Box, type LabelPlacement, type PlacedLabel, boxOf, unionOf, boxAround, estimateTextWidth, labelPlacementOf, placeLabel, } from './geometry.js';
10
+ export { type ShapeStyle, shapeMarkup, glyphMarkup } from './shapes.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEjF,OAAO,EACL,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,KAAK,GAAG,EACR,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,EACL,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,GACX,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @rankonelabs/livid-svg — `LaidOutDiagram` to an SVG string.
3
+ *
4
+ * Build-time, zero client JS. Geometry comes from core already computed, so
5
+ * this package measures nothing and lays out nothing; it draws.
6
+ */
7
+ export { renderSvg, escapeText, escapeAttr } from './render.js';
8
+ export { DEFAULT_PALETTE, DEFAULT_TYPOGRAPHY, DEFAULT_METRICS, DEFAULT_THEME, resolveTheme, lineColour, } from './theme.js';
9
+ export { boxOf, unionOf, boxAround, estimateTextWidth, labelPlacementOf, placeLabel, } from './geometry.js';
10
+ export { shapeMarkup, glyphMarkup } from './shapes.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAmB,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEjF,OAAO,EAOL,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,YAAY,CAAC;AAEpB,OAAO,EAIL,KAAK,EACL,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,GACX,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAmB,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `LaidOutDiagram` to an SVG string.
3
+ *
4
+ * Build-time only: no DOM, no client JS, and nothing measured — geometry
5
+ * arrives from core already computed. The renderer's whole job is turning
6
+ * placed boxes and routes into markup, plus the one thing core cannot know,
7
+ * which is where labels go.
8
+ *
9
+ * Drill-down levels are stacked rather than made interactive. A static file
10
+ * has to contain every level it can reveal, and stacking reveals them without
11
+ * script; the React renderer is where descending on demand belongs.
12
+ */
13
+ import type { AnyRegistry, LaidOutDiagram } from '@rankonelabs/livid-core';
14
+ import { type SvgThemeOverrides } from './theme.js';
15
+ export interface SvgOptions {
16
+ readonly theme?: SvgThemeOverrides;
17
+ /** Accessible name for the whole figure. */
18
+ readonly title?: string | null;
19
+ /** Caption above the top level. Nested levels caption themselves. */
20
+ readonly caption?: string | null;
21
+ /** `all` stacks every drill-down level present; `root` draws only the top. */
22
+ readonly levels?: 'root' | 'all';
23
+ }
24
+ export declare function renderSvg<R extends AnyRegistry>(diagram: LaidOutDiagram<R>, options?: SvgOptions): string;
25
+ export declare function escapeText(value: string): string;
26
+ export declare function escapeAttr(value: string): string;
27
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAmC,MAAM,yBAAyB,CAAC;AAI5G,OAAO,EAAiB,KAAK,iBAAiB,EAA4B,MAAM,YAAY,CAAC;AAE7F,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC;IACnC,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAClC;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,WAAW,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,UAAe,GAAG,MAAM,CAsD7G;AAmKD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD"}
package/dist/render.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * `LaidOutDiagram` to an SVG string.
3
+ *
4
+ * Build-time only: no DOM, no client JS, and nothing measured — geometry
5
+ * arrives from core already computed. The renderer's whole job is turning
6
+ * placed boxes and routes into markup, plus the one thing core cannot know,
7
+ * which is where labels go.
8
+ *
9
+ * Drill-down levels are stacked rather than made interactive. A static file
10
+ * has to contain every level it can reveal, and stacking reveals them without
11
+ * script; the React renderer is where descending on demand belongs.
12
+ */
13
+ import { boxAround, boxOf, placeLabel, unionOf } from './geometry.js';
14
+ import { glyphMarkup, shapeMarkup } from './shapes.js';
15
+ import { lineColour, resolveTheme } from './theme.js';
16
+ export function renderSvg(diagram, options = {}) {
17
+ const theme = resolveTheme(options.theme);
18
+ const { padding, levelGap } = theme.metrics;
19
+ const levels = levelsOf(diagram, options.caption ?? null, options.levels !== 'root');
20
+ const rendered = levels.map((level) => renderLevel(level, theme));
21
+ const captionHeight = theme.typography.captionSize + theme.metrics.labelGap * 2;
22
+ const width = Math.max(...rendered.map((level) => level.box.maxX - level.box.minX), 0) + padding * 2;
23
+ const placed = rendered.map((level, index) => {
24
+ const above = rendered
25
+ .slice(0, index)
26
+ .reduce((total, previous) => total + heightOf(previous, captionHeight) + levelGap, 0);
27
+ return { ...level, top: padding + above };
28
+ });
29
+ const height = placed.reduce((total, level) => total + heightOf(level, captionHeight) + levelGap, 0) - levelGap + padding * 2;
30
+ const body = placed
31
+ .map((level) => {
32
+ const captionY = level.top + theme.typography.captionSize;
33
+ const contentTop = level.caption === null ? level.top : level.top + captionHeight;
34
+ const shiftX = padding - level.box.minX;
35
+ const shiftY = contentTop - level.box.minY;
36
+ return [
37
+ level.caption === null
38
+ ? ''
39
+ : `<text x="${padding}" y="${round(captionY)}" font-family="${escapeAttr(theme.typography.captionFamily)}" ` +
40
+ `font-size="${theme.typography.captionSize}" fill="${theme.palette.caption}">${escapeText(level.caption)}</text>`,
41
+ `<g transform="translate(${round(shiftX)},${round(shiftY)})">`,
42
+ level.markup,
43
+ `</g>`,
44
+ ]
45
+ .filter((part) => part !== '')
46
+ .join('\n');
47
+ })
48
+ .join(`\n<line x1="${padding}" x2="${round(width - padding)}" y1="0" y2="0" stroke="${theme.palette.divider}" stroke-width="0"/>\n`);
49
+ const title = options.title ?? null;
50
+ return [
51
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${round(width)}" height="${round(height)}" ` +
52
+ `viewBox="0 0 ${round(width)} ${round(height)}" role="img"${title === null ? '' : ` aria-label="${escapeAttr(title)}"`}>`,
53
+ title === null ? '' : `<title>${escapeText(title)}</title>`,
54
+ `<rect width="100%" height="100%" fill="${theme.palette.surface}"/>`,
55
+ body,
56
+ `</svg>`,
57
+ ]
58
+ .filter((part) => part !== '')
59
+ .join('\n');
60
+ }
61
+ /** The tree of drill-down levels, flattened depth-first into a stack. */
62
+ function levelsOf(diagram, caption, deep) {
63
+ const here = { caption, diagram };
64
+ if (!deep)
65
+ return [here];
66
+ const nested = diagram.nodes.flatMap((node) => node.children === null
67
+ ? []
68
+ : levelsOf(node.children, caption === null ? node.node.label : `${caption} ▸ ${node.node.label}`, true));
69
+ return [here, ...nested];
70
+ }
71
+ function heightOf(level, captionHeight) {
72
+ const content = level.box.maxY - level.box.minY;
73
+ return level.caption === null ? content : content + captionHeight;
74
+ }
75
+ /* ------------------------------------------------------------------ *
76
+ * One level
77
+ * ------------------------------------------------------------------ */
78
+ function renderLevel(level, theme) {
79
+ const { diagram } = level;
80
+ const colours = new Map(diagram.lines.map((line, index) => [line.id, lineColour(theme.palette, line.color, index)]));
81
+ const colourOf = (line) => (line === null ? undefined : colours.get(line)) ?? theme.palette.caption;
82
+ const lineOfNode = new Map(diagram.nodes.map((node) => [node.node.id, node.node.line]));
83
+ const edges = diagram.edges.map((edge) => renderEdge(edge, lineOfNode, colourOf, theme));
84
+ const nodes = diagram.nodes.map((node) => renderNode(node, diagram, colourOf, theme));
85
+ const box = unionOf([...edges.map((part) => part.box), ...nodes.map((part) => part.box)]);
86
+ // Edges first so tracks pass under stations rather than over them.
87
+ return {
88
+ caption: level.caption,
89
+ markup: [...edges.map((part) => part.markup), ...nodes.map((part) => part.markup)].join('\n'),
90
+ box,
91
+ };
92
+ }
93
+ /**
94
+ * An edge takes the colour of where it is going. Track between two stations on
95
+ * one line is that line; track leaving a router onto another line already
96
+ * belongs to the new one, which is what makes an interchange read as a change.
97
+ */
98
+ function renderEdge(edge, lineOfNode, colourOf, theme) {
99
+ const sourceLine = lineOfNode.get(edge.edge.source) ?? null;
100
+ const targetLine = lineOfNode.get(edge.edge.target) ?? null;
101
+ const branching = sourceLine !== targetLine;
102
+ const weight = branching ? theme.metrics.branchWeight : theme.metrics.lineWeight;
103
+ if (edge.route.length < 2)
104
+ return { markup: '', box: { ...EMPTY } };
105
+ const points = edge.route.map((point) => `${round(point.x)},${round(point.y)}`).join(' ');
106
+ return {
107
+ markup: `<polyline points="${points}" fill="none" stroke="${colourOf(targetLine)}" stroke-width="${weight}" ` +
108
+ `stroke-linejoin="round" stroke-linecap="round"/>`,
109
+ box: boxAround(edge.route, weight / 2),
110
+ };
111
+ }
112
+ const EMPTY = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
113
+ function renderNode(placed, diagram, colourOf, theme) {
114
+ const typeDef = diagram.registry.nodeTypes[placed.node.type];
115
+ const shape = typeDef?.shape ?? 'rect';
116
+ const glyph = typeDef?.glyph ?? 'none';
117
+ const colour = colourOf(placed.node.line);
118
+ const box = boxOf(placed.position, placed.size);
119
+ const label = placeLabel(placed.node.label, shape, placed.position, placed.size, theme.typography, theme.metrics);
120
+ const leader = label.leader === null
121
+ ? ''
122
+ : `<line x1="${round(label.leader[0].x)}" y1="${round(label.leader[0].y)}" ` +
123
+ `x2="${round(label.leader[1].x)}" y2="${round(label.leader[1].y)}" ` +
124
+ `stroke="${colour}" stroke-width="${theme.metrics.nodeStroke}" stroke-linecap="round"/>`;
125
+ const description = typeDef === undefined ? placed.node.label : `${placed.node.label} — ${typeDef.label}`;
126
+ return {
127
+ markup: [
128
+ `<g>`,
129
+ `<title>${escapeText(description)}</title>`,
130
+ shapeMarkup(shape, box, {
131
+ fill: theme.palette.nodeFill,
132
+ stroke: colour,
133
+ strokeWidth: theme.metrics.nodeStroke,
134
+ }),
135
+ glyph === 'none' ? '' : glyphMarkup(glyph, box, colour),
136
+ leader,
137
+ `<text x="${round(label.x)}" y="${round(label.y)}" text-anchor="${label.anchor}" ` +
138
+ `font-family="${escapeAttr(theme.typography.fontFamily)}" font-size="${theme.typography.labelSize}" ` +
139
+ `font-weight="${theme.typography.labelWeight}" fill="${theme.palette.label}">${escapeText(label.text)}</text>`,
140
+ `</g>`,
141
+ ]
142
+ .filter((part) => part !== '')
143
+ .join('\n'),
144
+ box: unionOf([box, label.box]),
145
+ };
146
+ }
147
+ /* ------------------------------------------------------------------ *
148
+ * Markup helpers
149
+ * ------------------------------------------------------------------ */
150
+ const round = (value) => (Math.round(value * 100) / 100).toString();
151
+ const TEXT_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
152
+ const ATTR_ESCAPES = { ...TEXT_ESCAPES, '"': '&quot;', "'": '&#39;' };
153
+ export function escapeText(value) {
154
+ return value.replace(/[&<>]/g, (character) => TEXT_ESCAPES[character] ?? character);
155
+ }
156
+ export function escapeAttr(value) {
157
+ return value.replace(/[&<>"']/g, (character) => ATTR_ESCAPES[character] ?? character);
158
+ }
159
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shape and glyph markup. One function per closed vocabulary in core, so a
3
+ * shape core can name is always a shape this can draw — which is the whole
4
+ * reason `NodeShape` and `Glyph` are closed sets.
5
+ */
6
+ import type { Glyph, NodeShape } from '@rankonelabs/livid-core';
7
+ import type { Box } from './geometry.js';
8
+ export interface ShapeStyle {
9
+ readonly fill: string;
10
+ readonly stroke: string;
11
+ readonly strokeWidth: number;
12
+ }
13
+ export declare function shapeMarkup(shape: NodeShape, box: Box, style: ShapeStyle): string;
14
+ /**
15
+ * The station mark. Drawn at the leading edge rather than the centre so it
16
+ * does not fight the label for the middle of a box.
17
+ */
18
+ export declare function glyphMarkup(glyph: Glyph, box: Box, colour: string): string;
19
+ //# sourceMappingURL=shapes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shapes.d.ts","sourceRoot":"","sources":["../src/shapes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAQD,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CA+CjF;AAMD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAqB1E"}
package/dist/shapes.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Shape and glyph markup. One function per closed vocabulary in core, so a
3
+ * shape core can name is always a shape this can draw — which is the whole
4
+ * reason `NodeShape` and `Glyph` are closed sets.
5
+ */
6
+ function attrs(style) {
7
+ return `fill="${style.fill}" stroke="${style.stroke}" stroke-width="${style.strokeWidth}"`;
8
+ }
9
+ const round = (value) => (Math.round(value * 100) / 100).toString();
10
+ export function shapeMarkup(shape, box, style) {
11
+ const x = box.minX;
12
+ const y = box.minY;
13
+ const w = box.maxX - box.minX;
14
+ const h = box.maxY - box.minY;
15
+ const common = attrs(style);
16
+ switch (shape) {
17
+ case 'rect':
18
+ return `<rect x="${round(x)}" y="${round(y)}" width="${round(w)}" height="${round(h)}" ${common}/>`;
19
+ case 'rounded':
20
+ return `<rect x="${round(x)}" y="${round(y)}" width="${round(w)}" height="${round(h)}" rx="10" ${common}/>`;
21
+ case 'stadium':
22
+ return `<rect x="${round(x)}" y="${round(y)}" width="${round(w)}" height="${round(h)}" rx="${round(h / 2)}" ${common}/>`;
23
+ case 'circle':
24
+ return `<circle cx="${round(x + w / 2)}" cy="${round(y + h / 2)}" r="${round(Math.min(w, h) / 2)}" ${common}/>`;
25
+ case 'hexagon': {
26
+ const inset = w * 0.18;
27
+ const points = [
28
+ [x + inset, y],
29
+ [x + w - inset, y],
30
+ [x + w, y + h / 2],
31
+ [x + w - inset, y + h],
32
+ [x + inset, y + h],
33
+ [x, y + h / 2],
34
+ ];
35
+ return `<polygon points="${polygon(points)}" ${common}/>`;
36
+ }
37
+ case 'diamond': {
38
+ const points = [
39
+ [x + w / 2, y],
40
+ [x + w, y + h / 2],
41
+ [x + w / 2, y + h],
42
+ [x, y + h / 2],
43
+ ];
44
+ return `<polygon points="${polygon(points)}" ${common}/>`;
45
+ }
46
+ case 'cylinder': {
47
+ const lip = 9;
48
+ return (`<path d="M${round(x)},${round(y + lip)} a${round(w / 2)},${lip} 0 0 1 ${round(w)},0 ` +
49
+ `v${round(h - lip * 2)} a${round(w / 2)},${lip} 0 0 1 ${round(-w)},0 z" ${common}/>` +
50
+ `<ellipse cx="${round(x + w / 2)}" cy="${round(y + lip)}" rx="${round(w / 2)}" ry="${lip}" ` +
51
+ `fill="none" stroke="${style.stroke}" stroke-width="${style.strokeWidth}"/>`);
52
+ }
53
+ }
54
+ }
55
+ function polygon(points) {
56
+ return points.map((point) => `${round(point[0] ?? 0)},${round(point[1] ?? 0)}`).join(' ');
57
+ }
58
+ /**
59
+ * The station mark. Drawn at the leading edge rather than the centre so it
60
+ * does not fight the label for the middle of a box.
61
+ */
62
+ export function glyphMarkup(glyph, box, colour) {
63
+ if (glyph === 'none')
64
+ return '';
65
+ const cx = box.minX + (box.maxX - box.minX) / 2;
66
+ const cy = box.minY + (box.maxY - box.minY) / 2;
67
+ switch (glyph) {
68
+ case 'dot':
69
+ return `<circle cx="${round(cx)}" cy="${round(cy)}" r="4" fill="${colour}"/>`;
70
+ case 'ring':
71
+ return `<circle cx="${round(cx)}" cy="${round(cy)}" r="4.5" fill="none" stroke="${colour}" stroke-width="2"/>`;
72
+ case 'bar':
73
+ return `<rect x="${round(cx - 1.5)}" y="${round(cy - 6)}" width="3" height="12" rx="1.5" fill="${colour}"/>`;
74
+ case 'square':
75
+ return `<rect x="${round(cx - 3.5)}" y="${round(cy - 3.5)}" width="7" height="7" fill="${colour}"/>`;
76
+ case 'chevron':
77
+ return (`<path d="M${round(cx - 3)},${round(cy - 5)} L${round(cx + 3)},${round(cy)} L${round(cx - 3)},${round(cy + 5)}" ` +
78
+ `fill="none" stroke="${colour}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`);
79
+ }
80
+ }
81
+ //# sourceMappingURL=shapes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shapes.js","sourceRoot":"","sources":["../src/shapes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Everything about how a diagram looks, as config.
3
+ *
4
+ * User control stops at config here as it does in core: a consumer picks
5
+ * colours, weights, and type, and cannot inject markup. That is what keeps the
6
+ * SVG in a post and the React canvas in an app the same map.
7
+ */
8
+ /** Where a label sits when its shape cannot hold text. */
9
+ export type LabelSide = 'below' | 'right';
10
+ export interface Palette {
11
+ /**
12
+ * Colour token from `LineSpec.color` to a CSS colour. Tokens are the
13
+ * consumer's vocabulary — core never sees a colour — so any token missing
14
+ * here falls back to the ramp below, by line order.
15
+ */
16
+ readonly lines: Readonly<Record<string, string>>;
17
+ /** Assigned by line index to any line whose token is not in `lines`. */
18
+ readonly ramp: readonly string[];
19
+ readonly surface: string;
20
+ readonly nodeFill: string;
21
+ readonly label: string;
22
+ readonly caption: string;
23
+ readonly divider: string;
24
+ }
25
+ export interface Typography {
26
+ readonly fontFamily: string;
27
+ readonly labelSize: number;
28
+ readonly labelWeight: number;
29
+ readonly captionFamily: string;
30
+ readonly captionSize: number;
31
+ /**
32
+ * Mean glyph advance as a fraction of font size. Text is measured by
33
+ * estimate rather than measurement because there is no DOM at build time;
34
+ * see `estimateTextWidth`.
35
+ */
36
+ readonly advanceRatio: number;
37
+ }
38
+ export interface Metrics {
39
+ /** Track weight on the critical path. Thick lines are what read as transit. */
40
+ readonly lineWeight: number;
41
+ /** Branches are lighter, so the eye follows the main route first. */
42
+ readonly branchWeight: number;
43
+ readonly nodeStroke: number;
44
+ /** Tick drawn from a point-shaped node out to its label. */
45
+ readonly leader: number;
46
+ readonly labelGap: number;
47
+ readonly padding: number;
48
+ /** Vertical space between stacked drill-down levels. */
49
+ readonly levelGap: number;
50
+ readonly labelSide: LabelSide;
51
+ }
52
+ export interface SvgTheme {
53
+ readonly palette: Palette;
54
+ readonly typography: Typography;
55
+ readonly metrics: Metrics;
56
+ }
57
+ /** Overrides are per-section rather than deep, so the shapes stay nameable. */
58
+ export interface SvgThemeOverrides {
59
+ readonly palette?: Partial<Palette>;
60
+ readonly typography?: Partial<Typography>;
61
+ readonly metrics?: Partial<Metrics>;
62
+ }
63
+ /**
64
+ * Restrained and high-contrast: the map should read on a white page in a post
65
+ * and survive being printed. Deliberately not any existing brand palette.
66
+ */
67
+ export declare const DEFAULT_PALETTE: Palette;
68
+ export declare const DEFAULT_TYPOGRAPHY: Typography;
69
+ export declare const DEFAULT_METRICS: Metrics;
70
+ export declare const DEFAULT_THEME: SvgTheme;
71
+ export declare function resolveTheme(overrides?: SvgThemeOverrides): SvgTheme;
72
+ /**
73
+ * Colour for one line. A configured token wins; otherwise the ramp assigns by
74
+ * declaration order, so an unconfigured diagram still renders as a map rather
75
+ * than as one grey tangle.
76
+ */
77
+ export declare function lineColour(palette: Palette, token: string, index: number): string;
78
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1C,MAAM,WAAW,OAAO;IACtB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,OAAO;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,OAQ7B,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,UAOhC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,OAS7B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,QAI3B,CAAC;AAEF,wBAAgB,YAAY,CAAC,SAAS,GAAE,iBAAsB,GAAG,QAAQ,CAMxE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjF"}
package/dist/theme.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Everything about how a diagram looks, as config.
3
+ *
4
+ * User control stops at config here as it does in core: a consumer picks
5
+ * colours, weights, and type, and cannot inject markup. That is what keeps the
6
+ * SVG in a post and the React canvas in an app the same map.
7
+ */
8
+ /**
9
+ * Restrained and high-contrast: the map should read on a white page in a post
10
+ * and survive being printed. Deliberately not any existing brand palette.
11
+ */
12
+ export const DEFAULT_PALETTE = {
13
+ lines: {},
14
+ ramp: ['#D64500', '#1B6CA8', '#5B3E96', '#0F7B6C', '#A8341F', '#6B7280'],
15
+ surface: '#FFFFFF',
16
+ nodeFill: '#FFFFFF',
17
+ label: '#16181D',
18
+ caption: '#6B7280',
19
+ divider: '#D8DCE3',
20
+ };
21
+ export const DEFAULT_TYPOGRAPHY = {
22
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif',
23
+ labelSize: 13,
24
+ labelWeight: 600,
25
+ captionFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
26
+ captionSize: 11,
27
+ advanceRatio: 0.55,
28
+ };
29
+ export const DEFAULT_METRICS = {
30
+ lineWeight: 7,
31
+ branchWeight: 4,
32
+ nodeStroke: 2.5,
33
+ leader: 14,
34
+ labelGap: 6,
35
+ padding: 44,
36
+ levelGap: 96,
37
+ labelSide: 'below',
38
+ };
39
+ export const DEFAULT_THEME = {
40
+ palette: DEFAULT_PALETTE,
41
+ typography: DEFAULT_TYPOGRAPHY,
42
+ metrics: DEFAULT_METRICS,
43
+ };
44
+ export function resolveTheme(overrides = {}) {
45
+ return {
46
+ palette: { ...DEFAULT_PALETTE, ...overrides.palette, lines: { ...DEFAULT_PALETTE.lines, ...overrides.palette?.lines } },
47
+ typography: { ...DEFAULT_TYPOGRAPHY, ...overrides.typography },
48
+ metrics: { ...DEFAULT_METRICS, ...overrides.metrics },
49
+ };
50
+ }
51
+ /**
52
+ * Colour for one line. A configured token wins; otherwise the ramp assigns by
53
+ * declaration order, so an unconfigured diagram still renders as a map rather
54
+ * than as one grey tangle.
55
+ */
56
+ export function lineColour(palette, token, index) {
57
+ const configured = palette.lines[token];
58
+ if (configured !== undefined)
59
+ return configured;
60
+ const fallback = palette.ramp[index % palette.ramp.length];
61
+ return fallback ?? palette.caption;
62
+ }
63
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA+DH;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAY;IACtC,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC;IACxE,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,SAAS;IACnB,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;CACnB,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAe;IAC5C,UAAU,EAAE,uEAAuE;IACnF,SAAS,EAAE,EAAE;IACb,WAAW,EAAE,GAAG;IAChB,aAAa,EAAE,gDAAgD;IAC/D,WAAW,EAAE,EAAE;IACf,YAAY,EAAE,IAAI;CACnB,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAY;IACtC,UAAU,EAAE,CAAC;IACb,YAAY,EAAE,CAAC;IACf,UAAU,EAAE,GAAG;IACf,MAAM,EAAE,EAAE;IACV,QAAQ,EAAE,CAAC;IACX,OAAO,EAAE,EAAE;IACX,QAAQ,EAAE,EAAE;IACZ,SAAS,EAAE,OAAO;CACnB,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAa;IACrC,OAAO,EAAE,eAAe;IACxB,UAAU,EAAE,kBAAkB;IAC9B,OAAO,EAAE,eAAe;CACzB,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,YAA+B,EAAE;IAC5D,OAAO;QACL,OAAO,EAAE,EAAE,GAAG,eAAe,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,GAAG,eAAe,CAAC,KAAK,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;QACvH,UAAU,EAAE,EAAE,GAAG,kBAAkB,EAAE,GAAG,SAAS,CAAC,UAAU,EAAE;QAC9D,OAAO,EAAE,EAAE,GAAG,eAAe,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE;KACtD,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAAgB,EAAE,KAAa,EAAE,KAAa;IACvE,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACxC,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAEhD,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3D,OAAO,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;AACrC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@rankonelabs/livid-svg",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "LaidOutDiagram to SVG string. Build-time, zero client JS.",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "diagram",
9
+ "svg",
10
+ "architecture",
11
+ "static-site"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/RankOneLabs/livid.git",
16
+ "directory": "packages/svg"
17
+ },
18
+ "homepage": "https://github.com/RankOneLabs/livid#readme",
19
+ "bugs": "https://github.com/RankOneLabs/livid/issues",
20
+ "engines": {
21
+ "node": ">=20.19.0"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -b",
39
+ "check": "tsc -b",
40
+ "prepublishOnly": "tsc -b --force"
41
+ },
42
+ "peerDependencies": {
43
+ "@rankonelabs/livid-core": "^0.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@rankonelabs/livid-core": "^0.1.0"
47
+ }
48
+ }