@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,43 @@
1
+ /**
2
+ * Forest theme fragment — UEmpty (empty-state placeholder).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `root`: radius bumped to rounded-2xl for the spacious, card-like Forest feel; padding
6
+ * stepped up to p-8 sm:p-10 lg:p-12 to give the empty state breathing room — it is the
7
+ * centrepiece of a blank surface, not a tucked-away widget.
8
+ * - `title`: font-display (MohrAlt — always uppercase + black weight) to carry
9
+ * the brand voice; the display cut renders all-caps naturally so no text-transform needed.
10
+ * Bumped two steps up per size (stock caps the title at text-sm..text-lg, undersized for a
11
+ * display face) so the MohrAlt title reads with presence: xs/sm text-lg, md/lg text-xl,
12
+ * xl text-2xl. Overridden on the SIZE VARIANT (stock sets title size there, so a plain base
13
+ * class would lose the ordering race — see the per-size-radius-hook rule).
14
+ * - `description`: max-w-xs tightens the measure to ~12 words per line for comfortable reading in
15
+ * the centred layout. The colour is deliberately NOT set here — stock already assigns the right
16
+ * per-variant description colour (outline text-muted, soft/subtle text-toned, solid text-dimmed
17
+ * so it stays legible on the inverted surface), and the variant always wins the merge, so a
18
+ * slot-base colour here would just be dead weight. Leave it to stock.
19
+ * - `actions`: gap-3 (12 px) — a touch more breath between CTA buttons than the stock gap-2.
20
+ *
21
+ * Slots touched: root, title, description, actions.
22
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/empty.ts
23
+ */
24
+
25
+ export const emptyTheme = {
26
+ slots: {
27
+ root: 'rounded-2xl p-8 sm:p-10 lg:p-12',
28
+ title: 'font-display',
29
+ description: 'max-w-xs',
30
+ actions: 'gap-3',
31
+ },
32
+ variants: {
33
+ // One step up from stock (xs/sm text-sm, md/lg text-base, xl text-lg) so the display title
34
+ // carries presence. Set on the size variant because stock puts the title size here.
35
+ size: {
36
+ xs: { title: 'text-lg' },
37
+ sm: { title: 'text-lg' },
38
+ md: { title: 'text-xl' },
39
+ lg: { title: 'text-xl' },
40
+ xl: { title: 'text-2xl' },
41
+ },
42
+ },
43
+ } as const;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Forest theme fragment — UFieldGroup (joined input/button cluster).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `base`: radius set to `rounded-lg` (0.5 rem, matching --ui-radius) so the outer corners of
6
+ * the joined bar read with the same soft radius used across Forest controls (input, button,
7
+ * card). Stock Nuxt UI's FieldGroup has no explicit radius on the wrapper — the outer corners
8
+ * are therefore whatever the first/last child uses. Pinning `rounded-lg` on the wrapper ensures
9
+ * the group's clip boundary matches the brand standard even when children vary.
10
+ *
11
+ * STRUCTURE (do not wrap in `slots`): UFieldGroup is a SINGLE-SLOT component — its stock theme is
12
+ * FLAT (`{ base: 'relative', variants: { size, orientation } }`), where `base` is the root's class
13
+ * string and `variants.orientation.horizontal` carries the load-bearing `inline-flex -space-x-px`
14
+ * (the 1px negative gap that overlaps children so adjacent inset rings/borders collapse to one).
15
+ * A previous version wrapped this in `{ slots: { base } }` (multi-slot shape) — that mismatched the
16
+ * flat recipe, broke tv() resolution (root rendered literal class "base", display:block), and DROPPED
17
+ * the orientation variant → no overlap → ring variants (subtle/outline) double-bordered at every join.
18
+ * Keep this fragment FLAT so the top-level `base` merges and the orientation/size variants survive.
19
+ *
20
+ * With the overlap intact, inner-corner stripping + ring collapse are handled automatically by Nuxt
21
+ * (fieldGroup variant on each child + `-space-x-px` here), so ring variants join cleanly. The one
22
+ * caveat is the KEYCAP lip variants (keycap/pad/framed): their ::before lip slab reads lumpy when
23
+ * segments butt together — for a segmented control prefer solid (active) + soft/subtle (inactive).
24
+ *
25
+ * Slot sourced from: apps/docs/node_modules/.nuxt-ui/ui/field-group.ts
26
+ */
27
+
28
+ export const fieldGroupTheme = {
29
+ // FLAT top-level `base` (single-slot component) — merges with stock's `relative` base and keeps the
30
+ // size/orientation variants (incl. the load-bearing `-space-x-px` overlap). rounded-md = the joined
31
+ // bar's outer corner; the wrapper itself paints nothing, so the VISIBLE corner is the child's —
32
+ // capped to the same value by the orientation variants below.
33
+ base: 'rounded-md',
34
+
35
+ // OUTER-CORNER CAP. A joined bar's end segments must not keep their own standalone corner: the
36
+ // mobile cut gives every button `rounded-full`, which lands after the field-group corner-strip and
37
+ // domed the bar's ends (a 36px segment clamped to an 18px half-round — overshooting the group's own
38
+ // radius). A segmented bar is one slab, not a row of pills, so the group pins its end corners to
39
+ // rounded-md in EVERY context.
40
+ //
41
+ // `[&>*:first-child]` is (0,2,0), so it beats a child's plain `rounded-full`/`rounded-lg` (0,1,0),
42
+ // while Nuxt's inner-corner strip (`not-only:first:rounded-e-none`, higher specificity still) keeps
43
+ // stripping the shared edges. Flat STRINGS here to match stock's orientation variant shape — a
44
+ // `{ base: … }` object would mismatch the flat recipe and drop the overlap (the 2026-07-17 bug).
45
+ // Trade-off: inside a group the per-size radius ramp flattens to one corner, which is the point.
46
+ variants: {
47
+ orientation: {
48
+ horizontal: '[&>*:first-child]:rounded-s-md [&>*:last-child]:rounded-e-md',
49
+ vertical: '[&>*:first-child]:rounded-t-md [&>*:last-child]:rounded-b-md',
50
+ },
51
+ },
52
+ } as const;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Forest theme fragment — UFileUpload (drop area / button upload).
3
+ *
4
+ * Deltas only — every class here overrides stock and actually lands (verified against the merged
5
+ * tv() output). Forest opinions:
6
+ * - `base`: radius lifted from stock `rounded-lg` to `rounded-xl` (the Forest card tier, 24 px on
7
+ * the scaled --ui-radius) so the drop-zone shell reads at the card/modal level, not the tighter
8
+ * input level. The transition is widened
9
+ * to include `border-color` (on the Forest `--motion-fast` duration) so the drag-highlight
10
+ * border animates in smoothly alongside the background.
11
+ * - `label`: `font-bold` — a step above stock `font-medium`, giving the call-to-action label a
12
+ * confident hierarchy over the description.
13
+ *
14
+ * Deliberately NOT set (would be inert — stock overrides them in a later-merged variant, so the
15
+ * class is silently dropped by tailwind-merge; documenting the trap so nobody re-adds them):
16
+ * - a `focus-visible:outline-*` on base is clobbered by the per-colour compoundVariant
17
+ * (`focus-visible:outline-3`); the focus ring stays stock.
18
+ * - an `icon` size on the base slot is clobbered by the size variant (`size-4/5/6`); a bigger
19
+ * empty-state glyph would need per-size `variants.size.{…}.icon` overrides.
20
+ * - a `file` radius on the base slot is clobbered by `variants.layout.list.file` (`rounded-md`);
21
+ * softer list rows would need to override that layout variant.
22
+ * - `fileLeadingAvatar` `rounded-lg` in the grid layout is already stock — nothing to add.
23
+ *
24
+ * Focus rings and the drag-highlight border colour are Nuxt UI compoundVariants keyed on the
25
+ * semantic colour vars (`--ui-color-<family>-*`), wired to the Forest OKLCH ramps in docs.css —
26
+ * they resolve to the correct brand values with no Forest override.
27
+ */
28
+
29
+ export const fileUploadTheme = {
30
+ slots: {
31
+ // rounded-xl — Forest card/modal radius (24 px scaled; stock base is rounded-lg). border-color added
32
+ // to the transition so the drag-highlight border animates on the Forest fast duration.
33
+ base: 'rounded-xl transition-[background,border-color] duration-(--motion-fast)',
34
+
35
+ // font-bold — lifts the call-to-action above stock font-medium.
36
+ label: 'font-bold',
37
+ },
38
+ } as const;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Forest theme fragment for UFooterColumns.
3
+ *
4
+ * One delta: the column label is `text-sm font-semibold`, a size off Tailwind's default scale. A
5
+ * footer column heading is an overline in Forest's vocabulary — small, spaced, quiet — so it takes
6
+ * `.type-overline`, which carries size, tracking and case together.
7
+ */
8
+
9
+ export const footerColumnsTheme = {
10
+ slots: {
11
+ label: 'type-overline',
12
+ },
13
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Reviewed, no delta. UFooter is pure layout — every slot is flex/grid/spacing and it sets no type,
3
+ * colour or radius of its own. The type that appears in a footer comes from UFooterColumns.
4
+ *
5
+ * ★ It DOES cap its own width: the container slot embeds a UContainer, so a footer must not be
6
+ * nested inside a well. Registered on /layout/well.
7
+ */
8
+
9
+ export const footerTheme = {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Forest theme fragment — UFormField (field layout: label, help, error).
3
+ *
4
+ * Forest opinions applied:
5
+ *
6
+ * `label`:
7
+ * Lifted from stock `font-medium` to `font-bold` — clearly above the field content,
8
+ * giving the label a clear hierarchy. Matches the legend treatment in checkboxGroupTheme /
9
+ * radioGroupTheme.
10
+ *
11
+ * Everything else is stock. Text size belongs to `variants.size`, which drives the `root` slot and
12
+ * lets the secondary slots (description, error, hint, help) inherit from it — a flat `text-*` here
13
+ * would pin one rung of a five-rung ladder. The secondary slots keep stock's `text-muted`, so the
14
+ * label carries the hierarchy on weight alone.
15
+ *
16
+ * Slots touched: label.
17
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/form-field.ts
18
+ */
19
+
20
+ export const formFieldTheme = {
21
+ slots: {
22
+ // font-bold — clearly above the field content.
23
+ label: 'font-bold',
24
+ },
25
+ } as const;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Forest theme fragment — UForm + UFormField (validation wrapper + field layout).
3
+ *
4
+ * Forest opinions applied:
5
+ *
6
+ * UForm (`base`):
7
+ * No visual chrome of its own — UForm is a pure behaviour wrapper (validation, loading state,
8
+ * submit gate). The only Forest opinion is a comfortable `space-y-5` vertical rhythm between
9
+ * FormFields, replacing the stock empty string. This gives Forest forms a consistent,
10
+ * breathing density without each page needing to repeat the spacing utility.
11
+ *
12
+ * Slots touched (form): base.
13
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/form.ts
14
+ *
15
+ * The UFormField opinions (label, description, error, hint) live in their own sibling fragment
16
+ * (form-field.ts), matching the one-file-per-component convention (cf. radio-group.ts).
17
+ */
18
+
19
+ export const formTheme = {
20
+ // space-y-5: comfortable Forest density between FormFields (stock: empty string — no spacing).
21
+ base: 'space-y-5',
22
+ } as const;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Forest theme fragment for UHeader.
3
+ *
4
+ * One delta: the title takes `text-xl` off Tailwind's default scale. Retyped onto `.type-title` so
5
+ * the site wordmark sits on Forest's ladder rather than a parallel one.
6
+ */
7
+
8
+ export const headerTheme = {
9
+ slots: {
10
+ title: 'type-title',
11
+ },
12
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Forest theme fragment — UIcon
3
+ *
4
+ * UIcon is a thin pass-through to @nuxt/icon — it has no Tailwind Variants theme slots,
5
+ * no compound variants, and no class-generating machinery of its own. There is nothing to
6
+ * override in the Tailwind Variants sense.
7
+ *
8
+ * The Forest opinion for icons therefore lives entirely at the USAGE layer:
9
+ *
10
+ * Sizing — always use Tailwind `size-*` utilities, never the `:size` prop.
11
+ * The `:size` prop inlines a `font-size` style which misaligns SVG icons in
12
+ * flex/grid contexts. `size-*` sets both `width` and `height` as layout properties.
13
+ *
14
+ * size-3 (12 px) — inline annotation, caption badge icon
15
+ * size-3.5 (14 px) — UBadge leading/trailing icon slot
16
+ * size-4 (16 px) — UButton icon · sidebar nav item ← default
17
+ * size-5 (20 px) — list row · prominent inline icon
18
+ * size-6 (24 px) — card icon · empty-state accent block
19
+ * size-8 (32 px) — avatar-scale accent
20
+ * size-10 (40 px) — hero / illustration icon
21
+ *
22
+ * Colour — always use semantic `text-*` classes so dark mode adapts automatically.
23
+ * Never use raw palette classes (text-forest-500, text-maple-600, …) on icons.
24
+ *
25
+ * text-primary — brand accent (forest)
26
+ * text-success — active / healthy states
27
+ * text-warning — low battery / caution
28
+ * text-error — fault / critical
29
+ * text-info — location / informational
30
+ * text-muted — secondary / inactive
31
+ * text-dimmed — decorative / lowest emphasis
32
+ * text-highlighted — high-contrast foreground
33
+ *
34
+ * Flex rows — always add `shrink-0` when UIcon sits beside text in a flex row;
35
+ * omitting it causes the icon to be squeezed on narrow viewports.
36
+ *
37
+ * Icon family — two prefixes, one drawing contract. `i-lucide-*` is the base set
38
+ * (@iconify-json/lucide) and resolves over the Iconify API. `i-forest-*` is
39
+ * Forest's own (packages/ui/icons/forest, packed by scripts/icons.ts) and must be
40
+ * registered from the bundle with addCollection() — there is no API behind it.
41
+ * Draw a Forest icon only where Lucide's glyph would misstate the operation, and
42
+ * draw it to Lucide's conventions: 24×24, fill none, stroke currentColor,
43
+ * stroke-width 2, round caps and joins. Full doctrine: /foundations/icons.
44
+ *
45
+ * Accessibility — decorative icons (beside a label): add `aria-hidden="true"`.
46
+ * Standalone icon controls: use <UButton icon="…" aria-label="…" />.
47
+ * Meaningful icons without visible label: add <span class="sr-only">.
48
+ *
49
+ * Render mode — keep the default mode="svg" for crisp rendering at all pixel densities;
50
+ * do not switch to mode="css" in Forest UIs.
51
+ *
52
+ * Slots touched: none — UIcon has no Tailwind Variants theme.
53
+ *
54
+ * This file exists as a Forest convention record and a named export so the theme registry
55
+ * can include it without breaking the import graph. The exported object is intentionally
56
+ * empty; the real Forest opinion is documented above and enforced at the usage layer.
57
+ */
58
+ export const iconTheme = {
59
+ // UIcon has no Tailwind Variants slots.
60
+ // Forest opinions are usage conventions documented in the comment above.
61
+ } as const;
@@ -0,0 +1,99 @@
1
+ // Forest theme fragments — one re-export per component Forest uses, aliased to its Nuxt UI
2
+ // ui-config key. An EMPTY fragment ({}) records a reviewed component whose stock recipe is
3
+ // correct as-is — presence means reviewed, content means delta.
4
+ // Spread into forestPreset.ui, where this barrel is the single source of component themes
5
+ // (asserted by the static tests). Hand-maintained; keep the list ordered by export key.
6
+
7
+ export { accordionTheme as accordion } from './accordion';
8
+ export { alertTheme as alert } from './alert';
9
+ export { authFormTheme as authForm } from './auth-form';
10
+ export { avatarTheme as avatar } from './avatar';
11
+ export { avatarGroupTheme as avatarGroup } from './avatar-group';
12
+ export { badgeTheme as badge } from './badge';
13
+ export { bannerTheme as banner } from './banner';
14
+ export { breadcrumbTheme as breadcrumb } from './breadcrumb';
15
+ // button.mechanics is deliberately absent — it exports the button's private recipe/palette
16
+ // constants, not a component theme.
17
+ export { buttonTheme as button } from './button';
18
+ export { calendarTheme as calendar } from './calendar';
19
+ export { cardTheme as card } from './card';
20
+ export { carouselTheme as carousel } from './carousel';
21
+ export { checkboxTheme as checkbox } from './checkbox';
22
+ export { checkboxGroupTheme as checkboxGroup } from './checkbox-group';
23
+ export { chipTheme as chip } from './chip';
24
+ export { collapsibleTheme as collapsible } from './collapsible';
25
+ export { colorPickerTheme as colorPicker } from './color-picker';
26
+ export { commandPaletteTheme as commandPalette } from './command-palette';
27
+ export { containerTheme as container } from './container';
28
+ export { contextMenuTheme as contextMenu } from './context-menu';
29
+ export { dashboardGroupTheme as dashboardGroup } from './dashboard-group';
30
+ export { dashboardNavbarTheme as dashboardNavbar } from './dashboard-navbar';
31
+ export { dashboardPanelTheme as dashboardPanel } from './dashboard-panel';
32
+ export { dashboardResizeHandleTheme as dashboardResizeHandle } from './dashboard-resize-handle';
33
+ export { dashboardSidebarTheme as dashboardSidebar } from './dashboard-sidebar';
34
+ export { dashboardSidebarCollapseTheme as dashboardSidebarCollapse } from './dashboard-sidebar-collapse';
35
+ export { dashboardSidebarToggleTheme as dashboardSidebarToggle } from './dashboard-sidebar-toggle';
36
+ export { dashboardToolbarTheme as dashboardToolbar } from './dashboard-toolbar';
37
+ export { drawerTheme as drawer } from './drawer';
38
+ export { dropdownMenuTheme as dropdownMenu } from './dropdown-menu';
39
+ export { emptyTheme as empty } from './empty';
40
+ export { footerTheme as footer } from './footer';
41
+ export { footerColumnsTheme as footerColumns } from './footer-columns';
42
+ export { fieldGroupTheme as fieldGroup } from './field-group';
43
+ export { fileUploadTheme as fileUpload } from './file-upload';
44
+ export { formTheme as form } from './form';
45
+ export { formFieldTheme as formField } from './form-field';
46
+ export { headerTheme as header } from './header';
47
+ export { iconTheme as icon } from './icon';
48
+ export { inputDateTheme as inputDate } from './input-date';
49
+ export { inputMenuTheme as inputMenu } from './input-menu';
50
+ export { inputNumberTheme as inputNumber } from './input-number';
51
+ export { inputRatingTheme as inputRating } from './input-rating';
52
+ export { inputTagsTheme as inputTags } from './input-tags';
53
+ export { inputTimeTheme as inputTime } from './input-time';
54
+ export { inputTheme as input } from './input';
55
+ export { kbdTheme as kbd } from './kbd';
56
+ export { linkTheme as link } from './link';
57
+ export { listboxTheme as listbox } from './listbox';
58
+ export { mainTheme as main } from './main';
59
+ export { marqueeTheme as marquee } from './marquee';
60
+ export { modalTheme as modal } from './modal';
61
+ export { navigationMenuTheme as navigationMenu } from './navigation-menu';
62
+ export { pageTheme as page } from './page';
63
+ export { pageAnchorsTheme as pageAnchors } from './page-anchors';
64
+ export { pageAsideTheme as pageAside } from './page-aside';
65
+ export { pageCardTheme as pageCard } from './page-card';
66
+ export { pageCtaTheme as pageCta } from './page-cta';
67
+ export { pageFeatureTheme as pageFeature } from './page-feature';
68
+ export { pageHeaderTheme as pageHeader } from './page-header';
69
+ export { pageHeroTheme as pageHero } from './page-hero';
70
+ export { pageLinksTheme as pageLinks } from './page-links';
71
+ export { pageSectionTheme as pageSection } from './page-section';
72
+ export { paginationTheme as pagination } from './pagination';
73
+ export { pinInputTheme as pinInput } from './pin-input';
74
+ export { popoverTheme as popover } from './popover';
75
+ export { progressTheme as progress } from './progress';
76
+ // Prose components are namespaced under ui.prose.* — the fragment runs stock, carrying only the
77
+ // `ed-demo` marker the docs editorial grid reads off a demo pane.
78
+ export { proseTheme as prose } from './prose';
79
+ export { radioGroupTheme as radioGroup } from './radio-group';
80
+ export { scrollAreaTheme as scrollArea } from './scroll-area';
81
+ export { selectMenuTheme as selectMenu } from './select-menu';
82
+ export { selectTheme as select } from './select';
83
+ export { separatorTheme as separator } from './separator';
84
+ export { sidebarTheme as sidebar } from './sidebar';
85
+ export { skeletonTheme as skeleton } from './skeleton';
86
+ export { slideoverTheme as slideover } from './slideover';
87
+ export { sliderTheme as slider } from './slider';
88
+ export { stepperTheme as stepper } from './stepper';
89
+ export { switchTheme as switch } from './switch';
90
+ export { tableTheme as table } from './table';
91
+ export { tabsTheme as tabs } from './tabs';
92
+ export { textareaTheme as textarea } from './textarea';
93
+ export { timelineTheme as timeline } from './timeline';
94
+ export { toastTheme as toast } from './toast';
95
+ // toaster is the STACK (viewport, entry/exit, swipe); toast above is a single notification's surface.
96
+ export { toasterTheme as toaster } from './toaster';
97
+ export { tooltipTheme as tooltip } from './tooltip';
98
+ export { treeTheme as tree } from './tree';
99
+ export { userTheme as user } from './user';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Forest theme fragment — UInputDate (date / range picker field).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `base`: the recessed depth line ported from UInput (inset top edge at rest, dropped on focus)
6
+ * so the field family shares one pressed-into-the-page identity. Radius stays stock rounded-md.
7
+ * - `segment`: focused segments receive `focus:rounded` (a mild inner radius) so the highlight
8
+ * bubble fits the overall radius language rather than appearing perfectly square.
9
+ * - `leadingIcon` / `trailingIcon`: already inherit `text-dimmed` from stock; no override needed.
10
+ * - `separatorIcon`: sized and dimmed to match the rest of the field chrome at every size.
11
+ *
12
+ * - touch ladder: like UInput, each size swaps to its touch rung (min-h 28/36/40/44/52)
13
+ * (py, gap, segment/base type, icon) so the field reads as a bigger version of itself. Horizontal
14
+ * px is held (stock's absolute leading / trailing ps- / pe- insets would be clobbered). Like UInput
15
+ * the base type carries a responsive `md:text-*` shrink, so the coarse type bump targets the next
16
+ * size's EFFECTIVE desktop value (sm→md `touch:text-sm`, lg→xl `touch:text-base`) rather than its
17
+ * base token, or a bumped sm would outgrow md and invert the ladder. xl has a touch rung of
18
+ * its own (52), and the mobile cut needs no escape — it lands on the same rung a coarse pointer would.
19
+ *
20
+ * Slots touched: segment; size variant (touch deltas).
21
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/input-date.ts
22
+ *
23
+ * Focus rings (outline/subtle variants × every colour) resolve correctly via Nuxt UI's own
24
+ * compoundVariants reading --ui-color-<family>-* — already wired to the Forest OKLCH ramps in
25
+ * docs.css. No override needed here.
26
+ */
27
+
28
+ export const inputDateTheme = {
29
+ slots: {
30
+ // The recessed depth line, ported from UInput so the field family shares one "pressed-into-the-
31
+ // page" identity: a solid inset top edge (no blur) at rest, dropped on focus (has-focus-visible,
32
+ // since focus lives on a segment) so the active field lifts to its focus ring — the same
33
+ // rest-pressed / focus-lifted behaviour UInput now uses. Depth scales per size via the var below;
34
+ // the recess colour is mode-aware (forest.css). transition-[color,box-shadow] eases it in/out.
35
+ 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',
36
+ // focus:rounded — mild inner radius on the active segment bubble matches the field's soft radius.
37
+ segment: 'focus:rounded',
38
+ },
39
+ // Per-size recessed depth (2/2/3/3/4, shallower on short fields) + the touch ladder — one
40
+ // size up as a unit. Each size lists only its deltas.
41
+ // `touch:` = the shared TOUCH ladder (styles/forest.css) — height+type twin of button.ts/input.ts
42
+ // so a joined <UFieldGroup> matches. Fired by a coarse pointer or by the mobile cut's
43
+ // baked marker — same numbers either way, so a real phone can't step twice.
44
+ variants: {
45
+ size: {
46
+ xs: { base: '[--forest-input-depth:2px] touch:min-h-7 touch:gap-1.5' }, // touch 28
47
+ 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)
48
+ md: { base: '[--forest-input-depth:3px] touch:min-h-10 touch:text-sm touch:gap-2' }, // touch 40
49
+ 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
50
+ 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
51
+ },
52
+ },
53
+ compoundVariants: [
54
+ // ghost / none carry no fill, so a resting depth line floats with nothing to recess into. ghost
55
+ // picks it up on hover (the elevated fill arrives); none stays flat.
56
+ { 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)]' } },
57
+ { variant: 'none', class: { base: 'shadow-none' } },
58
+ // Recess tint gated to `highlight` (the persistent coloured-ring state) — a plain rest field is
59
+ // neutral (ring-accented border), so the line only takes the accent where the border does.
60
+ // neutral omitted (its highlight ring is the inverted tone, matched by the neutral line).
61
+ { color: 'primary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-primary)]' } },
62
+ { color: 'secondary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-secondary)]' } },
63
+ { color: 'success', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-success)]' } },
64
+ { color: 'info', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-info)]' } },
65
+ { color: 'warning', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warning)]' } },
66
+ { color: 'error', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-error)]' } },
67
+ { color: 'forest', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-forest)]' } },
68
+ { color: 'maple', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-maple)]' } },
69
+ { color: 'river', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-river)]' } },
70
+ { color: 'warm', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warm)]' } },
71
+ { color: 'acid', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-acid)]' } },
72
+ ],
73
+ } as const;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Forest theme fragment — UInputMenu (autocomplete combobox: input + dropdown panel).
3
+ *
4
+ * Forest opinions applied:
5
+ * - base (input): 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:outline-none
7
+ * suppresses the native ring; the brand forest-500 focus ring on outline.
8
+ * - touch ladder: each size takes its touch rung (min-h 28/36/40/44/52) under a coarse
9
+ * pointer or the mobile cut — box, gap,
10
+ * effective text, icons); px held. The base carries a responsive `md:text-*` shrink, so the coarse
11
+ * type bump targets the next size's EFFECTIVE value (sm→md `touch:text-sm`, lg→xl `touch:text-base`).
12
+ * - recess tint: an explicit intent tints the depth line to its ring colour, but only in `highlight`.
13
+ * - content: opens/closes on the Forest motion tokens.
14
+ * - item: before:rounded-sm (8px) — concentric inside the 12px panel.
15
+ * - tagsItem: rounded-md — comfortable tag pills, consistent with the Forest field family.
16
+ *
17
+ * Slots touched: base, content, item, tagsItem; size variant (depth + touch); variant (focus ring).
18
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/input-menu.ts
19
+ */
20
+
21
+ export const inputMenuTheme = {
22
+ slots: {
23
+ 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-none focus:outline-none',
24
+ // Panel radius/ring come from stock (rounded-md / ring-default); opens/closes with Forest motion.
25
+ content: '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)]',
26
+ // Item slab: before:rounded-sm (8px) — concentric inside the 12px panel.
27
+ item: 'before:rounded-sm',
28
+ // Tag pills in multiple mode: rounded-md — comfortable, consistent with the Forest field family.
29
+ tagsItem: 'rounded-md',
30
+ },
31
+ variants: {
32
+ variant: {
33
+ // Reinforce the focus ring with forest-500 so the open combobox matches UInput's ring colour.
34
+ outline: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--forest-500)]',
35
+ subtle: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--forest-500)]',
36
+ },
37
+ // Per-size recessed depth (2/2/3/3/4) + the touch ladder. Object-shaped; deltas only.
38
+ // `touch:` = the shared TOUCH ladder (styles/forest.css) — height+type twin of button.ts/input.ts
39
+ // so a joined <UFieldGroup> matches. Fired by a coarse pointer or by the mobile cut's
40
+ // baked marker — same numbers either way, so a real phone can't step twice.
41
+ size: {
42
+ xs: { base: '[--forest-input-depth:2px] touch:min-h-7 touch:gap-1.5' }, // touch 28
43
+ 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
44
+ md: { base: '[--forest-input-depth:3px] touch:min-h-10 touch:text-sm touch:gap-2' }, // touch 40
45
+ 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
46
+ 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
47
+ },
48
+ },
49
+ compoundVariants: [
50
+ // ghost / none carry no fill, so a resting depth line floats with nothing to recess into.
51
+ { 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)]' } },
52
+ { variant: 'none', class: { base: 'shadow-none' } },
53
+ // Recess tint gated to `highlight`; a plain rest field keeps the neutral line. neutral omitted.
54
+ { color: 'primary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-primary)]' } },
55
+ { color: 'secondary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-secondary)]' } },
56
+ { color: 'success', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-success)]' } },
57
+ { color: 'info', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-info)]' } },
58
+ { color: 'warning', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warning)]' } },
59
+ { color: 'error', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-error)]' } },
60
+ { color: 'forest', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-forest)]' } },
61
+ { color: 'maple', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-maple)]' } },
62
+ { color: 'river', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-river)]' } },
63
+ { color: 'warm', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warm)]' } },
64
+ { color: 'acid', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-acid)]' } },
65
+ ],
66
+ } as const;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Forest theme fragment — UInputNumber (stepper number field).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `base`: the recessed depth line ported from UInput (inset top edge at rest, dropped on focus)
6
+ * so the field family shares one pressed-into-the-page identity; focus:outline-none suppresses the
7
+ * stock ring. Radius/shape stay stock.
8
+ * - touch ladder: like UInput, each size swaps to its touch rung (min-h 28/36/40/44/52)
9
+ * (py, gap, effective text). Horizontal px is held (the absolute increment/decrement insets would
10
+ * be clobbered). The base type carries a responsive `md:text-*` shrink, so the coarse type bump
11
+ * targets the next size's EFFECTIVE value (sm→md `touch:text-sm`, lg→xl `touch:text-base`) or a
12
+ * promoted sm would outgrow md and invert the ladder. xl has a touch rung of its own (52).
13
+ * - recess tint: an explicit intent tints the depth line to its ring colour, but only in `highlight`
14
+ * (the persistent coloured-ring state) — a plain rest field stays neutral. Same rule as UInput.
15
+ *
16
+ * The size variant is a FLAT string per size (stock's own shape — it targets the base slot); the
17
+ * touch deltas + depth var append as flat strings to match, or the merge would drop them.
18
+ *
19
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/input-number.ts
20
+ * Focus rings (outline/subtle × every colour) resolve via Nuxt UI's own compoundVariants reading
21
+ * --ui-color-<family>-*, wired to the Forest ramps in docs.css. No override needed there.
22
+ */
23
+
24
+ export const inputNumberTheme = {
25
+ slots: {
26
+ 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-none focus:outline-none',
27
+ },
28
+ variants: {
29
+ // Per-size recessed depth (2/2/3/3/4) + the touch ladder. FLAT strings — matches stock's size
30
+ // shape (a { base: … } object here would be dropped by the merge).
31
+ size: {
32
+ xs: '[--forest-input-depth:2px] touch:min-h-7 touch:gap-1.5', // touch 28
33
+ sm: '[--forest-input-depth:2px] touch:min-h-9 touch:text-sm touch:rounded-md touch:[--forest-input-depth:3px]', // touch 36 (type = md's effective value; stock md:text-sm shrink)
34
+ md: '[--forest-input-depth:3px] touch:min-h-10 touch:text-sm touch:gap-2', // touch 40
35
+ lg: '[--forest-input-depth:3px] touch:min-h-11 touch:text-base touch:rounded-lg touch:[--forest-input-depth:4px]', // touch 44
36
+ xl: '[--forest-input-depth:4px] touch:min-h-13 touch:text-lg touch:rounded-lg touch:[--forest-input-depth:4px]', // touch 52
37
+ },
38
+ },
39
+ compoundVariants: [
40
+ // ghost / none carry no fill, so a resting depth line floats with nothing to recess into. ghost
41
+ // picks it up on hover (elevated fill arrives); none stays flat.
42
+ { 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)]' } },
43
+ { variant: 'none', class: { base: 'shadow-none' } },
44
+ // Recess tint gated to `highlight` (the persistent coloured-ring state); a plain rest field keeps
45
+ // the neutral line. neutral omitted (its highlight ring is the inverted tone).
46
+ { color: 'primary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-primary)]' } },
47
+ { color: 'secondary', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-secondary)]' } },
48
+ { color: 'success', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-success)]' } },
49
+ { color: 'info', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-info)]' } },
50
+ { color: 'warning', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warning)]' } },
51
+ { color: 'error', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-error)]' } },
52
+ { color: 'forest', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-forest)]' } },
53
+ { color: 'maple', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-maple)]' } },
54
+ { color: 'river', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-river)]' } },
55
+ { color: 'warm', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-warm)]' } },
56
+ { color: 'acid', highlight: true, class: { base: '[--forest-input-recess:var(--forest-recess-acid)]' } },
57
+ ],
58
+ } as const;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Forest theme fragment for UInputRating.
3
+ *
4
+ * Amber by default — the universal gold-star rating look. `color` is a variant, so a
5
+ * defaultVariants override is the clean lever (stock defaults to primary). Explicit color on a
6
+ * component still wins. The filled star glyph is the vendored solid star (the preset's icons.star
7
+ * alias); the empty state reuses it muted unless a component passes its own empty-icon.
8
+ */
9
+
10
+ export const inputRatingTheme = {
11
+ defaultVariants: { color: 'warning' },
12
+
13
+ // Touch: each star promotes ONE size up under a coarse pointer — the whole ladder is a square
14
+ // dimension (size-3 → size-7), so promotion is a straight one-step size bump on both the hit area
15
+ // (`item`) and the glyph (`icon`). xl (size-7) is the ceiling; deltas only. `coarse:` carries the
16
+ // :not(.cut-fixed) escape so a baked mobile cut opts out.
17
+ variants: {
18
+ size: {
19
+ xs: { item: 'coarse:size-4', icon: 'coarse:size-4' },
20
+ sm: { item: 'coarse:size-5', icon: 'coarse:size-5' },
21
+ md: { item: 'coarse:size-6', icon: 'coarse:size-6' },
22
+ lg: { item: 'coarse:size-7', icon: 'coarse:size-7' },
23
+ },
24
+ },
25
+ } as const