@tellegen/svelte 0.1.1 → 0.2.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 +15 -0
- package/dist/Sparkline.svelte.d.ts +0 -1
- package/dist/TellegenMap.svelte +109 -47
- package/dist/TellegenMap.svelte.d.ts +0 -1
- package/dist/TellegenProvider.svelte.d.ts +0 -1
- package/dist/TellegenShell.svelte +28 -3
- package/dist/TellegenShell.svelte.d.ts +0 -1
- package/dist/TellegenViewer.svelte.d.ts +0 -1
- package/dist/api.d.ts +0 -1
- package/dist/colors.d.ts +0 -1
- package/dist/components/AppFooter.svelte +26 -1
- package/dist/components/AppFooter.svelte.d.ts +5 -18
- package/dist/components/AppHeader.svelte +63 -3
- package/dist/components/AppHeader.svelte.d.ts +0 -1
- package/dist/components/BindingLines.svelte +13 -0
- package/dist/components/BindingLines.svelte.d.ts +0 -1
- package/dist/components/BusPicker.svelte +40 -0
- package/dist/components/BusPicker.svelte.d.ts +4 -2
- package/dist/components/ControlPanel.svelte +359 -72
- package/dist/components/ControlPanel.svelte.d.ts +2 -18
- package/dist/components/DemandSlider.svelte +28 -8
- package/dist/components/DemandSlider.svelte.d.ts +0 -1
- package/dist/components/DisplayControls.svelte.d.ts +0 -1
- package/dist/components/DropZone.svelte.d.ts +0 -1
- package/dist/components/FormulationSelector.svelte +17 -0
- package/dist/components/FormulationSelector.svelte.d.ts +0 -1
- package/dist/components/LocalCaseDetails.svelte +5 -2
- package/dist/components/LocalCaseDetails.svelte.d.ts +0 -1
- package/dist/components/MulticonductorDetails.svelte +3 -3
- package/dist/components/MulticonductorDetails.svelte.d.ts +0 -1
- package/dist/components/NetworkStats.svelte.d.ts +0 -1
- package/dist/components/PlacementCue.svelte.d.ts +0 -1
- package/dist/components/RatingSlider.svelte.d.ts +0 -1
- package/dist/components/RestoreDefaultsButton.svelte.d.ts +0 -1
- package/dist/components/SensitivityReadout.svelte +10 -1
- package/dist/components/SensitivityReadout.svelte.d.ts +0 -1
- package/dist/components/SizeLegend.svelte.d.ts +0 -1
- package/dist/components/SolveCard.svelte +2 -1
- package/dist/components/SolveCard.svelte.d.ts +0 -1
- package/dist/components/TopMovers.svelte.d.ts +0 -1
- package/dist/components.d.ts +0 -1
- package/dist/context.svelte.d.ts +0 -1
- package/dist/controller.svelte.d.ts +24 -12
- package/dist/controller.svelte.js +218 -101
- package/dist/display.d.ts +0 -1
- package/dist/drop-classify.d.ts +5 -42
- package/dist/drop-classify.js +5 -78
- package/dist/drop-limits.d.ts +27 -0
- package/dist/drop-limits.js +83 -0
- package/dist/format.d.ts +0 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.js +2 -2
- package/dist/map-bounds.d.ts +5 -0
- package/dist/map-bounds.js +41 -0
- package/dist/map.d.ts +0 -1
- package/dist/multiconductor.d.ts +0 -1
- package/dist/state.svelte.d.ts +13 -1
- package/dist/state.svelte.js +13 -0
- package/dist/study-package.d.ts +0 -1
- package/dist/styles.css +16 -2
- package/dist/synthetic-layout.d.ts +0 -1
- package/dist/synthetic-layout.js +16 -2
- package/dist/transformer-icon.d.ts +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +14 -11
- package/dist/Sparkline.svelte.d.ts.map +0 -1
- package/dist/TellegenMap.svelte.d.ts.map +0 -1
- package/dist/TellegenProvider.svelte.d.ts.map +0 -1
- package/dist/TellegenShell.svelte.d.ts.map +0 -1
- package/dist/TellegenViewer.svelte.d.ts.map +0 -1
- package/dist/api.d.ts.map +0 -1
- package/dist/colors.d.ts.map +0 -1
- package/dist/components/AppFooter.svelte.d.ts.map +0 -1
- package/dist/components/AppHeader.svelte.d.ts.map +0 -1
- package/dist/components/BindingLines.svelte.d.ts.map +0 -1
- package/dist/components/BusPicker.svelte.d.ts.map +0 -1
- package/dist/components/ControlPanel.svelte.d.ts.map +0 -1
- package/dist/components/DemandSlider.svelte.d.ts.map +0 -1
- package/dist/components/DisplayControls.svelte.d.ts.map +0 -1
- package/dist/components/DropZone.svelte.d.ts.map +0 -1
- package/dist/components/FormulationSelector.svelte.d.ts.map +0 -1
- package/dist/components/LocalCaseDetails.svelte.d.ts.map +0 -1
- package/dist/components/MulticonductorDetails.svelte.d.ts.map +0 -1
- package/dist/components/NetworkStats.svelte.d.ts.map +0 -1
- package/dist/components/PlacementCue.svelte.d.ts.map +0 -1
- package/dist/components/RatingSlider.svelte.d.ts.map +0 -1
- package/dist/components/RestoreDefaultsButton.svelte.d.ts.map +0 -1
- package/dist/components/SensitivityReadout.svelte.d.ts.map +0 -1
- package/dist/components/SizeLegend.svelte.d.ts.map +0 -1
- package/dist/components/SolveCard.svelte.d.ts.map +0 -1
- package/dist/components/TopMovers.svelte.d.ts.map +0 -1
- package/dist/components.d.ts.map +0 -1
- package/dist/context.svelte.d.ts.map +0 -1
- package/dist/controller.svelte.d.ts.map +0 -1
- package/dist/display.d.ts.map +0 -1
- package/dist/drop-classify.d.ts.map +0 -1
- package/dist/format.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/map.d.ts.map +0 -1
- package/dist/multiconductor.d.ts.map +0 -1
- package/dist/state.svelte.d.ts.map +0 -1
- package/dist/study-package.d.ts.map +0 -1
- package/dist/synthetic-layout.d.ts.map +0 -1
- package/dist/transformer-icon.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -28,6 +28,10 @@ Use local files only by disabling bundled case loading:
|
|
|
28
28
|
<TellegenViewer loadDefaultCases={false} showFooter={false} />
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
The viewer accepts at most 32 files in one ingestion batch. Each file must be
|
|
32
|
+
at most 128 MiB, and the aggregate batch must also be at most 128 MiB. Files
|
|
33
|
+
remain in the browser on this path.
|
|
34
|
+
|
|
31
35
|
`TellegenViewer` accepts:
|
|
32
36
|
|
|
33
37
|
- `apiBase`, default `/api`
|
|
@@ -50,6 +54,17 @@ Use `TellegenProvider` and `TellegenShell` when state should survive route chang
|
|
|
50
54
|
</TellegenProvider>
|
|
51
55
|
```
|
|
52
56
|
|
|
57
|
+
## Engine Reexports
|
|
58
|
+
|
|
59
|
+
The package reexports the browser engine helpers, including `ingestCase`,
|
|
60
|
+
`classifyJson`, and `ingestJsonDrop`, for custom drop surfaces. In 0.2 these
|
|
61
|
+
APIs use byte input: pass `Uint8Array` to `ingestCase`, await
|
|
62
|
+
`classifyJson(bytes)` and read its `{ kind, format }` result, and use
|
|
63
|
+
`ingestJsonDrop(bytes)` to classify and parse in one call. `isStudyPackageText`
|
|
64
|
+
was removed; test for `kind === "balanced-package"` instead. `JsonDropKind` now
|
|
65
|
+
includes `transmission`, `distribution`, `ambiguous`, and `unknown`; BMOPF and
|
|
66
|
+
PMD are distribution `format` values rather than separate kinds.
|
|
67
|
+
|
|
53
68
|
## Release
|
|
54
69
|
|
|
55
70
|
Build and inspect the package from the repository root:
|
package/dist/TellegenMap.svelte
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
type TransformerMark
|
|
33
33
|
} from './multiconductor.js';
|
|
34
34
|
import { transformerIcon } from './transformer-icon.js';
|
|
35
|
+
import { foldMapBounds } from './map-bounds.js';
|
|
35
36
|
import { getAppState, getController } from './context.svelte.js';
|
|
36
37
|
import { displayFmt, fmt, rgbaCss } from './format.js';
|
|
37
38
|
import type { RGBA } from './colors.js';
|
|
@@ -495,54 +496,18 @@
|
|
|
495
496
|
* The branch variant also clears the rating readout on the panel's lower edge. */
|
|
496
497
|
function framePadding(width: number, height: number, kind: 'case' | 'branch') {
|
|
497
498
|
if (width <= 760) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return { top: 130, left: 24, right: 24, bottom };
|
|
499
|
+
// bottom follows the sheet; capped so a fully open sheet still leaves a band to fit into
|
|
500
|
+
const clearance = kind === 'branch' ? 16 : 28;
|
|
501
|
+
const bottom = Math.min(app.sheetInset + clearance, Math.round(height * 0.55));
|
|
502
|
+
return { top: app.headerInset + 8, left: 24, right: 24, bottom };
|
|
503
503
|
}
|
|
504
504
|
return kind === 'branch'
|
|
505
505
|
? { top: 106, left: 388, right: 76, bottom: 80 }
|
|
506
506
|
: { top: 96, left: 380, right: 60, bottom: 64 };
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
-
/** Fold lon/lat pairs into LngLat bounds; null when no point is finite. */
|
|
510
|
-
function foldBounds(
|
|
511
|
-
points: Iterable<[number, number]>
|
|
512
|
-
): [[number, number], [number, number]] | null {
|
|
513
|
-
let minLon = Infinity;
|
|
514
|
-
let minLat = Infinity;
|
|
515
|
-
let maxLon = -Infinity;
|
|
516
|
-
let maxLat = -Infinity;
|
|
517
|
-
for (const [lon, lat] of points) {
|
|
518
|
-
minLon = Math.min(minLon, lon);
|
|
519
|
-
minLat = Math.min(minLat, lat);
|
|
520
|
-
maxLon = Math.max(maxLon, lon);
|
|
521
|
-
maxLat = Math.max(maxLat, lat);
|
|
522
|
-
}
|
|
523
|
-
if (!Number.isFinite(minLon) || !Number.isFinite(minLat)) return null;
|
|
524
|
-
return [
|
|
525
|
-
[minLon, minLat],
|
|
526
|
-
[maxLon, maxLat]
|
|
527
|
-
];
|
|
528
|
-
}
|
|
529
|
-
|
|
530
509
|
function branchBounds(path: [number, number][]): LngLatBoundsLike | null {
|
|
531
|
-
|
|
532
|
-
if (!bounds) return null;
|
|
533
|
-
let [[minLon, minLat], [maxLon, maxLat]] = bounds;
|
|
534
|
-
if (minLon === maxLon) {
|
|
535
|
-
minLon -= 0.005;
|
|
536
|
-
maxLon += 0.005;
|
|
537
|
-
}
|
|
538
|
-
if (minLat === maxLat) {
|
|
539
|
-
minLat -= 0.005;
|
|
540
|
-
maxLat += 0.005;
|
|
541
|
-
}
|
|
542
|
-
return [
|
|
543
|
-
[minLon, minLat],
|
|
544
|
-
[maxLon, maxLat]
|
|
545
|
-
];
|
|
510
|
+
return foldMapBounds(path);
|
|
546
511
|
}
|
|
547
512
|
|
|
548
513
|
function prefersReducedMotion(): boolean {
|
|
@@ -552,6 +517,25 @@
|
|
|
552
517
|
);
|
|
553
518
|
}
|
|
554
519
|
|
|
520
|
+
function coarsePointer(): boolean {
|
|
521
|
+
return typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Compact chrome anchoring. maplibre's bottom-right controls sit over the map,
|
|
525
|
+
// so the sheet would bury them; they ride above it instead. Under
|
|
526
|
+
// CHROME_MIN_BAND_PX of map left they drop out rather than climb into the
|
|
527
|
+
// header — at that point the sheet covers what they annotate.
|
|
528
|
+
const CHROME_MIN_BAND_PX = 132;
|
|
529
|
+
|
|
530
|
+
/** How far the sheet can push the chrome up before it would reach the header. */
|
|
531
|
+
const chromeLiftCap = $derived(
|
|
532
|
+
Math.max(0, app.viewportHeight - app.headerInset - CHROME_MIN_BAND_PX)
|
|
533
|
+
);
|
|
534
|
+
const chromeInset = $derived(
|
|
535
|
+
app.compactLayout ? Math.min(app.sheetInset, chromeLiftCap) : 0
|
|
536
|
+
);
|
|
537
|
+
const chromeVisible = $derived(!app.compactLayout || app.sheetInset <= chromeLiftCap);
|
|
538
|
+
|
|
555
539
|
/** Fly to the selected branch; true when a camera move started. */
|
|
556
540
|
function focusSelectedBranch(): boolean {
|
|
557
541
|
if (!map || !selectedBranchCue) return false;
|
|
@@ -699,6 +683,7 @@
|
|
|
699
683
|
// deck layers also inherit maplibre's 4096 canvas clamp. The flat positron
|
|
700
684
|
// basemap writes no depth, so the overlay always draws on top.
|
|
701
685
|
function buildOverlay(MapboxOverlay: MapModules['MapboxOverlay']): MapboxOverlay {
|
|
686
|
+
const touch = coarsePointer();
|
|
702
687
|
return new MapboxOverlay({
|
|
703
688
|
interleaved: true,
|
|
704
689
|
layers: [],
|
|
@@ -706,10 +691,13 @@
|
|
|
706
691
|
depthWriteEnabled: false,
|
|
707
692
|
depthCompare: 'always'
|
|
708
693
|
},
|
|
694
|
+
// a small bus draws ~9px across, well under a fingertip
|
|
695
|
+
pickingRadius: touch ? 12 : 0,
|
|
709
696
|
onClick: (info: PickingInfo) => {
|
|
710
697
|
if (!app.placingId && !isSelectableLayer(info.layer?.id)) onmapclick();
|
|
711
698
|
},
|
|
712
|
-
|
|
699
|
+
// a touch tooltip opens under the finger and stays until the next tap
|
|
700
|
+
getTooltip: touch ? undefined : tooltip,
|
|
713
701
|
getCursor: ({ isHovering, isDragging }: CursorState) =>
|
|
714
702
|
app.placingId ? 'crosshair' : isDragging ? 'grabbing' : isHovering ? 'pointer' : 'grab'
|
|
715
703
|
});
|
|
@@ -1121,6 +1109,25 @@
|
|
|
1121
1109
|
void tick().then(scheduleCueSync);
|
|
1122
1110
|
});
|
|
1123
1111
|
|
|
1112
|
+
// Pan the selected bus into the band the sheet leaves. The delay outlasts the
|
|
1113
|
+
// sheet's snap transition; sheetInset is read inside the timeout so the effect
|
|
1114
|
+
// does not re-run (and re-pan) on every frame of a sheet drag.
|
|
1115
|
+
$effect(() => {
|
|
1116
|
+
const cue = selectedBusCue;
|
|
1117
|
+
if (!map || !cue || !app.compactLayout) return;
|
|
1118
|
+
void cue.key;
|
|
1119
|
+
const m = map;
|
|
1120
|
+
const timer = setTimeout(() => {
|
|
1121
|
+
const { clientHeight } = m.getContainer();
|
|
1122
|
+
const y = m.project([cue.lon, cue.lat]).y;
|
|
1123
|
+
const floor = clientHeight - app.sheetInset - CUE_MARGIN_PX;
|
|
1124
|
+
const ceiling = 104;
|
|
1125
|
+
const dy = y > floor ? y - floor : y < ceiling ? y - ceiling : 0;
|
|
1126
|
+
if (dy !== 0) m.panBy([0, dy], { duration: prefersReducedMotion() ? 0 : 320 });
|
|
1127
|
+
}, 340);
|
|
1128
|
+
return () => clearTimeout(timer);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1124
1131
|
function boundsFor(target: string): LngLatBoundsLike | null {
|
|
1125
1132
|
const points: [number, number][] = [];
|
|
1126
1133
|
const fold = (pts: { lon: number; lat: number }[]) => {
|
|
@@ -1139,7 +1146,7 @@
|
|
|
1139
1146
|
if (target !== 'all' && c.id !== target) continue;
|
|
1140
1147
|
if (c.view) fold(c.view.buses);
|
|
1141
1148
|
}
|
|
1142
|
-
return
|
|
1149
|
+
return foldMapBounds(points);
|
|
1143
1150
|
}
|
|
1144
1151
|
|
|
1145
1152
|
// Fly to whatever the header, the initial load, or a branch selection asked
|
|
@@ -1176,7 +1183,11 @@
|
|
|
1176
1183
|
</script>
|
|
1177
1184
|
|
|
1178
1185
|
{#key mapGen}
|
|
1179
|
-
<div
|
|
1186
|
+
<div
|
|
1187
|
+
class="map-stage"
|
|
1188
|
+
class:chrome-clear={!chromeVisible}
|
|
1189
|
+
style="--chrome-inset: {chromeInset}px"
|
|
1190
|
+
>
|
|
1180
1191
|
<div class="map" {@attach initMap}></div>
|
|
1181
1192
|
{#if selectedBusCue}
|
|
1182
1193
|
{#key selectedBusCue.key}
|
|
@@ -1203,6 +1214,7 @@
|
|
|
1203
1214
|
<button
|
|
1204
1215
|
type="button"
|
|
1205
1216
|
class="place-at-center mono"
|
|
1217
|
+
style="--chrome-inset: {chromeInset}px"
|
|
1206
1218
|
onclick={() => {
|
|
1207
1219
|
if (!map) return;
|
|
1208
1220
|
const center = map.getCenter();
|
|
@@ -1386,7 +1398,7 @@
|
|
|
1386
1398
|
.place-at-center {
|
|
1387
1399
|
position: absolute;
|
|
1388
1400
|
left: 50%;
|
|
1389
|
-
bottom: 92px;
|
|
1401
|
+
bottom: calc(92px + var(--chrome-inset, 0px));
|
|
1390
1402
|
z-index: 15;
|
|
1391
1403
|
transform: translateX(-50%);
|
|
1392
1404
|
padding: 7px 14px;
|
|
@@ -1401,17 +1413,27 @@
|
|
|
1401
1413
|
|
|
1402
1414
|
@media (max-width: 420px) {
|
|
1403
1415
|
.place-at-center {
|
|
1404
|
-
bottom: 78px;
|
|
1416
|
+
bottom: calc(78px + var(--chrome-inset, 0px));
|
|
1405
1417
|
}
|
|
1406
1418
|
}
|
|
1407
1419
|
|
|
1408
1420
|
.map :global(.maplibregl-ctrl-bottom-right) {
|
|
1409
1421
|
right: 20px;
|
|
1410
|
-
bottom
|
|
1422
|
+
/* rides above the bottom sheet so attribution stays legible */
|
|
1423
|
+
bottom: calc(18px + var(--chrome-inset, 0px));
|
|
1411
1424
|
display: flex;
|
|
1412
1425
|
flex-direction: column;
|
|
1413
1426
|
align-items: flex-end;
|
|
1414
1427
|
gap: 6px;
|
|
1428
|
+
transition:
|
|
1429
|
+
bottom var(--dur-med) var(--ease-out),
|
|
1430
|
+
opacity var(--dur-fast) var(--ease-out);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/* The sheet has taken the map; nothing left down there to attribute. */
|
|
1434
|
+
.map-stage.chrome-clear :global(.maplibregl-ctrl-bottom-right) {
|
|
1435
|
+
opacity: 0;
|
|
1436
|
+
pointer-events: none;
|
|
1415
1437
|
}
|
|
1416
1438
|
|
|
1417
1439
|
.map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl) {
|
|
@@ -1439,6 +1461,26 @@
|
|
|
1439
1461
|
color: var(--text-secondary);
|
|
1440
1462
|
}
|
|
1441
1463
|
|
|
1464
|
+
@media (hover: none), (pointer: coarse) {
|
|
1465
|
+
/* A long press on the canvas must not raise the iOS callout or magnifier.
|
|
1466
|
+
Scoped to touch so a mouse can still select the attribution text. */
|
|
1467
|
+
.map {
|
|
1468
|
+
-webkit-touch-callout: none;
|
|
1469
|
+
-webkit-tap-highlight-color: transparent;
|
|
1470
|
+
user-select: none;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/* maplibre ships 29px zoom buttons; a fingertip wants 44. */
|
|
1474
|
+
.map :global(.maplibregl-ctrl-group button) {
|
|
1475
|
+
width: 44px;
|
|
1476
|
+
height: 44px;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.map :global(.maplibregl-ctrl-group button .maplibregl-ctrl-icon) {
|
|
1480
|
+
background-size: 22px 22px;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1442
1484
|
.map :global(.deck-tooltip) {
|
|
1443
1485
|
background: var(--panel) !important;
|
|
1444
1486
|
border: 1px solid var(--line);
|
|
@@ -1453,11 +1495,31 @@
|
|
|
1453
1495
|
|
|
1454
1496
|
@media (max-width: 760px) {
|
|
1455
1497
|
.map :global(.maplibregl-ctrl-attrib) {
|
|
1498
|
+
/* last in the column so it rests on the sheet's edge instead of
|
|
1499
|
+
floating over the network a zoom control's height above it */
|
|
1500
|
+
order: 1;
|
|
1456
1501
|
width: auto;
|
|
1457
1502
|
min-width: 0;
|
|
1458
1503
|
right: 0;
|
|
1459
1504
|
white-space: normal;
|
|
1460
1505
|
}
|
|
1506
|
+
|
|
1507
|
+
/* The band of map a phone has left between the header and the sheet is
|
|
1508
|
+
about the height of the solve card, which covers the buttons wherever
|
|
1509
|
+
they land. Pinch and double tap zoom; the attribution stays. */
|
|
1510
|
+
.map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl-group) {
|
|
1511
|
+
display: none;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/* One line on a small phone: wrapped attribution turns into a wide block
|
|
1516
|
+
sitting over the network. */
|
|
1517
|
+
@media (max-width: 460px) {
|
|
1518
|
+
.map :global(.maplibregl-ctrl-attrib) {
|
|
1519
|
+
max-width: calc(100vw - 20px);
|
|
1520
|
+
font-size: 9px;
|
|
1521
|
+
white-space: nowrap;
|
|
1522
|
+
}
|
|
1461
1523
|
}
|
|
1462
1524
|
|
|
1463
1525
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -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 {
|
|
@@ -109,11 +131,14 @@
|
|
|
109
131
|
<AppHeader />
|
|
110
132
|
<ControlPanel />
|
|
111
133
|
<SolveCard />
|
|
112
|
-
|
|
134
|
+
<!-- ControlPanel mounts the lookup and the footer inline when compact. -->
|
|
135
|
+
{#if !app.compactLayout}
|
|
136
|
+
<BusPicker />
|
|
137
|
+
{/if}
|
|
113
138
|
<DropZone />
|
|
114
139
|
<PlacementCue />
|
|
115
140
|
<RestoreDefaultsButton />
|
|
116
|
-
{#if config.showFooter}
|
|
141
|
+
{#if config.showFooter && !app.compactLayout}
|
|
117
142
|
<AppFooter />
|
|
118
143
|
{/if}
|
|
119
144
|
</main>
|
package/dist/api.d.ts
CHANGED
package/dist/colors.d.ts
CHANGED
|
@@ -48,4 +48,3 @@ export declare function branchWidth(loading: number): number;
|
|
|
48
48
|
/** Bus radius in px; area tracks max(load, gen) so big plants and big loads
|
|
49
49
|
* both read at a glance. Shared by the map layers and the size legend. */
|
|
50
50
|
export declare function busRadius(maxMw: number): number;
|
|
51
|
-
//# sourceMappingURL=colors.d.ts.map
|
|
@@ -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,6 @@
|
|
|
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
|
-
//# sourceMappingURL=AppFooter.svelte.d.ts.map
|
|
@@ -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" />
|
|
@@ -83,13 +97,15 @@
|
|
|
83
97
|
</span>
|
|
84
98
|
<input
|
|
85
99
|
type="file"
|
|
86
|
-
accept=".m,.raw,.aux,.dss,.pwd,.csv,.json,.geojson"
|
|
100
|
+
accept=".m,.raw,.aux,.epc,.pwb,.dss,.pwd,.csv,.json,.geojson"
|
|
87
101
|
multiple
|
|
88
102
|
hidden
|
|
89
103
|
bind:this={fileInput}
|
|
90
104
|
onchange={(e) => {
|
|
91
105
|
const input = e.currentTarget;
|
|
92
|
-
|
|
106
|
+
// Pass the FileList through unchanged: the controller checks its declared
|
|
107
|
+
// count before materializing any entries.
|
|
108
|
+
if (input.files) ctrl.ingestFiles(input.files);
|
|
93
109
|
input.value = '';
|
|
94
110
|
}}
|
|
95
111
|
/>
|
|
@@ -397,6 +413,50 @@
|
|
|
397
413
|
}
|
|
398
414
|
}
|
|
399
415
|
|
|
416
|
+
/* Landscape phone: height is the scarce axis. Keep the case tabs on the
|
|
417
|
+
brand's row instead of giving them one of their own, which halves the
|
|
418
|
+
header and leaves the map a band worth looking at. */
|
|
419
|
+
@media (max-width: 760px) and (max-height: 520px) {
|
|
420
|
+
header {
|
|
421
|
+
flex-wrap: nowrap;
|
|
422
|
+
align-items: center;
|
|
423
|
+
padding: 6px 10px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
h1 {
|
|
427
|
+
font-size: 17px;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.cases {
|
|
431
|
+
order: 0;
|
|
432
|
+
width: auto;
|
|
433
|
+
justify-content: flex-start;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.kicker {
|
|
437
|
+
display: none;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Touch sizing keys off the pointer, not the breakpoint: a tablet gets the
|
|
442
|
+
wide layout and still needs a fingertip's worth of chip. */
|
|
443
|
+
@media (hover: none), (pointer: coarse) {
|
|
444
|
+
.cases > button,
|
|
445
|
+
.case-chip {
|
|
446
|
+
min-height: 44px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.case-activate {
|
|
450
|
+
min-height: 44px;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.kicker a {
|
|
454
|
+
display: inline-flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
min-height: 44px;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
400
460
|
@media (prefers-reduced-motion: reduce) {
|
|
401
461
|
header {
|
|
402
462
|
animation: none;
|
|
@@ -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,6 @@
|
|
|
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
|
-
//# sourceMappingURL=BusPicker.svelte.d.ts.map
|