@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,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';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Forest theme fragment — UAccordion (collapsible sections).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `trigger`: font-bold on the trigger label (stock is font-medium) — lifts the section heading
6
+ * above body copy in the two-weight type system; adds a brand focus ring using the forest-500
7
+ * ramp var so tab navigation is clearly visible. Stock's `outline-primary/25` resolves correctly
8
+ * but we anchor to the ramp directly for determinism across all consumer surfaces.
9
+ * - `item`: the default border-b divider is retained (it reads cleanly); no radius override needed
10
+ * at the item level — the root container carries any shell radius from the consumer.
11
+ * - `body`: text-sm is stock; we add text-muted so body copy reads at the Forest semantic muted
12
+ * level (slightly softer than text-default), keeping content subordinate to the trigger label.
13
+ * - `content` / `trailingIcon`: the height animation and the chevron rotate move onto the shared
14
+ * motion tokens (stock is a raw 200 ms both). The accordion runs the same keyframe pair as the
15
+ * collapsible, so it takes the same timing — --motion-slow for the height travel — and the
16
+ * chevron, a small near element, takes --motion-fast.
17
+ *
18
+ * Slots touched: trigger, body, content, trailingIcon.
19
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/accordion.ts
20
+ */
21
+
22
+ export const accordionTheme = {
23
+ slots: {
24
+ // font-bold: clear heading hierarchy; focus ring pinned to forest-500 ramp var for
25
+ // dark-mode correctness (stock outline-primary resolves correctly but we anchor to the ramp
26
+ // directly for determinism across all consumer surfaces).
27
+ trigger: 'font-bold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-forest-500)]',
28
+ // text-muted: subordinates body content below the trigger label in the Forest reading hierarchy.
29
+ body: 'text-muted',
30
+ // Height travel on --motion-slow, matching the collapsible (same keyframes, same bucket).
31
+ content:
32
+ 'data-[state=open]:animate-[accordion-down_var(--motion-slow)_var(--motion-ease-out)] data-[state=closed]:animate-[accordion-up_var(--motion-slow)_var(--motion-ease-out)]',
33
+ // Chevron rotate — a small near element, so --motion-fast.
34
+ trailingIcon: 'duration-(--motion-fast) ease-(--motion-ease-out)',
35
+ },
36
+ } as const;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Forest theme fragment — UAlert (inline message banner, all intents).
3
+ *
4
+ * Deltas only — the preset APPENDS to stock, so restating stock's layout classes would only invite
5
+ * drift. One change:
6
+ * - `title`: font-bold (over stock font-medium) — clear hierarchy above the description
7
+ * without font-bold, which is too heavy for inline copy.
8
+ * Everything else (root layout/radius, text-sm, description opacity-90, icon/actions/close) is stock.
9
+ * The bold contexts round the banner corner up to rounded-xl (mobile/marketing).
10
+ */
11
+
12
+ export const alertTheme = {
13
+ slots: {
14
+ title: 'font-bold',
15
+ },
16
+ } as const;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Forest theme fragment for UAuthForm.
3
+ *
4
+ * UAuthForm ships the contents of a sign-in screen — fields, provider buttons, the separator — and
5
+ * takes its title and description sizes from Tailwind's default scale (`text-xl` and `text-base`)
6
+ * rather than from Forest's. That is a second type ladder running beside the real one and drifting the
7
+ * moment the real one moves. Both slots move onto the ladder: `.type-title` and `.type-body`, which
8
+ * carry size, line-height, tracking and family together.
9
+ *
10
+ * Weight was never the problem — `styles/forest.css` remaps `--font-weight-semibold` to 700 in
11
+ * `@theme`, so stock's `font-semibold` already lands on a cut the brand fonts ship.
12
+ */
13
+
14
+ export const authFormTheme = {
15
+ slots: {
16
+ title: 'type-title',
17
+ description: 'type-body',
18
+ },
19
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Forest theme fragment — UAvatarGroup. No delta: the stock recipe is correct as-is.
3
+ *
4
+ * - The overlap ring is ring-bg (surface-coloured), so it adapts to light/dark and to tinted
5
+ * surfaces through the semantic token — nothing to re-point.
6
+ * - Shape and sizing ride UAvatar, which carries the Forest opinion in its own fragment.
7
+ */
8
+
9
+ export const avatarGroupTheme = {} as const
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Forest theme fragment — Avatar / AvatarGroup
3
+ *
4
+ * Deltas only (the preset APPENDS to stock, so this carries just what changes):
5
+ * - root: `font-bold` — confident initials; stock root is unweighted.
6
+ * - fallback: `font-bold` — stock is `font-medium`, which reads thin at xs/2xs on the tint fills.
7
+ *
8
+ * Everything else is stock and correct: rounded-full shape, the /10 colour-ramp fills
9
+ * (bg-{color}/10 · text-{color}, wired to the Forest ramps in docs.css), the 3xs–3xl ladder,
10
+ * `object-cover rounded-[inherit]` images, and the AvatarGroup `ring-bg` separator. AvatarGroup
11
+ * needs no override.
12
+ */
13
+ export const avatarTheme = {
14
+ slots: {
15
+ root: 'font-bold',
16
+ fallback: 'font-bold',
17
+ },
18
+ } as const;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Forest theme fragment for UBadge.
3
+ *
4
+ * Standard badge is a soft-cornered tag (rounded-sm/8px), not a pill: rectangular enough to
5
+ * read as a label on a data-dense web surface. Bold, uppercase, wide-tracked lettering is the
6
+ * Forest opinion at every scale; the bold contexts round it to a full pill (mobile/marketing).
7
+ *
8
+ * Stock UBadge sets the corner PER SIZE (rounded-sm on xs/sm, rounded-md on md/lg/xl), and tv
9
+ * applies the size variant after the base slot — so `rounded-sm` in base alone would lose to
10
+ * stock's rounded-md at md/lg/xl. Follow stock's shape: override the corner in the size
11
+ * variants (tv deep-merges variants, tailwind-merge collapses the per-size token). The Forest
12
+ * ladder tightens toward the small end: xs/sm rounded-xs, md/lg/xl rounded-sm. The base copy
13
+ * (rounded-sm) is the representative Standard value the derived contexts card reads.
14
+ */
15
+
16
+ export const badgeTheme = {
17
+ slots: { base: 'rounded-sm font-bold uppercase tracking-wide' },
18
+ variants: {
19
+ // Per size: the Forest radius override (see comment above) PLUS a `coarse:` touch bump.
20
+ // Under a coarse primary pointer each size renders as the NEXT SIZE UP — the full bundle
21
+ // (text, padding, gap, radius, icon), so a touched md badge reads like an lg badge. Unlike
22
+ // the button this is legibility, not a tap target (a badge is static) — a chip at arm's
23
+ // length on a phone wants the bigger step. `coarse:` is the custom variant in
24
+ // styles/forest.css: it fires only on a coarse pointer AND skips `.cut-fixed` elements, so
25
+ // the mobile cut (which bakes its own increase and stamps cut-fixed) opts out and doesn't
26
+ // double-promote; marketing has no cut-fixed, so it adapts like standard. xl is the ceiling.
27
+ size: {
28
+ xs: { base: 'rounded-xs coarse:text-[10px]/3 coarse:px-1.5 coarse:py-1' },
29
+ sm: {
30
+ base: 'rounded-xs coarse:text-xs coarse:px-2 coarse:rounded-sm',
31
+ leadingIcon: 'coarse:size-4',
32
+ trailingIcon: 'coarse:size-4',
33
+ },
34
+ md: {
35
+ base: 'rounded-sm coarse:text-sm coarse:gap-1.5',
36
+ leadingIcon: 'coarse:size-5',
37
+ trailingIcon: 'coarse:size-5',
38
+ },
39
+ lg: {
40
+ base: 'rounded-sm coarse:text-base coarse:px-2.5',
41
+ leadingIcon: 'coarse:size-6',
42
+ trailingIcon: 'coarse:size-6',
43
+ },
44
+ xl: { base: 'rounded-sm' },
45
+ },
46
+ },
47
+ // A badge labels something; it does not ask to be pressed. Subtle (tinted fill + inset ring)
48
+ // rather than stock's solid, which reads as a control. Being ringed, it also carries the bold
49
+ // contexts' 2px edge.
50
+ defaultVariants: { variant: 'subtle' },
51
+ } as const
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Forest theme fragment — UBanner.
3
+ *
4
+ * Deltas only (the preset appends to stock, so nothing stock is restated):
5
+ * • title — steps up to font-bold so the copy holds against every solid brand fill, and
6
+ * takes `me-auto` to shove the actions to the far right.
7
+ * • layout — Forest reads the strip as title-LEFT / actions-RIGHT. Stock centres the title
8
+ * between two flex-1 spacers (left/right) with the actions beside it; we drop those
9
+ * spacers (`lg:flex-none`) and let the centre grow (`flex-1`) so the actions land at
10
+ * the end of the row, just before the close button.
11
+ * The banner has no radius by design — it spans the viewport edge to edge.
12
+ */
13
+
14
+ export const bannerTheme = {
15
+ slots: {
16
+ left: 'lg:flex-none',
17
+ center: 'flex-1',
18
+ title: 'font-bold me-auto',
19
+ right: 'lg:flex-none',
20
+ },
21
+ } as const;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Forest theme fragment — UBreadcrumb. No delta: the stock recipe is correct as-is.
3
+ *
4
+ * - Inactive crumb text-muted with hover:text-default, active crumb font-semibold (bold via
5
+ * the two-weight re-map), per-colour focus ring, text-muted separator — all right by default.
6
+ * - In a two-weight system there is no intermediate label weight to seat, so the crumb ladder
7
+ * is regular → bold exactly as stock wires it.
8
+ */
9
+
10
+ export const breadcrumbTheme = {} as const