@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.
Files changed (105) hide show
  1. package/README.md +15 -0
  2. package/dist/Sparkline.svelte.d.ts +0 -1
  3. package/dist/TellegenMap.svelte +109 -47
  4. package/dist/TellegenMap.svelte.d.ts +0 -1
  5. package/dist/TellegenProvider.svelte.d.ts +0 -1
  6. package/dist/TellegenShell.svelte +28 -3
  7. package/dist/TellegenShell.svelte.d.ts +0 -1
  8. package/dist/TellegenViewer.svelte.d.ts +0 -1
  9. package/dist/api.d.ts +0 -1
  10. package/dist/colors.d.ts +0 -1
  11. package/dist/components/AppFooter.svelte +26 -1
  12. package/dist/components/AppFooter.svelte.d.ts +5 -18
  13. package/dist/components/AppHeader.svelte +63 -3
  14. package/dist/components/AppHeader.svelte.d.ts +0 -1
  15. package/dist/components/BindingLines.svelte +13 -0
  16. package/dist/components/BindingLines.svelte.d.ts +0 -1
  17. package/dist/components/BusPicker.svelte +40 -0
  18. package/dist/components/BusPicker.svelte.d.ts +4 -2
  19. package/dist/components/ControlPanel.svelte +359 -72
  20. package/dist/components/ControlPanel.svelte.d.ts +2 -18
  21. package/dist/components/DemandSlider.svelte +28 -8
  22. package/dist/components/DemandSlider.svelte.d.ts +0 -1
  23. package/dist/components/DisplayControls.svelte.d.ts +0 -1
  24. package/dist/components/DropZone.svelte.d.ts +0 -1
  25. package/dist/components/FormulationSelector.svelte +17 -0
  26. package/dist/components/FormulationSelector.svelte.d.ts +0 -1
  27. package/dist/components/LocalCaseDetails.svelte +5 -2
  28. package/dist/components/LocalCaseDetails.svelte.d.ts +0 -1
  29. package/dist/components/MulticonductorDetails.svelte +3 -3
  30. package/dist/components/MulticonductorDetails.svelte.d.ts +0 -1
  31. package/dist/components/NetworkStats.svelte.d.ts +0 -1
  32. package/dist/components/PlacementCue.svelte.d.ts +0 -1
  33. package/dist/components/RatingSlider.svelte.d.ts +0 -1
  34. package/dist/components/RestoreDefaultsButton.svelte.d.ts +0 -1
  35. package/dist/components/SensitivityReadout.svelte +10 -1
  36. package/dist/components/SensitivityReadout.svelte.d.ts +0 -1
  37. package/dist/components/SizeLegend.svelte.d.ts +0 -1
  38. package/dist/components/SolveCard.svelte +2 -1
  39. package/dist/components/SolveCard.svelte.d.ts +0 -1
  40. package/dist/components/TopMovers.svelte.d.ts +0 -1
  41. package/dist/components.d.ts +0 -1
  42. package/dist/context.svelte.d.ts +0 -1
  43. package/dist/controller.svelte.d.ts +24 -12
  44. package/dist/controller.svelte.js +218 -101
  45. package/dist/display.d.ts +0 -1
  46. package/dist/drop-classify.d.ts +5 -42
  47. package/dist/drop-classify.js +5 -78
  48. package/dist/drop-limits.d.ts +27 -0
  49. package/dist/drop-limits.js +83 -0
  50. package/dist/format.d.ts +0 -1
  51. package/dist/index.d.ts +3 -5
  52. package/dist/index.js +2 -2
  53. package/dist/map-bounds.d.ts +5 -0
  54. package/dist/map-bounds.js +41 -0
  55. package/dist/map.d.ts +0 -1
  56. package/dist/multiconductor.d.ts +0 -1
  57. package/dist/state.svelte.d.ts +13 -1
  58. package/dist/state.svelte.js +13 -0
  59. package/dist/study-package.d.ts +0 -1
  60. package/dist/styles.css +16 -2
  61. package/dist/synthetic-layout.d.ts +0 -1
  62. package/dist/synthetic-layout.js +16 -2
  63. package/dist/transformer-icon.d.ts +0 -1
  64. package/dist/types.d.ts +0 -1
  65. package/package.json +14 -11
  66. package/dist/Sparkline.svelte.d.ts.map +0 -1
  67. package/dist/TellegenMap.svelte.d.ts.map +0 -1
  68. package/dist/TellegenProvider.svelte.d.ts.map +0 -1
  69. package/dist/TellegenShell.svelte.d.ts.map +0 -1
  70. package/dist/TellegenViewer.svelte.d.ts.map +0 -1
  71. package/dist/api.d.ts.map +0 -1
  72. package/dist/colors.d.ts.map +0 -1
  73. package/dist/components/AppFooter.svelte.d.ts.map +0 -1
  74. package/dist/components/AppHeader.svelte.d.ts.map +0 -1
  75. package/dist/components/BindingLines.svelte.d.ts.map +0 -1
  76. package/dist/components/BusPicker.svelte.d.ts.map +0 -1
  77. package/dist/components/ControlPanel.svelte.d.ts.map +0 -1
  78. package/dist/components/DemandSlider.svelte.d.ts.map +0 -1
  79. package/dist/components/DisplayControls.svelte.d.ts.map +0 -1
  80. package/dist/components/DropZone.svelte.d.ts.map +0 -1
  81. package/dist/components/FormulationSelector.svelte.d.ts.map +0 -1
  82. package/dist/components/LocalCaseDetails.svelte.d.ts.map +0 -1
  83. package/dist/components/MulticonductorDetails.svelte.d.ts.map +0 -1
  84. package/dist/components/NetworkStats.svelte.d.ts.map +0 -1
  85. package/dist/components/PlacementCue.svelte.d.ts.map +0 -1
  86. package/dist/components/RatingSlider.svelte.d.ts.map +0 -1
  87. package/dist/components/RestoreDefaultsButton.svelte.d.ts.map +0 -1
  88. package/dist/components/SensitivityReadout.svelte.d.ts.map +0 -1
  89. package/dist/components/SizeLegend.svelte.d.ts.map +0 -1
  90. package/dist/components/SolveCard.svelte.d.ts.map +0 -1
  91. package/dist/components/TopMovers.svelte.d.ts.map +0 -1
  92. package/dist/components.d.ts.map +0 -1
  93. package/dist/context.svelte.d.ts.map +0 -1
  94. package/dist/controller.svelte.d.ts.map +0 -1
  95. package/dist/display.d.ts.map +0 -1
  96. package/dist/drop-classify.d.ts.map +0 -1
  97. package/dist/format.d.ts.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/map.d.ts.map +0 -1
  100. package/dist/multiconductor.d.ts.map +0 -1
  101. package/dist/state.svelte.d.ts.map +0 -1
  102. package/dist/study-package.d.ts.map +0 -1
  103. package/dist/synthetic-layout.d.ts.map +0 -1
  104. package/dist/transformer-icon.d.ts.map +0 -1
  105. 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 class="panel">
21
- {#if app.error}
22
- <p class="error mono">{app.error}</p>
23
- <div class="error-actions">
24
- <button class="reset mono" onclick={ctrl.retryError}>retry</button>
25
- <button class="reset mono" onclick={() => (app.error = null)}>dismiss</button>
26
- </div>
27
- {/if}
28
- {#if app.parsingFile}
29
- <p class="dim mono blink">parsing&hellip;</p>
30
- {/if}
31
- {#if app.activeLocal}
32
- <LocalCaseDetails />
33
- {/if}
34
- {#if app.activeMulti}
35
- <MulticonductorDetails />
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
- {#if !ctrl.networkStats}
38
- {#if !app.error && !app.activeLocal && !app.activeMulti}
39
- {#if ctrl.casesLoaded && app.cases.length === 0}
40
- <p class="dim mono">
41
- {config.loadDefaultCases ? 'no default cases loaded' : 'drop a case file to begin'}
42
- </p>
43
- {#if config.loadDefaultCases}
44
- <button class="reset mono" onclick={ctrl.restoreDefaultCases}
45
- >restore default cases</button
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&hellip;</p>
50
234
  {:else}
51
- <p class="dim mono blink">loading cases&hellip;</p>
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
- {:else}
55
- {#if !app.activeLocal}
56
- <NetworkStats />
246
+ {#if app.parsingFile}
247
+ <p class="dim mono blink">parsing&hellip;</p>
57
248
  {/if}
58
249
 
59
- {#if ctrl.activeSolvable}
60
- <FormulationSelector />
61
- {/if}
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
- {#if !app.placingLocalId}
64
- <BindingLines />
255
+ <hr />
65
256
  {/if}
66
257
 
67
- <hr />
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&hellip;</p>
277
+ {:else}
278
+ <p class="dim mono blink">loading cases&hellip;</p>
279
+ {/if}
280
+ {/if}
281
+ {:else}
282
+ {#if !app.activeLocal}
283
+ <NetworkStats />
284
+ {/if}
68
285
 
69
- {#if app.selectedBus !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)}
70
- <SensitivityReadout />
286
+ {#if ctrl.activeSolvable}
287
+ <FormulationSelector />
288
+ {/if}
71
289
 
72
- <DemandSlider />
290
+ {#if app.compactLayout}
291
+ <BusPicker inline />
292
+ {/if}
73
293
 
74
- {#if ctrl.showMoverSlot}
75
- <TopMovers />
294
+ {#if !app.placingLocalId}
295
+ <BindingLines />
76
296
  {/if}
77
- {:else if app.selectedBranch !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)}
78
- <SensitivityReadout />
79
297
 
80
- <RatingSlider />
298
+ {#if !selectionLeads}
299
+ <hr />
81
300
 
82
- {#if ctrl.showMoverSlot}
83
- <TopMovers />
301
+ {@render selectionBlock()}
84
302
  {/if}
85
- {:else}
86
- <DisplayControls />
87
- {/if}
88
303
 
89
- <hr />
304
+ <hr />
90
305
 
91
- <SizeLegend />
92
- {/if}
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-y: auto;
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
- @media (max-width: 760px) {
130
- .panel {
131
- top: auto;
132
- left: 10px;
133
- right: 10px;
134
- bottom: 40px;
135
- width: auto;
136
- max-height: 44dvh;
137
- padding: 14px 16px;
138
- background: var(--paper);
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
- @media (max-width: 420px) {
143
- .panel {
144
- bottom: 34px;
145
- max-height: 46dvh;
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
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
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
- aria-pressed={app.demandRangeMode === 'local'}
43
- aria-label="nearby demand range"
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
- aria-pressed={app.demandRangeMode === 'full'}
51
- aria-label="full demand range"
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
- <span class="mono dim">{fmt.format(ctrl.sliderMin)} to {fmt.format(ctrl.sliderMax)} MW</span>
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
- title={sliderTip}
71
+ title={disabled ? disabledTip : sliderTip}
62
72
  >
63
73
  <input
64
74
  type="range"
65
75
  min={ctrl.sliderMin}
66
- max={ctrl.sliderMax}
67
- step="any"
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;
@@ -1,4 +1,3 @@
1
1
  declare const DemandSlider: import("svelte").Component<Record<string, never>, {}, "">;
2
2
  type DemandSlider = ReturnType<typeof DemandSlider>;
3
3
  export default DemandSlider;
4
- //# sourceMappingURL=DemandSlider.svelte.d.ts.map
@@ -16,4 +16,3 @@ declare const DisplayControls: $$__sveltets_2_IsomorphicComponent<Record<string,
16
16
  }, {}, {}, string>;
17
17
  type DisplayControls = InstanceType<typeof DisplayControls>;
18
18
  export default DisplayControls;
19
- //# sourceMappingURL=DisplayControls.svelte.d.ts.map
@@ -16,4 +16,3 @@ declare const DropZone: $$__sveltets_2_IsomorphicComponent<Record<string, never>
16
16
  }, {}, {}, string>;
17
17
  type DropZone = InstanceType<typeof DropZone>;
18
18
  export default DropZone;
19
- //# sourceMappingURL=DropZone.svelte.d.ts.map
@@ -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
- // Powerio writer tokens the committed state exports to; label carries the extension.
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: 'powerio-json', label: 'PowerIO (.json)' }
18
+ { token: 'model-json', label: 'PowerIO model (.json)' }
16
19
  ];
17
20
 
18
21
  let busy = $state(false);
@@ -1,4 +1,3 @@
1
1
  declare const LocalCaseDetails: import("svelte").Component<Record<string, never>, {}, "">;
2
2
  type LocalCaseDetails = ReturnType<typeof LocalCaseDetails>;
3
3
  export default LocalCaseDetails;
4
- //# sourceMappingURL=LocalCaseDetails.svelte.d.ts.map
@@ -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>