@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,67 @@
1
+ /**
2
+ * Forest theme fragment for UCard.
3
+ *
4
+ * Radius xl (24px). `forest-card` marker (PRESET_STRUCTURAL_MARKERS) drives the NESTED-surface +
5
+ * concentric-radius CSS in styles/forest.css (§ card nesting): each nested card steps one
6
+ * neutral-ramp level (light darker / dark lighter) and its corners stay concentric
7
+ * (radius = parent − padding). Uniform p-4 padding — kill the stock desktop bump (body sm:p-6,
8
+ * header/footer sm:px-6).
9
+ */
10
+
11
+ // Stock puts `divide-y` on the VARIANT root (outline/soft/subtle), not the base slots — so a variant
12
+ // with no divider (solid, and the tinted set below, which stock never supplied one for) shows the
13
+ // header→body→footer gap DOUBLED (header pb-4 + body pt-4 = 32px) with nothing sitting in it. These
14
+ // two rules collapse it back to the single 16px the body's own p-4 already provides.
15
+ // The :not(:last-child)/:not(:first-child) guards are load-bearing: a header-only card (title +
16
+ // description, no body/footer) must keep its bottom padding — with a bare pb-0 the text sat flush
17
+ // against the edge. The collapse only fires when a sibling actually follows/precedes.
18
+ const flatGap = {
19
+ header: '[&:not(:last-child)]:pb-0',
20
+ footer: '[&:not(:first-child)]:pt-0',
21
+ } as const
22
+
23
+ export const cardTheme = {
24
+ slots: {
25
+ root: 'rounded-xl forest-card',
26
+ body: 'sm:p-4',
27
+ header: 'sm:px-4',
28
+ footer: 'sm:px-4',
29
+ // Title voice rides the --fx-* traits (forest.css): sans/none at rest, so web is unchanged —
30
+ // a louder context (marketing delta) re-points the vars to Mohr + uppercase without touching
31
+ // this recipe. Weight is NOT a trait: the delta adds font-black as a plain class instead
32
+ // (Mohr only ships 900; a weight var would alter the stock title at rest).
33
+ title: '[font-family:var(--fx-title-family)] [text-transform:var(--fx-title-transform)]',
34
+ },
35
+ // Quiet fill: `soft`/`subtle` both draw from --forest-card-bg (the depth-stepped neutral surface,
36
+ // set by the nesting CSS) — soft no ring, subtle + ring (the stock "soft +ring = subtle"
37
+ // relationship, now on an opaque ramp surface that reads distinctly when nested and in dark,
38
+ // where bg-muted == bg-elevated). soft also kills the divide-y for a flat single surface.
39
+ // `outline` (bg-default + ring) and `solid` (bg-inverted) stay stock — the ring/contrast do the
40
+ // separating there.
41
+ variants: {
42
+ variant: {
43
+ // Borderless variants take flatGap; divided ones keep the full gap — the hairline sits in it.
44
+ soft: { root: 'bg-[var(--forest-card-bg)] divide-y-0', ...flatGap },
45
+ solid: { ...flatGap },
46
+ // subtle AND outline use the translucent RELATIVE hairline (--forest-card-ring) for ring +
47
+ // divide, not the fixed border-default ramp token — one consistent border language across
48
+ // cards (and it keeps subtle collision-proof on the stepped surface). Only the FILL differs:
49
+ // subtle = stepped surface, outline = bg-default.
50
+ subtle: { root: 'bg-[var(--forest-card-bg)] ring-[var(--forest-card-ring)] divide-[color:var(--forest-card-ring)]' },
51
+ outline: { root: 'ring-[var(--forest-card-ring)] divide-[color:var(--forest-card-ring)]' },
52
+ // Tinted accent variants — surfaces off the brand ramps (see --forest-card-<hue>-* in
53
+ // styles/forest.css: soft tint in light, deep ramp tone in dark). Ink is tone-on-tone: title +
54
+ // description take the hue's own -text / -muted step (deep on the light tint, light on the deep
55
+ // tone) so the copy reads as part of the accent, not neutral ink dropped on top. For featured/
56
+ // promo cards. Flat like soft: the hue carries the surface, so there is no divider and the
57
+ // sections take flatGap. Stock has no tinted variant to inherit a `divide-y` from.
58
+ peach: { root: 'bg-[var(--forest-card-peach-bg)] ring-[var(--forest-card-peach-ring)]', title: 'text-[var(--forest-card-peach-text)]', description: 'text-[var(--forest-card-peach-muted)]', ...flatGap },
59
+ mint: { root: 'bg-[var(--forest-card-mint-bg)] ring-[var(--forest-card-mint-ring)]', title: 'text-[var(--forest-card-mint-text)]', description: 'text-[var(--forest-card-mint-muted)]', ...flatGap },
60
+ sky: { root: 'bg-[var(--forest-card-sky-bg)] ring-[var(--forest-card-sky-ring)]', title: 'text-[var(--forest-card-sky-text)]', description: 'text-[var(--forest-card-sky-muted)]', ...flatGap },
61
+ amber: { root: 'bg-[var(--forest-card-amber-bg)] ring-[var(--forest-card-amber-ring)]', title: 'text-[var(--forest-card-amber-text)]', description: 'text-[var(--forest-card-amber-muted)]', ...flatGap },
62
+ },
63
+ },
64
+ defaultVariants: {
65
+ variant: 'soft',
66
+ },
67
+ } as const
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Forest theme fragment — UCarousel (Embla-powered sliding gallery). Deltas only.
3
+ *
4
+ * Two opinions, both on the controls:
5
+ *
6
+ * prev / next — the arrow buttons gain the Forest outline focus ring
7
+ * (`focus-visible:outline-2 outline-offset-2 outline-[var(--forest-500)]`) so keyboard
8
+ * navigation matches every other Forest interactive primitive. Stock leaves the slot at
9
+ * `absolute rounded-full` and relies on the UButton's own focus-visible ring.
10
+ *
11
+ * dot — the active pagination pip adopts the primary brand green
12
+ * (`var(--forest-500)` light / `var(--forest-400)` dark) instead of stock's `bg-inverted`,
13
+ * plus the same outline focus ring. ★ The colour MUST live in the `active` variant, not the
14
+ * base slot: stock sets `data-[state=active]:bg-inverted` in `variants.active.true.dot`, and
15
+ * tv applies the size/state variant AFTER the base slot — so a base-slot colour loses the
16
+ * tailwind-merge race and the pip stays inverted. Overriding the variant (deep-merged, appended
17
+ * last) is the seam that actually wins. Inactive pip stays stock `bg-accented`.
18
+ *
19
+ * Untouched (inherited from stock): root/viewport/container/item layout, controls/arrows wrappers,
20
+ * dots positioning, orientation variants, drag/keyboard/plugin behaviour (all Embla's).
21
+ */
22
+
23
+ const FOCUS_RING =
24
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--forest-500)]';
25
+
26
+ export const carouselTheme = {
27
+ slots: {
28
+ prev: FOCUS_RING,
29
+ next: FOCUS_RING,
30
+ dot: `focus:outline-none ${FOCUS_RING}`,
31
+ },
32
+ variants: {
33
+ active: {
34
+ true: {
35
+ // Overrides stock's `data-[state=active]:bg-inverted` — wins because the active variant is
36
+ // deep-merged (Forest appended last) and applied after the base slot.
37
+ dot: 'data-[state=active]:bg-[var(--forest-500)] dark:data-[state=active]:bg-[var(--forest-400)]',
38
+ },
39
+ },
40
+ },
41
+ } as const;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Forest theme fragment — UCheckboxGroup.
3
+ *
4
+ * Forest opinions applied:
5
+ * `legend` slot: font-bold lifts the group legend above the individual checkbox labels
6
+ * (stock: font-medium), matching the legend treatment in radioGroupTheme and the label
7
+ * treatment in formFieldTheme.
8
+ *
9
+ * The card and table variants are left entirely stock — no radius override.
10
+ *
11
+ * Slots touched: legend.
12
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/checkbox-group.ts
13
+ */
14
+
15
+ export const checkboxGroupTheme = {
16
+ slots: {
17
+ // font-bold lifts the group legend above the individual checkbox labels (stock: font-medium).
18
+ legend: 'font-bold',
19
+ },
20
+ } as const;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Forest theme fragment — UCheckbox + UCheckboxGroup.
3
+ *
4
+ * Forest opinions applied:
5
+ * - `base` (checkbox): radius lifted from `rounded-sm` (2 px stock) to `rounded` (4 px, i.e.
6
+ * 0.25 rem), which is softer without being pill-like. Keeps it reading as a square control
7
+ * while aligning with Forest's overall soft-radius vocabulary (--ui-radius = 0.5 rem).
8
+ * - `label`: font-medium is already stock; we keep it and add nothing — the Nuxt UI default is
9
+ * correct here.
10
+ * - `base` focus ring: Forest's house convention `focus-visible:outline-2 focus-visible:outline-offset-2`
11
+ * (an offset ring), over stock's flush `focus-visible:outline-3`. The outline COLOUR is stock's
12
+ * semantic value, resolved to the Forest OKLCH ramp via docs.css / forest-preset — no colour
13
+ * override needed, only the width + offset.
14
+ * - `legend` (CheckboxGroup): font-bold — bold against the regular item labels, giving the
15
+ * group label a clear hierarchy above the individual checkbox labels.
16
+ *
17
+ * The card variant is left entirely stock — no radius override.
18
+ *
19
+ * Slots touched: base.
20
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/checkbox.ts
21
+ *
22
+ * The CheckboxGroup legend opinion lives in its own sibling fragment (checkbox-group.ts),
23
+ * matching the one-file-per-component convention (cf. radio-group.ts, field-group.ts).
24
+ */
25
+
26
+ export const checkboxTheme = {
27
+ slots: {
28
+ // Deltas over stock: `rounded` (4px) for the Forest soft radius (stock rounded-sm/2px), and the
29
+ // house focus ring `outline-2 outline-offset-2` (stock is a flush outline-3). Ring colour and the
30
+ // indicator bg resolve to the Forest OKLCH ramps via the colour-alias bridge — no override there.
31
+ base: 'rounded focus-visible:outline-2 focus-visible:outline-offset-2',
32
+ },
33
+ // Touch: a checkbox's tap target is the whole label ROW, and at md that row is short (~20px). Under
34
+ // a coarse (finger) pointer each size grows one size up — box (base), row height (container) and
35
+ // label text (wrapper) — so the row becomes a taller, easier target. No sliding part, so the bump
36
+ // is clean (unlike the switch thumb). xl is the ceiling; each size lists only its deltas up.
37
+ // `coarse:` carries the `:not(.cut-fixed)` escape so the mobile cut (baked size lg) opts out.
38
+ variants: {
39
+ size: {
40
+ xs: { base: 'coarse:size-3.5' },
41
+ sm: { base: 'coarse:size-4', container: 'coarse:h-5', wrapper: 'coarse:text-sm' },
42
+ md: { base: 'coarse:size-4.5' },
43
+ lg: { base: 'coarse:size-5', container: 'coarse:h-6', wrapper: 'coarse:text-base' },
44
+ // xl: ceiling
45
+ },
46
+ },
47
+ } as const;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Forest theme fragment — UChip
3
+ *
4
+ * Two deltas over stock (the preset APPENDS, so this carries only what changes):
5
+ *
6
+ * 1. base: `font-bold` — stock is `font-medium`, thin on the tiny count labels.
7
+ *
8
+ * 2. Text-mode bump. Stock sizes a chip as a DOT: height = min-width = text all equal (md is an
9
+ * 8px dot with 8px text), so a count digit is unreadable. A chip WITH content is distinguishable
10
+ * from an empty dot via `:not(:empty)` (an empty dot holds only Vue comment placeholders, which
11
+ * `:empty` ignores). So we bump ONLY text chips and leave status dots byte-for-byte stock:
12
+ * - base: `px-1` breathing room + `leading-none` centring, size-independent.
13
+ * - size: per step, a `:not(:empty)` text floor + a min-height/width so a single digit sits
14
+ * in a roughly square pill. `min-height` beats the stock fixed `h-[Npx]` on its own
15
+ * (no `h-auto` needed); the arbitrary `[&:not(:empty)]` variant is (0,2,0), so it
16
+ * wins over the stock `text-[Npx]`/`min-w-[Npx]` (0,1,0) without a tailwind-merge
17
+ * collision. Text scales md 11 → 3xl 15px.
18
+ *
19
+ * Everything else is stock: rounded-full pill, the `ring ring-bg` separator, bg-{color} faces
20
+ * (bridged to the Forest ramps in docs.css), text-inverted ink, position / inset / standalone.
21
+ * `root` needs no override.
22
+ */
23
+ export const chipTheme = {
24
+ slots: {
25
+ base: 'font-bold [&:not(:empty)]:px-1 [&:not(:empty)]:leading-none',
26
+ },
27
+ variants: {
28
+ // Standalone chips have no host corner to hug, but stock's position compound-variant still
29
+ // applies `-translate-y-1/2 translate-x-1/2`, shoving the inline dot up-and-right off the text
30
+ // centre. Zero the translate so a standalone dot sits centred in its flow. Hooked on `.ring`
31
+ // (always on the base slot) for (0,2,0) specificity — beats the compound regardless of tv
32
+ // ordering, no !important.
33
+ standalone: {
34
+ true: '[&.ring]:translate-x-0 [&.ring]:translate-y-0',
35
+ },
36
+ // Per-size text-mode floor — applies ONLY when the chip has content (a count/label), so empty
37
+ // status dots keep the stock 4–12px ladder untouched.
38
+ size: {
39
+ '3xs': '[&:not(:empty)]:min-h-[14px] [&:not(:empty)]:min-w-[14px] [&:not(:empty)]:text-[10px]',
40
+ '2xs': '[&:not(:empty)]:min-h-[14px] [&:not(:empty)]:min-w-[14px] [&:not(:empty)]:text-[10px]',
41
+ 'xs': '[&:not(:empty)]:min-h-[15px] [&:not(:empty)]:min-w-[15px] [&:not(:empty)]:text-[10px]',
42
+ 'sm': '[&:not(:empty)]:min-h-[15px] [&:not(:empty)]:min-w-[15px] [&:not(:empty)]:text-[11px]',
43
+ 'md': '[&:not(:empty)]:min-h-[16px] [&:not(:empty)]:min-w-[16px] [&:not(:empty)]:text-[11px]',
44
+ 'lg': '[&:not(:empty)]:min-h-[17px] [&:not(:empty)]:min-w-[17px] [&:not(:empty)]:text-[12px]',
45
+ 'xl': '[&:not(:empty)]:min-h-[18px] [&:not(:empty)]:min-w-[18px] [&:not(:empty)]:text-[13px]',
46
+ '2xl': '[&:not(:empty)]:min-h-[20px] [&:not(:empty)]:min-w-[20px] [&:not(:empty)]:text-[14px]',
47
+ '3xl': '[&:not(:empty)]:min-h-[22px] [&:not(:empty)]:min-w-[22px] [&:not(:empty)]:text-[15px]',
48
+ },
49
+ },
50
+ } as const;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Forest theme fragment — UCollapsible (single animated expand/collapse).
3
+ *
4
+ * Deltas over stock (stock: root '', content = raw collapsible keyframes at 200ms ease-out):
5
+ * - `root`: adds `rounded-lg` so the container reads at the Forest soft-radius register when
6
+ * consumers wrap the collapsible in a bordered shell.
7
+ * - `content`: retimes the open/close keyframes to the shared --motion-slow / --motion-ease-out
8
+ * tokens (stock is a raw 200ms ease-out) and adds `text-sm text-muted` so body content is
9
+ * subordinate in the reading hierarchy — consistent with how the accordion body slot is styled.
10
+ * `overflow-hidden` clips the region during the height animation. Trigger styling is entirely the
11
+ * consumer's concern; the default slot exposes a scoped `{ open }`.
12
+ *
13
+ * Slots touched: root, content.
14
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/collapsible.ts
15
+ */
16
+
17
+ export const collapsibleTheme = {
18
+ slots: {
19
+ // rounded-lg: aligns with Forest's --ui-radius = 0.5rem soft-radius register. Keeps the
20
+ // collapsible shell consistent with other Forest panel-level components (Card, Alert, etc.).
21
+ root: 'rounded-lg',
22
+ // text-sm text-muted: matches Forest's body-content hierarchy (content is subordinate to the
23
+ // trigger/heading above it). The stock open/close keyframe animations are intentionally kept.
24
+ content: 'data-[state=open]:animate-[collapsible-down_var(--motion-slow)_var(--motion-ease-out)] data-[state=closed]:animate-[collapsible-up_var(--motion-slow)_var(--motion-ease-out)] overflow-hidden text-sm text-muted',
25
+ },
26
+ } as const;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Forest theme fragment — UColorPicker. No delta: the stock recipe is correct as-is.
3
+ *
4
+ * - Selector, gradient canvas and track all sit on rounded-md — already the control-family
5
+ * standard, so there is nothing to override.
6
+ * - The thumbs are rounded-full with the ring-2 ring-white halo — circular handles are the
7
+ * right affordance and the halo is already tactile.
8
+ *
9
+ * The empty export keeps the component in the register as reviewed; the deltas-only test keeps
10
+ * it from ever restating stock.
11
+ */
12
+
13
+ export const colorPickerTheme = {} as const
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Forest theme fragment — UCommandPalette (searchable command list).
3
+ *
4
+ * Deltas only (the preset appends to stock Nuxt UI):
5
+ * - root — rounded-xl: a soft card boundary at the palette container; stock has no radius
6
+ * (flat edges, since the palette usually lives inside a modal/drawer).
7
+ * - label — font-bold + tracking-wide: group headings get brand confidence; stock is
8
+ * font-semibold with no tracking.
9
+ *
10
+ * The `item` hover/active slab is stock before:rounded-md (12px) — the same shape the sidebar's nav
11
+ * rows carry, since UNavigationMenu's `link` takes no Forest radius delta either. A row is a row on
12
+ * both surfaces.
13
+ *
14
+ * Everything else (density ladder, match-mark highlight [&>mark]:text-primary, focus, close/back
15
+ * icons, transitions) is stock on the Forest ramps. Slot names sourced from:
16
+ * apps/docs/node_modules/.nuxt-ui/ui/command-palette.ts
17
+ */
18
+
19
+ export const commandPaletteTheme = {
20
+ slots: {
21
+ root: 'rounded-xl',
22
+ label: 'font-bold tracking-wide',
23
+ },
24
+
25
+ // Touch: each result row promotes ONE size up under a coarse pointer (borrows the next size's
26
+ // padding / gap / type / icon) — same ladder as the menu family. The search `input` slot already
27
+ // ships explicit h-12+ heights so it needs no bump. xl is the ceiling; deltas only. `coarse:`
28
+ // carries the :not(.cut-fixed) escape so a baked mobile cut opts out.
29
+ variants: {
30
+ size: {
31
+ xs: { item: 'coarse:p-1.5 coarse:gap-1.5', label: 'coarse:p-1.5 coarse:gap-1.5' },
32
+ sm: { item: 'coarse:text-sm', label: 'coarse:text-xs', itemLeadingIcon: 'coarse:size-5', itemTrailingIcon: 'coarse:size-5', itemTrailingHighlightedIcon: 'coarse:size-5' },
33
+ md: { item: 'coarse:p-2 coarse:gap-2', label: 'coarse:p-2 coarse:gap-2' },
34
+ lg: { item: 'coarse:text-base', label: 'coarse:text-sm', itemLeadingIcon: 'coarse:size-6', itemTrailingIcon: 'coarse:size-6', itemTrailingHighlightedIcon: 'coarse:size-6' },
35
+ },
36
+ },
37
+ } as const;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Forest theme fragment for UContainer — deliberately EMPTY.
3
+ *
4
+ * UContainer is the WELL: the only thing in the system that caps page width. Stock's recipe is
5
+ * already correct for Forest — `max-w-(--ui-container) mx-auto px-4 sm:px-6 lg:px-8`, where the cap
6
+ * rides the `--ui-container` lever (80rem) and the gutters land on 2rem at `lg`, which is what the
7
+ * docs shell was already hand-typing. So there is no delta to record, only a review.
8
+ *
9
+ * The three well modes are NOT variants here: UContainer exposes no `width` prop, so a Tailwind
10
+ * Variants variant would have nothing to drive it. They ship instead as utilities that re-point
11
+ * `--ui-container` in scope — `well-reading` / `well-full` / `well-flush` in styles/forest.css.
12
+ */
13
+
14
+ export const containerTheme = {} as const
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Forest theme fragment — UContextMenu (right-click contextual menu).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `item` — before:rounded-sm (8 px) — concentric one step inside the 12 px panel, giving
6
+ * the hover/active slab a natural inset. Stock is before:rounded-md.
7
+ * - `label` — font-bold + tracking-wide gives section headings confident hierarchy over item
8
+ * rows; stock is font-semibold with no tracking. Mirrors the CommandPalette label
9
+ * treatment for visual consistency across Forest menu surfaces.
10
+ *
11
+ * Slots touched: content, item, label.
12
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/context-menu.ts
13
+ */
14
+
15
+ export const contextMenuTheme = {
16
+ slots: {
17
+ 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)]',
18
+
19
+ // Item slab: before:rounded-sm (8 px) — concentric inside the 12 px panel.
20
+ item: 'before:rounded-sm',
21
+
22
+ // Section labels: bold weight + wide tracking — confident group hierarchy, not just slightly-bolder body.
23
+ label: 'font-bold tracking-wide',
24
+ },
25
+
26
+ // Touch: each row promotes ONE size up under a coarse pointer (borrows the next size's padding /
27
+ // gap / type / icon), scaling the tap slab as a unit — same ladder as DropdownMenu and the
28
+ // input/button family. xl is the ceiling; deltas only. `coarse:` carries the :not(.cut-fixed)
29
+ // escape so a baked mobile cut opts out.
30
+ variants: {
31
+ size: {
32
+ xs: { item: 'coarse:p-1.5 coarse:gap-1.5', label: 'coarse:p-1.5 coarse:gap-1.5' },
33
+ sm: { item: 'coarse:text-sm', label: 'coarse:text-sm', itemLeadingIcon: 'coarse:size-5', itemTrailingIcon: 'coarse:size-5' },
34
+ md: { item: 'coarse:p-2 coarse:gap-2', label: 'coarse:p-2 coarse:gap-2' },
35
+ lg: { item: 'coarse:text-base', label: 'coarse:text-base', itemLeadingIcon: 'coarse:size-6', itemTrailingIcon: 'coarse:size-6' },
36
+ },
37
+ },
38
+ } as const;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Forest theme fragment for UDashboardGroup.
3
+ *
4
+ * The group is the muted page shell the inset panels float on: bg-muted in light, the deep
5
+ * neutral-950 in dark (bg-muted collapses into the card surface there — the explicit step keeps
6
+ * shell and card separable). text-default seats the base ink for everything inside.
7
+ */
8
+
9
+ export const dashboardGroupTheme = {
10
+ base: 'bg-muted dark:bg-(--ui-color-neutral-950) text-default',
11
+ } as const
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Forest theme fragment for UDashboardNavbar. No delta: the stock recipe is correct as-is.
3
+ *
4
+ * - Height rides --ui-header-height, the hairline is border-default, the title weight is
5
+ * font-semibold (bold via the two-weight re-map) — all already on Forest tokens.
6
+ * - The docs shell composes its crumb and actions through the navbar's slots; that is usage,
7
+ * not theme.
8
+ */
9
+
10
+ export const dashboardNavbarTheme = {} as const
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Forest theme fragment for UDashboardPanel.
3
+ *
4
+ * The Forest dashboard panel is an INSET CARD, not stock's flat full-bleed column: the panel
5
+ * floats on the group's muted shell with a margin (--forest-panel-inset, which the app shell's bands
6
+ * read too so their edges track the card's), soft corners and the relative hairline
7
+ * (--forest-card-ring, the same translucent edge cards use). `lg:ms-0` hugs the panel against
8
+ * the sidebar rail on desktop so the shell reads as one composition, not two islands.
9
+ * `min-h-0` counters stock's min-h-svh — inside the group's fixed viewport the margins would
10
+ * otherwise push the panel past the bottom edge. `overflow-hidden` clips content to the
11
+ * rounded corners.
12
+ */
13
+
14
+ export const dashboardPanelTheme = {
15
+ slots: {
16
+ root: 'bg-default overflow-hidden m-(--forest-panel-inset) lg:ms-0 rounded-lg ring ring-[var(--forest-card-ring)] min-h-0',
17
+ },
18
+ } as const
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Forest theme fragment for UDashboardResizeHandle (flat component, single `base` slot).
3
+ *
4
+ * A grab pill in the middle of the gutter, on hover and while dragging. Hidden at rest: the gutter is a
5
+ * seam between two surfaces, and anything permanent sitting in it reads as a divider rather than as a
6
+ * control — which is what the full-height hairline this replaces did.
7
+ *
8
+ * Short, and it fades rather than moves: the pill has to say "grab here", not draw the edge.
9
+ *
10
+ * It sits CLEAR of the panel's edge rather than centred on the handle. The handle's own box is zero-wide
11
+ * and lands exactly on the seam, so a centred pill straddles the card's rounded corner and reads as part
12
+ * of it; `right-1` backs it off into the shell where it reads as a separate control.
13
+ *
14
+ * Stock draws nothing and carries `cursor-ew-resize`, `touch-none` and a ±6px `before:` hit area. The
15
+ * paint is added here and the hit area is widened to hold it — see below.
16
+ */
17
+
18
+ export const dashboardResizeHandleTheme = {
19
+ base: [
20
+ // ★ THE PILL HAS TO SIT INSIDE THE ZONE THAT REVEALS IT. Drawn to the shell side of the seam against
21
+ // stock's ±6px area, it fell outside: it appeared where the cursor was not, and vanished on approach.
22
+ // Widened leftwards only — the sidebar's links and its scroller's padding both stop 16px short of the
23
+ // seam and its scrollbar is `width: none`, so this reaches nothing. The right stays at stock.
24
+ 'before:-left-3.5',
25
+ 'after:absolute after:top-1/2 after:right-1 after:-translate-y-1/2',
26
+ 'after:h-16 after:w-1 after:rounded-full after:bg-(--ui-text-dimmed)',
27
+ 'after:opacity-0 after:transition-opacity after:duration-(--motion-fast)',
28
+ // ★ ONE-WAY DWELL. The delay rides the HOVER state only, so the pill waits half a second before it
29
+ // appears and leaves the instant the pointer does — the base carries no delay to revert to. A cursor
30
+ // crossing the seam on its way into the sidebar never summons it. `active` clears the wait so a drag
31
+ // that starts before the pill has arrived still shows what is being dragged.
32
+ 'hover:after:opacity-100 hover:after:delay-(--motion-dwell)',
33
+ 'active:after:opacity-100 active:after:delay-0',
34
+ ].join(' '),
35
+ } as const
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Forest theme fragment for UDashboardSidebarCollapse. No delta: the stock recipe is correct
3
+ * as-is — a desktop-only (hidden lg:flex) neutral ghost button whose panel-open/close icon and
4
+ * behaviour come from the dashboard context.
5
+ */
6
+
7
+ export const dashboardSidebarCollapseTheme = {} as const
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Forest theme fragment for UDashboardSidebarToggle. No delta: the stock recipe is correct
3
+ * as-is — a mobile-only (lg:hidden) neutral ghost button whose menu/close icon and behaviour
4
+ * come from the dashboard context.
5
+ */
6
+
7
+ export const dashboardSidebarToggleTheme = {} as const
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Forest theme fragment for UDashboardSidebar.
3
+ *
4
+ * The sidebar sits directly on the group's muted shell with no divider — the inset panel's own
5
+ * hairline does the separating, so stock's border-e (side=left variant) is zeroed at the same
6
+ * variant path. The rail sizing vars and the mobile slideover are stock and ride the tokens.
7
+ *
8
+ * The rest is the COLLAPSED RAIL, which stock does not size for. These are properties of the
9
+ * component at collapsed-size=4, not of any one app, so they belong here rather than as a `:ui` on
10
+ * each consumer — every app would otherwise rediscover them by eye:
11
+ *
12
+ * · Stock spends 16px a side, leaving 32px of content in a 64px rail — too little for an icon-sized
13
+ * row to be square or centred, and narrower than the 41px logo badge. Halved here.
14
+ * · The scrollbar claims another 11px, offsetting every row and the logo from the rail's centre
15
+ * line; expanded, the same bar draws hard against the inset panel's edge and reads as a seam
16
+ * between two surfaces meant to be one. Hidden in BOTH states: a nav column shows its overflow
17
+ * by fading, not by a bar. Never via ::-webkit-scrollbar, which forces the classic wide
18
+ * bar back — see the scrollbar note in styles/forest.css. The `!` is load-bearing: forest.css
19
+ * sets `scrollbar-width: thin` on `*`, which otherwise wins.
20
+ *
21
+ * `data-collapsed` lives on the ROOT and the padding belongs to its children, so the root carries a
22
+ * `group` for header, body and footer to hang off — a footer carries account chrome, which collapses
23
+ * to an avatar and needs the same centre line as the rows above it. Nothing here touches the resize
24
+ * handle: it is a sibling element and keeps its own hit area, cursor and drag.
25
+ *
26
+ * ── The collapse travel ──
27
+ * Stock ships this component with no transition at all: the root is `w-(--width)` and nothing else, so
28
+ * 16rem → 4rem lands in one frame. USidebar DOES carry a `transition` variant, and themes/sidebar.ts
29
+ * re-points its timing at the motion tokens — but that is a different ui key and a different component,
30
+ * and the app rail never reads it. The width travel is re-declared here so the shell obeys the same
31
+ * doctrine: the largest movement the shell makes, starting and ending in view, so --motion-slow on
32
+ * --motion-ease-in-out rather than the near-element pair.
33
+ *
34
+ * ★ DRAGGING MUST OPT OUT. The resize handle writes `--width` on every pointermove, so a transition
35
+ * left on would run the column 240ms behind the pointer for the whole drag. Stock puts `data-dragging`
36
+ * on the root for exactly this, and the attribute selector out-specifies the bare utility, so the
37
+ * override holds whatever order Tailwind emits the two in.
38
+ *
39
+ * Reduced motion needs no gate here: the floor in the motion tokens re-declares transition-property as
40
+ * an allow-list of colour and opacity properties, and `width` is not in it, so the rail snaps.
41
+ */
42
+
43
+ export const dashboardSidebarTheme = {
44
+ slots: {
45
+ root: [
46
+ 'group transition-[width] duration-(--motion-slow) ease-(--motion-ease-in-out)',
47
+ 'data-[dragging=true]:transition-none',
48
+ ].join(' '),
49
+ header: 'group-data-[collapsed=true]:px-2 group-data-[collapsed=true]:justify-center',
50
+ body: '[scrollbar-width:none]! group-data-[collapsed=true]:px-2',
51
+ footer: 'group-data-[collapsed=true]:px-2 group-data-[collapsed=true]:justify-center',
52
+ },
53
+ variants: {
54
+ side: {
55
+ left: { root: 'border-e-0' },
56
+ },
57
+ },
58
+ } as const
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Forest theme fragment for UDashboardToolbar.
3
+ *
4
+ * One delta: the height. Stock hard-codes min-h-[49px], which tracks no token and sits a pixel proud
5
+ * of the navbar's h-(--ui-header-height). A floor rather than a fixed height, because a wrapping
6
+ * filter bar has to grow.
7
+ *
8
+ * Slots sourced from test/fixture/node_modules/.nuxt-ui/ui/dashboard-toolbar.ts.
9
+ */
10
+
11
+ export const dashboardToolbarTheme = {
12
+ slots: {
13
+ root: 'min-h-(--ui-header-height)',
14
+ },
15
+ } as const
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Forest theme fragment — UDrawer (drag-dismissable edge sheet).
3
+ *
4
+ * Deltas only (the preset APPENDS to stock Nuxt UI); slots touched: title, content radius.
5
+ *
6
+ * content radius — rounder edge corners than stock's rounded-*-lg. CRITICAL: stock sets the edge
7
+ * radius in COMPOUND VARIANTS (per direction + inset, e.g. `{direction:bottom, inset:false}
8
+ * → content: rounded-t-lg`), which OUTRANK a base `slots.content` class in tv. A flat
9
+ * `slots: { content: 'rounded-t-xl' }` was silently overridden (rendered rounded-t-lg).
10
+ * So the override must live in matching compoundVariants — appended after stock's, they win
11
+ * the merge. rounded-*-2xl (32px) on the four non-inset directions; the inset (floating)
12
+ * cut steps stock's rounded-lg up to rounded-xl.
13
+ *
14
+ * handle — untouched: stock's neutral `!bg-accented` affordance. Only mobile re-colours it (warm,
15
+ * to sit on that context's peach surface — mobile-theme.ts). NOTE for anyone overriding
16
+ * it: stock's class is IMPORTANT and tw-merge keeps an important class alongside a plain
17
+ * one of the same bucket, so an override has to be important too.
18
+ *
19
+ * title — text-base + tracking-tight, added to stock's font-semibold. Polishes the built-in
20
+ * `title`-prop path; the #header slot (used in the docs demos) is free-form.
21
+ *
22
+ * Recipe shape sourced from apps/docs/node_modules/.nuxt-ui/ui/drawer.ts (radius = compoundVariants).
23
+ */
24
+
25
+ export const drawerTheme = {
26
+ slots: {
27
+ title: 'text-base tracking-tight',
28
+ },
29
+ variants: {
30
+ // Inset (floating) cut rounds all four corners; read the same trait var.
31
+ inset: { true: { content: 'rounded-[var(--forest-drawer-radius)]' } },
32
+ },
33
+ // Edge radius via the --forest-drawer-radius TRAIT (forest.css :root = 1.5rem / xl, standard). The
34
+ // radius MUST live in compoundVariants — stock sets it there per direction+inset, beating any base
35
+ // slot class. But a per-CONTEXT override can't add compoundVariants (context UTheme merges only
36
+ // slots, and those lose to compoundVariants anyway). So the compoundVariant reads a VAR, and mobile/
37
+ // marketing re-point that var with a `[--forest-drawer-radius:2rem]` slot class — which rides the
38
+ // portal and never collides in tw-merge. Same trick as --fx-title-* on the card title.
39
+ compoundVariants: [
40
+ { direction: 'bottom', inset: false, class: { content: 'rounded-t-[var(--forest-drawer-radius)]' } },
41
+ { direction: 'top', inset: false, class: { content: 'rounded-b-[var(--forest-drawer-radius)]' } },
42
+ { direction: 'left', inset: false, class: { content: 'rounded-r-[var(--forest-drawer-radius)]' } },
43
+ { direction: 'right', inset: false, class: { content: 'rounded-l-[var(--forest-drawer-radius)]' } },
44
+ ],
45
+ } as const;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Forest theme fragment — UDropdownMenu (action menu, groups + nesting).
3
+ *
4
+ * Forest opinions applied:
5
+ * - `item` — before:rounded-sm (8 px) — concentric one step inside the 12 px panel, giving the
6
+ * hover/active slab a natural inset. Stock is before:rounded-md.
7
+ * - `label` — font-bold tracking-wide (group + section headings read with brand confidence;
8
+ * stock is font-semibold with no tracking).
9
+ *
10
+ * Slots touched: content · item · label
11
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/dropdown-menu.ts
12
+ */
13
+
14
+ export const dropdownMenuTheme = {
15
+ slots: {
16
+ 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)]',
17
+
18
+ // Item slab: before:rounded-sm (8 px) — concentric inside the 12 px panel.
19
+ item: 'before:rounded-sm',
20
+
21
+ // Group headings: bold weight + wide tracking — confident, readable at small size.
22
+ label: 'font-bold tracking-wide',
23
+ },
24
+
25
+ // Touch: under a coarse pointer each row promotes ONE size up — it borrows the next size's
26
+ // padding / gap / type / icon, so the tap slab scales as a unit (mirrors the input/button ladder).
27
+ // xl is the ceiling. Deltas only: each size lists just the tokens that differ from its next step
28
+ // up (e.g. sm→md changes only type + icon, so no padding delta). `coarse:` carries the
29
+ // :not(.cut-fixed) escape, so a baked mobile cut opts out instead of promoting again.
30
+ variants: {
31
+ size: {
32
+ xs: { item: 'coarse:p-1.5 coarse:gap-1.5', label: 'coarse:p-1.5 coarse:gap-1.5' },
33
+ sm: { item: 'coarse:text-sm', label: 'coarse:text-sm', itemLeadingIcon: 'coarse:size-5', itemTrailingIcon: 'coarse:size-5' },
34
+ md: { item: 'coarse:p-2 coarse:gap-2', label: 'coarse:p-2 coarse:gap-2' },
35
+ lg: { item: 'coarse:text-base', label: 'coarse:text-base', itemLeadingIcon: 'coarse:size-6', itemTrailingIcon: 'coarse:size-6' },
36
+ },
37
+ },
38
+ } as const;