@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,127 @@
1
+ /**
2
+ * Forest theme fragment — USidebar (the persistent navigation shell).
3
+ *
4
+ * Deltas over stock (the preset APPENDS to Nuxt UI's recipe):
5
+ * - `title`: font-display renders the shell heading in MohrAlt (Black, uppercase) — the same
6
+ * treatment UModal and USlideover give their titles. It also closes a breakpoint seam: below
7
+ * lg the sidebar forwards `title` to its mobile menu (a USlideover by default), whose own
8
+ * fragment already stamps font-display, so without this the same string changed face at
9
+ * 1024px.
10
+ * Not cap-trimmed. Mohr's cap band is centred in its own box by the @font-face metric overrides
11
+ * in packages/fonts/forest-fonts.css, so a heading needs no box-level treatment.
12
+ * - `variants.variant.floating.inner`: `bg-default shadow-none`. Two changes to one slot.
13
+ * Stock lifts the floating panel with a `shadow-lg` — Forest surfaces separate by ring and
14
+ * tint, not by drop shadow. And stock gives the panel no background at all, so it showed the
15
+ * shell bed through its own ring; `bg-default` makes it the same surface as the content panel
16
+ * beside it, which is what "two floating parts on one bed" has to mean to read.
17
+ * ★ The variant lifts the COLUMN only. The content panel beside it is a usage-site concern,
18
+ * and its START margin must follow `open` — while the column is out, the gap on the panel's
19
+ * leading edge is the column's own p-4, and nothing supplies it once the column slides away.
20
+ * ★ FLOATING IS OPT-IN, and the default is stock's flush `sidebar`. This fragment used to flip
21
+ * that, on the reasoning that Forest shells float their parts on a muted bed — but the shell's
22
+ * float comes from dashboardPanel and dashboardSidebar, which are different ui keys and never
23
+ * read this one. The population a default here actually governs is a bare `<USidebar>`, and the
24
+ * app rail is not in it: FAppSidebar wraps UDashboardSidebar. What is left is the static column
25
+ * — inside a modal, a settings pane, a card — where floating is usually wrong twice over: it
26
+ * draws a card inside a card, and its `collapsible="none"` compound puts `p-4` on the root,
27
+ * which is border-box and `w-(--sidebar-width)`, so the padding comes out of the column's own
28
+ * width. Floating is a claim about what lies UNDERNEATH the column, and only the usage site can
29
+ * see that. FSidePanel passes it explicitly for its overlay cut; a shell column should too.
30
+ * - `variants.collapsible.icon`: the collapsed rail halves its side padding across header, body and
31
+ * footer, and centres the two regions that hold a single control. Mirrors dashboardSidebar, which
32
+ * carries the same pair — see the note on the variant.
33
+ * - `variants.transition.true`: the collapse/expand timing moves onto the shared motion tokens —
34
+ * --motion-slow / --motion-ease-in-out on the width travel, the near-element pair on the rail.
35
+ *
36
+ * Slots touched by the transition variant: gap, container, rail.
37
+ *
38
+ * Everything else is stock and already rides the Forest ramps:
39
+ * - Every colour and border in the recipe is a semantic token (divide-default, text-highlighted,
40
+ * --ui-border-accented on the rail, --ui-header-height on the header).
41
+ * - The width vars (--sidebar-width 16rem, --sidebar-width-icon 4rem) and the
42
+ * side × collapsible × variant matrix are structure, set at the usage site.
43
+ * - The docs shell's inset look is the `variant="inset"` prop at the usage site, not a theme
44
+ * concern.
45
+ *
46
+ * Slots touched: title, description, header/body/footer under the icon collapse, and inner under the
47
+ * floating variant.
48
+ * Stock recipe: apps/docs/node_modules/.nuxt-ui/ui/sidebar.ts
49
+ */
50
+
51
+ export const sidebarTheme = {
52
+ slots: {
53
+ title: 'font-display',
54
+ // Stock gives USidebar no gap between its title and description, where it gives UCard's
55
+ // description an `mt-1`. Same idiom here rather than a `space-y-*` on the wrapper: the gap belongs
56
+ // to the description in both components, so the two read the same way when someone compares them.
57
+ description: 'mt-1',
58
+ },
59
+ variants: {
60
+ // ★ The collapsed rail halves its side padding in EVERY region, because stock's leaves its own
61
+ // controls no room. Stock keeps `p-4` at any width, which spends 32px of a rail that is 56–64px
62
+ // wide; the nav item is `size-8` (32px) and a footer button's own `px-3.5` makes it 28px at
63
+ // minimum. At stock's default 4rem the nav item fits with exactly zero slack, and at any narrower
64
+ // rail everything overflows — measured at 3.5rem: nav icons 4.5px off centre and hanging past the
65
+ // trailing edge, the footer's collapse button 2.5px off.
66
+ //
67
+ // ★ AND CENTRING CANNOT FIX IT. `mx-auto` and `justify-center` distribute LEFTOVER space, so they
68
+ // do nothing once the item is wider than the box it sits in — the item overflows and the centring
69
+ // silently stops applying. The box has to be wide enough first; that is what this is.
70
+ //
71
+ // Halved rather than zeroed: at `px-0` the controls would centre just as well, but a ghost
72
+ // button's hover fill would run edge to edge across the rail. 8px keeps the fill inset and still
73
+ // clears a 32px control at 3.5rem, with room to spare at stock's 4rem.
74
+ //
75
+ // ★ `justify-center` on the two REGIONS that hold a single control, because room alone only
76
+ // centres a child that stretches to fill it. The body's rows carry their own `mx-auto`, but a
77
+ // header's mark and a footer's avatar are content-width in a flex row that stays `flex-start` —
78
+ // measured, a 28px avatar sat 5.5px off the line every row below it shares. This mirrors
79
+ // themes/dashboard-sidebar.ts, which has carried the same pair since the docs rail hit it; the
80
+ // two components are separate ui keys, so the fix does not travel on its own.
81
+ collapsible: {
82
+ icon: {
83
+ header: 'group-data-[state=collapsed]/sidebar:px-2 group-data-[state=collapsed]/sidebar:justify-center',
84
+ body: 'group-data-[state=collapsed]/sidebar:px-2',
85
+ footer: 'group-data-[state=collapsed]/sidebar:px-2 group-data-[state=collapsed]/sidebar:justify-center',
86
+ },
87
+ },
88
+ // Collapse / expand is the largest on-screen travel the shell performs, and it both starts and
89
+ // ends in view — the exact case the doctrine reserves --motion-slow + --motion-ease-in-out for
90
+ // (it names the nav viewport resize as the precedent). Stock runs a flat duration-200 on the
91
+ // built-in `ease-out`, which the doctrine calls too weak to read as intentional. The rail is a
92
+ // thin hover affordance, not travel, so it takes the near-element pair instead.
93
+ transition: {
94
+ true: {
95
+ gap: 'duration-(--motion-slow) ease-(--motion-ease-in-out)',
96
+ container: 'duration-(--motion-slow) ease-(--motion-ease-in-out)',
97
+ rail: 'duration-(--motion-fast) ease-(--motion-ease-out)',
98
+ },
99
+ },
100
+ variant: {
101
+ floating: {
102
+ inner: 'bg-default shadow-none',
103
+ },
104
+ },
105
+ },
106
+ // Kills the hairline stock leaves hanging in the gutter of a floating sidebar.
107
+ //
108
+ // Stock MEANT to remove it — `variant.floating.container` carries `border-transparent` against
109
+ // the `border-e border-default` that `side.left` sets. But tv applies the `side` variant AFTER
110
+ // `variant`, so tailwind-merge resolves the pair the wrong way round and `border-default` is the
111
+ // one that survives (measured: the container ships `p-4 … border-e border-default`, no
112
+ // `border-transparent` in sight). The result is a 1px line in the gap between the floating
113
+ // column and the content panel, running the container's full height — so it overshoots both
114
+ // rounded cards by the 16px of container padding and reads as a stray rule rather than an edge.
115
+ //
116
+ // Restating `border-transparent` at the variant path would lose the same race. compoundVariants
117
+ // are applied after all variants, so this is the seam that actually wins. Colour, not width:
118
+ // stock keeps the 1px so the floating and flush cuts measure the same.
119
+ compoundVariants: [
120
+ {
121
+ variant: 'floating',
122
+ class: {
123
+ container: 'border-transparent',
124
+ },
125
+ },
126
+ ],
127
+ } as const
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Forest theme fragment — USkeleton (loading placeholder).
3
+ *
4
+ * Delta only (single-element component, one `base`): `rounded-sm` over stock's `rounded-md`. The
5
+ * base radius is mostly the TEXT-LINE / generic-box look (avatars and pills override to rounded-full
6
+ * per instance, card images to rounded-xl), so it stays tight — rounded-sm reads as a clean bar, not
7
+ * a pill, and matches Forest's item-slab radius tier. The fill stays stock `bg-elevated`: measured
8
+ * against the page/card surface it has ~2× the contrast of bg-muted (which is nearly invisible on
9
+ * bg-default), so the placeholder actually reads. `animate-pulse` and bg-elevated both carry through
10
+ * from stock — the preset appends, nothing restated.
11
+ */
12
+
13
+ export const skeletonTheme = {
14
+ base: 'rounded-sm',
15
+ } as const;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Forest theme fragment — USlideover (edge side panel).
3
+ *
4
+ * Deltas over stock (the preset appends to Nuxt UI's recipe):
5
+ * - `inset` defaults to TRUE, in every context — standard, mobile and marketing. The panel floats
6
+ * 1 rem off each viewport edge instead of welding itself to it, so it reads as a surface sitting
7
+ * on the app rather than a second chrome, and the corners it earns put it in the same shape
8
+ * family as the card and the drawer. Authors opt back out with `:inset="false"` for the flush cut.
9
+ * - `inset` radius: xl / 24px (stock rounded-lg / 16px). A floating slideover is a full-surface
10
+ * panel, so it takes the card step. Set on the `inset` VARIANT, not the base `content` slot —
11
+ * stock puts its radius on that same variant, so a base override loses the tailwind-merge race
12
+ * (the per-variant seam — see the modal and badge fragments).
13
+ * - `content`: shadow-xl lifts the panel one step above stock's sm:shadow-lg.
14
+ * - `title`: font-display renders the heading in MohrAlt (uppercase, black weight) —
15
+ * the same heading treatment as every other Forest panel/modal.
16
+ * - `header` / `footer`: matching border separators (stock leaves both borderless).
17
+ * - `transition`: the panel slide and the overlay fade move onto the shared motion tokens. A
18
+ * slideover is the longest travel in the overlay family — a full panel crossing in from off
19
+ * screen — so it enters on --motion-slow and leaves on --motion-fast. Both directions ease-out:
20
+ * stock's `ease-in-out` belongs to movement that starts and ends on screen (the tabs indicator,
21
+ * a nav resize), not to something arriving from outside the viewport. The overlay exit also drops
22
+ * stock's banned `ease-in`. The per-side keyframes live in compoundVariants because that's where
23
+ * stock puts them; a variant-level override would never reach them.
24
+ *
25
+ * Everything else — side offsets, inset offsets, dividers — is stock.
26
+ * Slots touched: content, title, header, footer, overlay.
27
+ * Stock recipe: apps/docs/node_modules/.nuxt-ui/ui/slideover.ts
28
+ */
29
+
30
+ export const slideoverTheme = {
31
+ slots: {
32
+ content: 'shadow-xl',
33
+ title: 'font-display',
34
+ header: 'border-b border-default',
35
+ footer: 'border-t border-default',
36
+ },
37
+ variants: {
38
+ inset: {
39
+ true: { content: 'rounded-xl' },
40
+ },
41
+ transition: {
42
+ true: {
43
+ overlay:
44
+ '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)]',
45
+ },
46
+ },
47
+ },
48
+ // Panel slide, one entry per side — same keyframes as stock, retimed onto the tokens. Compounds
49
+ // apply last, so these win the tailwind-merge over stock's flat 200ms ease-in-out pair.
50
+ compoundVariants: [
51
+ {
52
+ transition: true,
53
+ side: 'top',
54
+ class: {
55
+ content:
56
+ 'data-[state=open]:animate-[slide-in-from-top_var(--motion-slow)_var(--motion-ease-out)] data-[state=closed]:animate-[slide-out-to-top_var(--motion-fast)_var(--motion-ease-out)]',
57
+ },
58
+ },
59
+ {
60
+ transition: true,
61
+ side: 'right',
62
+ class: {
63
+ content:
64
+ 'data-[state=open]:animate-[slide-in-from-right_var(--motion-slow)_var(--motion-ease-out)] data-[state=closed]:animate-[slide-out-to-right_var(--motion-fast)_var(--motion-ease-out)]',
65
+ },
66
+ },
67
+ {
68
+ transition: true,
69
+ side: 'bottom',
70
+ class: {
71
+ content:
72
+ 'data-[state=open]:animate-[slide-in-from-bottom_var(--motion-slow)_var(--motion-ease-out)] data-[state=closed]:animate-[slide-out-to-bottom_var(--motion-fast)_var(--motion-ease-out)]',
73
+ },
74
+ },
75
+ {
76
+ transition: true,
77
+ side: 'left',
78
+ class: {
79
+ content:
80
+ 'data-[state=open]:animate-[slide-in-from-left_var(--motion-slow)_var(--motion-ease-out)] data-[state=closed]:animate-[slide-out-to-left_var(--motion-fast)_var(--motion-ease-out)]',
81
+ },
82
+ },
83
+ ],
84
+ defaultVariants: {
85
+ inset: true,
86
+ },
87
+ } as const;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Forest theme fragment — USlider (range slider, single / multi thumb).
3
+ *
4
+ * Forest opinions applied:
5
+ *
6
+ * thumb — Forest's soft-radius vocabulary uses `rounded-full` (stock already does this — no
7
+ * change there). We add `shadow-sm` for a subtle lift that reinforces the tactile brand feel
8
+ * (consistent with the keycap button metaphor), and `transition-[box-shadow,ring-color]` so the
9
+ * thumb reacts smoothly when it enters the focused / hovered state. We also add
10
+ * `cursor-grab active:cursor-grabbing` so the drag affordance is immediately clear on pointer
11
+ * devices — the stock cursor is the default arrow.
12
+ *
13
+ * Slots touched: `thumb` (resting look + coarse size-up).
14
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/slider.ts
15
+ *
16
+ * We do NOT override:
17
+ * - Colour variants — `bg-primary`, `ring-primary`, `focus-visible:outline-primary/50` etc. are
18
+ * already wired to the Forest OKLCH ramps via docs.css / forest-preset.ts.
19
+ * - Track height ladder — the xs–xl per-size groove height is left stock (only the thumb takes the
20
+ * coarse touch step-up below).
21
+ * - `track` — stock `bg-accented` groove is the shape; the coloured range is the signal. No
22
+ * border token reads consistently against the track in both modes, so no ring.
23
+ * - `range` slot — `rounded-full` + colour is correct; no additional opinion needed.
24
+ * - `root` slot — layout is correct for both orientations.
25
+ * - `disabled` variant — stock `opacity-75 cursor-not-allowed` is appropriate.
26
+ */
27
+
28
+ export const sliderTheme = {
29
+ slots: {
30
+ // Thumb: shadow lift for tactile feel; grab cursor; smooth transition on focus/hover.
31
+ thumb: 'focus-visible:outline-2 shadow-sm cursor-grab active:cursor-grabbing transition-[box-shadow,ring-color] duration-(--motion-fast)',
32
+ },
33
+ // Touch: the thumb is the whole drag target and it is small (md 16px). Under a coarse (finger)
34
+ // pointer the thumb grows one size up — a bigger, easier-to-grab handle — the same proportional
35
+ // step-up the radio circle takes. Only the THUMB bumps: the track is a thin visual groove the thumb
36
+ // overhangs, not a hit surface, so its stock per-size height is left alone. xl is the ceiling; each
37
+ // size lists only its delta up. `coarse:` carries the `:not(.cut-fixed)` escape so the mobile cut
38
+ // (baked size lg) opts out and holds instead of stepping again.
39
+ variants: {
40
+ size: {
41
+ xs: { thumb: 'coarse:size-3.5' },
42
+ sm: { thumb: 'coarse:size-4' },
43
+ md: { thumb: 'coarse:size-4.5' },
44
+ lg: { thumb: 'coarse:size-5' },
45
+ // xl: ceiling
46
+ },
47
+ },
48
+ } as const;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Forest theme fragment — UStepper (multi-step progress).
3
+ *
4
+ * Forest opinions applied:
5
+ *
6
+ * title — lifted to `font-bold` (stock: `font-medium`) so step labels hold hierarchy over
7
+ * the description text. Matches Forest's general heading-density opinion: bold labels, muted
8
+ * descriptions.
9
+ *
10
+ * separator — kept `rounded-full` (stock). Forest opinion: a subtle `opacity-60` on the base
11
+ * so pending separators recede without disappearing; completed separators override via the
12
+ * colour variant's `group-data-[state=completed]:bg-{color}` which restores full opacity.
13
+ *
14
+ * Slots touched: `trigger`, `title`, `separator`.
15
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/stepper.ts
16
+ *
17
+ * We do NOT override:
18
+ * - `root`, `header`, `item`, `container`, `wrapper`, `content` — stock layout is correct for
19
+ * both horizontal and vertical orientations; no Forest density opinion required.
20
+ * - `indicator`, `icon` — stock flex/size utilities are correct.
21
+ * - `description` — `text-muted text-wrap` is the right voice; no override needed.
22
+ * - Colour variants — active/completed bg and focus-visible:outline already resolve to the
23
+ * Forest OKLCH ramps via the Nuxt UI colour-alias bridge in docs.css.
24
+ * - Size compound variants — the xs–xl trigger/text ladder is suitable; no Forest density
25
+ * adjustment needed.
26
+ */
27
+
28
+ export const stepperTheme = {
29
+ slots: {
30
+ // Pending trigger: stock shape and state classes; bg-elevated carries the pending fill.
31
+ trigger:
32
+ 'focus-visible:outline-2 focus-visible:outline-offset-2',
33
+
34
+ // Step label: font-bold (stock: font-medium) — Forest heading-density opinion.
35
+ title: 'font-bold',
36
+
37
+ // Pending separator: opacity-60 so incomplete connectors recede; completed/active state
38
+ // overrides via colour variant class (group-data-[state=completed]:bg-{color}).
39
+ separator: 'opacity-60 group-data-[state=completed]:opacity-100',
40
+ },
41
+ } as const;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Forest theme fragment — USwitch.
3
+ *
4
+ * Forest opinions applied:
5
+ * - `base` (the track): the stock component is already `rounded-full` — that's correct for a
6
+ * toggle track and we preserve it. The Forest opinion here is on the focus ring: we pin
7
+ * `focus-visible:outline-[var(--forest-500)]` so that regardless of which semantic colour the
8
+ * switch is rendered in, the keyboard ring is the brand green (consistent with button / checkbox
9
+ * behaviour). The stock outline class (`focus-visible:outline-primary` etc.) is colour-keyed and
10
+ * resolves correctly via the colour-alias bridge, so for `color="primary"` it already renders
11
+ * forest-500. For the other colours (error, warning, neutral…) the focus ring follows that
12
+ * colour — a deliberate product choice to deviate: neutral's outline-inverted is near-black in
13
+ * light mode and hard to distinguish from the track; pinning brand-500 is safer there.
14
+ * NOTE: the base focus-visible outline is OVERRIDDEN per-colour via the `color` variant entries
15
+ * (see switch.ts generated theme). We patch only the default — callers can still pass
16
+ * `ui="{ base: 'focus-visible:outline-forest' }"` to pin it explicitly.
17
+ * - `thumb`: `shadow-md` replaces the stock `shadow-lg` — the Forest surface vocabulary is
18
+ * tactile but not deeply raised. A softer shadow keeps the thumb readable without adding
19
+ * skeuomorphic depth.
20
+ * - `label`: `font-medium` is already stock; we add nothing — the Nuxt UI default is correct.
21
+ * - `description`: `text-muted` is already stock.
22
+ *
23
+ * Under a coarse (touch) pointer the switch grows one size up — a genuine tap target — via the size
24
+ * variant below; the mobile cut bakes size lg and opts out.
25
+ *
26
+ * KNOWN LIMIT: the thumb's ON position is a discrete `data-[state=checked]:translate-x-*`, and a
27
+ * stacked `coarse:data-[state=checked]:translate-x-*` loses the cascade to stock's same-property rule
28
+ * (both emit; stock wins on source order), so under the ADAPTIVE coarse bump the thumb undershoots
29
+ * the enlarged track by ~2px. Accepted as a minor cosmetic tradeoff. The MOBILE bake is unaffected —
30
+ * a real `size` prop applies the whole lg variant (track + translate together) so its thumb is flush.
31
+ *
32
+ * Slots touched: base, thumb, and the size variant (base/container/thumb/wrapper) for the coarse bump.
33
+ * Slot names sourced from:
34
+ * apps/docs/node_modules/.nuxt-ui/ui/switch.ts
35
+ */
36
+
37
+ export const switchTheme = {
38
+ slots: {
39
+ // Softer focus ring anchored to the brand green regardless of switch colour intent.
40
+ // shadow-sm on the thumb: Forest tactile but not skeuomorphic.
41
+ base: 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--forest-500)] duration-(--motion-base)',
42
+ thumb: 'shadow-md duration-(--motion-base) ease-(--motion-ease-out)',
43
+ },
44
+ // Touch: a switch's whole control IS the tap target (unlike a checkbox, whose target is the label
45
+ // row), so under a coarse (finger) primary pointer each size grows one size up — the full size
46
+ // bundle: track width (base), track height (container), thumb size + its checked translate, and the
47
+ // label text (wrapper). The thumb's `data-[state=checked]:translate-x-*` must bump in lockstep with
48
+ // the track width or the thumb would overshoot/undershoot the ON position. xl is the ceiling.
49
+ // `coarse:` carries the `:not(.cut-fixed)` escape so the mobile cut (baked size lg) opts out.
50
+ variants: {
51
+ size: {
52
+ // → sm: wider track + bigger thumb (height/text unchanged)
53
+ xs: { base: 'coarse:w-8', thumb: 'coarse:size-3.5 coarse:data-[state=checked]:translate-x-3.5' },
54
+ // → md: wider track, taller, bigger thumb, larger text
55
+ sm: { base: 'coarse:w-9', container: 'coarse:h-5', thumb: 'coarse:size-4 coarse:data-[state=checked]:translate-x-4', wrapper: 'coarse:text-sm' },
56
+ // → lg: wider track + bigger thumb (height/text unchanged)
57
+ md: { base: 'coarse:w-10', thumb: 'coarse:size-4.5 coarse:data-[state=checked]:translate-x-4.5' },
58
+ // → xl: wider track, taller, bigger thumb, larger text
59
+ lg: { base: 'coarse:w-11', container: 'coarse:h-6', thumb: 'coarse:size-5 coarse:data-[state=checked]:translate-x-5', wrapper: 'coarse:text-base' },
60
+ // xl: ceiling — no bump
61
+ },
62
+ },
63
+ } as const;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Forest theme fragment — UTable (data table, sticky header, loading bar).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `th`: font-semibold is already stock — we bump it to `font-bold` so column headings read with
6
+ * the same confident weight as the rest of the Forest type system, and switch the text role from
7
+ * `text-highlighted` to `text-default` to sit one step quieter than page headings while still
8
+ * reading clearly. Slight uppercase + tracking gives the table header a structured ops-dashboard
9
+ * feel consistent with the Forest label vocabulary.
10
+ * - `td`: `text-default` instead of the stock `text-muted` — data cells carry primary content and
11
+ * should read at full default weight rather than the slightly dimmed muted role.
12
+ * - `tbody`: the selectable-row hover bg is already `bg-elevated/50`; no changes needed.
13
+ * - `empty`: align with `text-dimmed` (one step quieter than stock `text-muted`) to visually
14
+ * distinguish placeholder copy from real cell content.
15
+ *
16
+ * Slots touched: th, td, empty.
17
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/table.ts
18
+ *
19
+ * The loading bar colour is driven by the `loadingColor` compoundVariants in the generated theme —
20
+ * no override needed here; `primary` defaults to the forest ramp via the semantic colour wiring
21
+ * in docs.css / forest-preset.ts.
22
+ */
23
+
24
+ export const tableTheme = {
25
+ slots: {
26
+ // font-bold + uppercase tracking — gives column headings a confident, ops-dashboard weight.
27
+ // text-default sits one step quieter than text-highlighted (stock) without losing legibility.
28
+ th: 'text-xs font-bold uppercase tracking-wider text-default',
29
+ // text-default — data is primary content; stock text-muted reads too low for ops data cells.
30
+ td: 'text-default',
31
+ // text-dimmed — visually distinct from real data; subtler than stock text-muted placeholder.
32
+ empty: 'text-dimmed',
33
+ },
34
+ } as const;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Forest theme fragment — UTabs (tabbed panels). Deltas only; everything else is inherited from
3
+ * stock Nuxt UI.
4
+ *
5
+ * Forest opinions applied:
6
+ * - `trigger`: font-bold (stock font-medium) — one stable weight in every state, so the active tab
7
+ * is signalled by the coloured indicator alone, never a weight shift (which would relayout).
8
+ * Under a coarse (touch) pointer the trigger grows one size up per the size-variant ladder below.
9
+ * - `indicator`: the Forest motion tokens (stock is a flat duration-200).
10
+ * - Pill radius (compoundVariants): a CONCENTRIC per-size ladder — the sliding indicator sits on
11
+ * the input field's radius (xs/sm 8px, md/lg/xl 12px) and the tray rounds one step past it, so
12
+ * the pill nests as outer = inner + the 4px pad. Same relationship stock uses; only the small
13
+ * sizes are new. sm promotes to md's pair under coarse.
14
+ *
15
+ * Slots touched: trigger, indicator, list (pill radius + pill tray bg/border).
16
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/tabs.ts
17
+ */
18
+
19
+ export const tabsTheme = {
20
+ slots: {
21
+ // indicator — Forest motion tokens over stock's flat duration-200 (applies to both variants).
22
+ // Stock's `absolute transition-[translate,width]` is inherited. Radius is NOT set here — it's
23
+ // per-size and pill-scoped, so it lives in compoundVariants below (a base value would lose the
24
+ // ordering race to stock's `variant.pill` radius anyway).
25
+ indicator: 'duration-(--motion-base) ease-(--motion-ease-in-out)',
26
+ // trigger — font-bold over stock's font-medium: one stable weight in every state (legible at
27
+ // xs–sm, no active/inactive layout shift; colour carries the hierarchy, not weight). Stock's
28
+ // layout/state/rounded/transition classes are inherited, not restated.
29
+ trigger: 'font-bold',
30
+ },
31
+ // Touch: tab triggers are tap targets, so under a coarse (finger) primary pointer each size grows
32
+ // one size UP — the full trigger geometry (padding, gap, text, icon), same proportional step as the
33
+ // input field. Tabs is simpler than input here: no responsive `md:text-*` shrink to fight, and the
34
+ // leading icon is gap-spaced (no absolute `ps-9`), so horizontal `px` is safe to bump too — the step
35
+ // is the true next-size trigger. xl is the ceiling. Each size lists only its deltas up. `coarse:`
36
+ // carries the `:not(.cut-fixed)` escape, so the mobile cut (which bakes size lg) opts out below.
37
+ variants: {
38
+ size: {
39
+ // → sm: wider padding + gap (text/icon same)
40
+ xs: { trigger: 'coarse:px-2.5 coarse:py-1.5 coarse:gap-1.5' },
41
+ // → md: wider padding, larger text + icon (py/gap same)
42
+ sm: { trigger: 'coarse:px-3 coarse:text-sm', leadingIcon: 'coarse:size-5' },
43
+ // → lg: taller padding + gap (px/text/icon same)
44
+ md: { trigger: 'coarse:py-2 coarse:gap-2' },
45
+ // → xl: larger text + icon (padding/gap same)
46
+ lg: { trigger: 'coarse:text-base', leadingIcon: 'coarse:size-6' },
47
+ // xl: ceiling — no bump
48
+ },
49
+ },
50
+ // Pill radius is CONCENTRIC, the way stock builds it: the tray (`list`) has a p-1 (4px) inset, so
51
+ // the sliding `indicator` (the inner, filled pill) rounds one step TIGHTER than the tray — outer =
52
+ // inner + padding — and the active pill nests cleanly in the tray corner. The inner value is the
53
+ // input field's per-size ladder (xs/sm rounded-sm 8px, md/lg/xl rounded-md 12px), so the active tab
54
+ // reads at the same radius as a field; the tray is one step up (12 / 16). (md/lg/xl land back on
55
+ // stock's own values — tray rounded-lg, indicator rounded-md; only xs/sm add the smaller concentric
56
+ // pair.) Under coarse, sm promotes to md's pair (indicator 8→12, tray 12→16), tracking its size-up.
57
+ //
58
+ // Why compoundVariants: the radius must (a) beat stock's `variant.pill` values — compounds apply
59
+ // last, so they win — and (b) apply ONLY to the pill. A size-variant `indicator` would also hit the
60
+ // LINK variant and clobber its rounded-full underline ends; scoping to `variant: 'pill'` leaves link
61
+ // alone.
62
+ compoundVariants: [
63
+ // Pill tray as an outlined field, not a raised grey slab: bg-default (the base neutral-0 surface,
64
+ // mode-aware) over stock's bg-elevated, plus a 1px inset accented ring for the border. Scoped to
65
+ // `variant: 'pill'` (no size) so it hits every pill size and leaves LINK untouched; a compound
66
+ // applies last, so bg-default wins the tailwind-merge over the base variant's bg-elevated.
67
+ { variant: 'pill', class: { list: 'bg-default ring ring-inset ring-accented' } },
68
+ { variant: 'pill', size: 'xs', class: { list: 'rounded-md', indicator: 'rounded-sm' } },
69
+ { variant: 'pill', size: 'sm', class: { list: 'rounded-md coarse:rounded-lg', indicator: 'rounded-sm coarse:rounded-md' } },
70
+ { variant: 'pill', size: 'md', class: { list: 'rounded-lg', indicator: 'rounded-md' } },
71
+ { variant: 'pill', size: 'lg', class: { list: 'rounded-lg', indicator: 'rounded-md' } },
72
+ { variant: 'pill', size: 'xl', class: { list: 'rounded-lg', indicator: 'rounded-md' } },
73
+ ],
74
+ } as const;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Forest theme fragment — UTextarea (multiline input, autoresize).
3
+ *
4
+ * Textarea is a multiline UInput, so it wears the SAME Forest identity — this fragment mirrors
5
+ * themes/input.ts: the recessed depth line, the colour-tracking focus halo, the per-size radius
6
+ * ladder, and the touch ladder. Stock UTextarea shares UInput's size architecture verbatim
7
+ * (same px/py/text/gap/icon per size, same responsive `md:text-*` shrink), so the deltas are the same.
8
+ * Keeping the two in lockstep is deliberate — a note field and a single-line field on the same form
9
+ * must read as one control family. See input.ts for the full rationale on each opinion; the notes
10
+ * here are the textarea-specific deltas only.
11
+ *
12
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/textarea.ts
13
+ */
14
+
15
+ export const textareaTheme = {
16
+ slots: {
17
+ // Same base as UInput: the solid inset depth line at rest (stacks UNDER the ring via separate
18
+ // shadow vars), a box-shadow transition so focus eases, and the depth line + soft halo on focus.
19
+ // The old `focus:outline-none` is dropped ON PURPOSE — it was suppressing the very focus outline
20
+ // UInput keeps, so the two fields disagreed on focus; parity restores the shared treatment.
21
+ 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-[inset_0_var(--forest-input-depth)_0_0_var(--forest-input-recess),0_0_0_3px_var(--forest-input-halo,var(--forest-halo-primary))]',
22
+ },
23
+ // Touch ladder + per-size radius/depth — identical to input.ts. Under a coarse primary
24
+ // pointer each size grows PROPORTIONALLY to the next size up (height via py or type, gap, radius,
25
+ // icon, recess), reading as a bigger version of itself. Two things it does NOT bump: horizontal
26
+ // `px` (a `touch:px-*` emitted after the icon's inline padding would clobber it) and the naive
27
+ // base type (stock's responsive `md:text-*` shrink means the type bump must target the next size's
28
+ // EFFECTIVE value — sm→`text-sm`, lg→`text-base` — or the ladder inverts). sm also rounds up: its
29
+ // type-driven height gain earns the rounded-md step, same as md→lg. xl has a touch rung of
30
+ // its own (52). See input.ts.
31
+ // PER-SIZE RADIUS: xs/sm rounded-sm (8px); md/lg/xl keep stock's base rounded-md (12px) — only
32
+ // xs/sm are overridden, md+ not restated. Depth line scales per size.
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
+ variants: {
37
+ size: {
38
+ // touch 28: taller padding + gap
39
+ xs: { base: 'rounded-sm [--forest-input-depth:2px] touch:min-h-7 touch:gap-1.5' },
40
+ // touch 36: larger type (md's effective 14/20) + icon + deeper recess, and rounder (type-driven
41
+ // height gain earns the rounded-md step)
42
+ 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' },
43
+ // touch 40: taller padding + gap (radius inherits stock base rounded-md — not restated)
44
+ md: { base: '[--forest-input-depth:3px] touch:min-h-10 touch:text-sm touch:gap-2' },
45
+ // touch 44: larger type (xl's 16/24) + icon + deepest recess
46
+ 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' },
47
+ // touch 52
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' },
49
+ },
50
+ // Focus halo tracks `color` — FLAT strings (stock's colour variant is flat; a `{ base: … }` shape
51
+ // would be dropped by the merge). Each colour re-points --forest-input-halo at its own 200 step.
52
+ color: {
53
+ primary: '[--forest-input-halo:var(--forest-halo-primary)]',
54
+ secondary: '[--forest-input-halo:var(--forest-halo-secondary)]',
55
+ success: '[--forest-input-halo:var(--forest-halo-success)]',
56
+ info: '[--forest-input-halo:var(--forest-halo-info)]',
57
+ warning: '[--forest-input-halo:var(--forest-halo-warning)]',
58
+ error: '[--forest-input-halo:var(--forest-halo-error)]',
59
+ forest: '[--forest-input-halo:var(--forest-halo-forest)]',
60
+ maple: '[--forest-input-halo:var(--forest-halo-maple)]',
61
+ river: '[--forest-input-halo:var(--forest-halo-river)]',
62
+ warm: '[--forest-input-halo:var(--forest-halo-warm)]',
63
+ acid: '[--forest-input-halo:var(--forest-halo-acid)]',
64
+ neutral: '[--forest-input-halo:var(--forest-halo-neutral)]',
65
+ },
66
+ },
67
+ // Ghost drops the resting depth line (no fill/ring = nothing to recess into); it takes the line on
68
+ // hover/focus with the elevated fill. Same as input.
69
+ compoundVariants: [
70
+ {
71
+ variant: 'ghost',
72
+ class: {
73
+ base: 'shadow-none hover:shadow-[inset_0_calc(var(--forest-input-depth)_+_var(--forest-field-edge,0px))_0_0_var(--forest-input-recess)]',
74
+ },
75
+ },
76
+ ],
77
+ } as const;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Forest theme fragment — UTimeline (vertical / horizontal event track).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `title`: steps up from stock `font-medium` to `font-bold` — gives the event name a
6
+ * clear hierarchy above the dimmed date and muted description without reaching for a display
7
+ * heading or extra font-size.
8
+ * - `date`: kept minimal; `text-xs/5` and `text-dimmed` are already stock, no opinion needed.
9
+ * - `description`: stock `text-muted text-sm` is correct — no override required.
10
+ * - `separator`: stock `rounded-full` matches Forest's pill language (chips, badges); no
11
+ * override needed. Background colours are handled by the colour + reverse compound variants
12
+ * already in the generated theme.
13
+ * - `indicator`: stock `text-muted` rest state is intentionally kept — the indicator picks up
14
+ * brand colour from the `color` variant when `state="active"` or `state="completed"`. No
15
+ * override needed here.
16
+ *
17
+ * Slots touched: title.
18
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/timeline.ts
19
+ */
20
+
21
+ export const timelineTheme = {
22
+ slots: {
23
+ title: 'font-bold',
24
+ },
25
+ } as const
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Forest theme fragment — UToast (transient notification).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `root`: radius bumped to rounded-xl (matches the Forest card/alert language — 16 px, softer
6
+ * than the stock rounded-lg); the gap is comfortable at gap-3 for breathing room between
7
+ * icon/content/actions. Shadow, ring, and padding are unchanged from stock.
8
+ * - `title`: steps up from stock `font-medium` to `font-bold` — gives the notification
9
+ * headline a clear hierarchy above the description, matching the alert/banner convention.
10
+ * - `description`: text-sm text-muted is unchanged — the muted colour already reads as
11
+ * secondary copy, matching the Forest hierarchy. No override needed.
12
+ *
13
+ * Deliberately untouched (stock is already correct for Forest):
14
+ * - `wrapper`, `icon`, `avatar`, `actions`, `progress`, `close` — stock layout and sizing are
15
+ * appropriate; brand colour intent flows in automatically through the semantic colour map.
16
+ * - All `color` variants — the focus-visible ring and icon colour already resolve through the
17
+ * semantic wiring (primary→forest, error→maple, info→river, warning→amber, secondary→warm).
18
+ * - Orientation variants — stock horizontal/vertical layout is correct as-is.
19
+ *
20
+ * Slots touched: root, title.
21
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/toast.ts
22
+ */
23
+
24
+ export const toastTheme = {
25
+ slots: {
26
+ root: 'rounded-xl gap-3 focus:outline-none',
27
+ title: 'font-bold',
28
+ },
29
+ } as const;