@tellegen/svelte 0.3.0 → 0.4.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 (67) hide show
  1. package/README.md +233 -0
  2. package/dist/Sparkline.svelte +2 -2
  3. package/dist/TellegenMap.svelte +201 -47
  4. package/dist/TellegenMap.svelte.d.ts +45 -0
  5. package/dist/TellegenProvider.svelte +36 -4
  6. package/dist/TellegenShell.svelte +14 -42
  7. package/dist/TellegenViewer.svelte +12 -2
  8. package/dist/api.d.ts +10 -2
  9. package/dist/api.js +2 -0
  10. package/dist/basemap.d.ts +192 -0
  11. package/dist/basemap.js +259 -0
  12. package/dist/colors.d.ts +18 -0
  13. package/dist/colors.js +26 -0
  14. package/dist/components/AppFooter.svelte +3 -2
  15. package/dist/components/AppHeader.svelte +21 -8
  16. package/dist/components/BindingLines.svelte +1 -1
  17. package/dist/components/BusPicker.svelte +3 -3
  18. package/dist/components/DemandSlider.svelte +16 -7
  19. package/dist/components/DiagramCanvas.svelte +1 -1
  20. package/dist/components/DisplayControls.svelte +5 -1
  21. package/dist/components/DropZone.svelte +1 -1
  22. package/dist/components/FormulationSelector.svelte +1 -1
  23. package/dist/components/LocalCaseDetails.svelte +6 -3
  24. package/dist/components/McResults.svelte +98 -35
  25. package/dist/components/McResults.svelte.d.ts +8 -2
  26. package/dist/components/ModelDetails.svelte +5 -5
  27. package/dist/components/MulticonductorDetails.svelte +9 -9
  28. package/dist/components/NotificationCenter.svelte +4 -2
  29. package/dist/components/PanelCard.svelte +72 -7
  30. package/dist/components/PanelHost.svelte +59 -34
  31. package/dist/components/PanelHost.svelte.d.ts +4 -1
  32. package/dist/components/PlacementCue.svelte +1 -1
  33. package/dist/components/PowerFlowDetails.svelte +1 -1
  34. package/dist/components/RatingSlider.svelte +14 -5
  35. package/dist/components/RestoreDefaultsButton.svelte +1 -1
  36. package/dist/components/SavedNetworkDetails.svelte +3 -3
  37. package/dist/components/SensitivityReadout.svelte +2 -2
  38. package/dist/components/ShellEnvironment.svelte +51 -0
  39. package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
  40. package/dist/components/SizeLegend.svelte +2 -2
  41. package/dist/components/SolveCard.svelte +3 -3
  42. package/dist/components/TopMovers.svelte +1 -1
  43. package/dist/components.d.ts +3 -0
  44. package/dist/components.js +3 -0
  45. package/dist/context.svelte.d.ts +8 -1
  46. package/dist/context.svelte.js +8 -3
  47. package/dist/controller.svelte.d.ts +65 -5
  48. package/dist/controller.svelte.js +262 -76
  49. package/dist/format.d.ts +9 -0
  50. package/dist/format.js +12 -0
  51. package/dist/index.d.ts +5 -1
  52. package/dist/index.js +2 -0
  53. package/dist/map.d.ts +4 -2
  54. package/dist/map.js +2 -1
  55. package/dist/maplibre-worker.d.ts +5 -0
  56. package/dist/maplibre-worker.dev.d.ts +11 -0
  57. package/dist/maplibre-worker.dev.js +15 -0
  58. package/dist/maplibre-worker.js +8 -0
  59. package/dist/notices.svelte.js +5 -1
  60. package/dist/panels.svelte.d.ts +6 -0
  61. package/dist/panels.svelte.js +31 -1
  62. package/dist/slider.d.ts +12 -0
  63. package/dist/slider.js +31 -0
  64. package/dist/state.svelte.d.ts +19 -2
  65. package/dist/state.svelte.js +12 -0
  66. package/dist/styles.css +59 -6
  67. package/package.json +13 -8
@@ -31,7 +31,7 @@
31
31
  padding: 8px 20px;
32
32
  font-size: 10.5px;
33
33
  color: var(--text-secondary);
34
- background: linear-gradient(rgba(236, 233, 226, 0), rgba(236, 233, 226, 0.9));
34
+ background: linear-gradient(rgb(var(--bg-rgb) / 0), rgb(var(--bg-rgb) / 0.9));
35
35
  animation: rise 0.5s 0.24s ease-out both;
36
36
  pointer-events: none;
37
37
  }
@@ -53,10 +53,12 @@
53
53
  color: var(--text-accent);
54
54
  }
55
55
 
56
+ /* Inside the Network panel on narrow screens, panel text size applies. */
56
57
  footer.inline {
57
58
  position: static;
58
59
  margin-top: 14px;
59
60
  padding: 0;
61
+ font-size: 12px;
60
62
  background: none;
61
63
  animation: none;
62
64
  pointer-events: auto;
@@ -71,7 +73,6 @@
71
73
 
72
74
  footer.inline {
73
75
  margin-top: 4px;
74
- font-size: 11px;
75
76
  }
76
77
  }
77
78
 
@@ -26,10 +26,10 @@
26
26
  <header bind:this={headerEl}>
27
27
  <div class="brand">
28
28
  <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
29
- <path d="M4 18 L12 6 L20 18" stroke="#b25e00" stroke-width="1.6" fill="none" />
30
- <circle cx="4" cy="18" r="2.4" fill="#b25e00" />
31
- <circle cx="12" cy="6" r="2.4" fill="#20242b" />
32
- <circle cx="20" cy="18" r="2.4" fill="#b25e00" />
29
+ <path class="brand-accent-stroke" d="M4 18 L12 6 L20 18" stroke-width="1.6" fill="none" />
30
+ <circle class="brand-accent" cx="4" cy="18" r="2.4" />
31
+ <circle class="brand-ink" cx="12" cy="6" r="2.4" />
32
+ <circle class="brand-accent" cx="20" cy="18" r="2.4" />
33
33
  </svg>
34
34
  <h1>tellegen</h1>
35
35
  </div>
@@ -130,7 +130,7 @@
130
130
  justify-content: space-between;
131
131
  gap: 16px;
132
132
  padding: 10px 20px;
133
- background: linear-gradient(rgba(236, 233, 226, 0.95), rgba(236, 233, 226, 0));
133
+ background: linear-gradient(rgb(var(--bg-rgb) / 0.95), rgb(var(--bg-rgb) / 0));
134
134
  animation: drop 0.5s ease-out both;
135
135
  }
136
136
 
@@ -141,6 +141,19 @@
141
141
  gap: 10px;
142
142
  }
143
143
 
144
+ /* The brand mark follows the theme: accent legs, ink apex. */
145
+ .brand-accent {
146
+ fill: var(--accent);
147
+ }
148
+
149
+ .brand-accent-stroke {
150
+ stroke: var(--accent);
151
+ }
152
+
153
+ .brand-ink {
154
+ fill: var(--ink);
155
+ }
156
+
144
157
  h1 {
145
158
  margin: 0;
146
159
  font-size: 22px;
@@ -305,8 +318,8 @@
305
318
 
306
319
  /* Ghost chip: standing invitation to drop or pick a case file. */
307
320
  .cases > button.ghost {
308
- background: rgba(252, 251, 247, 0.36);
309
- border: 1px dashed rgba(178, 94, 0, 0.55);
321
+ background: rgb(var(--paper-rgb) / 0.36);
322
+ border: 1px dashed rgb(var(--accent-rgb) / 0.55);
310
323
  color: var(--text-secondary);
311
324
  box-shadow: inset 0 0 0 1px rgba(212, 116, 34, 0.08);
312
325
  }
@@ -351,7 +364,7 @@
351
364
  flex-wrap: wrap;
352
365
  gap: 8px;
353
366
  padding: 8px 10px 12px;
354
- background: linear-gradient(rgba(236, 233, 226, 0.97), rgba(236, 233, 226, 0.72));
367
+ background: linear-gradient(rgb(var(--bg-rgb) / 0.97), rgb(var(--bg-rgb) / 0.72));
355
368
  }
356
369
 
357
370
  .brand {
@@ -59,7 +59,7 @@
59
59
  }
60
60
 
61
61
  .head {
62
- font-size: 10.5px;
62
+ font-size: 12.5px;
63
63
  margin: 0 0 4px;
64
64
  }
65
65
 
@@ -155,7 +155,7 @@
155
155
 
156
156
  label {
157
157
  display: block;
158
- font-size: 10px;
158
+ font-size: 12px;
159
159
  text-transform: uppercase;
160
160
  margin-bottom: 3px;
161
161
  }
@@ -220,7 +220,7 @@
220
220
  background: var(--panel);
221
221
  border: 1px solid var(--line);
222
222
  border-radius: var(--radius-xs, 2px);
223
- box-shadow: 0 4px 18px rgba(32, 36, 43, 0.14);
223
+ box-shadow: var(--elev-menu);
224
224
  }
225
225
 
226
226
  li {
@@ -244,7 +244,7 @@
244
244
  border: 1px solid var(--line);
245
245
  border-radius: var(--radius-xs, 2px);
246
246
  box-shadow: var(--elev-1);
247
- font-size: 10.5px;
247
+ font-size: 12.5px;
248
248
  margin: 4px 0 0;
249
249
  }
250
250
 
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { getAppState, getController } from '../context.svelte.js';
3
3
  import { fmt, signed } from '../format.js';
4
+ import { sliderKeyValue } from '../slider.js';
4
5
 
5
6
  const app = getAppState();
6
7
  const ctrl = getController();
@@ -37,7 +38,7 @@
37
38
  <span class="val">{signed(ctrl.sliderValue)} MW</span>
38
39
  </div>
39
40
  <div class="range-mode">
40
- <div class="segment mono" aria-label="demand range">
41
+ <div class="segment mono" role="group" aria-label="demand range">
41
42
  <button
42
43
  type="button"
43
44
  class:active={app.demandRangeMode === 'local'}
@@ -51,7 +52,7 @@
51
52
  type="button"
52
53
  class:active={app.demandRangeMode === 'full'}
53
54
  aria-pressed={app.demandRangeMode === 'full'}
54
- aria-label="full demand range"
55
+ aria-label="full range of demand"
55
56
  {disabled}
56
57
  title="range from zero load to the local physical limit"
57
58
  onclick={() => ctrl.setDemandRangeMode('full')}>full range</button
@@ -79,7 +80,15 @@
79
80
  bind:value={ctrl.sliderCurrent, ctrl.setSliderPreview}
80
81
  aria-label="demand delta at selected bus"
81
82
  onpointerdown={() => ctrl.setSliderPreview(ctrl.sliderValue)}
82
- onkeydown={() => ctrl.setSliderPreview(ctrl.sliderValue)}
83
+ onkeydown={(e) => {
84
+ const next = sliderKeyValue(e.key, ctrl.sliderValue, ctrl.sliderMin, ctrl.sliderMax);
85
+ if (next === null) {
86
+ ctrl.setSliderPreview(ctrl.sliderValue);
87
+ return;
88
+ }
89
+ e.preventDefault();
90
+ ctrl.setSliderPreview(next);
91
+ }}
83
92
  onpointerup={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
84
93
  onkeyup={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
85
94
  onchange={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
@@ -120,7 +129,7 @@
120
129
  .slider-head {
121
130
  display: flex;
122
131
  justify-content: space-between;
123
- font-size: 11.5px;
132
+ font-size: 13.5px;
124
133
  color: var(--text-secondary);
125
134
  margin-bottom: 4px;
126
135
  }
@@ -135,7 +144,7 @@
135
144
  justify-content: space-between;
136
145
  gap: 8px;
137
146
  margin: 6px 0 7px;
138
- font-size: 10.5px;
147
+ font-size: 12.5px;
139
148
  }
140
149
 
141
150
  .slider-track {
@@ -256,13 +265,13 @@
256
265
 
257
266
  .pred {
258
267
  margin: 2px 0 0;
259
- font-size: 11px;
268
+ font-size: 13px;
260
269
  min-height: 16px;
261
270
  }
262
271
 
263
272
  .score {
264
273
  margin: 8px 0 0;
265
- font-size: 11px;
274
+ font-size: 13px;
266
275
  color: var(--ink);
267
276
  min-height: 16px;
268
277
  }
@@ -350,7 +350,7 @@
350
350
  cy={bus.lat}
351
351
  r={Math.max(5, busRadius(Math.max(bus.demand_mw, bus.gen_mw))) / scale}
352
352
  fill={color(bus)}
353
- stroke={selectedBus === bus.id ? '#2f6fbb' : '#756a5b'}
353
+ style:stroke={selectedBus === bus.id ? '#2f6fbb' : 'var(--diagram-bus-stroke)'}
354
354
  stroke-width={selectedBus === bus.id ? 3 : 1}
355
355
  vector-effect="non-scaling-stroke"
356
356
  />
@@ -59,10 +59,14 @@
59
59
  </span>
60
60
  {/if}
61
61
  </div>
62
- {:else}
62
+ {:else if ctrl.activeSolvable?.solving}
63
63
  <p class="dim small blink">
64
64
  Solving {formulationLabel(ctrl.activeFormulation)}&hellip;
65
65
  </p>
66
+ {:else}
67
+ <p class="dim small">
68
+ No {formulationLabel(ctrl.activeFormulation)} results available.
69
+ </p>
66
70
  {/if}
67
71
 
68
72
  <style>
@@ -19,7 +19,7 @@
19
19
  inset: 0;
20
20
  z-index: 20;
21
21
  pointer-events: none;
22
- background: rgba(236, 233, 226, 0.75);
22
+ background: rgb(var(--bg-rgb) / 0.75);
23
23
  }
24
24
 
25
25
  .dropframe {
@@ -53,7 +53,7 @@
53
53
 
54
54
  .formulation-row select {
55
55
  font-family: var(--font-mono);
56
- font-size: 11px;
56
+ font-size: 13px;
57
57
  padding: 3px 22px 3px 8px;
58
58
  border: 1px solid var(--line);
59
59
  border-radius: 2px;
@@ -11,6 +11,7 @@
11
11
  const EXPORT_FORMATS = [
12
12
  { token: 'matpower', label: 'MATPOWER (.m)' },
13
13
  { token: 'psse', label: 'PSS/E (.raw)' },
14
+ { token: 'psse35', label: 'PSS/E v35 (.raw)' },
14
15
  { token: 'powerworld', label: 'PowerWorld (.aux)' },
15
16
  { token: 'pslf', label: 'PSLF (.epc)' },
16
17
  { token: 'powermodels-json', label: 'PowerModels (.json)' },
@@ -117,6 +118,8 @@
117
118
  <p class="footnote mono">
118
119
  no coordinates in this file: click the map or drop a geographic file
119
120
  </p>
121
+ {:else if lc.coordsKind === 'synthetic' && lc.summary.coords_method === 'generated'}
122
+ <p class="footnote mono">coordinates: synthetic grid sited on real geography</p>
120
123
  {:else if lc.coordsKind === 'synthetic' || lc.coordsKind === 'manual'}
121
124
  <p class="footnote mono">
122
125
  coordinates: {lc.coordsKind} topology layout centered where it was placed
@@ -222,7 +225,7 @@
222
225
 
223
226
  .footnote {
224
227
  margin: 8px 0 0;
225
- font-size: 10px;
228
+ font-size: 12px;
226
229
  color: var(--text-tertiary);
227
230
  letter-spacing: 0;
228
231
  }
@@ -231,7 +234,7 @@
231
234
  margin: 8px 0 0;
232
235
  padding: 0;
233
236
  list-style: none;
234
- font-size: 10.5px;
237
+ font-size: 12.5px;
235
238
  line-height: 1.5;
236
239
  color: var(--text-accent);
237
240
  }
@@ -254,7 +257,7 @@
254
257
  padding: 4px;
255
258
  list-style: none;
256
259
  min-width: 100%;
257
- background: var(--surface, #fff);
260
+ background: var(--surface, var(--surface-menu));
258
261
  border: 1px solid var(--border, rgba(0, 0, 0, 0.15));
259
262
  border-radius: 6px;
260
263
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
@@ -1,19 +1,43 @@
1
1
  <script lang="ts">
2
- import type { McPfResult, McComplex, DistGraph } from '@tellegen/engine';
2
+ import {
3
+ mcPfResultDetail,
4
+ summarizeMcPfResult,
5
+ type DistGraph,
6
+ type McComplex,
7
+ type McPfDetail,
8
+ type McPfDetailQuery,
9
+ type McPfResult,
10
+ type McPfSummary
11
+ } from '@tellegen/engine';
3
12
  import { neutralTerminal } from '../multiconductor.js';
13
+ import type { McEditTiming } from '../state.svelte.js';
4
14
  let {
5
15
  result,
16
+ detail,
17
+ timing = null,
6
18
  elapsedMs,
7
19
  selectedBus,
8
20
  selectedEdge,
9
21
  graph
10
22
  }: {
11
- result: McPfResult;
23
+ /** A live session's summary, or a stored full result. */
24
+ result: McPfSummary | McPfResult;
25
+ /** Fetches one bus's terminals and a page of equipment ports. A full
26
+ * `result` serves its own pages when this is omitted. */
27
+ detail?: (query: McPfDetailQuery) => Promise<McPfDetail | null>;
28
+ timing?: McEditTiming | null;
12
29
  elapsedMs: number | null;
13
30
  selectedBus: string | null;
14
31
  selectedEdge: string | null;
15
32
  graph: DistGraph | null;
16
33
  } = $props();
34
+ const summary = $derived('terminals' in result ? summarizeMcPfResult(result) : result);
35
+ const load = $derived.by(() => {
36
+ if (detail) return detail;
37
+ if (!('terminals' in result)) return null;
38
+ const full = result;
39
+ return (query: McPfDetailQuery) => Promise.resolve(mcPfResultDetail(full, query));
40
+ });
17
41
  let busChoice = $state('');
18
42
  let edgeChoice = $state('');
19
43
  const busId = $derived(
@@ -23,29 +47,48 @@
23
47
  );
24
48
  const bus = $derived(graph?.buses.find((b) => b.id === busId));
25
49
  const neutralName = $derived(bus ? neutralTerminal(bus.terminals, bus.neutral_terminal) : null);
26
- const neutral = $derived(
27
- result.terminals.find((t) => t.bus === busId && t.terminal === neutralName)?.voltage
28
- );
29
50
  const edgeId = $derived(selectedEdge ?? edgeChoice);
30
- const sourcePower = $derived(
31
- result.source_reactions.reduce(
32
- (sum, port) => ({
33
- re: sum.re + port.power_into_network.re,
34
- im: sum.im + port.power_into_network.im
35
- }),
36
- { re: 0, im: 0 }
37
- )
38
- );
39
- const passiveLoss = $derived(
40
- result.element_ports
41
- .filter((port) => !['load', 'generator', 'ibr'].includes(port.kind))
42
- .reduce((sum, port) => sum + port.power_into_element.re, 0)
43
- );
44
51
  let search = $state('');
45
52
  let page = $state(0);
46
53
  const size = 20;
54
+ let portPage = $state(0);
55
+ // Only the selected bus and one equipment page are fetched; the previous
56
+ // rows stay visible until the page for a new operating point arrives.
57
+ let current = $state.raw<McPfDetail | null>(null);
58
+ let requested = 0;
59
+ $effect(() => {
60
+ const fetchDetail = load;
61
+ const query: McPfDetailQuery = {
62
+ bus: busId || null,
63
+ element: edgeId || null,
64
+ port_offset: portPage * size,
65
+ port_limit: size
66
+ };
67
+ void result;
68
+ if (!fetchDetail) {
69
+ current = null;
70
+ return;
71
+ }
72
+ const seq = ++requested;
73
+ fetchDetail(query).then(
74
+ (next) => {
75
+ if (seq !== requested || !next) return;
76
+ if (next.element_port_total > 0 && query.port_offset! >= next.element_port_total) {
77
+ portPage = 0;
78
+ return;
79
+ }
80
+ current = next;
81
+ },
82
+ () => {}
83
+ );
84
+ });
85
+ const neutral = $derived(
86
+ current?.terminals.find((t) => t.bus === busId && t.terminal === neutralName)?.voltage
87
+ );
88
+ const sourcePower = $derived(summary.source_power_into_network);
89
+ const passiveLoss = $derived(summary.passive_loss.re);
47
90
  const terminals = $derived(
48
- result.terminals.filter(
91
+ (current?.terminals ?? []).filter(
49
92
  (row) =>
50
93
  (!busId || row.bus === busId) &&
51
94
  `${row.bus} ${row.terminal}`.toLowerCase().includes(search.toLowerCase())
@@ -54,13 +97,13 @@
54
97
  const pageCount = $derived(Math.max(1, Math.ceil(terminals.length / size)));
55
98
  const currentPage = $derived(Math.min(page, pageCount - 1));
56
99
  const visible = $derived(terminals.slice(currentPage * size, (currentPage + 1) * size));
57
- const ports = $derived(result.element_ports.filter((row) => !edgeId || row.element === edgeId));
58
- let portPage = $state(0);
59
- const portPages = $derived(Math.max(1, Math.ceil(ports.length / size)));
100
+ const portTotal = $derived(current?.element_port_total ?? 0);
101
+ const portPages = $derived(Math.max(1, Math.ceil(portTotal / size)));
60
102
  const currentPortPage = $derived(Math.min(portPage, portPages - 1));
61
- const visiblePorts = $derived(ports.slice(currentPortPage * size, (currentPortPage + 1) * size));
103
+ const visiblePorts = $derived(current?.element_ports ?? []);
62
104
  const magnitude = (value: McComplex) => Math.hypot(value.re, value.im);
63
105
  const fixed = (value: number) => (Number.isFinite(value) ? value.toFixed(3) : '-');
106
+ const ms = (value: number | null) => (value === null ? '-' : `${value.toFixed(1)} ms`);
64
107
  const angle = (value: McComplex) =>
65
108
  magnitude(value) > 1e-12 ? ((Math.atan2(value.im, value.re) * 180) / Math.PI).toFixed(2) : '-';
66
109
  </script>
@@ -68,7 +111,7 @@
68
111
  <section aria-label="AC power flow results" class="mc-results">
69
112
  <div class="result-heading">
70
113
  <strong>AC power flow</strong><span
71
- >{result.iterations} iterations{elapsedMs === null
114
+ >{summary.iterations} iterations{elapsedMs === null
72
115
  ? ''
73
116
  : `, ${Math.round(elapsedMs)} ms`}</span
74
117
  >
@@ -76,15 +119,15 @@
76
119
  <dl class="totals">
77
120
  <div>
78
121
  <dt>Status</dt>
79
- <dd>{result.converged ? 'Converged' : 'Not converged'}</dd>
122
+ <dd>{summary.converged ? 'Converged' : 'Not converged'}</dd>
80
123
  </div>
81
124
  <div>
82
125
  <dt>Voltage band</dt>
83
- <dd>{result.voltage_valid ? 'Valid' : `${result.voltage_violations.length} violations`}</dd>
126
+ <dd>{summary.voltage_valid ? 'Valid' : `${summary.voltage_violation_count} violations`}</dd>
84
127
  </div>
85
128
  <div>
86
129
  <dt>Load voltage range</dt>
87
- <dd>{result.min_voltage_pu == null || result.max_voltage_pu == null ? 'n/a' : `${fixed(result.min_voltage_pu)}–${fixed(result.max_voltage_pu)} pu`}</dd>
130
+ <dd>{summary.min_voltage_pu == null || summary.max_voltage_pu == null ? 'n/a' : `${fixed(summary.min_voltage_pu)}–${fixed(summary.max_voltage_pu)} pu`}</dd>
88
131
  </div>
89
132
  <div>
90
133
  <dt>Source P / Q</dt>
@@ -212,7 +255,7 @@
212
255
  </table>
213
256
  </div>
214
257
  <div class="pagination">
215
- <span>{ports.length} connections</span>{#if portPages > 1}<button
258
+ <span>{portTotal} connections</span>{#if portPages > 1}<button
216
259
  disabled={currentPortPage === 0}
217
260
  onclick={() => (portPage = currentPortPage - 1)}
218
261
  aria-label="Previous currents">Previous</button
@@ -228,28 +271,48 @@
228
271
  <dl>
229
272
  <div>
230
273
  <dt>Maximum KCL residual</dt>
231
- <dd>{result.physical_kcl_residual.toExponential(3)} A</dd>
274
+ <dd>{summary.physical_kcl_residual.toExponential(3)} A</dd>
232
275
  </div>
233
276
  <div>
234
277
  <dt>Scaled KCL residual</dt>
235
- <dd>{result.scaled_kcl_residual.toExponential(3)}</dd>
278
+ <dd>{summary.scaled_kcl_residual.toExponential(3)}</dd>
236
279
  </div>
237
280
  <div>
238
281
  <dt>Voltage change</dt>
239
- <dd>{result.voltage_change.toExponential(3)} V</dd>
282
+ <dd>{summary.voltage_change.toExponential(3)} V</dd>
240
283
  </div>
241
284
  <div>
242
285
  <dt>Matrix size</dt>
243
- <dd>{result.matrix_dimension}</dd>
286
+ <dd>{summary.matrix_dimension}</dd>
244
287
  </div>
245
288
  <div>
246
289
  <dt>Nonzero entries</dt>
247
- <dd>{result.matrix_nonzeros}</dd>
290
+ <dd>{summary.matrix_nonzeros}</dd>
248
291
  </div>
249
292
  <div>
250
293
  <dt>Factorizations</dt>
251
- <dd>{result.factorization_count}</dd>
294
+ <dd>{summary.factorization_count}</dd>
252
295
  </div>
296
+ {#if timing}
297
+ <div>
298
+ <dt>Engine solve</dt>
299
+ <dd>{ms(timing.engine_ms)}</dd>
300
+ </div>
301
+ <div>
302
+ <dt>Transfer and parse</dt>
303
+ <dd>
304
+ {ms(
305
+ timing.engine_ms === null
306
+ ? null
307
+ : timing.round_trip_ms - timing.engine_ms + timing.parse_ms
308
+ )}
309
+ </dd>
310
+ </div>
311
+ <div>
312
+ <dt>Total update</dt>
313
+ <dd>{ms(timing.total_ms)}</dd>
314
+ </div>
315
+ {/if}
253
316
  </dl>
254
317
  <p>
255
318
  Voltage is measured to ground. Net current is the injection into the network. The calculation
@@ -1,6 +1,12 @@
1
- import type { McPfResult, DistGraph } from '@tellegen/engine';
1
+ import { type DistGraph, type McPfDetail, type McPfDetailQuery, type McPfResult, type McPfSummary } from '@tellegen/engine';
2
+ import type { McEditTiming } from '../state.svelte.js';
2
3
  type $$ComponentProps = {
3
- result: McPfResult;
4
+ /** A live session's summary, or a stored full result. */
5
+ result: McPfSummary | McPfResult;
6
+ /** Fetches one bus's terminals and a page of equipment ports. A full
7
+ * `result` serves its own pages when this is omitted. */
8
+ detail?: (query: McPfDetailQuery) => Promise<McPfDetail | null>;
9
+ timing?: McEditTiming | null;
4
10
  elapsedMs: number | null;
5
11
  selectedBus: string | null;
6
12
  selectedEdge: string | null;
@@ -138,7 +138,7 @@
138
138
  display: grid;
139
139
  gap: 6px;
140
140
  margin: 12px 0;
141
- font-size: 11px;
141
+ font-size: 13px;
142
142
  }
143
143
  input {
144
144
  min-width: 0;
@@ -158,7 +158,7 @@
158
158
  table {
159
159
  width: 100%;
160
160
  border-collapse: collapse;
161
- font-size: 10px;
161
+ font-size: 12px;
162
162
  }
163
163
  caption {
164
164
  text-align: left;
@@ -194,7 +194,7 @@
194
194
  justify-content: space-between;
195
195
  gap: 8px;
196
196
  margin: 10px 0;
197
- font-size: 10px;
197
+ font-size: 12px;
198
198
  color: var(--text-secondary);
199
199
  }
200
200
  button {
@@ -218,13 +218,13 @@
218
218
  }
219
219
  .references {
220
220
  margin-top: 12px;
221
- font-size: 10px;
221
+ font-size: 12px;
222
222
  }
223
223
  .references dl > div {
224
224
  grid-template-columns: 42px minmax(0, 1fr);
225
225
  }
226
226
  code {
227
227
  overflow-wrap: anywhere;
228
- font: 10px/1.5 var(--font-mono);
228
+ font: 12px/1.5 var(--font-mono);
229
229
  }
230
230
  </style>
@@ -306,7 +306,7 @@
306
306
 
307
307
  .tag {
308
308
  margin: 0 0 10px;
309
- font-size: 10px;
309
+ font-size: 12px;
310
310
  color: var(--text-secondary);
311
311
  letter-spacing: 0;
312
312
  text-transform: uppercase;
@@ -353,7 +353,7 @@
353
353
  background: color-mix(in srgb, var(--tc) 26%, transparent);
354
354
  color: var(--ink);
355
355
  font-family: var(--font-mono);
356
- font-size: 11px;
356
+ font-size: 13px;
357
357
  }
358
358
 
359
359
  .term.grounded {
@@ -377,7 +377,7 @@
377
377
  border-radius: 3px;
378
378
  background: var(--bc);
379
379
  color: #fff;
380
- font-size: 10px;
380
+ font-size: 12px;
381
381
  letter-spacing: 0;
382
382
  }
383
383
 
@@ -389,7 +389,7 @@
389
389
 
390
390
  .load-editor h4 {
391
391
  margin: 0 0 8px;
392
- font-size: 11px;
392
+ font-size: 13px;
393
393
  }
394
394
 
395
395
  .load-row {
@@ -404,7 +404,7 @@
404
404
  display: flex;
405
405
  flex-direction: column;
406
406
  gap: 2px;
407
- font-size: 9px;
407
+ font-size: 12px;
408
408
  color: var(--text-secondary);
409
409
  }
410
410
 
@@ -412,11 +412,11 @@
412
412
  min-width: 0;
413
413
  width: 100%;
414
414
  box-sizing: border-box;
415
- font: 10px var(--font-mono);
415
+ font: 12px var(--font-mono);
416
416
  }
417
417
 
418
418
  .load-row button {
419
- font-size: 9px;
419
+ font-size: 12px;
420
420
  }
421
421
 
422
422
  /* The global stylesheet's .legend is the 6px color-ramp strip; its height
@@ -426,7 +426,7 @@
426
426
  flex-direction: column;
427
427
  gap: 4px;
428
428
  margin-top: 12px;
429
- font-size: 10px;
429
+ font-size: 12px;
430
430
  color: var(--text-secondary);
431
431
  }
432
432
 
@@ -469,7 +469,7 @@
469
469
 
470
470
  .footnote {
471
471
  margin: 8px 0 0;
472
- font-size: 10px;
472
+ font-size: 12px;
473
473
  color: var(--text-tertiary);
474
474
  letter-spacing: 0;
475
475
  }
@@ -12,14 +12,16 @@
12
12
  const observed = new WeakSet<object>();
13
13
  $effect(() => {
14
14
  const message = app.error;
15
+ const detail = app.errorDetail;
15
16
  const revision = app.errorRevision;
16
17
  const retry = app.errorRetry;
17
18
  if (message)
18
19
  untrack(() =>
19
20
  notices.push({
20
21
  kind: 'error',
21
- title: errorNoticeTitle(message),
22
- details: message,
22
+ // A message with its own detail is already written for people.
23
+ title: detail ? message : errorNoticeTitle(message),
24
+ details: detail ?? message,
23
25
  retry: () => ctrl.retryError(),
24
26
  canRetry: () =>
25
27
  app.errorRevision === revision && app.error === message && app.errorRetry === retry