@humanforest/nuxt-layer 0.1.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/LICENSE +67 -0
- package/README.md +208 -0
- package/app.config.ts +10 -0
- package/assets/icons/forest/bike-asterisk.svg +21 -0
- package/assets/icons/forest/bike-down.svg +19 -0
- package/assets/icons/forest/bike-off.svg +20 -0
- package/assets/icons/forest/bike-up.svg +19 -0
- package/assets/icons/forest/bike-x.svg +19 -0
- package/assets/icons/forest/bike.svg +18 -0
- package/assets/icons/forest/mark-fill.svg +6 -0
- package/assets/icons/forest/mark.svg +6 -0
- package/assets/icons/forest/parking.svg +3 -0
- package/assets/icons/forest/star-fill.svg +4 -0
- package/assets/icons/forest/star.svg +4 -0
- package/assets/icons/forest/traffic-light-caution.svg +8 -0
- package/assets/icons/forest/traffic-light-go.svg +8 -0
- package/assets/icons/forest/traffic-light-lit.svg +8 -0
- package/assets/icons/forest/traffic-light-stop.svg +8 -0
- package/assets/icons/forest/traffic-light.svg +8 -0
- package/nuxt.config.ts +81 -0
- package/package.json +35 -0
- package/packages/charts/src/FBoroughShape.vue +52 -0
- package/packages/charts/src/FCalendarGrid.vue +332 -0
- package/packages/charts/src/FCellLegend.vue +54 -0
- package/packages/charts/src/FChartFrame.vue +201 -0
- package/packages/charts/src/FChartFrameSingle.vue +91 -0
- package/packages/charts/src/FChartLegend.vue +26 -0
- package/packages/charts/src/FDistributionBar.vue +224 -0
- package/packages/charts/src/FDottedMap.vue +335 -0
- package/packages/charts/src/FLondonMap.vue +399 -0
- package/packages/charts/src/FSparkline.vue +198 -0
- package/packages/charts/src/FStatusTrack.vue +151 -0
- package/packages/charts/src/cellSize.ts +46 -0
- package/packages/charts/src/distribution.ts +99 -0
- package/packages/charts/src/engine.ts +15 -0
- package/packages/charts/src/forestTooltip.ts +110 -0
- package/packages/charts/src/fromCategories.ts +43 -0
- package/packages/charts/src/index.ts +87 -0
- package/packages/charts/src/londonAreas.ts +75 -0
- package/packages/charts/src/londonGrid.ts +67 -0
- package/packages/charts/src/motionDuration.ts +49 -0
- package/packages/charts/src/presets.ts +143 -0
- package/packages/charts/src/resolveVar.ts +21 -0
- package/packages/charts/src/snapTooltip.ts +154 -0
- package/packages/charts/src/tooltipStandIn.ts +58 -0
- package/packages/charts/src/useCellScale.ts +155 -0
- package/packages/charts/src/useChartPalette.ts +61 -0
- package/packages/charts/src/useChartRepaintKey.ts +22 -0
- package/packages/charts/src/useThemeVersion.ts +17 -0
- package/packages/frames/src/FBrowserFrame.vue +53 -0
- package/packages/frames/src/FDeviceFrame.vue +242 -0
- package/packages/frames/src/FFitFrame.vue +83 -0
- package/packages/frames/src/art/iphone-11.svg +124 -0
- package/packages/frames/src/art/iphone-12.svg +601 -0
- package/packages/frames/src/art/iphone-13-pro.svg +820 -0
- package/packages/frames/src/art/iphone-13.svg +849 -0
- package/packages/frames/src/art/iphone-14.svg +681 -0
- package/packages/frames/src/art/iphone-15-pro.svg +633 -0
- package/packages/frames/src/art/iphone-15.svg +713 -0
- package/packages/frames/src/art/iphone-16-plus.svg +685 -0
- package/packages/frames/src/art/iphone-16-pro-max.svg +611 -0
- package/packages/frames/src/art/iphone-16-pro.svg +631 -0
- package/packages/frames/src/art/iphone-16.svg +713 -0
- package/packages/frames/src/art/iphone-17-pro-max.svg +355 -0
- package/packages/frames/src/art/iphone-17.svg +305 -0
- package/packages/frames/src/art/iphone-air.svg +389 -0
- package/packages/frames/src/art/iphone-se-3.svg +473 -0
- package/packages/frames/src/art/iphone-x.svg +652 -0
- package/packages/frames/src/art/pixel-10-pro-xl.svg +32 -0
- package/packages/frames/src/art/pixel-10-pro.svg +32 -0
- package/packages/frames/src/art/pixel-10.svg +32 -0
- package/packages/frames/src/art/pixel-8-pro.svg +208 -0
- package/packages/frames/src/art/pixel-8.svg +208 -0
- package/packages/frames/src/art/pixel-8a.svg +8 -0
- package/packages/frames/src/art/pixel-9-pro-xl.svg +32 -0
- package/packages/frames/src/art/pixel-9-pro.svg +34 -0
- package/packages/frames/src/art/pixel-9a.svg +25 -0
- package/packages/frames/src/art.ts +29 -0
- package/packages/frames/src/devices.ts +337 -0
- package/packages/frames/src/index.ts +15 -0
- package/packages/tokens/geo/london-boroughs.json +1 -0
- package/packages/tokens/geo/london-dots-coarse.json +1 -0
- package/packages/tokens/geo/london-dots-fine.json +1 -0
- package/packages/tokens/geo/london-dots-medium.json +1 -0
- package/packages/tokens/geo/london-dots-ultra.json +1 -0
- package/packages/tokens/geo/london-wards.json +1 -0
- package/packages/tokens/glyphs/glyphs.json +1 -0
- package/packages/tokens/logos/f-rot.svg +4 -0
- package/packages/tokens/logos/f.svg +4 -0
- package/packages/tokens/logos/favicon-c2w.svg +9 -0
- package/packages/tokens/logos/favicon-console.svg +9 -0
- package/packages/tokens/logos/favicon-ds.svg +9 -0
- package/packages/tokens/logos/favicon-fleet.svg +9 -0
- package/packages/tokens/logos/favicon-flex.svg +9 -0
- package/packages/tokens/logos/favicon-plus.svg +9 -0
- package/packages/tokens/logos/favicon-radar.svg +9 -0
- package/packages/tokens/logos/favicon-tower.svg +9 -0
- package/packages/tokens/logos/lockup-horizontal.svg +17 -0
- package/packages/tokens/logos/lockup-vertical.svg +17 -0
- package/packages/tokens/logos/mark-mono.svg +4 -0
- package/packages/tokens/logos/mark.svg +7 -0
- package/packages/tokens/logos/wordmark.svg +9 -0
- package/packages/tokens/src/colourEngine.ts +604 -0
- package/packages/tokens/src/glyphs.ts +127 -0
- package/packages/tokens/src/logo.ts +50 -0
- package/packages/tokens/src/subbrands.ts +44 -0
- package/packages/tokens/src/sublogo.ts +80 -0
- package/packages/ui/icons/forest/map/forest-place-count.svg +21 -0
- package/packages/ui/icons/forest/map/forest-place-marker.svg +17 -0
- package/packages/ui/icons/forest/map/forest-vehicle-marker.svg +73 -0
- package/packages/ui/icons/forest/map/manifest.json +373 -0
- package/packages/ui/icons/forest/map/place-count-manifest.json +83 -0
- package/packages/ui/icons/forest/map/place-manifest.json +74 -0
- package/packages/ui/src/address/address.ts +125 -0
- package/packages/ui/src/address/autofill.ts +78 -0
- package/packages/ui/src/address/geocode.ts +55 -0
- package/packages/ui/src/address/index.ts +31 -0
- package/packages/ui/src/address/validate.ts +91 -0
- package/packages/ui/src/canvas/FCanvasCard.vue +97 -0
- package/packages/ui/src/canvas/FCanvasControl.vue +121 -0
- package/packages/ui/src/canvas/FCanvasControlGroup.vue +100 -0
- package/packages/ui/src/canvas/FCanvasFlyout.vue +216 -0
- package/packages/ui/src/canvas/FCanvasHud.vue +402 -0
- package/packages/ui/src/canvas/canvasSurface.ts +39 -0
- package/packages/ui/src/canvas/index.ts +17 -0
- package/packages/ui/src/contexts/ForestContext.vue +53 -0
- package/packages/ui/src/contexts/contexts.ts +153 -0
- package/packages/ui/src/contexts/index.ts +23 -0
- package/packages/ui/src/contexts/marketing-theme.ts +214 -0
- package/packages/ui/src/contexts/mobile-theme.ts +402 -0
- package/packages/ui/src/filter/FFilterBar.vue +72 -0
- package/packages/ui/src/filter/FFilterChip.vue +362 -0
- package/packages/ui/src/filter/FFilterClauses.vue +104 -0
- package/packages/ui/src/filter/FFilterTrigger.vue +183 -0
- package/packages/ui/src/filter/filter.theme.ts +27 -0
- package/packages/ui/src/filter/filterClause.ts +265 -0
- package/packages/ui/src/filter/filterPredicate.ts +134 -0
- package/packages/ui/src/filter/filterSchema.ts +82 -0
- package/packages/ui/src/filter/index.ts +36 -0
- package/packages/ui/src/forest-preset.ts +68 -0
- package/packages/ui/src/icons.ts +79 -0
- package/packages/ui/src/index.ts +17 -0
- package/packages/ui/src/kpi/FKpi.vue +253 -0
- package/packages/ui/src/kpi/FKpiGroup.vue +161 -0
- package/packages/ui/src/kpi/index.ts +20 -0
- package/packages/ui/src/kpi/kpi.theme.ts +168 -0
- package/packages/ui/src/kpi/kpiDelta.ts +82 -0
- package/packages/ui/src/kpi/kpiFormat.ts +72 -0
- package/packages/ui/src/kpi/kpiGroupState.ts +63 -0
- package/packages/ui/src/logo/FLogo.vue +52 -0
- package/packages/ui/src/logo/FSubLogo.vue +214 -0
- package/packages/ui/src/logo/index.ts +8 -0
- package/packages/ui/src/map/FMap.vue +355 -0
- package/packages/ui/src/map/FMapCompass.vue +154 -0
- package/packages/ui/src/map/FMapCoordinates.vue +134 -0
- package/packages/ui/src/map/FMapDrawTools.vue +116 -0
- package/packages/ui/src/map/FMapFullscreen.vue +92 -0
- package/packages/ui/src/map/FMapGeolocate.vue +208 -0
- package/packages/ui/src/map/FMapLayers.vue +75 -0
- package/packages/ui/src/map/FMapLegend.vue +45 -0
- package/packages/ui/src/map/FMapPopup.vue +129 -0
- package/packages/ui/src/map/FMapRamp.vue +69 -0
- package/packages/ui/src/map/FMapRegion.vue +77 -0
- package/packages/ui/src/map/FMapScale.vue +107 -0
- package/packages/ui/src/map/FMapSearch.vue +362 -0
- package/packages/ui/src/map/FMapTilt.vue +87 -0
- package/packages/ui/src/map/FMapTools.vue +61 -0
- package/packages/ui/src/map/FMapZoom.vue +81 -0
- package/packages/ui/src/map/FMarker.vue +218 -0
- package/packages/ui/src/map/FTrackScrubber.vue +94 -0
- package/packages/ui/src/map/FVehicleLegend.vue +72 -0
- package/packages/ui/src/map/FVehicleMark.vue +166 -0
- package/packages/ui/src/map/bin.ts +196 -0
- package/packages/ui/src/map/coordinates.ts +83 -0
- package/packages/ui/src/map/density.ts +401 -0
- package/packages/ui/src/map/draw.ts +356 -0
- package/packages/ui/src/map/ease.ts +67 -0
- package/packages/ui/src/map/fullscreen.ts +59 -0
- package/packages/ui/src/map/heat.ts +160 -0
- package/packages/ui/src/map/index.ts +351 -0
- package/packages/ui/src/map/liveMarkers.ts +60 -0
- package/packages/ui/src/map/map.css +116 -0
- package/packages/ui/src/map/mapColor.ts +31 -0
- package/packages/ui/src/map/mapContext.ts +12 -0
- package/packages/ui/src/map/mapLayers.ts +55 -0
- package/packages/ui/src/map/mapLegend.ts +70 -0
- package/packages/ui/src/map/mapSearch.theme.ts +79 -0
- package/packages/ui/src/map/mapSearch.ts +279 -0
- package/packages/ui/src/map/mapUrl.ts +117 -0
- package/packages/ui/src/map/markerAsset.ts +3 -0
- package/packages/ui/src/map/padding.ts +39 -0
- package/packages/ui/src/map/place.ts +192 -0
- package/packages/ui/src/map/ramp.ts +95 -0
- package/packages/ui/src/map/regionSelect.ts +163 -0
- package/packages/ui/src/map/route.ts +167 -0
- package/packages/ui/src/map/scale.ts +329 -0
- package/packages/ui/src/map/scaleBar.ts +67 -0
- package/packages/ui/src/map/selectionModel.ts +50 -0
- package/packages/ui/src/map/sources/coordinates.ts +58 -0
- package/packages/ui/src/map/sources/mapboxPlaces.ts +71 -0
- package/packages/ui/src/map/style.ts +14 -0
- package/packages/ui/src/map/track.ts +279 -0
- package/packages/ui/src/map/useFeatureSelect.ts +230 -0
- package/packages/ui/src/map/useLiveMarkers.ts +84 -0
- package/packages/ui/src/map/useMap.ts +150 -0
- package/packages/ui/src/map/useMapDraw.ts +776 -0
- package/packages/ui/src/map/useMapOverlay.ts +421 -0
- package/packages/ui/src/map/useMapTableSelection.ts +101 -0
- package/packages/ui/src/map/useMapUrlState.ts +155 -0
- package/packages/ui/src/map/useMarkerSource.ts +163 -0
- package/packages/ui/src/map/useRegionSelect.ts +309 -0
- package/packages/ui/src/map/useTokenMode.ts +61 -0
- package/packages/ui/src/map/useTrackPlayback.ts +131 -0
- package/packages/ui/src/map/useVehicleFilter.ts +51 -0
- package/packages/ui/src/map/useVisibleFeatures.ts +124 -0
- package/packages/ui/src/map/vehicle.ts +807 -0
- package/packages/ui/src/map/vehicleFilter.ts +284 -0
- package/packages/ui/src/map/vehicleLegend.ts +155 -0
- package/packages/ui/src/map/visibleFeatures.ts +192 -0
- package/packages/ui/src/map/zone.ts +291 -0
- package/packages/ui/src/shell/FAppNavbar.vue +68 -0
- package/packages/ui/src/shell/FAppShell.vue +163 -0
- package/packages/ui/src/shell/FAppSidebar.vue +215 -0
- package/packages/ui/src/shell/FPageToolbar.vue +126 -0
- package/packages/ui/src/shell/FScrollShadow.vue +43 -0
- package/packages/ui/src/shell/FSidePanel.vue +258 -0
- package/packages/ui/src/shell/FToolbarActions.vue +32 -0
- package/packages/ui/src/shell/index.ts +33 -0
- package/packages/ui/src/shell/nav.ts +37 -0
- package/packages/ui/src/shell/pageToolbar.ts +69 -0
- package/packages/ui/src/shell/shell.ts +74 -0
- package/packages/ui/src/shell/sidePanel.ts +115 -0
- package/packages/ui/src/themes/accordion.ts +36 -0
- package/packages/ui/src/themes/alert.ts +16 -0
- package/packages/ui/src/themes/auth-form.ts +19 -0
- package/packages/ui/src/themes/avatar-group.ts +9 -0
- package/packages/ui/src/themes/avatar.ts +18 -0
- package/packages/ui/src/themes/badge.ts +51 -0
- package/packages/ui/src/themes/banner.ts +21 -0
- package/packages/ui/src/themes/breadcrumb.ts +10 -0
- package/packages/ui/src/themes/button.mechanics.ts +191 -0
- package/packages/ui/src/themes/button.ts +136 -0
- package/packages/ui/src/themes/calendar.ts +29 -0
- package/packages/ui/src/themes/card.ts +67 -0
- package/packages/ui/src/themes/carousel.ts +41 -0
- package/packages/ui/src/themes/checkbox-group.ts +20 -0
- package/packages/ui/src/themes/checkbox.ts +47 -0
- package/packages/ui/src/themes/chip.ts +50 -0
- package/packages/ui/src/themes/collapsible.ts +26 -0
- package/packages/ui/src/themes/color-picker.ts +13 -0
- package/packages/ui/src/themes/command-palette.ts +37 -0
- package/packages/ui/src/themes/container.ts +14 -0
- package/packages/ui/src/themes/context-menu.ts +38 -0
- package/packages/ui/src/themes/dashboard-group.ts +11 -0
- package/packages/ui/src/themes/dashboard-navbar.ts +10 -0
- package/packages/ui/src/themes/dashboard-panel.ts +18 -0
- package/packages/ui/src/themes/dashboard-resize-handle.ts +35 -0
- package/packages/ui/src/themes/dashboard-sidebar-collapse.ts +7 -0
- package/packages/ui/src/themes/dashboard-sidebar-toggle.ts +7 -0
- package/packages/ui/src/themes/dashboard-sidebar.ts +58 -0
- package/packages/ui/src/themes/dashboard-toolbar.ts +15 -0
- package/packages/ui/src/themes/drawer.ts +45 -0
- package/packages/ui/src/themes/dropdown-menu.ts +38 -0
- package/packages/ui/src/themes/empty.ts +43 -0
- package/packages/ui/src/themes/field-group.ts +52 -0
- package/packages/ui/src/themes/file-upload.ts +38 -0
- package/packages/ui/src/themes/footer-columns.ts +13 -0
- package/packages/ui/src/themes/footer.ts +9 -0
- package/packages/ui/src/themes/form-field.ts +25 -0
- package/packages/ui/src/themes/form.ts +22 -0
- package/packages/ui/src/themes/header.ts +12 -0
- package/packages/ui/src/themes/icon.ts +61 -0
- package/packages/ui/src/themes/index.ts +99 -0
- package/packages/ui/src/themes/input-date.ts +73 -0
- package/packages/ui/src/themes/input-menu.ts +66 -0
- package/packages/ui/src/themes/input-number.ts +58 -0
- package/packages/ui/src/themes/input-rating.ts +25 -0
- package/packages/ui/src/themes/input-tags.ts +63 -0
- package/packages/ui/src/themes/input-time.ts +70 -0
- package/packages/ui/src/themes/input.ts +148 -0
- package/packages/ui/src/themes/kbd.ts +27 -0
- package/packages/ui/src/themes/link.ts +27 -0
- package/packages/ui/src/themes/listbox.ts +48 -0
- package/packages/ui/src/themes/main.ts +6 -0
- package/packages/ui/src/themes/marquee.ts +45 -0
- package/packages/ui/src/themes/modal.ts +46 -0
- package/packages/ui/src/themes/navigation-menu.ts +80 -0
- package/packages/ui/src/themes/page-anchors.ts +7 -0
- package/packages/ui/src/themes/page-aside.ts +7 -0
- package/packages/ui/src/themes/page-card.ts +22 -0
- package/packages/ui/src/themes/page-cta.ts +18 -0
- package/packages/ui/src/themes/page-feature.ts +13 -0
- package/packages/ui/src/themes/page-header.ts +16 -0
- package/packages/ui/src/themes/page-hero.ts +15 -0
- package/packages/ui/src/themes/page-links.ts +12 -0
- package/packages/ui/src/themes/page-section.ts +14 -0
- package/packages/ui/src/themes/page.ts +7 -0
- package/packages/ui/src/themes/pagination.ts +17 -0
- package/packages/ui/src/themes/pin-input.ts +73 -0
- package/packages/ui/src/themes/popover.ts +30 -0
- package/packages/ui/src/themes/progress.ts +29 -0
- package/packages/ui/src/themes/prose.ts +45 -0
- package/packages/ui/src/themes/radio-group.ts +39 -0
- package/packages/ui/src/themes/scroll-area.ts +23 -0
- package/packages/ui/src/themes/select-menu.ts +71 -0
- package/packages/ui/src/themes/select.ts +72 -0
- package/packages/ui/src/themes/separator.ts +28 -0
- package/packages/ui/src/themes/sidebar.ts +127 -0
- package/packages/ui/src/themes/skeleton.ts +15 -0
- package/packages/ui/src/themes/slideover.ts +87 -0
- package/packages/ui/src/themes/slider.ts +48 -0
- package/packages/ui/src/themes/stepper.ts +41 -0
- package/packages/ui/src/themes/switch.ts +63 -0
- package/packages/ui/src/themes/table.ts +34 -0
- package/packages/ui/src/themes/tabs.ts +74 -0
- package/packages/ui/src/themes/textarea.ts +77 -0
- package/packages/ui/src/themes/timeline.ts +25 -0
- package/packages/ui/src/themes/toast.ts +29 -0
- package/packages/ui/src/themes/toaster.ts +66 -0
- package/packages/ui/src/themes/tooltip.ts +31 -0
- package/packages/ui/src/themes/tree.ts +41 -0
- package/packages/ui/src/themes/user.ts +28 -0
- package/styles.cdn.gen.css +2047 -0
- package/theme.gen.json +1 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The compass — where north is, how far the view is tilted, and one click back to square.
|
|
3
|
+
//
|
|
4
|
+
// <FMapCompass :map="map" />
|
|
5
|
+
//
|
|
6
|
+
// ★ IT STAYS, AND DISABLES AT SQUARE. It used to hide itself at north, following Google's mobile
|
|
7
|
+
// behaviour ("the compass will only ever appear when the camera is oriented such that it has a non-zero
|
|
8
|
+
// bearing or non-zero tilt"). Two things make that wrong HERE:
|
|
9
|
+
//
|
|
10
|
+
// · Google's compass sits alone in a corner. Ours sits in a RAIL, so appearing and disappearing
|
|
11
|
+
// shifts every control below it — the layout jumps each time someone nudges the map off north.
|
|
12
|
+
// · Our own zoom pair disables at the zoom limits rather than vanishing. A control that goes inert
|
|
13
|
+
// when it has nothing to do is the pattern this rail already teaches; a control that evaporates is
|
|
14
|
+
// the exception.
|
|
15
|
+
//
|
|
16
|
+
// Present-and-inert also makes the capability discoverable: a pointer user who never guesses the
|
|
17
|
+
// modifier-drag can still see that the map has an orientation to reset. `hideWhenSquare` restores the
|
|
18
|
+
// mobile behaviour for a surface where the compass stands alone and shifts nothing.
|
|
19
|
+
//
|
|
20
|
+
// ★ THREE STATES, TWO CONTINUOUS CHANNELS. The glyph is not a binary indicator:
|
|
21
|
+
//
|
|
22
|
+
// rotateZ(-bearing) where north is
|
|
23
|
+
// rotateX(pitch) how far the view leans away from you
|
|
24
|
+
//
|
|
25
|
+
// The second is Mapbox's own device — its NavigationControl documents `visualizePitch` as "the pitch is
|
|
26
|
+
// visualized by rotating X-axis of compass". So tilt is legible on the compass itself, and FMapTilt is
|
|
27
|
+
// left as what it should be: the way IN to the mode, not a second indicator of it.
|
|
28
|
+
//
|
|
29
|
+
// Fill is the third signal, and the one readable without measuring an angle: outline means square-on,
|
|
30
|
+
// filled means off-axis on either channel.
|
|
31
|
+
//
|
|
32
|
+
// ★ The needle reads the map's OWN bearing and pitch from its `rotate`/`pitch` events rather than
|
|
33
|
+
// mirroring them in state — a second copy drifts during an inertial spin, which is exactly when it is
|
|
34
|
+
// being watched.
|
|
35
|
+
//
|
|
36
|
+
// ★ `navigation-2`, not `navigation`. The needle has to be SYMMETRIC about its vertical axis or its
|
|
37
|
+
// resting position already looks like a heading: `navigation` is a tilted paper plane, so at north it
|
|
38
|
+
// reads as pointing north-east and every rotation carries the glyph's own lean as an offset.
|
|
39
|
+
//
|
|
40
|
+
// The map arrives as a prop for the same reason it does in FMapZoom: <FMap> is a SIBLING, sitting in the
|
|
41
|
+
// HUD's default slot while a rail control sits in a rail, so it is never on the control's ancestor chain.
|
|
42
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
43
|
+
import type mapboxgl from 'mapbox-gl';
|
|
44
|
+
import FCanvasControl from '../canvas/FCanvasControl.vue';
|
|
45
|
+
|
|
46
|
+
const props = withDefaults(
|
|
47
|
+
defineProps<{
|
|
48
|
+
map?: mapboxgl.Map | null;
|
|
49
|
+
/**
|
|
50
|
+
* Hide it entirely while the view is square, the way a phone map does. Only right where the compass
|
|
51
|
+
* stands alone — in a rail it takes its siblings with it every time it appears.
|
|
52
|
+
*/
|
|
53
|
+
hideWhenSquare?: boolean;
|
|
54
|
+
}>(),
|
|
55
|
+
{ map: null, hideWhenSquare: false },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/** What the view is doing, which is what the glyph and the label both read from. */
|
|
59
|
+
export type CompassState = 'square' | 'rotated' | 'tilted';
|
|
60
|
+
|
|
61
|
+
const bearing = ref(0);
|
|
62
|
+
const pitch = ref(0);
|
|
63
|
+
|
|
64
|
+
// Under a degree reads as square to anyone looking, and floating-point camera values rarely land on 0.
|
|
65
|
+
const SQUARE_EPSILON = 1;
|
|
66
|
+
|
|
67
|
+
const rotated = computed(() => Math.abs(bearing.value) >= SQUARE_EPSILON);
|
|
68
|
+
const tilted = computed(() => pitch.value >= SQUARE_EPSILON);
|
|
69
|
+
const state = computed<CompassState>(() =>
|
|
70
|
+
tilted.value ? 'tilted' : rotated.value ? 'rotated' : 'square',
|
|
71
|
+
);
|
|
72
|
+
const square = computed(() => state.value === 'square');
|
|
73
|
+
const visible = computed(() => !!props.map && (!props.hideWhenSquare || !square.value));
|
|
74
|
+
|
|
75
|
+
const sync = () => {
|
|
76
|
+
const m = props.map;
|
|
77
|
+
if (!m) return;
|
|
78
|
+
bearing.value = m.getBearing();
|
|
79
|
+
pitch.value = m.getPitch();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
watch(
|
|
83
|
+
() => props.map,
|
|
84
|
+
(m, _old, onCleanup) => {
|
|
85
|
+
if (!m) return;
|
|
86
|
+
sync();
|
|
87
|
+
m.on('rotate', sync);
|
|
88
|
+
m.on('pitch', sync);
|
|
89
|
+
onCleanup(() => {
|
|
90
|
+
m.off('rotate', sync);
|
|
91
|
+
m.off('pitch', sync);
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
{ immediate: true },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
onBeforeUnmount(() => {
|
|
98
|
+
props.map?.off('rotate', sync);
|
|
99
|
+
props.map?.off('pitch', sync);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ★ easeTo, not jumpTo: the reset is a camera move the operator is watching, and a north-up snap loses
|
|
103
|
+
// the connection between where they were pointing and where they end up. The duration rides the motion
|
|
104
|
+
// tokens like every other travel in the system.
|
|
105
|
+
const reset = () => props.map?.easeTo({ bearing: 0, pitch: 0 });
|
|
106
|
+
|
|
107
|
+
// ★ The label names the axis that is ACTUALLY off. A fixed "reset to north" once announced a heading of
|
|
108
|
+
// 0 degrees on a map that was merely tilted — a state a sighted user can see and a screen-reader user
|
|
109
|
+
// was being told the opposite of.
|
|
110
|
+
const label = computed(() => {
|
|
111
|
+
const deg = Math.round((bearing.value + 360) % 360);
|
|
112
|
+
if (rotated.value && tilted.value) return `Facing ${deg} degrees and tilted — reset the view`;
|
|
113
|
+
if (rotated.value) return `Facing ${deg} degrees — reset to north`;
|
|
114
|
+
if (tilted.value) return 'Tilted — reset to flat';
|
|
115
|
+
return 'Facing north, flat';
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ★ FILL, not a second glyph. `navigation-2`'s path is a closed shape, so `fill-current` turns the same
|
|
119
|
+
// needle solid — the state reads without changing what the icon IS. The obvious alternative,
|
|
120
|
+
// `navigation-2-off`, is the crossed-out variant: in every icon set a slash means DISABLED, and the
|
|
121
|
+
// off-axis state is the one where the control is most alive.
|
|
122
|
+
//
|
|
123
|
+
// ★ And the selector has to reach the PATH, not the <svg>. Lucide puts `fill="none"` on the path itself,
|
|
124
|
+
// and a presentation attribute on an element beats a value INHERITED from its parent — so `fill-current`
|
|
125
|
+
// on the svg is silently inert. Measured: computed fill stayed `none` with the class applied.
|
|
126
|
+
const iconClass = computed(() =>
|
|
127
|
+
square.value
|
|
128
|
+
? ''
|
|
129
|
+
: '[&_[data-slot=leadingIcon]]:text-primary [&_[data-slot=leadingIcon]_path]:fill-current',
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// ★ The pitch lean is DAMPENED. A 60-degree map tilted through a 20px glyph is nearly edge-on and reads
|
|
133
|
+
// as a rendering fault rather than as a state; half the angle keeps the lean legible while staying
|
|
134
|
+
// monotonic with the camera. The needle still says "leaning", which is the whole job.
|
|
135
|
+
const PITCH_VISUAL_RATIO = 0.5;
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<FCanvasControl
|
|
140
|
+
v-if="visible"
|
|
141
|
+
icon="i-lucide-navigation-2"
|
|
142
|
+
:label="label"
|
|
143
|
+
:disabled="square"
|
|
144
|
+
:style="{
|
|
145
|
+
'--forest-compass-bearing': `${-bearing}deg`,
|
|
146
|
+
'--forest-compass-pitch': `${pitch * PITCH_VISUAL_RATIO}deg`,
|
|
147
|
+
}"
|
|
148
|
+
:class="[
|
|
149
|
+
'[&_[data-slot=leadingIcon]]:[transform:perspective(120px)_rotateX(var(--forest-compass-pitch))_rotateZ(var(--forest-compass-bearing))]',
|
|
150
|
+
iconClass,
|
|
151
|
+
]"
|
|
152
|
+
@click="reset"
|
|
153
|
+
/>
|
|
154
|
+
</template>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Where the map is, in numbers.
|
|
3
|
+
//
|
|
4
|
+
// <FMapCoordinates :map="map" />
|
|
5
|
+
//
|
|
6
|
+
// ★ A READOUT, LIKE THE SCALE BAR, and it takes the same corner and the same tile: the chrome surface
|
|
7
|
+
// shared with the controls, and the tighter 8px radius that says a thing is not a control. The two are
|
|
8
|
+
// the only pieces of map chrome that answer a question rather than take an instruction.
|
|
9
|
+
//
|
|
10
|
+
// ★ ON TOUCH THERE IS NO POINTER, so a pointer readout on a phone is a tile that never says anything.
|
|
11
|
+
// `auto` reads the pointer where one exists and the map's centre where it does not — the same shape as
|
|
12
|
+
// <FMap>'s own `lightPreset: 'auto'`: read the environment, and let a surface with a reason pin it.
|
|
13
|
+
//
|
|
14
|
+
// ★ AND IT COPIES. This is the one place a readout earns an affordance. What an operator does with a
|
|
15
|
+
// coordinate is paste it — into a ticket, a message, another tool — and a number that can only be read
|
|
16
|
+
// off the screen is one somebody retypes by hand and gets a digit wrong. The pair is printed the way it
|
|
17
|
+
// is typed: "lat, lng", the opposite of the [lng, lat] the rest of the subsystem speaks.
|
|
18
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
19
|
+
import type mapboxgl from 'mapbox-gl';
|
|
20
|
+
import { coordinatePlaces, formatCoordinates, type LngLat } from './coordinates';
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(
|
|
23
|
+
defineProps<{
|
|
24
|
+
/** The live map, from <FMap @ready>. Null until it arrives, which reads as nothing yet. */
|
|
25
|
+
map?: mapboxgl.Map | null;
|
|
26
|
+
/** `auto` follows the device: the pointer where there is one, the centre where there is not. */
|
|
27
|
+
source?: 'auto' | 'pointer' | 'centre';
|
|
28
|
+
/** Click to copy. Off returns it to a plain, unpressable readout. */
|
|
29
|
+
copy?: boolean;
|
|
30
|
+
}>(),
|
|
31
|
+
{ map: null, source: 'auto', copy: true },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Read once, at setup: a device does not grow a mouse mid-session, and a media query listener here
|
|
35
|
+
// would be machinery for a case that does not arise.
|
|
36
|
+
const coarse = typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches;
|
|
37
|
+
const mode = computed(() => (props.source === 'auto' ? (coarse ? 'centre' : 'pointer') : props.source));
|
|
38
|
+
|
|
39
|
+
const at = ref<LngLat | null>(null);
|
|
40
|
+
const zoom = ref(0);
|
|
41
|
+
const copied = ref(false);
|
|
42
|
+
let clearCopied: ReturnType<typeof setTimeout> | undefined;
|
|
43
|
+
|
|
44
|
+
const text = computed(() =>
|
|
45
|
+
at.value ? formatCoordinates(at.value, coordinatePlaces(zoom.value, at.value[1])) : null,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
watch(
|
|
49
|
+
[() => props.map, mode],
|
|
50
|
+
([m, which], _old, onCleanup) => {
|
|
51
|
+
at.value = null;
|
|
52
|
+
if (!m) return;
|
|
53
|
+
|
|
54
|
+
const fromCentre = () => {
|
|
55
|
+
const c = m.getCenter();
|
|
56
|
+
at.value = [c.lng, c.lat];
|
|
57
|
+
zoom.value = m.getZoom();
|
|
58
|
+
};
|
|
59
|
+
const fromPointer = (e: mapboxgl.MapMouseEvent) => {
|
|
60
|
+
at.value = [e.lngLat.lng, e.lngLat.lat];
|
|
61
|
+
zoom.value = m.getZoom();
|
|
62
|
+
};
|
|
63
|
+
// ★ The tile stays, holding a placeholder, rather than unmounting when the pointer leaves the
|
|
64
|
+
// canvas. A readout that appears and disappears reflows the rail every time a reader reaches for
|
|
65
|
+
// the sidebar, and a stale coordinate left behind would be worse than an em dash.
|
|
66
|
+
const clear = () => {
|
|
67
|
+
at.value = null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (which === 'centre') {
|
|
71
|
+
fromCentre();
|
|
72
|
+
m.on('move', fromCentre);
|
|
73
|
+
onCleanup(() => m.off('move', fromCentre));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
m.on('mousemove', fromPointer);
|
|
78
|
+
m.on('mouseout', clear);
|
|
79
|
+
onCleanup(() => {
|
|
80
|
+
m.off('mousemove', fromPointer);
|
|
81
|
+
m.off('mouseout', clear);
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
{ immediate: true },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
onBeforeUnmount(() => clearTimeout(clearCopied));
|
|
88
|
+
|
|
89
|
+
async function copyPair() {
|
|
90
|
+
if (!text.value || !navigator.clipboard) return;
|
|
91
|
+
try {
|
|
92
|
+
await navigator.clipboard.writeText(text.value);
|
|
93
|
+
} catch {
|
|
94
|
+
// Denied permission, an insecure origin, a browser that refuses outside a trusted gesture. The
|
|
95
|
+
// label simply does not change, which is the honest report — claiming "Copied" over an empty
|
|
96
|
+
// clipboard is the one outcome worth avoiding.
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
copied.value = true;
|
|
100
|
+
clearTimeout(clearCopied);
|
|
101
|
+
clearCopied = setTimeout(() => (copied.value = false), 1200);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The scale bar's tile, exactly — the two are a pair and must read as one material. Kept as a constant
|
|
105
|
+
// rather than duplicated across the two branches below.
|
|
106
|
+
const TILE =
|
|
107
|
+
'forest-canvas-chrome pointer-events-auto block rounded-sm px-2 py-1 ring ring-[var(--neutral-300)] ring-inset dark:ring-[var(--neutral-700)]';
|
|
108
|
+
const INK =
|
|
109
|
+
'text-[0.6875rem] leading-none font-bold tracking-wide tabular-nums text-[var(--neutral-900)] dark:text-[var(--neutral-100)]';
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<template>
|
|
113
|
+
<!-- Pressable only when it can copy. The affordance is a fill and a focus ring, never a second
|
|
114
|
+
radius or a shadow: it is the same tile as the scale bar, with something to do. -->
|
|
115
|
+
<button
|
|
116
|
+
v-if="copy"
|
|
117
|
+
type="button"
|
|
118
|
+
:class="[
|
|
119
|
+
TILE,
|
|
120
|
+
'cursor-pointer transition-[background-color] duration-(--motion-fast) ease-(--motion-ease-out) motion-reduce:transition-none',
|
|
121
|
+
'hover:[--forest-canvas-chrome-fill:var(--ui-bg)]',
|
|
122
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary',
|
|
123
|
+
]"
|
|
124
|
+
:disabled="!text"
|
|
125
|
+
:aria-label="text ? `Copy the coordinate ${text}` : 'No coordinate to copy yet'"
|
|
126
|
+
@click="copyPair()"
|
|
127
|
+
>
|
|
128
|
+
<span :class="INK">{{ copied ? 'Copied' : (text ?? '—') }}</span>
|
|
129
|
+
</button>
|
|
130
|
+
|
|
131
|
+
<div v-else :class="TILE">
|
|
132
|
+
<span :class="INK">{{ text ?? '—' }}</span>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The drawing tool switch — one control that opens into the tools, plus what an unfinished shape needs.
|
|
3
|
+
//
|
|
4
|
+
// <FMapDrawTools
|
|
5
|
+
// v-model:tool="tool"
|
|
6
|
+
// :drafting="!!draw.draft.value"
|
|
7
|
+
// :can-finish="!draw.invalid.value"
|
|
8
|
+
// :can-undo="draw.canUndo.value"
|
|
9
|
+
// @finish="draw.finish" @cancel="draw.cancel" @undo="draw.undo"
|
|
10
|
+
// />
|
|
11
|
+
//
|
|
12
|
+
// ★ THE TRIGGER WEARS THE ACTIVE TOOL, as <FMapTools> does: the question a reader asks passing a map is
|
|
13
|
+
// "what happens if I drag", and a fixed icon would make them open the group to find out.
|
|
14
|
+
//
|
|
15
|
+
// ★ FINISH AND CANCEL ARE CONTROLS, NOT JUST KEYS. Enter and Escape are the fast path and they are not
|
|
16
|
+
// reachable on a phone, so an unfinished ring would be unfinishable there — the same reason region
|
|
17
|
+
// select grew an armed tool instead of a held modifier. They appear only while something is in flight,
|
|
18
|
+
// because a Finish that is always present is a button that usually does nothing.
|
|
19
|
+
//
|
|
20
|
+
// ★ UNDO IS ALWAYS THERE WHILE A TOOL IS ARMED, and disabled when there is nothing to take back. A
|
|
21
|
+
// control that came and went as history filled would move the two beside it every time a vertex landed.
|
|
22
|
+
import { computed } from 'vue';
|
|
23
|
+
import FCanvasControl from '../canvas/FCanvasControl.vue';
|
|
24
|
+
import FCanvasControlGroup from '../canvas/FCanvasControlGroup.vue';
|
|
25
|
+
import FCanvasFlyout from '../canvas/FCanvasFlyout.vue';
|
|
26
|
+
import type { DrawTool } from './draw';
|
|
27
|
+
|
|
28
|
+
const tool = defineModel<DrawTool>('tool', { default: 'off' });
|
|
29
|
+
|
|
30
|
+
const props = withDefaults(
|
|
31
|
+
defineProps<{
|
|
32
|
+
/** whether a ring is being placed right now — reveals finish and cancel */
|
|
33
|
+
drafting?: boolean;
|
|
34
|
+
/** whether the ring in flight can close. A self-crossing one cannot */
|
|
35
|
+
canFinish?: boolean;
|
|
36
|
+
canUndo?: boolean;
|
|
37
|
+
/** The axis the cluster opens along — horizontal for a vertical rail, and the reverse. */
|
|
38
|
+
orientation?: 'vertical' | 'horizontal';
|
|
39
|
+
/** Which end the trigger is pinned to, so the cluster opens inward. */
|
|
40
|
+
side?: 'start' | 'end';
|
|
41
|
+
/** Drop a tool a surface has no use for. Keep `off` — it is how a reader puts one down. */
|
|
42
|
+
tools?: DrawTool[];
|
|
43
|
+
}>(),
|
|
44
|
+
{
|
|
45
|
+
drafting: false,
|
|
46
|
+
canFinish: true,
|
|
47
|
+
canUndo: false,
|
|
48
|
+
orientation: 'horizontal',
|
|
49
|
+
side: 'end',
|
|
50
|
+
tools: () => ['off', 'polygon', 'rectangle', 'circle', 'edit'],
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const emit = defineEmits<{ finish: []; cancel: []; undo: [] }>();
|
|
55
|
+
|
|
56
|
+
const ICON: Record<DrawTool, string> = {
|
|
57
|
+
off: 'i-lucide-hand',
|
|
58
|
+
polygon: 'i-lucide-pentagon',
|
|
59
|
+
rectangle: 'i-lucide-square',
|
|
60
|
+
circle: 'i-lucide-circle',
|
|
61
|
+
edit: 'i-lucide-spline-pointer',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const LABEL: Record<DrawTool, string> = {
|
|
65
|
+
off: 'Pan the map',
|
|
66
|
+
polygon: 'Draw a boundary',
|
|
67
|
+
rectangle: 'Draw a rectangle',
|
|
68
|
+
circle: 'Draw a radius',
|
|
69
|
+
edit: 'Edit a shape',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const items = computed(() =>
|
|
73
|
+
props.tools.map((t) => ({ value: t, icon: ICON[t], label: LABEL[t], pressed: tool.value === t })),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const armed = computed(() => tool.value !== 'off');
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<FCanvasControlGroup :orientation="orientation">
|
|
81
|
+
<FCanvasFlyout
|
|
82
|
+
:icon="ICON[tool]"
|
|
83
|
+
:label="LABEL[tool]"
|
|
84
|
+
:pressed="armed"
|
|
85
|
+
:items="items"
|
|
86
|
+
:orientation="orientation"
|
|
87
|
+
:side="side"
|
|
88
|
+
close-label="Close the tools"
|
|
89
|
+
@select="tool = $event as DrawTool"
|
|
90
|
+
/>
|
|
91
|
+
<FCanvasControl
|
|
92
|
+
v-if="armed"
|
|
93
|
+
icon="i-lucide-undo-2"
|
|
94
|
+
label="Undo"
|
|
95
|
+
:disabled="!canUndo"
|
|
96
|
+
@click="emit('undo')"
|
|
97
|
+
/>
|
|
98
|
+
<template v-if="drafting">
|
|
99
|
+
<!-- Finish is disabled rather than hidden while the ring cannot close: the reader has asked for a
|
|
100
|
+
shape that crosses itself, and a control that vanished would leave them with no statement of
|
|
101
|
+
why nothing happens. The map paints the crossing in the error colour beside it. -->
|
|
102
|
+
<FCanvasControl
|
|
103
|
+
icon="i-lucide-check"
|
|
104
|
+
label="Finish the shape"
|
|
105
|
+
:disabled="!canFinish"
|
|
106
|
+
@click="emit('finish')"
|
|
107
|
+
/>
|
|
108
|
+
<!-- ★ A BIN, NOT A CROSS, and the reason is two controls away. While the flyout is OPEN its
|
|
109
|
+
trigger becomes a dismiss wearing `i-lucide-x`, so a cross here put two identical glyphs in
|
|
110
|
+
one rail — one closing a menu, one destroying the ring the reader is drawing. Tick against
|
|
111
|
+
cross is the tidier pair right up until the cross means two things; the destructive one is
|
|
112
|
+
the one that has to move, because it is the one with no way back. -->
|
|
113
|
+
<FCanvasControl icon="i-lucide-trash-2" label="Discard the shape" @click="emit('cancel')" />
|
|
114
|
+
</template>
|
|
115
|
+
</FCanvasControlGroup>
|
|
116
|
+
</template>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Fullscreen, as canvas chrome.
|
|
3
|
+
//
|
|
4
|
+
// <FMapFullscreen :map="map" />
|
|
5
|
+
//
|
|
6
|
+
// ★ REBUILT, NOT WRAPPED — the split FMapZoom and FMapScale already draw. Mapbox's FullscreenControl
|
|
7
|
+
// mounts into a corner container inside the map element, which is not one of the HUD's areas: it cannot
|
|
8
|
+
// sit in a rail, cannot take the canvas-chrome surface with the rest of the chrome, and cannot step
|
|
9
|
+
// aside for an open side panel. What is underneath is one method and one event, so nothing is lost.
|
|
10
|
+
//
|
|
11
|
+
// ★ THE HUD GOES FULLSCREEN, NOT THE CANVAS. The chrome is a sibling layer ABOVE the canvas inside the
|
|
12
|
+
// HUD, so fullscreening the map element alone drops the zoom pair, the compass, the scale bar and this
|
|
13
|
+
// button at the exact moment there is the most map to operate. Mapbox's control could not have done
|
|
14
|
+
// this — it has never heard of the HUD — which is the second reason not to wrap it.
|
|
15
|
+
//
|
|
16
|
+
// ★ NO RESIZE CODE, and the absence is deliberate rather than an omission. <FMap> observes its
|
|
17
|
+
// CONTAINER rather than the window, because a sidebar collapsing does not resize the window either.
|
|
18
|
+
// Entering fullscreen is that same case, handled by machinery that already exists.
|
|
19
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
20
|
+
import type mapboxgl from 'mapbox-gl';
|
|
21
|
+
import FCanvasControl from '../canvas/FCanvasControl.vue';
|
|
22
|
+
import { fullscreenApi, isFullscreen } from './fullscreen';
|
|
23
|
+
|
|
24
|
+
const props = withDefaults(
|
|
25
|
+
defineProps<{
|
|
26
|
+
/** The live map, from <FMap @ready>. Null until it arrives, which disables the control. */
|
|
27
|
+
map?: mapboxgl.Map | null;
|
|
28
|
+
/**
|
|
29
|
+
* What goes fullscreen. Defaults to the enclosing <FCanvasHud>, and to the map's own container
|
|
30
|
+
* where there is none — pass an element for a surface that frames the map in something wider,
|
|
31
|
+
* a split view or a dashboard tile that should travel with it.
|
|
32
|
+
*/
|
|
33
|
+
target?: HTMLElement | null;
|
|
34
|
+
}>(),
|
|
35
|
+
{ map: null, target: null },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Resolved once. In an SSR pass there is no document at all, and `null` is also the honest answer for
|
|
39
|
+
// a browser that has no fullscreen — see fullscreen.ts.
|
|
40
|
+
const api = fullscreenApi(typeof document === 'undefined' ? null : (document as unknown as Record<string, unknown>));
|
|
41
|
+
|
|
42
|
+
const active = ref(false);
|
|
43
|
+
|
|
44
|
+
const element = computed<HTMLElement | null>(() => {
|
|
45
|
+
if (props.target) return props.target;
|
|
46
|
+
const container = props.map?.getContainer() ?? null;
|
|
47
|
+
return (container?.closest('[data-forest-canvas]') as HTMLElement | null) ?? container;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ★ READ FROM THE DOCUMENT, NEVER FROM A CLICK COUNT. Escape leaves fullscreen without touching this
|
|
51
|
+
// button, and so does the browser's own chrome — a control holding a private boolean would sit lit
|
|
52
|
+
// over a document that had already left. Same rule the tilt toggle follows for pitch.
|
|
53
|
+
const sync = () => {
|
|
54
|
+
active.value = isFullscreen(document as unknown as Record<string, unknown>, element.value);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
watch(
|
|
58
|
+
element,
|
|
59
|
+
() => sync(),
|
|
60
|
+
{ immediate: true },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (api) document.addEventListener(api.event, sync);
|
|
64
|
+
onBeforeUnmount(() => api && document.removeEventListener(api.event, sync));
|
|
65
|
+
|
|
66
|
+
function toggle() {
|
|
67
|
+
const el = element.value;
|
|
68
|
+
if (!api || !el) return;
|
|
69
|
+
const doc = document as unknown as Record<string, () => Promise<void> | void>;
|
|
70
|
+
const node = el as unknown as Record<string, () => Promise<void> | void>;
|
|
71
|
+
// Both can reject — a browser may refuse a request that did not come from a gesture it trusts —
|
|
72
|
+
// and there is nothing useful to do about it: the event never fires, so the button stays unlit and
|
|
73
|
+
// says so. Swallowed rather than left to surface as an unhandled rejection in the console.
|
|
74
|
+
void Promise.resolve(active.value ? doc[api.exit]?.() : node[api.request]?.()).catch(() => {});
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<!-- ★ ABSENT RATHER THAN DISABLED where fullscreen does not exist — Safari on iPhone, or an iframe
|
|
80
|
+
without `allowfullscreen`. Forest's disabled treatment says "not right now", which would be a
|
|
81
|
+
lie about a browser that will never allow it, and a control that can never do its one thing is
|
|
82
|
+
worse than a missing one: it invites a press that fails silently. <FMapCompass> conditions its
|
|
83
|
+
own presence for the same reason. -->
|
|
84
|
+
<FCanvasControl
|
|
85
|
+
v-if="api"
|
|
86
|
+
:icon="active ? 'i-lucide-minimize' : 'i-lucide-maximize'"
|
|
87
|
+
:label="active ? 'Leave fullscreen' : 'Fullscreen'"
|
|
88
|
+
:disabled="!map && !target"
|
|
89
|
+
:pressed="active"
|
|
90
|
+
@click="toggle()"
|
|
91
|
+
/>
|
|
92
|
+
</template>
|