@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,351 @@
1
+ // Forest's map subsystem — one entry, like @humanforest/charts.
2
+ //
3
+ // The split it commits to: DOM-world things are COMPONENTS (the artefact is DOM, which is what Vue is
4
+ // for — tokens, focus, aria, slots, keyed reconciliation), and GL-world things are COMPOSABLES over
5
+ // style-spec objects (the artefact is GPU state, and mapbox's spec is already declarative).
6
+ //
7
+ // <FMap> the host: instance, lazy WebGL init, token guard, chrome on tokens
8
+ // <FMarker> a marker, declared — pin or pill, tokened, focusable
9
+ // <FVehicleMark> ONE vehicle, painted from the uploaded asset. Works with no map at all
10
+ // useMarkerSource() many markers from one clustered source — only the visible ones get an element
11
+ // useLiveMarkers() glide those between fixes. DOM tier only: the GL fleet snaps, on purpose
12
+ // densityLayers() how MUCH, as counted cells you can point at and read off a legend
13
+ // heatLayers() the same question as a FIELD — no cell, no unit, no buckets; ramp is a prop
14
+ // balanceLayers() a SIGNED gap around zero — surplus against deficit, magnitude kept
15
+ // <FMapRamp> the key to a quantity: ordered buckets, a stated unit, a stated cell size
16
+ // bin.ts the H3 seam — cell sizes, the zoom→resolution ladder, joins and robust domains
17
+ // <FMapPopup> a popup whose content is a Vue slot
18
+ // <FMapSearch> one field over several sources — places, vehicles, bays — grouped and keyboard-driven
19
+ // <FMapScale> the scale bar, measured off the canvas so latitude and pitch are accounted for
20
+ // <FMapCoordinates> where the map is, in numbers — at the precision the camera has earned
21
+ // <FMapFullscreen> fills the screen with the HUD, chrome and all, rather than with a bare canvas
22
+ // <FMapRegion> the lasso, box or circle a region gesture is drawing — paint only, no listeners
23
+ // <FMapTools> the tool switch: one control at rest, opening in place to the four
24
+ // <FMapDrawTools> the same switch for drawing, plus finish and cancel while a ring is in flight
25
+ // useMapDraw() author a boundary — the one seam here that WRITES geometry rather than reading it
26
+ // <FMapLegend> the key to what is painted — swatches drawn the way their layers are drawn
27
+ // <FMapLayers> the same rows, made operable. The model holds what is HIDDEN, never the map
28
+ // <FVehicleLegend> the key to a vehicle mark — a section per axis, each varying one channel
29
+ // useRegionSelect() drag a region and feed useFeatureSelect's set: replace, shift-add, alt-subtract
30
+ // useMapOverlay() sources, layers and basemap config as data, with paint order and tokens owned
31
+ // scale.ts the fleet seam: crossover zoom, mass-tier ramps, symbol defaults
32
+ // useFeatureSelect() hit-testing at 3px and selection-as-a-set on a GL layer
33
+ // vehicle.ts the three axes (state / connectivity / battery) and how a marker encodes them
34
+ // useTokenMode() ticks when the mode LANDS on <html> — the fix for resolved-token staleness
35
+ // useMapReady() the enclosing map, once its style has loaded
36
+ // useMapCamera() camera moves that respect prefers-reduced-motion
37
+ // token() marks a paint value as token-derived
38
+ // MAP_SLOT which Standard slot a layer belongs in — marks go ABOVE the 3D buildings
39
+ // MARK_CLAIMS_SPACE symbol layout that makes a colliding basemap LABEL yield to the mark
40
+ // resolveMapColor() the low-level token → sRGB step, for driving the raw instance
41
+ //
42
+ // The raw instance stays first-class: `@ready`, the default slot's `map`, and `defineExpose({ map })`
43
+ // are the contract, not a failure mode. Anything unwrapped must be reachable without ceremony.
44
+ export { default as FMap } from './FMap.vue';
45
+ export { FOREST_MAP_STYLE } from './style';
46
+ export { default as FMarker } from './FMarker.vue';
47
+ // ★ NOT a map component — it needs no <FMap>. The vehicle mark, painted from the uploaded asset, for a list
48
+ // row or a detail header as much as for FMarker's default slot. See the file's own header.
49
+ export { default as FVehicleMark } from './FVehicleMark.vue';
50
+ export { default as FMapPopup } from './FMapPopup.vue';
51
+ // Map-aware canvas controls. They render FCanvasControl, so they sit in a HUD rail like any other
52
+ // chrome — which Mapbox's own controls cannot, mounting into its corner containers instead. Each takes
53
+ // the map as a PROP: FMap sits in the HUD's default slot and the control in a rail, so they are
54
+ // siblings and no injection chain runs between them.
55
+ export { default as FMapZoom } from './FMapZoom.vue';
56
+ export { default as FMapCompass } from './FMapCompass.vue';
57
+ export { default as FMapGeolocate } from './FMapGeolocate.vue';
58
+ export { default as FMapTilt } from './FMapTilt.vue';
59
+ export { default as FMapFullscreen } from './FMapFullscreen.vue';
60
+ export { default as FMapSearch } from './FMapSearch.vue';
61
+ export { default as FMapScale } from './FMapScale.vue';
62
+ export { default as FMapCoordinates } from './FMapCoordinates.vue';
63
+ export { default as FMapRegion } from './FMapRegion.vue';
64
+ export { default as FMapTools } from './FMapTools.vue';
65
+ export { default as FMapDrawTools } from './FMapDrawTools.vue';
66
+ // Not a control — it takes no map and operates nothing. It reads as chrome because it sits in the HUD,
67
+ // but it is a list of meanings, and works anywhere a reader needs one.
68
+ export { default as FMapLegend } from './FMapLegend.vue';
69
+ export type { MapLegendItem } from './mapLegend';
70
+ // The same rows with a control on each — the legend, made operable. Takes no map: the model feeds
71
+ // `visible:` in the surface's own OverlaySpec, which useMapOverlay applies and would otherwise revert.
72
+ export { default as FMapLayers } from './FMapLayers.vue';
73
+ export { legendSwatchStyle } from './mapLegend';
74
+ export { sectionState, toggleKey, toggleSection } from './mapLayers';
75
+ export type { MapLayerSection, SectionState } from './mapLayers';
76
+ // The vehicle mark's own key. Not an FMapLegend variant: its swatch is a rendered <FVehicleMark>, which
77
+ // a colour-and-dash item cannot describe, and its rows are grouped by AXIS rather than listed.
78
+ export { default as FVehicleLegend } from './FVehicleLegend.vue';
79
+ export { vehicleLegendSections } from './vehicleLegend';
80
+ export type { VehicleLegendAxis, VehicleLegendRow, VehicleLegendSection } from './vehicleLegend';
81
+ // Drawing — the seam that WRITES. Everything else in this module reads a payload someone else
82
+ // authored; this one hands a ring back. The arithmetic is separable from the map on purpose (node runs
83
+ // the suite), so the guards and the ring operations are importable on their own.
84
+ export { useMapDraw } from './useMapDraw';
85
+ export type { MapDrawOptions } from './useMapDraw';
86
+ export {
87
+ CIRCLE_SEGMENTS,
88
+ MIN_DRAW_PX,
89
+ SNAP_PX,
90
+ boxCorners,
91
+ closeRing,
92
+ drawFeatureToGeoJSON,
93
+ geodesicRing,
94
+ // metresBetween stays inside draw.ts. It is a haversine, which is the kind of helper every geometry
95
+ // module grows its own copy of, and a bare name that generic on the map's public surface is a
96
+ // collision waiting for the next one.
97
+ midpoints,
98
+ ringProblem,
99
+ segmentsCross,
100
+ selfIntersects,
101
+ } from './draw';
102
+ export type { DrawFeature, DrawProblem, DrawRing, DrawTool } from './draw';
103
+ export { useRegionSelect } from './useRegionSelect';
104
+ export type { RegionSelectOptions, RegionSelectTarget } from './useRegionSelect';
105
+ export {
106
+ } from './regionSelect';
107
+ export type { RegionMode, RegionShape, RegionTool, ScreenBox, ScreenPoint } from './regionSelect';
108
+ export { coordinatePlaces, formatCoordinates, parseCoordinates } from './coordinates';
109
+ export type { LngLat, ParseCoordinatesOptions } from './coordinates';
110
+ export { fullscreenApi, isFullscreen } from './fullscreen';
111
+ export type { FullscreenApi } from './fullscreen';
112
+ export { roundedDistance, scaleReading } from './scaleBar';
113
+ export type { ScaleReading, ScaleUnit } from './scaleBar';
114
+ // The time axis. One list of fixes, four readings of it: a route is the track painted, playback is it
115
+ // read at a time, a trail is its tail, and a live vehicle is a track of length two growing at the head.
116
+ export {
117
+ MAX_GAP_MS,
118
+ TRAIL_WINDOW_MS,
119
+ // `advance`, `fitSpeed`, `elapsedLabel` and MAX_FRAME_MS are the clock's own arithmetic, kept in
120
+ // track.ts so a node test can reach them. useTrackPlayback and FTrackScrubber are the public way in.
121
+ // metresBetween and bearingBetween stay inside track.ts, for the reason draw.ts keeps its own
122
+ // haversine private: names that generic on a shared public surface collide with the next module
123
+ // that needs one. `trackDistance` is the public answer for distance, and a bearing arrives on the
124
+ // TrackPoint that needed it.
125
+ positionAt,
126
+ progressAt,
127
+ trackBounds,
128
+ trackDistance,
129
+ trackLine,
130
+ trackSegments,
131
+ trackSlice,
132
+ trackSpan,
133
+ trackTail,
134
+ } from './track';
135
+ export type { Fix, Track, TrackPoint, TrackSpan } from './track';
136
+ export {
137
+ ROUTE_CASING_WIDTH,
138
+ ROUTE_WIDTH,
139
+ TRAIL_WIDTH,
140
+ routeLayers,
141
+ routeSource,
142
+ trailLayers,
143
+ // The played portion is its OWN geometry, sliced from the track. `line-progress` indexes distance
144
+ // and a playhead is a fraction of time, so a gradient stop trails the mark by every second the
145
+ // vehicle spent halted or out of signal.
146
+ travelledLayers,
147
+ } from './route';
148
+ export type { RouteLayerOptions, TrailLayerOptions, TravelledLayerOptions } from './route';
149
+ export { useTrackPlayback } from './useTrackPlayback';
150
+ export type { TrackPlayback, TrackPlaybackOptions } from './useTrackPlayback';
151
+ // The live tier: the viewport's DOM markers glide between fixes. The GL fleet keeps snapping on
152
+ // purpose — a full re-upload is 222 ms per tick at 30k, which a 2–5 s cadence absorbs and a frame
153
+ // does not.
154
+ export { useLiveMarkers } from './useLiveMarkers';
155
+ export type { LiveMarkerOptions } from './useLiveMarkers';
156
+ // `liveMarkers.ts` stays module-internal — `advanceLeg`, `legAt` and the two tween helpers are the
157
+ // composable's own mechanics, and the tests import them from the file rather than from here. Names
158
+ // that generic on a shared surface are a collision waiting for the next module that needs one.
159
+ // Not a control: it takes no map and operates nothing. It scrubs a track, and works anywhere one is
160
+ // being read.
161
+ export { default as FTrackScrubber } from './FTrackScrubber.vue';
162
+ export type { MapSearchResult, MapSearchSource } from './mapSearch';
163
+
164
+ export { FOREST_MAP, FOREST_MARKER } from './mapContext';
165
+ export { resolveMapColor } from './mapColor';
166
+ // useLazyHost stays internal: it exists so FMarker and FMapPopup can create the element mapbox owns
167
+ // without touching `document` during setup. It is not a seam a surface should reach for — the public
168
+ // way to attach to a map is useMapReady, or the injection keys above for a bespoke child.
169
+ export { useMapCamera, useMapReady, useMapScaleFactor, readerTextScale } from './useMap';
170
+ export { MAP_SLOT, MARK_CLAIMS_SPACE, token, useMapOverlay } from './useMapOverlay';
171
+ export type { OverlayLayer, OverlaySpec } from './useMapOverlay';
172
+ export {
173
+ DISSOLVE_START_ZOOM,
174
+ FLEET_SYMBOL_LAYOUT,
175
+ MARKER_CROSSOVER_ZOOM,
176
+ MARKER_MIN_ICON_SIZE,
177
+ MARKER_PX,
178
+ MARKER_SIZE_SCALE_RANGE,
179
+ fleetCircleOpacity,
180
+ fleetCircleRadius,
181
+ fleetCircleStroke,
182
+ fleetSymbolOpacity,
183
+ markerIconSize,
184
+ } from './scale';
185
+ export {
186
+ AVAILABLE_BATTERY,
187
+ AVAILABLE_STATES,
188
+ BATTERY_RING,
189
+ BATTERY_RING_BAND,
190
+ IN_USE_GRADIENT,
191
+ LOCKED_MARK,
192
+ RENTAL_TAKEN,
193
+ RENTAL_PAUSED,
194
+ batteryRingToken,
195
+ STATE_GLYPH,
196
+ STATE_LABEL,
197
+ stateLabel,
198
+ markerImageExpression,
199
+ markerGlyphCrossFade,
200
+ OFFLINE_DOT,
201
+ markerParams,
202
+ paintMarkerSvg,
203
+ BADGE_SHAPES,
204
+ BADGE_PARAMS,
205
+ BADGE_COLOUR,
206
+ BADGE_COLOURS,
207
+ BADGE_SAFE,
208
+ BADGE_EDGE,
209
+ MARKER_CANVAS,
210
+ STATE_GROUP_COLOUR,
211
+ SOC_THRESHOLDS,
212
+ STATE_GROUP,
213
+ groupMatchInput,
214
+ labelOf,
215
+ isAvailable,
216
+ unavailableReason,
217
+ unavailableReasons,
218
+ } from './vehicle';
219
+ export type {
220
+ BadgeShape,
221
+ BadgeColour,
222
+ BatteryLevel,
223
+ BatteryRingBand,
224
+ MarkerBadge,
225
+ RentalState,
226
+ StateGroup,
227
+ VehicleFacts,
228
+ VehicleState,
229
+ } from './vehicle';
230
+ // Places — the fixed things an operation works from. A place is not a vehicle: different shape,
231
+ // different vocabulary, no battery and no connectivity. Zones are paint, not marks — see zone.ts.
232
+ export {
233
+ PLACE_STATE_COLOUR,
234
+ placeState,
235
+ placeColour,
236
+ WAREHOUSE_COLOUR,
237
+ COUNT_GLYPH,
238
+ PLACE_COUNT_LAYOUT,
239
+ PLACE_GLYPH,
240
+ placeImageExpression,
241
+ placeParams,
242
+ } from './place';
243
+ export type { Place, PlaceColours, PlaceKind, PlaceState } from './place';
244
+ // Zones — a rule with a boundary. Paint, never a mark, and AUDIENCE decides which ones exist at all.
245
+ export {
246
+ ZONE_AUDIENCE,
247
+ ZONE_COLOUR,
248
+ ZONE_DASH,
249
+ ZONE_FILL_OPACITY,
250
+ ZONE_LABEL,
251
+ ZONE_RANK,
252
+ ZONE_RULE,
253
+ ZONE_RULE_TEXT,
254
+ zoneFeatures,
255
+ zoneKinds,
256
+ zoneLayers,
257
+ zoneLegendItems,
258
+ zoneVisibleTo,
259
+ } from './zone';
260
+ export type { ZoneAudience, ZoneKind, ZoneLayerOptions, ZoneRule } from './zone';
261
+ export { onTokenModeChange, useTokenMode } from './useTokenMode';
262
+ export { useFeatureSelect } from './useFeatureSelect';
263
+ export type { FeatureSelectOptions, PickedFeature } from './useFeatureSelect';
264
+ export { useMarkerSource } from './useMarkerSource';
265
+ export type { MarkerSourceItem, MarkerSourceOptions } from './useMarkerSource';
266
+ // Density — how MUCH, as ground paint. `density.ts` is the paint doctrine (heat for the glance, bins
267
+ // for the decision); `bin.ts` is the H3 seam, which takes cell GEOMETRY in and therefore needs no h3
268
+ // dependency. <FMapRamp> is the ordered legend a categorical one cannot draw.
269
+ export { default as FMapRamp } from './FMapRamp.vue';
270
+ export {
271
+ DENSITY_ALPHA,
272
+ DENSITY_STEPS,
273
+ BALANCE_STEPS,
274
+ balanceLayers,
275
+ balanceLegend,
276
+ balanceStops,
277
+ densityLayers,
278
+ densityLegend,
279
+ densityStops,
280
+ stepColour,
281
+ stepEdges,
282
+ } from './density';
283
+ export type { BalanceOptions, DensityOptions, LegendOptions, RampLegend, RampStop } from './density';
284
+ // The kernel field, and it is a separate module on purpose: it shares nothing with bins but a basemap.
285
+ // No cell, no id, no resolution ladder, no H3 — raw events and a radius somebody chose. Its ramp and
286
+ // its ramp is a PROP where the bin ramp is fixed — but it has NO bucket count at all, because a
287
+ // field is continuous and nobody matches a pixel to a swatch.
288
+ export { HEAT_FLOOR, HEAT_RAMPS, heatLayers, heatRadiusByZoom } from './heat';
289
+ // One place samples the engine's sequential ramps and reverses them after dark — both mechanisms build
290
+ // their colours through it, so neither can ship a ramp that means the opposite thing at night.
291
+ export {
292
+ BALANCE_CENTRE_TOKEN,
293
+ DIVERGING_RAMPS,
294
+ RAMP_LENGTH,
295
+ SEQUENTIAL_RAMPS,
296
+ divergingStops,
297
+ prefersInvertedRamp,
298
+ rampStops,
299
+ } from './ramp';
300
+ export type { DivergingRamp, SequentialRamp } from './ramp';
301
+ export type { HeatOptions, HeatRamp } from './heat';
302
+ export {
303
+ CELL_PX_BAND,
304
+ H3_EDGE_M,
305
+ H3_RES_RANGE,
306
+ cellFitsBand,
307
+ cellWidthPx,
308
+ h3ResForZoom,
309
+ joinCellValues,
310
+ metresPerPixel,
311
+ robustDomain,
312
+ symmetricDomain,
313
+ } from './bin';
314
+ export type { CellCollection, Domain, JoinOptions } from './bin';
315
+ // ── State ───────────────────────────────────────────────────────────────────────────────────────
316
+ //
317
+ // A console is not one view of a map. It is a map, a list, a set of filters and a link somebody
318
+ // pastes into Slack, and these four keep those in step. Each is a pure core plus a thin wrapper: the
319
+ // core takes plain values so the node suite can exercise every edge, the wrapper owns the events.
320
+ //
321
+ // useMapUrlState where the reader was looking, in the address bar — replaceState, never push
322
+ // useVisibleFeatures what is on screen right now, deduped across tile seams and capped
323
+ // useMapTableSelection the map's selection and a table's, as one thing
324
+ // useVehicleFilter one filter, rendered as a GL expression, a predicate and a query string
325
+ export { useMapUrlState } from './useMapUrlState';
326
+ export type { MapUrlAdapter, MapUrlStateOptions } from './useMapUrlState';
327
+ export { MAP_URL_PRECISION, decodeCamera, encodeCamera, patchQuery, readQuery, sameCamera } from './mapUrl';
328
+ export type { MapCamera } from './mapUrl';
329
+ export { useVisibleFeatures } from './useVisibleFeatures';
330
+ export type { VisibleFeaturesOptions } from './useVisibleFeatures';
331
+ export { VISIBLE_LIMIT, boxContains, collectVisible, featureAnchor, featureInBox, featureKey } from './visibleFeatures';
332
+ export type { CollectOptions, ViewBox, VisibleFeature } from './visibleFeatures';
333
+ export { useMapTableSelection } from './useMapTableSelection';
334
+ export type { FeatureSelectionApi, MapTableSelectionOptions } from './useMapTableSelection';
335
+ export { rowKey, sameSelection, selectedKeys, toRowSelection } from './selectionModel';
336
+ export type { RowSelection } from './selectionModel';
337
+ export { useVehicleFilter } from './useVehicleFilter';
338
+ // VEHICLE_FILTER_AXES and VEHICLE_FILTER_LIST_AXES stay inside the module: nothing needs to iterate
339
+ // the axes, and the list type — which `toggle` puts in a caller's signature — is exported on its own.
340
+ export {
341
+ activeAxisCount,
342
+ availableExpression,
343
+ decodeVehicleFilter,
344
+ encodeVehicleFilter,
345
+ filterStates,
346
+ isEmptyVehicleFilter,
347
+ toggleVehicleFilter,
348
+ vehicleFilterExpression,
349
+ vehicleFilterMatch,
350
+ } from './vehicleFilter';
351
+ export type { VehicleFilter, VehicleFilterListAxis, VehicleFilterProps } from './vehicleFilter';
@@ -0,0 +1,60 @@
1
+ // The arithmetic behind a marker that glides between fixes. Pure, so a node test can hold it.
2
+ //
3
+ // A leg is one hop: where the marker was when the fix landed, where the fix says it is, and when the
4
+ // hop started. Everything the composable does is bookkeeping over a map of these.
5
+
6
+ export interface Leg {
7
+ from: [number, number];
8
+ to: [number, number];
9
+ /** the clock reading the hop began at */
10
+ startedAt: number;
11
+ }
12
+
13
+ /** Linear between two positions. At a few seconds per fix the great-circle correction is sub-pixel. */
14
+ export function tweenPosition(from: [number, number], to: [number, number], t: number): [number, number] {
15
+ return [from[0] + (to[0] - from[0]) * t, from[1] + (to[1] - from[1]) * t];
16
+ }
17
+
18
+ /**
19
+ * How far through the expected interval `now` is, 0–1.
20
+ *
21
+ * ★ Clamped at 1. A fix arriving late is normal, and running past the target would extrapolate a
22
+ * position nothing measured — a vehicle confidently riding into the river.
23
+ */
24
+ export function tweenFraction(now: number, startedAt: number, ms: number): number {
25
+ if (ms <= 0) return 1;
26
+ return Math.min(1, Math.max(0, (now - startedAt) / ms));
27
+ }
28
+
29
+ /** Where a leg has reached by `now`. */
30
+ export function legAt(leg: Leg, now: number, ms: number): [number, number] {
31
+ return tweenPosition(leg.from, leg.to, tweenFraction(now, leg.startedAt, ms));
32
+ }
33
+
34
+ /**
35
+ * The leg a key should be on, given the position its latest fix reports.
36
+ *
37
+ * Three cases, and the first two are the ones that go wrong if they are not stated:
38
+ *
39
+ * ★ A KEY SEEN FOR THE FIRST TIME APPEARS WHERE IT IS. Its leg starts and ends at the same point, so
40
+ * nothing animates. Otherwise a marker entering the viewport flies in from wherever the previous
41
+ * occupant of that slot sat — a vehicle that was always parked there would slide across the screen
42
+ * the moment a pan brought it into view.
43
+ *
44
+ * ★ AN UNCHANGED TARGET KEEPS THE SAME LEG OBJECT. Returning a fresh leg would reset `startedAt`
45
+ * every evaluation, the fraction would never leave zero, and the marker would crawl instead of
46
+ * arriving.
47
+ *
48
+ * ★ A NEW FIX MID-GLIDE BENDS FROM WHERE THE MARKER IS. Starting the new leg from the old leg's
49
+ * `from` would snap the marker backwards up the road each time a fix landed.
50
+ */
51
+ export function advanceLeg(
52
+ leg: Leg | undefined,
53
+ target: [number, number],
54
+ now: number,
55
+ ms: number,
56
+ ): Leg {
57
+ if (!leg) return { from: target, to: target, startedAt: now };
58
+ if (leg.to[0] === target[0] && leg.to[1] === target[1]) return leg;
59
+ return { from: legAt(leg, now, ms), to: target, startedAt: now };
60
+ }
@@ -0,0 +1,116 @@
1
+ /* Mapbox chrome on Forest tokens. mapbox-gl.css hardcodes #fff surfaces, #333 text and 3–4px radii
2
+ on everything it creates at runtime — popups, controls, attribution, scale. Left alone those are
3
+ white boxes in dark mode (and Forest text classes inside a popup go near-white on near-white), and
4
+ their corners ignore --ui-radius. Every rule here re-points one of those literals at a token.
5
+ Imported by FMap after mapbox-gl.css, so equal-specificity rules land last and win.
6
+
7
+ Radius follows the contract: chrome is a control/panel surface, so 12px — calc(--ui-radius × 1.5).
8
+ Control glyphs are background-image data URIs with baked-in dark strokes, so dark mode inverts
9
+ them rather than recolouring (hue-rotate keeps the coloured ones, e.g. active geolocate, on hue). */
10
+
11
+ /* ── Popup ───────────────────────────────────────────────────────────────── */
12
+ .mapboxgl-popup-content {
13
+ background: var(--ui-bg);
14
+ color: var(--ui-text);
15
+ border-radius: calc(var(--ui-radius) * 1.5);
16
+ box-shadow: 0 4px 12px -2px rgb(0 0 0 / 0.12), 0 0 0 1px var(--ui-border);
17
+ padding: 0.625rem 0.75rem;
18
+ font-family: var(--font-sans);
19
+ }
20
+
21
+ /* The tip is a CSS triangle: the visible edge is whichever border side faces the anchor. */
22
+ .mapboxgl-popup-anchor-top .mapboxgl-popup-tip,
23
+ .mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip,
24
+ .mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
25
+ border-bottom-color: var(--ui-bg);
26
+ }
27
+ .mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip,
28
+ .mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip,
29
+ .mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
30
+ border-top-color: var(--ui-bg);
31
+ }
32
+ .mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
33
+ border-right-color: var(--ui-bg);
34
+ }
35
+ .mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
36
+ border-left-color: var(--ui-bg);
37
+ }
38
+
39
+ .mapboxgl-popup-close-button {
40
+ color: var(--ui-text-dimmed);
41
+ border-radius: calc(var(--ui-radius) * 1.5);
42
+ width: 1.5rem;
43
+ height: 1.5rem;
44
+ font-size: 1rem;
45
+ line-height: 1;
46
+ }
47
+ .mapboxgl-popup-close-button:hover {
48
+ background: var(--ui-bg-elevated);
49
+ color: var(--ui-text);
50
+ }
51
+
52
+ /* ── Controls ────────────────────────────────────────────────────────────── */
53
+ /* Selectors mirror mapbox's own specificity — `:not(:empty)`, the compound `.mapboxgl-ctrl.…`, the
54
+ button corner rules. A plainer selector silently loses the cascade, and !important is not the fix. */
55
+ .mapboxgl-ctrl-group,
56
+ .mapboxgl-ctrl-group:not(:empty) {
57
+ background: var(--ui-bg);
58
+ border-radius: calc(var(--ui-radius) * 1.5);
59
+ box-shadow: 0 2px 8px -2px rgb(0 0 0 / 0.12), 0 0 0 1px var(--ui-border);
60
+ }
61
+ /* Buttons carry their own 4px corners, which would peek out of a 12px group. */
62
+ .mapboxgl-ctrl-group button:first-child {
63
+ border-radius: calc(var(--ui-radius) * 1.5) calc(var(--ui-radius) * 1.5) 0 0;
64
+ }
65
+ .mapboxgl-ctrl-group button:last-child {
66
+ border-radius: 0 0 calc(var(--ui-radius) * 1.5) calc(var(--ui-radius) * 1.5);
67
+ }
68
+ .mapboxgl-ctrl-group button + button {
69
+ border-top-color: var(--ui-border);
70
+ }
71
+ .mapboxgl-ctrl-group button:hover {
72
+ background: var(--ui-bg-elevated);
73
+ }
74
+ /* Mapbox glows blue on plain :focus, i.e. also on a mouse press. Forest shows a ring on keyboard
75
+ focus only. */
76
+ .mapboxgl-ctrl-group button:focus {
77
+ box-shadow: none;
78
+ }
79
+ .mapboxgl-ctrl-group button:focus-visible {
80
+ outline: 2px solid var(--ui-primary);
81
+ outline-offset: -2px;
82
+ }
83
+ .dark .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon {
84
+ filter: invert(1) hue-rotate(180deg);
85
+ }
86
+
87
+ /* ── Attribution + scale ─────────────────────────────────────────────────── */
88
+ /* Attribution stays legible in both modes — it is a Mapbox terms requirement, not decoration. */
89
+ .mapboxgl-ctrl.mapboxgl-ctrl-attrib {
90
+ background-color: color-mix(in oklab, var(--ui-bg) 80%, transparent);
91
+ font-family: var(--font-sans);
92
+ }
93
+ /* Collapsed pill (small maps): opaque, and on the radius contract. */
94
+ .mapboxgl-ctrl-attrib.mapboxgl-compact {
95
+ background-color: var(--ui-bg);
96
+ border-radius: calc(var(--ui-radius) * 1.5);
97
+ }
98
+ .mapboxgl-ctrl-attrib,
99
+ .mapboxgl-ctrl-attrib a {
100
+ color: var(--ui-text-muted);
101
+ }
102
+ .dark .mapboxgl-ctrl-attrib-button {
103
+ filter: invert(1) hue-rotate(180deg);
104
+ }
105
+
106
+ .mapboxgl-ctrl-scale {
107
+ background-color: color-mix(in oklab, var(--ui-bg) 75%, transparent);
108
+ border-color: var(--ui-border-accented);
109
+ color: var(--ui-text-muted);
110
+ font-family: var(--font-sans);
111
+ }
112
+
113
+ /* ── Cooperative-gesture scrim ───────────────────────────────────────────── */
114
+ .mapboxgl-cooperative-gesture-screen {
115
+ font-family: var(--font-sans);
116
+ }
@@ -0,0 +1,31 @@
1
+ // The token → GL colour seam, in one place.
2
+ //
3
+ // GL paint properties and basemap config take sRGB strings. Forest tokens are wide-gamut, so
4
+ // `getComputedStyle` hands back `color(display-p3 …)` — which mapbox's style parser rejects outright.
5
+ // A 1×1 canvas parses that value and reads it back gamut-mapped to sRGB bytes.
6
+ //
7
+ // DOM markers are exempt: they take CSS vars directly. This is only for GL layers and basemap config.
8
+ // Prefer `token()` + `useMapOverlay`, which calls this and re-resolves on a colour-mode flip; reach
9
+ // for it directly only when driving the raw map instance.
10
+ let probe: HTMLCanvasElement | null = null;
11
+
12
+ export function resolveMapColor(expr: string): string {
13
+ const el = document.createElement('div');
14
+ el.style.color = expr;
15
+ el.style.display = 'none';
16
+ document.body.appendChild(el);
17
+ const computed = getComputedStyle(el).color;
18
+ el.remove();
19
+
20
+ if (!probe) {
21
+ probe = document.createElement('canvas');
22
+ probe.width = probe.height = 1;
23
+ }
24
+ const ctx = probe.getContext('2d');
25
+ if (!ctx) return computed;
26
+ ctx.clearRect(0, 0, 1, 1);
27
+ ctx.fillStyle = computed;
28
+ ctx.fillRect(0, 0, 1, 1);
29
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
30
+ return `rgb(${r}, ${g}, ${b})`;
31
+ }
@@ -0,0 +1,12 @@
1
+ // Injection seam between the map primitives. <FMap> provides the map instance; <FMarker> provides
2
+ // the marker it created. Anything nested finds its parent without prop-drilling, so a page can write
3
+ // <FMap><FMarker><FMapPopup>…</FMapPopup></FMarker></FMap> and nothing has to thread refs through.
4
+ //
5
+ // Both are shallowRefs holding a mapbox object, and both start null: FMap creates the map lazily
6
+ // (WebGL only once the frame nears the viewport), so children must watch rather than read once.
7
+ // It is also what makes the DOM-side components testable — inject a fake map and assert on real DOM.
8
+ import type { InjectionKey, ShallowRef } from 'vue';
9
+ import type { Map, Marker } from 'mapbox-gl';
10
+
11
+ export const FOREST_MAP: InjectionKey<ShallowRef<Map | null>> = Symbol('forest-map');
12
+ export const FOREST_MARKER: InjectionKey<ShallowRef<Marker | null>> = Symbol('forest-marker');
@@ -0,0 +1,55 @@
1
+ // What a layers switch is made of, and the arithmetic of turning things off.
2
+ //
3
+ // ★ THE MODEL HOLDS WHAT IS HIDDEN, NOT WHAT IS SHOWN. A map's resting state is everything the surface
4
+ // authored, painted. With a hidden-set `[]` says exactly that, and a surface that never seeds the model
5
+ // gets the map it wrote. With a visible-set the same oversight paints nothing at all — and a blank map
6
+ // reads as a broken overlay rather than as a missing default, which is the harder bug to find.
7
+ //
8
+ // ★ NOTHING HERE TOUCHES THE MAP. The model feeds `visible:` in the surface's OverlaySpec and
9
+ // useMapOverlay applies it. A switch calling setLayoutProperty directly would work until the next
10
+ // apply — a spec change, a colour-mode flip, a new style — and then be reverted while the control
11
+ // carried on reporting the state it had asked for.
12
+ import type { MapLegendItem } from './mapLegend';
13
+
14
+ /** A group of rows. */
15
+ export interface MapLayerSection {
16
+ /**
17
+ * What the group is called.
18
+ *
19
+ * ★ A LABELLED SECTION GETS AN ALL/NONE HEADER; an unlabelled one does not. There is nothing to name
20
+ * the toggle after, and a control for "everything" is the map itself. A flat list is one section
21
+ * without a label, which is why there is no second shape for it.
22
+ */
23
+ label?: string;
24
+ items: MapLegendItem[];
25
+ }
26
+
27
+ export type SectionState = 'all' | 'none' | 'some';
28
+
29
+ /** `some` renders indeterminate. An empty section is `none` — nothing is shown, so nothing is all. */
30
+ export function sectionState(items: MapLegendItem[], hidden: readonly string[]): SectionState {
31
+ if (!items.length) return 'none';
32
+ const off = items.filter((i) => hidden.includes(i.key)).length;
33
+ if (off === 0) return 'all';
34
+ if (off === items.length) return 'none';
35
+ return 'some';
36
+ }
37
+
38
+ /**
39
+ * The header's press.
40
+ *
41
+ * ★ PARTIAL REVEALS RATHER THAN HIDES. It is the direction every tri-state checkbox a reader has met
42
+ * takes, and the non-destructive one: pressing a header should never be the thing that empties a map.
43
+ */
44
+ export function toggleSection(items: MapLegendItem[], hidden: readonly string[]): string[] {
45
+ const keys = items.map((i) => i.key);
46
+ if (sectionState(items, hidden) === 'all') {
47
+ return [...hidden, ...keys.filter((k) => !hidden.includes(k))];
48
+ }
49
+ return hidden.filter((k) => !keys.includes(k));
50
+ }
51
+
52
+ /** One row. Returns a new array — a model mutated in place does not reach the parent. */
53
+ export function toggleKey(key: string, hidden: readonly string[]): string[] {
54
+ return hidden.includes(key) ? hidden.filter((k) => k !== key) : [...hidden, key];
55
+ }