@humanforest/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/LICENSE +67 -0
  2. package/icons/forest/bike-asterisk.svg +21 -0
  3. package/icons/forest/bike-down.svg +19 -0
  4. package/icons/forest/bike-off.svg +20 -0
  5. package/icons/forest/bike-up.svg +19 -0
  6. package/icons/forest/bike-x.svg +19 -0
  7. package/icons/forest/bike.svg +18 -0
  8. package/icons/forest/map/forest-place-count.svg +21 -0
  9. package/icons/forest/map/forest-place-marker.svg +17 -0
  10. package/icons/forest/map/forest-vehicle-marker.svg +73 -0
  11. package/icons/forest/map/manifest.json +373 -0
  12. package/icons/forest/map/place-count-manifest.json +83 -0
  13. package/icons/forest/map/place-manifest.json +74 -0
  14. package/icons/forest/mark-fill.svg +6 -0
  15. package/icons/forest/mark.svg +6 -0
  16. package/icons/forest/parking.svg +3 -0
  17. package/icons/forest/star-fill.svg +4 -0
  18. package/icons/forest/star.svg +4 -0
  19. package/icons/forest/traffic-light-caution.svg +8 -0
  20. package/icons/forest/traffic-light-go.svg +8 -0
  21. package/icons/forest/traffic-light-lit.svg +8 -0
  22. package/icons/forest/traffic-light-stop.svg +8 -0
  23. package/icons/forest/traffic-light.svg +8 -0
  24. package/package.json +64 -0
  25. package/src/address/address.ts +125 -0
  26. package/src/address/autofill.ts +78 -0
  27. package/src/address/geocode.ts +55 -0
  28. package/src/address/index.ts +31 -0
  29. package/src/address/validate.ts +91 -0
  30. package/src/canvas/FCanvasCard.vue +97 -0
  31. package/src/canvas/FCanvasControl.vue +121 -0
  32. package/src/canvas/FCanvasControlGroup.vue +100 -0
  33. package/src/canvas/FCanvasFlyout.vue +216 -0
  34. package/src/canvas/FCanvasHud.vue +402 -0
  35. package/src/canvas/canvasSurface.ts +39 -0
  36. package/src/canvas/index.ts +17 -0
  37. package/src/contexts/ForestContext.vue +53 -0
  38. package/src/contexts/contexts.ts +153 -0
  39. package/src/contexts/index.ts +23 -0
  40. package/src/contexts/marketing-theme.ts +214 -0
  41. package/src/contexts/mobile-theme.ts +402 -0
  42. package/src/filter/FFilterBar.vue +72 -0
  43. package/src/filter/FFilterChip.vue +362 -0
  44. package/src/filter/FFilterClauses.vue +104 -0
  45. package/src/filter/FFilterTrigger.vue +183 -0
  46. package/src/filter/filter.theme.ts +27 -0
  47. package/src/filter/filterClause.ts +265 -0
  48. package/src/filter/filterPredicate.ts +134 -0
  49. package/src/filter/filterSchema.ts +82 -0
  50. package/src/filter/index.ts +36 -0
  51. package/src/forest-preset.ts +68 -0
  52. package/src/icons.ts +79 -0
  53. package/src/index.ts +17 -0
  54. package/src/kpi/FKpi.vue +253 -0
  55. package/src/kpi/FKpiGroup.vue +161 -0
  56. package/src/kpi/index.ts +20 -0
  57. package/src/kpi/kpi.theme.ts +168 -0
  58. package/src/kpi/kpiDelta.ts +82 -0
  59. package/src/kpi/kpiFormat.ts +72 -0
  60. package/src/kpi/kpiGroupState.ts +63 -0
  61. package/src/logo/FLogo.vue +52 -0
  62. package/src/logo/FSubLogo.vue +214 -0
  63. package/src/logo/index.ts +8 -0
  64. package/src/map/FMap.vue +355 -0
  65. package/src/map/FMapCompass.vue +154 -0
  66. package/src/map/FMapCoordinates.vue +134 -0
  67. package/src/map/FMapDrawTools.vue +116 -0
  68. package/src/map/FMapFullscreen.vue +92 -0
  69. package/src/map/FMapGeolocate.vue +208 -0
  70. package/src/map/FMapLayers.vue +75 -0
  71. package/src/map/FMapLegend.vue +45 -0
  72. package/src/map/FMapPopup.vue +129 -0
  73. package/src/map/FMapRamp.vue +69 -0
  74. package/src/map/FMapRegion.vue +77 -0
  75. package/src/map/FMapScale.vue +107 -0
  76. package/src/map/FMapSearch.vue +362 -0
  77. package/src/map/FMapTilt.vue +87 -0
  78. package/src/map/FMapTools.vue +61 -0
  79. package/src/map/FMapZoom.vue +81 -0
  80. package/src/map/FMarker.vue +218 -0
  81. package/src/map/FTrackScrubber.vue +94 -0
  82. package/src/map/FVehicleLegend.vue +72 -0
  83. package/src/map/FVehicleMark.vue +166 -0
  84. package/src/map/bin.ts +196 -0
  85. package/src/map/coordinates.ts +83 -0
  86. package/src/map/density.ts +401 -0
  87. package/src/map/draw.ts +356 -0
  88. package/src/map/ease.ts +67 -0
  89. package/src/map/fullscreen.ts +59 -0
  90. package/src/map/heat.ts +160 -0
  91. package/src/map/index.ts +351 -0
  92. package/src/map/liveMarkers.ts +60 -0
  93. package/src/map/map.css +116 -0
  94. package/src/map/mapColor.ts +31 -0
  95. package/src/map/mapContext.ts +12 -0
  96. package/src/map/mapLayers.ts +55 -0
  97. package/src/map/mapLegend.ts +70 -0
  98. package/src/map/mapSearch.theme.ts +79 -0
  99. package/src/map/mapSearch.ts +279 -0
  100. package/src/map/mapUrl.ts +117 -0
  101. package/src/map/markerAsset.ts +3 -0
  102. package/src/map/padding.ts +39 -0
  103. package/src/map/place.ts +192 -0
  104. package/src/map/ramp.ts +95 -0
  105. package/src/map/regionSelect.ts +163 -0
  106. package/src/map/route.ts +167 -0
  107. package/src/map/scale.ts +329 -0
  108. package/src/map/scaleBar.ts +67 -0
  109. package/src/map/selectionModel.ts +50 -0
  110. package/src/map/sources/coordinates.ts +58 -0
  111. package/src/map/sources/mapboxPlaces.ts +71 -0
  112. package/src/map/style.ts +14 -0
  113. package/src/map/track.ts +279 -0
  114. package/src/map/useFeatureSelect.ts +230 -0
  115. package/src/map/useLiveMarkers.ts +84 -0
  116. package/src/map/useMap.ts +150 -0
  117. package/src/map/useMapDraw.ts +776 -0
  118. package/src/map/useMapOverlay.ts +421 -0
  119. package/src/map/useMapTableSelection.ts +101 -0
  120. package/src/map/useMapUrlState.ts +155 -0
  121. package/src/map/useMarkerSource.ts +163 -0
  122. package/src/map/useRegionSelect.ts +309 -0
  123. package/src/map/useTokenMode.ts +61 -0
  124. package/src/map/useTrackPlayback.ts +131 -0
  125. package/src/map/useVehicleFilter.ts +51 -0
  126. package/src/map/useVisibleFeatures.ts +124 -0
  127. package/src/map/vehicle.ts +807 -0
  128. package/src/map/vehicleFilter.ts +284 -0
  129. package/src/map/vehicleLegend.ts +155 -0
  130. package/src/map/visibleFeatures.ts +192 -0
  131. package/src/map/zone.ts +291 -0
  132. package/src/shell/FAppNavbar.vue +68 -0
  133. package/src/shell/FAppShell.vue +163 -0
  134. package/src/shell/FAppSidebar.vue +215 -0
  135. package/src/shell/FPageToolbar.vue +126 -0
  136. package/src/shell/FScrollShadow.vue +43 -0
  137. package/src/shell/FSidePanel.vue +258 -0
  138. package/src/shell/FToolbarActions.vue +32 -0
  139. package/src/shell/index.ts +33 -0
  140. package/src/shell/nav.ts +37 -0
  141. package/src/shell/pageToolbar.ts +69 -0
  142. package/src/shell/shell.ts +74 -0
  143. package/src/shell/sidePanel.ts +115 -0
  144. package/src/styles/forest.css +852 -0
  145. package/src/styles/index.css +39 -0
  146. package/src/themes/accordion.ts +36 -0
  147. package/src/themes/alert.ts +16 -0
  148. package/src/themes/auth-form.ts +19 -0
  149. package/src/themes/avatar-group.ts +9 -0
  150. package/src/themes/avatar.ts +18 -0
  151. package/src/themes/badge.ts +51 -0
  152. package/src/themes/banner.ts +21 -0
  153. package/src/themes/breadcrumb.ts +10 -0
  154. package/src/themes/button.mechanics.ts +191 -0
  155. package/src/themes/button.ts +136 -0
  156. package/src/themes/calendar.ts +29 -0
  157. package/src/themes/card.ts +67 -0
  158. package/src/themes/carousel.ts +41 -0
  159. package/src/themes/checkbox-group.ts +20 -0
  160. package/src/themes/checkbox.ts +47 -0
  161. package/src/themes/chip.ts +50 -0
  162. package/src/themes/collapsible.ts +26 -0
  163. package/src/themes/color-picker.ts +13 -0
  164. package/src/themes/command-palette.ts +37 -0
  165. package/src/themes/container.ts +14 -0
  166. package/src/themes/context-menu.ts +38 -0
  167. package/src/themes/dashboard-group.ts +11 -0
  168. package/src/themes/dashboard-navbar.ts +10 -0
  169. package/src/themes/dashboard-panel.ts +18 -0
  170. package/src/themes/dashboard-resize-handle.ts +35 -0
  171. package/src/themes/dashboard-sidebar-collapse.ts +7 -0
  172. package/src/themes/dashboard-sidebar-toggle.ts +7 -0
  173. package/src/themes/dashboard-sidebar.ts +58 -0
  174. package/src/themes/dashboard-toolbar.ts +15 -0
  175. package/src/themes/drawer.ts +45 -0
  176. package/src/themes/dropdown-menu.ts +38 -0
  177. package/src/themes/empty.ts +43 -0
  178. package/src/themes/field-group.ts +52 -0
  179. package/src/themes/file-upload.ts +38 -0
  180. package/src/themes/footer-columns.ts +13 -0
  181. package/src/themes/footer.ts +9 -0
  182. package/src/themes/form-field.ts +25 -0
  183. package/src/themes/form.ts +22 -0
  184. package/src/themes/header.ts +12 -0
  185. package/src/themes/icon.ts +61 -0
  186. package/src/themes/index.ts +99 -0
  187. package/src/themes/input-date.ts +73 -0
  188. package/src/themes/input-menu.ts +66 -0
  189. package/src/themes/input-number.ts +58 -0
  190. package/src/themes/input-rating.ts +25 -0
  191. package/src/themes/input-tags.ts +63 -0
  192. package/src/themes/input-time.ts +70 -0
  193. package/src/themes/input.ts +148 -0
  194. package/src/themes/kbd.ts +27 -0
  195. package/src/themes/link.ts +27 -0
  196. package/src/themes/listbox.ts +48 -0
  197. package/src/themes/main.ts +6 -0
  198. package/src/themes/marquee.ts +45 -0
  199. package/src/themes/modal.ts +46 -0
  200. package/src/themes/navigation-menu.ts +80 -0
  201. package/src/themes/page-anchors.ts +7 -0
  202. package/src/themes/page-aside.ts +7 -0
  203. package/src/themes/page-card.ts +22 -0
  204. package/src/themes/page-cta.ts +18 -0
  205. package/src/themes/page-feature.ts +13 -0
  206. package/src/themes/page-header.ts +16 -0
  207. package/src/themes/page-hero.ts +15 -0
  208. package/src/themes/page-links.ts +12 -0
  209. package/src/themes/page-section.ts +14 -0
  210. package/src/themes/page.ts +7 -0
  211. package/src/themes/pagination.ts +17 -0
  212. package/src/themes/pin-input.ts +73 -0
  213. package/src/themes/popover.ts +30 -0
  214. package/src/themes/progress.ts +29 -0
  215. package/src/themes/prose.ts +45 -0
  216. package/src/themes/radio-group.ts +39 -0
  217. package/src/themes/scroll-area.ts +23 -0
  218. package/src/themes/select-menu.ts +71 -0
  219. package/src/themes/select.ts +72 -0
  220. package/src/themes/separator.ts +28 -0
  221. package/src/themes/sidebar.ts +127 -0
  222. package/src/themes/skeleton.ts +15 -0
  223. package/src/themes/slideover.ts +87 -0
  224. package/src/themes/slider.ts +48 -0
  225. package/src/themes/stepper.ts +41 -0
  226. package/src/themes/switch.ts +63 -0
  227. package/src/themes/table.ts +34 -0
  228. package/src/themes/tabs.ts +74 -0
  229. package/src/themes/textarea.ts +77 -0
  230. package/src/themes/timeline.ts +25 -0
  231. package/src/themes/toast.ts +29 -0
  232. package/src/themes/toaster.ts +66 -0
  233. package/src/themes/tooltip.ts +31 -0
  234. package/src/themes/tree.ts +41 -0
  235. package/src/themes/user.ts +28 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Forest theme fragment — UInputTags (tag/token input: a field of removable chips + a text input).
3
+ *
4
+ * Forest opinions applied:
5
+ * - base (field): the recessed depth line ported from UInput (inset top edge at rest, dropped on
6
+ * focus) so the field family shares one pressed-into-the-page identity. Focus lives on the inner
7
+ * input, so the drop and the stock ring both key off `has-focus-visible`. The ring already follows
8
+ * the colour intent (ring-primary → forest via the alias), so no forest-500 override is needed.
9
+ * - touch ladder: each size takes its touch rung (min-h 28/36/40/44/52) under a coarse
10
+ * pointer or the mobile cut — box, gap,
11
+ * effective text, icons); px held. The base carries a responsive `md:text-*` shrink, so the coarse
12
+ * type bump targets the next size's EFFECTIVE value (sm→md `touch:text-sm`, lg→xl `touch:text-base`).
13
+ * - recess tint: an explicit intent tints the depth line to its ring colour, but only in `highlight`.
14
+ * - item (chip): rounded-lg (Forest soft radius, not stock's rounded-sm).
15
+ * - itemDelete (remove button): rounded-sm — a tight close icon inside a pill should not itself be
16
+ * pill-shaped (avoids a nested pill-in-pill look).
17
+ *
18
+ * Slots touched: base, item, itemDelete; size variant (depth + touch).
19
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/input-tags.ts
20
+ * The remove-icon (appConfig close) and the raw inner input slot are stock.
21
+ */
22
+
23
+ export const inputTagsTheme = {
24
+ slots: {
25
+ base: 'transition-[color,box-shadow] duration-(--motion-fast) shadow-[inset_0_calc(var(--forest-input-depth)_+_var(--forest-field-edge,0px))_0_0_var(--forest-input-recess)] has-focus-visible:shadow-none',
26
+ // Pill chip: rounded-lg (Forest soft radius, not stock's rounded-sm).
27
+ item: 'rounded-lg',
28
+ // rounded-sm (not rounded-lg) — a tight close icon inside a pill should not itself be pill-shaped.
29
+ itemDelete: 'rounded-sm',
30
+ },
31
+ variants: {
32
+ // Per-size recessed depth (2/2/3/3/4) + the touch ladder. Object-shaped; deltas only.
33
+ // `touch:` = the shared TOUCH ladder (styles/forest.css) — height+type twin of button.ts/input.ts
34
+ // so a joined <UFieldGroup> matches. Fired by a coarse pointer or by the mobile cut's
35
+ // baked marker — same numbers either way, so a real phone can't step twice.
36
+ size: {
37
+ xs: { base: '[--forest-input-depth:2px] touch:min-h-7 touch:gap-1.5' }, // touch 28
38
+ sm: { base: '[--forest-input-depth:2px] touch:min-h-9 touch:text-sm touch:rounded-md touch:[--forest-input-depth:3px]', leadingIcon: 'touch:size-5', trailingIcon: 'touch:size-5' }, // touch 36
39
+ md: { base: '[--forest-input-depth:3px] touch:min-h-10 touch:text-sm touch:gap-2' }, // touch 40
40
+ lg: { base: '[--forest-input-depth:3px] touch:min-h-11 touch:text-base touch:rounded-lg touch:[--forest-input-depth:4px]', leadingIcon: 'touch:size-6', trailingIcon: 'touch:size-6' }, // touch 44
41
+ xl: { base: '[--forest-input-depth:4px] touch:min-h-13 touch:text-lg touch:rounded-lg touch:[--forest-input-depth:4px]', leadingIcon: 'touch:size-7', trailingIcon: 'touch:size-7' }, // touch 52
42
+ },
43
+ },
44
+ compoundVariants: [
45
+ // ghost / none carry no fill, so a resting depth line floats with nothing to recess into. ghost
46
+ // picks it up on hover (elevated fill arrives); none stays flat.
47
+ { variant: 'ghost', class: { base: 'shadow-none hover:shadow-[inset_0_calc(var(--forest-input-depth)_+_var(--forest-field-edge,0px))_0_0_var(--forest-input-recess)]' } },
48
+ { variant: 'none', class: { base: 'shadow-none' } },
49
+ // Recess tint gated to `highlight` (the persistent coloured-ring state); a plain rest field keeps
50
+ // the neutral line. neutral omitted (its highlight ring is the inverted tone).
51
+ { color: 'primary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-primary)]' } },
52
+ { color: 'secondary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-secondary)]' } },
53
+ { color: 'success', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-success)]' } },
54
+ { color: 'info', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-info)]' } },
55
+ { color: 'warning', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warning)]' } },
56
+ { color: 'error', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-error)]' } },
57
+ { color: 'forest', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-forest)]' } },
58
+ { color: 'maple', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-maple)]' } },
59
+ { color: 'river', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-river)]' } },
60
+ { color: 'warm', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warm)]' } },
61
+ { color: 'acid', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-acid)]' } },
62
+ ],
63
+ } as const;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Forest theme fragment — UInputTime (time-picker field).
3
+ *
4
+ * Forest opinions applied:
5
+ *
6
+ * outline variant:
7
+ * Brand forest-500 focus ring on the wrapper — ring-[var(--forest-500)] — consistent with
8
+ * UInput, USelect, and UTextarea. Keeps the full set of field controls on one focus-ring
9
+ * language.
10
+ *
11
+ * subtle variant:
12
+ * Same brand forest-500 focus ring as outline, matching USelect's subtle treatment.
13
+ *
14
+ * base/segment carry no resting Forest opinion — shape and state styling match stock; the size
15
+ * variant adds the touch ladder, so the field takes its touch rung on a finger pointer.
16
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/input-time.ts
17
+ *
18
+ * Focus rings on compound colour variants (outline × primary/error/…) are handled by Nuxt UI's
19
+ * own compoundVariants using the semantic colour vars, which are wired to the Forest OKLCH ramps
20
+ * in docs.css. No override needed — they resolve to the correct ramp values automatically.
21
+ */
22
+
23
+ export const inputTimeTheme = {
24
+ slots: {
25
+ // Recessed depth line ported from UInput — inset top edge at rest, dropped on focus (has-focus-
26
+ // visible; focus lives on a segment) so the field lifts to its ring. Depth scales per size below;
27
+ // recess colour is mode-aware + tints to the intent (colour variant). Same identity as every field.
28
+ base: 'transition-[color,box-shadow] duration-(--motion-fast) shadow-[inset_0_calc(var(--forest-input-depth)_+_var(--forest-field-edge,0px))_0_0_var(--forest-input-recess)] has-focus-visible:shadow-none',
29
+ },
30
+ variants: {
31
+ variant: {
32
+ // Brand forest-500 focus ring — consistent with UInput / USelect / UTextarea.
33
+ outline: 'hover:bg-elevated disabled:bg-default focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--forest-500)]',
34
+ // Subtle gets the same brand focus-ring treatment.
35
+ subtle: 'hover:bg-accented/75 disabled:bg-elevated focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--forest-500)]',
36
+ },
37
+ // Coarse touch step-up — one size up as a unit (py, gap, segment/base type, icon); px held. Same
38
+ // ladder as UInputDate — the two share a size scale. Every rung has a touch twin, xl
39
+ // included (52), so the mobile cut needs no escape: it lands on the same rung.
40
+ // `touch:` = the shared TOUCH ladder (styles/forest.css) — height+type twin of button.ts/input.ts
41
+ // so a joined <UFieldGroup> matches. Fired by a coarse pointer or by the mobile cut's
42
+ // baked marker — same numbers either way, so a real phone can't step twice.
43
+ size: {
44
+ xs: { base: '[--forest-input-depth:2px] touch:min-h-7 touch:gap-1.5' }, // touch 28
45
+ sm: { base: '[--forest-input-depth:2px] touch:min-h-9 touch:text-sm touch:rounded-md touch:[--forest-input-depth:3px]', leadingIcon: 'touch:size-5', trailingIcon: 'touch:size-5' }, // touch 36 (type = md's effective value; stock md:text-sm shrink)
46
+ md: { base: '[--forest-input-depth:3px] touch:min-h-10 touch:text-sm touch:gap-2' }, // touch 40
47
+ lg: { base: '[--forest-input-depth:3px] touch:min-h-11 touch:text-base touch:rounded-lg touch:[--forest-input-depth:4px]', leadingIcon: 'touch:size-6', trailingIcon: 'touch:size-6' }, // touch 44
48
+ xl: { base: '[--forest-input-depth:4px] touch:min-h-13 touch:text-lg touch:rounded-lg touch:[--forest-input-depth:4px]', leadingIcon: 'touch:size-7', trailingIcon: 'touch:size-7' }, // touch 52
49
+ },
50
+ },
51
+ compoundVariants: [
52
+ // ghost / none carry no fill, so a resting depth line floats with nothing to recess into. ghost
53
+ // picks it up on hover (elevated fill arrives); none stays flat.
54
+ { variant: 'ghost', class: { base: 'shadow-none hover:shadow-[inset_0_calc(var(--forest-input-depth)_+_var(--forest-field-edge,0px))_0_0_var(--forest-input-recess)]' } },
55
+ { variant: 'none', class: { base: 'shadow-none' } },
56
+ // Recess tint gated to `highlight` (persistent coloured ring) — a plain rest field is neutral, so
57
+ // the line only takes the accent where the border does. neutral omitted (inverted highlight ring).
58
+ { color: 'primary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-primary)]' } },
59
+ { color: 'secondary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-secondary)]' } },
60
+ { color: 'success', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-success)]' } },
61
+ { color: 'info', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-info)]' } },
62
+ { color: 'warning', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warning)]' } },
63
+ { color: 'error', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-error)]' } },
64
+ { color: 'forest', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-forest)]' } },
65
+ { color: 'maple', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-maple)]' } },
66
+ { color: 'river', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-river)]' } },
67
+ { color: 'warm', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warm)]' } },
68
+ { color: 'acid', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-acid)]' } },
69
+ ],
70
+ } as const;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Forest theme fragment — UInput (text field).
3
+ *
4
+ * Ports the old custom <FInput> "recessed" identity onto the real Nuxt UI <UInput>. The signature is
5
+ * a SOLID inset top line (no blur) that makes the field read as pressed-INTO the page — the exact
6
+ * inverse of the keycap button's raised face. It is the depth effect; everything else is stock.
7
+ *
8
+ * How it composes (important): UInput's resting border and focus border are Tailwind `ring`
9
+ * utilities, which write to `--tw-ring-shadow`. Our depth line is a `shadow-[...]` utility, which
10
+ * writes to `--tw-shadow`. Tailwind merges both into one `box-shadow`, so the inset line stacks
11
+ * UNDER the ring border + focus ring — just like FInput layered an inset line under its 1px border.
12
+ *
13
+ * Forest opinions:
14
+ * - radius ramps w/ size — small fields read proportionally rounder at a fixed radius, so radius
15
+ * climbs with height: xs/sm rounded-sm (8px), md/lg/xl rounded-md (12px).
16
+ * Set per-size in the size variant (NOT the base slot) so one lever owns it. [base slot]
17
+ * - transition — animate box-shadow too (stock only transitions colour) so focus eases in. [base slot]
18
+ * - shadow-[inset…] — `inset 0 var(--forest-input-depth) 0 0 var(--forest-input-recess)`: the
19
+ * recessed depth line, at rest. Depth scales per size (2/2/3/3/4 for
20
+ * xs/sm/md/lg/xl) via the var, set in the size variant, so the shadow itself
21
+ * is written once on the base slot. The colour token is mode-aware
22
+ * (forest.css) so the line stays darker than the field in dark mode
23
+ * instead of reading as a white edge.
24
+ * - focus halo — on focus the resting inset depth line is dropped and replaced by a soft
25
+ * 3px outer halo alone, so the active field reads as lifted-to-focus rather
26
+ * than still pressed-in. The halo colour
27
+ * tracks `color` via --forest-input-halo (the colour variant below):
28
+ * stock only tints the RING, and only on outline/subtle, so a soft or
29
+ * ghost field shows the halo alone and every intent needs its own tone.
30
+ * The --forest-halo-* tokens it points at also flip by mode (forest.css) —
31
+ * numbered ramp steps don't, so one fixed step reads soft on the light
32
+ * page and glares on the dark one.
33
+ * - ghost has no rest — ghost carries no fill and no ring, so a resting depth line floats with
34
+ * nothing to be recessed INTO. It picks the line up on hover/focus, where
35
+ * the elevated fill arrives to hold it. [compoundVariants]
36
+ *
37
+ * Error (color="error" via UFormField) and per-colour focus rings resolve to the Forest ramps
38
+ * automatically through the semantic colour map (maple/forest), so no override is needed for those.
39
+ *
40
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/input.ts
41
+ */
42
+
43
+ export const inputTheme = {
44
+ slots: {
45
+ base: 'transition-[color,box-shadow] duration-(--motion-fast) shadow-[inset_0_calc(var(--forest-input-depth)_+_var(--forest-field-edge,0px))_0_0_var(--forest-input-recess)] focus-visible:shadow-[0_0_0_3px_var(--forest-input-halo,var(--forest-halo-primary))]',
46
+ },
47
+ // Touch: the field is drawn on the TOUCH ladder (min-h 28/36/40/44/52 for xs→xl, vs the fine-pointer
48
+ // 24/28/32/36/40) whenever it is being touched or is baked for touch — `touch:`, the styles/forest.css
49
+ // custom variant, NOT the built-in `pointer-coarse:`. Each size keeps its own rung, so lg is 44 (Apple's
50
+ // min target) and xl 52: a LADDER SWAP, not a shift to the next size up, which is why xl has somewhere
51
+ // to go. It scales as a UNIT (height, padding, gap, radius, icon, type, recess) so it reads as a bigger
52
+ // version of itself, not the same box stretched taller with stranded small type — a flat min-height
53
+ // floor was rejected for exactly that (xs at 48px with 12px text looked empty). Button and badge carry
54
+ // the same rungs, so a mixed <UFieldGroup> stays flush on either ladder.
55
+ //
56
+ // Two things it deliberately does NOT bump, and why:
57
+ // · Horizontal padding (px): stock adds a fixed `ps-9`/`pe-9` when a leading/trailing icon is
58
+ // present, and a `touch:px-*` (padding-inline) emitted after it clobbers that inset — the
59
+ // native `type=file` button then slides under the icon. Width contributes ~nothing to a tap
60
+ // target, so the step holds px and moves py.
61
+ // · Naive base type: stock hangs a RESPONSIVE `md:text-*` shrink per size (a `fixed:false`
62
+ // compound — a size's EFFECTIVE desktop type is one step below its base token). A raw
63
+ // `touch:text-base/5` ignores that and wins at desktop, so a bumped `sm` outgrew its sibling
64
+ // `md` and the ladder inverted. The type step therefore targets the next size's EFFECTIVE value
65
+ // (sm→md `touch:text-sm` 14/20, lg→xl `touch:text-base` 16/24), line-height riding along so
66
+ // the type-driven steps also gain height. Ladder stays monotonic AND every step grows.
67
+ //
68
+ // The recessed depth line scales per size too (shallower on short fields, deeper on tall) and steps
69
+ // with the rest of the bundle. PER-SIZE RADIUS climbs so no size reads over-rounded: xs/sm rounded-sm
70
+ // (8px); md/lg/xl keep stock's base rounded-md (12px) — only xs/sm are overridden, md+ are not
71
+ // restated. Each size lists only its deltas up. The mobile cut ALSO defaults to a bigger `size` (lg
72
+ // via forestMobileProps), so the two effects compound there: bigger rung values AND a bigger default
73
+ // rung, which is what makes the bold cut read as a real touch surface.
74
+ variants: {
75
+ size: {
76
+ // `touch:` = the shared TOUCH ladder (styles/forest.css), the height+type twin of button.ts so a
77
+ // joined <UFieldGroup> matches. min-h is the height authority (28/36/40/44/52 — stock input is
78
+ // padding-driven with no h-*, so min-h floors it and the text centres). text-* is PINNED (not left
79
+ // to the stock base) because stock md/lg run text-base/5 with a responsive desktop shrink — a plain
80
+ // class would flip between the desktop mock (≥768: shrunk) and a real phone (base). Every `touch:`
81
+ // branch is a 2-class selector for exactly that reason, so it outranks the shrink and the size is
82
+ // identical on both, equalling the button's text (xs12·sm14·md14·lg16·xl18). Depth/radius/icon
83
+ // step with the bigger box.
84
+ xs: { base: 'rounded-sm [--forest-input-depth:2px] touch:min-h-7 touch:gap-1.5' },
85
+ sm: { base: 'rounded-sm [--forest-input-depth:2px] touch:min-h-9 touch:text-sm touch:rounded-md touch:[--forest-input-depth:3px]', leadingIcon: 'touch:size-5', trailingIcon: 'touch:size-5' },
86
+ md: { base: '[--forest-input-depth:3px] touch:min-h-10 touch:text-sm touch:gap-2' },
87
+ lg: { base: '[--forest-input-depth:3px] touch:min-h-11 touch:text-base touch:rounded-lg touch:[--forest-input-depth:4px]', leadingIcon: 'touch:size-6', trailingIcon: 'touch:size-6' },
88
+ xl: { base: '[--forest-input-depth:4px] touch:min-h-13 touch:text-lg touch:rounded-lg touch:[--forest-input-depth:4px]', leadingIcon: 'touch:size-7', trailingIcon: 'touch:size-7' },
89
+ },
90
+ // The focus halo follows `color`. Stock only pairs colour with the RING (and only on the ringed
91
+ // variants); the halo is ours, so without this an error field wore a maple ring and a forest
92
+ // green glow. Each colour re-points the halo var at its own 200 step — primary resolves to the
93
+ // same value the hardcoded --forest-200 did, so the default field is unchanged.
94
+ //
95
+ // FLAT strings, not { base: … } objects — stock's colour variant is flat (every entry is ""),
96
+ // and a shape mismatch makes the merge drop these silently. Same trap as the FieldGroup ring bug:
97
+ // match the stock recipe's shape.
98
+ // Halo (focus glow) follows every colour. The recess is NOT set here — `color` alone leaves a
99
+ // resting field neutral (its border is ring-accented; the intent only shows on the focus/highlight
100
+ // ring), so a coloured recess at plain rest would contradict the neutral border. The recess tint is
101
+ // gated to `highlight` in compoundVariants below, where the ring IS persistently coloured.
102
+ color: {
103
+ primary: '[--forest-input-halo:var(--forest-halo-primary)]',
104
+ secondary: '[--forest-input-halo:var(--forest-halo-secondary)]',
105
+ success: '[--forest-input-halo:var(--forest-halo-success)]',
106
+ info: '[--forest-input-halo:var(--forest-halo-info)]',
107
+ warning: '[--forest-input-halo:var(--forest-halo-warning)]',
108
+ error: '[--forest-input-halo:var(--forest-halo-error)]',
109
+ forest: '[--forest-input-halo:var(--forest-halo-forest)]',
110
+ maple: '[--forest-input-halo:var(--forest-halo-maple)]',
111
+ river: '[--forest-input-halo:var(--forest-halo-river)]',
112
+ warm: '[--forest-input-halo:var(--forest-halo-warm)]',
113
+ acid: '[--forest-input-halo:var(--forest-halo-acid)]',
114
+ neutral: '[--forest-input-halo:var(--forest-halo-neutral)]',
115
+ },
116
+ },
117
+ // Ghost drops the resting depth line: with no fill and no ring there is no surface for the field to
118
+ // be recessed into, so the line reads as a stray mark. Hover brings the elevated fill (stock ghost
119
+ // behaviour) and the line with it; focus keeps the base slot's line + halo.
120
+ compoundVariants: [
121
+ {
122
+ variant: 'ghost',
123
+ class: {
124
+ // No fill at rest → no depth line and no focus halo (nothing for either to sit on/behind);
125
+ // hover/focus bring the elevated fill and the depth line back. The halo is dropped in both.
126
+ base: 'shadow-none focus-visible:shadow-none hover:shadow-[inset_0_calc(var(--forest-input-depth)_+_var(--forest-field-edge,0px))_0_0_var(--forest-input-recess)]',
127
+ },
128
+ },
129
+ // none is fill-less in every state (a bare field, e.g. a select-menu's panel search box) — drop
130
+ // the depth line AND the focus halo so it reads as a plain flush input, not a recessed glowing one.
131
+ { variant: 'none', class: { base: 'shadow-none focus-visible:shadow-none' } },
132
+ // Recess tint gated to `highlight` (the persistent coloured-ring state) — the ONLY resting state
133
+ // where the intent shows on the border, so the depth line reads as the same accent, not a stray
134
+ // colour on an otherwise-neutral field. neutral is omitted — its highlight ring is the inverted
135
+ // tone, which the neutral recess line already matches.
136
+ { color: 'primary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-primary)]' } },
137
+ { color: 'secondary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-secondary)]' } },
138
+ { color: 'success', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-success)]' } },
139
+ { color: 'info', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-info)]' } },
140
+ { color: 'warning', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warning)]' } },
141
+ { color: 'error', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-error)]' } },
142
+ { color: 'forest', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-forest)]' } },
143
+ { color: 'maple', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-maple)]' } },
144
+ { color: 'river', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-river)]' } },
145
+ { color: 'warm', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warm)]' } },
146
+ { color: 'acid', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-acid)]' } },
147
+ ],
148
+ } as const;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Forest theme fragment for UKbd (flat component, single `base` slot).
3
+ *
4
+ * Deltas only — the preset APPENDS to stock's base, so restating its layout classes would invite
5
+ * drift. Tokens:
6
+ * rounded-xs from stock rounded-sm — a tight corner so the cap reads as a physical key, not a
7
+ * pill/chip. Stock keeps the corner in `base` (not per size), so overriding it in base
8
+ * is clean. Contexts round it up (mobile/marketing).
9
+ * font-mono from stock font-sans — labels sit alongside code/token surfaces.
10
+ * px-2 from stock px-1 — a little breathing room, except sm keeps px-1: at 16px tall px-2
11
+ * is cramped.
12
+ *
13
+ * The sm padding exception lives in the SIZE VARIANT, not a `[&.h-4]` base hook — a preset/theme
14
+ * fragment can carry `variants` (tv deep-merges them; the size variant is applied after base, so
15
+ * `size.sm: 'px-1'` overrides base `px-2` at sm only). Stock's size variant holds no padding, so
16
+ * this ADDS one. (A CONTEXT can't do this — `<UTheme :ui>` carries slot/base classes only and
17
+ * silently ignores `variants` — which is why mobile/marketing keep an `[&.h-4]:` hook for their own
18
+ * per-size radius exception.)
19
+ *
20
+ * `font-medium`/`uppercase` and the colour/variant compounds are stock (already on the Forest
21
+ * ramps) — not Forest's to restate.
22
+ */
23
+
24
+ export const kbdTheme = {
25
+ base: 'rounded-xs font-mono px-2',
26
+ variants: { size: { sm: 'px-1' } },
27
+ } as const
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Forest theme fragment — ULink (styled router/anchor link, active states).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `base` (the single root slot): the stock `focus-visible:outline-primary` is replaced with a
6
+ * ramp-pinned focus outline anchored to var(--color-forest-500) — 2px, 2px offset — so the
7
+ * focus ring tracks the brand scale in both light and dark mode exactly as keycap buttons do.
8
+ * - Active state: text-primary (maps to forest-500 via the semantic colour wiring) is retained from
9
+ * the generated default — it reads correctly against Forest surfaces. No override needed.
10
+ * - Inactive rest: text-muted is the Forest semantic level for navigation links at rest (softer
11
+ * than text-default, never as dim as text-dimmed). Stock default is text-muted so this is kept.
12
+ * - Disabled: the generated cursor-not-allowed + opacity-75 is correct; no override needed.
13
+ * - Hover (non-disabled, non-active): text-default is the stock value and matches Forest's reading
14
+ * hierarchy (link darkens on hover but doesn't jump to active/brand colour). Kept as-is.
15
+ * - transition-colors: stock, retained for smooth colour transitions on hover and active change.
16
+ *
17
+ * Only the focus ring is a genuine Forest override (pinning the outline to the ramp var rather than
18
+ * relying on the semantic `outline-primary` token, for determinism across every Forest surface).
19
+ * Everything else is in harmony with stock and intentionally left unchanged.
20
+ *
21
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/link.ts
22
+ * Slots touched: base (root element).
23
+ */
24
+
25
+ export const linkTheme = {
26
+ base: 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-forest-500)]',
27
+ } as const;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Forest theme fragment — UListbox (standalone selectable list, optional filter input).
3
+ *
4
+ * Deltas only (the preset appends to stock Nuxt UI):
5
+ * - label — font-bold + tracking-wide: group headings read with brand confidence; stock is
6
+ * font-semibold, a weight the two-weight type system does not carry.
7
+ *
8
+ * The shell radius runs STOCK (rounded-lg). The standard voice states no opinion on it: a listbox
9
+ * is sized by its own options rather than by a surface it belongs to, so there is no tier to pin it
10
+ * against. The expressive contexts do have an opinion — mobile and marketing round the shell to
11
+ * 24px and inset the rows to clear the arc (contexts/{mobile,marketing}-theme.ts).
12
+ *
13
+ * The `item` hover/select slab keeps the stock before:rounded-md (12px) — the standalone-row shape
14
+ * a sidebar nav row, a palette result and a tree node all carry. The 8px slab belongs to rows inside
15
+ * an anchored 12px menu panel (dropdown-menu, select-menu), which this is not.
16
+ *
17
+ * The filter `input` is a UInput carrying the listbox's own size, so it takes its touch rungs from
18
+ * themes/input.ts and needs nothing here. The avatar and chip sizes ride prop-valued slots
19
+ * (itemLeadingAvatarSize / itemLeadingChipSize), which a class variant cannot reach — they hold
20
+ * their fine-pointer step under the bump below.
21
+ *
22
+ * Everything else (density ladder, colour ring, virtualize/divider split, selected check, empty and
23
+ * loading states, transitions) is stock on the Forest ramps.
24
+ *
25
+ * Slots touched: root · label; size variant (touch).
26
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/listbox.ts
27
+ */
28
+
29
+ export const listboxTheme = {
30
+ slots: {
31
+ // Group headings: bold weight + wide tracking — confident, readable at small size.
32
+ label: 'font-bold tracking-wide',
33
+ },
34
+
35
+ // Touch: under a coarse pointer each row promotes ONE size up — it borrows the next size's
36
+ // padding / gap / type / icon, so the tap slab scales as a unit (mirrors the menu family). xl is
37
+ // the ceiling. Deltas only: each size lists just the tokens that differ from its next step up
38
+ // (e.g. md→lg changes only padding + gap, so no type delta). `coarse:` carries the
39
+ // :not(.cut-fixed) escape, so a baked mobile cut opts out instead of promoting again.
40
+ variants: {
41
+ size: {
42
+ xs: { item: 'coarse:p-1.5 coarse:gap-1.5', label: 'coarse:p-1.5 coarse:gap-1.5' },
43
+ sm: { item: 'coarse:text-sm', label: 'coarse:text-xs', itemLeadingIcon: 'coarse:size-5', itemTrailingIcon: 'coarse:size-5' },
44
+ md: { item: 'coarse:p-2 coarse:gap-2', label: 'coarse:p-2 coarse:gap-2' },
45
+ lg: { item: 'coarse:text-base', label: 'coarse:text-sm', itemDescription: 'coarse:text-sm', itemLeadingIcon: 'coarse:size-6', itemTrailingIcon: 'coarse:size-6' },
46
+ },
47
+ },
48
+ } as const;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Reviewed, no delta. UMain is a single flex-1 wrapper with min-height derived from
3
+ * --ui-header-height. It sets no width, so it composes with the well rather than competing.
4
+ */
5
+
6
+ export const mainTheme = {};
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Forest theme fragment — UMarquee (infinite scrolling strip).
3
+ *
4
+ * Forest opinions applied:
5
+ *
6
+ * root — the default `[--duration:20s]` is preserved (Nuxt UI default). We extend the root
7
+ * with a subtle `rounded-xl` so that when a Marquee is placed inside a bordered card or on a
8
+ * clipped surface the overflow boundary matches the Forest --ui-radius soft-corner vocabulary
9
+ * (0.5 rem base, rounded-xl ≈ 0.75 rem for container-level trim). This prevents the infinite
10
+ * strip from bleeding hard corners out of a soft-radius container.
11
+ *
12
+ * content — we set `gap-6` (both axes) so items (badges, cards, avatars) always have a tactile,
13
+ * tighter separation than the stock `--gap` (--spacing(16) ≈ 64px, too airy — especially for a
14
+ * vertical feed, where 64px reads as broken spacing). `gap-6` = 24px on row AND column, so a
15
+ * horizontal strip and a vertical feed are both comfortably dense. Note this overrides the
16
+ * `gap-(--gap)` shorthand, so a per-instance `--gap` override no longer applies — pass a `gap-*`
17
+ * utility via `ui.content` to retune. The `items-center` alignment (stock) keeps mixed-height
18
+ * children centred — Forest preserves this.
19
+ *
20
+ * The overlay gradient blends into `from-default` (the semantic surface token), which adapts
21
+ * automatically in dark mode — no Forest override needed; the stock `before:from-default /
22
+ * after:from-default` is already semantically correct.
23
+ *
24
+ * Slots NOT overridden:
25
+ * - Orientation variants — the flex-row / flex-col layout is correct as-is.
26
+ * - `pauseOnHover` variant — stock `group-hover:[animation-play-state:paused]` is correct.
27
+ * - `reverse` variant — stock `![animation-direction:reverse]` is correct.
28
+ * - overlay compoundVariants — gradient geometry does not need a Forest delta.
29
+ *
30
+ * Slots touched: `root`, `content`.
31
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/marquee.ts
32
+ */
33
+
34
+ export const marqueeTheme = {
35
+ slots: {
36
+ // Soft-corner clip so the strip trims flush with Forest card/surface containers, paired with a
37
+ // small vertical inset (py-1.5): the rounded clip would otherwise shave the top/bottom of any
38
+ // item sitting flush to the strip edge (rounded-full pills read as flat-topped). The clip and its
39
+ // inset are one opinion. Override via `ui.root`/`class` for a deliberately full-bleed strip.
40
+ root: 'rounded-xl py-1.5',
41
+ // Tighter, tactile spacing on both axes (24px) vs the airy stock --gap (64px) — matters most
42
+ // for a vertical feed. Overrides gap-(--gap): retune via a gap-* utility on ui.content.
43
+ content: 'gap-6',
44
+ },
45
+ } as const;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Forest theme fragment — UModal (centred dialog on Reka UI's Dialog primitive).
3
+ *
4
+ * Deltas only (the preset APPENDS to stock Nuxt UI):
5
+ * - `title`: font-display (MohrAlt Black, uppercase) at text-xl — the dialog heading is a moment
6
+ * of clear hierarchy; the display face makes it unmistakable. Stock sets `font-semibold`.
7
+ * - radius: rounded-xl (0.75 rem) on the non-fullscreen panel — one step softer than stock's
8
+ * rounded-lg. It rides `variants.fullscreen.false.content`, NOT the base `content` slot: stock
9
+ * puts the radius on that same size variant, so a base override loses the tailwind-merge race
10
+ * (the per-size seam — see the badge fragment). Overriding the variant is also what keeps the
11
+ * fullscreen panel square (its `inset-0` content carries no radius).
12
+ * - `transition`: the open/close keyframes move onto the shared motion tokens. Stock runs a flat
13
+ * 200 ms both ways and eases the EXIT with `ease-in`, which the doctrine bans — it starts slow at
14
+ * the exact moment the user has already decided to dismiss. Forest enters on --motion-base (the
15
+ * scale-in bucket the popover/menu family sits in) and exits on --motion-fast, both --motion-ease-out.
16
+ *
17
+ * Everything else — overlay tint, header/body/footer padding, divide rules, close button — is stock
18
+ * and needs no Forest opinion.
19
+ *
20
+ * Slot names + variants sourced from: apps/docs/node_modules/.nuxt-ui/ui/modal.ts
21
+ */
22
+
23
+ export const modalTheme = {
24
+ slots: {
25
+ // Display face for the dialog title — MohrAlt Black, uppercase (via .font-display).
26
+ title: 'font-display text-xl',
27
+ },
28
+ variants: {
29
+ // Same keyframes as stock, retimed and re-eased onto the tokens. Only the animate-* utilities are
30
+ // restated (tailwind-merge takes the last one in the concat); the variant carries nothing else.
31
+ transition: {
32
+ true: {
33
+ overlay:
34
+ 'data-[state=open]:animate-[fade-in_var(--motion-base)_var(--motion-ease-out)] data-[state=closed]:animate-[fade-out_var(--motion-fast)_var(--motion-ease-out)]',
35
+ content:
36
+ 'data-[state=open]:animate-[scale-in_var(--motion-base)_var(--motion-ease-out)] data-[state=closed]:animate-[scale-out_var(--motion-fast)_var(--motion-ease-out)]',
37
+ },
38
+ },
39
+ fullscreen: {
40
+ // Softer brand radius on the standard centred panel; stock ring + shadow are retained.
41
+ false: {
42
+ content: 'rounded-xl',
43
+ },
44
+ },
45
+ },
46
+ } as const;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Forest theme fragment — NavigationMenu
3
+ *
4
+ * Forest opinion:
5
+ * - `viewport` no Forest radius override (rounded-md matches stock already); motion-timing
6
+ * classes (duration/easing) are retained on this slot.
7
+ * - `arrow` rounded-sm to keep the caret consistent with the viewport
8
+ * - `label` text-xs uppercase tracking-wider text-dimmed — Forest's
9
+ * standard section-label treatment (same as docs headings, sidebar groups)
10
+ * - `childLink` before:rounded-sm (8 px) — concentric one step inside the 12 px viewport panel.
11
+ *
12
+ * Active treatment — the house nav look (mirrors AppSidebar's active link:
13
+ * `!bg-primary/10 !text-primary font-bold`): the active item is a soft primary-tint slab with
14
+ * primary text, bold. Inactive items stay neutral (stock text-muted). Colour intent still tints the
15
+ * active *text* per ramp (stock colour compounds untouched); only the slab wash + weight are set here.
16
+ * - pill active link: stock paints a grey before:bg-elevated slab, unweighted — retint to the soft
17
+ * primary wash and bold it. Matches for both highlight on/off (no highlight key ⇒ both).
18
+ * - link variant: no slab, just bold the active text (colour still from the ramp).
19
+ * - childLink active: stock sets before:bg-elevated + text-highlighted (NOT colour-driven) — give the
20
+ * vertical/flyout child items the same brand accent so they read like the sidebar's active row.
21
+ *
22
+ * Slots touched: viewport, arrow, label, childLink
23
+ * All other slots inherit Nuxt UI defaults; default colour stays primary (forest accent for a bare menu).
24
+ */
25
+ export const navigationMenuTheme = {
26
+ slots: {
27
+ // Panel: rounded-md matches stock already; motion-timing (duration/ease) retained below.
28
+ viewport: 'duration-(--motion-slow) ease-(--motion-ease-in-out) data-[state=open]:animate-[scale-in_var(--motion-base)_var(--motion-ease-out)] data-[state=closed]:animate-[scale-out_var(--motion-fast)_var(--motion-ease-out)]',
29
+ // Arrow caret rounded-sm — consistent with the viewport
30
+ arrow: 'rounded-sm',
31
+ // Group labels: Forest section-header treatment — uppercase, wider tracking, dimmed weight
32
+ label: 'text-xs uppercase tracking-wider text-dimmed',
33
+ // Child link hover slab: before:rounded-sm (8 px) — concentric inside the 12 px viewport panel.
34
+ childLink: 'before:rounded-sm dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2',
35
+ },
36
+ variants: {
37
+ active: {
38
+ true: {
39
+ // Vertical/flyout child active row → the house accent (stock: before:bg-elevated + text-highlighted).
40
+ childLink: 'before:bg-primary/10 text-primary font-bold',
41
+ },
42
+ },
43
+ },
44
+ compoundVariants: [
45
+ // Top-level active pill: swap the grey slab for the soft primary wash + bold. text-primary already
46
+ // comes from the colour=primary active compound; no highlight key ⇒ applies with highlight on or off.
47
+ {
48
+ variant: 'pill',
49
+ active: true,
50
+ class: { link: 'before:bg-primary/10 font-bold' },
51
+ },
52
+ // link variant carries no slab — just bold the active text (colour stays from the ramp).
53
+ {
54
+ variant: 'link',
55
+ active: true,
56
+ class: { link: 'font-bold' },
57
+ },
58
+ // A VERTICAL menu is a sidebar's nav, and Forest groups it. Stock leaves consecutive sections
59
+ // touching, so the uppercase label alone has to carry the boundary; give it the group's own
60
+ // air instead. The reset is scoped to the first LI, not to `:first-child` — the label is always
61
+ // the first child of ITS li, so `[&:first-child]` would match every group and reset them all.
62
+ {
63
+ orientation: 'vertical',
64
+ collapsed: false,
65
+ class: { label: 'mt-6 [li:first-child>&]:mt-0' },
66
+ },
67
+ // Collapsed to the icon rail. Stock keeps the row full-width, so the active state reads as a
68
+ // wide pill in a narrow column rather than a button; square it and centre it on the rail.
69
+ // The separator is the only thing marking a group once the labels are hidden, so it is kept but
70
+ // shortened — a tick between groups, not a cut across the rail.
71
+ {
72
+ orientation: 'vertical',
73
+ collapsed: true,
74
+ class: {
75
+ link: 'size-8 p-0 justify-center mx-auto',
76
+ separator: 'w-5 mx-auto my-1.5 px-0',
77
+ },
78
+ },
79
+ ],
80
+ } as const
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Reviewed, no delta. UPageAnchors is link-list layout: `text-sm` on the link matches the ladder's
3
+ * small step, and the active `font-semibold` resolves to 700 through the @theme weight remap in
4
+ * styles/forest.css rather than to a cut the brand fonts lack.
5
+ */
6
+
7
+ export const pageAnchorsTheme = {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Reviewed, no delta. The `from-default` scroll-fade gradients read correctly on Forest's surfaces
3
+ * in both modes — `default` is the warm neutral-0 in light and Nuxt's neutral-900 card in dark, which
4
+ * is exactly what the aside sits on. Sticky offsets track --ui-header-height, which Forest owns.
5
+ */
6
+
7
+ export const pageAsideTheme = {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Forest theme fragment for UPageCard.
3
+ *
4
+ * ★ Forest's UCard delta does NOT reach this component — different DOM, different recipe, and the
5
+ * `forest-card` marker that drives the nested-surface and concentric-corner CSS is set by the UCard
6
+ * theme alone. So a UPageCard beside a UCard reads as a different system unless corrected here.
7
+ *
8
+ * Two deltas: the corner moves from `rounded-lg` to the card corner (xl / 24px, per the radius
9
+ * contract), and the title and description come off Tailwind's default scale onto the ladder.
10
+ * `text-[15px]` on the description is an arbitrary value with no token behind it at all.
11
+ *
12
+ * The nesting CSS is deliberately NOT extended to this component. It is coupled to UCard's DOM by
13
+ * design, and a second consumer would make it a shared contract that neither component states.
14
+ */
15
+
16
+ export const pageCardTheme = {
17
+ slots: {
18
+ root: 'rounded-xl',
19
+ title: 'type-heading-xs',
20
+ description: 'type-body',
21
+ },
22
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Forest theme fragment for UPageCTA (ui key `pageCta`).
3
+ *
4
+ * Type only. Note what is NOT changed: `rounded-xl` on the root is already on-contract — the
5
+ * rounded-* utilities inline calc(var(--ui-radius) * N), so xl resolves to Forest's 24px card corner
6
+ * rather than a Tailwind literal. It looked like a bypass and is not.
7
+ *
8
+ * ★ It caps its own width (embedded UContainer), which matters most here: a CTA normally sits inside
9
+ * page content already, so nesting it in a well double-applies the gutters. Registered on
10
+ * /layout/well.
11
+ */
12
+
13
+ export const pageCtaTheme = {
14
+ slots: {
15
+ title: 'type-headline',
16
+ description: 'type-body',
17
+ },
18
+ };