@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.
Files changed (324) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +208 -0
  3. package/app.config.ts +10 -0
  4. package/assets/icons/forest/bike-asterisk.svg +21 -0
  5. package/assets/icons/forest/bike-down.svg +19 -0
  6. package/assets/icons/forest/bike-off.svg +20 -0
  7. package/assets/icons/forest/bike-up.svg +19 -0
  8. package/assets/icons/forest/bike-x.svg +19 -0
  9. package/assets/icons/forest/bike.svg +18 -0
  10. package/assets/icons/forest/mark-fill.svg +6 -0
  11. package/assets/icons/forest/mark.svg +6 -0
  12. package/assets/icons/forest/parking.svg +3 -0
  13. package/assets/icons/forest/star-fill.svg +4 -0
  14. package/assets/icons/forest/star.svg +4 -0
  15. package/assets/icons/forest/traffic-light-caution.svg +8 -0
  16. package/assets/icons/forest/traffic-light-go.svg +8 -0
  17. package/assets/icons/forest/traffic-light-lit.svg +8 -0
  18. package/assets/icons/forest/traffic-light-stop.svg +8 -0
  19. package/assets/icons/forest/traffic-light.svg +8 -0
  20. package/nuxt.config.ts +81 -0
  21. package/package.json +35 -0
  22. package/packages/charts/src/FBoroughShape.vue +52 -0
  23. package/packages/charts/src/FCalendarGrid.vue +332 -0
  24. package/packages/charts/src/FCellLegend.vue +54 -0
  25. package/packages/charts/src/FChartFrame.vue +201 -0
  26. package/packages/charts/src/FChartFrameSingle.vue +91 -0
  27. package/packages/charts/src/FChartLegend.vue +26 -0
  28. package/packages/charts/src/FDistributionBar.vue +224 -0
  29. package/packages/charts/src/FDottedMap.vue +335 -0
  30. package/packages/charts/src/FLondonMap.vue +399 -0
  31. package/packages/charts/src/FSparkline.vue +198 -0
  32. package/packages/charts/src/FStatusTrack.vue +151 -0
  33. package/packages/charts/src/cellSize.ts +46 -0
  34. package/packages/charts/src/distribution.ts +99 -0
  35. package/packages/charts/src/engine.ts +15 -0
  36. package/packages/charts/src/forestTooltip.ts +110 -0
  37. package/packages/charts/src/fromCategories.ts +43 -0
  38. package/packages/charts/src/index.ts +87 -0
  39. package/packages/charts/src/londonAreas.ts +75 -0
  40. package/packages/charts/src/londonGrid.ts +67 -0
  41. package/packages/charts/src/motionDuration.ts +49 -0
  42. package/packages/charts/src/presets.ts +143 -0
  43. package/packages/charts/src/resolveVar.ts +21 -0
  44. package/packages/charts/src/snapTooltip.ts +154 -0
  45. package/packages/charts/src/tooltipStandIn.ts +58 -0
  46. package/packages/charts/src/useCellScale.ts +155 -0
  47. package/packages/charts/src/useChartPalette.ts +61 -0
  48. package/packages/charts/src/useChartRepaintKey.ts +22 -0
  49. package/packages/charts/src/useThemeVersion.ts +17 -0
  50. package/packages/frames/src/FBrowserFrame.vue +53 -0
  51. package/packages/frames/src/FDeviceFrame.vue +242 -0
  52. package/packages/frames/src/FFitFrame.vue +83 -0
  53. package/packages/frames/src/art/iphone-11.svg +124 -0
  54. package/packages/frames/src/art/iphone-12.svg +601 -0
  55. package/packages/frames/src/art/iphone-13-pro.svg +820 -0
  56. package/packages/frames/src/art/iphone-13.svg +849 -0
  57. package/packages/frames/src/art/iphone-14.svg +681 -0
  58. package/packages/frames/src/art/iphone-15-pro.svg +633 -0
  59. package/packages/frames/src/art/iphone-15.svg +713 -0
  60. package/packages/frames/src/art/iphone-16-plus.svg +685 -0
  61. package/packages/frames/src/art/iphone-16-pro-max.svg +611 -0
  62. package/packages/frames/src/art/iphone-16-pro.svg +631 -0
  63. package/packages/frames/src/art/iphone-16.svg +713 -0
  64. package/packages/frames/src/art/iphone-17-pro-max.svg +355 -0
  65. package/packages/frames/src/art/iphone-17.svg +305 -0
  66. package/packages/frames/src/art/iphone-air.svg +389 -0
  67. package/packages/frames/src/art/iphone-se-3.svg +473 -0
  68. package/packages/frames/src/art/iphone-x.svg +652 -0
  69. package/packages/frames/src/art/pixel-10-pro-xl.svg +32 -0
  70. package/packages/frames/src/art/pixel-10-pro.svg +32 -0
  71. package/packages/frames/src/art/pixel-10.svg +32 -0
  72. package/packages/frames/src/art/pixel-8-pro.svg +208 -0
  73. package/packages/frames/src/art/pixel-8.svg +208 -0
  74. package/packages/frames/src/art/pixel-8a.svg +8 -0
  75. package/packages/frames/src/art/pixel-9-pro-xl.svg +32 -0
  76. package/packages/frames/src/art/pixel-9-pro.svg +34 -0
  77. package/packages/frames/src/art/pixel-9a.svg +25 -0
  78. package/packages/frames/src/art.ts +29 -0
  79. package/packages/frames/src/devices.ts +337 -0
  80. package/packages/frames/src/index.ts +15 -0
  81. package/packages/tokens/geo/london-boroughs.json +1 -0
  82. package/packages/tokens/geo/london-dots-coarse.json +1 -0
  83. package/packages/tokens/geo/london-dots-fine.json +1 -0
  84. package/packages/tokens/geo/london-dots-medium.json +1 -0
  85. package/packages/tokens/geo/london-dots-ultra.json +1 -0
  86. package/packages/tokens/geo/london-wards.json +1 -0
  87. package/packages/tokens/glyphs/glyphs.json +1 -0
  88. package/packages/tokens/logos/f-rot.svg +4 -0
  89. package/packages/tokens/logos/f.svg +4 -0
  90. package/packages/tokens/logos/favicon-c2w.svg +9 -0
  91. package/packages/tokens/logos/favicon-console.svg +9 -0
  92. package/packages/tokens/logos/favicon-ds.svg +9 -0
  93. package/packages/tokens/logos/favicon-fleet.svg +9 -0
  94. package/packages/tokens/logos/favicon-flex.svg +9 -0
  95. package/packages/tokens/logos/favicon-plus.svg +9 -0
  96. package/packages/tokens/logos/favicon-radar.svg +9 -0
  97. package/packages/tokens/logos/favicon-tower.svg +9 -0
  98. package/packages/tokens/logos/lockup-horizontal.svg +17 -0
  99. package/packages/tokens/logos/lockup-vertical.svg +17 -0
  100. package/packages/tokens/logos/mark-mono.svg +4 -0
  101. package/packages/tokens/logos/mark.svg +7 -0
  102. package/packages/tokens/logos/wordmark.svg +9 -0
  103. package/packages/tokens/src/colourEngine.ts +604 -0
  104. package/packages/tokens/src/glyphs.ts +127 -0
  105. package/packages/tokens/src/logo.ts +50 -0
  106. package/packages/tokens/src/subbrands.ts +44 -0
  107. package/packages/tokens/src/sublogo.ts +80 -0
  108. package/packages/ui/icons/forest/map/forest-place-count.svg +21 -0
  109. package/packages/ui/icons/forest/map/forest-place-marker.svg +17 -0
  110. package/packages/ui/icons/forest/map/forest-vehicle-marker.svg +73 -0
  111. package/packages/ui/icons/forest/map/manifest.json +373 -0
  112. package/packages/ui/icons/forest/map/place-count-manifest.json +83 -0
  113. package/packages/ui/icons/forest/map/place-manifest.json +74 -0
  114. package/packages/ui/src/address/address.ts +125 -0
  115. package/packages/ui/src/address/autofill.ts +78 -0
  116. package/packages/ui/src/address/geocode.ts +55 -0
  117. package/packages/ui/src/address/index.ts +31 -0
  118. package/packages/ui/src/address/validate.ts +91 -0
  119. package/packages/ui/src/canvas/FCanvasCard.vue +97 -0
  120. package/packages/ui/src/canvas/FCanvasControl.vue +121 -0
  121. package/packages/ui/src/canvas/FCanvasControlGroup.vue +100 -0
  122. package/packages/ui/src/canvas/FCanvasFlyout.vue +216 -0
  123. package/packages/ui/src/canvas/FCanvasHud.vue +402 -0
  124. package/packages/ui/src/canvas/canvasSurface.ts +39 -0
  125. package/packages/ui/src/canvas/index.ts +17 -0
  126. package/packages/ui/src/contexts/ForestContext.vue +53 -0
  127. package/packages/ui/src/contexts/contexts.ts +153 -0
  128. package/packages/ui/src/contexts/index.ts +23 -0
  129. package/packages/ui/src/contexts/marketing-theme.ts +214 -0
  130. package/packages/ui/src/contexts/mobile-theme.ts +402 -0
  131. package/packages/ui/src/filter/FFilterBar.vue +72 -0
  132. package/packages/ui/src/filter/FFilterChip.vue +362 -0
  133. package/packages/ui/src/filter/FFilterClauses.vue +104 -0
  134. package/packages/ui/src/filter/FFilterTrigger.vue +183 -0
  135. package/packages/ui/src/filter/filter.theme.ts +27 -0
  136. package/packages/ui/src/filter/filterClause.ts +265 -0
  137. package/packages/ui/src/filter/filterPredicate.ts +134 -0
  138. package/packages/ui/src/filter/filterSchema.ts +82 -0
  139. package/packages/ui/src/filter/index.ts +36 -0
  140. package/packages/ui/src/forest-preset.ts +68 -0
  141. package/packages/ui/src/icons.ts +79 -0
  142. package/packages/ui/src/index.ts +17 -0
  143. package/packages/ui/src/kpi/FKpi.vue +253 -0
  144. package/packages/ui/src/kpi/FKpiGroup.vue +161 -0
  145. package/packages/ui/src/kpi/index.ts +20 -0
  146. package/packages/ui/src/kpi/kpi.theme.ts +168 -0
  147. package/packages/ui/src/kpi/kpiDelta.ts +82 -0
  148. package/packages/ui/src/kpi/kpiFormat.ts +72 -0
  149. package/packages/ui/src/kpi/kpiGroupState.ts +63 -0
  150. package/packages/ui/src/logo/FLogo.vue +52 -0
  151. package/packages/ui/src/logo/FSubLogo.vue +214 -0
  152. package/packages/ui/src/logo/index.ts +8 -0
  153. package/packages/ui/src/map/FMap.vue +355 -0
  154. package/packages/ui/src/map/FMapCompass.vue +154 -0
  155. package/packages/ui/src/map/FMapCoordinates.vue +134 -0
  156. package/packages/ui/src/map/FMapDrawTools.vue +116 -0
  157. package/packages/ui/src/map/FMapFullscreen.vue +92 -0
  158. package/packages/ui/src/map/FMapGeolocate.vue +208 -0
  159. package/packages/ui/src/map/FMapLayers.vue +75 -0
  160. package/packages/ui/src/map/FMapLegend.vue +45 -0
  161. package/packages/ui/src/map/FMapPopup.vue +129 -0
  162. package/packages/ui/src/map/FMapRamp.vue +69 -0
  163. package/packages/ui/src/map/FMapRegion.vue +77 -0
  164. package/packages/ui/src/map/FMapScale.vue +107 -0
  165. package/packages/ui/src/map/FMapSearch.vue +362 -0
  166. package/packages/ui/src/map/FMapTilt.vue +87 -0
  167. package/packages/ui/src/map/FMapTools.vue +61 -0
  168. package/packages/ui/src/map/FMapZoom.vue +81 -0
  169. package/packages/ui/src/map/FMarker.vue +218 -0
  170. package/packages/ui/src/map/FTrackScrubber.vue +94 -0
  171. package/packages/ui/src/map/FVehicleLegend.vue +72 -0
  172. package/packages/ui/src/map/FVehicleMark.vue +166 -0
  173. package/packages/ui/src/map/bin.ts +196 -0
  174. package/packages/ui/src/map/coordinates.ts +83 -0
  175. package/packages/ui/src/map/density.ts +401 -0
  176. package/packages/ui/src/map/draw.ts +356 -0
  177. package/packages/ui/src/map/ease.ts +67 -0
  178. package/packages/ui/src/map/fullscreen.ts +59 -0
  179. package/packages/ui/src/map/heat.ts +160 -0
  180. package/packages/ui/src/map/index.ts +351 -0
  181. package/packages/ui/src/map/liveMarkers.ts +60 -0
  182. package/packages/ui/src/map/map.css +116 -0
  183. package/packages/ui/src/map/mapColor.ts +31 -0
  184. package/packages/ui/src/map/mapContext.ts +12 -0
  185. package/packages/ui/src/map/mapLayers.ts +55 -0
  186. package/packages/ui/src/map/mapLegend.ts +70 -0
  187. package/packages/ui/src/map/mapSearch.theme.ts +79 -0
  188. package/packages/ui/src/map/mapSearch.ts +279 -0
  189. package/packages/ui/src/map/mapUrl.ts +117 -0
  190. package/packages/ui/src/map/markerAsset.ts +3 -0
  191. package/packages/ui/src/map/padding.ts +39 -0
  192. package/packages/ui/src/map/place.ts +192 -0
  193. package/packages/ui/src/map/ramp.ts +95 -0
  194. package/packages/ui/src/map/regionSelect.ts +163 -0
  195. package/packages/ui/src/map/route.ts +167 -0
  196. package/packages/ui/src/map/scale.ts +329 -0
  197. package/packages/ui/src/map/scaleBar.ts +67 -0
  198. package/packages/ui/src/map/selectionModel.ts +50 -0
  199. package/packages/ui/src/map/sources/coordinates.ts +58 -0
  200. package/packages/ui/src/map/sources/mapboxPlaces.ts +71 -0
  201. package/packages/ui/src/map/style.ts +14 -0
  202. package/packages/ui/src/map/track.ts +279 -0
  203. package/packages/ui/src/map/useFeatureSelect.ts +230 -0
  204. package/packages/ui/src/map/useLiveMarkers.ts +84 -0
  205. package/packages/ui/src/map/useMap.ts +150 -0
  206. package/packages/ui/src/map/useMapDraw.ts +776 -0
  207. package/packages/ui/src/map/useMapOverlay.ts +421 -0
  208. package/packages/ui/src/map/useMapTableSelection.ts +101 -0
  209. package/packages/ui/src/map/useMapUrlState.ts +155 -0
  210. package/packages/ui/src/map/useMarkerSource.ts +163 -0
  211. package/packages/ui/src/map/useRegionSelect.ts +309 -0
  212. package/packages/ui/src/map/useTokenMode.ts +61 -0
  213. package/packages/ui/src/map/useTrackPlayback.ts +131 -0
  214. package/packages/ui/src/map/useVehicleFilter.ts +51 -0
  215. package/packages/ui/src/map/useVisibleFeatures.ts +124 -0
  216. package/packages/ui/src/map/vehicle.ts +807 -0
  217. package/packages/ui/src/map/vehicleFilter.ts +284 -0
  218. package/packages/ui/src/map/vehicleLegend.ts +155 -0
  219. package/packages/ui/src/map/visibleFeatures.ts +192 -0
  220. package/packages/ui/src/map/zone.ts +291 -0
  221. package/packages/ui/src/shell/FAppNavbar.vue +68 -0
  222. package/packages/ui/src/shell/FAppShell.vue +163 -0
  223. package/packages/ui/src/shell/FAppSidebar.vue +215 -0
  224. package/packages/ui/src/shell/FPageToolbar.vue +126 -0
  225. package/packages/ui/src/shell/FScrollShadow.vue +43 -0
  226. package/packages/ui/src/shell/FSidePanel.vue +258 -0
  227. package/packages/ui/src/shell/FToolbarActions.vue +32 -0
  228. package/packages/ui/src/shell/index.ts +33 -0
  229. package/packages/ui/src/shell/nav.ts +37 -0
  230. package/packages/ui/src/shell/pageToolbar.ts +69 -0
  231. package/packages/ui/src/shell/shell.ts +74 -0
  232. package/packages/ui/src/shell/sidePanel.ts +115 -0
  233. package/packages/ui/src/themes/accordion.ts +36 -0
  234. package/packages/ui/src/themes/alert.ts +16 -0
  235. package/packages/ui/src/themes/auth-form.ts +19 -0
  236. package/packages/ui/src/themes/avatar-group.ts +9 -0
  237. package/packages/ui/src/themes/avatar.ts +18 -0
  238. package/packages/ui/src/themes/badge.ts +51 -0
  239. package/packages/ui/src/themes/banner.ts +21 -0
  240. package/packages/ui/src/themes/breadcrumb.ts +10 -0
  241. package/packages/ui/src/themes/button.mechanics.ts +191 -0
  242. package/packages/ui/src/themes/button.ts +136 -0
  243. package/packages/ui/src/themes/calendar.ts +29 -0
  244. package/packages/ui/src/themes/card.ts +67 -0
  245. package/packages/ui/src/themes/carousel.ts +41 -0
  246. package/packages/ui/src/themes/checkbox-group.ts +20 -0
  247. package/packages/ui/src/themes/checkbox.ts +47 -0
  248. package/packages/ui/src/themes/chip.ts +50 -0
  249. package/packages/ui/src/themes/collapsible.ts +26 -0
  250. package/packages/ui/src/themes/color-picker.ts +13 -0
  251. package/packages/ui/src/themes/command-palette.ts +37 -0
  252. package/packages/ui/src/themes/container.ts +14 -0
  253. package/packages/ui/src/themes/context-menu.ts +38 -0
  254. package/packages/ui/src/themes/dashboard-group.ts +11 -0
  255. package/packages/ui/src/themes/dashboard-navbar.ts +10 -0
  256. package/packages/ui/src/themes/dashboard-panel.ts +18 -0
  257. package/packages/ui/src/themes/dashboard-resize-handle.ts +35 -0
  258. package/packages/ui/src/themes/dashboard-sidebar-collapse.ts +7 -0
  259. package/packages/ui/src/themes/dashboard-sidebar-toggle.ts +7 -0
  260. package/packages/ui/src/themes/dashboard-sidebar.ts +58 -0
  261. package/packages/ui/src/themes/dashboard-toolbar.ts +15 -0
  262. package/packages/ui/src/themes/drawer.ts +45 -0
  263. package/packages/ui/src/themes/dropdown-menu.ts +38 -0
  264. package/packages/ui/src/themes/empty.ts +43 -0
  265. package/packages/ui/src/themes/field-group.ts +52 -0
  266. package/packages/ui/src/themes/file-upload.ts +38 -0
  267. package/packages/ui/src/themes/footer-columns.ts +13 -0
  268. package/packages/ui/src/themes/footer.ts +9 -0
  269. package/packages/ui/src/themes/form-field.ts +25 -0
  270. package/packages/ui/src/themes/form.ts +22 -0
  271. package/packages/ui/src/themes/header.ts +12 -0
  272. package/packages/ui/src/themes/icon.ts +61 -0
  273. package/packages/ui/src/themes/index.ts +99 -0
  274. package/packages/ui/src/themes/input-date.ts +73 -0
  275. package/packages/ui/src/themes/input-menu.ts +66 -0
  276. package/packages/ui/src/themes/input-number.ts +58 -0
  277. package/packages/ui/src/themes/input-rating.ts +25 -0
  278. package/packages/ui/src/themes/input-tags.ts +63 -0
  279. package/packages/ui/src/themes/input-time.ts +70 -0
  280. package/packages/ui/src/themes/input.ts +148 -0
  281. package/packages/ui/src/themes/kbd.ts +27 -0
  282. package/packages/ui/src/themes/link.ts +27 -0
  283. package/packages/ui/src/themes/listbox.ts +48 -0
  284. package/packages/ui/src/themes/main.ts +6 -0
  285. package/packages/ui/src/themes/marquee.ts +45 -0
  286. package/packages/ui/src/themes/modal.ts +46 -0
  287. package/packages/ui/src/themes/navigation-menu.ts +80 -0
  288. package/packages/ui/src/themes/page-anchors.ts +7 -0
  289. package/packages/ui/src/themes/page-aside.ts +7 -0
  290. package/packages/ui/src/themes/page-card.ts +22 -0
  291. package/packages/ui/src/themes/page-cta.ts +18 -0
  292. package/packages/ui/src/themes/page-feature.ts +13 -0
  293. package/packages/ui/src/themes/page-header.ts +16 -0
  294. package/packages/ui/src/themes/page-hero.ts +15 -0
  295. package/packages/ui/src/themes/page-links.ts +12 -0
  296. package/packages/ui/src/themes/page-section.ts +14 -0
  297. package/packages/ui/src/themes/page.ts +7 -0
  298. package/packages/ui/src/themes/pagination.ts +17 -0
  299. package/packages/ui/src/themes/pin-input.ts +73 -0
  300. package/packages/ui/src/themes/popover.ts +30 -0
  301. package/packages/ui/src/themes/progress.ts +29 -0
  302. package/packages/ui/src/themes/prose.ts +45 -0
  303. package/packages/ui/src/themes/radio-group.ts +39 -0
  304. package/packages/ui/src/themes/scroll-area.ts +23 -0
  305. package/packages/ui/src/themes/select-menu.ts +71 -0
  306. package/packages/ui/src/themes/select.ts +72 -0
  307. package/packages/ui/src/themes/separator.ts +28 -0
  308. package/packages/ui/src/themes/sidebar.ts +127 -0
  309. package/packages/ui/src/themes/skeleton.ts +15 -0
  310. package/packages/ui/src/themes/slideover.ts +87 -0
  311. package/packages/ui/src/themes/slider.ts +48 -0
  312. package/packages/ui/src/themes/stepper.ts +41 -0
  313. package/packages/ui/src/themes/switch.ts +63 -0
  314. package/packages/ui/src/themes/table.ts +34 -0
  315. package/packages/ui/src/themes/tabs.ts +74 -0
  316. package/packages/ui/src/themes/textarea.ts +77 -0
  317. package/packages/ui/src/themes/timeline.ts +25 -0
  318. package/packages/ui/src/themes/toast.ts +29 -0
  319. package/packages/ui/src/themes/toaster.ts +66 -0
  320. package/packages/ui/src/themes/tooltip.ts +31 -0
  321. package/packages/ui/src/themes/tree.ts +41 -0
  322. package/packages/ui/src/themes/user.ts +28 -0
  323. package/styles.cdn.gen.css +2047 -0
  324. 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>