@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,402 @@
1
+ /**
2
+ * Forest MOBILE platform override for Nuxt UI.
3
+ *
4
+ * `forestPreset` is the canonical (web) theme; this is the DELTA that turns it into the bold mobile
5
+ * cut. Feed it to a root `<UTheme :ui="forestMobileTheme">` (or app.config) in a forest-mobile app —
6
+ * the same shape the showcase uses for its mobile lens. It carries the button deltas plus the
7
+ * rounder card/input cut; grow it as the mobile expression fills in.
8
+ *
9
+ * NOTE: the `ui` prop is slot-level (not per-variant), so `button.base` applies to ALL button variants
10
+ * in the wrapped subtree — mobile reads rounder across the board (on-brand); the deeper-lip vars
11
+ * (--btn-y…) only do anything on the keycap variant, which is the one that translates.
12
+ *
13
+ * Button cut: full pill, 2px edge (--btn-edge), deeper anchored lip (--btn-y*), Mohr-BlackIt display
14
+ * label (always italic + uppercase). NO hover states: touch has no hover (Tailwind v4 already gates
15
+ * `hover:` behind `@media (hover:hover)` on real devices) — and the lens preview on desktop must match,
16
+ * so the hover vars are pinned to their rest values: hover lift == rest lift (--btn-y-h/--btn-yhi) and
17
+ * hover face == rest face (--btn-hover; declared plain AND `dark:` — the PAD trap — because the colour
18
+ * PALs set `dark:[--btn-hover:…]`, which outranks a plain override in dark mode). Press (:active)
19
+ * feedback is kept — touch fires it. Focus is a POP, not a ring: with no hover to lean on, a focused
20
+ * keycap lifts past its rest height (-7px vs the -4px rest lip) and the inherited ::before focus ring is
21
+ * switched off (outline-0!) so the raise is the sole indicator. Mobile-only — the web preset's focus
22
+ * stays ring-on-::before with no lift. Mohr's caps are centred in their own box by the @font-face metric overrides in
23
+ * packages/fonts/forest-fonts.css, so the label needs no box-level treatment and button text may be passed
24
+ * either as the `label` prop or as default-slot children.
25
+ */
26
+ import type { ContextManifest } from './contexts';
27
+
28
+ /**
29
+ * The mobile field edge, shared by every control that can join a UFieldGroup — a mismatched ring
30
+ * along a joined seam is what this constant exists to prevent. `ring-2` matches the button's
31
+ * --btn-edge on this cut. The depth line is drawn from the top edge inward and the ring paints over
32
+ * its first pixels, so --forest-field-edge declares how much the ring exceeds the 1px the recipes
33
+ * assume; the field recipes add it to --forest-input-depth, which keeps the recess reading the same
34
+ * at every size rather than flattening the ramp. `cut-fixed` selects the baked touch rungs.
35
+ *
36
+ * `[&.ring]` scopes the edge to the ringed variants, the same guard the badge cut carries below. A
37
+ * borderless field — `variant="none"`, which is what a command palette renders its search box as —
38
+ * has no `ring` class to thicken, so an unscoped edge painted a 2px box around it and fed
39
+ * --forest-field-edge into the depth-line shadow, bending the seam under the search row around the
40
+ * field's own corners. `cut-fixed` stays unscoped: the touch ladder applies to every field.
41
+ */
42
+ const FIELD_EDGE = 'cut-fixed [&.ring]:ring-2 [&.ring]:[--forest-field-edge:1px]';
43
+
44
+ export const forestMobileTheme = {
45
+ button: {
46
+ // Focus is a POP, not a ring: a focused keycap/pad lifts past its rest lip (--btn-y-focus = -7px) and
47
+ // its ::before ring is switched off (--btn-ring-w = 0). All three are plain VARS the keycap recipe
48
+ // reads (PAD too) — NOT `before:`/`translate` utilities. That matters: the slot-level base hits every
49
+ // variant, and a `before:` utility here would auto-add `content:''` to the flat variants' ::before
50
+ // (which isn't absolute), turning it into a phantom leading flex item that `gap-1.5` shoves the label
51
+ // past. Vars touch only the variants that read them, so the flat variants stay put and gap-free.
52
+ // `cut-fixed` is the BAKED trigger for the shared touch ladder (styles/forest.css): the `touch:`
53
+ // rules in button.ts/input.ts fire either on a coarse pointer or on this marker, landing on the
54
+ // same rungs — 28/36/40/44/52 vs the fine-pointer 24/28/32/36/40. Baked, so the desktop mock and
55
+ // the native/Flutter export show true touch size with no pointer to query; and because both
56
+ // triggers name the same numbers, a real phone can't step twice. (Controls with no baked ladder of
57
+ // their own — badge, switch, checkbox, … — instead reach touch size via a bigger default `size`,
58
+ // so for THOSE this marker still means "opt out of the one-step `coarse:` bump".)
59
+ base: 'cut-fixed rounded-full [--btn-edge:2px] [--btn-y:-4px] [--btn-yi:4px] [--btn-y-h:-4px] [--btn-yhi:4px] [--btn-y-focus:-7px] [--btn-yi-focus:7px] [--btn-ring-w:0px] [--btn-hover:var(--btn-face)] dark:[--btn-hover:var(--btn-face)] [font-family:Mohr-BlackIt] italic font-black uppercase',
60
+ // Icon slots opt out of the coarse bump too — the preset hangs `coarse:size-*` on each icon slot
61
+ // (sm/lg), and the variant's `:not(.cut-fixed)` tests the element the utility sits on, so base
62
+ // alone would hold while the icon still grew a step. Same fix as the badge below.
63
+ leadingIcon: 'cut-fixed',
64
+ trailingIcon: 'cut-fixed',
65
+ },
66
+ // Card: one radius step up (2xl/32 over web's xl/24); --forest-card-radius follows so the nested
67
+ // concentric calc stays true (the 24→8 step becomes 32→16→8). Shape only — the mobile app keeps the
68
+ // readable normal title; the Mohr uppercase headline is marketing's voice, not the app's. Same
69
+ // radius as marketing (marketing adds the title on top).
70
+ card: {
71
+ // Roomier p-5 (20px) to carry the bigger title; --forest-card-pad follows so nested corners stay
72
+ // concentric (32→12→8). Radius one step up + the concentric var.
73
+ // The --fx-metric traits ride the ROOT, not a slot: a KPI figure sits in the card's body, so the
74
+ // var has to be declared above it. Mohr-BlackIt matches the title's cut.
75
+ root: 'rounded-2xl [--forest-card-radius:2rem] [--forest-card-pad:1.25rem] [--fx-metric-family:var(--font-display-italic)] [--fx-metric-transform:uppercase]',
76
+ body: 'sm:p-5',
77
+ header: 'sm:p-5',
78
+ footer: 'sm:p-5',
79
+ // Title: Mohr ITALIC (the Mohr-BlackIt cut, mobile's signature — the button uses the same face),
80
+ // uppercase (Mohr is ALWAYS uppercase), one size up. Family + transform via the --fx-title traits
81
+ // the preset's title slot reads; italic + weight as plain classes. Marketing uses the same face
82
+ // upright instead.
83
+ title: '[--fx-title-family:Mohr-BlackIt] [--fx-title-transform:uppercase] italic font-black text-xl',
84
+ // Description one size up for comfortable thumb reading; family stays GT Haptik (--font-sans).
85
+ description: 'text-base',
86
+ },
87
+ // Input: BAKES one size up (default md → lg, via forestMobileProps below) — the proportional touch
88
+ // step, same shape as button/badge. The native/Flutter surface is always touch but is prototyped on
89
+ // a fine-pointer desktop where a `coarse` media query never fires, so the step is baked via the size
90
+ // prop rather than the preset's adaptive `coarse:` promotion. `cut-fixed` opts this cut out of that
91
+ // promotion so a real coarse phone doesn't step AGAIN (lg→xl); it sits on base + the icon slots
92
+ // because those are the slots the preset bumps. Radius mirrors web — input.ts owns the ladder.
93
+ input: { base: FIELD_EDGE, leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
94
+ // Textarea: same touch step as input (a multiline input shares its identity). Bakes size lg via
95
+ // forestMobileProps + cut-fixed on the ladder slots, which selects the touch rungs.
96
+ textarea: { base: FIELD_EDGE, leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
97
+ // Field group: the overlap must equal the segment edge, or a joined seam paints both edges side by
98
+ // side. Stock's `-space-x-px` collapses two 1px rings to 1px; on this cut every segment carries a
99
+ // 2px edge, so the overlap steps to 2px (`-space-x-0.5`) to collapse them to one 2px line.
100
+ fieldGroup: { base: '-space-x-0.5' },
101
+ // Drawer: peach surface, matching mobile's default card variant — the same warm tint (light) /
102
+ // warm-950 (dark) fill and the aligned warm hairline ring, so a sheet and the cards inside it read
103
+ // as one family. Handle and edge share one solid mark colour (--forest-card-peach-mark: Bark Brown
104
+ // light, Acer Orange dark) instead of the stock neutral handle, which sat off-hue on peach. Radius stays with the preset (compoundVariants + the
105
+ // --forest-drawer-radius var re-pointed on the context marker class in styles/forest.css).
106
+ // TRAP: a local `:ui="{ content: '…' }"` on a UDrawer REPLACES this string (useComponentProps defus
107
+ // raw over theme, and defu overwrites strings rather than concatenating) — the surface silently
108
+ // reverts to bg-default. Pass one-off content classes via the `class` prop instead (it lands on
109
+ // content whenever the drawer has no trigger slot), which merges alongside the theme value.
110
+ drawer: {
111
+ // ring-2 over stock's 1px hairline, on --forest-card-peach-mark (Bark Brown light / Acer Orange
112
+ // dark) at /70 — matching what the handle actually paints, since vaul renders it at opacity .7
113
+ // at rest. A sheet floats over the page and needs an edge that holds its shape; the translucent
114
+ // peach CARD hairline is for something that sits IN the page.
115
+ content: 'bg-[var(--forest-card-peach-bg)] ring-2 ring-[var(--forest-card-peach-mark)]/70',
116
+ // Declared plain AND `dark:` — the PAD trap: the preset's handle sets `dark:bg-[var(--forest-700)]!`,
117
+ // which outranks a plain override in dark mode. The token is already mode-aware, so both point at it.
118
+ handle: 'bg-[var(--forest-card-peach-mark)]! dark:bg-[var(--forest-card-peach-mark)]!',
119
+ // Title in mobile's signature face — Mohr-BlackIt (the italic cut, licensed for this context
120
+ // only; web/marketing must not reach for it), ALWAYS uppercase, and stepped up from the preset's
121
+ // text-base to text-xl so a sheet header carries at arm's length. Same move as the mobile card
122
+ // title. Reaches the `title`-PROP path only; a free-form #header slot styles itself.
123
+ title: '[font-family:Mohr-BlackIt] italic font-black uppercase text-xl',
124
+ },
125
+ // Kbd: rounder corner (rounded-sm over web's rounded-xs); sm keeps rounded-xs (`[&.h-4]:`, stock
126
+ // sm height, (0,2,0) beats base) — at 16px tall rounded-sm reads as a pill. Padding is not touched
127
+ // here: the preset already runs px-2 (sm px-1), so this cut inherits it. Marketing matches.
128
+ kbd: { base: 'rounded-sm [&.h-4]:rounded-xs' },
129
+ // Alert: rounder banner (rounded-xl over web's rounded-lg) — the bold voice softens the corner a
130
+ // step further. Title weight comes from the preset, inherited. Marketing matches.
131
+ alert: { root: 'rounded-xl' },
132
+ // Calendar: rounder card frame (rounded-2xl over web's rounded-xl) — the bold voice softens the
133
+ // grid's containing card a step further, in step with the card cut. Shape only; the grid, date
134
+ // cells, heading weight and focus ring are all inherited from the preset.
135
+ calendar: { root: 'rounded-2xl' },
136
+ // Listbox: rounder shell (rounded-xl over the stock rounded-lg the standard cut leaves alone) —
137
+ // the bold voice takes the list from a field to a card. The corner has to be paid for: at 24px the
138
+ // arc cuts into the first and last rows, so the row inset steps 4px→12px and the rows sit inside
139
+ // the curve instead of under it.
140
+ //
141
+ // That inset lands on TWO slots because stock puts the padding in different places: a grouped list
142
+ // pads each `group` (and keeps the between-group rule full bleed on `content`), while a virtualised
143
+ // list renders rows straight into `content` and pads that instead. `[&.isolate]` scopes the content
144
+ // half to the virtualised cut — stock's own marker for it — so a grouped list is not double-padded.
145
+ // Coupled to that literal class; if it is ever renamed a virtualised list silently loses the inset.
146
+ // One flat value across xs→xl — a context delta cannot carry size variants. Marketing matches.
147
+ listbox: { root: 'rounded-xl', group: 'p-3', content: '[&.isolate]:p-3' },
148
+ // Badge: full pill (rounded-full over web's rounded-sm) — the bold voice rounds the chip all the
149
+ // way. Weight/case/tracking come from the preset, inherited here. The UTheme base merges after the
150
+ // preset's size variants, so a plain `rounded-full` wins the border-radius bucket at every size —
151
+ // no per-size override needed on this side. Marketing matches ON RADIUS.
152
+ //
153
+ // Touch increase, BAKED not adaptive: the default size steps up md→lg (forestMobileProps), and
154
+ // `cut-fixed` opts this cut out of the preset's `coarse:` bump (styles/forest.css) so it holds the
155
+ // baked size on a real coarse-pointer phone instead of promoting AGAIN. Same shape as the mobile
156
+ // button (props size + cut-fixed). Marketing does NOT do this — it has no cut-fixed, so it adapts
157
+ // via the preset coarse bump like standard.
158
+ // `cut-fixed` also on the icon slots, not just base: the preset hangs its `coarse:` bump on
159
+ // EACH slot (base carries text/padding/radius, leadingIcon/trailingIcon carry `coarse:size-*`),
160
+ // and the variant's `:not(.cut-fixed)` tests the element the utility sits on. Base alone would
161
+ // hold while the icon still promoted a step on a real coarse-pointer surface — so the opt-out has
162
+ // to land on every slot the preset bumps. Same shape needed on the button below.
163
+ // `[&.ring]` scopes the 2px edge to the ringed variants (outline/subtle) — a bare `ring-2` would
164
+ // paint a ring on solid/soft, which carry no ring classes at all. Coupled to stock's literal `ring`
165
+ // class from its colour×variant compounds; if that token is ever renamed the edge silently reverts.
166
+ badge: { base: 'rounded-full cut-fixed [&.ring]:ring-2', leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
167
+ // Tabs: bakes size lg (forestMobileProps) + cut-fixed on the slots the preset's coarse: ladder
168
+ // bumps (trigger + leadingIcon), so a real coarse phone holds lg instead of stepping to xl. Same
169
+ // shape as the input/badge mobile cut. Radius follows the size via the preset's pill ladder.
170
+ tabs: { trigger: 'cut-fixed', leadingIcon: 'cut-fixed' },
171
+ // Pin input: bakes size lg (forestMobileProps) + cut-fixed on the cell, which selects the touch rungs
172
+ // with no pointer to query. Same shape as the input mobile cut (a pin input is a split input).
173
+ pinInput: { base: FIELD_EDGE },
174
+ // Switch: bakes size lg + cut-fixed on every slot the preset's coarse: ladder bumps (track width,
175
+ // height, thumb, label) so a real phone holds lg instead of stepping to xl. The whole switch is the
176
+ // tap target, so the bigger size is a real touch win.
177
+ switch: { base: 'cut-fixed', container: 'cut-fixed', thumb: 'cut-fixed', wrapper: 'cut-fixed' },
178
+ // Checkbox / radio group: bake size lg + cut-fixed on the slots their coarse: ladders bump, so a
179
+ // real phone holds lg (taller tap rows) instead of stepping to xl.
180
+ checkbox: { base: 'cut-fixed', container: 'cut-fixed', wrapper: 'cut-fixed' },
181
+ radioGroup: { base: 'cut-fixed', item: 'cut-fixed', legend: 'cut-fixed', container: 'cut-fixed', fieldset: 'cut-fixed', indicator: 'cut-fixed' },
182
+ // Slider: bakes size lg + cut-fixed on the thumb — the only slot the preset's coarse: ladder bumps
183
+ // — so a real phone holds lg's bigger handle instead of stepping to xl.
184
+ slider: { thumb: 'cut-fixed' },
185
+ // Input date / time: field controls — same mobile cut as input. Bake size lg + cut-fixed on the
186
+ // ladder slots (base + the two icons), which selects the touch rungs on a fine-pointer mock.
187
+ inputDate: { base: FIELD_EDGE, leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
188
+ inputTime: { base: FIELD_EDGE, leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
189
+ // Input number: field control — same mobile cut as input. Bake size lg + cut-fixed on the base (its
190
+ // only ladder slot), which selects the touch rungs on a fine-pointer mock.
191
+ inputNumber: { base: FIELD_EDGE },
192
+ // Select: field control (trigger) — same mobile cut as input. Bake size lg + cut-fixed on the base
193
+ // and trigger icons (its ladder slots), which selects the touch rungs on a fine-pointer mock.
194
+ select: { base: FIELD_EDGE, leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
195
+ // Select menu / input menu: field controls (trigger + panel) — same mobile cut as input/select.
196
+ // Bake size lg + cut-fixed on base + trigger icons, selecting the touch rungs.
197
+ selectMenu: { base: FIELD_EDGE, leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
198
+ inputMenu: { base: FIELD_EDGE, leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
199
+ // Input tags: field control (chips + input) — same mobile cut. Bake size lg + cut-fixed on base +
200
+ // icons, selecting the touch rungs.
201
+ inputTags: { base: FIELD_EDGE, leadingIcon: 'cut-fixed', trailingIcon: 'cut-fixed' },
202
+ } as const;
203
+
204
+ /**
205
+ * Default PROPS for the mobile subtree — UTheme's `ui` only carries slot classes (a
206
+ * `defaultVariants` key in there is silently ignored), so per-component prop defaults ride the
207
+ * separate `props` channel: `<UTheme :ui="forestMobileTheme" :props="forestMobileProps">`.
208
+ * Mobile default emphasis: framed (web defaults to solid in the preset). Default size lg — which on
209
+ * the mobile cut renders at 44px (the `cut-fixed:` baked ladder in button.ts/input.ts: 28/36/40/44/52
210
+ * for xs→xl, vs web's 24/28/32/36/40), hitting Apple's 44pt min tap target; explicit `size` still wins
211
+ * and steps the same ladder (xl → 52). Web/marketing keep the shorter shared ladder.
212
+ */
213
+ export const forestMobileProps = {
214
+ button: { variant: 'framed', size: 'lg' },
215
+ // Cards default to the peach accent on mobile — warm tint + aligned warm border, matching the
216
+ // drawer surface. Explicit variant props still win.
217
+ card: { variant: 'peach' },
218
+ // Badge default steps up md→lg on mobile — the baked touch increase (paired with `cut-fixed` in
219
+ // the theme so the preset's coarse: bump doesn't stack on top). Explicit size props still win.
220
+ badge: { size: 'lg' },
221
+ // Chip defaults to INSET on mobile. Stock overflows the host boundary, which is right for a square
222
+ // icon button but wrong here: the mobile button cut is a full pill and the app's avatars are round,
223
+ // so an overflowing dot floats off a curved edge with a visible gap. Inset holds it at the inner
224
+ // edge the host's own shape defines. No theme entry needed — stock owns the geometry (the overflow
225
+ // translate lives in compoundVariants keyed on `inset: false`, so simply not matching it IS the
226
+ // inset render), which makes this a prop default and nothing else. Explicit `:inset="false"` wins.
227
+ chip: { inset: true },
228
+ // Input default steps up md→lg — the baked touch step, same shape as button/badge. `cut-fixed` in the
229
+ // theme selects the touch rungs, so the desktop mock shows the same size a real phone does. Explicit
230
+ // size props still win.
231
+ input: { size: 'lg' },
232
+ // Field group steps up md→lg like every other field control. It matters MORE here than elsewhere:
233
+ // UFieldGroup PROVIDES its size to its children (`useFieldGroup`: a child resolves
234
+ // `props.size ?? fieldGroup.size`), so without a default here a mobile group fell back to md and
235
+ // dragged every joined child down to md with it — overriding their own lg mobile defaults. Setting
236
+ // it on the group is what makes a joined bar read at mobile size. An explicit `size` on the group
237
+ // (as the docs demos use) still wins.
238
+ fieldGroup: { size: 'lg' },
239
+ // Textarea mirrors input — baked one step up (md→lg) for comfortable thumb input.
240
+ textarea: { size: 'lg' },
241
+ // Tabs step up md→lg for touch-sized tap targets — the baked touch increase paired with cut-fixed
242
+ // in the theme so the preset's coarse: ladder doesn't step it again on a real phone.
243
+ tabs: { size: 'lg' },
244
+ // Pin input steps up md→lg for thumb-sized OTP cells — same baked touch increase as input.
245
+ pinInput: { size: 'lg' },
246
+ // Switch steps up md→lg — the whole control is the tap target, so bigger reads as easier to hit.
247
+ switch: { size: 'lg' },
248
+ // Checkbox / radio group step up md→lg — a taller tap row for touch.
249
+ checkbox: { size: 'lg' },
250
+ radioGroup: { size: 'lg' },
251
+ // Slider steps up md→lg — a bigger thumb to grab on touch.
252
+ slider: { size: 'lg' },
253
+ // Date / time fields step up md→lg — same touch step as input.
254
+ inputDate: { size: 'lg' },
255
+ inputTime: { size: 'lg' },
256
+ inputNumber: { size: 'lg' },
257
+ select: { size: 'lg' },
258
+ selectMenu: { size: 'lg' },
259
+ inputMenu: { size: 'lg' },
260
+ inputTags: { size: 'lg' },
261
+ } as const;
262
+
263
+ /**
264
+ * Manifest — what the mobile context touches (CONTEXTS-SPEC.md §4). `moves` is the hand-written
265
+ * record; `channels` is derived from the exports above and test-asserted.
266
+ */
267
+ export const forestMobileManifest: ContextManifest = {
268
+ name: 'mobile',
269
+ binding: 'utheme',
270
+ surfaces: ['ride app (native/Flutter export; prototyped on desktop web)'],
271
+ darkPolicy: 'follows OS',
272
+ // The ride app is a touch surface — inherently coarse. Docs force the coarse simulation on and lock
273
+ // the pointer toggle here (see ContextScope / ContextSwitch); a "simulate touch" control on a
274
+ // context that IS touch is a no-op. Ladder components (button/input/…) carry cut-fixed and are already
275
+ // on the touch rungs; the rest promote under the forced coarse, exactly as on a real phone.
276
+ coarse: true,
277
+ components: {
278
+ button: {
279
+ moves:
280
+ 'Full-pill keycap cut: 2px edge, deep anchored lip, Mohr-BlackIt italic uppercase label, focus pop instead of ring; framed/lg by default. Sits on the shared touch ladder — height + type 28/36/40/44/52 via the touch: rules in button.ts, selected here by the baked cut-fixed marker rather than by a pointer query, so the desktop mock matches a real phone.',
281
+ channels: ['props', 'slot', 'var'],
282
+ },
283
+ input: {
284
+ moves:
285
+ 'lg default plus the shared touch ladder — height + type 28/36/40/44/52 via the touch: rules in input.ts (height-locked to the button so a joined field group matches), selected here by the baked cut-fixed marker rather than by a pointer query. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
286
+ channels: ['props', 'slot', 'var'],
287
+ },
288
+ textarea: {
289
+ moves:
290
+ 'Mirrors input — bakes one size up (md→lg default) for thumb input, opted out of the adaptive coarse promotion. Radius mirrors web. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
291
+ channels: ['props', 'slot', 'var'],
292
+ },
293
+ drawer: {
294
+ moves:
295
+ 'Peach surface matching the default card variant, with a solid Bark Brown mark (Acer Orange in dark) on both the 2px edge and the drag handle, and a Mohr-italic title one size up. Edge radius rounds to 2xl/32 and the container gets roomier 24px horizontal padding — those two ride CSS on the context marker class, not this theme, so the channel here is slots only.',
296
+ channels: ['slot'],
297
+ },
298
+ kbd: {
299
+ moves: 'Rounder corner (rounded-sm); the sm size keeps the tight rounded-xs. Same as marketing.',
300
+ channels: ['slot'],
301
+ },
302
+ listbox: {
303
+ moves:
304
+ 'Rounder shell (rounded-xl over the stock corner the standard cut leaves alone), with the row inset stepped 4px→12px so the first and last rows clear the arc instead of tucking under it. The inset lands on both group and content because stock pads a grouped list and a virtualised one in different slots. One flat value across every size — a context delta carries no size variants. Same as marketing.',
305
+ channels: ['slot'],
306
+ },
307
+ badge: {
308
+ moves:
309
+ 'Full-pill corner (rounded-full over the standard rounded-sm tag). Touch increase baked in: default size steps md→lg and cut-fixed opts out of the coarse bump so it holds that size. Marketing matches the corner but stays adaptive (no baked increase).',
310
+ channels: ['props', 'slot'],
311
+ },
312
+ chip: {
313
+ moves:
314
+ 'Defaults to inset — the dot sits inside the host boundary instead of overflowing it, because the mobile button is a full pill and the app\'s avatars are round, so an overflowing dot floats off a curved edge. Geometry is untouched stock (the overflow translate is a compound variant keyed on inset: false); this is a prop default and nothing more.',
315
+ channels: ['props'],
316
+ },
317
+ alert: {
318
+ moves: 'Rounder banner corner (rounded-xl over the standard rounded-lg). Same as marketing.',
319
+ channels: ['slot'],
320
+ },
321
+ calendar: {
322
+ moves:
323
+ 'Rounder card frame (rounded-2xl over the standard rounded-xl) — shape only; grid, date cells, heading and focus ring are inherited.',
324
+ channels: ['slot'],
325
+ },
326
+ card: {
327
+ moves:
328
+ 'Rounder cut: one radius step up (rounded-2xl/32 over the standard xl/24); --forest-card-radius follows so nested cards step 32→12→8 at the roomier p-5. Title is Mohr italic + uppercase (the Mohr-BlackIt cut), one size up; description one size up, still GT Haptik. Defaults to the peach accent variant. A KPI figure inside the card takes the same face through the --fx-metric traits. Same radius as marketing; marketing title is the same face upright.',
329
+ channels: ['props', 'slot', 'var'],
330
+ },
331
+ tabs: {
332
+ moves:
333
+ 'Bakes size lg default (md→lg) for touch tap targets; cut-fixed opts the trigger/icon out of the preset coarse ladder so it holds instead of stepping to xl.',
334
+ channels: ['props', 'slot'],
335
+ },
336
+ pinInput: {
337
+ moves:
338
+ 'Bakes size lg default (md→lg) for thumb-sized OTP cells; cut-fixed puts the cell on the shared touch ladder (28/36/40/44/52) with no pointer to query — the same rungs a coarse pointer selects. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
339
+ channels: ['props', 'slot', 'var'],
340
+ },
341
+ switch: {
342
+ moves:
343
+ 'Bakes size lg default (md→lg) for a thumb-sized tap target; cut-fixed opts the track/thumb/label out of the preset coarse ladder so it holds instead of stepping to xl.',
344
+ channels: ['props', 'slot'],
345
+ },
346
+ checkbox: {
347
+ moves:
348
+ 'Bakes size lg default (md→lg) for a taller tap row; cut-fixed opts the box/row/label out of the preset coarse ladder so it holds instead of stepping to xl.',
349
+ channels: ['props', 'slot'],
350
+ },
351
+ radioGroup: {
352
+ moves:
353
+ 'Bakes size lg default (md→lg) for a taller tap row; cut-fixed opts the circle/row/text out of the preset coarse ladder so it holds instead of stepping to xl.',
354
+ channels: ['props', 'slot'],
355
+ },
356
+ slider: {
357
+ moves:
358
+ 'Bakes size lg default (md→lg) for a bigger grab thumb; cut-fixed opts the thumb out of the preset coarse ladder so it holds instead of stepping to xl.',
359
+ channels: ['props', 'slot'],
360
+ },
361
+ inputDate: {
362
+ moves:
363
+ 'Bakes size lg default (md→lg) — the same touch step as input; cut-fixed puts the field/icons on the shared touch ladder (28/36/40/44/52) with no pointer to query — the same rungs a coarse pointer selects. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
364
+ channels: ['props', 'slot', 'var'],
365
+ },
366
+ inputTime: {
367
+ moves:
368
+ 'Bakes size lg default (md→lg) — the same touch step as input; cut-fixed puts the field/icons on the shared touch ladder (28/36/40/44/52) with no pointer to query — the same rungs a coarse pointer selects. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
369
+ channels: ['props', 'slot', 'var'],
370
+ },
371
+ inputNumber: {
372
+ moves:
373
+ 'Bakes size lg default (md→lg) — the same touch step as input; cut-fixed puts the field on the shared touch ladder (28/36/40/44/52) with no pointer to query — the same rungs a coarse pointer selects. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
374
+ channels: ['props', 'slot', 'var'],
375
+ },
376
+ select: {
377
+ moves:
378
+ 'Bakes size lg default (md→lg) — the same touch step as input; cut-fixed puts the trigger/icons on the shared touch ladder (28/36/40/44/52) with no pointer to query — the same rungs a coarse pointer selects. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
379
+ channels: ['props', 'slot', 'var'],
380
+ },
381
+ selectMenu: {
382
+ moves:
383
+ 'Bakes size lg default (md→lg) — the same touch step as input; cut-fixed puts the trigger/icons on the shared touch ladder (28/36/40/44/52) with no pointer to query — the same rungs a coarse pointer selects. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
384
+ channels: ['props', 'slot', 'var'],
385
+ },
386
+ inputMenu: {
387
+ moves:
388
+ 'Bakes size lg default (md→lg) — the same touch step as input; cut-fixed puts the field/icons on the shared touch ladder (28/36/40/44/52) with no pointer to query — the same rungs a coarse pointer selects. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
389
+ channels: ['props', 'slot', 'var'],
390
+ },
391
+ inputTags: {
392
+ moves:
393
+ 'Bakes size lg default (md→lg) — the same touch step as input; cut-fixed puts the field/icons on the shared touch ladder (28/36/40/44/52) with no pointer to query — the same rungs a coarse pointer selects. Carries the shared 2px field edge (matching the button) with the depth line stepped to 5px so the recess still reads 3px under it.',
394
+ channels: ['props', 'slot', 'var'],
395
+ },
396
+ fieldGroup: {
397
+ moves:
398
+ 'Bakes size lg default (md→lg). UFieldGroup provides its size to joined children, so setting it here makes a mobile group read at lg instead of dragging its children back down to md. Steps the overlap to 2px (-space-x-0.5) to match this cut\'s 2px segment edge, so a joined seam collapses to one line instead of painting both edges.',
399
+ channels: ['props', 'slot'],
400
+ },
401
+ },
402
+ };
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ // The filter row: a trigger, applied chips, and clear-all — the whole thing in one bar.
3
+ //
4
+ // This is the composition, not the implementation. <FFilterTrigger> is the add-filter control and
5
+ // <FFilterClauses> is the row of chips; both are controlled by the same Clause[], so a page that
6
+ // wants them in DIFFERENT bars writes the two directly and binds them to one v-model:
7
+ //
8
+ // <FPageToolbar>
9
+ // <template #left><UNavigationMenu :items="views" /></template>
10
+ // <template #right><FFilterTrigger v-model="clauses" :schema="schema" /></template>
11
+ // </FPageToolbar>
12
+ //
13
+ // <FPageToolbar v-if="clauses.length">
14
+ // <FFilterClauses v-model="clauses" :schema="schema" />
15
+ // </FPageToolbar>
16
+ //
17
+ // Nothing is teleported or injected to make that work — sharing a v-model is the whole mechanism.
18
+ //
19
+ // Controlled. The bar's data contract is a Clause[]: it never receives rows and never filters. A
20
+ // consumer passes the clauses to applyClauses, or toColumnFilters' output to a table.
21
+ import { computed, useTemplateRef } from 'vue';
22
+ import FFilterClauses from './FFilterClauses.vue';
23
+ import FFilterTrigger from './FFilterTrigger.vue';
24
+ import type { FilterSlot } from './filter.theme';
25
+ import type { FilterSchema } from './filterSchema';
26
+ import type { Clause } from './filterClause';
27
+
28
+ const props = defineProps<{
29
+ modelValue: Clause[];
30
+ schema: FilterSchema;
31
+ /** Text beside the trigger's icon. Off by default — see <FFilterTrigger>. */
32
+ label?: string;
33
+ ui?: Partial<Record<FilterSlot, string>>;
34
+ }>();
35
+
36
+ const emit = defineEmits<{ 'update:modelValue': [clauses: Clause[]] }>();
37
+
38
+ const trigger = useTemplateRef<InstanceType<typeof FFilterTrigger>>('trigger');
39
+ const clauses = useTemplateRef<InstanceType<typeof FFilterClauses>>('clauses');
40
+
41
+ const update = (next: Clause[]) => emit('update:modelValue', next);
42
+
43
+ const model = computed(() => props.modelValue);
44
+
45
+ defineExpose({ open: () => trigger.value?.open() });
46
+ </script>
47
+
48
+ <template>
49
+ <!-- The trigger goes through the clause row's slot rather than beside it, so both halves share ONE
50
+ flex flow: the trigger holds its place at the head of the row and the chips wrap past it,
51
+ which is what they did when this was a single component. -->
52
+ <FFilterClauses
53
+ ref="clauses"
54
+ :model-value="model"
55
+ :schema="props.schema"
56
+ :trigger="trigger"
57
+ :ui="props.ui"
58
+ @update:model-value="update"
59
+ >
60
+ <template #trigger>
61
+ <FFilterTrigger
62
+ ref="trigger"
63
+ :model-value="model"
64
+ :schema="props.schema"
65
+ :clauses="clauses"
66
+ :label="props.label"
67
+ :ui="props.ui"
68
+ @update:model-value="update"
69
+ />
70
+ </template>
71
+ </FFilterClauses>
72
+ </template>