@tellegen/svelte 0.1.1 → 0.2.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 +15 -0
- package/dist/Sparkline.svelte.d.ts +0 -1
- package/dist/TellegenMap.svelte +109 -47
- package/dist/TellegenMap.svelte.d.ts +0 -1
- package/dist/TellegenProvider.svelte.d.ts +0 -1
- package/dist/TellegenShell.svelte +28 -3
- package/dist/TellegenShell.svelte.d.ts +0 -1
- package/dist/TellegenViewer.svelte.d.ts +0 -1
- package/dist/api.d.ts +0 -1
- package/dist/colors.d.ts +0 -1
- package/dist/components/AppFooter.svelte +26 -1
- package/dist/components/AppFooter.svelte.d.ts +5 -18
- package/dist/components/AppHeader.svelte +63 -3
- package/dist/components/AppHeader.svelte.d.ts +0 -1
- package/dist/components/BindingLines.svelte +13 -0
- package/dist/components/BindingLines.svelte.d.ts +0 -1
- package/dist/components/BusPicker.svelte +40 -0
- package/dist/components/BusPicker.svelte.d.ts +4 -2
- package/dist/components/ControlPanel.svelte +359 -72
- package/dist/components/ControlPanel.svelte.d.ts +2 -18
- package/dist/components/DemandSlider.svelte +28 -8
- package/dist/components/DemandSlider.svelte.d.ts +0 -1
- package/dist/components/DisplayControls.svelte.d.ts +0 -1
- package/dist/components/DropZone.svelte.d.ts +0 -1
- package/dist/components/FormulationSelector.svelte +17 -0
- package/dist/components/FormulationSelector.svelte.d.ts +0 -1
- package/dist/components/LocalCaseDetails.svelte +5 -2
- package/dist/components/LocalCaseDetails.svelte.d.ts +0 -1
- package/dist/components/MulticonductorDetails.svelte +3 -3
- package/dist/components/MulticonductorDetails.svelte.d.ts +0 -1
- package/dist/components/NetworkStats.svelte.d.ts +0 -1
- package/dist/components/PlacementCue.svelte.d.ts +0 -1
- package/dist/components/RatingSlider.svelte.d.ts +0 -1
- package/dist/components/RestoreDefaultsButton.svelte.d.ts +0 -1
- package/dist/components/SensitivityReadout.svelte +10 -1
- package/dist/components/SensitivityReadout.svelte.d.ts +0 -1
- package/dist/components/SizeLegend.svelte.d.ts +0 -1
- package/dist/components/SolveCard.svelte +2 -1
- package/dist/components/SolveCard.svelte.d.ts +0 -1
- package/dist/components/TopMovers.svelte.d.ts +0 -1
- package/dist/components.d.ts +0 -1
- package/dist/context.svelte.d.ts +0 -1
- package/dist/controller.svelte.d.ts +24 -12
- package/dist/controller.svelte.js +218 -101
- package/dist/display.d.ts +0 -1
- 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/index.d.ts +3 -5
- package/dist/index.js +2 -2
- package/dist/map-bounds.d.ts +5 -0
- package/dist/map-bounds.js +41 -0
- package/dist/map.d.ts +0 -1
- package/dist/multiconductor.d.ts +0 -1
- package/dist/state.svelte.d.ts +13 -1
- package/dist/state.svelte.js +13 -0
- package/dist/study-package.d.ts +0 -1
- package/dist/styles.css +16 -2
- package/dist/synthetic-layout.d.ts +0 -1
- package/dist/synthetic-layout.js +16 -2
- package/dist/transformer-icon.d.ts +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +14 -11
- 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.map +0 -1
- 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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { getAppState, getController, getUiConfig } from '../context.svelte.js';
|
|
4
|
+
import { splitName } from '../format.js';
|
|
5
|
+
import AppFooter from './AppFooter.svelte';
|
|
3
6
|
import BindingLines from './BindingLines.svelte';
|
|
7
|
+
import BusPicker from './BusPicker.svelte';
|
|
4
8
|
import DemandSlider from './DemandSlider.svelte';
|
|
5
9
|
import DisplayControls from './DisplayControls.svelte';
|
|
6
10
|
import FormulationSelector from './FormulationSelector.svelte';
|
|
@@ -15,81 +19,297 @@
|
|
|
15
19
|
const app = getAppState();
|
|
16
20
|
const ctrl = getController();
|
|
17
21
|
const config = getUiConfig();
|
|
22
|
+
|
|
23
|
+
// Bottom sheet heights, compact layout only. peek is measured from the grab
|
|
24
|
+
// bar; half and full are fractions of the viewport.
|
|
25
|
+
type Snap = 'peek' | 'half' | 'full';
|
|
26
|
+
const SNAPS: Snap[] = ['peek', 'half', 'full'];
|
|
27
|
+
const SNAP_FRACTION: Record<Snap, number> = { peek: 0, half: 0.46, full: 0.92 };
|
|
28
|
+
/** Below this the viewport is a landscape phone and half has to give up room. */
|
|
29
|
+
const SHORT_VIEWPORT_PX = 560;
|
|
30
|
+
/** px/ms above which a release snaps in the drag direction instead of to the nearest. */
|
|
31
|
+
const FLICK_SPEED = 0.35;
|
|
32
|
+
/** Map left visible above a full sheet, on top of the header it also clears. */
|
|
33
|
+
const FULL_SNAP_MAP_BAND = 14;
|
|
34
|
+
|
|
35
|
+
let snap = $state<Snap>('half');
|
|
36
|
+
let headEl = $state.raw<HTMLElement | undefined>(undefined);
|
|
37
|
+
let bodyEl = $state.raw<HTMLElement | undefined>(undefined);
|
|
38
|
+
let headHeight = $state(52);
|
|
39
|
+
/** Height under an active drag; null when resting on a snap. */
|
|
40
|
+
let dragHeight = $state<number | null>(null);
|
|
41
|
+
|
|
42
|
+
const fractionFor = (s: Snap) =>
|
|
43
|
+
s === 'half' && app.viewportHeight < SHORT_VIEWPORT_PX ? 0.38 : SNAP_FRACTION[s];
|
|
44
|
+
|
|
45
|
+
const snapHeight = (s: Snap) =>
|
|
46
|
+
s === 'peek'
|
|
47
|
+
? headHeight
|
|
48
|
+
: Math.min(
|
|
49
|
+
Math.round(app.viewportHeight * fractionFor(s)),
|
|
50
|
+
app.viewportHeight - app.headerInset - FULL_SNAP_MAP_BAND
|
|
51
|
+
);
|
|
52
|
+
const sheetHeight = $derived(dragHeight ?? snapHeight(snap));
|
|
53
|
+
|
|
54
|
+
const caseLabel = $derived(
|
|
55
|
+
app.active
|
|
56
|
+
? splitName(app.active.name)[0]
|
|
57
|
+
: (app.activeLocal?.label ?? app.activeMulti?.label ?? '')
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/** Single line shown on the grab bar while collapsed. */
|
|
61
|
+
const summary = $derived.by(() => {
|
|
62
|
+
if (app.error) return 'error — open for details';
|
|
63
|
+
if (app.selectedBus !== null) return `bus ${app.selectedBus} · ∂LMP/∂d`;
|
|
64
|
+
if (app.selectedBranch !== null) return `line ${app.selectedBranch} · ∂LMP/∂rating`;
|
|
65
|
+
const stats = ctrl.networkStats;
|
|
66
|
+
if (!stats) return app.parsingFile ? 'parsing…' : 'loading…';
|
|
67
|
+
return `${caseLabel} · ${stats.buses} buses · ${stats.branches} lines`;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const expandLabel = $derived(
|
|
71
|
+
snap === 'full' ? 'collapse the control panel' : 'expand the control panel'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/** A selection whose readout has resolved (or is resolving). */
|
|
75
|
+
const busReadout = $derived(
|
|
76
|
+
app.selectedBus !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)
|
|
77
|
+
);
|
|
78
|
+
const branchReadout = $derived(
|
|
79
|
+
app.selectedBranch !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)
|
|
80
|
+
);
|
|
81
|
+
/** Compact only: a resolved selection outranks the case stats it was read
|
|
82
|
+
* against. Gated on networkStats because that is what renders the readout. */
|
|
83
|
+
const selectionLeads = $derived(
|
|
84
|
+
app.compactLayout && !!ctrl.networkStats && (busReadout || branchReadout)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
$effect(() => {
|
|
88
|
+
app.sheetInset = app.compactLayout ? sheetHeight : 0;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// peek height tracks the grab bar, which resizes when the summary line changes.
|
|
92
|
+
$effect(() => {
|
|
93
|
+
const el = headEl;
|
|
94
|
+
if (!el) return;
|
|
95
|
+
const measure = () => (headHeight = Math.round(el.getBoundingClientRect().height));
|
|
96
|
+
measure();
|
|
97
|
+
const observer = new ResizeObserver(measure);
|
|
98
|
+
observer.observe(el);
|
|
99
|
+
return () => observer.disconnect();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// A map selection renders the sensitivity readout and the demand control, both
|
|
103
|
+
// of which are below the fold at peek. The body also has to go back to the top:
|
|
104
|
+
// reaching the bus lookup scrolls it down, and the readout that replaces it as
|
|
105
|
+
// the first block would otherwise open out of view.
|
|
106
|
+
$effect(() => {
|
|
107
|
+
const selected = app.selectedBus ?? app.selectedBranch;
|
|
108
|
+
if (selected === null) return;
|
|
109
|
+
untrack(() => {
|
|
110
|
+
if (!app.compactLayout) return;
|
|
111
|
+
if (snap === 'peek') snap = 'half';
|
|
112
|
+
if (bodyEl) bodyEl.scrollTop = 0;
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function cycle() {
|
|
117
|
+
snap = snap === 'peek' ? 'half' : snap === 'half' ? 'full' : 'peek';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function nearestSnap(height: number): Snap {
|
|
121
|
+
return SNAPS.reduce((best, s) =>
|
|
122
|
+
Math.abs(snapHeight(s) - height) < Math.abs(snapHeight(best) - height) ? s : best
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let dragStartY = 0;
|
|
127
|
+
let dragStartHeight = 0;
|
|
128
|
+
let dragMoved = false;
|
|
129
|
+
let lastY = 0;
|
|
130
|
+
let lastT = 0;
|
|
131
|
+
let speed = 0;
|
|
132
|
+
|
|
133
|
+
function onPointerDown(e: PointerEvent) {
|
|
134
|
+
if (!app.compactLayout || !e.isPrimary) return;
|
|
135
|
+
if (e.currentTarget instanceof HTMLElement) e.currentTarget.setPointerCapture(e.pointerId);
|
|
136
|
+
dragStartY = e.clientY;
|
|
137
|
+
dragStartHeight = sheetHeight;
|
|
138
|
+
dragHeight = dragStartHeight;
|
|
139
|
+
dragMoved = false;
|
|
140
|
+
speed = 0;
|
|
141
|
+
lastY = e.clientY;
|
|
142
|
+
lastT = e.timeStamp;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function onPointerMove(e: PointerEvent) {
|
|
146
|
+
if (dragHeight === null) return;
|
|
147
|
+
const dy = dragStartY - e.clientY;
|
|
148
|
+
if (Math.abs(dy) > 4) dragMoved = true;
|
|
149
|
+
const dt = e.timeStamp - lastT;
|
|
150
|
+
if (dt > 0) speed = (lastY - e.clientY) / dt;
|
|
151
|
+
lastY = e.clientY;
|
|
152
|
+
lastT = e.timeStamp;
|
|
153
|
+
dragHeight = Math.max(
|
|
154
|
+
snapHeight('peek'),
|
|
155
|
+
Math.min(snapHeight('full'), dragStartHeight + dy)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onPointerUp() {
|
|
160
|
+
if (dragHeight === null) return;
|
|
161
|
+
const height = dragHeight;
|
|
162
|
+
dragHeight = null;
|
|
163
|
+
if (!dragMoved) return; // a tap; the click handler cycles
|
|
164
|
+
const landed = nearestSnap(height);
|
|
165
|
+
if (Math.abs(speed) > FLICK_SPEED) {
|
|
166
|
+
const next = SNAPS.indexOf(landed) + (speed > 0 ? 1 : -1);
|
|
167
|
+
snap = SNAPS[Math.max(0, Math.min(SNAPS.length - 1, next))];
|
|
168
|
+
} else {
|
|
169
|
+
snap = landed;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function onHeadKeydown(e: KeyboardEvent) {
|
|
174
|
+
const step = e.key === 'ArrowUp' ? 1 : e.key === 'ArrowDown' ? -1 : 0;
|
|
175
|
+
if (step === 0) return;
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
const next = SNAPS.indexOf(snap) + step;
|
|
178
|
+
snap = SNAPS[Math.max(0, Math.min(SNAPS.length - 1, next))];
|
|
179
|
+
}
|
|
18
180
|
</script>
|
|
19
181
|
|
|
20
|
-
<aside
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
182
|
+
<aside
|
|
183
|
+
class="panel"
|
|
184
|
+
class:sheet={app.compactLayout}
|
|
185
|
+
class:dragging={dragHeight !== null}
|
|
186
|
+
style:height={app.compactLayout ? `${sheetHeight}px` : null}
|
|
187
|
+
>
|
|
188
|
+
{#if app.compactLayout}
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
class="sheet-head"
|
|
192
|
+
bind:this={headEl}
|
|
193
|
+
aria-expanded={snap !== 'peek'}
|
|
194
|
+
aria-controls="control-panel-body"
|
|
195
|
+
aria-label={expandLabel}
|
|
196
|
+
onpointerdown={onPointerDown}
|
|
197
|
+
onpointermove={onPointerMove}
|
|
198
|
+
onpointerup={onPointerUp}
|
|
199
|
+
onpointercancel={onPointerUp}
|
|
200
|
+
onkeydown={onHeadKeydown}
|
|
201
|
+
onclick={() => {
|
|
202
|
+
// click fires after pointerup; a keyboard activation leaves dragMoved
|
|
203
|
+
// unset, so clear it here rather than in the pointer handlers
|
|
204
|
+
const moved = dragMoved;
|
|
205
|
+
dragMoved = false;
|
|
206
|
+
if (!moved) cycle();
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<span class="grabber" aria-hidden="true"></span>
|
|
210
|
+
{#if snap === 'peek'}
|
|
211
|
+
<span class="sheet-summary mono">{summary}</span>
|
|
212
|
+
{/if}
|
|
213
|
+
</button>
|
|
36
214
|
{/if}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
215
|
+
|
|
216
|
+
<div class="panel-body" id="control-panel-body" bind:this={bodyEl}>
|
|
217
|
+
{#snippet selectionBlock()}
|
|
218
|
+
{#if busReadout}
|
|
219
|
+
<SensitivityReadout />
|
|
220
|
+
|
|
221
|
+
<DemandSlider />
|
|
222
|
+
|
|
223
|
+
{#if ctrl.showMoverSlot}
|
|
224
|
+
<TopMovers />
|
|
225
|
+
{/if}
|
|
226
|
+
{:else if branchReadout}
|
|
227
|
+
<SensitivityReadout />
|
|
228
|
+
|
|
229
|
+
<RatingSlider />
|
|
230
|
+
|
|
231
|
+
{#if ctrl.showMoverSlot}
|
|
232
|
+
<TopMovers />
|
|
47
233
|
{/if}
|
|
48
|
-
{:else if ctrl.loadingBackendCase}
|
|
49
|
-
<p class="dim mono blink">loading selected case…</p>
|
|
50
234
|
{:else}
|
|
51
|
-
<
|
|
235
|
+
<DisplayControls />
|
|
52
236
|
{/if}
|
|
237
|
+
{/snippet}
|
|
238
|
+
|
|
239
|
+
{#if app.error}
|
|
240
|
+
<p class="error mono">{app.error}</p>
|
|
241
|
+
<div class="error-actions">
|
|
242
|
+
<button class="reset mono" onclick={ctrl.retryError}>retry</button>
|
|
243
|
+
<button class="reset mono" onclick={() => (app.error = null)}>dismiss</button>
|
|
244
|
+
</div>
|
|
53
245
|
{/if}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<NetworkStats />
|
|
246
|
+
{#if app.parsingFile}
|
|
247
|
+
<p class="dim mono blink">parsing…</p>
|
|
57
248
|
{/if}
|
|
58
249
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{
|
|
250
|
+
<!-- The sheet shows a few hundred px at most, so what the last tap produced
|
|
251
|
+
leads; the case provenance and stats it was read against follow. -->
|
|
252
|
+
{#if selectionLeads}
|
|
253
|
+
{@render selectionBlock()}
|
|
62
254
|
|
|
63
|
-
|
|
64
|
-
<BindingLines />
|
|
255
|
+
<hr />
|
|
65
256
|
{/if}
|
|
66
257
|
|
|
67
|
-
|
|
258
|
+
{#if app.activeLocal}
|
|
259
|
+
<LocalCaseDetails />
|
|
260
|
+
{/if}
|
|
261
|
+
{#if app.activeMulti}
|
|
262
|
+
<MulticonductorDetails />
|
|
263
|
+
{/if}
|
|
264
|
+
{#if !ctrl.networkStats}
|
|
265
|
+
{#if !app.error && !app.activeLocal && !app.activeMulti}
|
|
266
|
+
{#if ctrl.casesLoaded && app.cases.length === 0}
|
|
267
|
+
<p class="dim mono">
|
|
268
|
+
{config.loadDefaultCases ? 'no default cases loaded' : 'drop a case file to begin'}
|
|
269
|
+
</p>
|
|
270
|
+
{#if config.loadDefaultCases}
|
|
271
|
+
<button class="reset mono" onclick={ctrl.restoreDefaultCases}
|
|
272
|
+
>restore default cases</button
|
|
273
|
+
>
|
|
274
|
+
{/if}
|
|
275
|
+
{:else if ctrl.loadingBackendCase}
|
|
276
|
+
<p class="dim mono blink">loading selected case…</p>
|
|
277
|
+
{:else}
|
|
278
|
+
<p class="dim mono blink">loading cases…</p>
|
|
279
|
+
{/if}
|
|
280
|
+
{/if}
|
|
281
|
+
{:else}
|
|
282
|
+
{#if !app.activeLocal}
|
|
283
|
+
<NetworkStats />
|
|
284
|
+
{/if}
|
|
68
285
|
|
|
69
|
-
|
|
70
|
-
|
|
286
|
+
{#if ctrl.activeSolvable}
|
|
287
|
+
<FormulationSelector />
|
|
288
|
+
{/if}
|
|
71
289
|
|
|
72
|
-
|
|
290
|
+
{#if app.compactLayout}
|
|
291
|
+
<BusPicker inline />
|
|
292
|
+
{/if}
|
|
73
293
|
|
|
74
|
-
{#if
|
|
75
|
-
<
|
|
294
|
+
{#if !app.placingLocalId}
|
|
295
|
+
<BindingLines />
|
|
76
296
|
{/if}
|
|
77
|
-
{:else if app.selectedBranch !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)}
|
|
78
|
-
<SensitivityReadout />
|
|
79
297
|
|
|
80
|
-
|
|
298
|
+
{#if !selectionLeads}
|
|
299
|
+
<hr />
|
|
81
300
|
|
|
82
|
-
|
|
83
|
-
<TopMovers />
|
|
301
|
+
{@render selectionBlock()}
|
|
84
302
|
{/if}
|
|
85
|
-
{:else}
|
|
86
|
-
<DisplayControls />
|
|
87
|
-
{/if}
|
|
88
303
|
|
|
89
|
-
|
|
304
|
+
<hr />
|
|
90
305
|
|
|
91
|
-
|
|
92
|
-
|
|
306
|
+
<SizeLegend />
|
|
307
|
+
{/if}
|
|
308
|
+
|
|
309
|
+
{#if app.compactLayout && config.showFooter}
|
|
310
|
+
<AppFooter inline />
|
|
311
|
+
{/if}
|
|
312
|
+
</div>
|
|
93
313
|
</aside>
|
|
94
314
|
|
|
95
315
|
<style>
|
|
@@ -98,10 +318,11 @@
|
|
|
98
318
|
top: 76px;
|
|
99
319
|
left: 20px;
|
|
100
320
|
z-index: 10;
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-direction: column;
|
|
101
323
|
width: 312px;
|
|
102
324
|
max-height: calc(100% - 122px);
|
|
103
|
-
overflow
|
|
104
|
-
padding: 16px 18px;
|
|
325
|
+
overflow: hidden;
|
|
105
326
|
background: var(--panel);
|
|
106
327
|
border: 1px solid var(--line);
|
|
107
328
|
border-radius: 3px;
|
|
@@ -110,6 +331,16 @@
|
|
|
110
331
|
animation: rise 0.5s 0.12s ease-out both;
|
|
111
332
|
}
|
|
112
333
|
|
|
334
|
+
.panel-body {
|
|
335
|
+
flex: 1 1 auto;
|
|
336
|
+
min-height: 0;
|
|
337
|
+
overflow-y: auto;
|
|
338
|
+
/* A flick that runs past the end of the panel must not scroll the page or
|
|
339
|
+
pan the map underneath. */
|
|
340
|
+
overscroll-behavior: contain;
|
|
341
|
+
padding: 16px 18px;
|
|
342
|
+
}
|
|
343
|
+
|
|
113
344
|
hr {
|
|
114
345
|
border: 0;
|
|
115
346
|
border-top: 1px solid var(--line);
|
|
@@ -126,24 +357,80 @@
|
|
|
126
357
|
gap: 6px;
|
|
127
358
|
}
|
|
128
359
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
360
|
+
/* The grab bar: a 44px strip that drags the sheet and cycles it on tap. */
|
|
361
|
+
.sheet-head {
|
|
362
|
+
flex: 0 0 auto;
|
|
363
|
+
display: flex;
|
|
364
|
+
flex-direction: column;
|
|
365
|
+
align-items: center;
|
|
366
|
+
justify-content: center;
|
|
367
|
+
gap: 7px;
|
|
368
|
+
width: 100%;
|
|
369
|
+
min-height: 44px;
|
|
370
|
+
padding: 9px 16px 11px;
|
|
371
|
+
background: none;
|
|
372
|
+
border: 0;
|
|
373
|
+
border-bottom: 1px solid transparent;
|
|
374
|
+
color: inherit;
|
|
375
|
+
font: inherit;
|
|
376
|
+
cursor: grab;
|
|
377
|
+
/* the browser must not claim the vertical gesture */
|
|
378
|
+
touch-action: none;
|
|
140
379
|
}
|
|
141
380
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
381
|
+
.sheet-head:active {
|
|
382
|
+
cursor: grabbing;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.grabber {
|
|
386
|
+
width: 40px;
|
|
387
|
+
height: 4px;
|
|
388
|
+
border-radius: 2px;
|
|
389
|
+
background: var(--line);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.sheet-head:hover .grabber,
|
|
393
|
+
.sheet-head:focus-visible .grabber {
|
|
394
|
+
background: var(--accent);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.sheet-summary {
|
|
398
|
+
max-width: 100%;
|
|
399
|
+
overflow: hidden;
|
|
400
|
+
font-size: 11.5px;
|
|
401
|
+
color: var(--text-secondary);
|
|
402
|
+
text-overflow: ellipsis;
|
|
403
|
+
white-space: nowrap;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.sheet {
|
|
407
|
+
top: auto;
|
|
408
|
+
right: 0;
|
|
409
|
+
bottom: 0;
|
|
410
|
+
left: 0;
|
|
411
|
+
width: auto;
|
|
412
|
+
max-height: none;
|
|
413
|
+
background: var(--paper);
|
|
414
|
+
border-width: 1px 0 0;
|
|
415
|
+
border-radius: 12px 12px 0 0;
|
|
416
|
+
box-shadow: 0 -6px 28px rgba(32, 36, 43, 0.14);
|
|
417
|
+
transition: height var(--dur-med) var(--ease-out);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.sheet .panel-body {
|
|
421
|
+
padding: 0 16px 14px;
|
|
422
|
+
/* clear the home indicator on a phone with no bezel */
|
|
423
|
+
padding-bottom: max(14px, env(safe-area-inset-bottom));
|
|
424
|
+
-webkit-overflow-scrolling: touch;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.sheet .sheet-head {
|
|
428
|
+
border-bottom-color: var(--line);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* A dragged sheet tracks the finger; the snap animation is for releases. */
|
|
432
|
+
.sheet.dragging {
|
|
433
|
+
transition: none;
|
|
147
434
|
}
|
|
148
435
|
|
|
149
436
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
$$bindings?: Bindings;
|
|
4
|
-
} & Exports;
|
|
5
|
-
(internal: unknown, props: {
|
|
6
|
-
$$events?: Events;
|
|
7
|
-
$$slots?: Slots;
|
|
8
|
-
}): Exports & {
|
|
9
|
-
$set?: any;
|
|
10
|
-
$on?: any;
|
|
11
|
-
};
|
|
12
|
-
z_$$bindings?: Bindings;
|
|
13
|
-
}
|
|
14
|
-
declare const ControlPanel: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
-
[evt: string]: CustomEvent<any>;
|
|
16
|
-
}, {}, {}, string>;
|
|
17
|
-
type ControlPanel = InstanceType<typeof ControlPanel>;
|
|
1
|
+
declare const ControlPanel: import("svelte").Component<Record<string, never>, {}, "">;
|
|
2
|
+
type ControlPanel = ReturnType<typeof ControlPanel>;
|
|
18
3
|
export default ControlPanel;
|
|
19
|
-
//# sourceMappingURL=ControlPanel.svelte.d.ts.map
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const app = getAppState();
|
|
6
6
|
const ctrl = getController();
|
|
7
|
+
const disabled = $derived(ctrl.sliderDisabled);
|
|
7
8
|
|
|
8
9
|
// Fill from the committed demand to the live thumb. The local number line stays
|
|
9
10
|
// stable after release; only the committed tick moves.
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
const fillHi = $derived(valFrac < neutralFrac ? neutralPos : valPos);
|
|
24
25
|
const sliderTip =
|
|
25
26
|
'The black tick marks the last committed demand. Drag the knob to preview a new demand; release to solve and move the tick to that point.';
|
|
27
|
+
const disabledTip = 'this analysis bus is display-only; demand is not adjustable';
|
|
26
28
|
const scoreTip =
|
|
27
29
|
'Gradient is the estimate of total cost change versus base before the solve finishes. It uses the selected bus LMP times the demand step, plus local curvature when the preview engine is unavailable. Exact is the resolved OPF objective change.';
|
|
28
30
|
</script>
|
|
@@ -39,32 +41,41 @@
|
|
|
39
41
|
<button
|
|
40
42
|
type="button"
|
|
41
43
|
class:active={app.demandRangeMode === 'local'}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
aria-pressed={app.demandRangeMode === 'local'}
|
|
45
|
+
aria-label="nearby demand range"
|
|
46
|
+
disabled={disabled}
|
|
44
47
|
title="range near the selected demand setting"
|
|
45
48
|
onclick={() => ctrl.setDemandRangeMode('local')}>nearby</button
|
|
46
49
|
>
|
|
47
50
|
<button
|
|
48
51
|
type="button"
|
|
49
52
|
class:active={app.demandRangeMode === 'full'}
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
aria-pressed={app.demandRangeMode === 'full'}
|
|
54
|
+
aria-label="full demand range"
|
|
55
|
+
disabled={disabled}
|
|
52
56
|
title="range from zero load to the local physical limit"
|
|
53
57
|
onclick={() => ctrl.setDemandRangeMode('full')}>full range</button
|
|
54
58
|
>
|
|
55
59
|
</div>
|
|
56
|
-
|
|
60
|
+
<span class="mono dim">
|
|
61
|
+
{#if disabled}
|
|
62
|
+
not adjustable
|
|
63
|
+
{:else}
|
|
64
|
+
{fmt.format(ctrl.sliderMin)} to {fmt.format(ctrl.sliderMax)} MW
|
|
65
|
+
{/if}
|
|
66
|
+
</span>
|
|
57
67
|
</div>
|
|
58
68
|
<div
|
|
59
69
|
class="slider-track"
|
|
60
70
|
style="--fill-lo:{fillLo}; --fill-hi:{fillHi}; --neutral-pos:{neutralPos}"
|
|
61
|
-
|
|
71
|
+
title={disabled ? disabledTip : sliderTip}
|
|
62
72
|
>
|
|
63
73
|
<input
|
|
64
74
|
type="range"
|
|
65
75
|
min={ctrl.sliderMin}
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
max={ctrl.sliderMax}
|
|
77
|
+
step="any"
|
|
78
|
+
disabled={disabled}
|
|
68
79
|
bind:value={ctrl.sliderCurrent, ctrl.setSliderPreview}
|
|
69
80
|
aria-label="demand delta at selected bus"
|
|
70
81
|
onpointerdown={() => ctrl.setSliderPreview(ctrl.sliderValue)}
|
|
@@ -173,6 +184,15 @@
|
|
|
173
184
|
margin: 0;
|
|
174
185
|
}
|
|
175
186
|
|
|
187
|
+
input[type='range']:disabled {
|
|
188
|
+
opacity: 0.45;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.range-mode button:disabled {
|
|
192
|
+
cursor: default;
|
|
193
|
+
opacity: 0.45;
|
|
194
|
+
}
|
|
195
|
+
|
|
176
196
|
input[type='range']::-webkit-slider-thumb {
|
|
177
197
|
-webkit-appearance: none;
|
|
178
198
|
appearance: none;
|
|
@@ -79,4 +79,21 @@
|
|
|
79
79
|
opacity: 0.55;
|
|
80
80
|
cursor: progress;
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
@media (hover: none), (pointer: coarse) {
|
|
84
|
+
/* The select is sized to its longest option, which runs off a 320px screen.
|
|
85
|
+
Let it shrink around the current value rather than truncate the label.
|
|
86
|
+
16px also keeps Safari from zooming the layout viewport on focus. */
|
|
87
|
+
.formulation-row select {
|
|
88
|
+
flex: 0 1 auto;
|
|
89
|
+
min-width: 0;
|
|
90
|
+
min-height: 44px;
|
|
91
|
+
font-size: 16px;
|
|
92
|
+
padding-block: 6px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.formulation-row > span {
|
|
96
|
+
flex: 0 0 auto;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
82
99
|
</style>
|
|
@@ -16,4 +16,3 @@ declare const FormulationSelector: $$__sveltets_2_IsomorphicComponent<Record<str
|
|
|
16
16
|
}, {}, {}, string>;
|
|
17
17
|
type FormulationSelector = InstanceType<typeof FormulationSelector>;
|
|
18
18
|
export default FormulationSelector;
|
|
19
|
-
//# sourceMappingURL=FormulationSelector.svelte.d.ts.map
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
const app = getAppState();
|
|
7
7
|
const ctrl = getController();
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Export tokens the committed state writes to; labels carry their extensions.
|
|
10
|
+
// `model-json` is powerio's own model document, not a case format.
|
|
10
11
|
const EXPORT_FORMATS = [
|
|
11
12
|
{ token: 'matpower', label: 'MATPOWER (.m)' },
|
|
12
13
|
{ token: 'psse', label: 'PSS/E (.raw)' },
|
|
14
|
+
{ token: 'powerworld', label: 'PowerWorld (.aux)' },
|
|
15
|
+
{ token: 'pslf', label: 'PSLF (.epc)' },
|
|
13
16
|
{ token: 'powermodels-json', label: 'PowerModels (.json)' },
|
|
14
17
|
{ token: 'pandapower-json', label: 'pandapower (.json)' },
|
|
15
|
-
{ token: '
|
|
18
|
+
{ token: 'model-json', label: 'PowerIO model (.json)' }
|
|
16
19
|
];
|
|
17
20
|
|
|
18
21
|
let busy = $state(false);
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
<dt>sources / loads / gens</dt>
|
|
55
55
|
<dd>{s.n_source} / {s.n_load} / {s.n_generator}</dd>
|
|
56
56
|
</div>
|
|
57
|
-
{#if s.n_ibr > 0 || s.n_shunt > 0}
|
|
57
|
+
{#if s.n_ibr > 0 || s.n_shunt > 0 || (s.n_capacitor ?? 0) > 0}
|
|
58
58
|
<div>
|
|
59
|
-
<dt>IBRs / shunts</dt>
|
|
60
|
-
<dd>{s.n_ibr} / {s.n_shunt}</dd>
|
|
59
|
+
<dt>IBRs / shunts / caps</dt>
|
|
60
|
+
<dd>{s.n_ibr} / {s.n_shunt} / {s.n_capacitor ?? 0}</dd>
|
|
61
61
|
</div>
|
|
62
62
|
{/if}
|
|
63
63
|
</dl>
|