@tellegen/svelte 0.2.0 → 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.
Files changed (66) hide show
  1. package/README.md +36 -5
  2. package/dist/TellegenMap.svelte +325 -163
  3. package/dist/TellegenProvider.svelte +4 -0
  4. package/dist/TellegenShell.svelte +19 -13
  5. package/dist/api.d.ts +8 -1
  6. package/dist/api.js +6 -2
  7. package/dist/colors.d.ts +7 -7
  8. package/dist/colors.js +13 -13
  9. package/dist/components/AppHeader.svelte +23 -11
  10. package/dist/components/BindingLines.svelte +3 -6
  11. package/dist/components/BusPicker.svelte +8 -2
  12. package/dist/components/ControlPanel.svelte +106 -343
  13. package/dist/components/DemandSlider.svelte +21 -23
  14. package/dist/components/DiagramCanvas.svelte +486 -0
  15. package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
  16. package/dist/components/DisplayControls.svelte +4 -2
  17. package/dist/components/FormulationSelector.svelte +8 -3
  18. package/dist/components/LocalCaseDetails.svelte +55 -45
  19. package/dist/components/McResults.svelte +365 -0
  20. package/dist/components/McResults.svelte.d.ts +11 -0
  21. package/dist/components/ModelDetails.svelte +230 -0
  22. package/dist/components/ModelDetails.svelte.d.ts +7 -0
  23. package/dist/components/MulticonductorDetails.svelte +227 -78
  24. package/dist/components/NetworkStats.svelte +20 -18
  25. package/dist/components/NotificationCenter.svelte +243 -0
  26. package/dist/components/NotificationCenter.svelte.d.ts +3 -0
  27. package/dist/components/PanelCard.svelte +319 -0
  28. package/dist/components/PanelCard.svelte.d.ts +9 -0
  29. package/dist/components/PanelFrame.svelte +46 -0
  30. package/dist/components/PanelFrame.svelte.d.ts +16 -0
  31. package/dist/components/PanelHost.svelte +212 -0
  32. package/dist/components/PanelHost.svelte.d.ts +3 -0
  33. package/dist/components/PowerFlowDetails.svelte +99 -0
  34. package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
  35. package/dist/components/RatingSlider.svelte +5 -9
  36. package/dist/components/SavedNetworkDetails.svelte +214 -0
  37. package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
  38. package/dist/components/SensitivityReadout.svelte +24 -16
  39. package/dist/components/SolveCard.svelte +46 -49
  40. package/dist/components/TopMovers.svelte +8 -3
  41. package/dist/components/TopMovers.svelte.d.ts +2 -17
  42. package/dist/components.d.ts +2 -0
  43. package/dist/components.js +2 -0
  44. package/dist/controller.svelte.d.ts +54 -33
  45. package/dist/controller.svelte.js +927 -233
  46. package/dist/diagram-labels.d.ts +9 -0
  47. package/dist/diagram-labels.js +40 -0
  48. package/dist/display.d.ts +3 -7
  49. package/dist/display.js +31 -27
  50. package/dist/format.js +5 -4
  51. package/dist/index.d.ts +10 -4
  52. package/dist/index.js +7 -2
  53. package/dist/map.d.ts +1 -1
  54. package/dist/map.js +1 -1
  55. package/dist/multiconductor.d.ts +11 -2
  56. package/dist/multiconductor.js +107 -14
  57. package/dist/notices.svelte.d.ts +29 -0
  58. package/dist/notices.svelte.js +82 -0
  59. package/dist/panels.svelte.d.ts +65 -0
  60. package/dist/panels.svelte.js +215 -0
  61. package/dist/state.svelte.d.ts +90 -23
  62. package/dist/state.svelte.js +94 -20
  63. package/dist/synthetic-layout.js +1 -0
  64. package/package.json +22 -9
  65. package/dist/study-package.d.ts +0 -8
  66. package/dist/study-package.js +0 -17
@@ -0,0 +1,230 @@
1
+ <script lang="ts">
2
+ import type { ModelDetails as ModelDetailsRecord } from "@tellegen/engine";
3
+
4
+ let { details }: { details?: ModelDetailsRecord | null } = $props();
5
+ let open = $state(false);
6
+ let page = $state(0);
7
+ let search = $state("");
8
+ const pageSize = 20;
9
+ const method = $derived(
10
+ details?.method === "convex_quadratic_fit"
11
+ ? "Convex quadratic fit"
12
+ : details?.method === "exact"
13
+ ? "Source model"
14
+ : String(details?.method ?? "").replaceAll("_", " "),
15
+ );
16
+ const rows = $derived(
17
+ open
18
+ ? (details?.approximations.filter((row) =>
19
+ `${row.generator} ${row.bus}`.includes(search.trim()),
20
+ ) ?? [])
21
+ : [],
22
+ );
23
+ const pageCount = $derived(Math.max(1, Math.ceil(rows.length / pageSize)));
24
+ const currentPage = $derived(Math.min(page, pageCount - 1));
25
+ const number = (value: number) =>
26
+ value.toLocaleString(undefined, { maximumSignificantDigits: 4 });
27
+ </script>
28
+
29
+ {#if details}
30
+ <details class="model-details" bind:open>
31
+ <summary>Model details</summary>
32
+ {#if open}
33
+ <dl>
34
+ <div>
35
+ <dt>Method</dt>
36
+ <dd>{method}</dd>
37
+ </div>
38
+ <div>
39
+ <dt>Quantity</dt>
40
+ <dd>{details.quantity}</dd>
41
+ </div>
42
+ <div>
43
+ <dt>Approximated curves</dt>
44
+ <dd>{details.approximations.length.toLocaleString()}</dd>
45
+ </div>
46
+ </dl>
47
+ {#if details.approximations.length}
48
+ <label
49
+ >Find generator or bus<input
50
+ type="search"
51
+ value={search}
52
+ oninput={(event) => {
53
+ search = event.currentTarget.value;
54
+ page = 0;
55
+ }}
56
+ /></label
57
+ >
58
+ <div class="table-scroll">
59
+ <table>
60
+ <caption>Errors at source breakpoints ({details.units})</caption>
61
+ <thead
62
+ ><tr
63
+ ><th>Generator</th><th>Bus</th><th
64
+ ><abbr title="Root mean square error">RMS</abbr></th
65
+ ><th>Maximum</th></tr
66
+ ></thead
67
+ >
68
+ <tbody
69
+ >{#each rows.slice(currentPage * pageSize, (currentPage + 1) * pageSize) as row (row.generator)}<tr
70
+ ><th>{row.generator}</th><td>{row.bus}</td><td
71
+ >{number(row.rms_breakpoint_error)}</td
72
+ ><td>{number(row.max_breakpoint_error)}</td></tr
73
+ >{/each}</tbody
74
+ >
75
+ </table>
76
+ </div>
77
+ {#if !rows.length}<p>No matching generators.</p>{/if}
78
+ {#if pageCount > 1}<div class="pagination">
79
+ <button
80
+ disabled={currentPage === 0}
81
+ onclick={() => (page = currentPage - 1)}>Previous</button
82
+ ><span
83
+ >{currentPage * pageSize + 1}-{Math.min(
84
+ (currentPage + 1) * pageSize,
85
+ rows.length,
86
+ )} of {rows.length}</span
87
+ ><button
88
+ disabled={currentPage + 1 >= pageCount}
89
+ onclick={() => (page = currentPage + 1)}>Next</button
90
+ >
91
+ </div>{/if}
92
+ {/if}
93
+ <details class="references">
94
+ <summary>Source and model IDs</summary>
95
+ <dl>
96
+ <div>
97
+ <dt>Source</dt>
98
+ <dd><code>{details.source}</code></dd>
99
+ </div>
100
+ <div>
101
+ <dt>Model</dt>
102
+ <dd><code>{details.model}</code></dd>
103
+ </div>
104
+ </dl>
105
+ </details>
106
+ {/if}
107
+ </details>
108
+ {/if}
109
+
110
+ <style>
111
+ .model-details {
112
+ border-top: 1px solid var(--line);
113
+ padding: 12px 0;
114
+ font: 12px/1.45 var(--font-display);
115
+ color: var(--ink);
116
+ }
117
+ summary {
118
+ cursor: pointer;
119
+ font-weight: 400;
120
+ }
121
+ dl {
122
+ display: grid;
123
+ gap: 7px;
124
+ margin: 14px 0;
125
+ }
126
+ dl > div {
127
+ display: grid;
128
+ grid-template-columns: 110px minmax(0, 1fr);
129
+ gap: 10px;
130
+ }
131
+ dt {
132
+ color: var(--text-secondary);
133
+ }
134
+ dd {
135
+ margin: 0;
136
+ }
137
+ label {
138
+ display: grid;
139
+ gap: 6px;
140
+ margin: 12px 0;
141
+ font-size: 11px;
142
+ }
143
+ input {
144
+ min-width: 0;
145
+ width: 100%;
146
+ box-sizing: border-box;
147
+ background: var(--paper);
148
+ border: 1px solid var(--line);
149
+ padding: 6px 8px;
150
+ border-radius: 4px;
151
+ color: inherit;
152
+ font: inherit;
153
+ }
154
+ .table-scroll {
155
+ max-height: 250px;
156
+ overflow: auto;
157
+ }
158
+ table {
159
+ width: 100%;
160
+ border-collapse: collapse;
161
+ font-size: 10px;
162
+ }
163
+ caption {
164
+ text-align: left;
165
+ color: var(--text-secondary);
166
+ padding-bottom: 8px;
167
+ }
168
+ th,
169
+ td {
170
+ padding: 7px 4px;
171
+ text-align: right;
172
+ border-bottom: 1px solid var(--line);
173
+ font-variant-numeric: tabular-nums;
174
+ }
175
+ th:first-child {
176
+ text-align: left;
177
+ }
178
+ tbody th {
179
+ font-weight: 400;
180
+ }
181
+ thead th {
182
+ position: sticky;
183
+ top: 0;
184
+ background: var(--paper);
185
+ font-weight: 500;
186
+ }
187
+ abbr {
188
+ text-decoration-style: dotted;
189
+ text-underline-offset: 3px;
190
+ }
191
+ .pagination {
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: space-between;
195
+ gap: 8px;
196
+ margin: 10px 0;
197
+ font-size: 10px;
198
+ color: var(--text-secondary);
199
+ }
200
+ button {
201
+ padding: 4px 6px;
202
+ font: inherit;
203
+ border: 1px solid var(--line);
204
+ background: transparent;
205
+ border-radius: 4px;
206
+ cursor: pointer;
207
+ color: inherit;
208
+ }
209
+ button:disabled {
210
+ opacity: 0.45;
211
+ cursor: default;
212
+ }
213
+ button:focus-visible,
214
+ input:focus-visible,
215
+ summary:focus-visible {
216
+ outline: 2px solid var(--focus-ring);
217
+ outline-offset: 2px;
218
+ }
219
+ .references {
220
+ margin-top: 12px;
221
+ font-size: 10px;
222
+ }
223
+ .references dl > div {
224
+ grid-template-columns: 42px minmax(0, 1fr);
225
+ }
226
+ code {
227
+ overflow-wrap: anywhere;
228
+ font: 10px/1.5 var(--font-mono);
229
+ }
230
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { ModelDetails as ModelDetailsRecord } from "@tellegen/engine";
2
+ type $$ComponentProps = {
3
+ details?: ModelDetailsRecord | null;
4
+ };
5
+ declare const ModelDetails: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ModelDetails = ReturnType<typeof ModelDetails>;
7
+ export default ModelDetails;
@@ -8,19 +8,35 @@
8
8
  isPhaseTerminal,
9
9
  phaseColor
10
10
  } from '../multiconductor.js';
11
- import type { DistAttachmentKind } from '@tellegen/engine';
11
+ import { getPanelLayout } from '../panels.svelte.js';
12
+ import { getNoticeCenter } from '../notices.svelte.js';
13
+ import { type DistAttachmentKind } from '@tellegen/engine';
12
14
 
13
15
  const app = getAppState();
14
16
  const ctrl = getController();
17
+ const notices = getNoticeCenter();
18
+ const panels = getPanelLayout();
19
+ function openResults() {
20
+ const panel = panels.panels.find((p) => p.id === 'studies');
21
+ if (panel) {
22
+ const network = panels.panels.find((p) => p.id === 'network');
23
+ if (network) panels.close(network);
24
+ panel.setOpen(true);
25
+ panels.activate(panel);
26
+ if (panels.compact) panels.drawer = panel.id;
27
+ }
28
+ }
15
29
 
16
30
  const NEUTRAL_RGBA = [120, 114, 102, 255] as const;
17
- const ATTACHMENT_LEGEND: DistAttachmentKind[] = [
18
- 'source',
19
- 'generator',
20
- 'ibr',
21
- 'load',
22
- 'shunt'
23
- ];
31
+
32
+ /** A cleared or unparsable number field is not a request for zero load;
33
+ * only a finite value counts as an edit. */
34
+ function parseLoadInput(raw: string): number | null {
35
+ if (raw.trim() === '') return null;
36
+ const value = Number(raw);
37
+ return Number.isFinite(value) ? value : null;
38
+ }
39
+ const ATTACHMENT_LEGEND: DistAttachmentKind[] = ['source', 'generator', 'ibr', 'load', 'shunt'];
24
40
 
25
41
  function terminalColor(t: string): string {
26
42
  return rgbaCss(isPhaseTerminal(t) ? phaseColor(t) : [...NEUTRAL_RGBA]);
@@ -32,60 +48,76 @@
32
48
  {@const s = mc.summary}
33
49
  <h2>{mc.label} <span class="region mono">via {mc.fileName}</span></h2>
34
50
  {#if s}
35
- <p class="tag mono">multiconductor &#8901; viewing only</p>
36
- <dl class="mono">
37
- <div>
38
- <dt>buses</dt>
39
- <dd>{s.n_bus}</dd>
40
- </div>
41
- <div>
42
- <dt>lines / switches / xfmrs</dt>
43
- <dd>{s.n_line} / {s.n_switch} / {s.n_transformer}</dd>
44
- </div>
45
- <div>
46
- <dt>load</dt>
47
- <dd>{fmt.format(s.load_kw)} kW</dd>
48
- </div>
49
- <div>
50
- <dt>gen capacity</dt>
51
- <dd>{fmt.format(s.gen_kw)} kW</dd>
52
- </div>
53
- <div>
54
- <dt>sources / loads / gens</dt>
55
- <dd>{s.n_source} / {s.n_load} / {s.n_generator}</dd>
56
- </div>
57
- {#if s.n_ibr > 0 || s.n_shunt > 0 || (s.n_capacitor ?? 0) > 0}
51
+ <p class="tag mono">Multiconductor</p>
52
+ <details class="case-details">
53
+ <summary>{s.n_bus} buses, {s.n_edge} {s.n_edge === 1 ? 'branch' : 'branches'}</summary>
54
+ <dl class="mono">
58
55
  <div>
59
- <dt>IBRs / shunts / caps</dt>
60
- <dd>{s.n_ibr} / {s.n_shunt} / {s.n_capacitor ?? 0}</dd>
56
+ <dt>buses</dt>
57
+ <dd>{s.n_bus}</dd>
61
58
  </div>
62
- {/if}
63
- </dl>
64
-
65
- {#if s.warnings.length > 0}
66
- <ul class="warnings mono">
67
- {#each s.warnings.slice(0, 4) as w, i (i)}
68
- <li>{w}</li>
69
- {/each}
70
- {#if s.warnings.length > 4}
71
- <li>+{s.warnings.length - 4} more</li>
59
+ <div>
60
+ <dt>Lines / switches / transformers</dt>
61
+ <dd>{s.n_line} / {s.n_switch} / {s.n_transformer}</dd>
62
+ </div>
63
+ <div>
64
+ <dt>load</dt>
65
+ <dd>{fmt.format(s.load_kw)} kW</dd>
66
+ </div>
67
+ <div>
68
+ <dt>Generation capacity</dt>
69
+ <dd>{fmt.format(s.gen_kw)} kW</dd>
70
+ </div>
71
+ <div>
72
+ <dt>sources / loads / gens</dt>
73
+ <dd>{s.n_source} / {s.n_load} / {s.n_generator}</dd>
74
+ </div>
75
+ {#if s.n_ibr > 0 || s.n_shunt > 0 || (s.n_capacitor ?? 0) > 0}
76
+ <div>
77
+ <dt>IBRs / shunts / caps</dt>
78
+ <dd>{s.n_ibr} / {s.n_shunt} / {s.n_capacitor ?? 0}</dd>
79
+ </div>
72
80
  {/if}
73
- </ul>
74
- {/if}
75
-
76
- {#if !mc.placed}
77
- <p class="footnote mono">
78
- {s.coords_kind === 'planar'
79
- ? 'coordinates are diagram-only: click the map to place the layout'
80
- : 'no coordinates in this file: click the map to place the layout'}
81
- </p>
82
- {:else if mc.coordsKind === 'geographic'}
83
- <p class="footnote mono">coordinates: geographic, from the case file</p>
84
- {:else if mc.coordsKind === 'planar'}
85
- <p class="footnote mono">coordinates: diagram layout fit where you placed it</p>
86
- {:else}
87
- <p class="footnote mono">coordinates: synthetic topology layout centered where you placed it</p>
88
- {/if}
81
+ </dl>
82
+ </details>
83
+
84
+ <p class="footnote mono">
85
+ {mc.coordsKind === 'geographic'
86
+ ? 'Geographic coordinates'
87
+ : mc.coordsKind === 'planar'
88
+ ? 'Drawing coordinates'
89
+ : 'Generated diagram'}
90
+ </p>
91
+ <div class="calculation-actions">
92
+ {#if mc.solving}
93
+ <span role="status">Calculating...</span>{#if mc.solveAbort}<button
94
+ onclick={() => mc.solveAbort?.abort()}>Cancel</button
95
+ >{/if}
96
+ {:else}
97
+ <button
98
+ disabled={!mc.mcPfSupported}
99
+ onclick={() => {
100
+ void ctrl
101
+ .solveMultiCase(mc)
102
+ .then(openResults)
103
+ .catch(() => {});
104
+ }}>Solve AC power flow</button
105
+ >
106
+ {#if !mc.mcPfSupported}
107
+ <button
108
+ class="quiet"
109
+ onclick={() =>
110
+ notices.push({
111
+ kind: 'warning',
112
+ title: 'AC power flow unavailable',
113
+ details: mc.mcPfReason ?? 'This case cannot run AC power flow in this browser'
114
+ })}>Why unavailable</button
115
+ >
116
+ {/if}
117
+ {/if}
118
+ </div>
119
+ {#if mc.result}<p class="footnote">Converged, {mc.result.iterations} iterations</p>
120
+ <button class="reset mono" onclick={openResults}>View results in Studies</button>{/if}
89
121
  {/if}
90
122
 
91
123
  {#if mc.selectedBus}
@@ -115,17 +147,74 @@
115
147
  {:else}
116
148
  <p class="footnote mono">no attachments on this bus</p>
117
149
  {/if}
150
+ {@const loadBranches = mc.mcLoadBranches.filter((load) => load.bus === b.id)}
151
+ {#if loadBranches.length > 0}
152
+ <div class="load-editor">
153
+ <h4>Load power</h4>
154
+ {#each loadBranches as load (`${load.load}:${load.branch}`)}
155
+ <div class="load-row">
156
+ <span class="mono"
157
+ >{load.load}{loadBranches.length > 1 ? ` · ${load.branch + 1}` : ''}</span
158
+ >
159
+ <label
160
+ >P kW<input
161
+ type="number"
162
+ step="any"
163
+ value={load.p_w / 1000}
164
+ onchange={(event) => {
165
+ const kw = parseLoadInput(event.currentTarget.value);
166
+ if (kw === null) {
167
+ event.currentTarget.value = String(load.p_w / 1000);
168
+ return;
169
+ }
170
+ ctrl.queueMultiLoadPower(mc, load.load, load.branch, kw * 1000, load.q_var);
171
+ }}
172
+ /></label
173
+ >
174
+ <label
175
+ >Q kvar<input
176
+ type="number"
177
+ step="any"
178
+ value={load.q_var / 1000}
179
+ onchange={(event) => {
180
+ const kvar = parseLoadInput(event.currentTarget.value);
181
+ if (kvar === null) {
182
+ event.currentTarget.value = String(load.q_var / 1000);
183
+ return;
184
+ }
185
+ ctrl.queueMultiLoadPower(mc, load.load, load.branch, load.p_w, kvar * 1000);
186
+ }}
187
+ /></label
188
+ >
189
+ <button
190
+ class="quiet"
191
+ disabled={load.p_w === load.base_p_w && load.q_var === load.base_q_var}
192
+ onclick={() =>
193
+ ctrl.queueMultiLoadPower(
194
+ mc,
195
+ load.load,
196
+ load.branch,
197
+ load.base_p_w,
198
+ load.base_q_var
199
+ )}>Reset</button
200
+ >
201
+ </div>
202
+ {/each}
203
+ <p class="footnote">Changes re-solve automatically using the retained factorization.</p>
204
+ </div>
205
+ {/if}
118
206
  {:else if mc.selectedEdge}
119
207
  {@const e = mc.selectedEdge}
120
208
  <hr />
121
209
  <h3 class="mono">
122
210
  <i class="swatch" style={`--sc:${rgbaCss([...edgeColor(e.kind, e.closed)])}`}></i>
123
- {e.kind} {e.from}&#8201;&ndash;&#8201;{e.to}
211
+ {e.kind}
212
+ {e.from}&#8201;&ndash;&#8201;{e.to}
124
213
  </h3>
125
214
  <dl class="mono">
126
215
  <div>
127
216
  <dt>phases</dt>
128
- <dd>{e.n_phases}&#966; &#8901; {e.conductors.length} conductors</dd>
217
+ <dd>{e.n_phases}&#966; , {e.conductors.length} conductors</dd>
129
218
  </div>
130
219
  {#if e.kind === 'switch'}
131
220
  <div>
@@ -156,22 +245,53 @@
156
245
  </span>
157
246
  <span class="legend-row">
158
247
  {#each ATTACHMENT_LEGEND as kind (kind)}
159
- <i class="swatch" style={`--sc:${rgbaCss([...attachmentColor(kind)])}`}></i>{attachmentGlyph(
160
- kind
161
- )}
248
+ <i class="swatch" style={`--sc:${rgbaCss([...attachmentColor(kind)])}`}
249
+ ></i>{attachmentGlyph(kind)}
162
250
  {/each}
163
251
  </span>
164
252
  </div>
165
253
 
166
- {#if !mc.placed}
167
- <button class="reset mono" onclick={() => ctrl.moveMultiCase(mc)}>place on map</button>
168
- {:else}
169
- <button class="reset mono" onclick={() => ctrl.moveMultiCase(mc)}>move layout</button>
170
- {/if}
254
+ <button class="reset mono" onclick={() => app.requestFrame(mc.id)}>Fit case</button>
171
255
  <button class="reset mono" onclick={() => ctrl.removeMultiCase(mc)}>remove</button>
172
256
  {/if}
173
257
 
174
258
  <style>
259
+ .case-details {
260
+ margin: 10px 0;
261
+ font-size: 12px;
262
+ }
263
+ .case-details summary {
264
+ cursor: pointer;
265
+ color: var(--text-secondary);
266
+ }
267
+ .case-details dl {
268
+ margin-top: 8px;
269
+ }
270
+ .calculation-actions {
271
+ display: flex;
272
+ flex-wrap: wrap;
273
+ gap: 8px;
274
+ align-items: center;
275
+ margin: 14px 0;
276
+ }
277
+ .calculation-actions button {
278
+ font: inherit;
279
+ padding: 7px 10px;
280
+ border: 1px solid var(--line);
281
+ border-radius: 3px;
282
+ background: var(--ink);
283
+ color: var(--paper);
284
+ cursor: pointer;
285
+ }
286
+ .calculation-actions button:disabled {
287
+ opacity: 0.5;
288
+ cursor: default;
289
+ }
290
+ .calculation-actions button.quiet {
291
+ background: transparent;
292
+ color: var(--ink);
293
+ }
294
+
175
295
  h2 {
176
296
  margin: 0 0 8px;
177
297
  font-size: 16px;
@@ -261,6 +381,44 @@
261
381
  letter-spacing: 0;
262
382
  }
263
383
 
384
+ .load-editor {
385
+ margin-top: 14px;
386
+ padding-top: 10px;
387
+ border-top: 1px solid var(--line);
388
+ }
389
+
390
+ .load-editor h4 {
391
+ margin: 0 0 8px;
392
+ font-size: 11px;
393
+ }
394
+
395
+ .load-row {
396
+ display: grid;
397
+ grid-template-columns: minmax(70px, 1fr) 72px 72px auto;
398
+ gap: 6px;
399
+ align-items: end;
400
+ margin-bottom: 7px;
401
+ }
402
+
403
+ .load-row label {
404
+ display: flex;
405
+ flex-direction: column;
406
+ gap: 2px;
407
+ font-size: 9px;
408
+ color: var(--text-secondary);
409
+ }
410
+
411
+ .load-row input {
412
+ min-width: 0;
413
+ width: 100%;
414
+ box-sizing: border-box;
415
+ font: 10px var(--font-mono);
416
+ }
417
+
418
+ .load-row button {
419
+ font-size: 9px;
420
+ }
421
+
264
422
  /* The global stylesheet's .legend is the 6px color-ramp strip; its height
265
423
  * leaks into a scoped .legend, so this container carries its own name. */
266
424
  .mc-legend {
@@ -316,15 +474,6 @@
316
474
  letter-spacing: 0;
317
475
  }
318
476
 
319
- .warnings {
320
- margin: 8px 0 0;
321
- padding: 0;
322
- list-style: none;
323
- font-size: 10.5px;
324
- line-height: 1.5;
325
- color: var(--text-accent);
326
- }
327
-
328
477
  hr {
329
478
  border: 0;
330
479
  border-top: 1px solid var(--line);
@@ -20,25 +20,27 @@
20
20
  <dt>lines</dt>
21
21
  <dd>{stats.branches}</dd>
22
22
  </div>
23
- <div>
24
- <dt>binding lines</dt>
25
- <dd>{stats.binding ?? '…'}</dd>
26
- </div>
27
- <div>
28
- <dt>cost</dt>
29
- <dd>
30
- {#if stats.objective === null}
31
- <span class="blink">solving&hellip;</span>
32
- {:else}
33
- {fmt.format(stats.objective)} $/h
34
- {/if}
35
- </dd>
36
- </div>
37
- {#if ctrl.isPerturbed(ctrl.activeSolvable) && deltaObjective !== null}
38
- <div class="delta">
39
- <dt>vs base</dt>
40
- <dd>{signed(deltaObjective)} $/h</dd>
23
+ {#if ctrl.activeFormulation !== 'acpf'}
24
+ <div>
25
+ <dt>binding lines</dt>
26
+ <dd>{stats.binding ?? '...'}</dd>
27
+ </div>
28
+ <div>
29
+ <dt>OPF objective</dt>
30
+ <dd>
31
+ {#if stats.objective === null}
32
+ <span class="blink">solving...</span>
33
+ {:else}
34
+ {fmt.format(stats.objective)}
35
+ {/if}
36
+ </dd>
41
37
  </div>
38
+ {#if ctrl.isPerturbed(ctrl.activeSolvable) && deltaObjective !== null}
39
+ <div class="delta">
40
+ <dt>vs base</dt>
41
+ <dd>{signed(deltaObjective)}</dd>
42
+ </div>
43
+ {/if}
42
44
  {/if}
43
45
  </dl>
44
46
  {/if}