@tellegen/svelte 0.1.1 → 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.
@@ -495,11 +495,10 @@
495
495
  * The branch variant also clears the rating readout on the panel's lower edge. */
496
496
  function framePadding(width: number, height: number, kind: 'case' | 'branch') {
497
497
  if (width <= 760) {
498
- const bottom =
499
- kind === 'branch'
500
- ? Math.min(Math.round(height * 0.42), 300)
501
- : Math.min(Math.round(height * 0.5), 330);
502
- 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 };
503
502
  }
504
503
  return kind === 'branch'
505
504
  ? { top: 106, left: 388, right: 76, bottom: 80 }
@@ -552,6 +551,25 @@
552
551
  );
553
552
  }
554
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
+
555
573
  /** Fly to the selected branch; true when a camera move started. */
556
574
  function focusSelectedBranch(): boolean {
557
575
  if (!map || !selectedBranchCue) return false;
@@ -699,6 +717,7 @@
699
717
  // deck layers also inherit maplibre's 4096 canvas clamp. The flat positron
700
718
  // basemap writes no depth, so the overlay always draws on top.
701
719
  function buildOverlay(MapboxOverlay: MapModules['MapboxOverlay']): MapboxOverlay {
720
+ const touch = coarsePointer();
702
721
  return new MapboxOverlay({
703
722
  interleaved: true,
704
723
  layers: [],
@@ -706,10 +725,13 @@
706
725
  depthWriteEnabled: false,
707
726
  depthCompare: 'always'
708
727
  },
728
+ // a small bus draws ~9px across, well under a fingertip
729
+ pickingRadius: touch ? 12 : 0,
709
730
  onClick: (info: PickingInfo) => {
710
731
  if (!app.placingId && !isSelectableLayer(info.layer?.id)) onmapclick();
711
732
  },
712
- getTooltip: tooltip,
733
+ // a touch tooltip opens under the finger and stays until the next tap
734
+ getTooltip: touch ? undefined : tooltip,
713
735
  getCursor: ({ isHovering, isDragging }: CursorState) =>
714
736
  app.placingId ? 'crosshair' : isDragging ? 'grabbing' : isHovering ? 'pointer' : 'grab'
715
737
  });
@@ -1121,6 +1143,25 @@
1121
1143
  void tick().then(scheduleCueSync);
1122
1144
  });
1123
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
+
1124
1165
  function boundsFor(target: string): LngLatBoundsLike | null {
1125
1166
  const points: [number, number][] = [];
1126
1167
  const fold = (pts: { lon: number; lat: number }[]) => {
@@ -1176,7 +1217,11 @@
1176
1217
  </script>
1177
1218
 
1178
1219
  {#key mapGen}
1179
- <div class="map-stage">
1220
+ <div
1221
+ class="map-stage"
1222
+ class:chrome-clear={!chromeVisible}
1223
+ style="--chrome-inset: {chromeInset}px"
1224
+ >
1180
1225
  <div class="map" {@attach initMap}></div>
1181
1226
  {#if selectedBusCue}
1182
1227
  {#key selectedBusCue.key}
@@ -1203,6 +1248,7 @@
1203
1248
  <button
1204
1249
  type="button"
1205
1250
  class="place-at-center mono"
1251
+ style="--chrome-inset: {chromeInset}px"
1206
1252
  onclick={() => {
1207
1253
  if (!map) return;
1208
1254
  const center = map.getCenter();
@@ -1386,7 +1432,7 @@
1386
1432
  .place-at-center {
1387
1433
  position: absolute;
1388
1434
  left: 50%;
1389
- bottom: 92px;
1435
+ bottom: calc(92px + var(--chrome-inset, 0px));
1390
1436
  z-index: 15;
1391
1437
  transform: translateX(-50%);
1392
1438
  padding: 7px 14px;
@@ -1401,17 +1447,27 @@
1401
1447
 
1402
1448
  @media (max-width: 420px) {
1403
1449
  .place-at-center {
1404
- bottom: 78px;
1450
+ bottom: calc(78px + var(--chrome-inset, 0px));
1405
1451
  }
1406
1452
  }
1407
1453
 
1408
1454
  .map :global(.maplibregl-ctrl-bottom-right) {
1409
1455
  right: 20px;
1410
- bottom: 18px;
1456
+ /* rides above the bottom sheet so attribution stays legible */
1457
+ bottom: calc(18px + var(--chrome-inset, 0px));
1411
1458
  display: flex;
1412
1459
  flex-direction: column;
1413
1460
  align-items: flex-end;
1414
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;
1415
1471
  }
1416
1472
 
1417
1473
  .map :global(.maplibregl-ctrl-bottom-right .maplibregl-ctrl) {
@@ -1439,6 +1495,26 @@
1439
1495
  color: var(--text-secondary);
1440
1496
  }
1441
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
+
1442
1518
  .map :global(.deck-tooltip) {
1443
1519
  background: var(--panel) !important;
1444
1520
  border: 1px solid var(--line);
@@ -1453,11 +1529,31 @@
1453
1529
 
1454
1530
  @media (max-width: 760px) {
1455
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;
1456
1535
  width: auto;
1457
1536
  min-width: 0;
1458
1537
  right: 0;
1459
1538
  white-space: normal;
1460
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
+ }
1461
1557
  }
1462
1558
 
1463
1559
  @media (prefers-reduced-motion: reduce) {
@@ -1 +1 @@
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;AAsoCH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,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
- return () => query.removeEventListener('change', syncFileDropUi);
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
- <BusPicker />
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>
@@ -1 +1 @@
1
- {"version":3,"file":"TellegenShell.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenShell.svelte.ts"],"names":[],"mappings":"AA4HA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
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
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
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":"AAyBA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,SAAS;;kBAA+E,CAAC;AAC7E,KAAK,SAAS,GAAG,YAAY,CAAC,OAAO,SAAS,CAAC,CAAC;AAClD,eAAe,SAAS,CAAC"}
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":"AA+EA,QAAA,MAAM,SAAS,2DAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
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
- declare const BusPicker: import("svelte").Component<Record<string, never>, {}, "">;
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":"AA0GA,QAAA,MAAM,SAAS,2DAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
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"}
@@ -1,6 +1,10 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte';
2
3
  import { getAppState, getController, getUiConfig } from '../context.svelte.js';
4
+ import { splitName } from '../format.js';
5
+ import AppFooter from './AppFooter.svelte';
3
6
  import BindingLines from './BindingLines.svelte';
7
+ import BusPicker from './BusPicker.svelte';
4
8
  import DemandSlider from './DemandSlider.svelte';
5
9
  import DisplayControls from './DisplayControls.svelte';
6
10
  import FormulationSelector from './FormulationSelector.svelte';
@@ -15,81 +19,297 @@
15
19
  const app = getAppState();
16
20
  const ctrl = getController();
17
21
  const config = getUiConfig();
22
+
23
+ // Bottom sheet heights, compact layout only. peek is measured from the grab
24
+ // bar; half and full are fractions of the viewport.
25
+ type Snap = 'peek' | 'half' | 'full';
26
+ const SNAPS: Snap[] = ['peek', 'half', 'full'];
27
+ const SNAP_FRACTION: Record<Snap, number> = { peek: 0, half: 0.46, full: 0.92 };
28
+ /** Below this the viewport is a landscape phone and half has to give up room. */
29
+ const SHORT_VIEWPORT_PX = 560;
30
+ /** px/ms above which a release snaps in the drag direction instead of to the nearest. */
31
+ const FLICK_SPEED = 0.35;
32
+ /** Map left visible above a full sheet, on top of the header it also clears. */
33
+ const FULL_SNAP_MAP_BAND = 14;
34
+
35
+ let snap = $state<Snap>('half');
36
+ let headEl = $state.raw<HTMLElement | undefined>(undefined);
37
+ let bodyEl = $state.raw<HTMLElement | undefined>(undefined);
38
+ let headHeight = $state(52);
39
+ /** Height under an active drag; null when resting on a snap. */
40
+ let dragHeight = $state<number | null>(null);
41
+
42
+ const fractionFor = (s: Snap) =>
43
+ s === 'half' && app.viewportHeight < SHORT_VIEWPORT_PX ? 0.38 : SNAP_FRACTION[s];
44
+
45
+ const snapHeight = (s: Snap) =>
46
+ s === 'peek'
47
+ ? headHeight
48
+ : Math.min(
49
+ Math.round(app.viewportHeight * fractionFor(s)),
50
+ app.viewportHeight - app.headerInset - FULL_SNAP_MAP_BAND
51
+ );
52
+ const sheetHeight = $derived(dragHeight ?? snapHeight(snap));
53
+
54
+ const caseLabel = $derived(
55
+ app.active
56
+ ? splitName(app.active.name)[0]
57
+ : (app.activeLocal?.label ?? app.activeMulti?.label ?? '')
58
+ );
59
+
60
+ /** Single line shown on the grab bar while collapsed. */
61
+ const summary = $derived.by(() => {
62
+ if (app.error) return 'error — open for details';
63
+ if (app.selectedBus !== null) return `bus ${app.selectedBus} · ∂LMP/∂d`;
64
+ if (app.selectedBranch !== null) return `line ${app.selectedBranch} · ∂LMP/∂rating`;
65
+ const stats = ctrl.networkStats;
66
+ if (!stats) return app.parsingFile ? 'parsing…' : 'loading…';
67
+ return `${caseLabel} · ${stats.buses} buses · ${stats.branches} lines`;
68
+ });
69
+
70
+ const expandLabel = $derived(
71
+ snap === 'full' ? 'collapse the control panel' : 'expand the control panel'
72
+ );
73
+
74
+ /** A selection whose readout has resolved (or is resolving). */
75
+ const busReadout = $derived(
76
+ app.selectedBus !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)
77
+ );
78
+ const branchReadout = $derived(
79
+ app.selectedBranch !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)
80
+ );
81
+ /** Compact only: a resolved selection outranks the case stats it was read
82
+ * against. Gated on networkStats because that is what renders the readout. */
83
+ const selectionLeads = $derived(
84
+ app.compactLayout && !!ctrl.networkStats && (busReadout || branchReadout)
85
+ );
86
+
87
+ $effect(() => {
88
+ app.sheetInset = app.compactLayout ? sheetHeight : 0;
89
+ });
90
+
91
+ // peek height tracks the grab bar, which resizes when the summary line changes.
92
+ $effect(() => {
93
+ const el = headEl;
94
+ if (!el) return;
95
+ const measure = () => (headHeight = Math.round(el.getBoundingClientRect().height));
96
+ measure();
97
+ const observer = new ResizeObserver(measure);
98
+ observer.observe(el);
99
+ return () => observer.disconnect();
100
+ });
101
+
102
+ // A map selection renders the sensitivity readout and the demand control, both
103
+ // of which are below the fold at peek. The body also has to go back to the top:
104
+ // reaching the bus lookup scrolls it down, and the readout that replaces it as
105
+ // the first block would otherwise open out of view.
106
+ $effect(() => {
107
+ const selected = app.selectedBus ?? app.selectedBranch;
108
+ if (selected === null) return;
109
+ untrack(() => {
110
+ if (!app.compactLayout) return;
111
+ if (snap === 'peek') snap = 'half';
112
+ if (bodyEl) bodyEl.scrollTop = 0;
113
+ });
114
+ });
115
+
116
+ function cycle() {
117
+ snap = snap === 'peek' ? 'half' : snap === 'half' ? 'full' : 'peek';
118
+ }
119
+
120
+ function nearestSnap(height: number): Snap {
121
+ return SNAPS.reduce((best, s) =>
122
+ Math.abs(snapHeight(s) - height) < Math.abs(snapHeight(best) - height) ? s : best
123
+ );
124
+ }
125
+
126
+ let dragStartY = 0;
127
+ let dragStartHeight = 0;
128
+ let dragMoved = false;
129
+ let lastY = 0;
130
+ let lastT = 0;
131
+ let speed = 0;
132
+
133
+ function onPointerDown(e: PointerEvent) {
134
+ if (!app.compactLayout || !e.isPrimary) return;
135
+ if (e.currentTarget instanceof HTMLElement) e.currentTarget.setPointerCapture(e.pointerId);
136
+ dragStartY = e.clientY;
137
+ dragStartHeight = sheetHeight;
138
+ dragHeight = dragStartHeight;
139
+ dragMoved = false;
140
+ speed = 0;
141
+ lastY = e.clientY;
142
+ lastT = e.timeStamp;
143
+ }
144
+
145
+ function onPointerMove(e: PointerEvent) {
146
+ if (dragHeight === null) return;
147
+ const dy = dragStartY - e.clientY;
148
+ if (Math.abs(dy) > 4) dragMoved = true;
149
+ const dt = e.timeStamp - lastT;
150
+ if (dt > 0) speed = (lastY - e.clientY) / dt;
151
+ lastY = e.clientY;
152
+ lastT = e.timeStamp;
153
+ dragHeight = Math.max(
154
+ snapHeight('peek'),
155
+ Math.min(snapHeight('full'), dragStartHeight + dy)
156
+ );
157
+ }
158
+
159
+ function onPointerUp() {
160
+ if (dragHeight === null) return;
161
+ const height = dragHeight;
162
+ dragHeight = null;
163
+ if (!dragMoved) return; // a tap; the click handler cycles
164
+ const landed = nearestSnap(height);
165
+ if (Math.abs(speed) > FLICK_SPEED) {
166
+ const next = SNAPS.indexOf(landed) + (speed > 0 ? 1 : -1);
167
+ snap = SNAPS[Math.max(0, Math.min(SNAPS.length - 1, next))];
168
+ } else {
169
+ snap = landed;
170
+ }
171
+ }
172
+
173
+ function onHeadKeydown(e: KeyboardEvent) {
174
+ const step = e.key === 'ArrowUp' ? 1 : e.key === 'ArrowDown' ? -1 : 0;
175
+ if (step === 0) return;
176
+ e.preventDefault();
177
+ const next = SNAPS.indexOf(snap) + step;
178
+ snap = SNAPS[Math.max(0, Math.min(SNAPS.length - 1, next))];
179
+ }
18
180
  </script>
19
181
 
20
- <aside class="panel">
21
- {#if app.error}
22
- <p class="error mono">{app.error}</p>
23
- <div class="error-actions">
24
- <button class="reset mono" onclick={ctrl.retryError}>retry</button>
25
- <button class="reset mono" onclick={() => (app.error = null)}>dismiss</button>
26
- </div>
27
- {/if}
28
- {#if app.parsingFile}
29
- <p class="dim mono blink">parsing&hellip;</p>
30
- {/if}
31
- {#if app.activeLocal}
32
- <LocalCaseDetails />
33
- {/if}
34
- {#if app.activeMulti}
35
- <MulticonductorDetails />
182
+ <aside
183
+ class="panel"
184
+ class:sheet={app.compactLayout}
185
+ class:dragging={dragHeight !== null}
186
+ style:height={app.compactLayout ? `${sheetHeight}px` : null}
187
+ >
188
+ {#if app.compactLayout}
189
+ <button
190
+ type="button"
191
+ class="sheet-head"
192
+ bind:this={headEl}
193
+ aria-expanded={snap !== 'peek'}
194
+ aria-controls="control-panel-body"
195
+ aria-label={expandLabel}
196
+ onpointerdown={onPointerDown}
197
+ onpointermove={onPointerMove}
198
+ onpointerup={onPointerUp}
199
+ onpointercancel={onPointerUp}
200
+ onkeydown={onHeadKeydown}
201
+ onclick={() => {
202
+ // click fires after pointerup; a keyboard activation leaves dragMoved
203
+ // unset, so clear it here rather than in the pointer handlers
204
+ const moved = dragMoved;
205
+ dragMoved = false;
206
+ if (!moved) cycle();
207
+ }}
208
+ >
209
+ <span class="grabber" aria-hidden="true"></span>
210
+ {#if snap === 'peek'}
211
+ <span class="sheet-summary mono">{summary}</span>
212
+ {/if}
213
+ </button>
36
214
  {/if}
37
- {#if !ctrl.networkStats}
38
- {#if !app.error && !app.activeLocal && !app.activeMulti}
39
- {#if ctrl.casesLoaded && app.cases.length === 0}
40
- <p class="dim mono">
41
- {config.loadDefaultCases ? 'no default cases loaded' : 'drop a case file to begin'}
42
- </p>
43
- {#if config.loadDefaultCases}
44
- <button class="reset mono" onclick={ctrl.restoreDefaultCases}
45
- >restore default cases</button
46
- >
215
+
216
+ <div class="panel-body" id="control-panel-body" bind:this={bodyEl}>
217
+ {#snippet selectionBlock()}
218
+ {#if busReadout}
219
+ <SensitivityReadout />
220
+
221
+ <DemandSlider />
222
+
223
+ {#if ctrl.showMoverSlot}
224
+ <TopMovers />
225
+ {/if}
226
+ {:else if branchReadout}
227
+ <SensitivityReadout />
228
+
229
+ <RatingSlider />
230
+
231
+ {#if ctrl.showMoverSlot}
232
+ <TopMovers />
47
233
  {/if}
48
- {:else if ctrl.loadingBackendCase}
49
- <p class="dim mono blink">loading selected case&hellip;</p>
50
234
  {:else}
51
- <p class="dim mono blink">loading cases&hellip;</p>
235
+ <DisplayControls />
52
236
  {/if}
237
+ {/snippet}
238
+
239
+ {#if app.error}
240
+ <p class="error mono">{app.error}</p>
241
+ <div class="error-actions">
242
+ <button class="reset mono" onclick={ctrl.retryError}>retry</button>
243
+ <button class="reset mono" onclick={() => (app.error = null)}>dismiss</button>
244
+ </div>
53
245
  {/if}
54
- {:else}
55
- {#if !app.activeLocal}
56
- <NetworkStats />
246
+ {#if app.parsingFile}
247
+ <p class="dim mono blink">parsing&hellip;</p>
57
248
  {/if}
58
249
 
59
- {#if ctrl.activeSolvable}
60
- <FormulationSelector />
61
- {/if}
250
+ <!-- The sheet shows a few hundred px at most, so what the last tap produced
251
+ leads; the case provenance and stats it was read against follow. -->
252
+ {#if selectionLeads}
253
+ {@render selectionBlock()}
62
254
 
63
- {#if !app.placingLocalId}
64
- <BindingLines />
255
+ <hr />
65
256
  {/if}
66
257
 
67
- <hr />
258
+ {#if app.activeLocal}
259
+ <LocalCaseDetails />
260
+ {/if}
261
+ {#if app.activeMulti}
262
+ <MulticonductorDetails />
263
+ {/if}
264
+ {#if !ctrl.networkStats}
265
+ {#if !app.error && !app.activeLocal && !app.activeMulti}
266
+ {#if ctrl.casesLoaded && app.cases.length === 0}
267
+ <p class="dim mono">
268
+ {config.loadDefaultCases ? 'no default cases loaded' : 'drop a case file to begin'}
269
+ </p>
270
+ {#if config.loadDefaultCases}
271
+ <button class="reset mono" onclick={ctrl.restoreDefaultCases}
272
+ >restore default cases</button
273
+ >
274
+ {/if}
275
+ {:else if ctrl.loadingBackendCase}
276
+ <p class="dim mono blink">loading selected case&hellip;</p>
277
+ {:else}
278
+ <p class="dim mono blink">loading cases&hellip;</p>
279
+ {/if}
280
+ {/if}
281
+ {:else}
282
+ {#if !app.activeLocal}
283
+ <NetworkStats />
284
+ {/if}
68
285
 
69
- {#if app.selectedBus !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)}
70
- <SensitivityReadout />
286
+ {#if ctrl.activeSolvable}
287
+ <FormulationSelector />
288
+ {/if}
71
289
 
72
- <DemandSlider />
290
+ {#if app.compactLayout}
291
+ <BusPicker inline />
292
+ {/if}
73
293
 
74
- {#if ctrl.showMoverSlot}
75
- <TopMovers />
294
+ {#if !app.placingLocalId}
295
+ <BindingLines />
76
296
  {/if}
77
- {:else if app.selectedBranch !== null && (ctrl.selectedSensitivity || app.sensitivityLoading)}
78
- <SensitivityReadout />
79
297
 
80
- <RatingSlider />
298
+ {#if !selectionLeads}
299
+ <hr />
81
300
 
82
- {#if ctrl.showMoverSlot}
83
- <TopMovers />
301
+ {@render selectionBlock()}
84
302
  {/if}
85
- {:else}
86
- <DisplayControls />
87
- {/if}
88
303
 
89
- <hr />
304
+ <hr />
90
305
 
91
- <SizeLegend />
92
- {/if}
306
+ <SizeLegend />
307
+ {/if}
308
+
309
+ {#if app.compactLayout && config.showFooter}
310
+ <AppFooter inline />
311
+ {/if}
312
+ </div>
93
313
  </aside>
94
314
 
95
315
  <style>
@@ -98,10 +318,11 @@
98
318
  top: 76px;
99
319
  left: 20px;
100
320
  z-index: 10;
321
+ display: flex;
322
+ flex-direction: column;
101
323
  width: 312px;
102
324
  max-height: calc(100% - 122px);
103
- overflow-y: auto;
104
- padding: 16px 18px;
325
+ overflow: hidden;
105
326
  background: var(--panel);
106
327
  border: 1px solid var(--line);
107
328
  border-radius: 3px;
@@ -110,6 +331,16 @@
110
331
  animation: rise 0.5s 0.12s ease-out both;
111
332
  }
112
333
 
334
+ .panel-body {
335
+ flex: 1 1 auto;
336
+ min-height: 0;
337
+ overflow-y: auto;
338
+ /* A flick that runs past the end of the panel must not scroll the page or
339
+ pan the map underneath. */
340
+ overscroll-behavior: contain;
341
+ padding: 16px 18px;
342
+ }
343
+
113
344
  hr {
114
345
  border: 0;
115
346
  border-top: 1px solid var(--line);
@@ -126,24 +357,80 @@
126
357
  gap: 6px;
127
358
  }
128
359
 
129
- @media (max-width: 760px) {
130
- .panel {
131
- top: auto;
132
- left: 10px;
133
- right: 10px;
134
- bottom: 40px;
135
- width: auto;
136
- max-height: 44dvh;
137
- padding: 14px 16px;
138
- background: var(--paper);
139
- }
360
+ /* The grab bar: a 44px strip that drags the sheet and cycles it on tap. */
361
+ .sheet-head {
362
+ flex: 0 0 auto;
363
+ display: flex;
364
+ flex-direction: column;
365
+ align-items: center;
366
+ justify-content: center;
367
+ gap: 7px;
368
+ width: 100%;
369
+ min-height: 44px;
370
+ padding: 9px 16px 11px;
371
+ background: none;
372
+ border: 0;
373
+ border-bottom: 1px solid transparent;
374
+ color: inherit;
375
+ font: inherit;
376
+ cursor: grab;
377
+ /* the browser must not claim the vertical gesture */
378
+ touch-action: none;
140
379
  }
141
380
 
142
- @media (max-width: 420px) {
143
- .panel {
144
- bottom: 34px;
145
- max-height: 46dvh;
146
- }
381
+ .sheet-head:active {
382
+ cursor: grabbing;
383
+ }
384
+
385
+ .grabber {
386
+ width: 40px;
387
+ height: 4px;
388
+ border-radius: 2px;
389
+ background: var(--line);
390
+ }
391
+
392
+ .sheet-head:hover .grabber,
393
+ .sheet-head:focus-visible .grabber {
394
+ background: var(--accent);
395
+ }
396
+
397
+ .sheet-summary {
398
+ max-width: 100%;
399
+ overflow: hidden;
400
+ font-size: 11.5px;
401
+ color: var(--text-secondary);
402
+ text-overflow: ellipsis;
403
+ white-space: nowrap;
404
+ }
405
+
406
+ .sheet {
407
+ top: auto;
408
+ right: 0;
409
+ bottom: 0;
410
+ left: 0;
411
+ width: auto;
412
+ max-height: none;
413
+ background: var(--paper);
414
+ border-width: 1px 0 0;
415
+ border-radius: 12px 12px 0 0;
416
+ box-shadow: 0 -6px 28px rgba(32, 36, 43, 0.14);
417
+ transition: height var(--dur-med) var(--ease-out);
418
+ }
419
+
420
+ .sheet .panel-body {
421
+ padding: 0 16px 14px;
422
+ /* clear the home indicator on a phone with no bezel */
423
+ padding-bottom: max(14px, env(safe-area-inset-bottom));
424
+ -webkit-overflow-scrolling: touch;
425
+ }
426
+
427
+ .sheet .sheet-head {
428
+ border-bottom-color: var(--line);
429
+ }
430
+
431
+ /* A dragged sheet tracks the finger; the snap animation is for releases. */
432
+ .sheet.dragging {
433
+ transition: none;
147
434
  }
148
435
 
149
436
  @media (prefers-reduced-motion: reduce) {
@@ -1,19 +1,4 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const ControlPanel: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type ControlPanel = InstanceType<typeof ControlPanel>;
1
+ declare const ControlPanel: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ControlPanel = ReturnType<typeof ControlPanel>;
18
3
  export default ControlPanel;
19
4
  //# sourceMappingURL=ControlPanel.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ControlPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ControlPanel.svelte.ts"],"names":[],"mappings":"AAgHA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,YAAY;;kBAA+E,CAAC;AAChF,KAAK,YAAY,GAAG,YAAY,CAAC,OAAO,YAAY,CAAC,CAAC;AACxD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"ControlPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ControlPanel.svelte.ts"],"names":[],"mappings":"AA6TA,QAAA,MAAM,YAAY,2DAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -79,4 +79,21 @@
79
79
  opacity: 0.55;
80
80
  cursor: progress;
81
81
  }
82
+
83
+ @media (hover: none), (pointer: coarse) {
84
+ /* The select is sized to its longest option, which runs off a 320px screen.
85
+ Let it shrink around the current value rather than truncate the label.
86
+ 16px also keeps Safari from zooming the layout viewport on focus. */
87
+ .formulation-row select {
88
+ flex: 0 1 auto;
89
+ min-width: 0;
90
+ min-height: 44px;
91
+ font-size: 16px;
92
+ padding-block: 6px;
93
+ }
94
+
95
+ .formulation-row > span {
96
+ flex: 0 0 auto;
97
+ }
98
+ }
82
99
  </style>
@@ -40,7 +40,9 @@
40
40
  <span class="mono dim">
41
41
  {#if branchMode}line {branchLabel}{:else}bus {app.selectedBus}{/if}
42
42
  </span>
43
- <button class="mono" onclick={ctrl.clearSelection}>esc&nbsp;clear</button>
43
+ <button class="mono" onclick={ctrl.clearSelection}
44
+ ><span class="key-hint">esc&nbsp;</span>clear</button
45
+ >
44
46
  </div>
45
47
  <div class="sensitivity-readout" aria-live="polite">
46
48
  {#if ctrl.previewing}
@@ -123,4 +125,11 @@
123
125
  line-height: 1.35;
124
126
  white-space: nowrap;
125
127
  }
128
+
129
+ /* No Esc key to name on a touch device. */
130
+ @media (hover: none), (pointer: coarse) {
131
+ .key-hint {
132
+ display: none;
133
+ }
134
+ }
126
135
  </style>
@@ -75,7 +75,8 @@
75
75
 
76
76
  @media (max-width: 760px) {
77
77
  .solvecard {
78
- top: 184px;
78
+ /* just clear of the header's case tabs */
79
+ top: 108px;
79
80
  left: auto;
80
81
  right: 10px;
81
82
  width: min(300px, calc(100% - 20px));
@@ -234,6 +234,19 @@ export declare class AppState {
234
234
  placingMultiId: string | null;
235
235
  dragOver: boolean;
236
236
  parsingFile: boolean;
237
+ /** True under `(max-width: 760px)`; the panel renders as a bottom sheet. Set by
238
+ * the shell from one media query so panel and map read the same value. */
239
+ compactLayout: boolean;
240
+ /** Height in px the bottom sheet covers; 0 when `compactLayout` is false.
241
+ * Read by the map's camera padding and by chrome anchored above the sheet. */
242
+ sheetInset: number;
243
+ /** Height in px the header covers at the top of the map, measured by
244
+ * `AppHeader`. Wrapping the case tabs onto their own row roughly doubles it,
245
+ * so callers that need to clear the header read this rather than a constant. */
246
+ headerInset: number;
247
+ /** `window.innerHeight`, tracked by the shell. The sheet's snaps and the map's
248
+ * chrome lift are both fractions of it; the fallback is a desktop window. */
249
+ viewportHeight: number;
237
250
  /** Map framing request: bump seq so repeat targets still fly. `requestFrame`
238
251
  * returns a promise the map resolves when the camera lands (or immediately
239
252
  * when it cannot fly), so a caller can defer heavy work until the animation
@@ -1 +1 @@
1
- {"version":3,"file":"state.svelte.d.ts","sourceRoot":"","sources":["../src/lib/state.svelte.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"state.svelte.d.ts","sourceRoot":"","sources":["../src/lib/state.svelte.ts"],"names":[],"mappings":"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"}
@@ -200,6 +200,19 @@ export class AppState {
200
200
  placingMultiId = $state(null);
201
201
  dragOver = $state(false);
202
202
  parsingFile = $state(false);
203
+ /** True under `(max-width: 760px)`; the panel renders as a bottom sheet. Set by
204
+ * the shell from one media query so panel and map read the same value. */
205
+ compactLayout = $state(false);
206
+ /** Height in px the bottom sheet covers; 0 when `compactLayout` is false.
207
+ * Read by the map's camera padding and by chrome anchored above the sheet. */
208
+ sheetInset = $state(0);
209
+ /** Height in px the header covers at the top of the map, measured by
210
+ * `AppHeader`. Wrapping the case tabs onto their own row roughly doubles it,
211
+ * so callers that need to clear the header read this rather than a constant. */
212
+ headerInset = $state(64);
213
+ /** `window.innerHeight`, tracked by the shell. The sheet's snaps and the map's
214
+ * chrome lift are both fractions of it; the fallback is a desktop window. */
215
+ viewportHeight = $state(800);
203
216
  /** Map framing request: bump seq so repeat targets still fly. `requestFrame`
204
217
  * returns a promise the map resolves when the camera lands (or immediately
205
218
  * when it cannot fly), so a caller can defer heavy work until the animation
package/dist/styles.css CHANGED
@@ -372,11 +372,25 @@ a {
372
372
  /* Coarse pointers: comfortable hit targets for the toggle and reset controls. */
373
373
  @media (hover: none), (pointer: coarse) {
374
374
  .segment button {
375
- min-height: 36px;
375
+ min-height: 44px;
376
376
  }
377
377
 
378
378
  .reset {
379
- min-height: 36px;
379
+ min-height: 44px;
380
+ }
381
+
382
+ /* Keep the chip visually small next to its short siblings; the ::before pads
383
+ the tap target out to 44px, as .case-remove does in AppHeader. */
384
+ .mode > button {
385
+ position: relative;
386
+ min-height: 28px;
387
+ padding-inline: 10px;
388
+ }
389
+
390
+ .mode > button::before {
391
+ content: '';
392
+ position: absolute;
393
+ inset: -8px -4px;
380
394
  }
381
395
  }
382
396
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellegen/svelte",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Svelte components for tellegen power system maps, local case files, and browser solves.",
5
5
  "license": "MIT",
6
6
  "type": "module",