@tellegen/svelte 0.4.1 → 0.4.2
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 +38 -10
- package/dist/Sparkline.svelte +4 -4
- package/dist/TellegenMap.svelte +41 -46
- package/dist/TellegenProvider.svelte +29 -13
- package/dist/components/AppFooter.svelte +4 -4
- package/dist/components/AppHeader.svelte +15 -19
- package/dist/components/BindingLines.svelte +10 -9
- package/dist/components/BusPicker.svelte +5 -6
- package/dist/components/ControlPanel.svelte +12 -9
- package/dist/components/ControlPanel.svelte.d.ts +6 -1
- package/dist/components/DemandSlider.svelte +26 -29
- package/dist/components/DisplayControls.svelte +5 -24
- package/dist/components/DropZone.svelte +7 -3
- package/dist/components/DropZone.svelte.d.ts +6 -17
- package/dist/components/FormulationSelector.svelte +2 -7
- package/dist/components/LocalCaseDetails.svelte +126 -126
- package/dist/components/LocalCaseDetails.svelte.d.ts +6 -1
- package/dist/components/MulticonductorDetails.svelte +48 -38
- package/dist/components/NetworkStats.svelte +14 -16
- package/dist/components/PanelCard.svelte +1 -1
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/RatingSlider.svelte +22 -23
- package/dist/components/RestoreDefaultsButton.svelte +2 -3
- package/dist/components/SensitivityReadout.svelte +9 -27
- package/dist/components/SolveCard.svelte +69 -35
- package/dist/components/TopMovers.svelte +5 -5
- package/dist/context.svelte.d.ts +6 -2
- package/dist/context.svelte.js +4 -2
- package/dist/controller.svelte.d.ts +7 -0
- package/dist/controller.svelte.js +33 -6
- package/dist/display.js +8 -9
- package/dist/format.d.ts +1 -9
- package/dist/format.js +0 -13
- package/dist/styles.css +3 -0
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
Svelte components for tellegen maps, case panels, local case files, and browser solves.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm install @tellegen/svelte
|
|
6
|
+
npm install @tellegen/svelte maplibre-gl @deck.gl/core @deck.gl/layers @deck.gl/maplibre
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
`maplibre-gl` and the three `@deck.gl` packages are peer dependencies: the map
|
|
10
|
+
shares their instances with the host through `extraLayers` and `onMapReady`, so
|
|
11
|
+
the app installs them and resolves one copy of each.
|
|
12
|
+
|
|
9
13
|
Import the component and stylesheet in your app:
|
|
10
14
|
|
|
11
15
|
```svelte
|
|
@@ -41,6 +45,10 @@ remain in the browser on this path.
|
|
|
41
45
|
- `orgLabel`
|
|
42
46
|
- `showFooter`, default `true`
|
|
43
47
|
- `theme`, `light` (default) or `dark`
|
|
48
|
+
- `cartoApiKey`, the basemap key (see [Basemap](#basemap))
|
|
49
|
+
|
|
50
|
+
`theme` and `cartoApiKey` follow their props. The others are read once, when
|
|
51
|
+
the viewer mounts; remount it to change them.
|
|
44
52
|
|
|
45
53
|
Use `TellegenProvider` and `TellegenShell` when state should survive route changes:
|
|
46
54
|
|
|
@@ -55,6 +63,11 @@ Use `TellegenProvider` and `TellegenShell` when state should survive route chang
|
|
|
55
63
|
</TellegenProvider>
|
|
56
64
|
```
|
|
57
65
|
|
|
66
|
+
The provider owns the controller and its cases. Unmounting it disposes the
|
|
67
|
+
controller, which frees every case's browser Study and multiconductor session
|
|
68
|
+
in the engine worker, so a provider that should keep its cases across pages
|
|
69
|
+
belongs in a layout that stays mounted.
|
|
70
|
+
|
|
58
71
|
## Panel layout
|
|
59
72
|
|
|
60
73
|
The shell docks Network and Studies on the left, and Solver and Agent on the
|
|
@@ -123,6 +136,19 @@ inside `TellegenProvider`:
|
|
|
123
136
|
<PanelFrame id="region" title="Region" side="left">...</PanelFrame>
|
|
124
137
|
```
|
|
125
138
|
|
|
139
|
+
The components show data, labels and controls, not host copy. A host that
|
|
140
|
+
wants a note under a local case's stats passes `ControlPanel` a `caseNote`
|
|
141
|
+
snippet, which receives the case; `DropZone` renders its children under the
|
|
142
|
+
drop prompt:
|
|
143
|
+
|
|
144
|
+
```svelte
|
|
145
|
+
<ControlPanel>
|
|
146
|
+
{#snippet caseNote(lc)}
|
|
147
|
+
<p>{lc.label} is a synthetic grid.</p>
|
|
148
|
+
{/snippet}
|
|
149
|
+
</ControlPanel>
|
|
150
|
+
```
|
|
151
|
+
|
|
126
152
|
`ShellEnvironment` is renderless and keeps the compact-layout, pointer and
|
|
127
153
|
viewport state in step, as the default shell does. `PanelHost` reads
|
|
128
154
|
`--tellegen-dock-left`, `--tellegen-dock-right` and `--tellegen-dock-bottom`
|
|
@@ -193,7 +219,8 @@ besides its click callbacks:
|
|
|
193
219
|
- `attribution`: `'compact'` (default) or `'full'`. There is no off switch.
|
|
194
220
|
- `onMapReady(map, overlay)`: called with the MapLibre map and the deck
|
|
195
221
|
overlay once they exist. It may return a cleanup, and it fires again after
|
|
196
|
-
a context-loss or basemap rebuild.
|
|
222
|
+
a context-loss or basemap rebuild. If it throws, the map and its window
|
|
223
|
+
and document listeners are still released when the map is torn down.
|
|
197
224
|
- `onBasemapFallback(reason)`: the switch to the fallback happened.
|
|
198
225
|
`reason.from` and `reason.to` name the two providers.
|
|
199
226
|
- `onBasemapError(reason)`: no basemap can be shown. The network layers
|
|
@@ -201,7 +228,8 @@ besides its click callbacks:
|
|
|
201
228
|
`reason.to` is `null`.
|
|
202
229
|
|
|
203
230
|
`TellegenProvider` and `TellegenViewer` also take `cartoApiKey`, and pass it,
|
|
204
|
-
together with `theme`, to the default shell's map.
|
|
231
|
+
together with `theme`, to the default shell's map. Both follow their props:
|
|
232
|
+
changing either rebuilds that map in place. A SvelteKit 3 host can
|
|
205
233
|
declare the key as an optional public build-time variable:
|
|
206
234
|
|
|
207
235
|
```ts
|
|
@@ -251,13 +279,13 @@ tiles, fonts and sprites are all fetched. The engine underneath also needs
|
|
|
251
279
|
`script-src 'wasm-unsafe-eval'` (never `'unsafe-eval'`) and `worker-src
|
|
252
280
|
'self'`, since its workers are same-origin.
|
|
253
281
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
(or `resolutions` in pnpm and Yarn).
|
|
282
|
+
`@deck.gl/core`, `@deck.gl/layers`, `@deck.gl/maplibre` and `maplibre-gl`
|
|
283
|
+
are peer dependencies (`^9.4.0` for deck.gl, `^6.15.0` for maplibre-gl), so
|
|
284
|
+
the host's copies are the ones the map uses, including the deck.gl layers it
|
|
285
|
+
passes in `extraLayers` and the MapLibre map it gets from `onMapReady`. When
|
|
286
|
+
two deck.gl versions load, deck.gl throws "multiple versions detected"; if
|
|
287
|
+
another dependency pulls in a second copy, point both at one version with npm
|
|
288
|
+
`overrides` (or `resolutions` in pnpm and Yarn).
|
|
261
289
|
|
|
262
290
|
The map needs no Vite settings, in a build or in the dev server. MapLibre's
|
|
263
291
|
worker comes from a `?worker&url` import, which a production build bundles
|
package/dist/Sparkline.svelte
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
viewBox="0 0 {W} {H}"
|
|
36
36
|
preserveAspectRatio="none"
|
|
37
37
|
role="img"
|
|
38
|
-
aria-label="
|
|
38
|
+
aria-label="Residual by solver iteration"
|
|
39
39
|
>
|
|
40
40
|
<line class="axis" x1={L} y1={T} x2={L} y2={H - B} />
|
|
41
41
|
<line class="axis" x1={L} y1={H - B} x2={W - R} y2={H - B} />
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
text-anchor="middle"
|
|
47
47
|
transform="rotate(-90)"
|
|
48
48
|
>
|
|
49
|
-
|
|
49
|
+
Residual
|
|
50
50
|
</text>
|
|
51
|
-
<text class="axis-label x-label" x={W - R} y={H - 4} text-anchor="end">
|
|
51
|
+
<text class="axis-label x-label" x={W - R} y={H - 4} text-anchor="end">Iterations</text>
|
|
52
52
|
<polyline {points} pathLength="1" />
|
|
53
53
|
</svg>
|
|
54
54
|
{/if}
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
.axis-label {
|
|
71
71
|
fill: var(--text-tertiary);
|
|
72
|
-
font-family: var(--font-
|
|
72
|
+
font-family: var(--font-display);
|
|
73
73
|
font-size: 12px;
|
|
74
74
|
letter-spacing: 0;
|
|
75
75
|
}
|
package/dist/TellegenMap.svelte
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
} from './basemap.js';
|
|
34
34
|
import { caseDeltas, caseRatings, displayMetaFor, displaySeriesFor } from './display.js';
|
|
35
35
|
import {
|
|
36
|
-
CaseState,
|
|
36
|
+
type CaseState,
|
|
37
37
|
type CameraSnapshot,
|
|
38
38
|
type DisplayMode,
|
|
39
39
|
type LocalCase
|
|
@@ -710,13 +710,13 @@
|
|
|
710
710
|
if (info.layer.id === 'study-branches') {
|
|
711
711
|
const flow = view?.flows?.find((f) => f.branch === id);
|
|
712
712
|
return {
|
|
713
|
-
html: `<b>
|
|
713
|
+
html: `<b>Branch ${id}</b><br>Saved state: ${esc(app.studyView.label)}${flow ? `<br>P from ${flow.pf.toFixed(3)} MW<br>Loading ${(100 * flow.loading).toFixed(1)}%` : ''}`
|
|
714
714
|
};
|
|
715
715
|
}
|
|
716
716
|
const p = view?.lmp?.find((v) => v.bus === id),
|
|
717
717
|
v = view?.vm?.find((v) => v.bus === id);
|
|
718
718
|
return {
|
|
719
|
-
html: `<b>
|
|
719
|
+
html: `<b>Bus ${id}</b><br>Saved state: ${esc(app.studyView.label)}${p ? `<br>LMP ${p.value.toFixed(4)}` : ''}${v ? `<br>Voltage ${v.value.toFixed(5)} pu` : ''}`
|
|
720
720
|
};
|
|
721
721
|
}
|
|
722
722
|
if (info.layer?.id.startsWith('local-subs-')) {
|
|
@@ -726,25 +726,22 @@
|
|
|
726
726
|
const s = object as { number: number; name: string };
|
|
727
727
|
const named = s.name ? ` ${esc(s.name)}` : '';
|
|
728
728
|
return {
|
|
729
|
-
html: `<div class="tt"><b>
|
|
729
|
+
html: `<div class="tt"><b>Substation ${Number(s.number)}</b>${named}<br>Approximate position</div>`
|
|
730
730
|
};
|
|
731
731
|
}
|
|
732
732
|
const layerId = info.layer?.id;
|
|
733
733
|
if (layerId?.startsWith('multi-edges-')) {
|
|
734
734
|
const e = object as PlacedMultiEdge;
|
|
735
|
-
const state = e.kind === 'switch' ? (e.closed ? '
|
|
735
|
+
const state = e.kind === 'switch' ? (e.closed ? '<br>Closed' : '<br>Open') : '';
|
|
736
736
|
return {
|
|
737
|
-
html: `<div class="tt"><b>${esc(e.kind)} ${esc(e.from)} – ${esc(e.to)}</b
|
|
737
|
+
html: `<div class="tt"><b>${esc(e.kind)} ${esc(e.from)} – ${esc(e.to)}</b><br>${e.n_phases} φ${state}</div>`
|
|
738
738
|
};
|
|
739
739
|
}
|
|
740
740
|
if (layerId?.startsWith('multi-buses-')) {
|
|
741
741
|
const b = object as PlacedMultiBus;
|
|
742
|
-
const roles = b.attachmentKinds.length ? b.attachmentKinds.join(', ') : '
|
|
742
|
+
const roles = b.attachmentKinds.length ? b.attachmentKinds.join(', ') : 'None';
|
|
743
743
|
return {
|
|
744
|
-
html: `<div class="tt"><b>
|
|
745
|
-
${b.terminals.length} terminals${b.grounded.length ? ` ⋅ ${b.grounded.length} grounded` : ''}<br>
|
|
746
|
-
load ${b.load_kw.toFixed(0)} kW ⋅ gen ${b.gen_kw.toFixed(0)} kW<br>
|
|
747
|
-
<span style="opacity:0.6">${esc(roles)}</span></div>`
|
|
744
|
+
html: `<div class="tt"><b>Bus ${esc(b.id)}</b><br>${b.terminals.length} terminals${b.grounded.length ? `, ${b.grounded.length} grounded` : ''}<br>Load ${b.load_kw.toFixed(0)} kW<br>Generation ${b.gen_kw.toFixed(0)} kW<br>Attachments: ${esc(roles)}</div>`
|
|
748
745
|
};
|
|
749
746
|
}
|
|
750
747
|
const c = caseOf(info);
|
|
@@ -753,17 +750,15 @@
|
|
|
753
750
|
const b = object as NetworkBranch;
|
|
754
751
|
const flow = d
|
|
755
752
|
? `${((d.loading.get(b.id) ?? 0) * 100).toFixed(0)}% of ${b.rate_mw.toFixed(0)} MW`
|
|
756
|
-
: `
|
|
753
|
+
: `Rating ${b.rate_mw.toFixed(0)} MW`;
|
|
757
754
|
return {
|
|
758
|
-
html: `<div class="tt"><b>
|
|
755
|
+
html: `<div class="tt"><b>Line ${b.from} – ${b.to}</b><br>${flow}</div>`
|
|
759
756
|
};
|
|
760
757
|
}
|
|
761
758
|
const bus = object as NetworkBus;
|
|
762
759
|
if (!c) {
|
|
763
760
|
return {
|
|
764
|
-
html: `<div class="tt"><b>
|
|
765
|
-
load ${bus.demand_mw.toFixed(0)} MW ⋅ gen ${bus.gen_mw.toFixed(0)} MW<br>
|
|
766
|
-
<span style="opacity:0.6">local file</span></div>`
|
|
761
|
+
html: `<div class="tt"><b>Bus ${bus.id}</b><br>Load ${bus.demand_mw.toFixed(0)} MW<br>Generation ${bus.gen_mw.toFixed(0)} MW</div>`
|
|
767
762
|
};
|
|
768
763
|
}
|
|
769
764
|
const price = d?.prices.get(bus.id);
|
|
@@ -773,8 +768,8 @@
|
|
|
773
768
|
const delta = caseDeltas(c)[bus.id] ?? 0;
|
|
774
769
|
const loadRow =
|
|
775
770
|
delta === 0
|
|
776
|
-
? `
|
|
777
|
-
: `
|
|
771
|
+
? `Load ${bus.demand_mw.toFixed(0)} MW`
|
|
772
|
+
: `Load ${(bus.demand_mw + delta).toFixed(0)} MW (${delta > 0 ? '+' : ''}${delta.toFixed(0)})`;
|
|
778
773
|
const sensRow =
|
|
779
774
|
sens === undefined
|
|
780
775
|
? ''
|
|
@@ -783,8 +778,6 @@
|
|
|
783
778
|
preview === undefined
|
|
784
779
|
? ''
|
|
785
780
|
: `<br>Δ LMP ${preview >= 0 ? '+' : ''}${preview.toExponential(2)} objective units/MW`;
|
|
786
|
-
const localRow =
|
|
787
|
-
c instanceof CaseState ? '' : '<br><span style="opacity:0.6">local file</span>';
|
|
788
781
|
// Demand already has its own row below, so the demand colouring shows LMP here alone.
|
|
789
782
|
const scalarRow =
|
|
790
783
|
!d || d.scalarMode === 'price' || d.scalarMode === 'demand'
|
|
@@ -793,9 +786,7 @@
|
|
|
793
786
|
scalar === undefined ? '—' : displayFmt(d.scalarMode, scalar)
|
|
794
787
|
} ${d.scalarUnit}<br>LMP ${price === undefined ? '—' : fmt.format(price)} objective units/MW`;
|
|
795
788
|
return {
|
|
796
|
-
html: `<div class="tt"><b>
|
|
797
|
-
${scalarRow}<br>
|
|
798
|
-
${loadRow} ⋅ gen ${bus.gen_mw.toFixed(0)} MW${sensRow}${previewRow}${localRow}</div>`
|
|
789
|
+
html: `<div class="tt"><b>Bus ${bus.id}</b><br>${scalarRow}<br>${loadRow}<br>Generation ${bus.gen_mw.toFixed(0)} MW${sensRow}${previewRow}</div>`
|
|
799
790
|
};
|
|
800
791
|
}
|
|
801
792
|
|
|
@@ -992,25 +983,9 @@
|
|
|
992
983
|
|
|
993
984
|
map = m;
|
|
994
985
|
overlay = o;
|
|
995
|
-
|
|
996
|
-
//
|
|
997
|
-
|
|
998
|
-
// be lost; re-issuing it here ensures the first view is the active case,
|
|
999
|
-
// not the default world view. A rebuilt map opens at the default view
|
|
1000
|
-
// too, so it restores the camera the last map saved.
|
|
1001
|
-
if (restoreCamera && app.camera) m.jumpTo(app.camera);
|
|
1002
|
-
else if (app.cameraSeq === handledCameraSeq)
|
|
1003
|
-
app.requestFrame(app.activeCaseId ?? app.activeLocalId ?? 'all');
|
|
1004
|
-
if (mapGen > 0) {
|
|
1005
|
-
// This is a rebuilt map. Once it has held a live context without
|
|
1006
|
-
// another loss, zero the budget so the cap only ever catches a tight
|
|
1007
|
-
// loss loop, not recoveries spread across a long session.
|
|
1008
|
-
stabilizeTimer = setTimeout(() => {
|
|
1009
|
-
glRebuilds = 0;
|
|
1010
|
-
stabilizeTimer = null;
|
|
1011
|
-
}, 10000);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
986
|
+
// Assign the teardown before host code runs: if onMapReady throws,
|
|
987
|
+
// the map and its window and document listeners are still released.
|
|
988
|
+
let detachReady: void | (() => void);
|
|
1014
989
|
cleanup = () => {
|
|
1015
990
|
clearRebuild();
|
|
1016
991
|
clearStabilize();
|
|
@@ -1028,6 +1003,24 @@
|
|
|
1028
1003
|
map = null;
|
|
1029
1004
|
overlay = null;
|
|
1030
1005
|
};
|
|
1006
|
+
detachReady = onMapReady?.(m, o);
|
|
1007
|
+
// Frame the active grid once the map is ready. The initial frame request
|
|
1008
|
+
// (from the case load) can fire before the async map import finishes and
|
|
1009
|
+
// be lost; re-issuing it here ensures the first view is the active case,
|
|
1010
|
+
// not the default world view. A rebuilt map opens at the default view
|
|
1011
|
+
// too, so it restores the camera the last map saved.
|
|
1012
|
+
if (restoreCamera && app.camera) m.jumpTo(app.camera);
|
|
1013
|
+
else if (app.cameraSeq === handledCameraSeq)
|
|
1014
|
+
app.requestFrame(app.activeCaseId ?? app.activeLocalId ?? 'all');
|
|
1015
|
+
if (mapGen > 0) {
|
|
1016
|
+
// This is a rebuilt map. Once it has held a live context without
|
|
1017
|
+
// another loss, zero the budget so the cap only ever catches a tight
|
|
1018
|
+
// loss loop, not recoveries spread across a long session.
|
|
1019
|
+
stabilizeTimer = setTimeout(() => {
|
|
1020
|
+
glRebuilds = 0;
|
|
1021
|
+
stabilizeTimer = null;
|
|
1022
|
+
}, 10000);
|
|
1023
|
+
}
|
|
1031
1024
|
})
|
|
1032
1025
|
.catch((e) => {
|
|
1033
1026
|
if (!cancelled) app.error = `map failed to load: ${e instanceof Error ? e.message : e}`;
|
|
@@ -1542,14 +1535,14 @@
|
|
|
1542
1535
|
{#if app.placingId && map}
|
|
1543
1536
|
<button
|
|
1544
1537
|
type="button"
|
|
1545
|
-
class="place-at-center
|
|
1538
|
+
class="place-at-center"
|
|
1546
1539
|
onclick={() => {
|
|
1547
1540
|
if (!map) return;
|
|
1548
1541
|
const center = map.getCenter();
|
|
1549
1542
|
onplacecase(center.lng, center.lat);
|
|
1550
1543
|
}}
|
|
1551
1544
|
>
|
|
1552
|
-
|
|
1545
|
+
Place at map center
|
|
1553
1546
|
</button>
|
|
1554
1547
|
{/if}
|
|
1555
1548
|
|
|
@@ -1739,6 +1732,7 @@
|
|
|
1739
1732
|
border: 1px solid var(--accent);
|
|
1740
1733
|
border-radius: 3px;
|
|
1741
1734
|
color: var(--text-accent);
|
|
1735
|
+
font-family: inherit;
|
|
1742
1736
|
font-size: 11px;
|
|
1743
1737
|
cursor: pointer;
|
|
1744
1738
|
box-shadow: var(--elev-cue);
|
|
@@ -1816,8 +1810,9 @@
|
|
|
1816
1810
|
background: var(--panel) !important;
|
|
1817
1811
|
border: 1px solid var(--line);
|
|
1818
1812
|
color: var(--ink) !important;
|
|
1819
|
-
font-family: var(--font-
|
|
1820
|
-
font-size:
|
|
1813
|
+
font-family: var(--font-display);
|
|
1814
|
+
font-size: 12px;
|
|
1815
|
+
font-variant-numeric: tabular-nums;
|
|
1821
1816
|
line-height: 1.5;
|
|
1822
1817
|
padding: 8px 10px !important;
|
|
1823
1818
|
border-radius: 2px;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { untrack, type Snippet } from 'svelte';
|
|
2
|
+
import { onDestroy, untrack, type Snippet } from 'svelte';
|
|
3
3
|
import {
|
|
4
|
+
resolveCartoApiKey,
|
|
4
5
|
resolveTellegenUiConfig,
|
|
6
|
+
resolveTheme,
|
|
5
7
|
setAppState,
|
|
6
8
|
setController,
|
|
7
9
|
setUiConfig,
|
|
10
|
+
type TellegenUiConfig,
|
|
8
11
|
type TellegenUiOptions
|
|
9
12
|
} from './context.svelte.js';
|
|
10
13
|
import { createController } from './controller.svelte.js';
|
|
@@ -28,18 +31,28 @@
|
|
|
28
31
|
children
|
|
29
32
|
}: Props = $props();
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
// Read once, at mount: `apiBase` and `loadDefaultCases` configure the
|
|
35
|
+
// controller this provider owns, and the links and footer are fixed copy.
|
|
36
|
+
// `theme` and `cartoApiKey` stay live, so a host can switch them and the
|
|
37
|
+
// root attribute and the default shell's map follow.
|
|
38
|
+
const config: TellegenUiConfig = {
|
|
39
|
+
...untrack(() =>
|
|
40
|
+
resolveTellegenUiConfig({
|
|
41
|
+
apiBase,
|
|
42
|
+
loadDefaultCases,
|
|
43
|
+
docsHref,
|
|
44
|
+
orgHref,
|
|
45
|
+
orgLabel,
|
|
46
|
+
showFooter
|
|
47
|
+
})
|
|
48
|
+
),
|
|
49
|
+
get theme() {
|
|
50
|
+
return resolveTheme(theme);
|
|
51
|
+
},
|
|
52
|
+
get cartoApiKey() {
|
|
53
|
+
return resolveCartoApiKey(cartoApiKey);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
43
56
|
const app = createAppState();
|
|
44
57
|
const ctrl = createController(app, {
|
|
45
58
|
apiBase: config.apiBase,
|
|
@@ -54,6 +67,9 @@
|
|
|
54
67
|
setUiConfig(config);
|
|
55
68
|
setAppState(app);
|
|
56
69
|
setController(ctrl);
|
|
70
|
+
// The controller's Studies and multiconductor sessions live in the engine
|
|
71
|
+
// worker; release them with the provider.
|
|
72
|
+
onDestroy(() => ctrl.dispose());
|
|
57
73
|
|
|
58
74
|
// The dark tokens in styles.css hang off `:root[data-theme='dark']`. Set it
|
|
59
75
|
// only for dark, and only while mounted, so a light host is untouched. A
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
let { inline = false }: { inline?: boolean } = $props();
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
|
-
<footer class
|
|
11
|
-
<a href="/credits/">
|
|
10
|
+
<footer class:inline>
|
|
11
|
+
<a href="/credits/">Credits</a>
|
|
12
12
|
<i class="sep"></i>
|
|
13
|
-
<a href="/privacy/">
|
|
13
|
+
<a href="/privacy/">Privacy</a>
|
|
14
14
|
{#if ctrl.showFileDropUi}
|
|
15
15
|
<i class="sep filedrop-ui"></i>
|
|
16
16
|
<span class="drophint filedrop-ui"
|
|
17
|
-
><span class="arrow">⇣</span>
|
|
17
|
+
><span class="arrow">⇣</span> Drop a case or geographic file anywhere</span
|
|
18
18
|
>
|
|
19
19
|
{/if}
|
|
20
20
|
</footer>
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</svg>
|
|
34
34
|
<h1>tellegen</h1>
|
|
35
35
|
</div>
|
|
36
|
-
<nav class="cases" aria-label="
|
|
36
|
+
<nav class="cases" aria-label="Networks">
|
|
37
37
|
{#each app.cases as c (c.id)}
|
|
38
38
|
{@const [cname, cregion] = splitName(c.name)}
|
|
39
39
|
<div class="case-chip" class:active={app.activeCaseId === c.id}>
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
onclick={() => ctrl.activateCase(c.id)}
|
|
45
45
|
>
|
|
46
46
|
<span class="cname"
|
|
47
|
-
>{cname}{#if c.perturbed}<i class="mark" title="
|
|
47
|
+
>{cname}{#if c.perturbed}<i class="mark" title="Demand changed"></i>{/if}</span
|
|
48
48
|
>
|
|
49
49
|
<span class="cregion mono">{c.unavailableReason ? 'Unavailable' : cregion}</span>
|
|
50
50
|
</button>
|
|
51
51
|
<button
|
|
52
52
|
class="case-remove mono"
|
|
53
|
-
aria-label="
|
|
54
|
-
title="
|
|
53
|
+
aria-label="Remove {c.name}"
|
|
54
|
+
title="Remove {c.name}"
|
|
55
55
|
onclick={(e) => ctrl.removeBackendCase(c, e)}>✕</button
|
|
56
56
|
>
|
|
57
57
|
</div>
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
<div class="case-chip local" class:active={app.activeLocalId === c.id}>
|
|
61
61
|
<button class="case-activate" onclick={() => ctrl.activateLocal(c)}>
|
|
62
62
|
<span class="cname">{c.label}</span>
|
|
63
|
-
<span class="cregion mono">
|
|
63
|
+
<span class="cregion mono">Local</span>
|
|
64
64
|
</button>
|
|
65
65
|
<button
|
|
66
66
|
class="case-remove mono"
|
|
67
|
-
aria-label="
|
|
68
|
-
title="
|
|
67
|
+
aria-label="Remove {c.label}"
|
|
68
|
+
title="Remove {c.label}"
|
|
69
69
|
onclick={(e) => ctrl.removeLocalCase(c, e)}>✕</button
|
|
70
70
|
>
|
|
71
71
|
</div>
|
|
@@ -74,33 +74,29 @@
|
|
|
74
74
|
<div class="case-chip local" class:active={app.activeMultiId === c.id}>
|
|
75
75
|
<button class="case-activate" onclick={() => ctrl.activateMulti(c)}>
|
|
76
76
|
<span class="cname">{c.label}</span>
|
|
77
|
-
<span class="cregion mono">
|
|
77
|
+
<span class="cregion mono">Multiconductor</span>
|
|
78
78
|
</button>
|
|
79
79
|
<button
|
|
80
80
|
class="case-remove mono"
|
|
81
|
-
aria-label="
|
|
82
|
-
title="
|
|
81
|
+
aria-label="Remove {c.label}"
|
|
82
|
+
title="Remove {c.label}"
|
|
83
83
|
onclick={(e) => ctrl.removeMultiCase(c, e)}>✕</button
|
|
84
84
|
>
|
|
85
85
|
</div>
|
|
86
86
|
{/each}
|
|
87
|
-
<button
|
|
88
|
-
class="ghost filedrop-ui"
|
|
89
|
-
title="parsed in your browser; the file never uploads"
|
|
90
|
-
onclick={() => fileInput?.click()}
|
|
91
|
-
>
|
|
87
|
+
<button class="ghost filedrop-ui" onclick={() => fileInput?.click()}>
|
|
92
88
|
<span class="cname"
|
|
93
89
|
><span class="arrow">⇣</span>{ctrl.showFileDropUi
|
|
94
|
-
? '
|
|
95
|
-
: '
|
|
90
|
+
? 'Drop a case file'
|
|
91
|
+
: 'Open a case file'}</span
|
|
96
92
|
>
|
|
97
|
-
<span class="cregion mono">
|
|
93
|
+
<span class="cregion mono">Case or geo files</span>
|
|
98
94
|
</button>
|
|
99
95
|
</nav>
|
|
100
96
|
<span class="kicker mono">
|
|
101
97
|
<a href={config.orgHref} target="_blank" rel="noreferrer">{config.orgLabel}</a>
|
|
102
98
|
<i class="sep"></i>
|
|
103
|
-
<a href={config.docsHref} target="_blank" rel="noreferrer">
|
|
99
|
+
<a href={config.docsHref} target="_blank" rel="noreferrer">Docs</a>
|
|
104
100
|
</span>
|
|
105
101
|
<input
|
|
106
102
|
type="file"
|
|
@@ -30,22 +30,17 @@
|
|
|
30
30
|
|
|
31
31
|
{#if bindingLines.length > 0}
|
|
32
32
|
<div class="binding-lines">
|
|
33
|
-
<p
|
|
34
|
-
|
|
35
|
-
title="lines loaded to their thermal rating; select one to see LMP sensitivity to the rating"
|
|
36
|
-
>
|
|
37
|
-
binding lines
|
|
38
|
-
</p>
|
|
39
|
-
<ul class="mono">
|
|
33
|
+
<p class="dim head" title="Lines loaded to their thermal rating">Binding lines</p>
|
|
34
|
+
<ul>
|
|
40
35
|
{#each bindingLines as { branch, loading } (branch.id)}
|
|
41
36
|
<li>
|
|
42
37
|
<button
|
|
43
|
-
class="mono"
|
|
44
38
|
class:selected={app.selectedBranch === branch.id}
|
|
45
39
|
aria-pressed={app.selectedBranch === branch.id}
|
|
46
40
|
onclick={() => select(branch.id)}
|
|
47
41
|
>
|
|
48
|
-
|
|
42
|
+
Line <span class="mono">{branch.from} – {branch.to}</span>
|
|
43
|
+
<span class="mono load">{Math.round(loading * 100)}%</span>
|
|
49
44
|
</button>
|
|
50
45
|
</li>
|
|
51
46
|
{/each}
|
|
@@ -72,6 +67,8 @@
|
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
li button {
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: 4px;
|
|
75
72
|
width: 100%;
|
|
76
73
|
padding: 3px 6px 3px 8px;
|
|
77
74
|
text-align: left;
|
|
@@ -84,6 +81,10 @@
|
|
|
84
81
|
cursor: pointer;
|
|
85
82
|
}
|
|
86
83
|
|
|
84
|
+
.load {
|
|
85
|
+
margin-left: auto;
|
|
86
|
+
}
|
|
87
|
+
|
|
87
88
|
li button:hover {
|
|
88
89
|
background: var(--accent-soft);
|
|
89
90
|
}
|
|
@@ -89,14 +89,14 @@
|
|
|
89
89
|
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) open = false;
|
|
90
90
|
}}
|
|
91
91
|
>
|
|
92
|
-
<label class="
|
|
92
|
+
<label class="dim" for="bus-lookup-input">Bus lookup</label>
|
|
93
93
|
<input
|
|
94
94
|
id="bus-lookup-input"
|
|
95
95
|
class="mono"
|
|
96
96
|
type="text"
|
|
97
97
|
inputmode="numeric"
|
|
98
98
|
autocomplete="off"
|
|
99
|
-
placeholder="
|
|
99
|
+
placeholder="Bus ID"
|
|
100
100
|
role="combobox"
|
|
101
101
|
aria-keyshortcuts="/"
|
|
102
102
|
aria-expanded={open}
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
onkeydown={onKeydown}
|
|
115
115
|
/>
|
|
116
116
|
{#if open && results.length > 0}
|
|
117
|
-
<ul
|
|
117
|
+
<ul id="bus-lookup-listbox" role="listbox" aria-label="Matching buses">
|
|
118
118
|
{#each results as bus, i (bus.id)}
|
|
119
119
|
<li
|
|
120
120
|
id="bus-lookup-opt-{bus.id}"
|
|
@@ -126,12 +126,12 @@
|
|
|
126
126
|
commit(bus.id);
|
|
127
127
|
}}
|
|
128
128
|
>
|
|
129
|
-
|
|
129
|
+
Bus <span class="mono">{bus.id}</span>
|
|
130
130
|
</li>
|
|
131
131
|
{/each}
|
|
132
132
|
</ul>
|
|
133
133
|
{:else if open && query.trim() !== ''}
|
|
134
|
-
<p class="
|
|
134
|
+
<p class="dim empty">No matching bus</p>
|
|
135
135
|
{/if}
|
|
136
136
|
</div>
|
|
137
137
|
{/if}
|
|
@@ -156,7 +156,6 @@
|
|
|
156
156
|
label {
|
|
157
157
|
display: block;
|
|
158
158
|
font-size: 12px;
|
|
159
|
-
text-transform: uppercase;
|
|
160
159
|
margin-bottom: 3px;
|
|
161
160
|
}
|
|
162
161
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { tick, untrack } from 'svelte';
|
|
2
|
+
import { tick, untrack, type Snippet } from 'svelte';
|
|
3
3
|
import { getAppState, getController, getUiConfig } from '../context.svelte.js';
|
|
4
|
+
import type { LocalCase } from '../state.svelte.js';
|
|
4
5
|
import PanelFrame from './PanelFrame.svelte';
|
|
5
6
|
import SavedNetworkDetails from './SavedNetworkDetails.svelte';
|
|
6
7
|
import ModelDetails from './ModelDetails.svelte';
|
|
@@ -20,6 +21,9 @@
|
|
|
20
21
|
import SizeLegend from './SizeLegend.svelte';
|
|
21
22
|
import TopMovers from './TopMovers.svelte';
|
|
22
23
|
|
|
24
|
+
/** Optional host content rendered under a local case's stats. */
|
|
25
|
+
let { caseNote }: { caseNote?: Snippet<[LocalCase]> } = $props();
|
|
26
|
+
|
|
23
27
|
const app = getAppState();
|
|
24
28
|
const ctrl = getController();
|
|
25
29
|
const config = getUiConfig();
|
|
@@ -90,7 +94,7 @@
|
|
|
90
94
|
<SavedNetworkDetails />
|
|
91
95
|
{:else}
|
|
92
96
|
{#if app.parsingFile}
|
|
93
|
-
<p class="dim
|
|
97
|
+
<p class="dim blink">Parsing…</p>
|
|
94
98
|
{/if}
|
|
95
99
|
|
|
96
100
|
<!-- Selected equipment leads on narrow screens. -->
|
|
@@ -113,7 +117,7 @@
|
|
|
113
117
|
>
|
|
114
118
|
</div>
|
|
115
119
|
{/if}
|
|
116
|
-
<LocalCaseDetails />
|
|
120
|
+
<LocalCaseDetails note={caseNote} />
|
|
117
121
|
{/if}
|
|
118
122
|
{#if app.activeMulti}
|
|
119
123
|
<MulticonductorDetails />
|
|
@@ -121,18 +125,17 @@
|
|
|
121
125
|
{#if !ctrl.networkStats}
|
|
122
126
|
{#if !app.activeLocal && !app.activeMulti}
|
|
123
127
|
{#if ctrl.casesLoaded && app.cases.length === 0}
|
|
124
|
-
<p class="dim
|
|
125
|
-
{config.loadDefaultCases ? '
|
|
128
|
+
<p class="dim">
|
|
129
|
+
{config.loadDefaultCases ? 'No default cases loaded' : 'Drop a case file to begin'}
|
|
126
130
|
</p>
|
|
127
131
|
{#if config.loadDefaultCases}
|
|
128
|
-
<button class="reset
|
|
129
|
-
>restore default cases</button
|
|
132
|
+
<button class="reset" onclick={ctrl.restoreDefaultCases}>Restore default cases</button
|
|
130
133
|
>
|
|
131
134
|
{/if}
|
|
132
135
|
{:else if ctrl.loadingBackendCase}
|
|
133
|
-
<p class="dim
|
|
136
|
+
<p class="dim blink">Loading case…</p>
|
|
134
137
|
{:else}
|
|
135
|
-
<p class="dim
|
|
138
|
+
<p class="dim blink">Loading cases…</p>
|
|
136
139
|
{/if}
|
|
137
140
|
{/if}
|
|
138
141
|
{:else}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { LocalCase } from '../state.svelte.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
caseNote?: Snippet<[LocalCase]>;
|
|
5
|
+
};
|
|
6
|
+
declare const ControlPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
7
|
type ControlPanel = ReturnType<typeof ControlPanel>;
|
|
3
8
|
export default ControlPanel;
|