@tellegen/svelte 0.2.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.
- package/README.md +269 -5
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +518 -202
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +40 -4
- package/dist/TellegenShell.svelte +24 -46
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +18 -3
- package/dist/api.js +8 -2
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +25 -7
- package/dist/colors.js +39 -13
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +44 -19
- package/dist/components/BindingLines.svelte +4 -7
- package/dist/components/BusPicker.svelte +11 -5
- package/dist/components/ControlPanel.svelte +106 -343
- package/dist/components/DemandSlider.svelte +36 -29
- package/dist/components/DiagramCanvas.svelte +486 -0
- package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
- package/dist/components/DisplayControls.svelte +8 -2
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +9 -4
- package/dist/components/LocalCaseDetails.svelte +61 -48
- package/dist/components/McResults.svelte +428 -0
- package/dist/components/McResults.svelte.d.ts +17 -0
- package/dist/components/ModelDetails.svelte +230 -0
- package/dist/components/ModelDetails.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +232 -83
- package/dist/components/NetworkStats.svelte +20 -18
- package/dist/components/NotificationCenter.svelte +245 -0
- package/dist/components/NotificationCenter.svelte.d.ts +3 -0
- package/dist/components/PanelCard.svelte +384 -0
- package/dist/components/PanelCard.svelte.d.ts +9 -0
- package/dist/components/PanelFrame.svelte +46 -0
- package/dist/components/PanelFrame.svelte.d.ts +16 -0
- package/dist/components/PanelHost.svelte +237 -0
- package/dist/components/PanelHost.svelte.d.ts +6 -0
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +99 -0
- package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
- package/dist/components/RatingSlider.svelte +19 -14
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +214 -0
- package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
- package/dist/components/SensitivityReadout.svelte +26 -18
- package/dist/components/ShellEnvironment.svelte +51 -0
- package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
- package/dist/components/SizeLegend.svelte +2 -2
- package/dist/components/SolveCard.svelte +48 -51
- package/dist/components/TopMovers.svelte +9 -4
- package/dist/components/TopMovers.svelte.d.ts +2 -17
- package/dist/components.d.ts +5 -0
- package/dist/components.js +5 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +117 -36
- package/dist/controller.svelte.js +1133 -253
- package/dist/diagram-labels.d.ts +9 -0
- package/dist/diagram-labels.js +40 -0
- package/dist/display.d.ts +3 -7
- package/dist/display.js +31 -27
- package/dist/format.d.ts +9 -0
- package/dist/format.js +17 -4
- package/dist/index.d.ts +15 -5
- package/dist/index.js +9 -2
- package/dist/map.d.ts +4 -2
- package/dist/map.js +2 -1
- package/dist/maplibre-worker.d.ts +5 -0
- package/dist/maplibre-worker.dev.d.ts +11 -0
- package/dist/maplibre-worker.dev.js +15 -0
- package/dist/maplibre-worker.js +8 -0
- package/dist/multiconductor.d.ts +11 -2
- package/dist/multiconductor.js +107 -14
- package/dist/notices.svelte.d.ts +29 -0
- package/dist/notices.svelte.js +86 -0
- package/dist/panels.svelte.d.ts +71 -0
- package/dist/panels.svelte.js +245 -0
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +107 -23
- package/dist/state.svelte.js +106 -20
- package/dist/styles.css +59 -6
- package/dist/synthetic-layout.js +1 -0
- package/package.json +29 -11
- package/dist/study-package.d.ts +0 -8
- package/dist/study-package.js +0 -17
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const KEY = Symbol('tellegen.notices');
|
|
3
|
+
const LIFETIME_MS = 6500;
|
|
4
|
+
const MAX_NOTICES = 30;
|
|
5
|
+
/** Recent messages stay in memory; dismissal hides the temporary notice only. */
|
|
6
|
+
export class NoticeCenter {
|
|
7
|
+
entries = $state.raw([]);
|
|
8
|
+
activeId = $state(null);
|
|
9
|
+
expanded = $state(false);
|
|
10
|
+
announcement = $state('');
|
|
11
|
+
#nextId = 0;
|
|
12
|
+
#timer;
|
|
13
|
+
#deadline = 0;
|
|
14
|
+
#remaining = LIFETIME_MS;
|
|
15
|
+
#paused = false;
|
|
16
|
+
get active() {
|
|
17
|
+
return this.entries.find((entry) => entry.id === this.activeId) ?? null;
|
|
18
|
+
}
|
|
19
|
+
push(input) {
|
|
20
|
+
if (!input.details)
|
|
21
|
+
return;
|
|
22
|
+
const previous = this.entries.find((entry) => entry.details === input.details && entry.kind === input.kind);
|
|
23
|
+
const entry = {
|
|
24
|
+
...input,
|
|
25
|
+
id: previous?.id ?? ++this.#nextId,
|
|
26
|
+
count: (previous?.count ?? 0) + 1,
|
|
27
|
+
updatedAt: Date.now()
|
|
28
|
+
};
|
|
29
|
+
this.entries = [entry, ...this.entries.filter((item) => item.id !== entry.id)].slice(0, MAX_NOTICES);
|
|
30
|
+
this.activeId = entry.id;
|
|
31
|
+
this.announcement = `${entry.title}${entry.count > 1 ? `, repeated ${entry.count} times` : ''}`;
|
|
32
|
+
this.#remaining = LIFETIME_MS;
|
|
33
|
+
this.#schedule();
|
|
34
|
+
}
|
|
35
|
+
#schedule() {
|
|
36
|
+
clearTimeout(this.#timer);
|
|
37
|
+
if (this.#paused || this.activeId === null)
|
|
38
|
+
return;
|
|
39
|
+
this.#deadline = Date.now() + this.#remaining;
|
|
40
|
+
this.#timer = setTimeout(() => this.dismiss(), this.#remaining);
|
|
41
|
+
}
|
|
42
|
+
pause(value) {
|
|
43
|
+
if (value === this.#paused)
|
|
44
|
+
return;
|
|
45
|
+
if (value)
|
|
46
|
+
this.#remaining = Math.max(0, this.#deadline - Date.now());
|
|
47
|
+
this.#paused = value;
|
|
48
|
+
this.#schedule();
|
|
49
|
+
}
|
|
50
|
+
dismiss() {
|
|
51
|
+
clearTimeout(this.#timer);
|
|
52
|
+
this.activeId = null;
|
|
53
|
+
}
|
|
54
|
+
clear() {
|
|
55
|
+
this.dismiss();
|
|
56
|
+
this.entries = [];
|
|
57
|
+
this.expanded = false;
|
|
58
|
+
}
|
|
59
|
+
dispose() {
|
|
60
|
+
clearTimeout(this.#timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export const setNoticeCenter = (value) => setContext(KEY, value);
|
|
64
|
+
export function getNoticeCenter() {
|
|
65
|
+
const value = getContext(KEY);
|
|
66
|
+
if (!value)
|
|
67
|
+
throw new Error('NoticeCenter requires a Tellegen provider');
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
export function errorNoticeTitle(details) {
|
|
71
|
+
if (/rate limit/i.test(details))
|
|
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';
|
|
77
|
+
if (/storage|quota|space|indexeddb/i.test(details))
|
|
78
|
+
return 'Could not save locally';
|
|
79
|
+
if (/sensitivity|derivative|singular/i.test(details))
|
|
80
|
+
return 'Sensitivity unavailable';
|
|
81
|
+
if (/infeasib|converg|solver|solving|calculation|compute|webassembly|power flow/i.test(details))
|
|
82
|
+
return 'Calculation did not complete';
|
|
83
|
+
if (/parse|read|file|format/i.test(details))
|
|
84
|
+
return 'Could not read this file';
|
|
85
|
+
return 'Operation did not complete';
|
|
86
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
export type PanelSide = 'left' | 'right';
|
|
3
|
+
export type PanelPosition = PanelSide | 'floating';
|
|
4
|
+
export interface PanelBounds {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PanelRegistration {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
side: PanelSide;
|
|
14
|
+
order: number;
|
|
15
|
+
width: number;
|
|
16
|
+
open: boolean;
|
|
17
|
+
setOpen: (open: boolean) => void;
|
|
18
|
+
children: Snippet;
|
|
19
|
+
headerActions?: Snippet;
|
|
20
|
+
onopen?: () => void;
|
|
21
|
+
}
|
|
22
|
+
interface SavedPanel extends PanelBounds {
|
|
23
|
+
position: PanelPosition;
|
|
24
|
+
}
|
|
25
|
+
export declare const PANEL_BREAKPOINT = 960;
|
|
26
|
+
export declare const PANEL_MIN_DOCK_HEIGHT = 500;
|
|
27
|
+
export declare const PANEL_COMPACT_QUERY = "(max-width: 960px), (max-height: 499px)";
|
|
28
|
+
/** Layout belongs to one provider and stores presentation preferences only. */
|
|
29
|
+
export declare class PanelLayout {
|
|
30
|
+
#private;
|
|
31
|
+
panels: PanelRegistration[];
|
|
32
|
+
placements: Record<string, SavedPanel>;
|
|
33
|
+
compact: boolean;
|
|
34
|
+
drawer: string | null;
|
|
35
|
+
lastActive: string;
|
|
36
|
+
dockActive: Record<PanelSide, string>;
|
|
37
|
+
viewportWidth: number;
|
|
38
|
+
viewportHeight: number;
|
|
39
|
+
top: number;
|
|
40
|
+
bottom: number;
|
|
41
|
+
storageError: string | null;
|
|
42
|
+
beginGesture(panel: PanelRegistration, kind: 'move' | 'resize', x: number, y: number, bounds: PanelBounds): void;
|
|
43
|
+
moveGesture(x: number, y: number): void;
|
|
44
|
+
finishGesture(): void;
|
|
45
|
+
register(panel: PanelRegistration): () => void;
|
|
46
|
+
update(id: string, open: boolean, title: string, width: number): void;
|
|
47
|
+
activate(panel: PanelRegistration): void;
|
|
48
|
+
preferred(panel: PanelRegistration): boolean;
|
|
49
|
+
position(panel: PanelRegistration): PanelPosition;
|
|
50
|
+
ordered(side?: PanelSide): PanelRegistration[];
|
|
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;
|
|
55
|
+
close(panel: PanelRegistration): void;
|
|
56
|
+
dock(panel: PanelRegistration, side?: PanelSide): void;
|
|
57
|
+
float(panel: PanelRegistration, bounds?: PanelBounds): void;
|
|
58
|
+
bounds(panel: PanelRegistration): PanelBounds;
|
|
59
|
+
clamp(bounds: PanelBounds): PanelBounds;
|
|
60
|
+
resizeViewport(width: number, height: number, top: number): void;
|
|
61
|
+
dockWidth(side: PanelSide): number;
|
|
62
|
+
reset(): void;
|
|
63
|
+
initialize(storage?: Storage): void;
|
|
64
|
+
save(): void;
|
|
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;
|
|
69
|
+
export declare const setPanelLayout: (layout: PanelLayout) => PanelLayout;
|
|
70
|
+
export declare const getPanelLayout: () => PanelLayout;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const KEY = Symbol('tellegen.panel-layout');
|
|
3
|
+
const STORAGE_KEY = 'tellegen.panel-layout.v1';
|
|
4
|
+
export const PANEL_BREAKPOINT = 960;
|
|
5
|
+
export const PANEL_MIN_DOCK_HEIGHT = 500;
|
|
6
|
+
export const PANEL_COMPACT_QUERY = `(max-width: ${PANEL_BREAKPOINT}px), (max-height: ${PANEL_MIN_DOCK_HEIGHT - 1}px)`;
|
|
7
|
+
/** Layout belongs to one provider and stores presentation preferences only. */
|
|
8
|
+
export class PanelLayout {
|
|
9
|
+
panels = $state.raw([]);
|
|
10
|
+
placements = $state({});
|
|
11
|
+
compact = $state(false);
|
|
12
|
+
drawer = $state('network');
|
|
13
|
+
lastActive = $state('network');
|
|
14
|
+
dockActive = $state({ left: 'network', right: 'solver' });
|
|
15
|
+
viewportWidth = $state(1280);
|
|
16
|
+
viewportHeight = $state(800);
|
|
17
|
+
top = $state(100);
|
|
18
|
+
bottom = $state(88);
|
|
19
|
+
storageError = $state(null);
|
|
20
|
+
#storage;
|
|
21
|
+
#openers = new Map();
|
|
22
|
+
#gesture = null;
|
|
23
|
+
beginGesture(panel, kind, x, y, bounds) {
|
|
24
|
+
this.#gesture = { panel, kind, x, y, bounds, moved: false };
|
|
25
|
+
}
|
|
26
|
+
moveGesture(x, y) {
|
|
27
|
+
const gesture = this.#gesture;
|
|
28
|
+
if (!gesture)
|
|
29
|
+
return;
|
|
30
|
+
const dx = x - gesture.x, dy = y - gesture.y;
|
|
31
|
+
if (!gesture.moved && Math.hypot(dx, dy) < 4)
|
|
32
|
+
return;
|
|
33
|
+
gesture.moved = true;
|
|
34
|
+
this.float(gesture.panel, gesture.kind === 'move'
|
|
35
|
+
? {
|
|
36
|
+
...gesture.bounds,
|
|
37
|
+
x: gesture.bounds.x + dx,
|
|
38
|
+
y: gesture.bounds.y + dy
|
|
39
|
+
}
|
|
40
|
+
: {
|
|
41
|
+
...gesture.bounds,
|
|
42
|
+
width: gesture.bounds.width + dx,
|
|
43
|
+
height: gesture.bounds.height + dy
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
finishGesture() {
|
|
47
|
+
if (this.#gesture?.moved)
|
|
48
|
+
this.save();
|
|
49
|
+
this.#gesture = null;
|
|
50
|
+
}
|
|
51
|
+
register(panel) {
|
|
52
|
+
this.panels = [...this.panels.filter((p) => p.id !== panel.id), panel];
|
|
53
|
+
return () => {
|
|
54
|
+
this.panels = this.panels.filter((p) => p.id !== panel.id);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
update(id, open, title, width) {
|
|
58
|
+
const previous = this.panels.find((p) => p.id === id);
|
|
59
|
+
if (!previous ||
|
|
60
|
+
(previous.open === open && previous.title === title && previous.width === width))
|
|
61
|
+
return;
|
|
62
|
+
this.panels = this.panels.map((p) => (p.id === id ? { ...p, open, title, width } : p));
|
|
63
|
+
if (open && !previous.open) {
|
|
64
|
+
this.#rememberOpener(id);
|
|
65
|
+
this.activate(previous);
|
|
66
|
+
if (this.compact)
|
|
67
|
+
this.drawer = id;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
activate(panel) {
|
|
71
|
+
this.lastActive = panel.id;
|
|
72
|
+
const side = this.position(panel);
|
|
73
|
+
if (side !== 'floating')
|
|
74
|
+
this.dockActive[side] = panel.id;
|
|
75
|
+
}
|
|
76
|
+
preferred(panel) {
|
|
77
|
+
const side = this.position(panel);
|
|
78
|
+
if (side === 'floating')
|
|
79
|
+
return false;
|
|
80
|
+
const open = this.ordered(side).filter((candidate) => candidate.open);
|
|
81
|
+
const selected = open.find((candidate) => candidate.id === this.dockActive[side]) ?? open[0];
|
|
82
|
+
return selected?.id === panel.id;
|
|
83
|
+
}
|
|
84
|
+
position(panel) {
|
|
85
|
+
return this.placements[panel.id]?.position ?? panel.side;
|
|
86
|
+
}
|
|
87
|
+
ordered(side) {
|
|
88
|
+
return this.panels
|
|
89
|
+
.filter((p) => side === undefined || this.position(p) === side)
|
|
90
|
+
.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
|
|
91
|
+
}
|
|
92
|
+
toggle(panel) {
|
|
93
|
+
if (this.compact) {
|
|
94
|
+
this.drawer = this.drawer === panel.id ? null : panel.id;
|
|
95
|
+
if (this.drawer === panel.id) {
|
|
96
|
+
this.#rememberOpener(panel.id);
|
|
97
|
+
this.activate(panel);
|
|
98
|
+
panel.setOpen(true);
|
|
99
|
+
panel.onopen?.();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
if (!panel.open) {
|
|
104
|
+
this.#rememberOpener(panel.id);
|
|
105
|
+
this.activate(panel);
|
|
106
|
+
}
|
|
107
|
+
panel.setOpen(!panel.open);
|
|
108
|
+
if (!panel.open)
|
|
109
|
+
panel.onopen?.();
|
|
110
|
+
}
|
|
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
|
+
}
|
|
125
|
+
close(panel) {
|
|
126
|
+
panel.setOpen(false);
|
|
127
|
+
if (this.drawer === panel.id)
|
|
128
|
+
this.drawer = null;
|
|
129
|
+
}
|
|
130
|
+
dock(panel, side = panel.side) {
|
|
131
|
+
this.placements[panel.id] = { ...this.bounds(panel), position: side };
|
|
132
|
+
this.activate(panel);
|
|
133
|
+
this.save();
|
|
134
|
+
}
|
|
135
|
+
float(panel, bounds) {
|
|
136
|
+
this.placements[panel.id] = {
|
|
137
|
+
...this.clamp(bounds ?? this.bounds(panel)),
|
|
138
|
+
position: 'floating'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
bounds(panel) {
|
|
142
|
+
return this.clamp(this.placements[panel.id] ?? {
|
|
143
|
+
x: panel.side === 'left' ? 24 : this.viewportWidth - panel.width - 24,
|
|
144
|
+
y: this.top + 16,
|
|
145
|
+
width: panel.width,
|
|
146
|
+
height: Math.min(520, this.viewportHeight - this.top - this.bottom - 22)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
clamp(bounds) {
|
|
150
|
+
const availableWidth = Math.max(1, this.viewportWidth - 32);
|
|
151
|
+
const availableHeight = Math.max(1, this.viewportHeight - this.top - this.bottom - 12);
|
|
152
|
+
const width = Math.min(availableWidth, Math.max(240, bounds.width));
|
|
153
|
+
const height = Math.min(availableHeight, Math.max(120, bounds.height));
|
|
154
|
+
return {
|
|
155
|
+
width,
|
|
156
|
+
height,
|
|
157
|
+
x: Math.max(16, Math.min(bounds.x, this.viewportWidth - width - 16)),
|
|
158
|
+
y: Math.max(this.top, Math.min(bounds.y, this.viewportHeight - height - this.bottom))
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
resizeViewport(width, height, top) {
|
|
162
|
+
this.viewportWidth = width;
|
|
163
|
+
this.viewportHeight = height;
|
|
164
|
+
this.top = top;
|
|
165
|
+
const wasCompact = this.compact;
|
|
166
|
+
this.compact = width <= PANEL_BREAKPOINT || height < PANEL_MIN_DOCK_HEIGHT;
|
|
167
|
+
if (this.compact && !wasCompact && this.panels.length > 0) {
|
|
168
|
+
const latest = this.panels.find((panel) => panel.id === this.lastActive && panel.open);
|
|
169
|
+
this.drawer = latest?.id ?? this.panels.find((panel) => panel.open)?.id ?? null;
|
|
170
|
+
}
|
|
171
|
+
for (const [id, panel] of Object.entries(this.placements)) {
|
|
172
|
+
if (panel.position === 'floating')
|
|
173
|
+
this.placements[id] = { ...this.clamp(panel), position: 'floating' };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
dockWidth(side) {
|
|
177
|
+
const panels = this.ordered(side).filter((p) => p.open);
|
|
178
|
+
return panels.length
|
|
179
|
+
? Math.min(Math.max(...panels.map((p) => p.width)), this.viewportWidth * 0.29)
|
|
180
|
+
: 0;
|
|
181
|
+
}
|
|
182
|
+
reset() {
|
|
183
|
+
this.placements = {};
|
|
184
|
+
this.save();
|
|
185
|
+
}
|
|
186
|
+
initialize(storage) {
|
|
187
|
+
this.#storage = storage;
|
|
188
|
+
if (!storage)
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
const text = storage.getItem(STORAGE_KEY);
|
|
192
|
+
if (!text)
|
|
193
|
+
return;
|
|
194
|
+
const value = JSON.parse(text);
|
|
195
|
+
if (value.version !== 1 || !value.panels || typeof value.panels !== 'object')
|
|
196
|
+
return;
|
|
197
|
+
for (const [id, panel] of Object.entries(value.panels)) {
|
|
198
|
+
if (!panel ||
|
|
199
|
+
!['left', 'right', 'floating'].includes(panel.position) ||
|
|
200
|
+
!['x', 'y', 'width', 'height'].every((key) => Number.isFinite(panel[key])))
|
|
201
|
+
continue;
|
|
202
|
+
this.placements[id] = {
|
|
203
|
+
...this.clamp(panel),
|
|
204
|
+
position: panel.position
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
this.storageError = 'Panel layout could not be restored. Reset layout to use the defaults.';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
save() {
|
|
213
|
+
if (!this.#storage)
|
|
214
|
+
return;
|
|
215
|
+
try {
|
|
216
|
+
this.#storage.setItem(STORAGE_KEY, JSON.stringify({
|
|
217
|
+
version: 1,
|
|
218
|
+
panels: this.placements
|
|
219
|
+
}));
|
|
220
|
+
this.storageError = null;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
this.storageError = 'Panel layout could not be saved. Changes last for this visit.';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
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
|
+
}
|
|
239
|
+
export const setPanelLayout = (layout) => setContext(KEY, layout);
|
|
240
|
+
export const getPanelLayout = () => {
|
|
241
|
+
const layout = getContext(KEY);
|
|
242
|
+
if (!layout)
|
|
243
|
+
throw new Error('PanelFrame requires a TellegenProvider');
|
|
244
|
+
return layout;
|
|
245
|
+
};
|
package/dist/slider.d.ts
ADDED
|
@@ -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
|
+
}
|