@tellegen/svelte 0.1.0 → 0.1.2
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/dist/TellegenMap.svelte +155 -19
- package/dist/TellegenMap.svelte.d.ts +1 -0
- package/dist/TellegenMap.svelte.d.ts.map +1 -1
- package/dist/TellegenShell.svelte +29 -3
- package/dist/TellegenShell.svelte.d.ts.map +1 -1
- package/dist/components/AppFooter.svelte +26 -1
- package/dist/components/AppFooter.svelte.d.ts +5 -17
- package/dist/components/AppFooter.svelte.d.ts.map +1 -1
- package/dist/components/AppHeader.svelte +59 -1
- package/dist/components/AppHeader.svelte.d.ts.map +1 -1
- package/dist/components/BindingLines.svelte +13 -0
- package/dist/components/BusPicker.svelte +40 -0
- package/dist/components/BusPicker.svelte.d.ts +4 -1
- package/dist/components/BusPicker.svelte.d.ts.map +1 -1
- package/dist/components/ControlPanel.svelte +359 -72
- package/dist/components/ControlPanel.svelte.d.ts +2 -17
- package/dist/components/ControlPanel.svelte.d.ts.map +1 -1
- package/dist/components/FormulationSelector.svelte +17 -0
- package/dist/components/MulticonductorDetails.svelte +58 -5
- package/dist/components/MulticonductorDetails.svelte.d.ts.map +1 -1
- package/dist/components/SensitivityReadout.svelte +10 -1
- package/dist/components/SolveCard.svelte +2 -1
- package/dist/controller.svelte.d.ts +3 -0
- package/dist/controller.svelte.d.ts.map +1 -1
- package/dist/controller.svelte.js +22 -4
- package/dist/multiconductor.d.ts +13 -0
- package/dist/multiconductor.d.ts.map +1 -1
- package/dist/multiconductor.js +39 -9
- package/dist/state.svelte.d.ts +18 -0
- package/dist/state.svelte.d.ts.map +1 -1
- package/dist/state.svelte.js +22 -0
- package/dist/styles.css +16 -2
- package/dist/synthetic-layout.d.ts +3 -1
- package/dist/synthetic-layout.d.ts.map +1 -1
- package/dist/synthetic-layout.js +130 -11
- package/dist/transformer-icon.d.ts +16 -0
- package/dist/transformer-icon.d.ts.map +1 -0
- package/dist/transformer-icon.js +35 -0
- package/package.json +1 -1
package/dist/TellegenMap.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
3
3
|
import { tick } from 'svelte';
|
|
4
4
|
import type { Layer, PickingInfo } from '@deck.gl/core';
|
|
5
|
-
import type { PathLayer, ScatterplotLayer } from '@deck.gl/layers';
|
|
5
|
+
import type { IconLayer, PathLayer, ScatterplotLayer } from '@deck.gl/layers';
|
|
6
6
|
import type { MapboxOverlay } from '@deck.gl/mapbox';
|
|
7
7
|
import type { LngLatBoundsLike, Map as MapLibreMap } from 'maplibre-gl';
|
|
8
8
|
import type { NetworkBranch, NetworkBus } from './api.js';
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
edgeWidth,
|
|
27
27
|
isPhaseTerminal,
|
|
28
28
|
phaseColor,
|
|
29
|
+
transformerMarks,
|
|
29
30
|
type PlacedMultiBus,
|
|
30
|
-
type PlacedMultiEdge
|
|
31
|
+
type PlacedMultiEdge,
|
|
32
|
+
type TransformerMark
|
|
31
33
|
} from './multiconductor.js';
|
|
34
|
+
import { transformerIcon } from './transformer-icon.js';
|
|
32
35
|
import { getAppState, getController } from './context.svelte.js';
|
|
33
36
|
import { displayFmt, fmt, rgbaCss } from './format.js';
|
|
34
37
|
import type { RGBA } from './colors.js';
|
|
@@ -39,6 +42,7 @@
|
|
|
39
42
|
onbranchclick,
|
|
40
43
|
onlocalbranchclick,
|
|
41
44
|
onmultibusclick,
|
|
45
|
+
onmultiedgeclick,
|
|
42
46
|
onplacecase,
|
|
43
47
|
onmapclick
|
|
44
48
|
}: {
|
|
@@ -47,6 +51,7 @@
|
|
|
47
51
|
onbranchclick?: (caseId: string, branchId: number) => void;
|
|
48
52
|
onlocalbranchclick?: (caseId: string, branchId: number) => void;
|
|
49
53
|
onmultibusclick: (caseId: string, busId: string) => void;
|
|
54
|
+
onmultiedgeclick?: (caseId: string, edgeId: string) => void;
|
|
50
55
|
onplacecase: (lon: number, lat: number) => void;
|
|
51
56
|
onmapclick: () => void;
|
|
52
57
|
} = $props();
|
|
@@ -70,6 +75,7 @@
|
|
|
70
75
|
type LayerCtors = {
|
|
71
76
|
PathLayer: typeof PathLayer;
|
|
72
77
|
ScatterplotLayer: typeof ScatterplotLayer;
|
|
78
|
+
IconLayer: typeof IconLayer;
|
|
73
79
|
};
|
|
74
80
|
type CursorState = {
|
|
75
81
|
isDragging: boolean;
|
|
@@ -489,11 +495,10 @@
|
|
|
489
495
|
* The branch variant also clears the rating readout on the panel's lower edge. */
|
|
490
496
|
function framePadding(width: number, height: number, kind: 'case' | 'branch') {
|
|
491
497
|
if (width <= 760) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return { top: 130, left: 24, right: 24, bottom };
|
|
498
|
+
// bottom follows the sheet; capped so a fully open sheet still leaves a band to fit into
|
|
499
|
+
const clearance = kind === 'branch' ? 16 : 28;
|
|
500
|
+
const bottom = Math.min(app.sheetInset + clearance, Math.round(height * 0.55));
|
|
501
|
+
return { top: app.headerInset + 8, left: 24, right: 24, bottom };
|
|
497
502
|
}
|
|
498
503
|
return kind === 'branch'
|
|
499
504
|
? { top: 106, left: 388, right: 76, bottom: 80 }
|
|
@@ -546,6 +551,25 @@
|
|
|
546
551
|
);
|
|
547
552
|
}
|
|
548
553
|
|
|
554
|
+
function coarsePointer(): boolean {
|
|
555
|
+
return typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Compact chrome anchoring. maplibre's bottom-right controls sit over the map,
|
|
559
|
+
// so the sheet would bury them; they ride above it instead. Under
|
|
560
|
+
// CHROME_MIN_BAND_PX of map left they drop out rather than climb into the
|
|
561
|
+
// header — at that point the sheet covers what they annotate.
|
|
562
|
+
const CHROME_MIN_BAND_PX = 132;
|
|
563
|
+
|
|
564
|
+
/** How far the sheet can push the chrome up before it would reach the header. */
|
|
565
|
+
const chromeLiftCap = $derived(
|
|
566
|
+
Math.max(0, app.viewportHeight - app.headerInset - CHROME_MIN_BAND_PX)
|
|
567
|
+
);
|
|
568
|
+
const chromeInset = $derived(
|
|
569
|
+
app.compactLayout ? Math.min(app.sheetInset, chromeLiftCap) : 0
|
|
570
|
+
);
|
|
571
|
+
const chromeVisible = $derived(!app.compactLayout || app.sheetInset <= chromeLiftCap);
|
|
572
|
+
|
|
549
573
|
/** Fly to the selected branch; true when a camera move started. */
|
|
550
574
|
function focusSelectedBranch(): boolean {
|
|
551
575
|
if (!map || !selectedBranchCue) return false;
|
|
@@ -679,7 +703,8 @@
|
|
|
679
703
|
maplibregl: maplibre.default,
|
|
680
704
|
MapboxOverlay: mapbox.MapboxOverlay,
|
|
681
705
|
PathLayer: layers.PathLayer,
|
|
682
|
-
ScatterplotLayer: layers.ScatterplotLayer
|
|
706
|
+
ScatterplotLayer: layers.ScatterplotLayer,
|
|
707
|
+
IconLayer: layers.IconLayer
|
|
683
708
|
};
|
|
684
709
|
}
|
|
685
710
|
|
|
@@ -692,6 +717,7 @@
|
|
|
692
717
|
// deck layers also inherit maplibre's 4096 canvas clamp. The flat positron
|
|
693
718
|
// basemap writes no depth, so the overlay always draws on top.
|
|
694
719
|
function buildOverlay(MapboxOverlay: MapModules['MapboxOverlay']): MapboxOverlay {
|
|
720
|
+
const touch = coarsePointer();
|
|
695
721
|
return new MapboxOverlay({
|
|
696
722
|
interleaved: true,
|
|
697
723
|
layers: [],
|
|
@@ -699,10 +725,13 @@
|
|
|
699
725
|
depthWriteEnabled: false,
|
|
700
726
|
depthCompare: 'always'
|
|
701
727
|
},
|
|
728
|
+
// a small bus draws ~9px across, well under a fingertip
|
|
729
|
+
pickingRadius: touch ? 12 : 0,
|
|
702
730
|
onClick: (info: PickingInfo) => {
|
|
703
731
|
if (!app.placingId && !isSelectableLayer(info.layer?.id)) onmapclick();
|
|
704
732
|
},
|
|
705
|
-
|
|
733
|
+
// a touch tooltip opens under the finger and stays until the next tap
|
|
734
|
+
getTooltip: touch ? undefined : tooltip,
|
|
706
735
|
getCursor: ({ isHovering, isDragging }: CursorState) =>
|
|
707
736
|
app.placingId ? 'crosshair' : isDragging ? 'grabbing' : isHovering ? 'pointer' : 'grab'
|
|
708
737
|
});
|
|
@@ -712,9 +741,9 @@
|
|
|
712
741
|
let cleanup = () => {};
|
|
713
742
|
let cancelled = false;
|
|
714
743
|
void loadMapModules()
|
|
715
|
-
.then(({ maplibregl, MapboxOverlay, PathLayer, ScatterplotLayer }) => {
|
|
744
|
+
.then(({ maplibregl, MapboxOverlay, PathLayer, ScatterplotLayer, IconLayer }) => {
|
|
716
745
|
if (cancelled) return;
|
|
717
|
-
layerCtors = { PathLayer, ScatterplotLayer };
|
|
746
|
+
layerCtors = { PathLayer, ScatterplotLayer, IconLayer };
|
|
718
747
|
const m = new maplibregl.Map({
|
|
719
748
|
container,
|
|
720
749
|
style: STYLE,
|
|
@@ -841,7 +870,7 @@
|
|
|
841
870
|
// updateTriggers tell deck.gl when accessor outputs changed.
|
|
842
871
|
$effect(() => {
|
|
843
872
|
if (!overlay || !layerCtors) return;
|
|
844
|
-
const { PathLayer, ScatterplotLayer } = layerCtors;
|
|
873
|
+
const { PathLayer, ScatterplotLayer, IconLayer } = layerCtors;
|
|
845
874
|
const layers: Layer[] = [];
|
|
846
875
|
for (const c of app.cases) {
|
|
847
876
|
if (!c.network) continue;
|
|
@@ -1014,13 +1043,17 @@
|
|
|
1014
1043
|
for (const c of app.multiCases) {
|
|
1015
1044
|
if (!c.view) continue;
|
|
1016
1045
|
const selectedId = c.id === app.activeMultiId ? c.selectedBusId : null;
|
|
1046
|
+
const selectedEdgeId = c.id === app.activeMultiId ? c.selectedEdgeId : null;
|
|
1017
1047
|
layers.push(
|
|
1018
1048
|
new PathLayer<PlacedMultiEdge>({
|
|
1019
1049
|
id: `multi-edges-${c.id}`,
|
|
1020
1050
|
data: c.view.edges,
|
|
1021
1051
|
getPath: (e) => e.path,
|
|
1022
|
-
|
|
1023
|
-
|
|
1052
|
+
// The selected edge draws in the selection blue of the branch cue,
|
|
1053
|
+
// wider, so its panel detail has an obvious source on the map.
|
|
1054
|
+
getColor: (e) =>
|
|
1055
|
+
e.id === selectedEdgeId ? [47, 111, 187, 255] : edgeColor(e.kind, e.closed),
|
|
1056
|
+
getWidth: (e) => edgeWidth(e.kind, e.n_phases) + (e.id === selectedEdgeId ? 2 : 0),
|
|
1024
1057
|
widthUnits: 'pixels',
|
|
1025
1058
|
widthMinPixels: 1.2,
|
|
1026
1059
|
capRounded: true,
|
|
@@ -1028,7 +1061,36 @@
|
|
|
1028
1061
|
miterLimit: 2,
|
|
1029
1062
|
pickable: true,
|
|
1030
1063
|
autoHighlight: true,
|
|
1031
|
-
highlightColor: [32, 36, 43, 90]
|
|
1064
|
+
highlightColor: [32, 36, 43, 90],
|
|
1065
|
+
onClick: (info: PickingInfo) => {
|
|
1066
|
+
const e = info.object as PlacedMultiEdge | undefined;
|
|
1067
|
+
if (!e || !onmultiedgeclick) return false;
|
|
1068
|
+
onmultiedgeclick(c.id, e.id);
|
|
1069
|
+
return true;
|
|
1070
|
+
},
|
|
1071
|
+
updateTriggers: {
|
|
1072
|
+
getColor: [selectedEdgeId],
|
|
1073
|
+
getWidth: [selectedEdgeId]
|
|
1074
|
+
}
|
|
1075
|
+
}),
|
|
1076
|
+
// The IEC two-circle symbol at each transformer midpoint, rotated into
|
|
1077
|
+
// the edge bearing. Not pickable: hover and click fall through to the
|
|
1078
|
+
// transformer edge beneath it.
|
|
1079
|
+
new IconLayer<TransformerMark>({
|
|
1080
|
+
id: `multi-xfmr-${c.id}`,
|
|
1081
|
+
data: transformerMarks(c.view.edges),
|
|
1082
|
+
getIcon: () => transformerIcon(),
|
|
1083
|
+
getPosition: (m) => m.position,
|
|
1084
|
+
getAngle: (m) => m.angle,
|
|
1085
|
+
billboard: false,
|
|
1086
|
+
getSize: 16,
|
|
1087
|
+
sizeUnits: 'pixels',
|
|
1088
|
+
getColor: (m) =>
|
|
1089
|
+
m.id === selectedEdgeId ? [47, 111, 187, 255] : edgeColor('transformer', true),
|
|
1090
|
+
pickable: false,
|
|
1091
|
+
updateTriggers: {
|
|
1092
|
+
getColor: [selectedEdgeId]
|
|
1093
|
+
}
|
|
1032
1094
|
}),
|
|
1033
1095
|
new ScatterplotLayer<PlacedMultiBus>({
|
|
1034
1096
|
id: `multi-buses-${c.id}`,
|
|
@@ -1081,6 +1143,25 @@
|
|
|
1081
1143
|
void tick().then(scheduleCueSync);
|
|
1082
1144
|
});
|
|
1083
1145
|
|
|
1146
|
+
// Pan the selected bus into the band the sheet leaves. The delay outlasts the
|
|
1147
|
+
// sheet's snap transition; sheetInset is read inside the timeout so the effect
|
|
1148
|
+
// does not re-run (and re-pan) on every frame of a sheet drag.
|
|
1149
|
+
$effect(() => {
|
|
1150
|
+
const cue = selectedBusCue;
|
|
1151
|
+
if (!map || !cue || !app.compactLayout) return;
|
|
1152
|
+
void cue.key;
|
|
1153
|
+
const m = map;
|
|
1154
|
+
const timer = setTimeout(() => {
|
|
1155
|
+
const { clientHeight } = m.getContainer();
|
|
1156
|
+
const y = m.project([cue.lon, cue.lat]).y;
|
|
1157
|
+
const floor = clientHeight - app.sheetInset - CUE_MARGIN_PX;
|
|
1158
|
+
const ceiling = 104;
|
|
1159
|
+
const dy = y > floor ? y - floor : y < ceiling ? y - ceiling : 0;
|
|
1160
|
+
if (dy !== 0) m.panBy([0, dy], { duration: prefersReducedMotion() ? 0 : 320 });
|
|
1161
|
+
}, 340);
|
|
1162
|
+
return () => clearTimeout(timer);
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1084
1165
|
function boundsFor(target: string): LngLatBoundsLike | null {
|
|
1085
1166
|
const points: [number, number][] = [];
|
|
1086
1167
|
const fold = (pts: { lon: number; lat: number }[]) => {
|
|
@@ -1136,7 +1217,11 @@
|
|
|
1136
1217
|
</script>
|
|
1137
1218
|
|
|
1138
1219
|
{#key mapGen}
|
|
1139
|
-
<div
|
|
1220
|
+
<div
|
|
1221
|
+
class="map-stage"
|
|
1222
|
+
class:chrome-clear={!chromeVisible}
|
|
1223
|
+
style="--chrome-inset: {chromeInset}px"
|
|
1224
|
+
>
|
|
1140
1225
|
<div class="map" {@attach initMap}></div>
|
|
1141
1226
|
{#if selectedBusCue}
|
|
1142
1227
|
{#key selectedBusCue.key}
|
|
@@ -1163,6 +1248,7 @@
|
|
|
1163
1248
|
<button
|
|
1164
1249
|
type="button"
|
|
1165
1250
|
class="place-at-center mono"
|
|
1251
|
+
style="--chrome-inset: {chromeInset}px"
|
|
1166
1252
|
onclick={() => {
|
|
1167
1253
|
if (!map) return;
|
|
1168
1254
|
const center = map.getCenter();
|
|
@@ -1346,7 +1432,7 @@
|
|
|
1346
1432
|
.place-at-center {
|
|
1347
1433
|
position: absolute;
|
|
1348
1434
|
left: 50%;
|
|
1349
|
-
bottom: 92px;
|
|
1435
|
+
bottom: calc(92px + var(--chrome-inset, 0px));
|
|
1350
1436
|
z-index: 15;
|
|
1351
1437
|
transform: translateX(-50%);
|
|
1352
1438
|
padding: 7px 14px;
|
|
@@ -1361,17 +1447,27 @@
|
|
|
1361
1447
|
|
|
1362
1448
|
@media (max-width: 420px) {
|
|
1363
1449
|
.place-at-center {
|
|
1364
|
-
bottom: 78px;
|
|
1450
|
+
bottom: calc(78px + var(--chrome-inset, 0px));
|
|
1365
1451
|
}
|
|
1366
1452
|
}
|
|
1367
1453
|
|
|
1368
1454
|
.map :global(.maplibregl-ctrl-bottom-right) {
|
|
1369
1455
|
right: 20px;
|
|
1370
|
-
bottom
|
|
1456
|
+
/* rides above the bottom sheet so attribution stays legible */
|
|
1457
|
+
bottom: calc(18px + var(--chrome-inset, 0px));
|
|
1371
1458
|
display: flex;
|
|
1372
1459
|
flex-direction: column;
|
|
1373
1460
|
align-items: flex-end;
|
|
1374
1461
|
gap: 6px;
|
|
1462
|
+
transition:
|
|
1463
|
+
bottom var(--dur-med) var(--ease-out),
|
|
1464
|
+
opacity var(--dur-fast) var(--ease-out);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/* The sheet has taken the map; nothing left down there to attribute. */
|
|
1468
|
+
.map-stage.chrome-clear :global(.maplibregl-ctrl-bottom-right) {
|
|
1469
|
+
opacity: 0;
|
|
1470
|
+
pointer-events: none;
|
|
1375
1471
|
}
|
|
1376
1472
|
|
|
1377
1473
|
.map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl) {
|
|
@@ -1399,6 +1495,26 @@
|
|
|
1399
1495
|
color: var(--text-secondary);
|
|
1400
1496
|
}
|
|
1401
1497
|
|
|
1498
|
+
@media (hover: none), (pointer: coarse) {
|
|
1499
|
+
/* A long press on the canvas must not raise the iOS callout or magnifier.
|
|
1500
|
+
Scoped to touch so a mouse can still select the attribution text. */
|
|
1501
|
+
.map {
|
|
1502
|
+
-webkit-touch-callout: none;
|
|
1503
|
+
-webkit-tap-highlight-color: transparent;
|
|
1504
|
+
user-select: none;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/* maplibre ships 29px zoom buttons; a fingertip wants 44. */
|
|
1508
|
+
.map :global(.maplibregl-ctrl-group button) {
|
|
1509
|
+
width: 44px;
|
|
1510
|
+
height: 44px;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.map :global(.maplibregl-ctrl-group button .maplibregl-ctrl-icon) {
|
|
1514
|
+
background-size: 22px 22px;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1402
1518
|
.map :global(.deck-tooltip) {
|
|
1403
1519
|
background: var(--panel) !important;
|
|
1404
1520
|
border: 1px solid var(--line);
|
|
@@ -1413,11 +1529,31 @@
|
|
|
1413
1529
|
|
|
1414
1530
|
@media (max-width: 760px) {
|
|
1415
1531
|
.map :global(.maplibregl-ctrl-attrib) {
|
|
1532
|
+
/* last in the column so it rests on the sheet's edge instead of
|
|
1533
|
+
floating over the network a zoom control's height above it */
|
|
1534
|
+
order: 1;
|
|
1416
1535
|
width: auto;
|
|
1417
1536
|
min-width: 0;
|
|
1418
1537
|
right: 0;
|
|
1419
1538
|
white-space: normal;
|
|
1420
1539
|
}
|
|
1540
|
+
|
|
1541
|
+
/* The band of map a phone has left between the header and the sheet is
|
|
1542
|
+
about the height of the solve card, which covers the buttons wherever
|
|
1543
|
+
they land. Pinch and double tap zoom; the attribution stays. */
|
|
1544
|
+
.map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl-group) {
|
|
1545
|
+
display: none;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/* One line on a small phone: wrapped attribution turns into a wide block
|
|
1550
|
+
sitting over the network. */
|
|
1551
|
+
@media (max-width: 460px) {
|
|
1552
|
+
.map :global(.maplibregl-ctrl-attrib) {
|
|
1553
|
+
max-width: calc(100vw - 20px);
|
|
1554
|
+
font-size: 9px;
|
|
1555
|
+
white-space: nowrap;
|
|
1556
|
+
}
|
|
1421
1557
|
}
|
|
1422
1558
|
|
|
1423
1559
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -5,6 +5,7 @@ type $$ComponentProps = {
|
|
|
5
5
|
onbranchclick?: (caseId: string, branchId: number) => void;
|
|
6
6
|
onlocalbranchclick?: (caseId: string, branchId: number) => void;
|
|
7
7
|
onmultibusclick: (caseId: string, busId: string) => void;
|
|
8
|
+
onmultiedgeclick?: (caseId: string, edgeId: string) => void;
|
|
8
9
|
onplacecase: (lon: number, lat: number) => void;
|
|
9
10
|
onmapclick: () => void;
|
|
10
11
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TellegenMap.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenMap.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"TellegenMap.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenMap.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,kCAAkC,CAAC;AAoEzC,KAAK,gBAAgB,GAAI;IACxB,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AA+qCH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import TellegenMap from './TellegenMap.svelte';
|
|
13
13
|
|
|
14
14
|
const FILE_DROP_QUERY = '(hover: hover) and (pointer: fine) and (min-width: 761px)';
|
|
15
|
+
const COMPACT_QUERY = '(max-width: 760px)';
|
|
15
16
|
|
|
16
17
|
const app = getAppState();
|
|
17
18
|
const ctrl = getController();
|
|
@@ -50,7 +51,28 @@
|
|
|
50
51
|
};
|
|
51
52
|
syncFileDropUi();
|
|
52
53
|
query.addEventListener('change', syncFileDropUi);
|
|
53
|
-
|
|
54
|
+
|
|
55
|
+
const compact = window.matchMedia(COMPACT_QUERY);
|
|
56
|
+
const syncCompact = () => {
|
|
57
|
+
app.compactLayout = compact.matches;
|
|
58
|
+
if (!compact.matches) app.sheetInset = 0;
|
|
59
|
+
};
|
|
60
|
+
syncCompact();
|
|
61
|
+
compact.addEventListener('change', syncCompact);
|
|
62
|
+
|
|
63
|
+
// The sheet sizes its snaps against the viewport and the map caps how far
|
|
64
|
+
// it lifts the basemap chrome, so both read one measurement taken here.
|
|
65
|
+
const syncViewport = () => (app.viewportHeight = window.innerHeight);
|
|
66
|
+
syncViewport();
|
|
67
|
+
window.addEventListener('resize', syncViewport);
|
|
68
|
+
window.addEventListener('orientationchange', syncViewport);
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
query.removeEventListener('change', syncFileDropUi);
|
|
72
|
+
compact.removeEventListener('change', syncCompact);
|
|
73
|
+
window.removeEventListener('resize', syncViewport);
|
|
74
|
+
window.removeEventListener('orientationchange', syncViewport);
|
|
75
|
+
};
|
|
54
76
|
});
|
|
55
77
|
|
|
56
78
|
function dragHasFiles(e: DragEvent): boolean {
|
|
@@ -101,6 +123,7 @@
|
|
|
101
123
|
onbranchclick={ctrl.selectBranch}
|
|
102
124
|
onlocalbranchclick={ctrl.selectLocalBranch}
|
|
103
125
|
onmultibusclick={ctrl.selectMultiBus}
|
|
126
|
+
onmultiedgeclick={ctrl.selectMultiEdge}
|
|
104
127
|
onplacecase={ctrl.placeCase}
|
|
105
128
|
onmapclick={ctrl.clearSelection}
|
|
106
129
|
/>
|
|
@@ -108,11 +131,14 @@
|
|
|
108
131
|
<AppHeader />
|
|
109
132
|
<ControlPanel />
|
|
110
133
|
<SolveCard />
|
|
111
|
-
|
|
134
|
+
<!-- ControlPanel mounts the lookup and the footer inline when compact. -->
|
|
135
|
+
{#if !app.compactLayout}
|
|
136
|
+
<BusPicker />
|
|
137
|
+
{/if}
|
|
112
138
|
<DropZone />
|
|
113
139
|
<PlacementCue />
|
|
114
140
|
<RestoreDefaultsButton />
|
|
115
|
-
{#if config.showFooter}
|
|
141
|
+
{#if config.showFooter && !app.compactLayout}
|
|
116
142
|
<AppFooter />
|
|
117
143
|
{/if}
|
|
118
144
|
</main>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TellegenShell.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenShell.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TellegenShell.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenShell.svelte.ts"],"names":[],"mappings":"AAqJA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { getController } from '../context.svelte.js';
|
|
3
3
|
|
|
4
4
|
const ctrl = getController();
|
|
5
|
+
|
|
6
|
+
/** Render in the parent's flow instead of pinned to the bottom of the map. */
|
|
7
|
+
let { inline = false }: { inline?: boolean } = $props();
|
|
5
8
|
</script>
|
|
6
9
|
|
|
7
|
-
<footer class="mono">
|
|
10
|
+
<footer class="mono" class:inline>
|
|
8
11
|
<a href="/credits/">credits</a>
|
|
9
12
|
<i class="sep"></i>
|
|
10
13
|
<a href="/privacy/">privacy</a>
|
|
@@ -50,6 +53,28 @@
|
|
|
50
53
|
color: var(--text-accent);
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
footer.inline {
|
|
57
|
+
position: static;
|
|
58
|
+
margin-top: 14px;
|
|
59
|
+
padding: 0;
|
|
60
|
+
background: none;
|
|
61
|
+
animation: none;
|
|
62
|
+
pointer-events: auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@media (hover: none), (pointer: coarse) {
|
|
66
|
+
footer a {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
min-height: 44px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
footer.inline {
|
|
73
|
+
margin-top: 4px;
|
|
74
|
+
font-size: 11px;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
@media (max-width: 760px) {
|
|
54
79
|
footer {
|
|
55
80
|
padding: 7px 10px;
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
$$events?: Events;
|
|
7
|
-
$$slots?: Slots;
|
|
8
|
-
}): Exports & {
|
|
9
|
-
$set?: any;
|
|
10
|
-
$on?: any;
|
|
11
|
-
};
|
|
12
|
-
z_$$bindings?: Bindings;
|
|
13
|
-
}
|
|
14
|
-
declare const AppFooter: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
-
[evt: string]: CustomEvent<any>;
|
|
16
|
-
}, {}, {}, string>;
|
|
17
|
-
type AppFooter = InstanceType<typeof AppFooter>;
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
inline?: boolean;
|
|
3
|
+
};
|
|
4
|
+
declare const AppFooter: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
5
|
+
type AppFooter = ReturnType<typeof AppFooter>;
|
|
18
6
|
export default AppFooter;
|
|
19
7
|
//# sourceMappingURL=AppFooter.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppFooter.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AppFooter.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppFooter.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AppFooter.svelte.ts"],"names":[],"mappings":"AAKC,KAAK,gBAAgB,GAAI;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAwB/C,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -7,9 +7,23 @@
|
|
|
7
7
|
const config = getUiConfig();
|
|
8
8
|
|
|
9
9
|
let fileInput = $state.raw<HTMLInputElement | undefined>(undefined);
|
|
10
|
+
let headerEl = $state.raw<HTMLElement | undefined>(undefined);
|
|
11
|
+
|
|
12
|
+
// The case tabs wrap onto their own row on a narrow viewport and unwrap on a
|
|
13
|
+
// short one, so the height moves with both the breakpoint and the case list.
|
|
14
|
+
// Publish it for the map chrome and the sheet, which both anchor below it.
|
|
15
|
+
$effect(() => {
|
|
16
|
+
const el = headerEl;
|
|
17
|
+
if (!el) return;
|
|
18
|
+
const measure = () => (app.headerInset = Math.round(el.getBoundingClientRect().height));
|
|
19
|
+
measure();
|
|
20
|
+
const observer = new ResizeObserver(measure);
|
|
21
|
+
observer.observe(el);
|
|
22
|
+
return () => observer.disconnect();
|
|
23
|
+
});
|
|
10
24
|
</script>
|
|
11
25
|
|
|
12
|
-
<header>
|
|
26
|
+
<header bind:this={headerEl}>
|
|
13
27
|
<div class="brand">
|
|
14
28
|
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
|
|
15
29
|
<path d="M4 18 L12 6 L20 18" stroke="#b25e00" stroke-width="1.6" fill="none" />
|
|
@@ -397,6 +411,50 @@
|
|
|
397
411
|
}
|
|
398
412
|
}
|
|
399
413
|
|
|
414
|
+
/* Landscape phone: height is the scarce axis. Keep the case tabs on the
|
|
415
|
+
brand's row instead of giving them one of their own, which halves the
|
|
416
|
+
header and leaves the map a band worth looking at. */
|
|
417
|
+
@media (max-width: 760px) and (max-height: 520px) {
|
|
418
|
+
header {
|
|
419
|
+
flex-wrap: nowrap;
|
|
420
|
+
align-items: center;
|
|
421
|
+
padding: 6px 10px;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
h1 {
|
|
425
|
+
font-size: 17px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.cases {
|
|
429
|
+
order: 0;
|
|
430
|
+
width: auto;
|
|
431
|
+
justify-content: flex-start;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.kicker {
|
|
435
|
+
display: none;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Touch sizing keys off the pointer, not the breakpoint: a tablet gets the
|
|
440
|
+
wide layout and still needs a fingertip's worth of chip. */
|
|
441
|
+
@media (hover: none), (pointer: coarse) {
|
|
442
|
+
.cases > button,
|
|
443
|
+
.case-chip {
|
|
444
|
+
min-height: 44px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.case-activate {
|
|
448
|
+
min-height: 44px;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.kicker a {
|
|
452
|
+
display: inline-flex;
|
|
453
|
+
align-items: center;
|
|
454
|
+
min-height: 44px;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
400
458
|
@media (prefers-reduced-motion: reduce) {
|
|
401
459
|
header {
|
|
402
460
|
animation: none;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppHeader.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AppHeader.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppHeader.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AppHeader.svelte.ts"],"names":[],"mappings":"AA6FA,QAAA,MAAM,SAAS,2DAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -98,4 +98,17 @@
|
|
|
98
98
|
box-shadow: inset 3px 0 0 var(--accent);
|
|
99
99
|
font-weight: 600;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/* Each row is a tap target that reframes the map, so give it a finger's worth
|
|
103
|
+
of height and show a couple more rows before the list scrolls. */
|
|
104
|
+
@media (hover: none), (pointer: coarse) {
|
|
105
|
+
ul {
|
|
106
|
+
max-height: 176px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
li button {
|
|
110
|
+
min-height: 44px;
|
|
111
|
+
padding-block: 6px;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
101
114
|
</style>
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
const app = getAppState();
|
|
6
6
|
const ctrl = getController();
|
|
7
7
|
|
|
8
|
+
/** Render in the parent's flow instead of as a card floating over the map. */
|
|
9
|
+
let { inline = false }: { inline?: boolean } = $props();
|
|
10
|
+
|
|
8
11
|
const buses = $derived(app.active?.network?.buses ?? app.activeLocal?.view?.buses ?? []);
|
|
9
12
|
|
|
10
13
|
let query = $state('');
|
|
@@ -27,6 +30,11 @@
|
|
|
27
30
|
query = '';
|
|
28
31
|
open = false;
|
|
29
32
|
highlighted = 0;
|
|
33
|
+
// Drop the on-screen keyboard, which otherwise covers the readout the
|
|
34
|
+
// selection just produced. A keyboard user keeps focus for the next lookup.
|
|
35
|
+
if (typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches) {
|
|
36
|
+
inputEl?.blur();
|
|
37
|
+
}
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
function onKeydown(e: KeyboardEvent) {
|
|
@@ -69,6 +77,7 @@
|
|
|
69
77
|
{#if buses.length > 0}
|
|
70
78
|
<div
|
|
71
79
|
class="bus-lookup"
|
|
80
|
+
class:inline
|
|
72
81
|
title="Press / to focus bus lookup"
|
|
73
82
|
onfocusout={(e) => {
|
|
74
83
|
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) open = false;
|
|
@@ -160,6 +169,37 @@
|
|
|
160
169
|
outline-offset: 1px;
|
|
161
170
|
}
|
|
162
171
|
|
|
172
|
+
/* Safari zooms the layout viewport when a focused input is under 16px. */
|
|
173
|
+
@media (hover: none), (pointer: coarse) {
|
|
174
|
+
input {
|
|
175
|
+
min-height: 44px;
|
|
176
|
+
font-size: 16px;
|
|
177
|
+
padding: 7px 10px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
li {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
min-height: 44px;
|
|
184
|
+
font-size: 14px;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.bus-lookup.inline {
|
|
189
|
+
/* relative, not static: the results list is absolute against this block */
|
|
190
|
+
position: relative;
|
|
191
|
+
inset: auto;
|
|
192
|
+
width: auto;
|
|
193
|
+
margin-top: 12px;
|
|
194
|
+
padding: 0;
|
|
195
|
+
background: none;
|
|
196
|
+
border: 0;
|
|
197
|
+
border-radius: 0;
|
|
198
|
+
backdrop-filter: none;
|
|
199
|
+
box-shadow: none;
|
|
200
|
+
animation: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
163
203
|
ul {
|
|
164
204
|
position: absolute;
|
|
165
205
|
z-index: 20;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
inline?: boolean;
|
|
3
|
+
};
|
|
4
|
+
declare const BusPicker: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
5
|
type BusPicker = ReturnType<typeof BusPicker>;
|
|
3
6
|
export default BusPicker;
|
|
4
7
|
//# sourceMappingURL=BusPicker.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BusPicker.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/BusPicker.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BusPicker.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/BusPicker.svelte.ts"],"names":[],"mappings":"AAMC,KAAK,gBAAgB,GAAI;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA6G/C,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|