@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.
- package/README.md +233 -0
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +239 -66
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +36 -4
- package/dist/TellegenShell.svelte +14 -42
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +10 -2
- package/dist/api.js +2 -0
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +55 -1
- package/dist/colors.js +89 -0
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +21 -8
- package/dist/components/BindingLines.svelte +1 -1
- package/dist/components/BusPicker.svelte +3 -3
- package/dist/components/ControlPanel.svelte +4 -2
- package/dist/components/DemandSlider.svelte +16 -7
- package/dist/components/DiagramCanvas.svelte +17 -5
- package/dist/components/DisplayControls.svelte +5 -1
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +6 -3
- package/dist/components/McResults.svelte +98 -35
- package/dist/components/McResults.svelte.d.ts +8 -2
- package/dist/components/ModelDetails.svelte +5 -5
- package/dist/components/MulticonductorDetails.svelte +9 -9
- package/dist/components/NotificationCenter.svelte +4 -2
- package/dist/components/PanelCard.svelte +72 -7
- package/dist/components/PanelHost.svelte +59 -34
- package/dist/components/PanelHost.svelte.d.ts +4 -1
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +1 -1
- package/dist/components/RatingSlider.svelte +14 -5
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +3 -3
- package/dist/components/SensitivityReadout.svelte +2 -2
- 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 +3 -3
- package/dist/components/TopMovers.svelte +1 -1
- package/dist/components.d.ts +3 -0
- package/dist/components.js +3 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +65 -5
- package/dist/controller.svelte.js +266 -78
- package/dist/display.d.ts +3 -2
- package/dist/display.js +22 -4
- package/dist/format.d.ts +9 -0
- package/dist/format.js +13 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -0
- 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/notices.svelte.js +5 -1
- package/dist/panels.svelte.d.ts +6 -0
- package/dist/panels.svelte.js +31 -1
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +25 -3
- package/dist/state.svelte.js +13 -0
- package/dist/styles.css +59 -6
- package/dist/types.d.ts +1 -1
- package/package.json +13 -8
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
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(
|
|
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
|
-
>{
|
|
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>{
|
|
122
|
+
<dd>{summary.converged ? 'Converged' : 'Not converged'}</dd>
|
|
80
123
|
</div>
|
|
81
124
|
<div>
|
|
82
125
|
<dt>Voltage band</dt>
|
|
83
|
-
<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>{
|
|
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>{
|
|
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>{
|
|
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>{
|
|
278
|
+
<dd>{summary.scaled_kcl_residual.toExponential(3)}</dd>
|
|
236
279
|
</div>
|
|
237
280
|
<div>
|
|
238
281
|
<dt>Voltage change</dt>
|
|
239
|
-
<dd>{
|
|
282
|
+
<dd>{summary.voltage_change.toExponential(3)} V</dd>
|
|
240
283
|
</div>
|
|
241
284
|
<div>
|
|
242
285
|
<dt>Matrix size</dt>
|
|
243
|
-
<dd>{
|
|
286
|
+
<dd>{summary.matrix_dimension}</dd>
|
|
244
287
|
</div>
|
|
245
288
|
<div>
|
|
246
289
|
<dt>Nonzero entries</dt>
|
|
247
|
-
<dd>{
|
|
290
|
+
<dd>{summary.matrix_nonzeros}</dd>
|
|
248
291
|
</div>
|
|
249
292
|
<div>
|
|
250
293
|
<dt>Factorizations</dt>
|
|
251
|
-
<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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
415
|
+
font: 12px var(--font-mono);
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
.load-row button {
|
|
419
|
-
font-size:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tick } from 'svelte';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
focusFirst,
|
|
5
|
+
getPanelLayout,
|
|
6
|
+
type PanelRegistration,
|
|
7
|
+
type PanelSide
|
|
8
|
+
} from '../panels.svelte.js';
|
|
4
9
|
let {
|
|
5
10
|
panel,
|
|
6
11
|
floating = false,
|
|
@@ -12,6 +17,7 @@
|
|
|
12
17
|
} = $props();
|
|
13
18
|
const layout = getPanelLayout();
|
|
14
19
|
let menu = $state(false);
|
|
20
|
+
let menuAnchor: HTMLDivElement | undefined = $state();
|
|
15
21
|
let card: HTMLElement;
|
|
16
22
|
const bounds = $derived(layout.bounds(panel));
|
|
17
23
|
const preferred = $derived(!floating && !mobile && layout.preferred(panel));
|
|
@@ -86,11 +92,40 @@
|
|
|
86
92
|
}
|
|
87
93
|
async function close() {
|
|
88
94
|
const title = panel.title;
|
|
95
|
+
const position = layout.position(panel);
|
|
96
|
+
const dock = position === 'floating' ? panel.side : position;
|
|
97
|
+
const opener = layout.opener(panel);
|
|
89
98
|
layout.close(panel);
|
|
90
99
|
await tick();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
// The toolbar button for the panel; a host without the toolbar gets the
|
|
101
|
+
// control that opened it, else the panel's dock (the drawer when narrow).
|
|
102
|
+
focusFirst([
|
|
103
|
+
document.querySelector<HTMLElement>(
|
|
104
|
+
`.panel-toolbar button[aria-label="${CSS.escape(title)}"]`
|
|
105
|
+
),
|
|
106
|
+
opener,
|
|
107
|
+
document.querySelector<HTMLElement>(`[data-panel-dock="${dock}"], [data-panel-drawer]`)
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
// Keyboard users can scroll an overflowing panel only through focus, so the
|
|
111
|
+
// content becomes a labelled, focusable region exactly while it overflows.
|
|
112
|
+
let overflowing = $state(false);
|
|
113
|
+
function trackOverflow(content: HTMLElement) {
|
|
114
|
+
const sync = () => (overflowing = content.scrollHeight > content.clientHeight + 1);
|
|
115
|
+
const resize = new ResizeObserver(sync);
|
|
116
|
+
const observe = () => {
|
|
117
|
+
resize.disconnect();
|
|
118
|
+
resize.observe(content);
|
|
119
|
+
for (const child of content.children) resize.observe(child);
|
|
120
|
+
sync();
|
|
121
|
+
};
|
|
122
|
+
const children = new MutationObserver(observe);
|
|
123
|
+
children.observe(content, { childList: true });
|
|
124
|
+
observe();
|
|
125
|
+
return () => {
|
|
126
|
+
resize.disconnect();
|
|
127
|
+
children.disconnect();
|
|
128
|
+
};
|
|
94
129
|
}
|
|
95
130
|
</script>
|
|
96
131
|
|
|
@@ -98,6 +133,9 @@
|
|
|
98
133
|
onkeydown={(event) => {
|
|
99
134
|
if (event.key === 'Escape') menu = false;
|
|
100
135
|
}}
|
|
136
|
+
onpointerdown={(event) => {
|
|
137
|
+
if (menu && !menuAnchor?.contains(event.target as Node)) menu = false;
|
|
138
|
+
}}
|
|
101
139
|
/>
|
|
102
140
|
<aside
|
|
103
141
|
bind:this={card}
|
|
@@ -127,7 +165,7 @@
|
|
|
127
165
|
>
|
|
128
166
|
{/if}
|
|
129
167
|
<div class="header-actions">{@render panel.headerActions?.()}</div>
|
|
130
|
-
{#if !mobile}<div class="menu-anchor">
|
|
168
|
+
{#if !mobile}<div class="menu-anchor" bind:this={menuAnchor}>
|
|
131
169
|
<button
|
|
132
170
|
class="icon"
|
|
133
171
|
aria-label={`${panel.title} panel options`}
|
|
@@ -150,7 +188,7 @@
|
|
|
150
188
|
</div>{/if}
|
|
151
189
|
</div>{/if}
|
|
152
190
|
<button
|
|
153
|
-
class="icon"
|
|
191
|
+
class="icon close"
|
|
154
192
|
aria-label={`Close ${panel.title.toLowerCase()} panel`}
|
|
155
193
|
title="Close panel"
|
|
156
194
|
onclick={close}
|
|
@@ -160,7 +198,18 @@
|
|
|
160
198
|
>
|
|
161
199
|
</button>
|
|
162
200
|
</header>
|
|
163
|
-
|
|
201
|
+
<!-- A scroll container must take focus for keyboard scrolling, and this one
|
|
202
|
+
takes it only while it overflows. -->
|
|
203
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
204
|
+
<div
|
|
205
|
+
class="panel-content"
|
|
206
|
+
{@attach trackOverflow}
|
|
207
|
+
tabindex={overflowing ? 0 : undefined}
|
|
208
|
+
role={overflowing ? 'region' : undefined}
|
|
209
|
+
aria-label={overflowing ? `${panel.title} panel content` : undefined}
|
|
210
|
+
>
|
|
211
|
+
{@render panel.children()}
|
|
212
|
+
</div>
|
|
164
213
|
{#if floating}<button
|
|
165
214
|
class="resize"
|
|
166
215
|
aria-label={`Resize ${panel.title} panel`}
|
|
@@ -238,6 +287,19 @@
|
|
|
238
287
|
.icon svg {
|
|
239
288
|
fill: currentColor;
|
|
240
289
|
}
|
|
290
|
+
/* Coarse pointers: a 44px hit area around the 26x28 close button. The
|
|
291
|
+
pseudo-element reaches into the header padding and the gap, so nothing
|
|
292
|
+
moves. */
|
|
293
|
+
@media (pointer: coarse) {
|
|
294
|
+
.close {
|
|
295
|
+
position: relative;
|
|
296
|
+
}
|
|
297
|
+
.close::after {
|
|
298
|
+
content: '';
|
|
299
|
+
position: absolute;
|
|
300
|
+
inset: -8px -10px -8px -8px;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
241
303
|
.header-actions {
|
|
242
304
|
display: flex;
|
|
243
305
|
align-items: center;
|
|
@@ -258,6 +320,9 @@
|
|
|
258
320
|
flex-direction: column;
|
|
259
321
|
border-radius: 0 0 5px 5px;
|
|
260
322
|
}
|
|
323
|
+
.panel-content:focus-visible {
|
|
324
|
+
outline-offset: -2px;
|
|
325
|
+
}
|
|
261
326
|
.menu-anchor {
|
|
262
327
|
position: relative;
|
|
263
328
|
}
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
import { getPanelLayout } from '../panels.svelte.js';
|
|
5
5
|
import { getNoticeCenter } from '../notices.svelte.js';
|
|
6
6
|
import PanelCard from './PanelCard.svelte';
|
|
7
|
+
|
|
8
|
+
// The dock insets come from `--tellegen-dock-left/right/bottom` on an
|
|
9
|
+
// ancestor (defaults 20px, 20px, 100px), so a custom shell with its own
|
|
10
|
+
// rail or status bar can move the docks without restyling them.
|
|
11
|
+
let { showToolbar = true }: { showToolbar?: boolean } = $props();
|
|
12
|
+
|
|
7
13
|
const app = getAppState();
|
|
8
14
|
const layout = getPanelLayout();
|
|
9
15
|
const notices = getNoticeCenter();
|
|
@@ -55,43 +61,59 @@
|
|
|
55
61
|
onpointerup={() => layout.finishGesture()}
|
|
56
62
|
onpointercancel={() => layout.finishGesture()}
|
|
57
63
|
/>
|
|
58
|
-
|
|
59
|
-
<div class="panel-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
{#if showToolbar}
|
|
65
|
+
<div class="panel-toolbar" style:top="{toolbarTop}px" aria-label="Panels">
|
|
66
|
+
<div class="panel-buttons">
|
|
67
|
+
{#each layout.ordered().filter((p) => p.side === 'left') as panel (panel.id)}
|
|
68
|
+
<button
|
|
69
|
+
aria-label={panel.title}
|
|
70
|
+
aria-pressed={layout.compact ? layout.drawer === panel.id : panel.open}
|
|
71
|
+
onclick={() => layout.toggle(panel)}>{panel.title}</button
|
|
72
|
+
>
|
|
73
|
+
{/each}
|
|
74
|
+
</div>
|
|
75
|
+
<div class="panel-buttons right">
|
|
76
|
+
{#each layout.ordered().filter((p) => p.side === 'right') as panel (panel.id)}
|
|
77
|
+
<button
|
|
78
|
+
aria-label={panel.title}
|
|
79
|
+
aria-pressed={layout.compact ? layout.drawer === panel.id : panel.open}
|
|
80
|
+
onclick={() => layout.toggle(panel)}>{panel.title}</button
|
|
81
|
+
>
|
|
82
|
+
{/each}
|
|
70
83
|
<button
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
class="reset-layout"
|
|
85
|
+
onclick={() => layout.reset()}
|
|
86
|
+
title="Restore the default panel positions">Reset layout</button
|
|
74
87
|
>
|
|
75
|
-
|
|
76
|
-
<button
|
|
77
|
-
class="reset-layout"
|
|
78
|
-
onclick={() => layout.reset()}
|
|
79
|
-
title="Restore the default panel positions">Reset layout</button
|
|
80
|
-
>
|
|
88
|
+
</div>
|
|
81
89
|
</div>
|
|
82
|
-
|
|
90
|
+
{/if}
|
|
83
91
|
{#if layout.storageError}<p class="layout-error" role="status" style:top="{panelTop}px">
|
|
84
92
|
{layout.storageError}
|
|
85
93
|
</p>{/if}
|
|
94
|
+
<!-- The docks and the drawer take focus (tabindex -1) when a closed panel has
|
|
95
|
+
no toolbar button or opener to hand it back to. The drawer stays rendered,
|
|
96
|
+
at zero height, while no panel is open in it. -->
|
|
86
97
|
{#if layout.compact}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
<div
|
|
99
|
+
class="drawer"
|
|
100
|
+
data-panel-drawer
|
|
101
|
+
role="region"
|
|
102
|
+
aria-label="Panels"
|
|
103
|
+
tabindex="-1"
|
|
104
|
+
style:bottom="{bottomReserve}px"
|
|
105
|
+
style:height="{drawer ? drawerHeight : 0}px"
|
|
106
|
+
>
|
|
107
|
+
{#if drawer}<PanelCard panel={drawer} mobile />{/if}
|
|
108
|
+
</div>
|
|
90
109
|
{:else}
|
|
91
110
|
<div
|
|
92
111
|
class="dock left"
|
|
93
|
-
style:bottom="
|
|
112
|
+
style:bottom="max(var(--tellegen-dock-bottom, 100px), {bottomReserve}px)"
|
|
94
113
|
data-panel-dock="left"
|
|
114
|
+
role="region"
|
|
115
|
+
aria-label="Left panels"
|
|
116
|
+
tabindex="-1"
|
|
95
117
|
style:top="{panelTop}px"
|
|
96
118
|
style:width="{layout.dockWidth('left')}px"
|
|
97
119
|
>
|
|
@@ -99,8 +121,11 @@
|
|
|
99
121
|
</div>
|
|
100
122
|
<div
|
|
101
123
|
class="dock right"
|
|
102
|
-
style:bottom="
|
|
124
|
+
style:bottom="max(var(--tellegen-dock-bottom, 100px), {bottomReserve}px)"
|
|
103
125
|
data-panel-dock="right"
|
|
126
|
+
role="region"
|
|
127
|
+
aria-label="Right panels"
|
|
128
|
+
tabindex="-1"
|
|
104
129
|
style:top="{panelTop}px"
|
|
105
130
|
style:width="{layout.dockWidth('right')}px"
|
|
106
131
|
>
|
|
@@ -113,8 +138,8 @@
|
|
|
113
138
|
.panel-toolbar {
|
|
114
139
|
position: fixed;
|
|
115
140
|
z-index: 30;
|
|
116
|
-
left: 20px;
|
|
117
|
-
right: 20px;
|
|
141
|
+
left: var(--tellegen-dock-left, 20px);
|
|
142
|
+
right: var(--tellegen-dock-right, 20px);
|
|
118
143
|
display: flex;
|
|
119
144
|
justify-content: space-between;
|
|
120
145
|
align-items: center;
|
|
@@ -159,7 +184,7 @@
|
|
|
159
184
|
.dock {
|
|
160
185
|
position: fixed;
|
|
161
186
|
z-index: 25;
|
|
162
|
-
bottom: 100px;
|
|
187
|
+
bottom: var(--tellegen-dock-bottom, 100px);
|
|
163
188
|
display: flex;
|
|
164
189
|
flex-direction: column;
|
|
165
190
|
align-items: stretch;
|
|
@@ -167,10 +192,10 @@
|
|
|
167
192
|
pointer-events: none;
|
|
168
193
|
}
|
|
169
194
|
.dock.left {
|
|
170
|
-
left: 20px;
|
|
195
|
+
left: var(--tellegen-dock-left, 20px);
|
|
171
196
|
}
|
|
172
197
|
.dock.right {
|
|
173
|
-
right: 20px;
|
|
198
|
+
right: var(--tellegen-dock-right, 20px);
|
|
174
199
|
}
|
|
175
200
|
.drawer {
|
|
176
201
|
position: fixed;
|
|
@@ -194,8 +219,8 @@
|
|
|
194
219
|
}
|
|
195
220
|
@media (max-width: 960px) {
|
|
196
221
|
.panel-toolbar {
|
|
197
|
-
left: 12px;
|
|
198
|
-
right: 12px;
|
|
222
|
+
left: var(--tellegen-dock-left, 12px);
|
|
223
|
+
right: var(--tellegen-dock-right, 12px);
|
|
199
224
|
gap: 6px;
|
|
200
225
|
}
|
|
201
226
|
.panel-buttons {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
showToolbar?: boolean;
|
|
3
|
+
};
|
|
4
|
+
declare const PanelHost: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
5
|
type PanelHost = ReturnType<typeof PanelHost>;
|
|
3
6
|
export default PanelHost;
|