@humanforest/ui 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 (235) hide show
  1. package/LICENSE +67 -0
  2. package/icons/forest/bike-asterisk.svg +21 -0
  3. package/icons/forest/bike-down.svg +19 -0
  4. package/icons/forest/bike-off.svg +20 -0
  5. package/icons/forest/bike-up.svg +19 -0
  6. package/icons/forest/bike-x.svg +19 -0
  7. package/icons/forest/bike.svg +18 -0
  8. package/icons/forest/map/forest-place-count.svg +21 -0
  9. package/icons/forest/map/forest-place-marker.svg +17 -0
  10. package/icons/forest/map/forest-vehicle-marker.svg +73 -0
  11. package/icons/forest/map/manifest.json +373 -0
  12. package/icons/forest/map/place-count-manifest.json +83 -0
  13. package/icons/forest/map/place-manifest.json +74 -0
  14. package/icons/forest/mark-fill.svg +6 -0
  15. package/icons/forest/mark.svg +6 -0
  16. package/icons/forest/parking.svg +3 -0
  17. package/icons/forest/star-fill.svg +4 -0
  18. package/icons/forest/star.svg +4 -0
  19. package/icons/forest/traffic-light-caution.svg +8 -0
  20. package/icons/forest/traffic-light-go.svg +8 -0
  21. package/icons/forest/traffic-light-lit.svg +8 -0
  22. package/icons/forest/traffic-light-stop.svg +8 -0
  23. package/icons/forest/traffic-light.svg +8 -0
  24. package/package.json +64 -0
  25. package/src/address/address.ts +125 -0
  26. package/src/address/autofill.ts +78 -0
  27. package/src/address/geocode.ts +55 -0
  28. package/src/address/index.ts +31 -0
  29. package/src/address/validate.ts +91 -0
  30. package/src/canvas/FCanvasCard.vue +97 -0
  31. package/src/canvas/FCanvasControl.vue +121 -0
  32. package/src/canvas/FCanvasControlGroup.vue +100 -0
  33. package/src/canvas/FCanvasFlyout.vue +216 -0
  34. package/src/canvas/FCanvasHud.vue +402 -0
  35. package/src/canvas/canvasSurface.ts +39 -0
  36. package/src/canvas/index.ts +17 -0
  37. package/src/contexts/ForestContext.vue +53 -0
  38. package/src/contexts/contexts.ts +153 -0
  39. package/src/contexts/index.ts +23 -0
  40. package/src/contexts/marketing-theme.ts +214 -0
  41. package/src/contexts/mobile-theme.ts +402 -0
  42. package/src/filter/FFilterBar.vue +72 -0
  43. package/src/filter/FFilterChip.vue +362 -0
  44. package/src/filter/FFilterClauses.vue +104 -0
  45. package/src/filter/FFilterTrigger.vue +183 -0
  46. package/src/filter/filter.theme.ts +27 -0
  47. package/src/filter/filterClause.ts +265 -0
  48. package/src/filter/filterPredicate.ts +134 -0
  49. package/src/filter/filterSchema.ts +82 -0
  50. package/src/filter/index.ts +36 -0
  51. package/src/forest-preset.ts +68 -0
  52. package/src/icons.ts +79 -0
  53. package/src/index.ts +17 -0
  54. package/src/kpi/FKpi.vue +253 -0
  55. package/src/kpi/FKpiGroup.vue +161 -0
  56. package/src/kpi/index.ts +20 -0
  57. package/src/kpi/kpi.theme.ts +168 -0
  58. package/src/kpi/kpiDelta.ts +82 -0
  59. package/src/kpi/kpiFormat.ts +72 -0
  60. package/src/kpi/kpiGroupState.ts +63 -0
  61. package/src/logo/FLogo.vue +52 -0
  62. package/src/logo/FSubLogo.vue +214 -0
  63. package/src/logo/index.ts +8 -0
  64. package/src/map/FMap.vue +355 -0
  65. package/src/map/FMapCompass.vue +154 -0
  66. package/src/map/FMapCoordinates.vue +134 -0
  67. package/src/map/FMapDrawTools.vue +116 -0
  68. package/src/map/FMapFullscreen.vue +92 -0
  69. package/src/map/FMapGeolocate.vue +208 -0
  70. package/src/map/FMapLayers.vue +75 -0
  71. package/src/map/FMapLegend.vue +45 -0
  72. package/src/map/FMapPopup.vue +129 -0
  73. package/src/map/FMapRamp.vue +69 -0
  74. package/src/map/FMapRegion.vue +77 -0
  75. package/src/map/FMapScale.vue +107 -0
  76. package/src/map/FMapSearch.vue +362 -0
  77. package/src/map/FMapTilt.vue +87 -0
  78. package/src/map/FMapTools.vue +61 -0
  79. package/src/map/FMapZoom.vue +81 -0
  80. package/src/map/FMarker.vue +218 -0
  81. package/src/map/FTrackScrubber.vue +94 -0
  82. package/src/map/FVehicleLegend.vue +72 -0
  83. package/src/map/FVehicleMark.vue +166 -0
  84. package/src/map/bin.ts +196 -0
  85. package/src/map/coordinates.ts +83 -0
  86. package/src/map/density.ts +401 -0
  87. package/src/map/draw.ts +356 -0
  88. package/src/map/ease.ts +67 -0
  89. package/src/map/fullscreen.ts +59 -0
  90. package/src/map/heat.ts +160 -0
  91. package/src/map/index.ts +351 -0
  92. package/src/map/liveMarkers.ts +60 -0
  93. package/src/map/map.css +116 -0
  94. package/src/map/mapColor.ts +31 -0
  95. package/src/map/mapContext.ts +12 -0
  96. package/src/map/mapLayers.ts +55 -0
  97. package/src/map/mapLegend.ts +70 -0
  98. package/src/map/mapSearch.theme.ts +79 -0
  99. package/src/map/mapSearch.ts +279 -0
  100. package/src/map/mapUrl.ts +117 -0
  101. package/src/map/markerAsset.ts +3 -0
  102. package/src/map/padding.ts +39 -0
  103. package/src/map/place.ts +192 -0
  104. package/src/map/ramp.ts +95 -0
  105. package/src/map/regionSelect.ts +163 -0
  106. package/src/map/route.ts +167 -0
  107. package/src/map/scale.ts +329 -0
  108. package/src/map/scaleBar.ts +67 -0
  109. package/src/map/selectionModel.ts +50 -0
  110. package/src/map/sources/coordinates.ts +58 -0
  111. package/src/map/sources/mapboxPlaces.ts +71 -0
  112. package/src/map/style.ts +14 -0
  113. package/src/map/track.ts +279 -0
  114. package/src/map/useFeatureSelect.ts +230 -0
  115. package/src/map/useLiveMarkers.ts +84 -0
  116. package/src/map/useMap.ts +150 -0
  117. package/src/map/useMapDraw.ts +776 -0
  118. package/src/map/useMapOverlay.ts +421 -0
  119. package/src/map/useMapTableSelection.ts +101 -0
  120. package/src/map/useMapUrlState.ts +155 -0
  121. package/src/map/useMarkerSource.ts +163 -0
  122. package/src/map/useRegionSelect.ts +309 -0
  123. package/src/map/useTokenMode.ts +61 -0
  124. package/src/map/useTrackPlayback.ts +131 -0
  125. package/src/map/useVehicleFilter.ts +51 -0
  126. package/src/map/useVisibleFeatures.ts +124 -0
  127. package/src/map/vehicle.ts +807 -0
  128. package/src/map/vehicleFilter.ts +284 -0
  129. package/src/map/vehicleLegend.ts +155 -0
  130. package/src/map/visibleFeatures.ts +192 -0
  131. package/src/map/zone.ts +291 -0
  132. package/src/shell/FAppNavbar.vue +68 -0
  133. package/src/shell/FAppShell.vue +163 -0
  134. package/src/shell/FAppSidebar.vue +215 -0
  135. package/src/shell/FPageToolbar.vue +126 -0
  136. package/src/shell/FScrollShadow.vue +43 -0
  137. package/src/shell/FSidePanel.vue +258 -0
  138. package/src/shell/FToolbarActions.vue +32 -0
  139. package/src/shell/index.ts +33 -0
  140. package/src/shell/nav.ts +37 -0
  141. package/src/shell/pageToolbar.ts +69 -0
  142. package/src/shell/shell.ts +74 -0
  143. package/src/shell/sidePanel.ts +115 -0
  144. package/src/styles/forest.css +852 -0
  145. package/src/styles/index.css +39 -0
  146. package/src/themes/accordion.ts +36 -0
  147. package/src/themes/alert.ts +16 -0
  148. package/src/themes/auth-form.ts +19 -0
  149. package/src/themes/avatar-group.ts +9 -0
  150. package/src/themes/avatar.ts +18 -0
  151. package/src/themes/badge.ts +51 -0
  152. package/src/themes/banner.ts +21 -0
  153. package/src/themes/breadcrumb.ts +10 -0
  154. package/src/themes/button.mechanics.ts +191 -0
  155. package/src/themes/button.ts +136 -0
  156. package/src/themes/calendar.ts +29 -0
  157. package/src/themes/card.ts +67 -0
  158. package/src/themes/carousel.ts +41 -0
  159. package/src/themes/checkbox-group.ts +20 -0
  160. package/src/themes/checkbox.ts +47 -0
  161. package/src/themes/chip.ts +50 -0
  162. package/src/themes/collapsible.ts +26 -0
  163. package/src/themes/color-picker.ts +13 -0
  164. package/src/themes/command-palette.ts +37 -0
  165. package/src/themes/container.ts +14 -0
  166. package/src/themes/context-menu.ts +38 -0
  167. package/src/themes/dashboard-group.ts +11 -0
  168. package/src/themes/dashboard-navbar.ts +10 -0
  169. package/src/themes/dashboard-panel.ts +18 -0
  170. package/src/themes/dashboard-resize-handle.ts +35 -0
  171. package/src/themes/dashboard-sidebar-collapse.ts +7 -0
  172. package/src/themes/dashboard-sidebar-toggle.ts +7 -0
  173. package/src/themes/dashboard-sidebar.ts +58 -0
  174. package/src/themes/dashboard-toolbar.ts +15 -0
  175. package/src/themes/drawer.ts +45 -0
  176. package/src/themes/dropdown-menu.ts +38 -0
  177. package/src/themes/empty.ts +43 -0
  178. package/src/themes/field-group.ts +52 -0
  179. package/src/themes/file-upload.ts +38 -0
  180. package/src/themes/footer-columns.ts +13 -0
  181. package/src/themes/footer.ts +9 -0
  182. package/src/themes/form-field.ts +25 -0
  183. package/src/themes/form.ts +22 -0
  184. package/src/themes/header.ts +12 -0
  185. package/src/themes/icon.ts +61 -0
  186. package/src/themes/index.ts +99 -0
  187. package/src/themes/input-date.ts +73 -0
  188. package/src/themes/input-menu.ts +66 -0
  189. package/src/themes/input-number.ts +58 -0
  190. package/src/themes/input-rating.ts +25 -0
  191. package/src/themes/input-tags.ts +63 -0
  192. package/src/themes/input-time.ts +70 -0
  193. package/src/themes/input.ts +148 -0
  194. package/src/themes/kbd.ts +27 -0
  195. package/src/themes/link.ts +27 -0
  196. package/src/themes/listbox.ts +48 -0
  197. package/src/themes/main.ts +6 -0
  198. package/src/themes/marquee.ts +45 -0
  199. package/src/themes/modal.ts +46 -0
  200. package/src/themes/navigation-menu.ts +80 -0
  201. package/src/themes/page-anchors.ts +7 -0
  202. package/src/themes/page-aside.ts +7 -0
  203. package/src/themes/page-card.ts +22 -0
  204. package/src/themes/page-cta.ts +18 -0
  205. package/src/themes/page-feature.ts +13 -0
  206. package/src/themes/page-header.ts +16 -0
  207. package/src/themes/page-hero.ts +15 -0
  208. package/src/themes/page-links.ts +12 -0
  209. package/src/themes/page-section.ts +14 -0
  210. package/src/themes/page.ts +7 -0
  211. package/src/themes/pagination.ts +17 -0
  212. package/src/themes/pin-input.ts +73 -0
  213. package/src/themes/popover.ts +30 -0
  214. package/src/themes/progress.ts +29 -0
  215. package/src/themes/prose.ts +45 -0
  216. package/src/themes/radio-group.ts +39 -0
  217. package/src/themes/scroll-area.ts +23 -0
  218. package/src/themes/select-menu.ts +71 -0
  219. package/src/themes/select.ts +72 -0
  220. package/src/themes/separator.ts +28 -0
  221. package/src/themes/sidebar.ts +127 -0
  222. package/src/themes/skeleton.ts +15 -0
  223. package/src/themes/slideover.ts +87 -0
  224. package/src/themes/slider.ts +48 -0
  225. package/src/themes/stepper.ts +41 -0
  226. package/src/themes/switch.ts +63 -0
  227. package/src/themes/table.ts +34 -0
  228. package/src/themes/tabs.ts +74 -0
  229. package/src/themes/textarea.ts +77 -0
  230. package/src/themes/timeline.ts +25 -0
  231. package/src/themes/toast.ts +29 -0
  232. package/src/themes/toaster.ts +66 -0
  233. package/src/themes/tooltip.ts +31 -0
  234. package/src/themes/tree.ts +41 -0
  235. package/src/themes/user.ts +28 -0
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ // A view's own controls, placed at the right-hand end of the section's toolbar.
3
+ //
4
+ // <FToolbarActions>
5
+ // <UButton label="Recentre" @click="recentre" />
6
+ // </FToolbarActions>
7
+ //
8
+ // A console section owns its navigation and renders it once, in the route that owns the section. Its
9
+ // VIEWS own their actions. The two are siblings in that route's template — toolbar beside the router
10
+ // view — so a view cannot fill the toolbar's slots, and provide/inject cannot cross siblings either.
11
+ // What both sit inside is the SHELL, so the shell carries the ref and this teleports into it.
12
+ //
13
+ // These land in the FIRST toolbar to claim the seam, which is not necessarily the nearest bar in the
14
+ // markup: on a section stacking a nav row over a filter row that is the NAV row, and a view writing
15
+ // its own second bar still sends its actions up to the section's.
16
+ //
17
+ // Renders where it was written when no toolbar has claimed the seam. A page whose actions vanished
18
+ // silently is broken in a way nobody notices; controls appearing somewhere odd say so immediately.
19
+ import { computed, inject } from 'vue';
20
+ import { TOOLBAR_SEAT_KEY } from './shell';
21
+
22
+ const seat = inject(TOOLBAR_SEAT_KEY, null);
23
+ const target = computed(() => seat?.actions.value ?? null);
24
+ </script>
25
+
26
+ <template>
27
+ <!-- `disabled` rather than v-if, so the slot still renders with no toolbar above. Teleport wants a
28
+ valid `to` even when disabled, hence the body fallback. -->
29
+ <Teleport :to="target ?? 'body'" :disabled="!target">
30
+ <slot />
31
+ </Teleport>
32
+ </template>
@@ -0,0 +1,33 @@
1
+ // The Forest app shell — the chrome AROUND a page, and the toolbar seat a page can reach into.
2
+ //
3
+ // <FAppShell> sidebar + header + the inset card the page lives in
4
+ // <FAppSidebar> the brand mark, the nav and the collapsed rail — the app supplies the list
5
+ // <FAppNavbar> the rail control and the crumb — the app supplies the crumb and its actions
6
+ // <FScrollShadow> the overflow signal for a column whose scrollbar is hidden — the sidebar's
7
+ // <FSidePanel> a column belonging to a CONTAINER rather than the app — settings sub-nav, or a
8
+ // filter/list panel over a canvas. Not the app rail; see the component's own note.
9
+ // <FPageToolbar> the page's filters, view controls or sub-route nav, flush at the top of the card
10
+ // <FToolbarActions> a view's own controls, placed at the right of the section's toolbar
11
+ // TOOLBAR_SEAT_KEY the injection carrying the toolbar seat; exported for a custom shell
12
+ // createToolbarClaims the claim ordering behind a seat's actions, for a shell of one's own
13
+ //
14
+ // The look of every region is theme fragments (themes/dashboard-*.ts), not this component — what
15
+ // lives here is the ARRANGEMENT: chrome outside the card, and a way for a page to reach it.
16
+ export { default as FAppShell } from './FAppShell.vue';
17
+ export { default as FAppSidebar } from './FAppSidebar.vue';
18
+ export { default as FAppNavbar } from './FAppNavbar.vue';
19
+ export { default as FScrollShadow } from './FScrollShadow.vue';
20
+ export { default as FSidePanel } from './FSidePanel.vue';
21
+ export {
22
+ CANVAS_INSET_VAR,
23
+ SIDE_PANEL_WIDTH,
24
+ sidePanelInsetPx,
25
+ type SidePanelBehaviour,
26
+ type SidePanelSide,
27
+ type SidePanelSize,
28
+ } from './sidePanel';
29
+ export { createToolbarClaims, TOOLBAR_SEAT_KEY, type ToolbarClaims, type ToolbarSeat } from './shell';
30
+ export type { FNavItem, FNavGroup, FCrumb } from './nav';
31
+ export { default as FPageToolbar } from './FPageToolbar.vue';
32
+ export { type PageToolbarWell } from './pageToolbar';
33
+ export { default as FToolbarActions } from './FToolbarActions.vue';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A navigable destination in the app shell's sidebar.
3
+ *
4
+ * Deliberately thin. It carries what a row NEEDS to draw itself and nothing about where the list came
5
+ * from — an app's own nav tree (routes, a manifest, a permissions filter) maps onto this rather than
6
+ * the shell prescribing a shape for it.
7
+ *
8
+ * `to` is optional on purpose: an entry with no destination renders disabled and badged rather than
9
+ * being dropped, so a system that is still growing reads as intentional instead of missing.
10
+ */
11
+ export type FNavItem = {
12
+ label: string;
13
+ /** Any icon name the app's icon set resolves. */
14
+ icon?: string;
15
+ /** Omit for a destination that does not exist yet — the row renders disabled. */
16
+ to?: string;
17
+ /** Flags the row as work in progress; it still links. */
18
+ wip?: boolean;
19
+ };
20
+
21
+ /** A labelled section of the sidebar. The label heads the group when expanded. */
22
+ export type FNavGroup = {
23
+ label: string;
24
+ items: FNavItem[];
25
+ };
26
+
27
+ /**
28
+ * A step in the header's crumb — `group · page`.
29
+ *
30
+ * `to` is optional and usually absent: a crumb says where you are, and the group half of `group ·
31
+ * page` is a section of the nav rather than a route. A step that genuinely is a destination can
32
+ * carry one.
33
+ */
34
+ export type FCrumb = {
35
+ label: string;
36
+ to?: string;
37
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Which well rung <FPageToolbar> aligns its controls to.
3
+ *
4
+ * A toolbar bleeds to the card's edge so its hairline reads as a rule across the whole card, but its
5
+ * CONTROLS have to line up with the page content below — on a 1720px card those are 252px apart. The
6
+ * rung must therefore match the page body's own well, which is why the page states it.
7
+ *
8
+ * There is no `flush` rung, and that is a judgement rather than an omission. The body's `.well-flush`
9
+ * exists so a CANVAS reaches the panel edge, and a canvas has no content start to line up with — so
10
+ * the alignment this prop is for does not arise. Controls also want their gutter whether or not the
11
+ * surface beneath them does: a filter jammed against the card's edge reads as broken. A canvas page's
12
+ * toolbar takes `full` — cap released so it spans the card, gutters kept so the controls can breathe.
13
+ */
14
+
15
+ export type PageToolbarWell = 'default' | 'reading' | 'full';
16
+
17
+ const WELLS: Record<PageToolbarWell, string> = {
18
+ default: '',
19
+ reading: 'well-reading',
20
+ full: 'well-full',
21
+ };
22
+
23
+ export function pageToolbarWellClass(well: PageToolbarWell): string {
24
+ return WELLS[well];
25
+ }
26
+
27
+ /**
28
+ * How the toolbar draws itself.
29
+ *
30
+ * `rule` — full-bleed, a hairline across the whole card, no fill. The default, and what a navigation
31
+ * row wants: it reads as part of the card's chrome.
32
+ *
33
+ * `card` — an inset panel: filled, rounded, and clear of all four edges. What a FILTER row wants,
34
+ * because a row of applied clauses is state the page is currently in rather than chrome, and it needs
35
+ * to read as a thing sitting on the surface. Linear's shape, and a page mixes the two — navigation on
36
+ * a rule, filters on a card beneath it.
37
+ *
38
+ * The fill alone does NOT carry this. Measured against the card behind it, `bg-muted` is 0.020
39
+ * luminance in light against 0.060 in dark — which is why a plain tint was probed for this row once
40
+ * before and rejected as imperceptible in light. What makes the panel read is the RADIUS and the
41
+ * INSET, which are geometry and behave the same in both modes; the ring gives it an edge of its own.
42
+ */
43
+ export type PageToolbarVariant = 'rule' | 'card';
44
+
45
+ export type PageToolbarSurface = {
46
+ /** Goes on UDashboardToolbar's root, which is the full-bleed layer. */
47
+ root: string;
48
+ /** Goes on the flex row inside the well, which is the layer that becomes the panel. */
49
+ row: string;
50
+ };
51
+
52
+ const SURFACES: Record<PageToolbarVariant, PageToolbarSurface> = {
53
+ rule: { root: '', row: '' },
54
+ // `border-b-0` cancels stock's rule: a panel with a line under it reads as neither one nor the
55
+ // other. `py-2` is the inset — the well's own gutter already handles the horizontal.
56
+ //
57
+ // The ring is MODE-SPLIT, and measured rather than guessed. Against this panel's own `bg-muted`
58
+ // fill, `ring-default` is 0.072 luminance apart in light but EXACTLY EQUAL in dark (both 0.1497) —
59
+ // an edge that simply is not there. `ring-accented` clears it by 0.099. Neither token works in both
60
+ // modes, which is the same collapse the docs code blocks split their frame for.
61
+ card: {
62
+ root: 'border-b-0 py-2',
63
+ row: 'rounded-md bg-muted ring ring-default dark:ring-accented px-2 py-1.5',
64
+ },
65
+ };
66
+
67
+ export function pageToolbarSurface(variant: PageToolbarVariant): PageToolbarSurface {
68
+ return SURFACES[variant];
69
+ }
@@ -0,0 +1,74 @@
1
+ import { computed, ref, type InjectionKey, type Ref } from 'vue';
2
+
3
+ /**
4
+ * The seat <FAppShell> hands down to <FPageToolbar>.
5
+ *
6
+ * An element, not a selector string. A shipped component cannot own a global id — two shells on one
7
+ * page would collide, and a page teleporting by selector has to guess a name the shell happens to
8
+ * render. Injection also gives the "no shell above me" case for free: `inject` returns null and the
9
+ * toolbar renders where it was written instead.
10
+ */
11
+ export type ToolbarSeat = {
12
+ /**
13
+ * Inside the panel, at the top of the inset card and ABOVE the page's own scrollport, so a toolbar
14
+ * holds while the page scrolls under it.
15
+ */
16
+ toolbar: Ref<HTMLElement | null>;
17
+ /**
18
+ * The right-hand end of the FIRST CLAIMANT's well, filled by <FToolbarActions>.
19
+ *
20
+ * The SHELL holds this although the TOOLBAR sets it: a section renders its toolbar and its router
21
+ * view as siblings, so a view's actions cannot inject from the toolbar — provide/inject walks the
22
+ * component parent chain and cannot cross siblings. Only something above both can carry it.
23
+ *
24
+ * Derived from the claimants rather than assigned, so that a section stacking a nav row over a
25
+ * filter row keeps the nav row's seam while it is there, and PROMOTES the survivor if it goes.
26
+ * Null only when no toolbar is mounted at all.
27
+ */
28
+ actions: Readonly<Ref<HTMLElement | null>>;
29
+ /** Called by <FPageToolbar> whenever it appears — mounted, or reactivated inside <KeepAlive>. */
30
+ claimActions: (el: HTMLElement) => void;
31
+ /**
32
+ * Called by <FPageToolbar> whenever it goes — unmounted, or deactivated inside <KeepAlive>.
33
+ * Promotes the next claimant if this one was first.
34
+ */
35
+ releaseActions: (el: HTMLElement) => void;
36
+ };
37
+
38
+ export const TOOLBAR_SEAT_KEY: InjectionKey<ToolbarSeat> = Symbol('forest-toolbar-seat');
39
+
40
+ /** The claimant list a seat's `actions` is derived from, plus the two functions that move it. */
41
+ export type ToolbarClaims = Pick<ToolbarSeat, 'actions' | 'claimActions' | 'releaseActions'> & {
42
+ /** Every claimant, in claim order. `actions` is the first of them. */
43
+ claimants: Ref<HTMLElement[]>;
44
+ };
45
+
46
+ /**
47
+ * Builds the claim state behind a seat.
48
+ *
49
+ * <FAppShell> provides one of these, and so does the docs preview that stages a toolbar inside a
50
+ * frame rather than in the real card. Both owe a page the same ordering, so the ordering lives here
51
+ * and neither restates it.
52
+ *
53
+ * Spread the three seat keys alongside a `toolbar` ref to make a {@link ToolbarSeat}; `claimants` is
54
+ * the list itself, held out so the order can be read directly.
55
+ *
56
+ * Both functions are idempotent — claiming a toolbar already in the list changes nothing, and so
57
+ * does releasing one that is not in it. A toolbar that claims twice for one appearance, or releases
58
+ * twice for one departure, still leaves the list right.
59
+ */
60
+ export function createToolbarClaims(): ToolbarClaims {
61
+ const claimants = ref<HTMLElement[]>([]);
62
+ const actions = computed(() => claimants.value[0] ?? null);
63
+
64
+ const claimActions = (el: HTMLElement) => {
65
+ if (!claimants.value.includes(el)) claimants.value.push(el);
66
+ };
67
+
68
+ const releaseActions = (el: HTMLElement) => {
69
+ const i = claimants.value.indexOf(el);
70
+ if (i !== -1) claimants.value.splice(i, 1);
71
+ };
72
+
73
+ return { claimants, actions, claimActions, releaseActions };
74
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * FSidePanel's measures, and the one calculation anything outside it needs.
3
+ *
4
+ * ★ WHY THIS IS A MODULE AND NOT TWO NUMBERS IN A COMPONENT. An overlay panel covers a strip of the
5
+ * surface it floats on, and three separate things have to agree on how wide that strip is:
6
+ *
7
+ * · the panel, which draws it;
8
+ * · the map camera, whose `padding` keeps it clear so a flyTo does not centre underneath the panel;
9
+ * · the canvas HUD, whose rails on that edge shift over rather than hiding behind it.
10
+ *
11
+ * The first two are the ones that drift: the panel is CSS and the camera is JS, so they cannot read one
12
+ * value unless something like this exists. (The HUD is reached through a custom property instead — see
13
+ * CANVAS_INSET_VAR.) This module is that one value, and `sidePanelInsetPx` is the only correct way to
14
+ * turn a size into the camera's number.
15
+ */
16
+
17
+ /** The two rungs. A free `width` prop is deliberately not offered — see SIDE_PANEL_WIDTH. */
18
+ export type SidePanelSize = 'md' | 'lg';
19
+
20
+ /**
21
+ * Which edge of its container the panel takes, in logical directions.
22
+ *
23
+ * `end` is the default: a panel is secondary chrome inside a shell whose own rail already holds the
24
+ * start edge, so start would stack two columns on one side. `start` is for the case where the panel is
25
+ * the navigation for its view — a settings sub-nav.
26
+ */
27
+ export type SidePanelSide = 'start' | 'end';
28
+
29
+ /**
30
+ * Does the content beside it move over, or does the panel lie on top of it?
31
+ *
32
+ * push the panel is a flow item — a settings sub-nav, a dashboard filter column. The page beside
33
+ * it is narrower while it is open.
34
+ * overlay the panel is absolutely positioned over the surface — a filter or list panel on a map. The
35
+ * canvas keeps its full size and the panel covers a strip of it.
36
+ */
37
+ export type SidePanelBehaviour = 'push' | 'overlay';
38
+
39
+ /**
40
+ * ★ TWO RUNGS, NOT A NUMBER. A panel's width is a system decision, not a per-page one: two panels a
41
+ * click apart at 320 and 344 read as a bug, and nothing in a usage site can see the other panel to stay
42
+ * consistent with it.
43
+ *
44
+ * md 17rem — the same measure as the canvas HUD's wide rail (--forest-canvas-rail-top), so a panel
45
+ * and a rail card on the same canvas line up rather than nearly lining up.
46
+ * lg 22rem — for a list of rows, where md leaves a row's secondary text with nowhere to go.
47
+ *
48
+ * Both are also expressed in px because the camera's `padding` is a JS number and rem is not.
49
+ */
50
+ export const SIDE_PANEL_WIDTH: Record<SidePanelSize, { css: string; px: number }> = {
51
+ md: { css: '17rem', px: 272 },
52
+ lg: { css: '22rem', px: 352 },
53
+ };
54
+
55
+ /**
56
+ * The gutter an overlay panel floats in, matching the HUD's own chrome gutter (--forest-chrome-gutter,
57
+ * 1rem). Duplicated here as a number for the same reason the widths are: the camera cannot read a custom
58
+ * property. The static test asserts the two stay equal.
59
+ */
60
+ export const SIDE_PANEL_GUTTER_PX = 16;
61
+
62
+ /**
63
+ * The strip an OVERLAY panel covers, in px — what `<FMap :padding>` should keep clear on that edge.
64
+ *
65
+ * gutter + panel + gutter
66
+ *
67
+ * Both gutters count HERE, and only here: the outer one is the space between the panel and the canvas
68
+ * edge, the inner one is clearance. A camera inset of just the panel's width parks a marker flush
69
+ * against the panel's edge, which reads as "hidden behind it" even though it is not. Chrome does not
70
+ * want that second gutter — it brings its own — so CANVAS_INSET_VAR publishes one gutter fewer.
71
+ *
72
+ * Returns 0 when the panel is closed, so a usage site is one expression rather than a branch:
73
+ *
74
+ * :padding="{ right: sidePanelInsetPx('lg', open) }" // key matches the panel's side
75
+ */
76
+ export const sidePanelInsetPx = (size: SidePanelSize, open = true): number =>
77
+ open ? SIDE_PANEL_WIDTH[size].px + SIDE_PANEL_GUTTER_PX * 2 : 0;
78
+
79
+ /**
80
+ * The custom property an overlay panel declares on its container so <FCanvasHud> insets its rails on
81
+ * that edge. The HUD adds it to the safe-area inset it already keeps clear, so a panel and a device
82
+ * notch compose instead of one overwriting the other.
83
+ *
84
+ * ★ ITS VALUE IS NOT `sidePanelInsetPx`, and the difference is deliberate. This publishes the strip the
85
+ * panel OCCUPIES (outer gutter + width); the camera's number adds a second gutter of clearance. The
86
+ * HUD's areas already carry the chrome gutter as internal padding, so a trailing gutter here lands
87
+ * twice — measured, it left a 32px trough between the panel and the first rail item where every other
88
+ * pair of chrome on that canvas shares 16.
89
+ *
90
+ * A property rather than a prop because the HUD and the panel are siblings, not parent and child: the
91
+ * panel cannot pass anything to the HUD, but it can declare on the ancestor they share.
92
+ *
93
+ * ★ PHYSICAL sides, because what they are added to is physical: `env(safe-area-inset-*)` describes a
94
+ * notch, which does not move under RTL. The panel's own `side` is logical and maps to these here — the
95
+ * one place that mapping would have to become direction-aware if an RTL surface ever ships.
96
+ */
97
+ export const CANVAS_INSET_VAR = {
98
+ start: '--forest-canvas-inset-left',
99
+ end: '--forest-canvas-inset-right',
100
+ } as const satisfies Record<SidePanelSide, string>;
101
+
102
+ /**
103
+ * How long the panel's own travel takes, published alongside the inset.
104
+ *
105
+ * ★ THE INSET WITHOUT THE DURATION IS WORSE THAN NEITHER. Everything that follows an open panel — the
106
+ * HUD's rails, the map's camera — otherwise moves in ONE frame while the panel glides for 240ms: three
107
+ * things responding to one gesture, only one of them animated. That reads as the panel being smooth and
108
+ * everything else being broken.
109
+ *
110
+ * Published rather than assumed because the panel's travel is ASYMMETRIC (a fast exit, a slower enter),
111
+ * so a follower that hard-coded one duration would lag on the way out. Written as a resolved time
112
+ * (`240ms`), not a `var()` reference, because one of the readers is JS: Mapbox's camera takes a number,
113
+ * and it cannot resolve a token itself.
114
+ */
115
+ export const CANVAS_INSET_DURATION_VAR = '--forest-canvas-inset-duration';