@tellegen/svelte 0.3.0 → 0.4.1

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 (71) hide show
  1. package/README.md +233 -0
  2. package/dist/Sparkline.svelte +2 -2
  3. package/dist/TellegenMap.svelte +239 -66
  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 +55 -1
  13. package/dist/colors.js +89 -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/ControlPanel.svelte +4 -2
  19. package/dist/components/DemandSlider.svelte +16 -7
  20. package/dist/components/DiagramCanvas.svelte +17 -5
  21. package/dist/components/DisplayControls.svelte +5 -1
  22. package/dist/components/DropZone.svelte +1 -1
  23. package/dist/components/FormulationSelector.svelte +1 -1
  24. package/dist/components/LocalCaseDetails.svelte +6 -3
  25. package/dist/components/McResults.svelte +98 -35
  26. package/dist/components/McResults.svelte.d.ts +8 -2
  27. package/dist/components/ModelDetails.svelte +5 -5
  28. package/dist/components/MulticonductorDetails.svelte +9 -9
  29. package/dist/components/NotificationCenter.svelte +4 -2
  30. package/dist/components/PanelCard.svelte +72 -7
  31. package/dist/components/PanelHost.svelte +59 -34
  32. package/dist/components/PanelHost.svelte.d.ts +4 -1
  33. package/dist/components/PlacementCue.svelte +1 -1
  34. package/dist/components/PowerFlowDetails.svelte +1 -1
  35. package/dist/components/RatingSlider.svelte +14 -5
  36. package/dist/components/RestoreDefaultsButton.svelte +1 -1
  37. package/dist/components/SavedNetworkDetails.svelte +3 -3
  38. package/dist/components/SensitivityReadout.svelte +2 -2
  39. package/dist/components/ShellEnvironment.svelte +51 -0
  40. package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
  41. package/dist/components/SizeLegend.svelte +2 -2
  42. package/dist/components/SolveCard.svelte +3 -3
  43. package/dist/components/TopMovers.svelte +1 -1
  44. package/dist/components.d.ts +3 -0
  45. package/dist/components.js +3 -0
  46. package/dist/context.svelte.d.ts +8 -1
  47. package/dist/context.svelte.js +8 -3
  48. package/dist/controller.svelte.d.ts +65 -5
  49. package/dist/controller.svelte.js +266 -78
  50. package/dist/display.d.ts +3 -2
  51. package/dist/display.js +22 -4
  52. package/dist/format.d.ts +9 -0
  53. package/dist/format.js +13 -1
  54. package/dist/index.d.ts +6 -2
  55. package/dist/index.js +2 -0
  56. package/dist/map.d.ts +4 -2
  57. package/dist/map.js +2 -1
  58. package/dist/maplibre-worker.d.ts +5 -0
  59. package/dist/maplibre-worker.dev.d.ts +11 -0
  60. package/dist/maplibre-worker.dev.js +15 -0
  61. package/dist/maplibre-worker.js +8 -0
  62. package/dist/notices.svelte.js +5 -1
  63. package/dist/panels.svelte.d.ts +6 -0
  64. package/dist/panels.svelte.js +31 -1
  65. package/dist/slider.d.ts +12 -0
  66. package/dist/slider.js +31 -0
  67. package/dist/state.svelte.d.ts +25 -3
  68. package/dist/state.svelte.js +13 -0
  69. package/dist/styles.css +59 -6
  70. package/dist/types.d.ts +1 -1
  71. package/package.json +13 -8
package/dist/format.js CHANGED
@@ -26,6 +26,18 @@ export function solveMetaLabel(c) {
26
26
  return 'server price sensitivity';
27
27
  return c.solveBackend === 'rust-server' ? 'server solve' : 'browser solve';
28
28
  }
29
+ export const INFEASIBLE_DISPATCH = 'No feasible dispatch: demand cannot be served within line and generator limits';
30
+ /** The message for a local case whose browser solve failed, and the
31
+ * technical detail to disclose behind it. An infeasible DC OPF means the
32
+ * network cannot carry the demand, which deserves plain words. The engine
33
+ * spells it `DC OPF solve infeasible`, and an unbounded one as `unbounded`. */
34
+ export function browserSolveFailure(label, reason) {
35
+ const technical = `${label}: browser solve unavailable${reason ? `: ${reason}` : ''}`;
36
+ if (reason && /\binfeasible\b/i.test(reason)) {
37
+ return { message: INFEASIBLE_DISPATCH, detail: technical };
38
+ }
39
+ return { message: technical, detail: null };
40
+ }
29
41
  export function rgbaCss([r, g, b, a]) {
30
42
  return `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
31
43
  }
@@ -47,4 +59,4 @@ export function extent(values) {
47
59
  export const fmt = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 });
48
60
  export const signed = (v) => `${v < 0 ? '−' : '+'}${fmt.format(Math.abs(v))}`;
49
61
  export const signedExp = (v) => `${v < 0 ? '−' : '+'}${Math.abs(v).toExponential(2)}`;
50
- export const displayFmt = (mode, value) => mode === 'price' ? fmt.format(value) : value.toFixed(3);
62
+ export const displayFmt = (mode, value) => mode === 'price' || mode === 'demand' ? fmt.format(value) : value.toFixed(3);
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@ export { default as TellegenViewer } from './TellegenViewer.svelte';
9
9
  export { default as TellegenMap } from './TellegenMap.svelte';
10
10
  export { DEFAULT_TELLEGEN_UI_CONFIG, getAppState, getController, getUiConfig, resolveTellegenUiConfig, setAppState, setController, setUiConfig } from './context.svelte.js';
11
11
  export { ApiError, createApiClient } from './api.js';
12
+ export { CARTO_STYLES, OPENFREEMAP_STYLES, attachBasemapFallback, cartoStyleUrl, defaultBasemap, isOpenFreeMapStyle, openFreeMapEdits, resolveBasemap } from './basemap.js';
13
+ export { MAP_PALETTE } from './colors.js';
12
14
  export { Controller, createController } from './controller.svelte.js';
13
15
  export { AppState, CaseState, LocalCase, MulticonductorCase, createAppState } from './state.svelte.js';
14
16
  export { attachmentColor, attachmentGlyph, edgeColor, edgeWidth, phaseColor, neutralTerminal, placeMultiView, buildGeographicView } from './multiconductor.js';
@@ -17,9 +19,11 @@ export { caseDeltas, caseRatings, displayMetaFor, displaySeriesFor } from './dis
17
19
  export { DEFAULT_FORMULATION, FORMULATIONS, BrowserStudy, Study, capabilities, createStudy, errorText, formatOf, classifyJson, ingestJsonDrop, ingestCase, isDisplayFile, isPermanentEngineFailure, parseDisplay, solveModule } from '@tellegen/engine';
18
20
  export type { BranchRatingDeltas, CaseSummary, DemandDeltas, Network, NetworkBranch, NetworkBus, SensitivityColumn, Solution, SolveIteration, SolveStreamHandlers, TellegenApiClient, TellegenApiClientOptions } from './api.js';
19
21
  export type { TellegenUiConfig, TellegenUiOptions } from './context.svelte.js';
20
- export type { ControllerOptions } from './controller.svelte.js';
22
+ export type { ControllerOptions, OpenBalancedModuleInput } from './controller.svelte.js';
23
+ export type { BasemapFallbackReason, BasemapMapLike, BasemapProvider, BasemapSpec, BasemapStyle, BasemapSwitch, BasemapTheme, DefaultBasemap, MapProjection, MapTheme, ResolvedBasemap, StyleEdit } from './basemap.js';
24
+ export type { MapPalette } from './colors.js';
21
25
  export type { DisplayOption } from './display.js';
22
- export type { CameraSnapshot, DiagramCameraSnapshot, DemandRangeMode, DisplayMode, FallbackTarget, LocalCaseInit, LocalSubstations, MultiCaseSummary, MultiCoordsKind, SolvableCase, StudyDisplaySnapshot, SolveBackend } from './state.svelte.js';
26
+ export type { BusSizing, CameraSnapshot, DiagramCameraSnapshot, DemandRangeMode, DisplayMode, FallbackTarget, LocalCaseInit, LocalSubstations, MultiCaseSummary, MultiCoordsKind, SolvableCase, StudyDisplaySnapshot, SolveBackend } from './state.svelte.js';
23
27
  export type { MultiView, PlacedMultiBus, PlacedMultiEdge } from './multiconductor.js';
24
28
  export type { CaseFileSummary, DistAttachmentKind, DistEdgeKind, DistGraph, DistGraphAttachment, DistGraphBus, DistGraphEdge, DisplayPreview, Formulation, IngestedCase, IngestedDistCase, IngestedJsonDrop, JsonDropClassification, JsonDropKind, CapacityPlanBusWeightJson, CapacityPlanIterationJson, CapacityPlanOutcomeJson, CapacityPlanRatingChangeJson, CapacityPlanResultSummaryJson, CapacityPlanSpecJson, ImplicitObjectiveJson, SensTarget, Topology, TopologyBranch, TopologyBus } from '@tellegen/engine';
25
29
  export { default as McResults } from './components/McResults.svelte';
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ export { default as TellegenViewer } from './TellegenViewer.svelte';
8
8
  export { default as TellegenMap } from './TellegenMap.svelte';
9
9
  export { DEFAULT_TELLEGEN_UI_CONFIG, getAppState, getController, getUiConfig, resolveTellegenUiConfig, setAppState, setController, setUiConfig } from './context.svelte.js';
10
10
  export { ApiError, createApiClient } from './api.js';
11
+ export { CARTO_STYLES, OPENFREEMAP_STYLES, attachBasemapFallback, cartoStyleUrl, defaultBasemap, isOpenFreeMapStyle, openFreeMapEdits, resolveBasemap } from './basemap.js';
12
+ export { MAP_PALETTE } from './colors.js';
11
13
  export { Controller, createController } from './controller.svelte.js';
12
14
  export { AppState, CaseState, LocalCase, MulticonductorCase, createAppState } from './state.svelte.js';
13
15
  export { attachmentColor, attachmentGlyph, edgeColor, edgeWidth, phaseColor, neutralTerminal, placeMultiView, buildGeographicView } from './multiconductor.js';
package/dist/map.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { ComponentProps } from 'svelte';
2
2
  import TellegenMap from './TellegenMap.svelte';
3
3
  export { default as TellegenMap } from './TellegenMap.svelte';
4
- export { branchColor, branchWidth, busNeutral, busRadius, priceColor, priceDomain, priceGradient, scalarDomain, sensColor, sensFlatColor, sensGradient, sensNeutral, sensitivityDomain } from './colors.js';
4
+ export { CARTO_STYLES, OPENFREEMAP_STYLES, attachBasemapFallback, cartoStyleUrl, defaultBasemap, isOpenFreeMapStyle, openFreeMapEdits, resolveBasemap } from './basemap.js';
5
+ export type { BasemapFallbackReason, BasemapMapLike, BasemapProvider, BasemapSpec, BasemapStyle, BasemapSwitch, BasemapTheme, DefaultBasemap, MapProjection, MapTheme, ResolvedBasemap, StyleEdit } from './basemap.js';
6
+ export { MAP_PALETTE, branchColor, branchWidth, busNeutral, busRadius, busMarkerRadius, busRadiusFor, demandColor, demandDomain, demandGradient, priceColor, priceDomain, priceGradient, scalarDomain, sensColor, sensFlatColor, sensGradient, sensNeutral, sensitivityDomain, UNIFORM_BUS_GROUND_RADIUS, UNIFORM_BUS_MIN_RADIUS, UNIFORM_BUS_RADIUS } from './colors.js';
5
7
  export { caseDeltas, displayMetaFor, displaySeriesFor } from './display.js';
6
- export type { RGBA, SensitivityDomain } from './colors.js';
8
+ export type { MapPalette, RGBA, SensitivityDomain } from './colors.js';
7
9
  export type { DisplayOption } from './display.js';
8
10
  export type TellegenMapProps = ComponentProps<typeof TellegenMap>;
package/dist/map.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import TellegenMap from './TellegenMap.svelte';
2
2
  export { default as TellegenMap } from './TellegenMap.svelte';
3
- export { branchColor, branchWidth, busNeutral, busRadius, priceColor, priceDomain, priceGradient, scalarDomain, sensColor, sensFlatColor, sensGradient, sensNeutral, sensitivityDomain } from './colors.js';
3
+ export { CARTO_STYLES, OPENFREEMAP_STYLES, attachBasemapFallback, cartoStyleUrl, defaultBasemap, isOpenFreeMapStyle, openFreeMapEdits, resolveBasemap } from './basemap.js';
4
+ export { MAP_PALETTE, branchColor, branchWidth, busNeutral, busRadius, busMarkerRadius, busRadiusFor, demandColor, demandDomain, demandGradient, priceColor, priceDomain, priceGradient, scalarDomain, sensColor, sensFlatColor, sensGradient, sensNeutral, sensitivityDomain, UNIFORM_BUS_GROUND_RADIUS, UNIFORM_BUS_MIN_RADIUS, UNIFORM_BUS_RADIUS } from './colors.js';
4
5
  export { caseDeltas, displayMetaFor, displaySeriesFor } from './display.js';
@@ -0,0 +1,5 @@
1
+ /** The URL of MapLibre's worker, which the host's Vite build bundles with its
2
+ * shared chunk. `TellegenMap` hands it to `setWorkerUrl` before it creates a
3
+ * map. Reached through the `@tellegen/svelte/maplibre-worker` export; see
4
+ * `maplibre-worker.dev.ts` for the dev server. */
5
+ export declare function maplibreWorkerUrl(): Promise<string>;
@@ -0,0 +1,11 @@
1
+ /** The dev server's `@tellegen/svelte/maplibre-worker`, chosen by the
2
+ * `development` export condition.
3
+ *
4
+ * Vite's dev server pre-bundles this package with Rolldown, which cannot load
5
+ * the `?worker&url` import in `maplibre-worker.ts`, so a static import of that
6
+ * module would stop the dev server at startup. Here it is imported at run time
7
+ * by URL instead, outside the pre-bundle, and the dev server resolves the
8
+ * query itself. Vite rewrites the `new URL` in a pre-bundled file to point at
9
+ * the original. A production build takes the default condition and imports
10
+ * `maplibre-worker.ts` directly. */
11
+ export declare function maplibreWorkerUrl(): Promise<string>;
@@ -0,0 +1,15 @@
1
+ /** The dev server's `@tellegen/svelte/maplibre-worker`, chosen by the
2
+ * `development` export condition.
3
+ *
4
+ * Vite's dev server pre-bundles this package with Rolldown, which cannot load
5
+ * the `?worker&url` import in `maplibre-worker.ts`, so a static import of that
6
+ * module would stop the dev server at startup. Here it is imported at run time
7
+ * by URL instead, outside the pre-bundle, and the dev server resolves the
8
+ * query itself. Vite rewrites the `new URL` in a pre-bundled file to point at
9
+ * the original. A production build takes the default condition and imports
10
+ * `maplibre-worker.ts` directly. */
11
+ export async function maplibreWorkerUrl() {
12
+ const url = new URL('./maplibre-worker.js', import.meta.url).href;
13
+ const module = await import(/* @vite-ignore */ url);
14
+ return module.maplibreWorkerUrl();
15
+ }
@@ -0,0 +1,8 @@
1
+ import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
2
+ /** The URL of MapLibre's worker, which the host's Vite build bundles with its
3
+ * shared chunk. `TellegenMap` hands it to `setWorkerUrl` before it creates a
4
+ * map. Reached through the `@tellegen/svelte/maplibre-worker` export; see
5
+ * `maplibre-worker.dev.ts` for the dev server. */
6
+ export async function maplibreWorkerUrl() {
7
+ return workerUrl;
8
+ }
@@ -70,11 +70,15 @@ export function getNoticeCenter() {
70
70
  export function errorNoticeTitle(details) {
71
71
  if (/rate limit/i.test(details))
72
72
  return 'Too many requests';
73
+ // MapLibre's WebGL failure includes "Read more"; classify our map errors
74
+ // before the generic file/read fallback so it does not blame a case file.
75
+ if (/^map failed to load:|^the map lost its graphics context/i.test(details))
76
+ return 'Map unavailable';
73
77
  if (/storage|quota|space|indexeddb/i.test(details))
74
78
  return 'Could not save locally';
75
79
  if (/sensitivity|derivative|singular/i.test(details))
76
80
  return 'Sensitivity unavailable';
77
- if (/infeasib|converg|solver|solving|calculation|compute|webassembly/i.test(details))
81
+ if (/infeasib|converg|solver|solving|calculation|compute|webassembly|power flow/i.test(details))
78
82
  return 'Calculation did not complete';
79
83
  if (/parse|read|file|format/i.test(details))
80
84
  return 'Could not read this file';
@@ -49,6 +49,9 @@ export declare class PanelLayout {
49
49
  position(panel: PanelRegistration): PanelPosition;
50
50
  ordered(side?: PanelSide): PanelRegistration[];
51
51
  toggle(panel: PanelRegistration): void;
52
+ /** The control that had focus when the panel last opened, while it is still
53
+ * in the document: where focus returns when the panel closes. */
54
+ opener(panel: PanelRegistration): HTMLElement | SVGElement | null;
52
55
  close(panel: PanelRegistration): void;
53
56
  dock(panel: PanelRegistration, side?: PanelSide): void;
54
57
  float(panel: PanelRegistration, bounds?: PanelBounds): void;
@@ -60,6 +63,9 @@ export declare class PanelLayout {
60
63
  initialize(storage?: Storage): void;
61
64
  save(): void;
62
65
  }
66
+ /** Focus the first candidate that takes focus. A detached or hidden element
67
+ * does not, so the next one gets its turn. */
68
+ export declare function focusFirst(candidates: Array<HTMLElement | SVGElement | null | undefined>): HTMLElement | SVGElement | null;
63
69
  export declare const setPanelLayout: (layout: PanelLayout) => PanelLayout;
64
70
  export declare const getPanelLayout: () => PanelLayout;
65
71
  export {};
@@ -18,6 +18,7 @@ export class PanelLayout {
18
18
  bottom = $state(88);
19
19
  storageError = $state(null);
20
20
  #storage;
21
+ #openers = new Map();
21
22
  #gesture = null;
22
23
  beginGesture(panel, kind, x, y, bounds) {
23
24
  this.#gesture = { panel, kind, x, y, bounds, moved: false };
@@ -60,6 +61,7 @@ export class PanelLayout {
60
61
  return;
61
62
  this.panels = this.panels.map((p) => (p.id === id ? { ...p, open, title, width } : p));
62
63
  if (open && !previous.open) {
64
+ this.#rememberOpener(id);
63
65
  this.activate(previous);
64
66
  if (this.compact)
65
67
  this.drawer = id;
@@ -91,19 +93,35 @@ export class PanelLayout {
91
93
  if (this.compact) {
92
94
  this.drawer = this.drawer === panel.id ? null : panel.id;
93
95
  if (this.drawer === panel.id) {
96
+ this.#rememberOpener(panel.id);
94
97
  this.activate(panel);
95
98
  panel.setOpen(true);
96
99
  panel.onopen?.();
97
100
  }
98
101
  }
99
102
  else {
100
- if (!panel.open)
103
+ if (!panel.open) {
104
+ this.#rememberOpener(panel.id);
101
105
  this.activate(panel);
106
+ }
102
107
  panel.setOpen(!panel.open);
103
108
  if (!panel.open)
104
109
  panel.onopen?.();
105
110
  }
106
111
  }
112
+ /** The control that had focus when the panel last opened, while it is still
113
+ * in the document: where focus returns when the panel closes. */
114
+ opener(panel) {
115
+ const element = this.#openers.get(panel.id);
116
+ return element?.isConnected ? element : null;
117
+ }
118
+ #rememberOpener(id) {
119
+ const active = typeof document === 'undefined' ? null : document.activeElement;
120
+ if ((active instanceof HTMLElement || active instanceof SVGElement) && active !== document.body)
121
+ this.#openers.set(id, active);
122
+ else
123
+ this.#openers.delete(id);
124
+ }
107
125
  close(panel) {
108
126
  panel.setOpen(false);
109
127
  if (this.drawer === panel.id)
@@ -206,6 +224,18 @@ export class PanelLayout {
206
224
  }
207
225
  }
208
226
  }
227
+ /** Focus the first candidate that takes focus. A detached or hidden element
228
+ * does not, so the next one gets its turn. */
229
+ export function focusFirst(candidates) {
230
+ for (const candidate of candidates) {
231
+ if (!candidate?.isConnected)
232
+ continue;
233
+ candidate.focus();
234
+ if (candidate.ownerDocument.activeElement === candidate)
235
+ return candidate;
236
+ }
237
+ return null;
238
+ }
209
239
  export const setPanelLayout = (layout) => setContext(KEY, layout);
210
240
  export const getPanelLayout = () => {
211
241
  const layout = getContext(KEY);
@@ -0,0 +1,12 @@
1
+ /** Smallest demand or rating edit that counts: a slider released closer than
2
+ * this to the committed value commits nothing and starts no solve. */
3
+ export declare const DEAD_ZONE_MW = 0.25;
4
+ /** Arrow-key step of a demand or rating slider spanning `span` MW: about 1%
5
+ * of the span, rounded up to 1, 2 or 5 times a power of ten. It is at least
6
+ * twice the dead zone, so one press always leaves it, rounding error
7
+ * included. The native step of a `step="any"` range is 1% of the span, which
8
+ * on a small bus never leaves the dead zone. */
9
+ export declare function sliderKeyStep(span: number): number;
10
+ /** The value an arrow key moves a slider to, kept within [min, max], or null
11
+ * for any other key. Right and up raise it. */
12
+ export declare function sliderKeyValue(key: string, value: number, min: number, max: number): number | null;
package/dist/slider.js ADDED
@@ -0,0 +1,31 @@
1
+ /** Smallest demand or rating edit that counts: a slider released closer than
2
+ * this to the committed value commits nothing and starts no solve. */
3
+ export const DEAD_ZONE_MW = 0.25;
4
+ const KEY_DIRECTIONS = {
5
+ ArrowRight: 1,
6
+ ArrowUp: 1,
7
+ ArrowLeft: -1,
8
+ ArrowDown: -1
9
+ };
10
+ /** Arrow-key step of a demand or rating slider spanning `span` MW: about 1%
11
+ * of the span, rounded up to 1, 2 or 5 times a power of ten. It is at least
12
+ * twice the dead zone, so one press always leaves it, rounding error
13
+ * included. The native step of a `step="any"` range is 1% of the span, which
14
+ * on a small bus never leaves the dead zone. */
15
+ export function sliderKeyStep(span) {
16
+ const target = Math.max(span / 100, 2 * DEAD_ZONE_MW);
17
+ const power = 10 ** Math.floor(Math.log10(target));
18
+ for (const factor of [1, 2, 5]) {
19
+ if (factor * power >= target)
20
+ return factor * power;
21
+ }
22
+ return 10 * power;
23
+ }
24
+ /** The value an arrow key moves a slider to, kept within [min, max], or null
25
+ * for any other key. Right and up raise it. */
26
+ export function sliderKeyValue(key, value, min, max) {
27
+ const direction = KEY_DIRECTIONS[key];
28
+ if (direction === undefined)
29
+ return null;
30
+ return Math.min(max, Math.max(min, value + direction * sliderKeyStep(max - min)));
31
+ }
@@ -1,5 +1,5 @@
1
1
  import type { CaseSummary, DemandDeltas, Network, NetworkBranch, NetworkBus, SensitivityColumn, Solution, SolveIteration } from './api.js';
2
- import { type StudyView, type BranchRatingDeltas, type BrowserMcPfSession, type CaseFileSummary, type DistGraph, type Formulation, type IngestedDistCase, type McPfResult, type McLoadBranchState, type McLoadPowerEdit, type McStudySnapshot, type SensTarget, type Topology } from '@tellegen/engine';
2
+ import { type StudyView, type BranchRatingDeltas, type BrowserMcPfSession, type CaseFileSummary, type DistGraph, type Formulation, type IngestedDistCase, type McPfCallTiming, type McPfResult, type McPfSummary, type McLoadBranchState, type McLoadPowerEdit, type McStudySnapshot, type SensTarget, type Topology } from '@tellegen/engine';
3
3
  import type { MultiView } from './multiconductor.js';
4
4
  export type SolveBackend = 'clarabel-wasm' | 'clarabel-wasm-server-sensitivity' | 'rust-server';
5
5
  /** A map framing request: a case id, 'all', or one branch to center. */
@@ -22,7 +22,11 @@ export interface DiagramCameraSnapshot {
22
22
  scale: number;
23
23
  }
24
24
  export type DemandRangeMode = 'local' | 'full';
25
- export type DisplayMode = 'price' | 'angle' | 'voltage';
25
+ export type DisplayMode = 'price' | 'angle' | 'voltage' | 'demand';
26
+ /** How bus markers are sized on the map: by the larger of demand and generation
27
+ * (`power`), or all at one size (`uniform`) so dense networks keep their lines
28
+ * readable and colour carries the variable. */
29
+ export type BusSizing = 'power' | 'uniform';
26
30
  /** The case a removal promoted to active, so the caller can hydrate it: a backend
27
31
  * case needs its network/solution loaded, a local needs a browser solve, and
28
32
  * `none` means nothing remains (or the removal left the active case untouched). */
@@ -174,6 +178,12 @@ export type SolvableCase = CaseState | LocalCase;
174
178
  export type MultiCaseSummary = Omit<IngestedDistCase, 'graph'>;
175
179
  /** Geographic coordinates use the map; other positions use the diagram canvas. */
176
180
  export type MultiCoordsKind = 'geographic' | 'planar' | 'synthetic';
181
+ /** Where one interactive load edit spent its time, in milliseconds: engine
182
+ * (solve and summary), transfer, and parsing as recorded by the session, and
183
+ * the total from the start of the solve until the result state is updated. */
184
+ export interface McEditTiming extends McPfCallTiming {
185
+ total_ms: number;
186
+ }
177
187
  /** A conductor-resolved case with retained electrical inputs and terminal results. */
178
188
  export declare class MulticonductorCase {
179
189
  readonly id: string;
@@ -182,7 +192,14 @@ export declare class MulticonductorCase {
182
192
  moduleJson: string | null;
183
193
  geoLayer: string | null;
184
194
  geoWarnings: string[];
185
- result: McPfResult | null;
195
+ /** Summary of the displayed operating point: from the live session after
196
+ * each solve or load edit, or derived from a stored full result. */
197
+ result: McPfSummary | null;
198
+ /** The complete result when no live session holds the operating point: a
199
+ * saved Study, a solve without sessions, or after coordinates are attached. */
200
+ mcFullResult: McPfResult | null;
201
+ /** Latency breakdown of the latest load-edit solve. */
202
+ mcTiming: McEditTiming | null;
186
203
  mcSnapshot: McStudySnapshot | null;
187
204
  mcSession: BrowserMcPfSession | null;
188
205
  mcLoadBranches: McLoadBranchState[];
@@ -280,11 +297,16 @@ export declare class AppState {
280
297
  } | null;
281
298
  demandRangeMode: DemandRangeMode;
282
299
  displayMode: DisplayMode;
300
+ busSizing: BusSizing;
283
301
  sensitivityLoading: boolean;
284
302
  errorRevision: number;
285
303
  /** Re-runs the operation behind the current `error`, when one applies. Every
286
304
  * write to `error` clears it, so a retry op can never outlive its message. */
287
305
  errorRetry: (() => void) | null;
306
+ /** Technical detail behind the current `error`, when the message itself is
307
+ * written for people; notices keep it in their disclosure. Every write to
308
+ * `error` clears it, as it clears `errorRetry`. */
309
+ errorDetail: string | null;
288
310
  get error(): string | null;
289
311
  set error(message: string | null);
290
312
  /** Case files parsed in the browser via the powerio wasm module. */
@@ -122,7 +122,14 @@ export class MulticonductorCase {
122
122
  moduleJson = $state.raw(null);
123
123
  geoLayer = $state.raw(null);
124
124
  geoWarnings = $state.raw([]);
125
+ /** Summary of the displayed operating point: from the live session after
126
+ * each solve or load edit, or derived from a stored full result. */
125
127
  result = $state.raw(null);
128
+ /** The complete result when no live session holds the operating point: a
129
+ * saved Study, a solve without sessions, or after coordinates are attached. */
130
+ mcFullResult = $state.raw(null);
131
+ /** Latency breakdown of the latest load-edit solve. */
132
+ mcTiming = $state.raw(null);
126
133
  mcSnapshot = $state.raw(null);
127
134
  mcSession = $state.raw(null);
128
135
  mcLoadBranches = $state.raw([]);
@@ -245,12 +252,17 @@ export class AppState {
245
252
  previewPrices = $state.raw(null);
246
253
  demandRangeMode = $state('local');
247
254
  displayMode = $state('price');
255
+ busSizing = $state('power');
248
256
  sensitivityLoading = $state(false);
249
257
  #error = $state(null);
250
258
  errorRevision = $state(0);
251
259
  /** Re-runs the operation behind the current `error`, when one applies. Every
252
260
  * write to `error` clears it, so a retry op can never outlive its message. */
253
261
  errorRetry = $state.raw(null);
262
+ /** Technical detail behind the current `error`, when the message itself is
263
+ * written for people; notices keep it in their disclosure. Every write to
264
+ * `error` clears it, as it clears `errorRetry`. */
265
+ errorDetail = $state(null);
254
266
  get error() {
255
267
  return this.#error;
256
268
  }
@@ -259,6 +271,7 @@ export class AppState {
259
271
  if (message)
260
272
  this.errorRevision++;
261
273
  this.errorRetry = null;
274
+ this.errorDetail = null;
262
275
  }
263
276
  /** Case files parsed in the browser via the powerio wasm module. */
264
277
  localCases = $state.raw([]);
package/dist/styles.css CHANGED
@@ -32,6 +32,7 @@
32
32
  --surface-panel: var(--panel);
33
33
  --surface-chip: rgb(var(--paper-rgb) / 0.65);
34
34
  --surface-control: rgb(var(--paper-rgb) / 0.55);
35
+ --surface-menu: #fff;
35
36
  --border-default: var(--line);
36
37
  --border-strong: var(--accent);
37
38
  --text-primary: var(--ink);
@@ -47,6 +48,10 @@
47
48
  --data-pos: var(--pos);
48
49
  --data-neg: var(--neg);
49
50
  --focus-ring: var(--accent);
51
+ /* unselected bus outline in the plain diagram view */
52
+ --diagram-bus-stroke: #756a5b;
53
+ /* ground symbol in the multiconductor bus detail */
54
+ --mc-ground-stroke: #3a3a3a;
50
55
 
51
56
  /* type scale, anchored at the 15px body */
52
57
  --fs-display-lg: 33px;
@@ -81,6 +86,11 @@
81
86
  --radius-full: 50%;
82
87
  --elev-1: 0 2px 10px rgb(var(--ink-rgb) / 0.1);
83
88
  --elev-2: 0 4px 24px rgb(var(--ink-rgb) / 0.08);
89
+ /* floating controls that keep their own light shadows; the dark theme maps
90
+ them onto the two steps above */
91
+ --elev-cue: 0 4px 18px rgb(var(--ink-rgb) / 0.1);
92
+ --elev-menu: 0 4px 18px rgb(var(--ink-rgb) / 0.14);
93
+ --elev-tooltip: 0 2px 10px rgb(var(--ink-rgb) / 0.12);
84
94
  --shadow-thumb: 0 1px 4px rgb(var(--ink-rgb) / 0.3);
85
95
  --inset-active: inset 0 -2px 0 var(--accent);
86
96
 
@@ -103,6 +113,49 @@
103
113
  --z-grain: 9999;
104
114
  }
105
115
 
116
+ /* Dark theme: `TellegenProvider theme="dark"` sets the attribute (a host may
117
+ also put it in its template). Contrast ratios are measured against the panel
118
+ composited over #4a4a4a, the darkest grey a dark basemap paints under it:
119
+ every text token clears 7:1. --border-strong marks a panel's edge over the
120
+ map, so it clears the 3:1 non-text minimum against that panel and against
121
+ bare #4a4a4a. The same markup passes WCAG AA on either ground. */
122
+ :root[data-theme='dark'] {
123
+ --bg: #0b1420;
124
+ --paper: #0f1a27;
125
+ --panel: rgba(10, 20, 31, 0.92);
126
+ --line: #34465a; /* decorative separators only */
127
+ --border-strong: #859cb4; /* 6.30:1 against the panel, 3.13:1 against bare #4a4a4a */
128
+ --ink: #e8eef4; /* 15.3:1 */
129
+ --ink-dim: #b4c1cd; /* 9.7:1 */
130
+ --ink-faint: #95a6b6; /* 7.14:1, and 7.02:1 on the opaque --paper of a panel card */
131
+ --accent: #ffb24d;
132
+ --accent-bright: #ffc46e; /* 11.4:1 */
133
+ --accent-soft: rgba(255, 178, 77, 0.14);
134
+ --red: #ff9f8b; /* 9.0:1 */
135
+ --pos: #c9a7e0;
136
+ --neg: #8fd0a4;
137
+ --bg-rgb: 11 20 32;
138
+ --ink-rgb: 232 238 244;
139
+ --accent-rgb: 255 178 77;
140
+ --paper-rgb: 15 26 39;
141
+ --text-primary: var(--ink);
142
+ --text-secondary: var(--ink-dim);
143
+ --text-tertiary: var(--ink-faint);
144
+ --text-accent: var(--accent-bright);
145
+ --text-danger: var(--red);
146
+ --focus-ring: #8de7f7; /* 12.7:1 */
147
+ --surface-menu: var(--paper);
148
+ --diagram-bus-stroke: var(--ink-faint);
149
+ --mc-ground-stroke: var(--ink-dim);
150
+ --elev-1: 0 2px 10px rgb(0 0 0 / 0.45);
151
+ --elev-2: 0 4px 24px rgb(0 0 0 / 0.4);
152
+ --elev-cue: var(--elev-2);
153
+ --elev-menu: var(--elev-2);
154
+ --elev-tooltip: var(--elev-1);
155
+ /* the light form derives from --ink-rgb, which would glow here */
156
+ --shadow-thumb: 0 1px 4px rgb(0 0 0 / 0.5);
157
+ }
158
+
106
159
  * {
107
160
  box-sizing: border-box;
108
161
  }
@@ -225,7 +278,7 @@ a {
225
278
  ControlPanel.svelte; the .mode / .display-mode / @media source order is
226
279
  preserved so the cascade resolves exactly as before. */
227
280
  .region {
228
- font-size: 10px;
281
+ font-size: 12px;
229
282
  font-weight: 400;
230
283
  color: var(--text-secondary);
231
284
  text-transform: uppercase;
@@ -245,7 +298,7 @@ a {
245
298
  before the component split. Scoping this would raise its specificity past that rule. */
246
299
  .mode > button {
247
300
  margin-left: auto;
248
- font-size: 10.5px;
301
+ font-size: 12.5px;
249
302
  padding: 2px 7px;
250
303
  background: none;
251
304
  border: 1px solid var(--line);
@@ -264,7 +317,7 @@ a {
264
317
  }
265
318
 
266
319
  .display-mode .segment button {
267
- font-size: 11px;
320
+ font-size: 13px;
268
321
  padding: 2px 9px;
269
322
  }
270
323
 
@@ -282,7 +335,7 @@ a {
282
335
  .legend-labels {
283
336
  display: flex;
284
337
  justify-content: space-between;
285
- font-size: 10.5px;
338
+ font-size: 12.5px;
286
339
  color: var(--text-tertiary);
287
340
  margin-top: 4px;
288
341
  }
@@ -301,7 +354,7 @@ a {
301
354
  border-radius: 999px;
302
355
  color: var(--text-secondary);
303
356
  background: var(--surface-control);
304
- font-size: 10px;
357
+ font-size: 12px;
305
358
  line-height: 1;
306
359
  cursor: help;
307
360
  }
@@ -334,7 +387,7 @@ a {
334
387
 
335
388
  .reset {
336
389
  margin-top: 10px;
337
- font-size: 10.5px;
390
+ font-size: 12.5px;
338
391
  padding: 3px 10px;
339
392
  background: none;
340
393
  border: 1px solid var(--line);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { CaseSummary, DemandDeltas, Network, NetworkBranch, NetworkBus, SensitivityColumn, Solution, SolveIteration, SolveStreamHandlers } from './api.js';
2
2
  export type { DisplayOption } from './display.js';
3
3
  export type { PlacedNetwork, PlacementCenter } from './synthetic-layout.js';
4
- export type { DemandRangeMode, DisplayMode, FallbackTarget, LocalCaseInit, LocalSubstations, SolvableCase, SolveBackend } from './state.svelte.js';
4
+ export type { BusSizing, DemandRangeMode, DisplayMode, FallbackTarget, LocalCaseInit, LocalSubstations, SolvableCase, SolveBackend } from './state.svelte.js';
5
5
  export type { AppliedGeoCase, CaseFileSummary, DisplayPreview, Formulation, GeoApplyReport, IngestedCase, ParsedGeoLayer, StampedLayoutCase, Topology, TopologyBranch, TopologyBus } from '@tellegen/engine';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellegen/svelte",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Svelte components for tellegen power system maps, local case files, and browser solves.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -53,6 +53,11 @@
53
53
  "svelte": "./dist/map.js",
54
54
  "default": "./dist/map.js"
55
55
  },
56
+ "./maplibre-worker": {
57
+ "types": "./dist/maplibre-worker.d.ts",
58
+ "development": "./dist/maplibre-worker.dev.js",
59
+ "default": "./dist/maplibre-worker.js"
60
+ },
56
61
  "./styles.css": "./dist/styles.css"
57
62
  },
58
63
  "scripts": {
@@ -65,19 +70,19 @@
65
70
  "@deck.gl/core": "^9.4.0",
66
71
  "@deck.gl/layers": "^9.4.0",
67
72
  "@deck.gl/maplibre": "^9.4.0",
68
- "@tellegen/engine": "^0.3.0",
69
- "maplibre-gl": "^6.10.0"
73
+ "@tellegen/engine": "^0.4.0",
74
+ "maplibre-gl": "^6.15.0"
70
75
  },
71
76
  "peerDependencies": {
72
77
  "svelte": ">=5.30 <6"
73
78
  },
74
79
  "devDependencies": {
75
80
  "@sveltejs/package": "^2.3.0",
76
- "@sveltejs/vite-plugin-svelte": "^7.3.0",
77
- "svelte": "^5.57.0",
78
- "svelte-check": "^4.7.6",
81
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
82
+ "svelte": "^5.57.2",
83
+ "svelte-check": "^4.7.7",
79
84
  "typescript": "^6.0.3",
80
- "vite": "^8.2.2",
81
- "vitest": "^5.0.0"
85
+ "vite": "^8.3.1",
86
+ "vitest": "^5.0.2"
82
87
  }
83
88
  }