@tellegen/svelte 0.1.2 → 0.3.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/README.md +46 -0
- package/dist/Sparkline.svelte.d.ts +0 -1
- package/dist/TellegenMap.svelte +328 -200
- package/dist/TellegenMap.svelte.d.ts +0 -1
- package/dist/TellegenProvider.svelte +4 -0
- package/dist/TellegenProvider.svelte.d.ts +0 -1
- package/dist/TellegenShell.svelte +19 -13
- package/dist/TellegenShell.svelte.d.ts +0 -1
- package/dist/TellegenViewer.svelte.d.ts +0 -1
- package/dist/api.d.ts +8 -2
- package/dist/api.js +6 -2
- package/dist/colors.d.ts +7 -8
- package/dist/colors.js +13 -13
- package/dist/components/AppFooter.svelte.d.ts +0 -1
- package/dist/components/AppHeader.svelte +27 -13
- package/dist/components/AppHeader.svelte.d.ts +0 -1
- package/dist/components/BindingLines.svelte +3 -6
- package/dist/components/BindingLines.svelte.d.ts +0 -1
- package/dist/components/BusPicker.svelte +8 -2
- package/dist/components/BusPicker.svelte.d.ts +0 -1
- package/dist/components/ControlPanel.svelte +106 -343
- package/dist/components/ControlPanel.svelte.d.ts +0 -1
- package/dist/components/DemandSlider.svelte +26 -8
- package/dist/components/DemandSlider.svelte.d.ts +0 -1
- package/dist/components/DiagramCanvas.svelte +486 -0
- package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
- package/dist/components/DisplayControls.svelte +4 -2
- package/dist/components/DisplayControls.svelte.d.ts +0 -1
- package/dist/components/DropZone.svelte.d.ts +0 -1
- package/dist/components/FormulationSelector.svelte +8 -3
- package/dist/components/FormulationSelector.svelte.d.ts +0 -1
- package/dist/components/LocalCaseDetails.svelte +57 -44
- package/dist/components/LocalCaseDetails.svelte.d.ts +0 -1
- package/dist/components/McResults.svelte +365 -0
- package/dist/components/McResults.svelte.d.ts +11 -0
- package/dist/components/ModelDetails.svelte +230 -0
- package/dist/components/ModelDetails.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +227 -78
- package/dist/components/MulticonductorDetails.svelte.d.ts +0 -1
- package/dist/components/NetworkStats.svelte +20 -18
- package/dist/components/NetworkStats.svelte.d.ts +0 -1
- package/dist/components/NotificationCenter.svelte +243 -0
- package/dist/components/NotificationCenter.svelte.d.ts +3 -0
- package/dist/components/PanelCard.svelte +319 -0
- package/dist/components/PanelCard.svelte.d.ts +9 -0
- package/dist/components/PanelFrame.svelte +46 -0
- package/dist/components/PanelFrame.svelte.d.ts +16 -0
- package/dist/components/PanelHost.svelte +212 -0
- package/dist/components/PanelHost.svelte.d.ts +3 -0
- package/dist/components/PlacementCue.svelte.d.ts +0 -1
- package/dist/components/PowerFlowDetails.svelte +99 -0
- package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
- package/dist/components/RatingSlider.svelte +5 -9
- package/dist/components/RatingSlider.svelte.d.ts +0 -1
- package/dist/components/RestoreDefaultsButton.svelte.d.ts +0 -1
- package/dist/components/SavedNetworkDetails.svelte +214 -0
- package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
- package/dist/components/SensitivityReadout.svelte +24 -16
- package/dist/components/SensitivityReadout.svelte.d.ts +0 -1
- package/dist/components/SizeLegend.svelte.d.ts +0 -1
- package/dist/components/SolveCard.svelte +46 -49
- package/dist/components/SolveCard.svelte.d.ts +0 -1
- package/dist/components/TopMovers.svelte +8 -3
- package/dist/components/TopMovers.svelte.d.ts +2 -18
- package/dist/components.d.ts +2 -1
- package/dist/components.js +2 -0
- package/dist/context.svelte.d.ts +0 -1
- package/dist/controller.svelte.d.ts +74 -41
- package/dist/controller.svelte.js +1095 -284
- package/dist/diagram-labels.d.ts +9 -0
- package/dist/diagram-labels.js +40 -0
- package/dist/display.d.ts +3 -8
- package/dist/display.js +31 -27
- package/dist/drop-classify.d.ts +5 -42
- package/dist/drop-classify.js +5 -78
- package/dist/drop-limits.d.ts +27 -0
- package/dist/drop-limits.js +83 -0
- package/dist/format.d.ts +0 -1
- package/dist/format.js +5 -4
- package/dist/index.d.ts +11 -7
- package/dist/index.js +8 -3
- package/dist/map-bounds.d.ts +5 -0
- package/dist/map-bounds.js +41 -0
- package/dist/map.d.ts +1 -2
- package/dist/map.js +1 -1
- package/dist/multiconductor.d.ts +11 -3
- package/dist/multiconductor.js +107 -14
- package/dist/notices.svelte.d.ts +29 -0
- package/dist/notices.svelte.js +82 -0
- package/dist/panels.svelte.d.ts +65 -0
- package/dist/panels.svelte.js +215 -0
- package/dist/state.svelte.d.ts +90 -24
- package/dist/state.svelte.js +94 -20
- package/dist/synthetic-layout.d.ts +0 -1
- package/dist/synthetic-layout.js +17 -2
- package/dist/transformer-icon.d.ts +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +28 -12
- package/dist/Sparkline.svelte.d.ts.map +0 -1
- package/dist/TellegenMap.svelte.d.ts.map +0 -1
- package/dist/TellegenProvider.svelte.d.ts.map +0 -1
- package/dist/TellegenShell.svelte.d.ts.map +0 -1
- package/dist/TellegenViewer.svelte.d.ts.map +0 -1
- package/dist/api.d.ts.map +0 -1
- package/dist/colors.d.ts.map +0 -1
- package/dist/components/AppFooter.svelte.d.ts.map +0 -1
- package/dist/components/AppHeader.svelte.d.ts.map +0 -1
- package/dist/components/BindingLines.svelte.d.ts.map +0 -1
- package/dist/components/BusPicker.svelte.d.ts.map +0 -1
- package/dist/components/ControlPanel.svelte.d.ts.map +0 -1
- package/dist/components/DemandSlider.svelte.d.ts.map +0 -1
- package/dist/components/DisplayControls.svelte.d.ts.map +0 -1
- package/dist/components/DropZone.svelte.d.ts.map +0 -1
- package/dist/components/FormulationSelector.svelte.d.ts.map +0 -1
- package/dist/components/LocalCaseDetails.svelte.d.ts.map +0 -1
- package/dist/components/MulticonductorDetails.svelte.d.ts.map +0 -1
- package/dist/components/NetworkStats.svelte.d.ts.map +0 -1
- package/dist/components/PlacementCue.svelte.d.ts.map +0 -1
- package/dist/components/RatingSlider.svelte.d.ts.map +0 -1
- package/dist/components/RestoreDefaultsButton.svelte.d.ts.map +0 -1
- package/dist/components/SensitivityReadout.svelte.d.ts.map +0 -1
- package/dist/components/SizeLegend.svelte.d.ts.map +0 -1
- package/dist/components/SolveCard.svelte.d.ts.map +0 -1
- package/dist/components/TopMovers.svelte.d.ts.map +0 -1
- package/dist/components.d.ts.map +0 -1
- package/dist/context.svelte.d.ts.map +0 -1
- package/dist/controller.svelte.d.ts.map +0 -1
- package/dist/display.d.ts.map +0 -1
- package/dist/drop-classify.d.ts.map +0 -1
- package/dist/format.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/map.d.ts.map +0 -1
- package/dist/multiconductor.d.ts.map +0 -1
- package/dist/state.svelte.d.ts.map +0 -1
- package/dist/study-package.d.ts +0 -9
- package/dist/study-package.d.ts.map +0 -1
- package/dist/study-package.js +0 -17
- package/dist/synthetic-layout.d.ts.map +0 -1
- package/dist/transformer-icon.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
package/dist/multiconductor.js
CHANGED
|
@@ -4,13 +4,7 @@
|
|
|
4
4
|
* `MulticonductorCase` state and the map own those. */
|
|
5
5
|
import { placeSyntheticTopology } from './synthetic-layout.js';
|
|
6
6
|
import { extent } from './format.js';
|
|
7
|
-
const ATTACHMENT_ORDER = [
|
|
8
|
-
'source',
|
|
9
|
-
'generator',
|
|
10
|
-
'ibr',
|
|
11
|
-
'load',
|
|
12
|
-
'shunt'
|
|
13
|
-
];
|
|
7
|
+
const ATTACHMENT_ORDER = ['source', 'generator', 'ibr', 'load', 'shunt'];
|
|
14
8
|
/** Flatten a bus's per-terminal attachments to the distinct kinds present, in a
|
|
15
9
|
* stable render order. */
|
|
16
10
|
function attachmentKindsOf(bus) {
|
|
@@ -28,6 +22,7 @@ function placedBus(bus, lon, lat) {
|
|
|
28
22
|
lat,
|
|
29
23
|
terminals: bus.terminals,
|
|
30
24
|
grounded: bus.grounded,
|
|
25
|
+
neutral_terminal: bus.neutral_terminal,
|
|
31
26
|
load_kw: bus.load_kw,
|
|
32
27
|
gen_kw: bus.gen_kw,
|
|
33
28
|
has_source: bus.has_source,
|
|
@@ -75,13 +70,105 @@ function viewFrom(graph, coords) {
|
|
|
75
70
|
}
|
|
76
71
|
/** Place a geographic graph: each bus's `xy` is `[longitude, latitude]` and
|
|
77
72
|
* drops straight onto the map. */
|
|
78
|
-
export function buildGeographicView(graph) {
|
|
73
|
+
export function buildGeographicView(graph, layer) {
|
|
79
74
|
const coords = new Map();
|
|
80
75
|
for (const bus of graph.buses) {
|
|
81
76
|
if (bus.xy)
|
|
82
77
|
coords.set(bus.id, bus.xy);
|
|
83
78
|
}
|
|
84
|
-
return viewFrom(graph, coords);
|
|
79
|
+
return withRoutes(viewFrom(graph, coords), layer);
|
|
80
|
+
}
|
|
81
|
+
/** Retain native drawing positions; unpositioned buses receive a separate generated layout. */
|
|
82
|
+
export function buildDiagramView(graph, layer) {
|
|
83
|
+
const generated = layoutSynthetic(graph, { lon: 0, lat: 0 });
|
|
84
|
+
const coords = new Map();
|
|
85
|
+
const positioned = graph.buses.filter((bus) => bus.xy?.every(Number.isFinite));
|
|
86
|
+
const offset = positioned.length ? Math.max(...positioned.map((bus) => bus.xy[0])) + 100 : 0;
|
|
87
|
+
for (const bus of graph.buses) {
|
|
88
|
+
if (bus.xy?.every(Number.isFinite))
|
|
89
|
+
coords.set(bus.id, bus.xy);
|
|
90
|
+
else {
|
|
91
|
+
const xy = generated.get(bus.id);
|
|
92
|
+
if (xy)
|
|
93
|
+
coords.set(bus.id, [offset + xy[0] * 100, xy[1] * 100]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return withRoutes(viewFrom(graph, coords), layer);
|
|
97
|
+
}
|
|
98
|
+
/** Match canonical GeoLayer paths by equipment identity or an unambiguous endpoint pair. */
|
|
99
|
+
function withRoutes(view, layer) {
|
|
100
|
+
if (!layer)
|
|
101
|
+
return view;
|
|
102
|
+
let features;
|
|
103
|
+
try {
|
|
104
|
+
features = JSON.parse(layer).features;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return view;
|
|
108
|
+
}
|
|
109
|
+
if (!Array.isArray(features))
|
|
110
|
+
return view;
|
|
111
|
+
const routes = new Map();
|
|
112
|
+
const same = (a, b) => typeof a === 'string' && a.toLowerCase() === b.toLowerCase();
|
|
113
|
+
for (const feature of features) {
|
|
114
|
+
const props = feature?.properties;
|
|
115
|
+
const geometry = feature?.geometry;
|
|
116
|
+
if (!props || geometry?.type !== 'LineString' || !Array.isArray(geometry.coordinates))
|
|
117
|
+
continue;
|
|
118
|
+
const points = geometry.coordinates
|
|
119
|
+
.filter((point) => Array.isArray(point) &&
|
|
120
|
+
point.length >= 2 &&
|
|
121
|
+
Number.isFinite(point[0]) &&
|
|
122
|
+
Number.isFinite(point[1]))
|
|
123
|
+
.map((point) => [point[0], point[1]]);
|
|
124
|
+
if (points.length < 2)
|
|
125
|
+
continue;
|
|
126
|
+
const from = props.from ?? props.bus_from;
|
|
127
|
+
const to = props.to ?? props.bus_to;
|
|
128
|
+
const id = props.id ?? props.uid;
|
|
129
|
+
const matches = view.edges.filter((edge) => id !== undefined
|
|
130
|
+
? same(id, edge.id)
|
|
131
|
+
: (same(from, edge.from) && same(to, edge.to)) ||
|
|
132
|
+
(same(from, edge.to) && same(to, edge.from)));
|
|
133
|
+
if (matches.length !== 1)
|
|
134
|
+
continue;
|
|
135
|
+
const edge = matches[0];
|
|
136
|
+
const reverse = same(from, edge.to) && same(to, edge.from);
|
|
137
|
+
routes.set(edge.id, reverse ? points.reverse() : points);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...view,
|
|
141
|
+
edges: view.edges.map((edge) => ({ ...edge, path: routes.get(edge.id) ?? edge.path }))
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Numeric indices address drawing objects only; source identities remain on each object. */
|
|
145
|
+
export function multiDiagramNetwork(id, name, view) {
|
|
146
|
+
const indexes = new Map(view.buses.map((bus, index) => [bus.id.toLowerCase(), index]));
|
|
147
|
+
return {
|
|
148
|
+
id,
|
|
149
|
+
name,
|
|
150
|
+
base_mva: 1,
|
|
151
|
+
coordinate_space: 'diagram',
|
|
152
|
+
synthetic_coords: false,
|
|
153
|
+
buses: view.buses.map((bus, index) => ({
|
|
154
|
+
id: index,
|
|
155
|
+
uid: bus.id,
|
|
156
|
+
name: bus.id,
|
|
157
|
+
lon: bus.lon,
|
|
158
|
+
lat: bus.lat,
|
|
159
|
+
demand_mw: bus.load_kw / 1000,
|
|
160
|
+
gen_mw: bus.gen_kw / 1000
|
|
161
|
+
})),
|
|
162
|
+
branches: view.edges.map((edge, index) => ({
|
|
163
|
+
id: index,
|
|
164
|
+
uid: edge.id,
|
|
165
|
+
from: indexes.get(edge.from.toLowerCase()),
|
|
166
|
+
to: indexes.get(edge.to.toLowerCase()),
|
|
167
|
+
path: edge.path,
|
|
168
|
+
rate_mw: 0,
|
|
169
|
+
status: edge.closed ? 1 : 0
|
|
170
|
+
}))
|
|
171
|
+
};
|
|
85
172
|
}
|
|
86
173
|
/** Whether at least two buses carry a planar `xy`, enough to fit a layout to. */
|
|
87
174
|
function hasPlanarCoords(graph) {
|
|
@@ -116,10 +203,7 @@ function projectPlanar(graph, center) {
|
|
|
116
203
|
// reads as further north.
|
|
117
204
|
const nx = 0.5 + (bus.xy[0] - cx) * s;
|
|
118
205
|
const ny = 0.5 + (bus.xy[1] - cy) * s;
|
|
119
|
-
coords.set(bus.id, [
|
|
120
|
-
center.lon + (nx - 0.5) * lonSpan,
|
|
121
|
-
center.lat + (ny - 0.5) * latSpan
|
|
122
|
-
]);
|
|
206
|
+
coords.set(bus.id, [center.lon + (nx - 0.5) * lonSpan, center.lat + (ny - 0.5) * latSpan]);
|
|
123
207
|
}
|
|
124
208
|
return coords;
|
|
125
209
|
}
|
|
@@ -188,7 +272,7 @@ export function placeMultiView(graph, center) {
|
|
|
188
272
|
// ---------------------------------------------------------------------------
|
|
189
273
|
/** Rust, green, and steel-blue for the three phases; a warm gray for the
|
|
190
274
|
* neutral/ground return. Distinct in hue and lightness so they survive color
|
|
191
|
-
* vision deficiency and never read as the
|
|
275
|
+
* vision deficiency and never read as the nodal value ramp. */
|
|
192
276
|
const PHASE_A = [194, 86, 75, 255];
|
|
193
277
|
const PHASE_B = [74, 143, 95, 255];
|
|
194
278
|
const PHASE_C = [63, 111, 187, 255];
|
|
@@ -215,6 +299,15 @@ export function phaseColor(terminal) {
|
|
|
215
299
|
export function isPhaseTerminal(terminal) {
|
|
216
300
|
return ['1', '2', '3', 'a', 'b', 'c'].includes(terminal.trim().toLowerCase());
|
|
217
301
|
}
|
|
302
|
+
/** Return the explicit neutral conductor in a terminal map. Other non-phase
|
|
303
|
+
* labels are left alone: an arbitrary terminal name is not evidence that the
|
|
304
|
+
* network has a usable neutral reference. */
|
|
305
|
+
export function neutralTerminal(terminals, declared) {
|
|
306
|
+
if (declared !== undefined)
|
|
307
|
+
return declared && terminals.includes(declared) ? declared : null;
|
|
308
|
+
return (terminals.find((terminal) => ['n', '4', 'neutral'].includes(terminal.trim().toLowerCase())) ??
|
|
309
|
+
null);
|
|
310
|
+
}
|
|
218
311
|
/** Base color for an edge by kind and state. Lines are the warm gray of the
|
|
219
312
|
* single-phase branches; transformers are steel; a switch is amber when closed
|
|
220
313
|
* and a faded red when open. */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface NoticeInput {
|
|
2
|
+
title: string;
|
|
3
|
+
details: string;
|
|
4
|
+
kind?: 'error' | 'warning';
|
|
5
|
+
retry?: () => void;
|
|
6
|
+
canRetry?: () => boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface Notice extends NoticeInput {
|
|
9
|
+
id: number;
|
|
10
|
+
count: number;
|
|
11
|
+
updatedAt: number;
|
|
12
|
+
}
|
|
13
|
+
/** Recent messages stay in memory; dismissal hides the temporary notice only. */
|
|
14
|
+
export declare class NoticeCenter {
|
|
15
|
+
#private;
|
|
16
|
+
entries: Notice[];
|
|
17
|
+
activeId: number | null;
|
|
18
|
+
expanded: boolean;
|
|
19
|
+
announcement: string;
|
|
20
|
+
get active(): Notice | null;
|
|
21
|
+
push(input: NoticeInput): void;
|
|
22
|
+
pause(value: boolean): void;
|
|
23
|
+
dismiss(): void;
|
|
24
|
+
clear(): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare const setNoticeCenter: (value: NoticeCenter) => NoticeCenter;
|
|
28
|
+
export declare function getNoticeCenter(): NoticeCenter;
|
|
29
|
+
export declare function errorNoticeTitle(details: string): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const KEY = Symbol('tellegen.notices');
|
|
3
|
+
const LIFETIME_MS = 6500;
|
|
4
|
+
const MAX_NOTICES = 30;
|
|
5
|
+
/** Recent messages stay in memory; dismissal hides the temporary notice only. */
|
|
6
|
+
export class NoticeCenter {
|
|
7
|
+
entries = $state.raw([]);
|
|
8
|
+
activeId = $state(null);
|
|
9
|
+
expanded = $state(false);
|
|
10
|
+
announcement = $state('');
|
|
11
|
+
#nextId = 0;
|
|
12
|
+
#timer;
|
|
13
|
+
#deadline = 0;
|
|
14
|
+
#remaining = LIFETIME_MS;
|
|
15
|
+
#paused = false;
|
|
16
|
+
get active() {
|
|
17
|
+
return this.entries.find((entry) => entry.id === this.activeId) ?? null;
|
|
18
|
+
}
|
|
19
|
+
push(input) {
|
|
20
|
+
if (!input.details)
|
|
21
|
+
return;
|
|
22
|
+
const previous = this.entries.find((entry) => entry.details === input.details && entry.kind === input.kind);
|
|
23
|
+
const entry = {
|
|
24
|
+
...input,
|
|
25
|
+
id: previous?.id ?? ++this.#nextId,
|
|
26
|
+
count: (previous?.count ?? 0) + 1,
|
|
27
|
+
updatedAt: Date.now()
|
|
28
|
+
};
|
|
29
|
+
this.entries = [entry, ...this.entries.filter((item) => item.id !== entry.id)].slice(0, MAX_NOTICES);
|
|
30
|
+
this.activeId = entry.id;
|
|
31
|
+
this.announcement = `${entry.title}${entry.count > 1 ? `, repeated ${entry.count} times` : ''}`;
|
|
32
|
+
this.#remaining = LIFETIME_MS;
|
|
33
|
+
this.#schedule();
|
|
34
|
+
}
|
|
35
|
+
#schedule() {
|
|
36
|
+
clearTimeout(this.#timer);
|
|
37
|
+
if (this.#paused || this.activeId === null)
|
|
38
|
+
return;
|
|
39
|
+
this.#deadline = Date.now() + this.#remaining;
|
|
40
|
+
this.#timer = setTimeout(() => this.dismiss(), this.#remaining);
|
|
41
|
+
}
|
|
42
|
+
pause(value) {
|
|
43
|
+
if (value === this.#paused)
|
|
44
|
+
return;
|
|
45
|
+
if (value)
|
|
46
|
+
this.#remaining = Math.max(0, this.#deadline - Date.now());
|
|
47
|
+
this.#paused = value;
|
|
48
|
+
this.#schedule();
|
|
49
|
+
}
|
|
50
|
+
dismiss() {
|
|
51
|
+
clearTimeout(this.#timer);
|
|
52
|
+
this.activeId = null;
|
|
53
|
+
}
|
|
54
|
+
clear() {
|
|
55
|
+
this.dismiss();
|
|
56
|
+
this.entries = [];
|
|
57
|
+
this.expanded = false;
|
|
58
|
+
}
|
|
59
|
+
dispose() {
|
|
60
|
+
clearTimeout(this.#timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export const setNoticeCenter = (value) => setContext(KEY, value);
|
|
64
|
+
export function getNoticeCenter() {
|
|
65
|
+
const value = getContext(KEY);
|
|
66
|
+
if (!value)
|
|
67
|
+
throw new Error('NoticeCenter requires a Tellegen provider');
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
export function errorNoticeTitle(details) {
|
|
71
|
+
if (/rate limit/i.test(details))
|
|
72
|
+
return 'Too many requests';
|
|
73
|
+
if (/storage|quota|space|indexeddb/i.test(details))
|
|
74
|
+
return 'Could not save locally';
|
|
75
|
+
if (/sensitivity|derivative|singular/i.test(details))
|
|
76
|
+
return 'Sensitivity unavailable';
|
|
77
|
+
if (/infeasib|converg|solver|solving|calculation|compute|webassembly/i.test(details))
|
|
78
|
+
return 'Calculation did not complete';
|
|
79
|
+
if (/parse|read|file|format/i.test(details))
|
|
80
|
+
return 'Could not read this file';
|
|
81
|
+
return 'Operation did not complete';
|
|
82
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
export type PanelSide = 'left' | 'right';
|
|
3
|
+
export type PanelPosition = PanelSide | 'floating';
|
|
4
|
+
export interface PanelBounds {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PanelRegistration {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
side: PanelSide;
|
|
14
|
+
order: number;
|
|
15
|
+
width: number;
|
|
16
|
+
open: boolean;
|
|
17
|
+
setOpen: (open: boolean) => void;
|
|
18
|
+
children: Snippet;
|
|
19
|
+
headerActions?: Snippet;
|
|
20
|
+
onopen?: () => void;
|
|
21
|
+
}
|
|
22
|
+
interface SavedPanel extends PanelBounds {
|
|
23
|
+
position: PanelPosition;
|
|
24
|
+
}
|
|
25
|
+
export declare const PANEL_BREAKPOINT = 960;
|
|
26
|
+
export declare const PANEL_MIN_DOCK_HEIGHT = 500;
|
|
27
|
+
export declare const PANEL_COMPACT_QUERY = "(max-width: 960px), (max-height: 499px)";
|
|
28
|
+
/** Layout belongs to one provider and stores presentation preferences only. */
|
|
29
|
+
export declare class PanelLayout {
|
|
30
|
+
#private;
|
|
31
|
+
panels: PanelRegistration[];
|
|
32
|
+
placements: Record<string, SavedPanel>;
|
|
33
|
+
compact: boolean;
|
|
34
|
+
drawer: string | null;
|
|
35
|
+
lastActive: string;
|
|
36
|
+
dockActive: Record<PanelSide, string>;
|
|
37
|
+
viewportWidth: number;
|
|
38
|
+
viewportHeight: number;
|
|
39
|
+
top: number;
|
|
40
|
+
bottom: number;
|
|
41
|
+
storageError: string | null;
|
|
42
|
+
beginGesture(panel: PanelRegistration, kind: 'move' | 'resize', x: number, y: number, bounds: PanelBounds): void;
|
|
43
|
+
moveGesture(x: number, y: number): void;
|
|
44
|
+
finishGesture(): void;
|
|
45
|
+
register(panel: PanelRegistration): () => void;
|
|
46
|
+
update(id: string, open: boolean, title: string, width: number): void;
|
|
47
|
+
activate(panel: PanelRegistration): void;
|
|
48
|
+
preferred(panel: PanelRegistration): boolean;
|
|
49
|
+
position(panel: PanelRegistration): PanelPosition;
|
|
50
|
+
ordered(side?: PanelSide): PanelRegistration[];
|
|
51
|
+
toggle(panel: PanelRegistration): void;
|
|
52
|
+
close(panel: PanelRegistration): void;
|
|
53
|
+
dock(panel: PanelRegistration, side?: PanelSide): void;
|
|
54
|
+
float(panel: PanelRegistration, bounds?: PanelBounds): void;
|
|
55
|
+
bounds(panel: PanelRegistration): PanelBounds;
|
|
56
|
+
clamp(bounds: PanelBounds): PanelBounds;
|
|
57
|
+
resizeViewport(width: number, height: number, top: number): void;
|
|
58
|
+
dockWidth(side: PanelSide): number;
|
|
59
|
+
reset(): void;
|
|
60
|
+
initialize(storage?: Storage): void;
|
|
61
|
+
save(): void;
|
|
62
|
+
}
|
|
63
|
+
export declare const setPanelLayout: (layout: PanelLayout) => PanelLayout;
|
|
64
|
+
export declare const getPanelLayout: () => PanelLayout;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const KEY = Symbol('tellegen.panel-layout');
|
|
3
|
+
const STORAGE_KEY = 'tellegen.panel-layout.v1';
|
|
4
|
+
export const PANEL_BREAKPOINT = 960;
|
|
5
|
+
export const PANEL_MIN_DOCK_HEIGHT = 500;
|
|
6
|
+
export const PANEL_COMPACT_QUERY = `(max-width: ${PANEL_BREAKPOINT}px), (max-height: ${PANEL_MIN_DOCK_HEIGHT - 1}px)`;
|
|
7
|
+
/** Layout belongs to one provider and stores presentation preferences only. */
|
|
8
|
+
export class PanelLayout {
|
|
9
|
+
panels = $state.raw([]);
|
|
10
|
+
placements = $state({});
|
|
11
|
+
compact = $state(false);
|
|
12
|
+
drawer = $state('network');
|
|
13
|
+
lastActive = $state('network');
|
|
14
|
+
dockActive = $state({ left: 'network', right: 'solver' });
|
|
15
|
+
viewportWidth = $state(1280);
|
|
16
|
+
viewportHeight = $state(800);
|
|
17
|
+
top = $state(100);
|
|
18
|
+
bottom = $state(88);
|
|
19
|
+
storageError = $state(null);
|
|
20
|
+
#storage;
|
|
21
|
+
#gesture = null;
|
|
22
|
+
beginGesture(panel, kind, x, y, bounds) {
|
|
23
|
+
this.#gesture = { panel, kind, x, y, bounds, moved: false };
|
|
24
|
+
}
|
|
25
|
+
moveGesture(x, y) {
|
|
26
|
+
const gesture = this.#gesture;
|
|
27
|
+
if (!gesture)
|
|
28
|
+
return;
|
|
29
|
+
const dx = x - gesture.x, dy = y - gesture.y;
|
|
30
|
+
if (!gesture.moved && Math.hypot(dx, dy) < 4)
|
|
31
|
+
return;
|
|
32
|
+
gesture.moved = true;
|
|
33
|
+
this.float(gesture.panel, gesture.kind === 'move'
|
|
34
|
+
? {
|
|
35
|
+
...gesture.bounds,
|
|
36
|
+
x: gesture.bounds.x + dx,
|
|
37
|
+
y: gesture.bounds.y + dy
|
|
38
|
+
}
|
|
39
|
+
: {
|
|
40
|
+
...gesture.bounds,
|
|
41
|
+
width: gesture.bounds.width + dx,
|
|
42
|
+
height: gesture.bounds.height + dy
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
finishGesture() {
|
|
46
|
+
if (this.#gesture?.moved)
|
|
47
|
+
this.save();
|
|
48
|
+
this.#gesture = null;
|
|
49
|
+
}
|
|
50
|
+
register(panel) {
|
|
51
|
+
this.panels = [...this.panels.filter((p) => p.id !== panel.id), panel];
|
|
52
|
+
return () => {
|
|
53
|
+
this.panels = this.panels.filter((p) => p.id !== panel.id);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
update(id, open, title, width) {
|
|
57
|
+
const previous = this.panels.find((p) => p.id === id);
|
|
58
|
+
if (!previous ||
|
|
59
|
+
(previous.open === open && previous.title === title && previous.width === width))
|
|
60
|
+
return;
|
|
61
|
+
this.panels = this.panels.map((p) => (p.id === id ? { ...p, open, title, width } : p));
|
|
62
|
+
if (open && !previous.open) {
|
|
63
|
+
this.activate(previous);
|
|
64
|
+
if (this.compact)
|
|
65
|
+
this.drawer = id;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
activate(panel) {
|
|
69
|
+
this.lastActive = panel.id;
|
|
70
|
+
const side = this.position(panel);
|
|
71
|
+
if (side !== 'floating')
|
|
72
|
+
this.dockActive[side] = panel.id;
|
|
73
|
+
}
|
|
74
|
+
preferred(panel) {
|
|
75
|
+
const side = this.position(panel);
|
|
76
|
+
if (side === 'floating')
|
|
77
|
+
return false;
|
|
78
|
+
const open = this.ordered(side).filter((candidate) => candidate.open);
|
|
79
|
+
const selected = open.find((candidate) => candidate.id === this.dockActive[side]) ?? open[0];
|
|
80
|
+
return selected?.id === panel.id;
|
|
81
|
+
}
|
|
82
|
+
position(panel) {
|
|
83
|
+
return this.placements[panel.id]?.position ?? panel.side;
|
|
84
|
+
}
|
|
85
|
+
ordered(side) {
|
|
86
|
+
return this.panels
|
|
87
|
+
.filter((p) => side === undefined || this.position(p) === side)
|
|
88
|
+
.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
|
|
89
|
+
}
|
|
90
|
+
toggle(panel) {
|
|
91
|
+
if (this.compact) {
|
|
92
|
+
this.drawer = this.drawer === panel.id ? null : panel.id;
|
|
93
|
+
if (this.drawer === panel.id) {
|
|
94
|
+
this.activate(panel);
|
|
95
|
+
panel.setOpen(true);
|
|
96
|
+
panel.onopen?.();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (!panel.open)
|
|
101
|
+
this.activate(panel);
|
|
102
|
+
panel.setOpen(!panel.open);
|
|
103
|
+
if (!panel.open)
|
|
104
|
+
panel.onopen?.();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
close(panel) {
|
|
108
|
+
panel.setOpen(false);
|
|
109
|
+
if (this.drawer === panel.id)
|
|
110
|
+
this.drawer = null;
|
|
111
|
+
}
|
|
112
|
+
dock(panel, side = panel.side) {
|
|
113
|
+
this.placements[panel.id] = { ...this.bounds(panel), position: side };
|
|
114
|
+
this.activate(panel);
|
|
115
|
+
this.save();
|
|
116
|
+
}
|
|
117
|
+
float(panel, bounds) {
|
|
118
|
+
this.placements[panel.id] = {
|
|
119
|
+
...this.clamp(bounds ?? this.bounds(panel)),
|
|
120
|
+
position: 'floating'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
bounds(panel) {
|
|
124
|
+
return this.clamp(this.placements[panel.id] ?? {
|
|
125
|
+
x: panel.side === 'left' ? 24 : this.viewportWidth - panel.width - 24,
|
|
126
|
+
y: this.top + 16,
|
|
127
|
+
width: panel.width,
|
|
128
|
+
height: Math.min(520, this.viewportHeight - this.top - this.bottom - 22)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
clamp(bounds) {
|
|
132
|
+
const availableWidth = Math.max(1, this.viewportWidth - 32);
|
|
133
|
+
const availableHeight = Math.max(1, this.viewportHeight - this.top - this.bottom - 12);
|
|
134
|
+
const width = Math.min(availableWidth, Math.max(240, bounds.width));
|
|
135
|
+
const height = Math.min(availableHeight, Math.max(120, bounds.height));
|
|
136
|
+
return {
|
|
137
|
+
width,
|
|
138
|
+
height,
|
|
139
|
+
x: Math.max(16, Math.min(bounds.x, this.viewportWidth - width - 16)),
|
|
140
|
+
y: Math.max(this.top, Math.min(bounds.y, this.viewportHeight - height - this.bottom))
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
resizeViewport(width, height, top) {
|
|
144
|
+
this.viewportWidth = width;
|
|
145
|
+
this.viewportHeight = height;
|
|
146
|
+
this.top = top;
|
|
147
|
+
const wasCompact = this.compact;
|
|
148
|
+
this.compact = width <= PANEL_BREAKPOINT || height < PANEL_MIN_DOCK_HEIGHT;
|
|
149
|
+
if (this.compact && !wasCompact && this.panels.length > 0) {
|
|
150
|
+
const latest = this.panels.find((panel) => panel.id === this.lastActive && panel.open);
|
|
151
|
+
this.drawer = latest?.id ?? this.panels.find((panel) => panel.open)?.id ?? null;
|
|
152
|
+
}
|
|
153
|
+
for (const [id, panel] of Object.entries(this.placements)) {
|
|
154
|
+
if (panel.position === 'floating')
|
|
155
|
+
this.placements[id] = { ...this.clamp(panel), position: 'floating' };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
dockWidth(side) {
|
|
159
|
+
const panels = this.ordered(side).filter((p) => p.open);
|
|
160
|
+
return panels.length
|
|
161
|
+
? Math.min(Math.max(...panels.map((p) => p.width)), this.viewportWidth * 0.29)
|
|
162
|
+
: 0;
|
|
163
|
+
}
|
|
164
|
+
reset() {
|
|
165
|
+
this.placements = {};
|
|
166
|
+
this.save();
|
|
167
|
+
}
|
|
168
|
+
initialize(storage) {
|
|
169
|
+
this.#storage = storage;
|
|
170
|
+
if (!storage)
|
|
171
|
+
return;
|
|
172
|
+
try {
|
|
173
|
+
const text = storage.getItem(STORAGE_KEY);
|
|
174
|
+
if (!text)
|
|
175
|
+
return;
|
|
176
|
+
const value = JSON.parse(text);
|
|
177
|
+
if (value.version !== 1 || !value.panels || typeof value.panels !== 'object')
|
|
178
|
+
return;
|
|
179
|
+
for (const [id, panel] of Object.entries(value.panels)) {
|
|
180
|
+
if (!panel ||
|
|
181
|
+
!['left', 'right', 'floating'].includes(panel.position) ||
|
|
182
|
+
!['x', 'y', 'width', 'height'].every((key) => Number.isFinite(panel[key])))
|
|
183
|
+
continue;
|
|
184
|
+
this.placements[id] = {
|
|
185
|
+
...this.clamp(panel),
|
|
186
|
+
position: panel.position
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
this.storageError = 'Panel layout could not be restored. Reset layout to use the defaults.';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
save() {
|
|
195
|
+
if (!this.#storage)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
this.#storage.setItem(STORAGE_KEY, JSON.stringify({
|
|
199
|
+
version: 1,
|
|
200
|
+
panels: this.placements
|
|
201
|
+
}));
|
|
202
|
+
this.storageError = null;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
this.storageError = 'Panel layout could not be saved. Changes last for this visit.';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export const setPanelLayout = (layout) => setContext(KEY, layout);
|
|
210
|
+
export const getPanelLayout = () => {
|
|
211
|
+
const layout = getContext(KEY);
|
|
212
|
+
if (!layout)
|
|
213
|
+
throw new Error('PanelFrame requires a TellegenProvider');
|
|
214
|
+
return layout;
|
|
215
|
+
};
|