@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,79 @@
1
+ // Generated by scripts/icons.ts — do not edit.
2
+ // Source: packages/ui/icons/forest/*.svg + @iconify-json/lucide.
3
+
4
+ /** Structural match for Iconify's IconifyJSON — avoids a runtime dep on @iconify/types here. */
5
+ export type ForestIconCollection = {
6
+ prefix: string;
7
+ icons: Record<string, { body: string }>;
8
+ width: number;
9
+ height: number;
10
+ };
11
+
12
+ /**
13
+ * Forest's own icons, drawn to Lucide's conventions (24×24, stroke-width 2, round caps and joins,
14
+ * currentColor). Most are line drawings with no fill; a few are FILLED shapes — the solid variants, and
15
+ * `parking`, which is the letter P in Mohr Black Italic converted to outlines. They exist only where
16
+ * Lucide has no glyph for something Forest needs.
17
+ * Register with addCollection() from @iconify/vue before mounting, then use as `i-forest-<name>`.
18
+ */
19
+ export const forestIcons: ForestIconCollection = {
20
+ "prefix": "forest",
21
+ "icons": {
22
+ "bike-asterisk": {
23
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M18.5 19C20.433 19 22 17.433 22 15.5C22 13.567 20.433 12 18.5 12C16.567 12 15 13.567 15 15.5C15 17.433 16.567 19 18.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M5.5 19C7.433 19 9 17.433 9 15.5C9 13.567 7.433 12 5.5 12C3.567 12 2 13.567 2 15.5C2 17.433 3.567 19 5.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M6 15.5H11.3602C11.7502 15.5 12.1047 15.2732 12.2681 14.9191L15 9\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M8 9H10\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M9 9L12 15\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M12 5H12.8686C13.2543 5 13.6056 5.22183 13.7715 5.57007L18.5 15.5\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M15 6H18.6126C19.2951 6 19.7771 6.6687 19.5613 7.31623L19.2279 8.31623C19.0918 8.72457 18.7097 9 18.2792 9H16\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M5.5 12H3\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M5 3V7.5\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M7 4L3 6.5\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M3 4L7 6.5\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /></g>"
24
+ },
25
+ "bike-down": {
26
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M18.5 19C20.433 19 22 17.433 22 15.5C22 13.567 20.433 12 18.5 12C16.567 12 15 13.567 15 15.5C15 17.433 16.567 19 18.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M5.5 19C7.433 19 9 17.433 9 15.5C9 13.567 7.433 12 5.5 12C3.567 12 2 13.567 2 15.5C2 17.433 3.567 19 5.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M6 15.5H11.3602C11.7502 15.5 12.1047 15.2732 12.2681 14.9191L15 9\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M8 9H10\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M9 9L12 15\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M12 5H12.8686C13.2543 5 13.6056 5.22183 13.7715 5.57007L18.5 15.5\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M15 6H18.6126C19.2951 6 19.7771 6.6687 19.5613 7.31623L19.2279 8.31623C19.0918 8.72457 18.7097 9 18.2792 9H16\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M5.5 12H3\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M4 8L4 2.5M4 8L2 5.5M4 8L6 5.5\" stroke-width=\"2\" stroke-linecap=\"round\" /></g>"
27
+ },
28
+ "bike-off": {
29
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M18.5 18C20.433 18 22 17.433 22 15.5C22 13.567 20.433 12 18.5 12C16.567 12 15 13.567 15 15.5C15 17.433 16.567 18 18.5 18Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M5.5 18C7.433 18 9 17.433 9 15.5C9 13.567 7.433 12 5.5 12C3.567 12 2 13.567 2 15.5C2 17.433 3.567 18 5.5 18Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M6 15.5H11.3602C11.7502 15.5 12.1047 15.2732 12.2681 14.9191L15 9\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M8 9H10\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M9 9L12 15\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M12 5H12.8686C13.2543 5 13.6056 5.22183 13.7715 5.57007L18.5 15.5\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M15 6H18.6126C19.2951 6 19.7771 6.6687 19.5613 7.31623L19.2279 8.31623C19.0918 8.72457 18.7097 9 18.2792 9H16\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M5.5 12H3\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M8 18H3\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M21 18H16\" stroke-width=\"2\" stroke-linecap=\"round\" /></g>"
30
+ },
31
+ "bike-up": {
32
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M18.5 19C20.433 19 22 17.433 22 15.5C22 13.567 20.433 12 18.5 12C16.567 12 15 13.567 15 15.5C15 17.433 16.567 19 18.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M5.5 19C7.433 19 9 17.433 9 15.5C9 13.567 7.433 12 5.5 12C3.567 12 2 13.567 2 15.5C2 17.433 3.567 19 5.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M6 15.5H11.3602C11.7502 15.5 12.1047 15.2732 12.2681 14.9191L15 9\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M8 9H10\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M9 9L12 15\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M12 5H12.8686C13.2543 5 13.6056 5.22183 13.7715 5.57007L18.5 15.5\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M15 6H18.6126C19.2951 6 19.7771 6.6687 19.5613 7.31623L19.2279 8.31623C19.0918 8.72457 18.7097 9 18.2792 9H16\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M5.5 12H3\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M4 2.5V8M4 2.5L6 5M4 2.5L2 5\" stroke-width=\"2\" stroke-linecap=\"round\" /></g>"
33
+ },
34
+ "bike-x": {
35
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M18.5 19C20.433 19 22 17.433 22 15.5C22 13.567 20.433 12 18.5 12C16.567 12 15 13.567 15 15.5C15 17.433 16.567 19 18.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M5.5 19C7.433 19 9 17.433 9 15.5C9 13.567 7.433 12 5.5 12C3.567 12 2 13.567 2 15.5C2 17.433 3.567 19 5.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M6 15.5H11.3602C11.7502 15.5 12.1047 15.2732 12.2681 14.9191L15 9\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M8 9H10\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M9 9L12 15\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M12 5H12.8686C13.2543 5 13.6056 5.22183 13.7715 5.57007L18.5 15.5\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M15 6H18.6126C19.2951 6 19.7771 6.6687 19.5613 7.31623L19.2279 8.31623C19.0918 8.72457 18.7097 9 18.2792 9H16\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M5.5 12H3\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M3 7L7 3M7 7L3 3\" stroke-width=\"2\" stroke-linecap=\"round\" /></g>"
36
+ },
37
+ "bike": {
38
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M18.5 19C20.433 19 22 17.433 22 15.5C22 13.567 20.433 12 18.5 12C16.567 12 15 13.567 15 15.5C15 17.433 16.567 19 18.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M5.5 19C7.433 19 9 17.433 9 15.5C9 13.567 7.433 12 5.5 12C3.567 12 2 13.567 2 15.5C2 17.433 3.567 19 5.5 19Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M6 15.5H11.3602C11.7502 15.5 12.1047 15.2732 12.2681 14.9191L15 9\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M8 9H10\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M9 9L12 15\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M12 5H12.8686C13.2543 5 13.6056 5.22183 13.7715 5.57007L18.5 15.5\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M15 6H18.6126C19.2951 6 19.7771 6.6687 19.5613 7.31623L19.2279 8.31623C19.0918 8.72457 18.7097 9 18.2792 9H16\" stroke-width=\"2\" stroke-linecap=\"round\" /><path d=\"M5.5 12H3\" stroke-width=\"2\" stroke-linecap=\"round\" /></g>"
39
+ },
40
+ "mark-fill": {
41
+ "body": "<g fill=\"currentColor\" stroke=\"currentColor\"><path d=\"M12 2L17.2 12.4H15.4L17.2 16H6.8L8.6 12.4H6.8L12 2Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M10.2 16V22H8.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M13.8 16V22H15.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /></g>"
42
+ },
43
+ "mark": {
44
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M12 2L17.2 12.4H15.4L17.2 16H6.8L8.6 12.4H6.8L12 2Z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M10.2 16V22H8.6\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M13.8 16V22H15.4\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /></g>"
45
+ },
46
+ "parking": {
47
+ "body": "<g fill=\"currentColor\"><path d=\"M4.68 20.5 6.48 3.5H12.99Q16.15 3.5 17.78 5.02Q19.41 6.53 19.32 9.35Q19.24 12.66 17.19 14.43Q15.14 16.2 11.62 16.2H10.22L9.78 20.5ZM10.7 11.79H11.95Q13 11.79 13.57 11.26Q14.14 10.73 14.2 9.74Q14.23 8.88 13.77 8.4Q13.3 7.91 12.38 7.91H11.1Z\" /></g>"
48
+ },
49
+ "star-fill": {
50
+ "body": "<g fill=\"currentColor\" stroke=\"currentColor\"><path d=\"M 11.5251 2.2969 a 0.5299 0.5299 0 0 1 0.9498 0 l 2.3095 4.6781 a 2.1196 2.1196 0 0 0 1.5947 1.1598 l 5.165 0.7558 a 0.5299 0.5299 0 0 1 0.2939 0.9038 l -3.7353 3.6373 a 2.1196 2.1196 0 0 0 -0.6109 1.8776 l 0.8818 5.139 a 0.5299 0.5299 0 0 1 -0.7708 0.5599 l -4.6171 -2.4275 a 2.1196 2.1196 0 0 0 -1.9726 0 L 6.3971 21.0082 a 0.5299 0.5299 0 0 1 -0.7698 -0.5599 l 0.8808 -5.138 a 2.1196 2.1196 0 0 0 -0.6109 -1.8786 L 2.162 9.7954 a 0.5299 0.5299 0 0 1 0.2939 -0.9058 l 5.164 -0.7548 a 2.1196 2.1196 0 0 0 1.5967 -1.1598 z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /></g>"
51
+ },
52
+ "star": {
53
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><path d=\"M 11.5251 2.2969 a 0.5299 0.5299 0 0 1 0.9498 0 l 2.3095 4.6781 a 2.1196 2.1196 0 0 0 1.5947 1.1598 l 5.165 0.7558 a 0.5299 0.5299 0 0 1 0.2939 0.9038 l -3.7353 3.6373 a 2.1196 2.1196 0 0 0 -0.6109 1.8776 l 0.8818 5.139 a 0.5299 0.5299 0 0 1 -0.7708 0.5599 l -4.6171 -2.4275 a 2.1196 2.1196 0 0 0 -1.9726 0 L 6.3971 21.0082 a 0.5299 0.5299 0 0 1 -0.7698 -0.5599 l 0.8808 -5.138 a 2.1196 2.1196 0 0 0 -0.6109 -1.8786 L 2.162 9.7954 a 0.5299 0.5299 0 0 1 0.2939 -0.9058 l 5.164 -0.7548 a 2.1196 2.1196 0 0 0 1.5967 -1.1598 z\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /></g>"
54
+ },
55
+ "traffic-light-caution": {
56
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><rect x=\"7\" y=\"2\" width=\"10\" height=\"18\" rx=\"2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M12 20v2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><circle cx=\"12\" cy=\"6\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"12\" cy=\"11\" r=\"2\" fill=\"var(--ui-warning, currentColor)\" stroke=\"none\" /><circle cx=\"12\" cy=\"16\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /></g>"
57
+ },
58
+ "traffic-light-go": {
59
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><rect x=\"7\" y=\"2\" width=\"10\" height=\"18\" rx=\"2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M12 20v2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><circle cx=\"12\" cy=\"6\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"12\" cy=\"11\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"12\" cy=\"16\" r=\"2\" fill=\"var(--ui-success, currentColor)\" stroke=\"none\" /></g>"
60
+ },
61
+ "traffic-light-lit": {
62
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><rect x=\"7\" y=\"2\" width=\"10\" height=\"18\" rx=\"2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M12 20v2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><circle cx=\"12\" cy=\"6\" r=\"2\" fill=\"var(--ui-error, currentColor)\" stroke=\"none\" /><circle cx=\"12\" cy=\"11\" r=\"2\" fill=\"var(--ui-warning, currentColor)\" stroke=\"none\" /><circle cx=\"12\" cy=\"16\" r=\"2\" fill=\"var(--ui-success, currentColor)\" stroke=\"none\" /></g>"
63
+ },
64
+ "traffic-light-stop": {
65
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><rect x=\"7\" y=\"2\" width=\"10\" height=\"18\" rx=\"2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M12 20v2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><circle cx=\"12\" cy=\"6\" r=\"2\" fill=\"var(--ui-error, currentColor)\" stroke=\"none\" /><circle cx=\"12\" cy=\"11\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"12\" cy=\"16\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /></g>"
66
+ },
67
+ "traffic-light": {
68
+ "body": "<g fill=\"none\" stroke=\"currentColor\"><rect x=\"7\" y=\"2\" width=\"10\" height=\"18\" rx=\"2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><path d=\"M12 20v2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" /><circle cx=\"12\" cy=\"6\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"12\" cy=\"11\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"12\" cy=\"16\" r=\"2\" fill=\"currentColor\" stroke=\"none\" /></g>"
69
+ }
70
+ },
71
+ "width": 24,
72
+ "height": 24
73
+ };
74
+
75
+ /** Every icon in the Forest set, e.g. `bike-off` → `i-forest-bike-off`. */
76
+ export const FOREST_ICON_NAMES: readonly string[] = ["bike-asterisk","bike-down","bike-off","bike-up","bike-x","bike","mark-fill","mark","parking","star-fill","star","traffic-light-caution","traffic-light-go","traffic-light-lit","traffic-light-stop","traffic-light"];
77
+
78
+ /** Every Lucide icon name, for the docs catalogue search. Names only — no path data. */
79
+ export const LUCIDE_ICON_NAMES: readonly string[] = ["a-arrow-down","a-arrow-up","a-large-small","accessibility","activity","air-vent","airplay","alarm-clock","alarm-clock-check","alarm-clock-minus","alarm-clock-off","alarm-clock-plus","alarm-smoke","album","align-center","align-center-horizontal","align-center-vertical","align-end-horizontal","align-end-vertical","align-horizontal-distribute-center","align-horizontal-distribute-end","align-horizontal-distribute-start","align-horizontal-justify-center","align-horizontal-justify-end","align-horizontal-justify-start","align-horizontal-space-around","align-horizontal-space-between","align-justify","align-left","align-right","align-start-horizontal","align-start-vertical","align-vertical-distribute-center","align-vertical-distribute-end","align-vertical-distribute-start","align-vertical-justify-center","align-vertical-justify-end","align-vertical-justify-start","align-vertical-space-around","align-vertical-space-between","ambulance","ampersand","ampersands","amphora","anchor","angry","annoyed","antenna","anvil","aperture","app-window","app-window-mac","apple","archive","archive-restore","archive-x","area-chart","armchair","arrow-big-down","arrow-big-down-dash","arrow-big-left","arrow-big-left-dash","arrow-big-right","arrow-big-right-dash","arrow-big-up","arrow-big-up-dash","arrow-down","arrow-down-0-1","arrow-down-1-0","arrow-down-a-z","arrow-down-from-line","arrow-down-left","arrow-down-narrow-wide","arrow-down-right","arrow-down-to-dot","arrow-down-to-line","arrow-down-up","arrow-down-wide-narrow","arrow-down-z-a","arrow-left","arrow-left-from-line","arrow-left-right","arrow-left-to-line","arrow-right","arrow-right-from-line","arrow-right-left","arrow-right-to-line","arrow-up","arrow-up-0-1","arrow-up-1-0","arrow-up-a-z","arrow-up-down","arrow-up-from-dot","arrow-up-from-line","arrow-up-left","arrow-up-narrow-wide","arrow-up-right","arrow-up-to-line","arrow-up-wide-narrow","arrow-up-z-a","arrows-up-from-line","asterisk","astroid","at-sign","atom","audio-lines","audio-waveform","award","axe","axis-3d","baby","backpack","badge","badge-alert","badge-cent","badge-check","badge-dollar-sign","badge-euro","badge-indian-rupee","badge-info","badge-japanese-yen","badge-minus","badge-percent","badge-plus","badge-pound-sterling","badge-question-mark","badge-russian-ruble","badge-swiss-franc","badge-turkish-lira","badge-x","baggage-claim","balloon","ban","banana","bandage","banknote","banknote-arrow-down","banknote-arrow-up","banknote-x","bar-chart-3","bar-chart-4","bar-chart-big","bar-chart-horizontal","bar-chart-horizontal-big","barcode","barrel","baseline","bath","battery","battery-charging","battery-full","battery-low","battery-medium","battery-plus","battery-warning","beaker","bean","bean-off","bed","bed-double","bed-single","beef","beef-off","beer","beer-off","bell","bell-check","bell-dot","bell-electric","bell-minus","bell-off","bell-plus","bell-ring","between-horizontal-end","between-horizontal-start","between-vertical-end","between-vertical-start","biceps-flexed","bike","binary","binoculars","biohazard","bird","birdhouse","bitcoin","blend","blender","blinds","blocks","bluetooth","bluetooth-connected","bluetooth-off","bluetooth-searching","bold","bolt","bomb","bone","book","book-a","book-alert","book-audio","book-check","book-copy","book-dashed","book-down","book-headphones","book-heart","book-image","book-key","book-lock","book-marked","book-minus","book-open","book-open-check","book-open-text","book-plus","book-search","book-text","book-type","book-up","book-up-2","book-user","book-x","bookmark","bookmark-check","bookmark-minus","bookmark-off","bookmark-plus","bookmark-x","boom-box","bot","bot-message-square","bot-off","bottle-wine","bow-arrow","box","boxes","braces","brackets","brain","brain-circuit","brain-cog","brick-wall","brick-wall-fire","brick-wall-shield","briefcase","briefcase-business","briefcase-conveyor-belt","briefcase-medical","bring-to-front","broccoli","brush","brush-cleaning","bubbles","bug","bug-off","bug-play","building","building-2","bus","bus-front","cable","cable-car","cake","cake-slice","calculator","calendar","calendar-1","calendar-arrow-down","calendar-arrow-up","calendar-check","calendar-check-2","calendar-clock","calendar-cog","calendar-days","calendar-fold","calendar-heart","calendar-minus","calendar-minus-2","calendar-off","calendar-plus","calendar-plus-2","calendar-range","calendar-search","calendar-sync","calendar-x","calendar-x-2","calendars","camera","camera-off","candlestick-chart","candy","candy-cane","candy-off","cannabis","cannabis-off","captions","captions-off","car","car-front","car-taxi-front","caravan","card-sim","carrot","case-lower","case-sensitive","case-upper","cassette-tape","cast","castle","cat","cctv","cctv-off","chart-area","chart-bar","chart-bar-big","chart-bar-decreasing","chart-bar-increasing","chart-bar-stacked","chart-candlestick","chart-column","chart-column-big","chart-column-decreasing","chart-column-increasing","chart-column-stacked","chart-gantt","chart-line","chart-network","chart-no-axes-column","chart-no-axes-column-decreasing","chart-no-axes-column-increasing","chart-no-axes-combined","chart-no-axes-gantt","chart-pie","chart-scatter","chart-spline","check","check-check","check-line","chef-hat","cherry","chess-bishop","chess-king","chess-knight","chess-pawn","chess-queen","chess-rook","chevron-down","chevron-first","chevron-last","chevron-left","chevron-right","chevron-up","chevrons-down","chevrons-down-up","chevrons-left","chevrons-left-right","chevrons-left-right-ellipsis","chevrons-right","chevrons-right-left","chevrons-up","chevrons-up-down","chrome","chromium","church","cigarette","cigarette-off","circle","circle-alert","circle-arrow-down","circle-arrow-left","circle-arrow-out-down-left","circle-arrow-out-down-right","circle-arrow-out-up-left","circle-arrow-out-up-right","circle-arrow-right","circle-arrow-up","circle-check","circle-check-big","circle-chevron-down","circle-chevron-left","circle-chevron-right","circle-chevron-up","circle-dashed","circle-divide","circle-dollar-sign","circle-dot","circle-dot-dashed","circle-ellipsis","circle-equal","circle-fading-arrow-up","circle-fading-plus","circle-gauge","circle-minus","circle-off","circle-parking","circle-parking-off","circle-pause","circle-percent","circle-pile","circle-play","circle-plus","circle-pound-sterling","circle-power","circle-question-mark","circle-slash","circle-slash-2","circle-small","circle-star","circle-stop","circle-user","circle-user-round","circle-x","circuit-board","citrus","clapperboard","clipboard","clipboard-check","clipboard-clock","clipboard-copy","clipboard-list","clipboard-minus","clipboard-paste","clipboard-pen","clipboard-pen-line","clipboard-plus","clipboard-type","clipboard-x","clock","clock-1","clock-10","clock-11","clock-12","clock-2","clock-3","clock-4","clock-5","clock-6","clock-7","clock-8","clock-9","clock-alert","clock-arrow-down","clock-arrow-up","clock-check","clock-fading","clock-plus","closed-caption","cloud","cloud-alert","cloud-backup","cloud-check","cloud-cog","cloud-download","cloud-drizzle","cloud-fog","cloud-hail","cloud-lightning","cloud-moon","cloud-moon-rain","cloud-off","cloud-rain","cloud-rain-wind","cloud-snow","cloud-sun","cloud-sun-rain","cloud-sync","cloud-upload","cloudy","clover","club","code","code-xml","codepen","codesandbox","coffee","cog","coins","columns-2","columns-3","columns-3-cog","columns-4","combine","command","compass","component","computer","concierge-bell","cone","construction","contact","contact-round","container","contrast","cookie","cooking-pot","copy","copy-check","copy-minus","copy-plus","copy-slash","copy-x","copyleft","copyright","corner-down-left","corner-down-right","corner-left-down","corner-left-up","corner-right-down","corner-right-up","corner-up-left","corner-up-right","cpu","creative-commons","credit-card","croissant","crop","cross","crosshair","crown","cuboid","cup-soda","currency","cylinder","dam","database","database-backup","database-search","database-zap","decimals-arrow-left","decimals-arrow-right","delete","dessert","diameter","diamond","diamond-minus","diamond-percent","diamond-plus","dice-1","dice-2","dice-3","dice-4","dice-5","dice-6","dices","diff","disc","disc-2","disc-3","disc-album","divide","dna","dna-off","dock","dog","dollar-sign","donut","door-closed","door-closed-locked","door-open","dot","download","drafting-compass","drama","dribbble","drill","drone","droplet","droplet-off","droplets","drum","drumstick","dumbbell","ear","ear-off","earth","earth-lock","eclipse","egg","egg-fried","egg-off","ellipse","ellipsis","ellipsis-vertical","equal","equal-approximately","equal-not","eraser","ethernet-port","euro","ev-charger","expand","external-link","eye","eye-closed","eye-off","facebook","factory","fan","fast-forward","feather","fence","ferris-wheel","figma","file","file-archive","file-audio","file-audio-2","file-axis-3d","file-badge","file-badge-2","file-box","file-braces","file-braces-corner","file-chart-column","file-chart-column-increasing","file-chart-line","file-chart-pie","file-check","file-check-2","file-check-corner","file-clock","file-code","file-code-2","file-code-corner","file-cog","file-diff","file-digit","file-down","file-exclamation-point","file-headphone","file-heart","file-image","file-input","file-json","file-json-2","file-key","file-key-2","file-lock","file-lock-2","file-minus","file-minus-2","file-minus-corner","file-music","file-output","file-pen","file-pen-line","file-pie-chart","file-play","file-plus","file-plus-2","file-plus-corner","file-question-mark","file-scan","file-search","file-search-2","file-search-corner","file-signal","file-sliders","file-spreadsheet","file-stack","file-symlink","file-terminal","file-text","file-type","file-type-2","file-type-corner","file-up","file-user","file-video-camera","file-volume","file-volume-2","file-warning","file-x","file-x-2","file-x-corner","files","film","filter","filter-x","fingerprint-pattern","fire-extinguisher","fish","fish-off","fish-symbol","fishing-hook","fishing-rod","flag","flag-off","flag-triangle-left","flag-triangle-right","flame","flame-kindling","flashlight","flashlight-off","flask-conical","flask-conical-off","flask-round","flip-horizontal-2","flip-vertical-2","flower","flower-2","focus","fold-horizontal","fold-vertical","folder","folder-archive","folder-bookmark","folder-check","folder-clock","folder-closed","folder-code","folder-cog","folder-dot","folder-down","folder-git","folder-git-2","folder-heart","folder-input","folder-kanban","folder-key","folder-lock","folder-minus","folder-open","folder-open-dot","folder-output","folder-pen","folder-plus","folder-root","folder-search","folder-search-2","folder-symlink","folder-sync","folder-tree","folder-up","folder-x","folders","footprints","forklift","form","forward","frame","framer","frown","fuel","fullscreen","funnel","funnel-plus","funnel-x","gallery-horizontal","gallery-horizontal-end","gallery-thumbnails","gallery-vertical","gallery-vertical-end","gamepad","gamepad-2","gamepad-directional","gauge","gavel","gem","georgian-lari","ghost","gift","git-branch","git-branch-minus","git-branch-plus","git-commit-horizontal","git-commit-vertical","git-compare","git-compare-arrows","git-fork","git-graph","git-merge","git-merge-conflict","git-pull-request","git-pull-request-arrow","git-pull-request-closed","git-pull-request-create","git-pull-request-create-arrow","git-pull-request-draft","github","gitlab","glass-water","glasses","globe","globe-check","globe-lock","globe-off","globe-x","goal","gpu","graduation-cap","grape","grid-2x2","grid-2x2-check","grid-2x2-plus","grid-2x2-x","grid-3x2","grid-3x3","grip","grip-horizontal","grip-vertical","group","guitar","ham","hamburger","hammer","hand","hand-coins","hand-fist","hand-grab","hand-heart","hand-helping","hand-metal","hand-platter","handbag","handshake","hard-drive","hard-drive-download","hard-drive-upload","hard-hat","hash","hat-glasses","haze","hd","hdmi-port","heading","heading-1","heading-2","heading-3","heading-4","heading-5","heading-6","headphone-off","headphones","headset","heart","heart-crack","heart-handshake","heart-minus","heart-off","heart-plus","heart-pulse","heart-x","heater","helicopter","hexagon","highlighter","history","hop","hop-off","hospital","hotel","hourglass","house","house-heart","house-plug","house-plus","house-wifi","ice-cream-bowl","ice-cream-cone","id-card","id-card-lanyard","image","image-down","image-minus","image-off","image-play","image-plus","image-up","image-upscale","images","import","inbox","indent-decrease","indent-increase","indian-rupee","infinity","info","inspection-panel","instagram","italic","iteration-ccw","iteration-cw","japanese-yen","joystick","kanban","kayak","key","key-round","key-square","keyboard","keyboard-music","keyboard-off","lamp","lamp-ceiling","lamp-desk","lamp-floor","lamp-wall-down","lamp-wall-up","land-plot","landmark","languages","laptop","laptop-minimal","laptop-minimal-check","lasso","lasso-select","laugh","layers","layers-2","layers-3","layers-minus","layers-plus","layout-dashboard","layout-grid","layout-list","layout-panel-left","layout-panel-top","layout-template","leaf","leafy-green","lectern","lens-concave","lens-convex","letter-text","library","library-big","life-buoy","ligature","lightbulb","lightbulb-off","line-chart","line-dot-right-horizontal","line-squiggle","line-style","link","link-2","link-2-off","linkedin","list","list-check","list-checks","list-chevrons-down-up","list-chevrons-up-down","list-collapse","list-end","list-filter","list-filter-plus","list-indent-decrease","list-indent-increase","list-minus","list-music","list-ordered","list-plus","list-restart","list-start","list-todo","list-tree","list-video","list-x","loader","loader-circle","loader-pinwheel","locate","locate-fixed","locate-off","lock","lock-keyhole","lock-keyhole-open","lock-open","log-in","log-out","logs","lollipop","luggage","magnet","mail","mail-check","mail-minus","mail-open","mail-plus","mail-question-mark","mail-search","mail-warning","mail-x","mailbox","mails","map","map-minus","map-pin","map-pin-check","map-pin-check-inside","map-pin-house","map-pin-minus","map-pin-minus-inside","map-pin-off","map-pin-pen","map-pin-plus","map-pin-plus-inside","map-pin-search","map-pin-x","map-pin-x-inside","map-pinned","map-plus","mars","mars-stroke","martini","maximize","maximize-2","medal","megaphone","megaphone-off","meh","memory-stick","menu","merge","message-circle","message-circle-check","message-circle-code","message-circle-dashed","message-circle-heart","message-circle-more","message-circle-off","message-circle-plus","message-circle-question-mark","message-circle-reply","message-circle-warning","message-circle-x","message-square","message-square-check","message-square-code","message-square-dashed","message-square-diff","message-square-dot","message-square-heart","message-square-lock","message-square-more","message-square-off","message-square-plus","message-square-quote","message-square-reply","message-square-share","message-square-text","message-square-warning","message-square-x","messages-square","metronome","mic","mic-off","mic-vocal","microchip","microscope","microwave","milestone","milk","milk-off","minimize","minimize-2","minus","mirror-rectangular","mirror-round","monitor","monitor-check","monitor-cloud","monitor-cog","monitor-dot","monitor-down","monitor-off","monitor-pause","monitor-play","monitor-smartphone","monitor-speaker","monitor-stop","monitor-up","monitor-x","moon","moon-star","motorbike","mountain","mountain-snow","mouse","mouse-left","mouse-off","mouse-pointer","mouse-pointer-2","mouse-pointer-2-off","mouse-pointer-ban","mouse-pointer-click","mouse-right","move","move-3d","move-diagonal","move-diagonal-2","move-down","move-down-left","move-down-right","move-horizontal","move-left","move-right","move-up","move-up-left","move-up-right","move-vertical","music","music-2","music-3","music-4","navigation","navigation-2","navigation-2-off","navigation-off","network","newspaper","nfc","non-binary","notebook","notebook-pen","notebook-tabs","notebook-text","notepad-text","notepad-text-dashed","nut","nut-off","octagon","octagon-alert","octagon-minus","octagon-pause","octagon-x","omega","option","orbit","origami","package","package-2","package-check","package-minus","package-open","package-plus","package-search","package-x","paint-bucket","paint-roller","paintbrush","paintbrush-vertical","palette","panda","panel-bottom","panel-bottom-close","panel-bottom-dashed","panel-bottom-open","panel-left","panel-left-close","panel-left-dashed","panel-left-open","panel-left-right-dashed","panel-right","panel-right-close","panel-right-dashed","panel-right-open","panel-top","panel-top-bottom-dashed","panel-top-close","panel-top-dashed","panel-top-open","panels-left-bottom","panels-right-bottom","panels-top-left","paperclip","parasol","parentheses","parking-meter","party-popper","pause","paw-print","pc-case","pen","pen-line","pen-off","pen-tool","pencil","pencil-line","pencil-off","pencil-ruler","pentagon","percent","person-standing","philippine-peso","phone","phone-call","phone-forwarded","phone-incoming","phone-missed","phone-off","phone-outgoing","pi","piano","pickaxe","picture-in-picture","picture-in-picture-2","pie-chart","piggy-bank","pilcrow","pilcrow-left","pilcrow-right","pill","pill-bottle","pin","pin-off","pipette","pizza","plane","plane-landing","plane-takeoff","play","play-off","plug","plug-2","plug-zap","plus","pocket","pocket-knife","podcast","pointer","pointer-off","popcorn","popsicle","pound-sterling","power","power-off","presentation","printer","printer-check","printer-x","projector","proportions","puzzle","pyramid","qr-code","quote","rabbit","radar","radiation","radical","radio","radio-off","radio-receiver","radio-tower","radius","rail-symbol","rainbow","rat","ratio","receipt","receipt-cent","receipt-euro","receipt-indian-rupee","receipt-japanese-yen","receipt-pound-sterling","receipt-russian-ruble","receipt-swiss-franc","receipt-text","receipt-turkish-lira","rectangle-circle","rectangle-ellipsis","rectangle-goggles","rectangle-horizontal","rectangle-vertical","recycle","redo","redo-2","redo-dot","refresh-ccw","refresh-ccw-dot","refresh-cw","refresh-cw-off","refrigerator","regex","remove-formatting","repeat","repeat-1","repeat-2","repeat-off","replace","replace-all","reply","reply-all","rewind","ribbon","road","rocket","rocking-chair","roller-coaster","rose","rotate-3d","rotate-ccw","rotate-ccw-key","rotate-ccw-square","rotate-cw","rotate-cw-square","route","route-off","router","rows-2","rows-3","rows-4","rss","ruler","ruler-dimension-line","russian-ruble","sailboat","salad","sandwich","satellite","satellite-dish","saudi-riyal","save","save-all","save-off","scale","scale-3d","scaling","scan","scan-barcode","scan-eye","scan-face","scan-heart","scan-line","scan-qr-code","scan-search","scan-text","scatter-chart","school","scissors","scissors-line-dashed","scooter","screen-share","screen-share-off","scroll","scroll-text","search","search-alert","search-check","search-code","search-large","search-slash","search-x","section","send","send-horizontal","send-to-back","separator-horizontal","separator-vertical","server","server-cog","server-crash","server-off","settings","settings-2","shapes","share","share-2","sheet","shell","shelving-unit","shield","shield-alert","shield-ban","shield-check","shield-cog","shield-cog-corner","shield-ellipsis","shield-half","shield-minus","shield-off","shield-plus","shield-question-mark","shield-user","shield-x","ship","ship-wheel","shirt","shopping-bag","shopping-basket","shopping-cart","shovel","shower-head","shredder","shrimp","shrink","shrub","shuffle","sigma","signal","signal-high","signal-low","signal-medium","signal-zero","signature","signpost","signpost-big","siren","skip-back","skip-forward","skull","slack","slash","slice","sliders-horizontal","sliders-vertical","smartphone","smartphone-charging","smartphone-nfc","smile","smile-plus","snail","snowflake","soap-dispenser-droplet","sofa","solar-panel","soup","space","spade","sparkle","sparkles","speaker","speech","spell-check","spell-check-2","spline","spline-pointer","split","spool","sport-shoe","spotlight","spray-can","sprout","square","square-activity","square-arrow-down","square-arrow-down-left","square-arrow-down-right","square-arrow-left","square-arrow-out-down-left","square-arrow-out-down-right","square-arrow-out-up-left","square-arrow-out-up-right","square-arrow-right","square-arrow-right-enter","square-arrow-right-exit","square-arrow-up","square-arrow-up-left","square-arrow-up-right","square-asterisk","square-bottom-dashed-scissors","square-centerline-dashed-horizontal","square-centerline-dashed-vertical","square-chart-gantt","square-check","square-check-big","square-chevron-down","square-chevron-left","square-chevron-right","square-chevron-up","square-code","square-dashed","square-dashed-bottom","square-dashed-bottom-code","square-dashed-kanban","square-dashed-mouse-pointer","square-dashed-text","square-dashed-top-solid","square-divide","square-dot","square-equal","square-function","square-kanban","square-library","square-m","square-menu","square-minus","square-mouse-pointer","square-parking","square-parking-off","square-pause","square-pen","square-percent","square-pi","square-pilcrow","square-play","square-plus","square-power","square-radical","square-round-corner","square-scissors","square-sigma","square-slash","square-split-horizontal","square-split-vertical","square-square","square-stack","square-star","square-stop","square-terminal","square-user","square-user-round","square-x","squares-exclude","squares-intersect","squares-subtract","squares-unite","squircle","squircle-dashed","squirrel","stamp","star","star-half","star-off","step-back","step-forward","stethoscope","sticker","sticky-note","sticky-note-check","sticky-note-minus","sticky-note-off","sticky-note-plus","sticky-note-x","sticky-notes","stone","store","stretch-horizontal","stretch-vertical","strikethrough","subscript","sun","sun-dim","sun-medium","sun-moon","sun-snow","sunrise","sunset","superscript","swatch-book","swiss-franc","switch-camera","sword","swords","syringe","table","table-2","table-cells-merge","table-cells-split","table-columns-split","table-of-contents","table-properties","table-rows-split","tablet","tablet-smartphone","tablets","tag","tags","tally-1","tally-2","tally-3","tally-4","tally-5","tangent","target","telescope","tent","tent-tree","terminal","test-tube","test-tube-diagonal","test-tubes","text","text-align-center","text-align-end","text-align-justify","text-align-start","text-cursor","text-cursor-input","text-initial","text-quote","text-search","text-wrap","theater","thermometer","thermometer-snowflake","thermometer-sun","thumbs-down","thumbs-up","ticket","ticket-check","ticket-minus","ticket-percent","ticket-plus","ticket-slash","ticket-x","tickets","tickets-plane","timeline","timer","timer-off","timer-reset","toggle-left","toggle-right","toilet","tool-case","toolbox","tornado","torus","touchpad","touchpad-off","towel-rack","tower-control","toy-brick","tractor","traffic-cone","train-front","train-front-tunnel","train-track","tram-front","transgender","trash","trash-2","tree-deciduous","tree-palm","tree-pine","trees","trello","trending-down","trending-up","trending-up-down","triangle","triangle-alert","triangle-dashed","triangle-right","trophy","truck","truck-electric","turkish-lira","turntable","turtle","tv","tv-minimal","tv-minimal-play","twitch","twitter","type","type-outline","umbrella","umbrella-off","underline","undo","undo-2","undo-dot","unfold-horizontal","unfold-vertical","ungroup","university","unlink","unlink-2","unplug","upload","usb","user","user-check","user-cog","user-key","user-lock","user-minus","user-pen","user-plus","user-round","user-round-check","user-round-cog","user-round-key","user-round-minus","user-round-pen","user-round-plus","user-round-search","user-round-x","user-search","user-star","user-x","users","users-round","utensils","utensils-crossed","utility-pole","van","variable","vault","vector-square","vegan","venetian-mask","venus","venus-and-mars","vibrate","vibrate-off","video","video-off","videotape","view","voicemail","volleyball","volume","volume-1","volume-2","volume-off","volume-x","vote","wallet","wallet-cards","wallet-minimal","wallpaper","wand","wand-sparkles","warehouse","washing-machine","watch","waves","waves-arrow-down","waves-arrow-up","waves-horizontal","waves-ladder","waves-vertical","waypoints","webcam","webhook","webhook-off","weight","weight-tilde","wheat","wheat-off","whole-word","wifi","wifi-cog","wifi-high","wifi-low","wifi-off","wifi-pen","wifi-sync","wifi-zero","wind","wind-arrow-down","wine","wine-off","workflow","worm","wrap-text","wrench","x","x-line-top","youtube","zap","zap-off","zodiac-aquarius","zodiac-aries","zodiac-cancer","zodiac-capricorn","zodiac-gemini","zodiac-leo","zodiac-libra","zodiac-ophiuchus","zodiac-pisces","zodiac-sagittarius","zodiac-scorpio","zodiac-taurus","zodiac-virgo","zoom-in","zoom-out"];
@@ -0,0 +1,17 @@
1
+ export { forestPreset, default, PRESET_STRUCTURAL_MARKERS } from './forest-preset';
2
+ // The three exports below import from ./contexts/* directly rather than the ./contexts barrel: that
3
+ // barrel also re-exports ForestContext.vue (an SFC), and this root barrel must stay SFC-free.
4
+ // scripts/layer.ts imports this file under bun; bun does not error on a .vue import, it loads the SFC
5
+ // as a plain string — so routing through ./contexts would degrade silently rather than throw.
6
+ export { forestMobileTheme, forestMobileProps, forestMobileManifest } from './contexts/mobile-theme';
7
+ export { forestMarketingTheme, forestMarketingProps, forestMarketingManifest } from './contexts/marketing-theme';
8
+ export {
9
+ forestContexts,
10
+ CONTEXT_REGISTER_ROWS,
11
+ HOOK_CLASSES,
12
+ deriveChannels,
13
+ themeTokens,
14
+ stampedMarkers,
15
+ consumedMarkers,
16
+ } from './contexts/contexts';
17
+ export type { ContextManifest, ContextChannel, ForestContextName } from './contexts/contexts';
@@ -0,0 +1,253 @@
1
+ <script setup lang="ts">
2
+ // A KPI tile: one figure, what it is, and optionally how it has moved.
3
+ //
4
+ // NAMING. `value` is the tile's stable KEY — what a group's `modelValue` matches on. The number is
5
+ // `metric`. `key` is reserved in Vue templates and `id` implies a DOM id, so the obvious spellings
6
+ // were both taken.
7
+ //
8
+ // The tile is a UCard, so it inherits the card surface and moves when cards move. Inside a group it
9
+ // registers itself, spans four subgrid rows, and may become a tab or a toggle.
10
+ import { computed, inject, onBeforeUnmount, onMounted, useId, useSlots } from 'vue';
11
+ import { kpiTheme, KPI_GROUP_KEY, type KpiSlot } from './kpi.theme';
12
+ import { formatMetric, type KpiFormat } from './kpiFormat';
13
+ import { resolveDelta, type KpiDelta, type KpiSentiment } from './kpiDelta';
14
+
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ label: string;
18
+ /** Stable key. Required when the tile sits in a selectable group. */
19
+ value?: string;
20
+ /** The number. Pre-formatted text goes through the `metric` slot instead. */
21
+ metric?: number | null;
22
+
23
+ format?: KpiFormat;
24
+ precision?: number;
25
+ locale?: string;
26
+ unit?: string;
27
+ noValue?: string;
28
+
29
+ /** Required for an aggregate ("last 7 days"); omit for an instantaneous reading. */
30
+ period?: string;
31
+ asOf?: Date | string;
32
+ /** Rendered, and wired to the value with aria-describedby. Never tooltip-only. */
33
+ caveat?: string;
34
+ /** Info tooltip. Ignored on a selectable tile, which may not hold interactive children. */
35
+ hint?: string;
36
+
37
+ /** Forces the figure's face. Unset lets the context's --fx-metric traits decide. */
38
+ face?: 'tabular' | 'display';
39
+
40
+ /**
41
+ * How the `visual` slot meets the card. `bleed` runs the graphic to the side and bottom edges —
42
+ * right for a sparkline, which is data and nothing else. `inset` keeps the card's padding and
43
+ * top-aligns, for a graphic carrying text such as FDistributionBar's legend. Ignored without a
44
+ * visual.
45
+ */
46
+ visualFit?: 'bleed' | 'inset';
47
+
48
+ sentiment?: KpiSentiment;
49
+ status?: 'ok' | 'warn' | 'critical';
50
+ emphasis?: 'value' | 'surface';
51
+ partial?: boolean;
52
+
53
+ delta?: KpiDelta;
54
+
55
+ to?: string;
56
+ linkTarget?: string;
57
+
58
+ state?: 'ok' | 'loading' | 'refreshing' | 'empty' | 'error';
59
+ reason?: 'no-data' | 'permission' | 'system' | 'not-configured';
60
+
61
+ ui?: Partial<Record<KpiSlot, string>>;
62
+ }>(),
63
+ {
64
+ sentiment: 'neutral',
65
+ status: 'ok',
66
+ emphasis: 'value',
67
+ state: 'ok',
68
+ noValue: '—',
69
+ // Bleed is what every visual did before there was a choice, so the default keeps sparkline
70
+ // tiles exactly as they were.
71
+ visualFit: 'bleed',
72
+ },
73
+ );
74
+
75
+ const slots = useSlots();
76
+ const group = inject(KPI_GROUP_KEY, undefined);
77
+ const uid = useId();
78
+ const key = computed(() => props.value ?? `${uid}`);
79
+
80
+ onMounted(() => {
81
+ if (import.meta.env.DEV && group?.selectable && !props.value)
82
+ console.warn(`[FKpi] "${props.label}" is in a selectable group and needs a \`value\` to be selectable.`);
83
+ group?.register(key.value);
84
+ });
85
+ onBeforeUnmount(() => group?.unregister(key.value));
86
+
87
+ const selectable = computed(() => !!group?.selectable && !!props.value);
88
+ const selected = computed(() => !!group?.isSelected(key.value));
89
+
90
+ const format = computed(() => props.format ?? group?.format);
91
+ const precision = computed(() => props.precision ?? group?.precision);
92
+
93
+ const isLoading = computed(() => props.state === 'loading');
94
+ const isBlank = computed(() => props.state === 'empty' || props.state === 'error');
95
+
96
+ const REASON_COPY: Record<string, string> = {
97
+ 'no-data': 'No data for this period',
98
+ permission: 'You do not have access to this metric',
99
+ system: 'Could not load this metric',
100
+ 'not-configured': 'Not configured yet',
101
+ };
102
+
103
+ const metricText = computed(() =>
104
+ formatMetric(props.metric, {
105
+ format: format.value,
106
+ precision: precision.value,
107
+ locale: props.locale,
108
+ noValue: props.noValue,
109
+ }),
110
+ );
111
+
112
+ const delta = computed(() =>
113
+ resolveDelta(props.delta, {
114
+ sentiment: props.sentiment,
115
+ locale: props.locale,
116
+ precision: precision.value,
117
+ }),
118
+ );
119
+
120
+ // Period first, then the baseline it was measured against — "last 7 days · vs previous 7 days".
121
+ // The other order reads as one repeated phrase.
122
+ const captionParts = computed(() =>
123
+ [
124
+ props.period,
125
+ props.delta ? `vs ${props.delta.comparisonLabel}` : undefined,
126
+ props.partial ? 'Period in progress' : undefined,
127
+ ].filter(Boolean),
128
+ );
129
+
130
+ const asOfText = computed(() => {
131
+ if (!props.asOf) return undefined;
132
+ const d = props.asOf instanceof Date ? props.asOf : new Date(props.asOf);
133
+ return Number.isNaN(d.getTime())
134
+ ? undefined
135
+ : `Updated ${new Intl.DateTimeFormat(props.locale ?? 'en-GB', { timeStyle: 'short' }).format(d)}`;
136
+ });
137
+
138
+ const caveatId = computed(() => (props.caveat ? `${uid}-caveat` : undefined));
139
+
140
+ const styles = computed(() =>
141
+ kpiTheme({
142
+ naked: group?.naked ?? false,
143
+ aligned: !!group,
144
+ selectable: selectable.value,
145
+ selected: selected.value,
146
+ hovered: selectable.value && !selected.value,
147
+ // Only when there is a visual: `bleed` zeroes the tile's bottom padding, which a tile with
148
+ // nothing in that row must keep.
149
+ visualFit: slots.visual ? props.visualFit : undefined,
150
+ intent: delta.value?.intent,
151
+ face: props.face,
152
+ status: props.status,
153
+ emphasis: props.emphasis,
154
+ dimmed: isBlank.value,
155
+ }),
156
+ );
157
+
158
+ const cls = (slot: KpiSlot) => styles.value[slot]({ class: props.ui?.[slot] });
159
+
160
+ const onKeydown = (e: KeyboardEvent) => {
161
+ if (!selectable.value || !group) return;
162
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
163
+ e.preventDefault();
164
+ group.focusStep(key.value, 1);
165
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
166
+ e.preventDefault();
167
+ group.focusStep(key.value, -1);
168
+ }
169
+ };
170
+ </script>
171
+
172
+ <template>
173
+ <UCard
174
+ :as="selectable ? 'button' : undefined"
175
+ :to="selectable ? undefined : to"
176
+ :target="selectable ? undefined : linkTarget"
177
+ :type="selectable ? 'button' : undefined"
178
+ :data-kpi="key"
179
+ :role="selectable && !group?.multiple ? 'tab' : undefined"
180
+ :aria-selected="selectable && !group?.multiple ? selected : undefined"
181
+ :aria-pressed="selectable && group?.multiple ? selected : undefined"
182
+ :aria-controls="selectable ? group?.controls : undefined"
183
+ :tabindex="selectable ? (group?.isFocusable(key) ? 0 : -1) : undefined"
184
+ :class="cls('root')"
185
+ :ui="{ body: 'contents' }"
186
+ @click="selectable && group?.select(key)"
187
+ @keydown="onKeydown"
188
+ >
189
+ <div :class="cls('header')">
190
+ <slot name="leading" />
191
+ <dt :class="cls('label')">
192
+ <slot name="label">{{ label }}</slot>
193
+ </dt>
194
+ <span v-if="hint && !selectable" :class="cls('hint')">
195
+ <UTooltip :text="hint">
196
+ <UIcon name="i-lucide-info" class="size-3.5" />
197
+ </UTooltip>
198
+ </span>
199
+ </div>
200
+
201
+ <dd :class="cls('body')">
202
+ <USkeleton v-if="isLoading" class="h-8 w-24" aria-hidden="true" />
203
+ <span v-else-if="isBlank" :class="cls('metric')">{{ noValue }}</span>
204
+ <template v-else>
205
+ <span v-if="$slots.prefix" :class="cls('prefix')"><slot name="prefix" /></span>
206
+ <span :class="cls('metric')" :aria-describedby="caveatId">
207
+ <slot name="metric">{{ metricText }}</slot>
208
+ </span>
209
+ <span v-if="unit || $slots.suffix" :class="cls('suffix')">
210
+ <slot name="suffix">{{ unit }}</slot>
211
+ </span>
212
+ </template>
213
+
214
+ <slot name="delta">
215
+ <!-- Not a UBadge: a filled chip reads as a second object beside the figure, and shrinking
216
+ it out of the way costs its legibility (xs resolves to 8px type). Bare text sizes off
217
+ the figure through the same container query. The arrow is aria-hidden — direction and
218
+ baseline are stated in words by the visually hidden sentence below. -->
219
+ <span v-if="delta && !delta.hidden && !isLoading && !isBlank" :class="cls('delta')">
220
+ <UIcon
221
+ :name="
222
+ delta.direction === 'flat'
223
+ ? 'i-lucide-minus'
224
+ : delta.direction === 'up'
225
+ ? 'i-lucide-trending-up'
226
+ : 'i-lucide-trending-down'
227
+ "
228
+ :class="cls('deltaIcon')"
229
+ aria-hidden="true"
230
+ />
231
+ {{ delta.text }}
232
+ </span>
233
+ </slot>
234
+ <span v-if="delta" class="sr-only">{{ delta.sentence }}</span>
235
+ </dd>
236
+
237
+ <p :class="cls('caption')">
238
+ <slot name="caption">
239
+ <span v-if="isBlank && reason">{{ REASON_COPY[reason] }}</span>
240
+ <span v-else>{{ captionParts.join(' · ') }}</span>
241
+ </slot>
242
+ <span v-if="asOfText" class="ms-1">{{ asOfText }}</span>
243
+ <span v-if="caveat" :id="caveatId" class="ms-1">{{ caveat }}</span>
244
+ </p>
245
+
246
+ <div v-if="$slots.visual" :class="cls('visual')"><slot name="visual" /></div>
247
+ <slot name="insight" />
248
+ <div v-if="$slots.links || $slots.footer" :class="cls('footer')">
249
+ <slot name="links" />
250
+ <slot name="footer" />
251
+ </div>
252
+ </UCard>
253
+ </template>
@@ -0,0 +1,161 @@
1
+ <script setup lang="ts">
2
+ // A row of KPI tiles.
3
+ //
4
+ // The row is a four-row grid and each tile spans all four with grid-template-rows: subgrid, so
5
+ // labels, values, captions and visuals align across the row however long any one label runs.
6
+ // Without it, a two-line label pushes its own value down and nothing lines up.
7
+ //
8
+ // Selection: single mode is a tablist with manual activation, pointing at the chart it controls;
9
+ // multiple mode is a set of aria-pressed toggles, because more than one selected tile is not a tab
10
+ // row. One live region for the whole row — a per-tile region turns a refresh into a chorus.
11
+ import { computed, provide, reactive, ref, watch } from 'vue';
12
+ import { KPI_GROUP_KEY, type KpiGroupContext } from './kpi.theme';
13
+ import type { KpiFormat } from './kpiFormat';
14
+ import {
15
+ createKpiRegistry,
16
+ isSelected as isSelectedIn,
17
+ nextKey,
18
+ resolveInitial,
19
+ toggleSelection,
20
+ } from './kpiGroupState';
21
+
22
+ const props = withDefaults(
23
+ defineProps<{
24
+ orientation?: 'horizontal' | 'vertical';
25
+ /** auto-fit floor for the columns. */
26
+ min?: string;
27
+ /** Maximum columns. A wallboard fits eleven tiles across, which nobody wants. */
28
+ columns?: number;
29
+ divided?: boolean;
30
+ /**
31
+ * Drops the divided row's own outer border and radius, so it can sit flush at the top of a card
32
+ * that already draws them. The row keeps the hairlines between tiles and the one along its
33
+ * bottom edge, which is the edge whatever it drives hangs off.
34
+ */
35
+ flush?: boolean;
36
+ format?: KpiFormat;
37
+ precision?: number;
38
+ selectable?: boolean;
39
+ multiple?: boolean;
40
+ modelValue?: string | string[];
41
+ defaultValue?: string;
42
+ /** id of the chart container the row drives. */
43
+ controls?: string;
44
+ announce?: 'off' | 'polite';
45
+ label?: string;
46
+ }>(),
47
+ {
48
+ orientation: 'horizontal',
49
+ min: '10rem',
50
+ announce: 'off',
51
+ },
52
+ );
53
+
54
+ const emit = defineEmits<{ 'update:modelValue': [value: string | string[]] }>();
55
+
56
+ const registry = createKpiRegistry();
57
+ const keys = ref<string[]>([]);
58
+ const focused = ref<string | undefined>(undefined);
59
+
60
+ const selection = computed(() => resolveInitial(keys.value, props.modelValue, props.defaultValue));
61
+
62
+ watch(keys, (list) => {
63
+ if (!props.selectable || !list.length) return;
64
+ const resolved = resolveInitial(list, props.modelValue, props.defaultValue);
65
+ if (resolved !== undefined && resolved !== props.modelValue) emit('update:modelValue', resolved);
66
+ if (focused.value === undefined || !list.includes(focused.value)) focused.value = list[0];
67
+ });
68
+
69
+ const context = reactive({
70
+ register(key: string) {
71
+ registry.register(key);
72
+ keys.value = registry.keys();
73
+ },
74
+ unregister(key: string) {
75
+ registry.unregister(key);
76
+ keys.value = registry.keys();
77
+ },
78
+ select(key: string) {
79
+ focused.value = key;
80
+ emit('update:modelValue', toggleSelection(props.modelValue, key, !!props.multiple));
81
+ },
82
+ focusStep(key: string, step: 1 | -1) {
83
+ const target = nextKey(keys.value, key, step);
84
+ if (!target) return;
85
+ focused.value = target;
86
+ document.querySelector<HTMLElement>(`[data-kpi="${CSS.escape(target)}"]`)?.focus();
87
+ },
88
+ // Gated on `selectable`: resolveInitial falls back to the first tile so a selectable row is never
89
+ // empty, and without this gate that fallback paints the selected treatment on the first tile of
90
+ // every plain row, where selection means nothing.
91
+ isSelected: (key: string) => !!props.selectable && isSelectedIn(selection.value, key),
92
+ isFocusable: (key: string) => (focused.value ?? keys.value[0]) === key,
93
+ get selectable() {
94
+ return !!props.selectable;
95
+ },
96
+ get multiple() {
97
+ return !!props.multiple;
98
+ },
99
+ get controls() {
100
+ return props.controls;
101
+ },
102
+ get format() {
103
+ return props.format;
104
+ },
105
+ get precision() {
106
+ return props.precision;
107
+ },
108
+ get naked() {
109
+ return !!props.divided;
110
+ },
111
+ }) as KpiGroupContext;
112
+
113
+ provide(KPI_GROUP_KEY, context);
114
+
115
+ const gridStyle = computed(() =>
116
+ props.orientation === 'vertical'
117
+ ? { gridTemplateColumns: 'minmax(0, 1fr)' }
118
+ : {
119
+ gridTemplateColumns: `repeat(auto-fit, minmax(${props.min}, 1fr))`,
120
+ ...(props.columns ? { maxWidth: `calc(${props.columns} * 20rem)` } : {}),
121
+ },
122
+ );
123
+ </script>
124
+
125
+ <template>
126
+ <div>
127
+ <!-- Four content-sized rows. Not 1fr on the last one: the group is sized by its content, so 1fr
128
+ resolves to whatever is left over — nothing — and a visual placed in that row overflows onto
129
+ the caption above it. -->
130
+ <div
131
+ :role="selectable && !multiple ? 'tablist' : selectable ? 'group' : undefined"
132
+ :aria-label="label"
133
+ :aria-orientation="selectable && !multiple ? orientation : undefined"
134
+ class="grid grid-rows-[auto_auto_auto_auto] items-stretch"
135
+ :class="[
136
+ divided ? 'gap-px bg-[var(--ui-border)]' : 'gap-4',
137
+ // Ring outset, not inset: `divided` rules are this element's background through 1px gaps,
138
+ // so the tiles sit flush to its border box and an inset stroke lands on them.
139
+ divided && !flush && 'overflow-hidden rounded-lg ring-1 ring-default',
140
+ divided && flush && 'border-b border-default',
141
+ ]"
142
+ :style="gridStyle"
143
+ >
144
+ <!-- The dl is display:contents so the tiles join the grid directly, which also means it is
145
+ the group's ONLY element child — a `divide-x` on the grid would target the dl and never
146
+ reach a tile. The hairlines are the grid's own 1px gaps showing the container's colour
147
+ through, which is the one treatment that stays correct when the row wraps: a `divide-*`
148
+ rule would draw a stray edge on the first tile of every wrapped row.
149
+
150
+ That colour is --ui-border, not bg-accented: these gaps read as the same rules as the
151
+ ring around the row, and accented sits several steps lighter, so the separators inside
152
+ the group came out brighter than the border enclosing them. -->
153
+ <dl class="contents">
154
+ <slot />
155
+ </dl>
156
+ </div>
157
+ <div v-if="announce === 'polite'" class="sr-only" role="status" aria-live="polite">
158
+ <slot name="announcement" />
159
+ </div>
160
+ </div>
161
+ </template>