@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,265 @@
1
+ // A clause is one row of the filter bar. The list is flat and conjunctive: every clause narrows the
2
+ // last, and two clauses on one field stay separate rows.
3
+ //
4
+ // Every function here is pure and takes the clause list in and hands a new one back, so the whole
5
+ // model is testable in node and FFilterBar holds no logic of its own. Ids are passed IN rather than
6
+ // generated, for the same reason — the bar supplies useId().
7
+ import {
8
+ OPERATOR_LABEL,
9
+ defaultOperator,
10
+ type FilterField,
11
+ type FilterSchema,
12
+ type Operator,
13
+ } from './filterSchema';
14
+
15
+ export type RelativeUnit = 'day' | 'week' | 'month';
16
+
17
+ export type ClauseValue =
18
+ | string[]
19
+ | string
20
+ | number
21
+ // A range holds a pair: two numbers on a number field, two ISO strings on a date field. A half
22
+ // filled range keeps a null in the empty half and stays inert until both ends are in.
23
+ | [number | null, number | null]
24
+ | [string | null, string | null]
25
+ | { n: number; unit: RelativeUnit }
26
+ | boolean
27
+ | null;
28
+
29
+ export interface Clause {
30
+ /** Stable across edits — the v-for key, and where focus returns after a value picker closes. */
31
+ id: string;
32
+ /** A key into the schema. */
33
+ field: string;
34
+ operator: Operator;
35
+ value: ClauseValue;
36
+ }
37
+
38
+ /** What an untouched clause of each kind holds. An enum starts empty, a boolean starts true. */
39
+ const emptyValue = (field: FilterField): ClauseValue => {
40
+ switch (field.kind) {
41
+ case 'enum':
42
+ return [];
43
+ case 'text':
44
+ return '';
45
+ case 'boolean':
46
+ return true;
47
+ default:
48
+ return null;
49
+ }
50
+ };
51
+
52
+ /** The set/singular pairs the promotion rule moves between, negation preserved. */
53
+ const PROMOTE: Partial<Record<Operator, Operator>> = { is: 'isAnyOf', isNot: 'isNoneOf' };
54
+ const DEMOTE: Partial<Record<Operator, Operator>> = { isAnyOf: 'is', isNoneOf: 'isNot' };
55
+
56
+ export const addClause = (
57
+ clauses: Clause[],
58
+ field: string,
59
+ schema: FilterSchema,
60
+ id: string,
61
+ ): Clause[] => {
62
+ const declared = schema[field];
63
+ if (!declared) return clauses;
64
+ return [
65
+ ...clauses,
66
+ { id, field, operator: defaultOperator(declared.kind), value: emptyValue(declared) },
67
+ ];
68
+ };
69
+
70
+ const isScalar = (v: unknown): v is string | number => typeof v === 'number' || typeof v === 'string';
71
+
72
+ /** An upper-bound operator inherits the top of a range; everything else inherits the bottom. */
73
+ const prefersUpperEnd = (operator: Operator): boolean =>
74
+ operator === 'isBelow' || operator === 'isBefore';
75
+
76
+ /**
77
+ * The one value a pair or a window can offer when it collapses to a single-value operator. A range
78
+ * hands over the end that reads the same way under the new operator — `10 – 40` narrowed to
79
+ * `is below` means 40, not 10 — and falls back to whichever end is filled.
80
+ */
81
+ const scalarFor = (value: ClauseValue, to: Operator): ClauseValue => {
82
+ if (Array.isArray(value)) {
83
+ const ends = value as unknown[];
84
+ const first = prefersUpperEnd(to) ? ends[1] : ends[0];
85
+ const second = prefersUpperEnd(to) ? ends[0] : ends[1];
86
+ if (isScalar(first)) return first;
87
+ return isScalar(second) ? second : null;
88
+ }
89
+ if (value !== null && typeof value === 'object') return null;
90
+ return value;
91
+ };
92
+
93
+ /**
94
+ * The value SHAPE follows the operator: `is between` holds a pair, `in the last` holds a window, and
95
+ * every other operator holds one value. Changing the operator carries across what already fits rather
96
+ * than stranding a value the new operator cannot read. Operators that share a shape — the enum
97
+ * one/many pairs, a text `contains` becoming an `is` — are left untouched.
98
+ */
99
+ const reshapeValue = (value: ClauseValue, from: Operator, to: Operator): ClauseValue => {
100
+ const wasRange = from === 'isBetween';
101
+ const wasWindow = from === 'inTheLast';
102
+ const isRange = to === 'isBetween';
103
+ const isWindow = to === 'inTheLast';
104
+ if (!wasRange && !wasWindow && !isRange && !isWindow) return value;
105
+
106
+ if (isRange) {
107
+ if (Array.isArray(value) && value.length === 2) return value;
108
+ const first = scalarFor(value, to);
109
+ // An upper-bound value becoming a range keeps its meaning as the range's top end. Both ends are
110
+ // the same kind by construction — a number field's scalar or a date field's ISO string.
111
+ if (isScalar(first)) {
112
+ return (prefersUpperEnd(from) ? [null, first] : [first, null]) as ClauseValue;
113
+ }
114
+ return [null, null];
115
+ }
116
+ if (isWindow) {
117
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) return value;
118
+ return { n: 0, unit: 'day' };
119
+ }
120
+ // Leaving a range or a window for a single-value operator: a window has no scalar to keep.
121
+ return wasWindow ? null : scalarFor(value, to);
122
+ };
123
+
124
+ /**
125
+ * Applies a patch to one clause.
126
+ *
127
+ * Three rules fire here rather than in the component. A value patch that crosses one/many promotes or
128
+ * demotes the operator on an enum field, which is why nobody has to think about `is any of`. An
129
+ * operator patch reshapes the value to the shape that operator reads. A field patch that changes the
130
+ * KIND resets the operator and value, because a charge threshold means nothing as a status — unless
131
+ * the same patch supplies its own operator or value, which is honoured instead.
132
+ */
133
+ export const updateClause = (
134
+ clauses: Clause[],
135
+ id: string,
136
+ patch: Partial<Omit<Clause, 'id'>>,
137
+ schema: FilterSchema,
138
+ ): Clause[] =>
139
+ clauses.map((clause) => {
140
+ if (clause.id !== id) return clause;
141
+
142
+ if (patch.field !== undefined && patch.field !== clause.field) {
143
+ const from = schema[clause.field];
144
+ const to = schema[patch.field];
145
+ if (!to) return clause;
146
+ if (from?.kind === to.kind) {
147
+ const swapped = { ...clause, ...patch };
148
+ // An enum's vocabulary belongs to its field: carrying `FUNCTIONAL` onto Borough would leave a
149
+ // clause that reads complete and matches nothing. Keep only what the new field declares.
150
+ if (to.kind === 'enum' && patch.value === undefined && Array.isArray(swapped.value)) {
151
+ const declared = new Set((to.values ?? []).map((v) => v.value));
152
+ swapped.value = (swapped.value as string[]).filter((v) => declared.has(v));
153
+ }
154
+ return swapped;
155
+ }
156
+ return {
157
+ id: clause.id,
158
+ field: patch.field,
159
+ operator: patch.operator !== undefined ? patch.operator : defaultOperator(to.kind),
160
+ value: patch.value !== undefined ? patch.value : emptyValue(to),
161
+ };
162
+ }
163
+
164
+ const next = { ...clause, ...patch };
165
+
166
+ // An operator the author picked reshapes the value it left behind, unless the same patch says
167
+ // what the value should be.
168
+ if (patch.operator !== undefined && patch.operator !== clause.operator && patch.value === undefined) {
169
+ next.value = reshapeValue(clause.value, clause.operator, patch.operator);
170
+ }
171
+
172
+ if (
173
+ schema[clause.field]?.kind === 'enum' &&
174
+ patch.value !== undefined &&
175
+ patch.operator === undefined &&
176
+ Array.isArray(patch.value)
177
+ ) {
178
+ const many = patch.value.length > 1;
179
+ const moved = many ? PROMOTE[clause.operator] : DEMOTE[clause.operator];
180
+ if (moved) next.operator = moved;
181
+ }
182
+
183
+ return next;
184
+ });
185
+
186
+ export const removeClause = (clauses: Clause[], id: string): Clause[] =>
187
+ clauses.filter((clause) => clause.id !== id);
188
+
189
+ /**
190
+ * A clause with nothing in it is inert, not invalid — the predicate skips it and the chip shows a
191
+ * placeholder. There is no error state anywhere in the bar.
192
+ */
193
+ export const isComplete = (clause: Clause): boolean => {
194
+ const { value } = clause;
195
+ if (value === null || value === undefined) return false;
196
+ // A range needs both ends: two finite numbers on a number field, two dates on a date field. A
197
+ // half-filled range stays inert rather than reaching a predicate that reads the missing end.
198
+ if (clause.operator === 'isBetween') {
199
+ if (!Array.isArray(value) || value.length !== 2) return false;
200
+ const [lo, hi] = value as [unknown, unknown];
201
+ if (typeof lo === 'number' || typeof hi === 'number') {
202
+ return typeof lo === 'number' && Number.isFinite(lo) && typeof hi === 'number' && Number.isFinite(hi);
203
+ }
204
+ return typeof lo === 'string' && lo.trim().length > 0 && typeof hi === 'string' && hi.trim().length > 0;
205
+ }
206
+ if (typeof value === 'boolean') return true;
207
+ if (typeof value === 'number') return Number.isFinite(value);
208
+ if (typeof value === 'string') return value.trim().length > 0;
209
+ if (Array.isArray(value)) return value.length > 0;
210
+ return Number.isFinite(value.n) && value.n > 0;
211
+ };
212
+
213
+ const labelOf = (field: FilterField | undefined, value: string): string =>
214
+ field?.values?.find((v) => v.value === value)?.label ?? value;
215
+
216
+ /** "statuses" from an explicit plural, else a lower-cased label with an s. */
217
+ const pluralOf = (field: FilterField | undefined, label: string): string =>
218
+ field?.plural ?? `${label.toLowerCase()}s`;
219
+
220
+ const withUnit = (n: number, field: FilterField | undefined): string => `${n}${field?.unit ?? ''}`;
221
+
222
+ /** A date clause holds an ISO string; the chip shows it the way a reader writes it. */
223
+ const formatDate = (iso: unknown): string => {
224
+ if (typeof iso !== 'string') return '';
225
+ const at = new Date(iso);
226
+ return Number.isNaN(at.getTime())
227
+ ? iso
228
+ : at.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });
229
+ };
230
+
231
+ /** What the chip's value segment reads. Empty for an incomplete clause. */
232
+ export const summariseClause = (clause: Clause, schema: FilterSchema): string => {
233
+ if (!isComplete(clause)) return '';
234
+ const field = schema[clause.field];
235
+ const { value } = clause;
236
+
237
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
238
+ if (typeof value === 'string') return field?.kind === 'date' ? formatDate(value) : value;
239
+ if (typeof value === 'number') return withUnit(value, field);
240
+
241
+ if (Array.isArray(value)) {
242
+ if (clause.operator === 'isBetween') {
243
+ const [lo, hi] = value as [unknown, unknown];
244
+ if (typeof lo === 'number' && typeof hi === 'number') {
245
+ return `${withUnit(lo, field)} – ${withUnit(hi, field)}`;
246
+ }
247
+ return `${formatDate(lo)} – ${formatDate(hi)}`;
248
+ }
249
+ const values = value as string[];
250
+ // One label, then a count. Two joined labels are wider than any collapsed count, so summarising
251
+ // the pair would swell the chip on the second pick and snap it back on the third.
252
+ if (values.length <= 1) return values.map((v) => labelOf(field, v)).join(', ');
253
+ return `${values.length} ${pluralOf(field, field?.label ?? clause.field)}`;
254
+ }
255
+
256
+ // The last shape is the relative window; the guard narrows value away from null (an incomplete
257
+ // clause already returned '' above), which also keeps the shared vue-tsc gate at zero errors.
258
+ return value ? `${value.n} ${value.unit}${value.n === 1 ? '' : 's'}` : '';
259
+ };
260
+
261
+ export const findClauseByField = (clauses: Clause[], field: string): Clause | undefined =>
262
+ clauses.find((clause) => clause.field === field);
263
+
264
+ /** Re-exported so a consumer rendering an operator menu needs one import, not two. */
265
+ export { OPERATOR_LABEL };
@@ -0,0 +1,134 @@
1
+ // Clauses in, matching rows out.
2
+ //
3
+ // Dates are ISO strings compared as epoch ms. @internationalized/date is a docs dependency, not one
4
+ // of this package's, and Date.parse already does this comparison — the CalendarDate conversion lives
5
+ // in FFilterChip's date segment, at the UI boundary.
6
+ import type { FilterSchema } from './filterSchema';
7
+ import { isComplete, type Clause, type RelativeUnit } from './filterClause';
8
+
9
+ const DAY_MS = 86_400_000;
10
+ const WINDOW_MS: Record<RelativeUnit, number> = {
11
+ day: DAY_MS,
12
+ week: 7 * DAY_MS,
13
+ month: 30 * DAY_MS,
14
+ };
15
+
16
+ const text = (v: unknown): string => (v == null ? '' : String(v)).toLowerCase();
17
+
18
+ /** NaN for anything unparseable, which then fails every comparison below. */
19
+ const ms = (v: unknown): number => (typeof v === 'string' || v instanceof Date ? Date.parse(String(v)) : Number.NaN);
20
+
21
+ // A date bound names a whole DAY — that is what the calendar picks — so the comparisons run against
22
+ // the day's edges rather than the instant stored. Without this, "before the 20th" and "between the
23
+ // 1st and the 20th" would each drop the rows recorded during the 20th.
24
+ const dayStart = (v: unknown): number => {
25
+ const at = ms(v);
26
+ if (!Number.isFinite(at)) return at;
27
+ const on = new Date(at);
28
+ return Date.UTC(on.getUTCFullYear(), on.getUTCMonth(), on.getUTCDate());
29
+ };
30
+
31
+ const nextDayStart = (v: unknown): number => {
32
+ const at = dayStart(v);
33
+ return Number.isFinite(at) ? at + DAY_MS : at;
34
+ };
35
+
36
+ /**
37
+ * Does one row satisfy one clause? `now` is a parameter rather than a Date.now() call so the date
38
+ * operators are deterministic under test.
39
+ */
40
+ export const matchesClause = (
41
+ row: Record<string, unknown>,
42
+ clause: Clause,
43
+ now: number = Date.now(),
44
+ ): boolean => {
45
+ const cell = row[clause.field];
46
+ const { value } = clause;
47
+
48
+ switch (clause.operator) {
49
+ case 'is':
50
+ if (Array.isArray(value)) return (value as string[]).includes(String(cell));
51
+ if (typeof value === 'boolean') return Boolean(cell) === value;
52
+ if (typeof value === 'number') return Number(cell) === value;
53
+ return text(cell) === text(value);
54
+
55
+ case 'isNot':
56
+ if (Array.isArray(value)) return !(value as string[]).includes(String(cell));
57
+ if (typeof value === 'number') return Number(cell) !== value;
58
+ return text(cell) !== text(value);
59
+
60
+ case 'isAnyOf':
61
+ return Array.isArray(value) && (value as string[]).includes(String(cell));
62
+
63
+ case 'isNoneOf':
64
+ return Array.isArray(value) && !(value as string[]).includes(String(cell));
65
+
66
+ case 'contains':
67
+ return text(cell).includes(text(value));
68
+
69
+ case 'doesNotContain':
70
+ return !text(cell).includes(text(value));
71
+
72
+ case 'isAbove':
73
+ return Number(cell) > Number(value);
74
+
75
+ case 'isBelow':
76
+ return Number(cell) < Number(value);
77
+
78
+ case 'isBetween': {
79
+ if (!Array.isArray(value)) return false;
80
+ const [lo, hi] = value as [unknown, unknown];
81
+ // One operator, two kinds: a numeric range on a number field, a date range on a date field.
82
+ if (typeof lo === 'number') return Number(cell) >= lo && Number(cell) <= Number(hi);
83
+ const at = ms(cell);
84
+ // Both ends are inclusive of the day they name, so a row timestamped during the last day
85
+ // still falls inside the range the chip reads out.
86
+ return at >= dayStart(lo) && at < nextDayStart(hi);
87
+ }
88
+
89
+ case 'isBefore':
90
+ return ms(cell) < dayStart(value);
91
+
92
+ case 'isAfter':
93
+ return ms(cell) >= nextDayStart(value);
94
+
95
+ case 'inTheLast': {
96
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
97
+ const { n, unit } = value as { n: number; unit: RelativeUnit };
98
+ const window = WINDOW_MS[unit];
99
+ if (!Number.isFinite(n) || window === undefined) return false;
100
+ const at = ms(cell);
101
+ return at >= now - n * window && at <= now;
102
+ }
103
+
104
+ default:
105
+ return true;
106
+ }
107
+ };
108
+
109
+ /**
110
+ * Every complete clause narrows the set; incomplete ones and clauses naming an undeclared field are
111
+ * skipped, so a half-built filter shows the unfiltered list rather than an empty one.
112
+ */
113
+ export const applyClauses = <T extends Record<string, unknown>>(
114
+ rows: readonly T[],
115
+ clauses: readonly Clause[],
116
+ schema: FilterSchema,
117
+ ): T[] => {
118
+ const live = clauses.filter((clause) => schema[clause.field] && isComplete(clause));
119
+ if (!live.length) return [...rows];
120
+ const now = Date.now();
121
+ return rows.filter((row) => live.every((clause) => matchesClause(row, clause, now)));
122
+ };
123
+
124
+ /**
125
+ * The `{ id, value }` shape TanStack's ColumnFiltersState uses, built without importing TanStack —
126
+ * this package must not depend on it. Pair with a column `filterFn` that hands the row and the
127
+ * rebuilt clause back to matchesClause.
128
+ */
129
+ export const toColumnFilters = (
130
+ clauses: readonly Clause[],
131
+ ): { id: string; value: unknown }[] =>
132
+ clauses
133
+ .filter(isComplete)
134
+ .map((clause) => ({ id: clause.field, value: { operator: clause.operator, value: clause.value } }));
@@ -0,0 +1,82 @@
1
+ // What a surface declares as filterable. A field's KIND decides its operators, so adding a
2
+ // filterable field costs one schema line and no operator wiring.
3
+
4
+ /** The five shapes a filterable value comes in. */
5
+ export type FieldKind = 'enum' | 'text' | 'number' | 'date' | 'boolean';
6
+
7
+ export type Operator =
8
+ | 'is'
9
+ | 'isNot'
10
+ | 'isAnyOf'
11
+ | 'isNoneOf'
12
+ | 'contains'
13
+ | 'doesNotContain'
14
+ | 'isAbove'
15
+ | 'isBelow'
16
+ | 'isBetween'
17
+ | 'isBefore'
18
+ | 'isAfter'
19
+ | 'inTheLast';
20
+
21
+ /** One selectable value of an enum field. `label` is what the picker and the chip show. */
22
+ export interface FilterValue {
23
+ value: string;
24
+ label: string;
25
+ icon?: string;
26
+ avatar?: { src?: string; alt?: string };
27
+ }
28
+
29
+ export interface FilterField {
30
+ kind: FieldKind;
31
+ /** Singular, as it reads in the chip's first segment: "Status". */
32
+ label: string;
33
+ /** Used when the chip summarises several values: "3 statuses". Falls back to a lower-cased label. */
34
+ plural?: string;
35
+ icon?: string;
36
+ /** Suffixed to a number in a summary: "above 20%". */
37
+ unit?: string;
38
+ /** Required for `enum`; the picker's value list. */
39
+ values?: readonly FilterValue[];
40
+ }
41
+
42
+ export type FilterSchema = Record<string, FilterField>;
43
+
44
+ /**
45
+ * The first entry of each list is that kind's default operator, so a freshly added clause reads the
46
+ * way it would in Linear: an enum starts at `is`, and picking a second value promotes it to `is any of`.
47
+ */
48
+ export const OPERATORS_BY_KIND: Record<FieldKind, readonly Operator[]> = {
49
+ enum: ['is', 'isNot', 'isAnyOf', 'isNoneOf'],
50
+ text: ['contains', 'doesNotContain', 'is', 'isNot'],
51
+ number: ['is', 'isNot', 'isAbove', 'isBelow', 'isBetween'],
52
+ date: ['isBefore', 'isAfter', 'isBetween', 'inTheLast'],
53
+ boolean: ['is'],
54
+ } as const;
55
+
56
+ /** Lower case: the label reads inline between the field and the value, not as a heading. */
57
+ export const OPERATOR_LABEL: Record<Operator, string> = {
58
+ is: 'is',
59
+ isNot: 'is not',
60
+ isAnyOf: 'is any of',
61
+ isNoneOf: 'is none of',
62
+ contains: 'contains',
63
+ doesNotContain: 'does not contain',
64
+ isAbove: 'is above',
65
+ isBelow: 'is below',
66
+ isBetween: 'is between',
67
+ isBefore: 'is before',
68
+ isAfter: 'is after',
69
+ inTheLast: 'in the last',
70
+ };
71
+
72
+ export const defaultOperator = (kind: FieldKind): Operator => OPERATORS_BY_KIND[kind][0];
73
+
74
+ export const operatorsFor = (field: FilterField): readonly Operator[] => OPERATORS_BY_KIND[field.kind];
75
+
76
+ /**
77
+ * Passing a row type as the explicit type argument constrains the schema's keys to that row's keys.
78
+ * Calling it without an argument accepts any keys. Identity at runtime.
79
+ */
80
+ export const defineFilterSchema = <T = Record<string, FilterField>>(
81
+ schema: Partial<Record<keyof T & string, FilterField>>,
82
+ ): FilterSchema => schema as FilterSchema;
@@ -0,0 +1,36 @@
1
+ // Forest's filter bar — one entry, matching ./map.
2
+ //
3
+ // <FFilterBar> the Linear-style bar: a row of clause chips plus an add-filter control
4
+ // <FFilterTrigger> just the add-filter control, for a bar that holds nothing else
5
+ // <FFilterClauses> just the applied chips, for the bar below it
6
+ // <FFilterChip> one clause, rendered as a UFieldGroup (field / operator / value)
7
+ // defineFilterSchema() declares the filterable fields and their kinds
8
+ // applyClauses() evaluates a clause list against rows
9
+ // OPERATORS_BY_KIND which operators a field kind permits — the operator/value-shape pairing
10
+ export { default as FFilterBar } from './FFilterBar.vue';
11
+ export { default as FFilterTrigger } from './FFilterTrigger.vue';
12
+ export { default as FFilterClauses } from './FFilterClauses.vue';
13
+ export { default as FFilterChip } from './FFilterChip.vue';
14
+
15
+ export type { FilterSlot } from './filter.theme';
16
+
17
+ export {
18
+ OPERATORS_BY_KIND,
19
+ OPERATOR_LABEL,
20
+ defineFilterSchema,
21
+ } from './filterSchema';
22
+ export type {
23
+ FieldKind,
24
+ Operator,
25
+ FilterValue,
26
+ FilterField,
27
+ FilterSchema,
28
+ } from './filterSchema';
29
+
30
+ export {
31
+ addClause,
32
+ isComplete,
33
+ } from './filterClause';
34
+ export type { RelativeUnit, ClauseValue, Clause } from './filterClause';
35
+
36
+ export { applyClauses, toColumnFilters } from './filterPredicate';
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Forest preset for Nuxt UI (Vue).
3
+ *
4
+ * Pass to the `ui()` Vite plugin: `ui(forestPreset)`. Maps Forest semantic colours onto Nuxt UI
5
+ * roles and spreads in the per-component theme fragments (./themes). Everything is driven by the
6
+ * per-family OKLCH ramp vars (--<family>-50..950, light + `.dark`), so it auto-themes. The
7
+ * signature <UButton> emphasis ladder lives in themes/button.ts + themes/button.mechanics.ts.
8
+ */
9
+
10
+ // Per-component Forest theme fragments — one file each in ./themes, aggregated (and aliased to the
11
+ // Nuxt UI ui-config keys) by ./themes/index.ts. Spread into `ui` below so every override is live.
12
+ import * as componentThemes from './themes';
13
+
14
+ export const forestPreset = {
15
+ ui: {
16
+ // All per-component fragments (button with its keycap mechanics, card with its nesting marker,
17
+ // input with its ported FInput depth line, popover, alert, table, …) — one file per component
18
+ // in ./themes; this spread is the single source of component themes (asserted by the static
19
+ // tests). NOTE: object spread does NOT merge — a component key declared after this spread
20
+ // would replace its fragment wholesale, which is why it comes first and the preset declares
21
+ // only non-component config (icons, colors) beside it.
22
+ ...componentThemes,
23
+
24
+ // Icon aliases (appConfig.ui.icons). `star` is read ONLY by UInputRating (verified in the 4.10
25
+ // source), so pointing it at a SOLID glyph is safe and system-wide: a filled rating star reads as
26
+ // a solid shape, not a coloured outline of the lucide default. It uses Forest's OWN vendored star
27
+ // (`i-forest-star-fill`, packages/ui/icons/forest) so the app stays on lucide + forest only. The
28
+ // empty state falls back to this same icon rendered muted (`emptyIcon ?? starIcon`).
29
+ icons: {
30
+ star: 'i-forest-star-fill',
31
+ },
32
+ // Nuxt UI semantic roles (drive --ui-* for non-button components: badge, alert, …). Buttons
33
+ // override their own colours via the per-family --btn-* vars in themes/button.mechanics.ts,
34
+ // independent of this map.
35
+ // Alias contract (engine NUXT_UI_ALIASES): primary/success→forest, error→maple, info→river,
36
+ // warning→amber, secondary→warm, neutral→neutral. Every family is a Tailwind colour registered by
37
+ // the engine @theme (packages/tokens/scales/theme.css), re-pointed to the P3 OKLCH ramps at :root —
38
+ // so all seven roles, surfaces and text included, are the Forest engine, not stock Tailwind ramps.
39
+ colors: {
40
+ primary: 'forest',
41
+ secondary: 'warm',
42
+ success: 'forest',
43
+ info: 'river',
44
+ warning: 'amber',
45
+ error: 'maple',
46
+ neutral: 'neutral', // Forest neutral ramp — drives every surface + muted/dimmed text and borders
47
+ },
48
+ },
49
+
50
+ // Register the scale families as Nuxt UI colours (color="forest|maple|river|warm|acid").
51
+ // Production wiring: docs.css bridges their --ui-color-* steps to the engine ramps — the charts
52
+ // palette and any color="<family>" usage depend on this list. `primary` is auto-prepended by the
53
+ // plugin; the other default roles must be relisted here or they'd be dropped.
54
+ theme: {
55
+ // Mutable assertion: NuxtUIOptions wants Color[], and the outer `as const` would freeze this
56
+ // to a readonly tuple.
57
+ colors: ['secondary', 'success', 'info', 'warning', 'error', 'forest', 'maple', 'river', 'warm', 'acid'] as string[],
58
+ },
59
+ } as const;
60
+
61
+ /**
62
+ * Marker classes the CORE preset stamps for its own structural CSS (styles/forest.css consumers).
63
+ * These are context-free — no manifest claims them; the census allowlists them and still requires
64
+ * a live CSS consumer for each.
65
+ */
66
+ export const PRESET_STRUCTURAL_MARKERS = ['forest-card'] as const;
67
+
68
+ export default forestPreset;