@loadmap/react 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +75 -2
- package/dist/elk.worker.js +14 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.js +2070 -0
- package/package.json +42 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jaime Rojas
|
|
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
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @loadmap/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Interactive capacity models for React. Declare the components of a system, the flows through it and what each one
|
|
4
|
+
can take; `<Loadmap>` draws the architecture with moving dots, ranks the components by when they run out, and writes
|
|
5
|
+
the verdict.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @loadmap/react react react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import {
|
|
13
|
+
defineModel,
|
|
14
|
+
defineView,
|
|
15
|
+
edge,
|
|
16
|
+
estimated,
|
|
17
|
+
Loadmap,
|
|
18
|
+
measured,
|
|
19
|
+
node,
|
|
20
|
+
p,
|
|
21
|
+
pool,
|
|
22
|
+
store,
|
|
23
|
+
workers,
|
|
24
|
+
} from '@loadmap/react'
|
|
25
|
+
|
|
26
|
+
// the model: plain TypeScript, no React
|
|
27
|
+
const shop = defineModel({
|
|
28
|
+
title: 'Web shop',
|
|
29
|
+
load: p.load({ label: 'Visitors, × today', max: 50, default: 1 }),
|
|
30
|
+
params: { pods: p.select({ label: 'App pods', options: [2, 4, 8], default: 2 }) },
|
|
31
|
+
flows: { requests: { label: 'requests', rate: ({ load }) => 300 * load, unit: 'request' } },
|
|
32
|
+
components: {
|
|
33
|
+
app: {
|
|
34
|
+
name: 'App pods',
|
|
35
|
+
short: 'the app pods',
|
|
36
|
+
plural: true,
|
|
37
|
+
demand: ({ flows }) => flows.requests * 0.003,
|
|
38
|
+
capacity: ({ pods }) => measured(pods * 2, 'cores', 'pod CPU limit'),
|
|
39
|
+
over: 'requests wait and time out',
|
|
40
|
+
},
|
|
41
|
+
db: {
|
|
42
|
+
name: 'Postgres',
|
|
43
|
+
short: 'the database',
|
|
44
|
+
demand: ({ flows }) => flows.requests * 2,
|
|
45
|
+
capacity: estimated(5_000, 'queries/s', { source: 'last year’s load test', within: 0.3 }),
|
|
46
|
+
over: 'queries queue on the primary',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
stages: { app: workers('app', { pods: ({ pods }) => pods, perPod: 4 }), db: store('db') },
|
|
50
|
+
routes: { requests: ['app', 'db'] },
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// the view: which boxes to draw and where the dots start
|
|
54
|
+
const view = defineView(shop, {
|
|
55
|
+
nodes: {
|
|
56
|
+
users: node({ label: 'Browsers' }),
|
|
57
|
+
app: pool({ label: 'App', sub: ({ pods }) => `${pods} pods`, stage: 'app' }),
|
|
58
|
+
db: node({ label: 'Postgres', stage: 'db', shape: 'cylinder' }),
|
|
59
|
+
},
|
|
60
|
+
edges: [edge('users', 'app'), edge('app', 'db')],
|
|
61
|
+
flows: { requests: { from: 'users', color: 'accent' } },
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export function App() {
|
|
65
|
+
return <Loadmap model={shop} view={view} />
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Every capacity says where it came from (`measured`, `estimated`, or a fit from measurements), and `± 30 %` turns
|
|
70
|
+
each limit into a range. Add `scenarios` to compare designs, `growth` to turn multiples into dates, `expect` to fail
|
|
71
|
+
a test when headroom is lost. The layout runs in a Web Worker; nothing loads it before the first diagram.
|
|
72
|
+
|
|
73
|
+
`useLoadmap` with `LoadmapRoot`, `Diagram`, `Verdict`, `Limits`, `Controls` and the others builds a page of your own
|
|
74
|
+
layout from the same pieces.
|
|
75
|
+
|
|
76
|
+
Docs, live examples and the editor: https://jaim3r0jas.github.io/loadmap/ · MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ELK off the main thread, so a page stays responsive while a layout is computed. Plain JavaScript: the same file
|
|
2
|
+
// serves the workspace and dist, where `new URL('./elk.worker.js', import.meta.url)` finds it next to the module.
|
|
3
|
+
import ELK from 'elkjs/lib/elk.bundled.js'
|
|
4
|
+
|
|
5
|
+
const elk = new ELK()
|
|
6
|
+
|
|
7
|
+
// set after the import: elk.bundled installs its own handler when it loads in a worker
|
|
8
|
+
self.onmessage = async ({ data: { id, graph } }) => {
|
|
9
|
+
try {
|
|
10
|
+
self.postMessage({ id, graph: await elk.layout(graph) })
|
|
11
|
+
} catch (err) {
|
|
12
|
+
self.postMessage({ id, error: String(err?.message ?? err) })
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Comparison, Evaluation, Model, Scenario, Values } from "@loadmap/core";
|
|
2
|
+
import { fitLinear } from "@loadmap/core/calibrate";
|
|
3
|
+
import { compact, fmt, multiple, pct, secs } from "@loadmap/core/format";
|
|
4
|
+
import { AnyNodeDef, ClusterDef, Direction, EdgeDef, ElkEngine, FlowColor, FlowView, Label, NodeDef, PoolDef, Scene, View, View as View$1, ViewDef, cluster, defineView, edge, labelsAt, node, pool } from "@loadmap/layout";
|
|
5
|
+
import { ReactNode, Ref } from "react";
|
|
6
|
+
import { Particle, Sim, SimOptions } from "@loadmap/sim";
|
|
7
|
+
export * from "@loadmap/core";
|
|
8
|
+
/**
|
|
9
|
+
* Everything that moves, without React: the sim, the dots on the canvas and the worker fills in the SVG. React
|
|
10
|
+
* renders the structure once per scene; the engine updates the moving parts every frame, and export drives it frame
|
|
11
|
+
* by frame instead of the animation loop.
|
|
12
|
+
*/
|
|
13
|
+
declare class Engine {
|
|
14
|
+
#private;
|
|
15
|
+
readonly model: Model;
|
|
16
|
+
readonly view: View$1;
|
|
17
|
+
sim: Sim;
|
|
18
|
+
ev: Evaluation;
|
|
19
|
+
scene: Scene | null;
|
|
20
|
+
/** The structure key the current values want; `scene.key` catches up once the layout is done. */
|
|
21
|
+
wantKey: string;
|
|
22
|
+
/** The values React last drew labels and colours for; a frame is complete once they match `ev.values`. */
|
|
23
|
+
drawnValues: string;
|
|
24
|
+
/** The component a story step points at, and the one the diagram last outlined: export waits for them to match. */
|
|
25
|
+
wantFocus: string | null;
|
|
26
|
+
/** The story step shown, set as soon as one is chosen: a recorder holding an old handle still reads it. */
|
|
27
|
+
storyStep: number | null;
|
|
28
|
+
drawnFocus: string | null;
|
|
29
|
+
paused: boolean;
|
|
30
|
+
colors: Record<string, string>;
|
|
31
|
+
svg: SVGSVGElement | null;
|
|
32
|
+
canvas: HTMLCanvasElement | null;
|
|
33
|
+
constructor(model: Model, view: View$1, ev: Evaluation, o?: SimOptions);
|
|
34
|
+
setEvaluation(ev: Evaluation): void;
|
|
35
|
+
setScene(scene: Scene): void;
|
|
36
|
+
/** Starts the sim over from empty queues, with this seed (default: the original one). */
|
|
37
|
+
reset(seed?: number): void;
|
|
38
|
+
bind(svg: SVGSVGElement | null, canvas: HTMLCanvasElement | null): void;
|
|
39
|
+
/** Reads the theme tokens from the element that carries them. Call again when the theme changes. */
|
|
40
|
+
readColors(root: Element): void;
|
|
41
|
+
flowColor(flow: string): string;
|
|
42
|
+
outcomeColor(outcome: Particle['outcome']): string;
|
|
43
|
+
resize(): void;
|
|
44
|
+
step(dtReal: number, o?: {
|
|
45
|
+
particles?: boolean;
|
|
46
|
+
}): void;
|
|
47
|
+
/** Draws the dots on the overlay canvas and the worker fills in the SVG. */
|
|
48
|
+
draw(o?: {
|
|
49
|
+
particles?: boolean;
|
|
50
|
+
}): void;
|
|
51
|
+
drawFills(): void;
|
|
52
|
+
/** Every dot as it is drawn now, in scene units: for the canvas, and for an SVG export. */
|
|
53
|
+
dots(): {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
r: number;
|
|
57
|
+
color: string;
|
|
58
|
+
alpha: number;
|
|
59
|
+
}[];
|
|
60
|
+
/** Paints the dots onto any 2D context, scene units × `scale`: the overlay, or an export frame. */
|
|
61
|
+
paintParticles(ctx: CanvasRenderingContext2D, scale: number): void;
|
|
62
|
+
}
|
|
63
|
+
interface LoadmapOptions<P> extends SimOptions {
|
|
64
|
+
/** Values to start from instead of the model's defaults. */
|
|
65
|
+
initial?: Partial<Values<P>>;
|
|
66
|
+
/** Where ELK lays the diagram out. Default: one worker shared by the page, or the main thread if it cannot start. */
|
|
67
|
+
elk?: ElkEngine;
|
|
68
|
+
/** Below this container width the diagram is laid out top to bottom. Default 640. */
|
|
69
|
+
narrowBelow?: number;
|
|
70
|
+
/** Which way the diagram runs where there is room. Default RIGHT; below `narrowBelow` it is always DOWN. */
|
|
71
|
+
direction?: Direction;
|
|
72
|
+
}
|
|
73
|
+
interface LoadmapController<P = any> {
|
|
74
|
+
model: Model<P>;
|
|
75
|
+
view: View$1;
|
|
76
|
+
values: Values<P>;
|
|
77
|
+
setValues: (v: Partial<Values<P>>) => void;
|
|
78
|
+
/** The scenario the values match, load aside; null for values of the reader's own. */
|
|
79
|
+
scenario: Scenario<P> | null;
|
|
80
|
+
/** Takes a scenario's values; the load stays. */
|
|
81
|
+
setScenario: (label: string) => void;
|
|
82
|
+
/** The story step shown, by index; null outside the story. */
|
|
83
|
+
step: number | null;
|
|
84
|
+
/** The reader changed a value since the step was shown: its outline is gone. */
|
|
85
|
+
stepChanged: boolean;
|
|
86
|
+
/** Shows a story step (its values, its focus), or leaves the story with null. */
|
|
87
|
+
setStep: (i: number | null) => void;
|
|
88
|
+
/** The component the story points at now. */
|
|
89
|
+
focus: string | null;
|
|
90
|
+
ev: Evaluation<P>;
|
|
91
|
+
/** null until the first layout is done. */
|
|
92
|
+
scene: Scene | null;
|
|
93
|
+
labels: ReturnType<typeof labelsAt>;
|
|
94
|
+
direction: Direction;
|
|
95
|
+
reducedMotion: boolean;
|
|
96
|
+
engine: Engine;
|
|
97
|
+
/** Attach to the element that carries the `.lm` class: its width picks the direction, its tokens the colours. */
|
|
98
|
+
rootRef: (el: HTMLElement | null) => void;
|
|
99
|
+
root: HTMLElement | null;
|
|
100
|
+
}
|
|
101
|
+
/** The state behind a loadmap: values, the model's evaluation, the layout and the engine. */
|
|
102
|
+
export declare function useLoadmap<P>(model: Model<P>, view: View$1, o?: LoadmapOptions<P>): LoadmapController<P>;
|
|
103
|
+
/** The architecture: SVG for the structure, a canvas on top for the dots. Expands to full screen. */
|
|
104
|
+
export declare function Diagram({ lm, legend }: {
|
|
105
|
+
lm: LoadmapController;
|
|
106
|
+
legend?: boolean;
|
|
107
|
+
}): import("react").JSX.Element;
|
|
108
|
+
type Theme = 'auto' | 'light' | 'dark';
|
|
109
|
+
/** What a page (or `@loadmap/export`) can drive from outside. */
|
|
110
|
+
interface LoadmapHandle {
|
|
111
|
+
/** Resolves once the current values are laid out and drawn. */
|
|
112
|
+
ready(timeoutMs?: number): Promise<void>;
|
|
113
|
+
pause(): void;
|
|
114
|
+
resume(): void;
|
|
115
|
+
values(): Values<any>;
|
|
116
|
+
setValues(v: Partial<Values<any>>): void;
|
|
117
|
+
/** Empties the queues and dots and starts over with this seed. */
|
|
118
|
+
reset(seed?: number): void;
|
|
119
|
+
/** Advances the sim by `dtReal` seconds and draws the frame: the deterministic way to animate. */
|
|
120
|
+
step(dtReal: number): void;
|
|
121
|
+
/** Shows a story step (its values and the box it points at), or leaves the story with null. */
|
|
122
|
+
setStoryStep(i: number | null): void;
|
|
123
|
+
/** The story step shown; null outside the story. */
|
|
124
|
+
storyStep(): number | null;
|
|
125
|
+
/** The drawn board, for capturing frames. */
|
|
126
|
+
board(): {
|
|
127
|
+
svg: SVGSVGElement;
|
|
128
|
+
canvas: HTMLCanvasElement;
|
|
129
|
+
scene: Scene;
|
|
130
|
+
} | null;
|
|
131
|
+
engine: Engine;
|
|
132
|
+
}
|
|
133
|
+
/** The root element: theme tokens, styles, and the size the layout direction follows. */
|
|
134
|
+
export declare function LoadmapRoot({ lm, theme, children }: {
|
|
135
|
+
lm: LoadmapController;
|
|
136
|
+
theme?: Theme;
|
|
137
|
+
children: ReactNode;
|
|
138
|
+
}): import("react").JSX.Element;
|
|
139
|
+
interface LoadmapProps<P> extends LoadmapOptions<P> {
|
|
140
|
+
model: Model<P>;
|
|
141
|
+
view: View$1;
|
|
142
|
+
theme?: Theme;
|
|
143
|
+
/** Defaults to the model's title; false hides it. */
|
|
144
|
+
title?: ReactNode | false;
|
|
145
|
+
/** Lines added before the generated assumptions. */
|
|
146
|
+
assumptions?: readonly ReactNode[];
|
|
147
|
+
/** The headroom chart. Default true. */
|
|
148
|
+
headroom?: boolean;
|
|
149
|
+
/** The scenarios side by side, when the model declares two or more. Default true. */
|
|
150
|
+
compare?: boolean;
|
|
151
|
+
ref?: Ref<LoadmapHandle>;
|
|
152
|
+
}
|
|
153
|
+
/** The whole page piece: scenarios, diagram, verdict, headroom, live meters, limits, controls and assumptions. */
|
|
154
|
+
export declare function Loadmap<P>({ model, view, theme, title, assumptions, headroom, compare, ref, ...o }: LoadmapProps<P>): import("react").JSX.Element;
|
|
155
|
+
export declare function handleOf(lm: LoadmapController): LoadmapHandle;
|
|
156
|
+
/** The model's one-sentence answer for the current values, and which capacity to measure when it is unsure. */
|
|
157
|
+
export declare function Verdict({ lm }: {
|
|
158
|
+
lm: LoadmapController;
|
|
159
|
+
}): import("react").JSX.Element;
|
|
160
|
+
/** Each component's utilization in the order they reach the target, with where they cross it and 100 %. */
|
|
161
|
+
export declare function Limits({ lm, title }: {
|
|
162
|
+
lm: LoadmapController;
|
|
163
|
+
title?: ReactNode;
|
|
164
|
+
}): import("react").JSX.Element;
|
|
165
|
+
/** Live numbers from the sim: lag and drops per queue, busy workers, rejected requests. */
|
|
166
|
+
export declare function Meters({ lm }: {
|
|
167
|
+
lm: LoadmapController;
|
|
168
|
+
}): import("react").JSX.Element;
|
|
169
|
+
/** The load slider, the presets and every parameter, greyed out where it does not apply. */
|
|
170
|
+
export declare function Controls({ lm }: {
|
|
171
|
+
lm: LoadmapController;
|
|
172
|
+
}): import("react").JSX.Element;
|
|
173
|
+
/** What the model assumes: every capacity with its provenance, every fit, and anything the page adds. */
|
|
174
|
+
export declare function Assumptions({ lm, extra }: {
|
|
175
|
+
lm: LoadmapController;
|
|
176
|
+
extra?: readonly ReactNode[];
|
|
177
|
+
}): import("react").JSX.Element;
|
|
178
|
+
/** The colour of a scenario by label; values of the reader's own are drawn in the text colour. */
|
|
179
|
+
export declare function scenarioColor(model: Model, label: string | null): string;
|
|
180
|
+
/** The comparison at the shown load: crossings do not depend on it, the verdicts do. */
|
|
181
|
+
export declare function useComparison(lm: LoadmapController): Comparison;
|
|
182
|
+
/** One button per scenario: a reader switches designs without touching the controls. The load stays. */
|
|
183
|
+
export declare function Scenarios({ lm }: {
|
|
184
|
+
lm: LoadmapController;
|
|
185
|
+
}): import("react").JSX.Element | null;
|
|
186
|
+
/** The scenarios side by side: which buys more headroom, where each first runs out, and per component. */
|
|
187
|
+
export declare function Compare({ lm, title }: {
|
|
188
|
+
lm: LoadmapController;
|
|
189
|
+
title?: ReactNode;
|
|
190
|
+
}): import("react").JSX.Element | null;
|
|
191
|
+
/**
|
|
192
|
+
* Utilization over the load multiple. Each scenario's line is its highest utilization, so it crosses the target and
|
|
193
|
+
* 100 % exactly where its first limit does; the dots are those crossings, taken from the limits, not from the line.
|
|
194
|
+
* With a ± on a capacity, a band shows the line at every capacity's low and high bound, and a bracket on the target
|
|
195
|
+
* and 100 % lines spans where the band crosses them. The shown scenario's components are the thin lines under it.
|
|
196
|
+
* Hover or arrow keys read every line at one load.
|
|
197
|
+
*/
|
|
198
|
+
export declare function HeadroomChart({ lm, title }: {
|
|
199
|
+
lm: LoadmapController;
|
|
200
|
+
title?: ReactNode;
|
|
201
|
+
}): import("react").JSX.Element;
|
|
202
|
+
/**
|
|
203
|
+
* The model's story, a step at a time: the step's text, where it is, back and next. ← and → move through it while
|
|
204
|
+
* the bar has the keyboard focus (the sliders keep their own arrows).
|
|
205
|
+
*/
|
|
206
|
+
export declare function Story({ lm }: {
|
|
207
|
+
lm: LoadmapController;
|
|
208
|
+
}): import("react").JSX.Element | null;
|
|
209
|
+
/** Theme tokens and the layout of every piece. Scoped to `.lm`; override any `--lm-*` token to restyle. */
|
|
210
|
+
export declare const LOADMAP_CSS = "\n.lm { \n --lm-bg: #eef2f5; --lm-surface: #ffffff; --lm-fg: #17212b; --lm-muted: #5d6d7c; --lm-line: #c6d0da;\n --lm-accent: #0b6b8f; --lm-accent-soft: #d6e9f0; --lm-alt: #7a4f9a; --lm-alt-soft: #efe5f6;\n --lm-ok: #2a7f5a; --lm-warn: #b86e00; --lm-drop: #b7392e; --lm-lane: #e3e9ee;\n --lm-s1: #2a78d6; --lm-s2: #eb6834; --lm-s3: #6250d6; --lm-s4: #eda100; color-scheme: light;\n --lm-font: ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n --lm-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font: 14px/1.45 var(--lm-font); color: var(--lm-fg); background: var(--lm-bg);\n display: grid; gap: 14px; padding: 16px; min-width: 0; box-sizing: border-box; }\n@media (prefers-color-scheme: dark) { .lm:not([data-theme=\"light\"]) { \n --lm-bg: #0d141b; --lm-surface: #151e27; --lm-fg: #e4ebf1; --lm-muted: #93a3b3; --lm-line: #2a3744;\n --lm-accent: #5fb7d6; --lm-accent-soft: #163442; --lm-alt: #c39be0; --lm-alt-soft: #2b2236;\n --lm-ok: #5fc08f; --lm-warn: #e2a24a; --lm-drop: #e26a5c; --lm-lane: #1b2631;\n --lm-s1: #3987e5; --lm-s2: #d95926; --lm-s3: #9085e9; --lm-s4: #c98500; color-scheme: dark; } }\n.lm[data-theme=\"dark\"] { \n --lm-bg: #0d141b; --lm-surface: #151e27; --lm-fg: #e4ebf1; --lm-muted: #93a3b3; --lm-line: #2a3744;\n --lm-accent: #5fb7d6; --lm-accent-soft: #163442; --lm-alt: #c39be0; --lm-alt-soft: #2b2236;\n --lm-ok: #5fc08f; --lm-warn: #e2a24a; --lm-drop: #e26a5c; --lm-lane: #1b2631;\n --lm-s1: #3987e5; --lm-s2: #d95926; --lm-s3: #9085e9; --lm-s4: #c98500; color-scheme: dark; }\n.lm *, .lm *::before, .lm *::after { box-sizing: border-box; }\n.lm h2 { font-size: 1.3rem; font-weight: 600; margin: 0; text-wrap: balance; }\n.lm h3 { font: 600 .74rem var(--lm-font); text-transform: uppercase; letter-spacing: .08em; color: var(--lm-muted); margin: 0; }\n.lm button:focus-visible, .lm input:focus-visible, .lm select:focus-visible { outline: 2px solid var(--lm-accent); outline-offset: 2px; }\n.lm-card { background: var(--lm-surface); border: 1px solid var(--lm-line); border-radius: 8px; padding: 14px 16px; display: grid; gap: 12px; align-content: start; min-width: 0; }\n\n.lm-wrap { display: grid; gap: 8px; min-width: 0; position: relative; }\n.lm-wrap:fullscreen { background: var(--lm-bg); padding: 16px; align-content: center; overflow: auto; }\n.lm-bar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }\n.lm-btn { font: 500 .8rem var(--lm-font); color: var(--lm-fg); background: var(--lm-surface); border: 1px solid var(--lm-line); border-radius: 6px; padding: 6px 12px; cursor: pointer; white-space: nowrap; }\n.lm-board { position: relative; background: var(--lm-surface); border: 1px solid var(--lm-line); border-radius: 8px; overflow: hidden; margin: 0 auto; width: 100%; }\n.lm-wrap:fullscreen .lm-board { width: auto; max-width: 100%; max-height: calc(100vh - 90px); }\n.lm-board svg { display: block; width: 100%; height: auto; font-family: var(--lm-font); }\n.lm-board canvas { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }\n.lm-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: .78rem; color: var(--lm-muted); }\n.lm-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: -1px; margin-right: 5px; }\n\n.lm-verdict { margin: 0; padding: 10px 14px; border-left: 4px solid var(--lm-ok); background: var(--lm-accent-soft); border-radius: 0 6px 6px 0; }\n.lm-verdict[data-tier=\"warn\"] { border-color: var(--lm-warn); }\n.lm-verdict[data-tier=\"over\"] { border-color: var(--lm-drop); }\n.lm-measure { margin: 6px 0 0; padding: 0 14px; font-size: .86rem; color: var(--lm-muted); }\n\n.lm-strip { display: grid; grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr); gap: 14px; align-items: start; }\n@container (max-width: 860px) { .lm-strip { grid-template-columns: minmax(0, 1fr); } }\n.lm { container-type: inline-size; }\n\n.lm-meters { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }\n.lm-meter { padding: 8px 10px; border-radius: 6px; background: var(--lm-bg); min-width: 0; }\n.lm-meter b { display: block; font: 500 1.15rem var(--lm-mono); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.lm-meter span { font-size: .74rem; color: var(--lm-muted); }\n.lm-meter[data-state=\"warn\"] b { color: var(--lm-warn); } .lm-meter[data-state=\"over\"] b { color: var(--lm-drop); }\n\n.lm-limits { display: grid; gap: 10px; }\n.lm-lim, .lm-scale { display: grid; grid-template-columns: minmax(0, 14rem) minmax(0, 1fr) 10rem; gap: 4px 14px; align-items: center; }\n@container (max-width: 640px) { .lm-lim, .lm-scale { grid-template-columns: minmax(0, 1fr); } .lm-scale > :first-child, .lm-scale > :last-child { display: none; } }\n.lm-lim b { display: block; font-weight: 600; font-size: .88rem; }\n.lm-lim .lm-sub { display: block; font-size: .74rem; color: var(--lm-muted); }\n.lm-tag { font: 600 .64rem var(--lm-font); text-transform: uppercase; letter-spacing: .06em; border-radius: 3px; padding: 1px 5px; margin-left: 6px; vertical-align: 1px; }\n.lm-tag[data-kind=\"first\"] { color: var(--lm-surface); background: var(--lm-drop); }\n.lm-tag[data-kind=\"estimated\"] { color: var(--lm-warn); border: 1px solid currentColor; }\n.lm-tag[data-kind=\"range\"] { color: var(--lm-muted); border: 1px solid currentColor; }\n.lm-track { position: relative; height: 16px; background: var(--lm-bg); border-radius: 3px; overflow: hidden; }\n.lm-fill { position: absolute; inset: 0 auto 0 0; background: var(--lm-ok); transition: width .25s ease; }\n.lm-fill[data-state=\"warn\"] { background: var(--lm-warn); } .lm-fill[data-state=\"over\"] { background: var(--lm-drop); }\n.lm-tick { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--lm-fg); opacity: .55; }\n.lm-val { font: 500 .8rem var(--lm-mono); font-variant-numeric: tabular-nums; }\n.lm-val small { display: block; font: 400 .72rem var(--lm-font); color: var(--lm-muted); }\n.lm-scale { font: 400 .7rem var(--lm-mono); color: var(--lm-muted); }\n.lm-scale div { position: relative; height: 12px; }\n.lm-scale div span { position: absolute; transform: translateX(-50%); white-space: nowrap; }\n\n.lm-controls label.lm-range { display: grid; gap: 4px; font-size: .84rem; }\n.lm-controls label.lm-range > span { display: flex; justify-content: space-between; gap: 8px; }\n.lm-controls output { font-family: var(--lm-mono); font-variant-numeric: tabular-nums; }\n.lm-controls input[type=range] { width: 100%; accent-color: var(--lm-accent); }\n.lm-presets { display: flex; flex-wrap: wrap; gap: 6px; }\n.lm-presets button { font: 500 .76rem var(--lm-font); background: var(--lm-accent-soft); color: var(--lm-fg); border: 1px solid transparent; border-radius: 5px; padding: 5px 10px; cursor: pointer; }\n.lm-presets button[aria-pressed=\"true\"] { border-color: var(--lm-accent); }\n.lm-switch { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 8px; align-items: start; font-size: .84rem; cursor: pointer; }\n.lm-switch input { margin: 3px 0 0; accent-color: var(--lm-accent); }\n.lm-switch small, .lm-select small { grid-column: 2; color: var(--lm-muted); font-size: .74rem; line-height: 1.3; }\n.lm-select { display: grid; gap: 4px; font-size: .84rem; }\n.lm-select select { width: 100%; min-width: 0; font: 400 .84rem var(--lm-font); color: var(--lm-fg); background: var(--lm-surface); border: 1px solid var(--lm-line); border-radius: 5px; padding: 5px 8px; }\n.lm-off { opacity: .45; cursor: default; }\n\n.lm-facts { margin: 0; padding-left: 1.1em; font-size: .88rem; columns: 2; column-gap: 28px; }\n@container (max-width: 860px) { .lm-facts { columns: 1; } }\n.lm-facts li { margin-bottom: 8px; break-inside: avoid; }\n.lm-prov { font: 600 .66rem var(--lm-font); text-transform: uppercase; letter-spacing: .06em; }\n.lm-prov[data-kind=\"measured\"] { color: var(--lm-ok); } .lm-prov[data-kind=\"estimated\"] { color: var(--lm-warn); }\n\n/* scenario identity colours (s1–s4) are checked for colour-blind separation on both surfaces; text never wears them */\n.lm-scenarios { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; border: 0; padding: 0; margin: 0; min-width: 0; }\n.lm-scenarios legend { float: left; font: 600 .74rem var(--lm-font); text-transform: uppercase; letter-spacing: .08em; color: var(--lm-muted); margin-right: 6px; padding: 0; }\n.lm-scenarios button { font: 500 .84rem var(--lm-font); color: var(--lm-fg); background: var(--lm-surface); border: 1px solid var(--lm-line); border-radius: 6px; padding: 6px 12px; cursor: pointer; }\n.lm-scenarios button[aria-pressed=\"true\"] { border-color: var(--lm-fg); box-shadow: inset 0 0 0 1px var(--lm-fg); }\n.lm-scenarios i, .lm-table i { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: -1px; margin-right: 6px; }\n.lm-story { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin: 0; min-width: 0; padding: 8px 12px; border: 1px solid var(--lm-line); border-left: 3px solid var(--lm-accent); border-radius: 6px; background: var(--lm-surface); }\n.lm-story legend { float: left; font: 600 .74rem var(--lm-font); text-transform: uppercase; letter-spacing: .08em; color: var(--lm-muted); padding: 0; }\n.lm-story p { flex: 1 1 240px; margin: 0; font: 400 .9rem/1.4 var(--lm-font); color: var(--lm-fg); }\n.lm-story b { font-weight: 600; color: var(--lm-muted); margin-right: 4px; white-space: nowrap; }\n.lm-story-nav { display: flex; gap: 6px; }\n.lm-story em { color: var(--lm-muted); }\n.lm-story button { font: 500 .84rem var(--lm-font); color: var(--lm-fg); background: var(--lm-surface); border: 1px solid var(--lm-line); border-radius: 6px; padding: 6px 12px; cursor: pointer; }\n.lm-story button[aria-disabled=\"true\"] { opacity: .45; cursor: default; }\n.lm-own { font-size: .78rem; color: var(--lm-muted); }\n.lm-key { display: inline-block; width: 14px; height: 2px; border-radius: 1px; vertical-align: 3px; margin-right: 6px; }\n\n.lm-compare-text { margin: 0; }\n.lm-table-wrap { overflow-x: auto; }\n.lm-table { border-collapse: collapse; width: 100%; font-size: .84rem; }\n.lm-table th, .lm-table td { text-align: left; padding: 6px 10px 6px 0; border-bottom: 1px solid var(--lm-line); vertical-align: top; }\n.lm-table thead th { font: 600 .7rem var(--lm-font); text-transform: uppercase; letter-spacing: .06em; color: var(--lm-muted); }\n.lm-table tbody th { font-weight: 600; white-space: nowrap; }\n.lm-table td { font-family: var(--lm-mono); font-variant-numeric: tabular-nums; }\n.lm-table td.lm-text { font-family: var(--lm-font); }\n.lm-table td small { display: block; font: 400 .72rem var(--lm-font); color: var(--lm-muted); }\n.lm-table tr[aria-current] th { text-decoration: underline; text-underline-offset: 3px; }\n\n.lm-chart { position: relative; min-width: 0; }\n.lm-chart svg { display: block; overflow: visible; }\n.lm-chart:focus-visible { outline: 2px solid var(--lm-accent); outline-offset: 2px; }\n.lm-axis, .lm-axis text { font: 400 11px var(--lm-mono); fill: var(--lm-muted); }\n.lm-mark { font: 600 11px var(--lm-mono); fill: var(--lm-fg); }\n.lm-tip { position: absolute; z-index: 2; display: grid; gap: 2px; width: max-content; max-width: 17rem; padding: 8px 10px; font-size: .78rem; background: var(--lm-surface); color: var(--lm-fg); border: 1px solid var(--lm-line); border-radius: 6px; box-shadow: 0 4px 14px rgb(0 0 0 / .14); pointer-events: none; }\n.lm-tip strong { font: 600 .82rem var(--lm-mono); font-variant-numeric: tabular-nums; }\n.lm-tip .lm-sub { color: var(--lm-muted); }\n.lm-nowrap { white-space: nowrap; }\n.lm-node { cursor: pointer; outline: none; }\n.lm-node:focus-visible > :first-child { stroke-width: 3; }\n\n@media (prefers-reduced-motion: reduce) { .lm-fill { transition: none; } }\n";
|
|
211
|
+
/** Adds the stylesheet once per document. */
|
|
212
|
+
export declare function injectStyles(doc?: Document): void;
|
|
213
|
+
export { type AnyNodeDef, type ClusterDef, type EdgeDef, type Engine, type FlowColor, type FlowView, type Label, type LoadmapController, type LoadmapHandle, type LoadmapOptions, type LoadmapProps, type NodeDef, type PoolDef, type Theme, type View, type ViewDef, cluster, compact, defineView, edge, fitLinear, fmt, multiple, node, pct, pool, secs };
|