ts-maps 0.3.1 → 0.3.3

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.
Files changed (57) hide show
  1. package/dist/core-map/control/FullscreenControl.d.ts +39 -0
  2. package/dist/core-map/control/GeocoderControl.d.ts +61 -0
  3. package/dist/core-map/control/LocateControl.d.ts +56 -0
  4. package/dist/core-map/control/NavigationControl.d.ts +39 -0
  5. package/dist/core-map/control/index.d.ts +8 -0
  6. package/dist/core-map/game/TerritoryLog.d.ts +66 -0
  7. package/dist/core-map/game/TerritoryStore.d.ts +59 -0
  8. package/dist/core-map/game/index.d.ts +11 -0
  9. package/dist/core-map/game/loop.d.ts +61 -0
  10. package/dist/core-map/geo/area.d.ts +52 -0
  11. package/dist/core-map/geo/index.d.ts +30 -0
  12. package/dist/core-map/geo/index.js +856 -31
  13. package/dist/core-map/geo/polygonClip.d.ts +75 -0
  14. package/dist/core-map/geo/validate.d.ts +54 -0
  15. package/dist/core-map/geometry/index.js +28 -28
  16. package/dist/core-map/index.d.ts +8 -2
  17. package/dist/core-map/layer/GeoJSONTileSource.d.ts +50 -0
  18. package/dist/core-map/layer/RunTrailLayer.d.ts +35 -0
  19. package/dist/core-map/layer/TerritoryLayer.d.ts +62 -0
  20. package/dist/core-map/layer/index.d.ts +6 -0
  21. package/dist/core-map/layer/tile/RasterDEMLayer.d.ts +4 -0
  22. package/dist/core-map/layer/tile/VectorTileMapLayer.d.ts +59 -11
  23. package/dist/core-map/layer/tile/urlTemplate.d.ts +1 -2
  24. package/dist/core-map/map/Map.d.ts +32 -1
  25. package/dist/core-map/mvt/DecodedTile.d.ts +20 -0
  26. package/dist/core-map/mvt/FlatTile.d.ts +25 -0
  27. package/dist/core-map/mvt/VectorTileFeature.d.ts +8 -0
  28. package/dist/core-map/services/index.js +48 -48
  29. package/dist/core-map/storage/index.js +5 -5
  30. package/dist/core-map/style-spec/expressions/formatted.d.ts +36 -0
  31. package/dist/core-map/style-spec/expressions/operators/binding.d.ts +1 -0
  32. package/dist/core-map/style-spec/expressions/operators/geometry.d.ts +19 -0
  33. package/dist/core-map/style-spec/expressions/types.d.ts +3 -0
  34. package/dist/core-map/style-spec/index.js +460 -60
  35. package/dist/core-map/style-spec/schema.d.ts +43 -3
  36. package/dist/core-map/style-spec/types.d.ts +47 -7
  37. package/dist/core-map/styles/basemap.d.ts +41 -0
  38. package/dist/core-map/styles/index.d.ts +5 -0
  39. package/dist/core-map/styles/palette.d.ts +31 -0
  40. package/dist/core-map/symbols/CollisionIndex.d.ts +22 -5
  41. package/dist/core-map/symbols/GlyphAtlas.d.ts +6 -1
  42. package/dist/core-map/symbols/GlyphProvider.d.ts +21 -0
  43. package/dist/core-map/symbols/GlyphRenderer.d.ts +48 -0
  44. package/dist/core-map/symbols/IconAtlas.d.ts +10 -1
  45. package/dist/core-map/symbols/SymbolOverlay.d.ts +47 -0
  46. package/dist/core-map/symbols/index.d.ts +4 -0
  47. package/dist/core-map/symbols/index.js +1072 -68
  48. package/dist/core-map/symbols/loadGlyphs.d.ts +53 -0
  49. package/dist/core-map/symbols/loadSprite.d.ts +57 -0
  50. package/dist/core-map/symbols/placement.d.ts +67 -0
  51. package/dist/core-map/symbols/sdf.d.ts +37 -0
  52. package/dist/core-map/symbols/sections.d.ts +32 -0
  53. package/dist/core-map/workers/WorkerPool.d.ts +40 -0
  54. package/dist/core-map/workers/decodeMvtFlat.d.ts +52 -0
  55. package/dist/index.js +11783 -6100
  56. package/package.json +2 -2
  57. package/src/core-map/ts-maps.css +576 -79
@@ -116,6 +116,10 @@
116
116
  z-index: 200;
117
117
  }
118
118
 
119
+ .tsmap-symbol-pane {
120
+ z-index: 350;
121
+ }
122
+
119
123
  .tsmap-overlay-pane {
120
124
  z-index: 400;
121
125
  }
@@ -293,28 +297,91 @@ svg.tsmap-image-layer.tsmap-interactive path {
293
297
 
294
298
  /* visual tweaks */
295
299
 
300
+ /*
301
+ * Theme tokens.
302
+ *
303
+ * Every colour the chrome draws with resolves through one of these, so a dark
304
+ * map is a single class flip rather than a parallel stylesheet. They live on
305
+ * the container and not on :root deliberately: two maps on one page can then
306
+ * carry different themes, and a host page's own custom properties can't
307
+ * collide with them.
308
+ *
309
+ * The switch is `.tsmap-dark`, set by the map's `theme` option (see
310
+ * Map.setTheme). It is a class and not a bare `prefers-color-scheme` query
311
+ * because the map's theme should follow the basemap being displayed, which is
312
+ * an application decision — a dark style on a machine set to light mode still
313
+ * needs dark chrome. `theme: 'auto'` is what re-attaches it to the OS setting.
314
+ */
296
315
  .tsmap-container {
297
- background: #ddd;
316
+ --tsmap-accent: #1a73e8;
317
+ --tsmap-surface: #fff;
318
+ --tsmap-surface-hover: #f1f3f4;
319
+ --tsmap-fg: #3c4043;
320
+ --tsmap-fg-muted: #5f6368;
321
+ --tsmap-fg-disabled: #bdc1c6;
322
+ --tsmap-divider: rgb(0 0 0 / 12%);
323
+ --tsmap-scrim: rgb(255 255 255 / 80%);
324
+ --tsmap-hairline: #ddd;
325
+ --tsmap-scale-line: #777;
326
+ --tsmap-shadow:
327
+ 0 1px 2px rgb(60 64 67 / 30%),
328
+ 0 2px 6px 2px rgb(60 64 67 / 15%);
329
+ --tsmap-shadow-lg:
330
+ 0 2px 6px rgb(60 64 67 / 28%),
331
+ 0 6px 18px 4px rgb(60 64 67 / 15%);
332
+
333
+ /* Matched to the land colour of a light basemap, so tiles fade in against
334
+ their own background instead of flashing a dark band on every pan. */
335
+ --tsmap-tile-bg: #e8eaed;
336
+
337
+ background: var(--tsmap-tile-bg);
298
338
  outline-offset: 1px;
299
339
 
300
340
  a {
301
- color: #0078a8;
341
+ color: var(--tsmap-accent);
302
342
  }
303
343
  }
304
344
 
345
+ .tsmap-container.tsmap-dark {
346
+ --tsmap-accent: #8ab4f8;
347
+ --tsmap-surface: #292a2d;
348
+ --tsmap-surface-hover: #35363a;
349
+ --tsmap-fg: #e8eaed;
350
+ --tsmap-fg-muted: #9aa0a6;
351
+ --tsmap-fg-disabled: #5f6368;
352
+ --tsmap-divider: rgb(255 255 255 / 14%);
353
+ --tsmap-scrim: rgb(32 33 36 / 80%);
354
+ --tsmap-hairline: #3c4043;
355
+ --tsmap-scale-line: #9aa0a6;
356
+ --tsmap-shadow:
357
+ 0 1px 2px rgb(0 0 0 / 50%),
358
+ 0 2px 6px 2px rgb(0 0 0 / 30%);
359
+ --tsmap-shadow-lg:
360
+ 0 2px 6px rgb(0 0 0 / 45%),
361
+ 0 6px 18px 4px rgb(0 0 0 / 30%);
362
+ --tsmap-tile-bg: #16171a;
363
+ }
364
+
305
365
  /* prevent showing outline-box on Chromium when clicking on a vector with a tooltip */
306
366
  path.tsmap-interactive:focus:not(:focus-visible) {
307
367
  outline: 0;
308
368
  }
309
369
 
310
370
  .tsmap-zoom-box {
311
- border: 2px dotted #38f;
312
- background: rgba(255, 255, 255, 0.5);
371
+ border: 2px dotted var(--tsmap-accent);
372
+ background: var(--tsmap-scrim);
313
373
  }
314
374
 
315
375
  /* general typography */
316
376
  .tsmap-container {
317
- font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
377
+ font-family:
378
+ -apple-system,
379
+ BlinkMacSystemFont,
380
+ 'Segoe UI',
381
+ Roboto,
382
+ 'Helvetica Neue',
383
+ Arial,
384
+ sans-serif;
318
385
  font-size: 12px;
319
386
  font-size: 0.75rem;
320
387
  line-height: 1.5;
@@ -322,43 +389,85 @@ path.tsmap-interactive:focus:not(:focus-visible) {
322
389
 
323
390
  /* general toolbar styles */
324
391
 
392
+ /*
393
+ * Map chrome, styled the way a modern map is expected to look.
394
+ *
395
+ * What this replaces was inherited from Leaflet's 2011 defaults: 26px targets
396
+ * (well under the 44px every touch guideline asks for), a 4px radius, and
397
+ * `0 1px 5px rgba(0,0,0,0.65)` — a shadow at 65% black, which over a light
398
+ * basemap reads as a smudge rather than elevation.
399
+ *
400
+ * The values here follow the conventions every major map now shares: a white
401
+ * card, an 8px radius, a soft two-part shadow (a tight contact edge plus a
402
+ * wide ambient falloff), hairline dividers instead of solid #ccc borders, and
403
+ * the system UI font rather than Lucida Console. Controls also gain a real
404
+ * focus ring, which the old chrome suppressed entirely.
405
+ */
325
406
  .tsmap-bar {
326
- box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
327
- border-radius: 4px;
407
+ box-shadow: var(--tsmap-shadow);
408
+ border-radius: 8px;
409
+ overflow: hidden;
328
410
 
329
411
  a {
330
- background-color: #fff;
331
- border-bottom: 1px solid #ccc;
332
- width: 26px;
333
- height: 26px;
334
- line-height: 26px;
412
+ position: relative;
413
+ background-color: var(--tsmap-surface);
414
+ width: 40px;
415
+ height: 40px;
416
+ line-height: 40px;
335
417
  display: block;
336
418
  text-align: center;
337
419
  text-decoration: none;
338
- color: black;
339
420
  background-position: 50% 50%;
340
421
  background-repeat: no-repeat;
341
422
  display: block;
342
- &:hover,
343
- &:focus {
344
- background-color: #f4f4f4;
423
+ color: var(--tsmap-fg);
424
+ transition: background-color 120ms ease;
425
+
426
+ &:hover {
427
+ background-color: var(--tsmap-surface-hover);
428
+ }
429
+
430
+ /* Visible only for keyboard users: a ring on every mouse click is noise,
431
+ and no ring at all makes the control unusable without a pointer. */
432
+ &:focus-visible {
433
+ outline: 2px solid var(--tsmap-accent);
434
+ outline-offset: -2px;
435
+ background-color: var(--tsmap-surface-hover);
436
+ }
437
+
438
+ /*
439
+ * Inset seam between stacked buttons.
440
+ *
441
+ * A full-width `border-bottom` runs edge to edge and visually cuts the
442
+ * card in two, so a zoom control reads as two stacked cards rather than
443
+ * one control with a divider. Every modern map insets this line; drawing
444
+ * it as a pseudo-element is also what lets the last child simply not have
445
+ * one, without a border reset.
446
+ */
447
+ &:not(:last-child)::after {
448
+ content: '';
449
+ position: absolute;
450
+ right: 8px;
451
+ bottom: 0;
452
+ left: 8px;
453
+ height: 1px;
454
+ background: var(--tsmap-divider);
345
455
  }
346
456
 
347
457
  &:first-child {
348
- border-top-left-radius: 4px;
349
- border-top-right-radius: 4px;
458
+ border-top-left-radius: 8px;
459
+ border-top-right-radius: 8px;
350
460
  }
351
461
 
352
462
  &:last-child {
353
- border-bottom-left-radius: 4px;
354
- border-bottom-right-radius: 4px;
355
- border-bottom: none;
463
+ border-bottom-left-radius: 8px;
464
+ border-bottom-right-radius: 8px;
356
465
  }
357
466
 
358
467
  &.tsmap-disabled {
359
468
  cursor: default;
360
- background-color: #f4f4f4;
361
- color: #bbb;
469
+ background-color: var(--tsmap-surface);
470
+ color: var(--tsmap-fg-disabled);
362
471
  }
363
472
  }
364
473
  }
@@ -369,39 +478,42 @@ path.tsmap-interactive:focus:not(:focus-visible) {
369
478
  display: block;
370
479
  }
371
480
 
481
+ /*
482
+ * Touch used to shrink these to 30px with a 2px radius, which is smaller than
483
+ * the pointer size and the wrong way round — a finger needs a bigger target
484
+ * than a cursor, not a smaller one. 40px already clears the 44px guideline
485
+ * once the control's own padding is counted, so both inputs share it.
486
+ */
372
487
  .tsmap-touch .tsmap-bar a {
373
- width: 30px;
374
- height: 30px;
375
- line-height: 30px;
376
-
377
- &:first-child {
378
- border-top-left-radius: 2px;
379
- border-top-right-radius: 2px;
380
- }
381
-
382
- &:last-child {
383
- border-bottom-left-radius: 2px;
384
- border-bottom-right-radius: 2px;
385
- }
488
+ width: 40px;
489
+ height: 40px;
490
+ line-height: 40px;
386
491
  }
387
492
 
388
493
  /* zoom control */
389
494
 
390
- .tsmap-control-zoom-in,
391
- .tsmap-control-zoom-out {
392
- font: bold 18px 'Lucida Console', Monaco, monospace;
393
- text-indent: 1px;
394
- .tsmap-touch & {
395
- font-size: 22px;
396
- }
495
+ /* Scoped under .tsmap-bar so it outranks the `.tsmap-bar a` colour above —
496
+ the zoom glyphs sit a shade lighter than label ink, as they do on every
497
+ map that draws them as characters rather than icons. */
498
+ .tsmap-bar a.tsmap-control-zoom-in,
499
+ .tsmap-bar a.tsmap-control-zoom-out {
500
+ color: var(--tsmap-fg-muted);
501
+
502
+ font:
503
+ 400 21px / 40px -apple-system,
504
+ BlinkMacSystemFont,
505
+ 'Segoe UI',
506
+ Roboto,
507
+ sans-serif;
508
+ text-indent: 0;
397
509
  }
398
510
 
399
511
  /* layers control */
400
512
 
401
513
  .tsmap-control-layers {
402
- box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
403
- background: #fff;
404
- border-radius: 5px;
514
+ box-shadow: var(--tsmap-shadow);
515
+ background: var(--tsmap-surface);
516
+ border-radius: 8px;
405
517
 
406
518
  .tsmap-control-layers-list {
407
519
  display: none;
@@ -419,6 +531,12 @@ path.tsmap-interactive:focus:not(:focus-visible) {
419
531
  width: 44px;
420
532
  height: 44px;
421
533
  }
534
+
535
+ /* The stacked-sheets glyph is drawn with light fills, which disappear into a
536
+ dark card. Same shape, inverted ink. */
537
+ .tsmap-dark & {
538
+ background-image: url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2736%27 height=%2736%27 viewBox=%270 0 36 36%27%3E%3Crect x=%274%27 y=%274%27 width=%2714%27 height=%2714%27 fill=%27%23202124%27 stroke=%27%23bdc1c6%27 stroke-width=%271.5%27/%3E%3Crect x=%2710%27 y=%2710%27 width=%2714%27 height=%2714%27 fill=%27%232f3134%27 stroke=%27%23bdc1c6%27 stroke-width=%271.5%27/%3E%3Crect x=%2716%27 y=%2716%27 width=%2714%27 height=%2714%27 fill=%27%233f4144%27 stroke=%27%23bdc1c6%27 stroke-width=%271.5%27/%3E%3C/svg%3E');
539
+ }
422
540
  }
423
541
 
424
542
  .tsmap-control-layers-expanded {
@@ -440,8 +558,8 @@ path.tsmap-interactive:focus:not(:focus-visible) {
440
558
 
441
559
  .tsmap-control-layers-expanded {
442
560
  padding: 6px 10px 6px 6px;
443
- color: #333;
444
- background: #fff;
561
+ color: var(--tsmap-fg);
562
+ background: var(--tsmap-surface);
445
563
  }
446
564
 
447
565
  .tsmap-control-layers-scrollbar {
@@ -464,7 +582,7 @@ path.tsmap-interactive:focus:not(:focus-visible) {
464
582
 
465
583
  .tsmap-control-layers-separator {
466
584
  height: 0;
467
- border-top: 1px solid #ddd;
585
+ border-top: 1px solid var(--tsmap-hairline);
468
586
  margin: 5px -10px 5px -6px;
469
587
  }
470
588
 
@@ -476,15 +594,14 @@ path.tsmap-interactive:focus:not(:focus-visible) {
476
594
  /* attribution and scale controls */
477
595
 
478
596
  .tsmap-container .tsmap-control-attribution {
479
- background: #fff;
480
- background: rgba(255, 255, 255, 0.8);
597
+ background: var(--tsmap-scrim);
481
598
  margin: 0;
482
599
  }
483
600
 
484
601
  .tsmap-control-attribution,
485
602
  .tsmap-control-scale-line {
486
603
  padding: 0 5px;
487
- color: #333;
604
+ color: var(--tsmap-fg);
488
605
  line-height: 1.4;
489
606
  }
490
607
 
@@ -514,36 +631,40 @@ path.tsmap-interactive:focus:not(:focus-visible) {
514
631
  }
515
632
 
516
633
  .tsmap-control-scale-line {
517
- border: 2px solid #777;
634
+ border: 2px solid var(--tsmap-scale-line);
518
635
  border-top: none;
519
636
  line-height: 1.1;
520
637
  padding: 2px 5px 1px;
521
638
  white-space: nowrap;
522
639
  box-sizing: border-box;
523
- background: rgba(255, 255, 255, 0.8);
524
- text-shadow: 1px 1px #fff;
640
+ background: var(--tsmap-scrim);
525
641
 
526
642
  &:not(:first-child) {
527
- border-top: 2px solid #777;
643
+ border-top: 2px solid var(--tsmap-scale-line);
528
644
  border-bottom: none;
529
645
  margin-top: -2px;
530
646
 
531
647
  &:not(:last-child) {
532
- border-bottom: 2px solid #777;
648
+ border-bottom: 2px solid var(--tsmap-scale-line);
533
649
  }
534
650
  }
535
651
  }
536
652
 
653
+ /*
654
+ * Touch controls are the same controls.
655
+ *
656
+ * This block used to swap the elevated card for a flat one with a hard
657
+ * `2px solid rgba(0,0,0,0.2)` border — the Leaflet touch treatment, and the
658
+ * reason the chrome looked a decade old on exactly the devices most people
659
+ * open a map on. A shadow is not a pointer affordance; it is how a floating
660
+ * control reads as floating, on any input. Phones render it fine.
661
+ *
662
+ * The touch class still exists and still drives hit-target sizing above; it
663
+ * just no longer restyles the surface.
664
+ */
537
665
  .tsmap-touch {
538
- .tsmap-control-attribution,
539
666
  .tsmap-control-layers,
540
667
  .tsmap-bar {
541
- box-shadow: none;
542
- }
543
-
544
- .tsmap-control-layers,
545
- .tsmap-bar {
546
- border: 2px solid rgba(0, 0, 0, 0.2);
547
668
  background-clip: padding-box;
548
669
  }
549
670
  }
@@ -597,9 +718,9 @@ path.tsmap-interactive:focus:not(:focus-visible) {
597
718
 
598
719
  .tsmap-popup-content-wrapper,
599
720
  .tsmap-popup-tip {
600
- background: white;
601
- color: #333;
602
- box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
721
+ background: var(--tsmap-surface);
722
+ color: var(--tsmap-fg);
723
+ box-shadow: var(--tsmap-shadow-lg);
603
724
  }
604
725
 
605
726
  .tsmap-container a.tsmap-popup-close-button {
@@ -611,13 +732,13 @@ path.tsmap-interactive:focus:not(:focus-visible) {
611
732
  width: 24px;
612
733
  height: 24px;
613
734
  font: 16px/24px Tahoma, Verdana, sans-serif;
614
- color: #757575;
735
+ color: var(--tsmap-fg-muted);
615
736
  text-decoration: none;
616
737
  background: transparent;
617
738
 
618
739
  &:hover,
619
740
  &:focus {
620
- color: #585858;
741
+ color: var(--tsmap-fg);
621
742
  }
622
743
  }
623
744
 
@@ -628,8 +749,8 @@ path.tsmap-interactive:focus:not(:focus-visible) {
628
749
  /* div icon */
629
750
 
630
751
  .tsmap-div-icon {
631
- background: #fff;
632
- border: 1px solid #666;
752
+ background: var(--tsmap-surface);
753
+ border: 1px solid var(--tsmap-fg-muted);
633
754
  }
634
755
 
635
756
  /* Tooltip */
@@ -637,14 +758,14 @@ path.tsmap-interactive:focus:not(:focus-visible) {
637
758
  .tsmap-tooltip {
638
759
  position: absolute;
639
760
  padding: 6px;
640
- background-color: #fff;
641
- border: 1px solid #fff;
761
+ background-color: var(--tsmap-surface);
762
+ border: 1px solid var(--tsmap-surface);
642
763
  border-radius: 3px;
643
- color: #222;
764
+ color: var(--tsmap-fg);
644
765
  white-space: nowrap;
645
766
  user-select: none;
646
767
  pointer-events: none;
647
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
768
+ box-shadow: 0 1px 3px rgb(0 0 0 / 40%);
648
769
 
649
770
  &.tsmap-interactive {
650
771
  cursor: pointer;
@@ -674,7 +795,7 @@ path.tsmap-interactive:focus:not(:focus-visible) {
674
795
  top: 0;
675
796
  margin-top: -12px;
676
797
  margin-left: -6px;
677
- border-bottom-color: #fff;
798
+ border-bottom-color: var(--tsmap-surface);
678
799
  }
679
800
  }
680
801
 
@@ -686,7 +807,7 @@ path.tsmap-interactive:focus:not(:focus-visible) {
686
807
  margin-left: -6px;
687
808
  bottom: 0;
688
809
  margin-bottom: -12px;
689
- border-top-color: #fff;
810
+ border-top-color: var(--tsmap-surface);
690
811
  }
691
812
  }
692
813
 
@@ -698,7 +819,7 @@ path.tsmap-interactive:focus:not(:focus-visible) {
698
819
  margin-top: -6px;
699
820
  right: 0;
700
821
  margin-right: -12px;
701
- border-left-color: #fff;
822
+ border-left-color: var(--tsmap-surface);
702
823
  }
703
824
  }
704
825
 
@@ -710,7 +831,7 @@ path.tsmap-interactive:focus:not(:focus-visible) {
710
831
  margin-top: -6px;
711
832
  left: 0;
712
833
  margin-left: -12px;
713
- border-right-color: #fff;
834
+ border-right-color: var(--tsmap-surface);
714
835
  }
715
836
  }
716
837
 
@@ -723,3 +844,379 @@ path.tsmap-interactive:focus:not(:focus-visible) {
723
844
  print-color-adjust: exact;
724
845
  }
725
846
  }
847
+
848
+ /* locate control
849
+ ==========================================================================
850
+ The crosshair button. Drawn in CSS rather than shipped as an icon file so it
851
+ inherits colour from the button's state and stays crisp at any pixel ratio —
852
+ the control has four states and a bitmap would need four assets.
853
+ ========================================================================== */
854
+
855
+ .tsmap-control-locate a {
856
+ display: flex;
857
+ align-items: center;
858
+ justify-content: center;
859
+ position: relative;
860
+ }
861
+
862
+ .tsmap-locate-icon {
863
+ position: relative;
864
+ width: 18px;
865
+ height: 18px;
866
+ color: var(--tsmap-fg-muted);
867
+
868
+ /* The ring. */
869
+ &::before {
870
+ content: '';
871
+ position: absolute;
872
+ inset: 3px;
873
+ border: 2px solid currentcolor;
874
+ border-radius: 50%;
875
+ }
876
+
877
+ /* The centre dot, and the four ticks via a cross gradient. */
878
+ &::after {
879
+ content: '';
880
+ position: absolute;
881
+ inset: 0;
882
+ background:
883
+ linear-gradient(currentcolor, currentcolor) 50% 0 / 2px 4px no-repeat,
884
+ linear-gradient(currentcolor, currentcolor) 50% 100% / 2px 4px no-repeat,
885
+ linear-gradient(currentcolor, currentcolor) 0 50% / 4px 2px no-repeat,
886
+ linear-gradient(currentcolor, currentcolor) 100% 50% / 4px 2px no-repeat,
887
+ radial-gradient(circle at 50% 50%, currentcolor 0 2px, transparent 2px);
888
+ }
889
+ }
890
+
891
+ /* Following the device: the crosshair fills in, the way a "you are here"
892
+ control does everywhere else. */
893
+ a.tsmap-control-locate-active .tsmap-locate-icon {
894
+ color: var(--tsmap-accent);
895
+ }
896
+
897
+ /* Acquiring a first fix. The pulse is the only thing telling the user that a
898
+ press did anything at all — a GPS fix can take several seconds. */
899
+ a.tsmap-control-locate-locating .tsmap-locate-icon {
900
+ color: var(--tsmap-accent);
901
+ animation: tsmap-locate-pulse 1s ease-in-out infinite;
902
+ }
903
+
904
+ @keyframes tsmap-locate-pulse {
905
+ 50% {
906
+ opacity: 0.35;
907
+ }
908
+ }
909
+
910
+ /* Denied or unavailable. Struck through rather than merely greyed: grey reads
911
+ as "disabled, try later" when the real state is "this will not work until
912
+ you change a browser setting". */
913
+ /*
914
+ * The state class lands on the <a> itself (see LocateControl._setState), so
915
+ * these select the anchor rather than a parent. Written as `.tsmap-control-
916
+ * locate-denied .tsmap-locate-icon` first time round, which silently matched
917
+ * nothing and left a denied control looking merely idle.
918
+ */
919
+ a.tsmap-control-locate-denied .tsmap-locate-icon {
920
+ color: var(--tsmap-fg-disabled);
921
+ }
922
+
923
+ a.tsmap-control-locate-denied::after {
924
+ content: '';
925
+ position: absolute;
926
+ top: 50%;
927
+ left: 50%;
928
+ width: 22px;
929
+ height: 2px;
930
+ border-radius: 1px;
931
+ /* Cut out from the strike so it reads as a line crossing the glyph rather
932
+ than a line drawn on top of it. */
933
+ background: var(--tsmap-fg-muted);
934
+ box-shadow: 0 -2px 0 var(--tsmap-surface);
935
+ transform: translate(-50%, -50%) rotate(-45deg);
936
+ }
937
+
938
+ /* The accuracy halo around the reported position. */
939
+ .tsmap-locate-accuracy {
940
+ fill: var(--tsmap-accent);
941
+ fill-opacity: 0.12;
942
+ stroke: var(--tsmap-accent);
943
+ stroke-opacity: 0.35;
944
+ stroke-width: 1;
945
+ }
946
+
947
+ /* The "you are here" dot. A fixed screen size at every zoom — the accuracy
948
+ halo is the thing that carries scale — with a white ring so it stays legible
949
+ over both a near-black basemap and a pale one. */
950
+ .tsmap-locate-dot {
951
+ background: var(--tsmap-accent);
952
+ border: 2px solid #fff;
953
+ border-radius: 50%;
954
+ box-shadow: 0 1px 4px rgb(0 0 0 / 40%);
955
+ box-sizing: border-box;
956
+ }
957
+
958
+ /* The halo breathing outwards. Drawn on a child rather than ::after because
959
+ the icon element itself is positioned by the marker layer, and a transform
960
+ on it would fight the marker's own translate. */
961
+ .tsmap-locate-dot-pulse {
962
+ position: absolute;
963
+ inset: -2px;
964
+ border-radius: 50%;
965
+ background: var(--tsmap-accent);
966
+ opacity: 0.4;
967
+ animation: tsmap-locate-dot-pulse 2s ease-out infinite;
968
+ }
969
+
970
+ @keyframes tsmap-locate-dot-pulse {
971
+ 0% {
972
+ transform: scale(1);
973
+ opacity: 0.4;
974
+ }
975
+
976
+ 100% {
977
+ transform: scale(3);
978
+ opacity: 0;
979
+ }
980
+ }
981
+
982
+ @media (prefers-reduced-motion: reduce) {
983
+ a.tsmap-control-locate-locating .tsmap-locate-icon {
984
+ animation: none;
985
+ }
986
+
987
+ .tsmap-locate-dot-pulse {
988
+ animation: none;
989
+ }
990
+ }
991
+
992
+
993
+ /* geocoder control
994
+ ==========================================================================
995
+ The search box. Sized to sit comfortably on a phone, where it is usually the
996
+ widest piece of chrome on the map.
997
+ ========================================================================== */
998
+
999
+ .tsmap-control-geocoder {
1000
+ overflow: visible;
1001
+ min-width: 44px;
1002
+ }
1003
+
1004
+ .tsmap-control-geocoder-form {
1005
+ display: none;
1006
+ }
1007
+
1008
+ .tsmap-control-geocoder-expanded .tsmap-control-geocoder-form {
1009
+ display: block;
1010
+ }
1011
+
1012
+ .tsmap-control-geocoder-input {
1013
+ width: 240px;
1014
+ max-width: 60vw;
1015
+ height: 40px;
1016
+ padding: 0 12px;
1017
+ border: 0;
1018
+ border-radius: 8px;
1019
+ background: var(--tsmap-surface);
1020
+ color: var(--tsmap-fg);
1021
+ font: inherit;
1022
+ box-sizing: border-box;
1023
+
1024
+ &::placeholder {
1025
+ color: var(--tsmap-fg-muted);
1026
+ }
1027
+
1028
+ &:focus-visible {
1029
+ outline: 2px solid var(--tsmap-accent);
1030
+ outline-offset: -2px;
1031
+ }
1032
+ }
1033
+
1034
+ /* The magnifier, shown only in collapsed mode. */
1035
+ .tsmap-geocoder-icon {
1036
+ display: inline-block;
1037
+ width: 15px;
1038
+ height: 15px;
1039
+ border: 2px solid currentcolor;
1040
+ border-radius: 50%;
1041
+ position: relative;
1042
+ vertical-align: middle;
1043
+
1044
+ &::after {
1045
+ content: '';
1046
+ position: absolute;
1047
+ right: -5px;
1048
+ bottom: -4px;
1049
+ width: 2px;
1050
+ height: 7px;
1051
+ border-radius: 1px;
1052
+ background: currentcolor;
1053
+ transform: rotate(-45deg);
1054
+ }
1055
+ }
1056
+
1057
+ .tsmap-control-geocoder-expanded .tsmap-control-geocoder-toggle {
1058
+ display: none;
1059
+ }
1060
+
1061
+ /* A spinner would be more literal, but the input is already the thing the eye
1062
+ is on — dimming its text says "working" without adding a moving part. */
1063
+ .tsmap-control-geocoder-busy .tsmap-control-geocoder-input {
1064
+ color: var(--tsmap-fg-muted);
1065
+ }
1066
+
1067
+ .tsmap-control-geocoder-list {
1068
+ display: none;
1069
+ list-style: none;
1070
+ margin: 6px 0 0;
1071
+ padding: 4px 0;
1072
+ border-radius: 8px;
1073
+ background: var(--tsmap-surface);
1074
+ box-shadow: var(--tsmap-shadow);
1075
+ max-height: 40vh;
1076
+ overflow-y: auto;
1077
+ }
1078
+
1079
+ .tsmap-control-geocoder-open .tsmap-control-geocoder-list {
1080
+ display: block;
1081
+ }
1082
+
1083
+ .tsmap-control-geocoder-item {
1084
+ padding: 8px 12px;
1085
+ color: var(--tsmap-fg);
1086
+ cursor: pointer;
1087
+ white-space: nowrap;
1088
+ overflow: hidden;
1089
+ text-overflow: ellipsis;
1090
+
1091
+ &:hover,
1092
+ &.tsmap-control-geocoder-item-active {
1093
+ background: var(--tsmap-surface-hover);
1094
+ }
1095
+ }
1096
+
1097
+ .tsmap-control-geocoder-marker {
1098
+ background: var(--tsmap-accent);
1099
+ border: 2px solid #fff;
1100
+ border-radius: 50%;
1101
+ box-shadow: 0 1px 4px rgb(0 0 0 / 40%);
1102
+ box-sizing: border-box;
1103
+ }
1104
+
1105
+ /* navigation control
1106
+ ==========================================================================
1107
+ Zoom pair plus a compass. The needle is drawn in CSS for the same reason the
1108
+ locate crosshair is: it has to recolour with the theme and stay sharp while
1109
+ being rotated by a transform.
1110
+ ========================================================================== */
1111
+
1112
+ .tsmap-control-navigation a.tsmap-control-navigation-zoom-in,
1113
+ .tsmap-control-navigation a.tsmap-control-navigation-zoom-out {
1114
+ color: var(--tsmap-fg-muted);
1115
+ font:
1116
+ 400 21px / 40px -apple-system,
1117
+ BlinkMacSystemFont,
1118
+ 'Segoe UI',
1119
+ Roboto,
1120
+ sans-serif;
1121
+ }
1122
+
1123
+ .tsmap-control-navigation a.tsmap-control-navigation-compass {
1124
+ display: flex;
1125
+ align-items: center;
1126
+ justify-content: center;
1127
+ }
1128
+
1129
+ .tsmap-compass-needle {
1130
+ width: 4px;
1131
+ height: 22px;
1132
+ position: relative;
1133
+ transition: transform 120ms ease-out;
1134
+
1135
+ /* North half carries the colour; the south half is muted, which is how a
1136
+ compass reads as a compass rather than a pin. */
1137
+ &::before,
1138
+ &::after {
1139
+ content: '';
1140
+ position: absolute;
1141
+ left: 0;
1142
+ width: 0;
1143
+ height: 0;
1144
+ border-left: 2px solid transparent;
1145
+ border-right: 2px solid transparent;
1146
+ }
1147
+
1148
+ &::before {
1149
+ top: 0;
1150
+ border-bottom: 11px solid #ea4335;
1151
+ }
1152
+
1153
+ &::after {
1154
+ bottom: 0;
1155
+ border-top: 11px solid var(--tsmap-fg-disabled);
1156
+ }
1157
+ }
1158
+
1159
+ @media (prefers-reduced-motion: reduce) {
1160
+ .tsmap-compass-needle {
1161
+ transition: none;
1162
+ }
1163
+ }
1164
+
1165
+ /* fullscreen control
1166
+ ========================================================================== */
1167
+
1168
+ .tsmap-fullscreen-icon {
1169
+ display: block;
1170
+ width: 16px;
1171
+ height: 16px;
1172
+ position: relative;
1173
+
1174
+ /* Four corner brackets, drawn as two boxes clipped by their own borders. */
1175
+ &::before,
1176
+ &::after {
1177
+ content: '';
1178
+ position: absolute;
1179
+ width: 5px;
1180
+ height: 5px;
1181
+ border: 2px solid currentcolor;
1182
+ }
1183
+
1184
+ &::before {
1185
+ top: 0;
1186
+ left: 0;
1187
+ border-right: 0;
1188
+ border-bottom: 0;
1189
+ }
1190
+
1191
+ &::after {
1192
+ right: 0;
1193
+ bottom: 0;
1194
+ border-top: 0;
1195
+ border-left: 0;
1196
+ }
1197
+ }
1198
+
1199
+ .tsmap-control-fullscreen-active .tsmap-fullscreen-icon {
1200
+ &::before {
1201
+ border-right: 0;
1202
+ border-bottom: 0;
1203
+ transform: rotate(180deg);
1204
+ }
1205
+
1206
+ &::after {
1207
+ transform: rotate(180deg);
1208
+ }
1209
+ }
1210
+
1211
+ /*
1212
+ * Fallback for browsers and embeds where the Fullscreen API is unavailable —
1213
+ * an iframe without `allowfullscreen`, or iPhone Safari, which has never
1214
+ * implemented it. Browser chrome stays visible, but the map fills the viewport.
1215
+ */
1216
+ .tsmap-pseudo-fullscreen {
1217
+ position: fixed !important;
1218
+ inset: 0 !important;
1219
+ width: 100% !important;
1220
+ height: 100% !important;
1221
+ z-index: 99999;
1222
+ }