@humanforest/nuxt-layer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +67 -0
- package/README.md +208 -0
- package/app.config.ts +10 -0
- package/assets/icons/forest/bike-asterisk.svg +21 -0
- package/assets/icons/forest/bike-down.svg +19 -0
- package/assets/icons/forest/bike-off.svg +20 -0
- package/assets/icons/forest/bike-up.svg +19 -0
- package/assets/icons/forest/bike-x.svg +19 -0
- package/assets/icons/forest/bike.svg +18 -0
- package/assets/icons/forest/mark-fill.svg +6 -0
- package/assets/icons/forest/mark.svg +6 -0
- package/assets/icons/forest/parking.svg +3 -0
- package/assets/icons/forest/star-fill.svg +4 -0
- package/assets/icons/forest/star.svg +4 -0
- package/assets/icons/forest/traffic-light-caution.svg +8 -0
- package/assets/icons/forest/traffic-light-go.svg +8 -0
- package/assets/icons/forest/traffic-light-lit.svg +8 -0
- package/assets/icons/forest/traffic-light-stop.svg +8 -0
- package/assets/icons/forest/traffic-light.svg +8 -0
- package/nuxt.config.ts +81 -0
- package/package.json +35 -0
- package/packages/charts/src/FBoroughShape.vue +52 -0
- package/packages/charts/src/FCalendarGrid.vue +332 -0
- package/packages/charts/src/FCellLegend.vue +54 -0
- package/packages/charts/src/FChartFrame.vue +201 -0
- package/packages/charts/src/FChartFrameSingle.vue +91 -0
- package/packages/charts/src/FChartLegend.vue +26 -0
- package/packages/charts/src/FDistributionBar.vue +224 -0
- package/packages/charts/src/FDottedMap.vue +335 -0
- package/packages/charts/src/FLondonMap.vue +399 -0
- package/packages/charts/src/FSparkline.vue +198 -0
- package/packages/charts/src/FStatusTrack.vue +151 -0
- package/packages/charts/src/cellSize.ts +46 -0
- package/packages/charts/src/distribution.ts +99 -0
- package/packages/charts/src/engine.ts +15 -0
- package/packages/charts/src/forestTooltip.ts +110 -0
- package/packages/charts/src/fromCategories.ts +43 -0
- package/packages/charts/src/index.ts +87 -0
- package/packages/charts/src/londonAreas.ts +75 -0
- package/packages/charts/src/londonGrid.ts +67 -0
- package/packages/charts/src/motionDuration.ts +49 -0
- package/packages/charts/src/presets.ts +143 -0
- package/packages/charts/src/resolveVar.ts +21 -0
- package/packages/charts/src/snapTooltip.ts +154 -0
- package/packages/charts/src/tooltipStandIn.ts +58 -0
- package/packages/charts/src/useCellScale.ts +155 -0
- package/packages/charts/src/useChartPalette.ts +61 -0
- package/packages/charts/src/useChartRepaintKey.ts +22 -0
- package/packages/charts/src/useThemeVersion.ts +17 -0
- package/packages/frames/src/FBrowserFrame.vue +53 -0
- package/packages/frames/src/FDeviceFrame.vue +242 -0
- package/packages/frames/src/FFitFrame.vue +83 -0
- package/packages/frames/src/art/iphone-11.svg +124 -0
- package/packages/frames/src/art/iphone-12.svg +601 -0
- package/packages/frames/src/art/iphone-13-pro.svg +820 -0
- package/packages/frames/src/art/iphone-13.svg +849 -0
- package/packages/frames/src/art/iphone-14.svg +681 -0
- package/packages/frames/src/art/iphone-15-pro.svg +633 -0
- package/packages/frames/src/art/iphone-15.svg +713 -0
- package/packages/frames/src/art/iphone-16-plus.svg +685 -0
- package/packages/frames/src/art/iphone-16-pro-max.svg +611 -0
- package/packages/frames/src/art/iphone-16-pro.svg +631 -0
- package/packages/frames/src/art/iphone-16.svg +713 -0
- package/packages/frames/src/art/iphone-17-pro-max.svg +355 -0
- package/packages/frames/src/art/iphone-17.svg +305 -0
- package/packages/frames/src/art/iphone-air.svg +389 -0
- package/packages/frames/src/art/iphone-se-3.svg +473 -0
- package/packages/frames/src/art/iphone-x.svg +652 -0
- package/packages/frames/src/art/pixel-10-pro-xl.svg +32 -0
- package/packages/frames/src/art/pixel-10-pro.svg +32 -0
- package/packages/frames/src/art/pixel-10.svg +32 -0
- package/packages/frames/src/art/pixel-8-pro.svg +208 -0
- package/packages/frames/src/art/pixel-8.svg +208 -0
- package/packages/frames/src/art/pixel-8a.svg +8 -0
- package/packages/frames/src/art/pixel-9-pro-xl.svg +32 -0
- package/packages/frames/src/art/pixel-9-pro.svg +34 -0
- package/packages/frames/src/art/pixel-9a.svg +25 -0
- package/packages/frames/src/art.ts +29 -0
- package/packages/frames/src/devices.ts +337 -0
- package/packages/frames/src/index.ts +15 -0
- package/packages/tokens/geo/london-boroughs.json +1 -0
- package/packages/tokens/geo/london-dots-coarse.json +1 -0
- package/packages/tokens/geo/london-dots-fine.json +1 -0
- package/packages/tokens/geo/london-dots-medium.json +1 -0
- package/packages/tokens/geo/london-dots-ultra.json +1 -0
- package/packages/tokens/geo/london-wards.json +1 -0
- package/packages/tokens/glyphs/glyphs.json +1 -0
- package/packages/tokens/logos/f-rot.svg +4 -0
- package/packages/tokens/logos/f.svg +4 -0
- package/packages/tokens/logos/favicon-c2w.svg +9 -0
- package/packages/tokens/logos/favicon-console.svg +9 -0
- package/packages/tokens/logos/favicon-ds.svg +9 -0
- package/packages/tokens/logos/favicon-fleet.svg +9 -0
- package/packages/tokens/logos/favicon-flex.svg +9 -0
- package/packages/tokens/logos/favicon-plus.svg +9 -0
- package/packages/tokens/logos/favicon-radar.svg +9 -0
- package/packages/tokens/logos/favicon-tower.svg +9 -0
- package/packages/tokens/logos/lockup-horizontal.svg +17 -0
- package/packages/tokens/logos/lockup-vertical.svg +17 -0
- package/packages/tokens/logos/mark-mono.svg +4 -0
- package/packages/tokens/logos/mark.svg +7 -0
- package/packages/tokens/logos/wordmark.svg +9 -0
- package/packages/tokens/src/colourEngine.ts +604 -0
- package/packages/tokens/src/glyphs.ts +127 -0
- package/packages/tokens/src/logo.ts +50 -0
- package/packages/tokens/src/subbrands.ts +44 -0
- package/packages/tokens/src/sublogo.ts +80 -0
- package/packages/ui/icons/forest/map/forest-place-count.svg +21 -0
- package/packages/ui/icons/forest/map/forest-place-marker.svg +17 -0
- package/packages/ui/icons/forest/map/forest-vehicle-marker.svg +73 -0
- package/packages/ui/icons/forest/map/manifest.json +373 -0
- package/packages/ui/icons/forest/map/place-count-manifest.json +83 -0
- package/packages/ui/icons/forest/map/place-manifest.json +74 -0
- package/packages/ui/src/address/address.ts +125 -0
- package/packages/ui/src/address/autofill.ts +78 -0
- package/packages/ui/src/address/geocode.ts +55 -0
- package/packages/ui/src/address/index.ts +31 -0
- package/packages/ui/src/address/validate.ts +91 -0
- package/packages/ui/src/canvas/FCanvasCard.vue +97 -0
- package/packages/ui/src/canvas/FCanvasControl.vue +121 -0
- package/packages/ui/src/canvas/FCanvasControlGroup.vue +100 -0
- package/packages/ui/src/canvas/FCanvasFlyout.vue +216 -0
- package/packages/ui/src/canvas/FCanvasHud.vue +402 -0
- package/packages/ui/src/canvas/canvasSurface.ts +39 -0
- package/packages/ui/src/canvas/index.ts +17 -0
- package/packages/ui/src/contexts/ForestContext.vue +53 -0
- package/packages/ui/src/contexts/contexts.ts +153 -0
- package/packages/ui/src/contexts/index.ts +23 -0
- package/packages/ui/src/contexts/marketing-theme.ts +214 -0
- package/packages/ui/src/contexts/mobile-theme.ts +402 -0
- package/packages/ui/src/filter/FFilterBar.vue +72 -0
- package/packages/ui/src/filter/FFilterChip.vue +362 -0
- package/packages/ui/src/filter/FFilterClauses.vue +104 -0
- package/packages/ui/src/filter/FFilterTrigger.vue +183 -0
- package/packages/ui/src/filter/filter.theme.ts +27 -0
- package/packages/ui/src/filter/filterClause.ts +265 -0
- package/packages/ui/src/filter/filterPredicate.ts +134 -0
- package/packages/ui/src/filter/filterSchema.ts +82 -0
- package/packages/ui/src/filter/index.ts +36 -0
- package/packages/ui/src/forest-preset.ts +68 -0
- package/packages/ui/src/icons.ts +79 -0
- package/packages/ui/src/index.ts +17 -0
- package/packages/ui/src/kpi/FKpi.vue +253 -0
- package/packages/ui/src/kpi/FKpiGroup.vue +161 -0
- package/packages/ui/src/kpi/index.ts +20 -0
- package/packages/ui/src/kpi/kpi.theme.ts +168 -0
- package/packages/ui/src/kpi/kpiDelta.ts +82 -0
- package/packages/ui/src/kpi/kpiFormat.ts +72 -0
- package/packages/ui/src/kpi/kpiGroupState.ts +63 -0
- package/packages/ui/src/logo/FLogo.vue +52 -0
- package/packages/ui/src/logo/FSubLogo.vue +214 -0
- package/packages/ui/src/logo/index.ts +8 -0
- package/packages/ui/src/map/FMap.vue +355 -0
- package/packages/ui/src/map/FMapCompass.vue +154 -0
- package/packages/ui/src/map/FMapCoordinates.vue +134 -0
- package/packages/ui/src/map/FMapDrawTools.vue +116 -0
- package/packages/ui/src/map/FMapFullscreen.vue +92 -0
- package/packages/ui/src/map/FMapGeolocate.vue +208 -0
- package/packages/ui/src/map/FMapLayers.vue +75 -0
- package/packages/ui/src/map/FMapLegend.vue +45 -0
- package/packages/ui/src/map/FMapPopup.vue +129 -0
- package/packages/ui/src/map/FMapRamp.vue +69 -0
- package/packages/ui/src/map/FMapRegion.vue +77 -0
- package/packages/ui/src/map/FMapScale.vue +107 -0
- package/packages/ui/src/map/FMapSearch.vue +362 -0
- package/packages/ui/src/map/FMapTilt.vue +87 -0
- package/packages/ui/src/map/FMapTools.vue +61 -0
- package/packages/ui/src/map/FMapZoom.vue +81 -0
- package/packages/ui/src/map/FMarker.vue +218 -0
- package/packages/ui/src/map/FTrackScrubber.vue +94 -0
- package/packages/ui/src/map/FVehicleLegend.vue +72 -0
- package/packages/ui/src/map/FVehicleMark.vue +166 -0
- package/packages/ui/src/map/bin.ts +196 -0
- package/packages/ui/src/map/coordinates.ts +83 -0
- package/packages/ui/src/map/density.ts +401 -0
- package/packages/ui/src/map/draw.ts +356 -0
- package/packages/ui/src/map/ease.ts +67 -0
- package/packages/ui/src/map/fullscreen.ts +59 -0
- package/packages/ui/src/map/heat.ts +160 -0
- package/packages/ui/src/map/index.ts +351 -0
- package/packages/ui/src/map/liveMarkers.ts +60 -0
- package/packages/ui/src/map/map.css +116 -0
- package/packages/ui/src/map/mapColor.ts +31 -0
- package/packages/ui/src/map/mapContext.ts +12 -0
- package/packages/ui/src/map/mapLayers.ts +55 -0
- package/packages/ui/src/map/mapLegend.ts +70 -0
- package/packages/ui/src/map/mapSearch.theme.ts +79 -0
- package/packages/ui/src/map/mapSearch.ts +279 -0
- package/packages/ui/src/map/mapUrl.ts +117 -0
- package/packages/ui/src/map/markerAsset.ts +3 -0
- package/packages/ui/src/map/padding.ts +39 -0
- package/packages/ui/src/map/place.ts +192 -0
- package/packages/ui/src/map/ramp.ts +95 -0
- package/packages/ui/src/map/regionSelect.ts +163 -0
- package/packages/ui/src/map/route.ts +167 -0
- package/packages/ui/src/map/scale.ts +329 -0
- package/packages/ui/src/map/scaleBar.ts +67 -0
- package/packages/ui/src/map/selectionModel.ts +50 -0
- package/packages/ui/src/map/sources/coordinates.ts +58 -0
- package/packages/ui/src/map/sources/mapboxPlaces.ts +71 -0
- package/packages/ui/src/map/style.ts +14 -0
- package/packages/ui/src/map/track.ts +279 -0
- package/packages/ui/src/map/useFeatureSelect.ts +230 -0
- package/packages/ui/src/map/useLiveMarkers.ts +84 -0
- package/packages/ui/src/map/useMap.ts +150 -0
- package/packages/ui/src/map/useMapDraw.ts +776 -0
- package/packages/ui/src/map/useMapOverlay.ts +421 -0
- package/packages/ui/src/map/useMapTableSelection.ts +101 -0
- package/packages/ui/src/map/useMapUrlState.ts +155 -0
- package/packages/ui/src/map/useMarkerSource.ts +163 -0
- package/packages/ui/src/map/useRegionSelect.ts +309 -0
- package/packages/ui/src/map/useTokenMode.ts +61 -0
- package/packages/ui/src/map/useTrackPlayback.ts +131 -0
- package/packages/ui/src/map/useVehicleFilter.ts +51 -0
- package/packages/ui/src/map/useVisibleFeatures.ts +124 -0
- package/packages/ui/src/map/vehicle.ts +807 -0
- package/packages/ui/src/map/vehicleFilter.ts +284 -0
- package/packages/ui/src/map/vehicleLegend.ts +155 -0
- package/packages/ui/src/map/visibleFeatures.ts +192 -0
- package/packages/ui/src/map/zone.ts +291 -0
- package/packages/ui/src/shell/FAppNavbar.vue +68 -0
- package/packages/ui/src/shell/FAppShell.vue +163 -0
- package/packages/ui/src/shell/FAppSidebar.vue +215 -0
- package/packages/ui/src/shell/FPageToolbar.vue +126 -0
- package/packages/ui/src/shell/FScrollShadow.vue +43 -0
- package/packages/ui/src/shell/FSidePanel.vue +258 -0
- package/packages/ui/src/shell/FToolbarActions.vue +32 -0
- package/packages/ui/src/shell/index.ts +33 -0
- package/packages/ui/src/shell/nav.ts +37 -0
- package/packages/ui/src/shell/pageToolbar.ts +69 -0
- package/packages/ui/src/shell/shell.ts +74 -0
- package/packages/ui/src/shell/sidePanel.ts +115 -0
- package/packages/ui/src/themes/accordion.ts +36 -0
- package/packages/ui/src/themes/alert.ts +16 -0
- package/packages/ui/src/themes/auth-form.ts +19 -0
- package/packages/ui/src/themes/avatar-group.ts +9 -0
- package/packages/ui/src/themes/avatar.ts +18 -0
- package/packages/ui/src/themes/badge.ts +51 -0
- package/packages/ui/src/themes/banner.ts +21 -0
- package/packages/ui/src/themes/breadcrumb.ts +10 -0
- package/packages/ui/src/themes/button.mechanics.ts +191 -0
- package/packages/ui/src/themes/button.ts +136 -0
- package/packages/ui/src/themes/calendar.ts +29 -0
- package/packages/ui/src/themes/card.ts +67 -0
- package/packages/ui/src/themes/carousel.ts +41 -0
- package/packages/ui/src/themes/checkbox-group.ts +20 -0
- package/packages/ui/src/themes/checkbox.ts +47 -0
- package/packages/ui/src/themes/chip.ts +50 -0
- package/packages/ui/src/themes/collapsible.ts +26 -0
- package/packages/ui/src/themes/color-picker.ts +13 -0
- package/packages/ui/src/themes/command-palette.ts +37 -0
- package/packages/ui/src/themes/container.ts +14 -0
- package/packages/ui/src/themes/context-menu.ts +38 -0
- package/packages/ui/src/themes/dashboard-group.ts +11 -0
- package/packages/ui/src/themes/dashboard-navbar.ts +10 -0
- package/packages/ui/src/themes/dashboard-panel.ts +18 -0
- package/packages/ui/src/themes/dashboard-resize-handle.ts +35 -0
- package/packages/ui/src/themes/dashboard-sidebar-collapse.ts +7 -0
- package/packages/ui/src/themes/dashboard-sidebar-toggle.ts +7 -0
- package/packages/ui/src/themes/dashboard-sidebar.ts +58 -0
- package/packages/ui/src/themes/dashboard-toolbar.ts +15 -0
- package/packages/ui/src/themes/drawer.ts +45 -0
- package/packages/ui/src/themes/dropdown-menu.ts +38 -0
- package/packages/ui/src/themes/empty.ts +43 -0
- package/packages/ui/src/themes/field-group.ts +52 -0
- package/packages/ui/src/themes/file-upload.ts +38 -0
- package/packages/ui/src/themes/footer-columns.ts +13 -0
- package/packages/ui/src/themes/footer.ts +9 -0
- package/packages/ui/src/themes/form-field.ts +25 -0
- package/packages/ui/src/themes/form.ts +22 -0
- package/packages/ui/src/themes/header.ts +12 -0
- package/packages/ui/src/themes/icon.ts +61 -0
- package/packages/ui/src/themes/index.ts +99 -0
- package/packages/ui/src/themes/input-date.ts +73 -0
- package/packages/ui/src/themes/input-menu.ts +66 -0
- package/packages/ui/src/themes/input-number.ts +58 -0
- package/packages/ui/src/themes/input-rating.ts +25 -0
- package/packages/ui/src/themes/input-tags.ts +63 -0
- package/packages/ui/src/themes/input-time.ts +70 -0
- package/packages/ui/src/themes/input.ts +148 -0
- package/packages/ui/src/themes/kbd.ts +27 -0
- package/packages/ui/src/themes/link.ts +27 -0
- package/packages/ui/src/themes/listbox.ts +48 -0
- package/packages/ui/src/themes/main.ts +6 -0
- package/packages/ui/src/themes/marquee.ts +45 -0
- package/packages/ui/src/themes/modal.ts +46 -0
- package/packages/ui/src/themes/navigation-menu.ts +80 -0
- package/packages/ui/src/themes/page-anchors.ts +7 -0
- package/packages/ui/src/themes/page-aside.ts +7 -0
- package/packages/ui/src/themes/page-card.ts +22 -0
- package/packages/ui/src/themes/page-cta.ts +18 -0
- package/packages/ui/src/themes/page-feature.ts +13 -0
- package/packages/ui/src/themes/page-header.ts +16 -0
- package/packages/ui/src/themes/page-hero.ts +15 -0
- package/packages/ui/src/themes/page-links.ts +12 -0
- package/packages/ui/src/themes/page-section.ts +14 -0
- package/packages/ui/src/themes/page.ts +7 -0
- package/packages/ui/src/themes/pagination.ts +17 -0
- package/packages/ui/src/themes/pin-input.ts +73 -0
- package/packages/ui/src/themes/popover.ts +30 -0
- package/packages/ui/src/themes/progress.ts +29 -0
- package/packages/ui/src/themes/prose.ts +45 -0
- package/packages/ui/src/themes/radio-group.ts +39 -0
- package/packages/ui/src/themes/scroll-area.ts +23 -0
- package/packages/ui/src/themes/select-menu.ts +71 -0
- package/packages/ui/src/themes/select.ts +72 -0
- package/packages/ui/src/themes/separator.ts +28 -0
- package/packages/ui/src/themes/sidebar.ts +127 -0
- package/packages/ui/src/themes/skeleton.ts +15 -0
- package/packages/ui/src/themes/slideover.ts +87 -0
- package/packages/ui/src/themes/slider.ts +48 -0
- package/packages/ui/src/themes/stepper.ts +41 -0
- package/packages/ui/src/themes/switch.ts +63 -0
- package/packages/ui/src/themes/table.ts +34 -0
- package/packages/ui/src/themes/tabs.ts +74 -0
- package/packages/ui/src/themes/textarea.ts +77 -0
- package/packages/ui/src/themes/timeline.ts +25 -0
- package/packages/ui/src/themes/toast.ts +29 -0
- package/packages/ui/src/themes/toaster.ts +66 -0
- package/packages/ui/src/themes/tooltip.ts +31 -0
- package/packages/ui/src/themes/tree.ts +41 -0
- package/packages/ui/src/themes/user.ts +28 -0
- package/styles.cdn.gen.css +2047 -0
- package/theme.gen.json +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UButton mechanics — the shared emphasis recipes and per-colour palettes consumed by ./button.ts.
|
|
3
|
+
* Private to the button theme: deliberately NOT re-exported by ./index.ts, so none of this reaches
|
|
4
|
+
* forestPreset.ui as a component key.
|
|
5
|
+
*
|
|
6
|
+
* NOTE: these class strings MUST be literal — Tailwind extracts utilities by scanning source text,
|
|
7
|
+
* so no template interpolation here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const FOCUS = 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--btn-ring)]';
|
|
11
|
+
|
|
12
|
+
// Emphasis classes — each consumes the per-colour --btn-* vars, so one string covers every intent.
|
|
13
|
+
// The keycap keeps the lip ANCHORED (a box-shadow lip moves with the button — the jank). The whole
|
|
14
|
+
// button (face + label) is the moving key; ::before is the lip slab behind it. ::before
|
|
15
|
+
// counter-translates by the EXACT inverse of the button's lift (--btn-yi/--btn-yhi), so it cancels
|
|
16
|
+
// out and sits at absolute 0 in every state — the key rises/presses, the base never moves.
|
|
17
|
+
// The keycap is built from TWO pseudo-layers, both BELOW the inline label (negative z), so the
|
|
18
|
+
// label always shows. ::after = the face (--btn-face + a 1px --btn-lip border); ::before = the base
|
|
19
|
+
// slab (--btn-lip). The button is a transparent frame that translates — the label + ::after face ride
|
|
20
|
+
// with it (lift on hover, flush on press), while ::before counter-translates by the exact inverse
|
|
21
|
+
// so it stays pinned at net 0. The lip NEVER moves; only the key does.
|
|
22
|
+
// NOTE: Tailwind v4 translate utilities set the `translate` property (not `transform`) — the
|
|
23
|
+
// transition MUST list `translate` or the motion snaps.
|
|
24
|
+
export const KEYCAP = [
|
|
25
|
+
// bg lives on ::after — keep the button frame transparent in every state (clears base-theme
|
|
26
|
+
// `hover:bg-primary` / `active:bg-primary` leaks that would otherwise sit behind the face).
|
|
27
|
+
'relative isolate overflow-visible border-0 text-[var(--btn-ink)]',
|
|
28
|
+
'bg-transparent hover:bg-transparent active:bg-transparent disabled:bg-transparent aria-disabled:bg-transparent',
|
|
29
|
+
// --btn-y-focus / --btn-yi-focus = the FOCUS lift + its ::before counter. Default to the REST values
|
|
30
|
+
// (focus moves nothing — web leans on the ::before ring below). The mobile theme overrides them to a
|
|
31
|
+
// -7px pop, the sole focus signal on touch (no hover to lean on). Living on the KEYCAP recipe (PAD
|
|
32
|
+
// inherits it) is what scopes the pop to the lip variants: the flat variants never read these vars, so
|
|
33
|
+
// the slot-level mobile base can't make them jump on focus.
|
|
34
|
+
// --btn-ring-w = the ::before focus-ring width; mobile sets it to 0 (pop replaces the ring). Routing
|
|
35
|
+
// the ring through a VAR (instead of a `before:outline-0!` override in the mobile base) is deliberate:
|
|
36
|
+
// a `before:` utility on the slot-level base auto-adds `content:''`, which on the flat variants — whose
|
|
37
|
+
// ::before is NOT absolute — becomes an in-flow flex item, and `gap-1.5` then shoves the label right
|
|
38
|
+
// (a phantom leading gap on focus). Keeping the only `before:` here, where ::before is absolute, avoids it.
|
|
39
|
+
'[--btn-y-focus:var(--btn-y)] [--btn-yi-focus:var(--btn-yi)] [--btn-ring-w:2px]',
|
|
40
|
+
// motion-reduce: the lift/press still HAPPENS (it's state, not decoration) but snaps instead of
|
|
41
|
+
// animating — no positional animation under prefers-reduced-motion (WCAG 2.3.3).
|
|
42
|
+
'translate-y-[var(--btn-y)] transition-[translate] duration-(--motion-instant) ease-(--motion-ease-out) motion-reduce:transition-none',
|
|
43
|
+
'hover:translate-y-[var(--btn-y-h)] active:translate-y-0 focus-visible:translate-y-[var(--btn-y-focus)]',
|
|
44
|
+
'disabled:translate-y-0 aria-disabled:translate-y-0',
|
|
45
|
+
// face — rides with the button (label sits above it), darkens on hover/press
|
|
46
|
+
// Edge = a CSS border on the absolute, border-box ::after (face). No layout shift (the pseudo is out
|
|
47
|
+
// of flow; border draws inside inset-0), even/concentric corners, no sliver. Width = --btn-edge (1px web,
|
|
48
|
+
// 2px mobile via the platform override).
|
|
49
|
+
"after:content-[''] after:absolute after:inset-0 after:-z-10 after:rounded-[inherit] after:bg-[var(--btn-face)] after:border-solid after:[border-width:var(--btn-edge,1px)] after:border-[var(--btn-lip)] after:transition-[background-color] after:duration-(--motion-instant) after:ease-(--motion-ease-out)",
|
|
50
|
+
'hover:after:bg-[var(--btn-hover)] active:after:bg-[var(--btn-hover)]',
|
|
51
|
+
// base slab. Bottom stays anchored (counter-translate cancels the button's lift); the TOP tracks
|
|
52
|
+
// the face lift (--btn-y per state) so the slab spans the FULL keycap silhouette — face top → lip
|
|
53
|
+
// bottom — in EVERY state (lifted at rest/hover, flush when pressed/disabled). The visible lip is
|
|
54
|
+
// still only the bottom strip: the extended top sits behind the opaque face. `top` animates with
|
|
55
|
+
// the lift so the ring tracks smoothly.
|
|
56
|
+
// Lip = a STATIC slab (full footprint, constant shape). It never transforms: the key (button +
|
|
57
|
+
// face) translates, and ::before counter-translates by the exact inverse so it stays anchored at
|
|
58
|
+
// net-0 in every state. Only the face moving over it reveals/hides the lip — the lip never morphs.
|
|
59
|
+
"before:content-[''] before:absolute before:inset-x-0 before:bottom-0 before:top-[var(--btn-y)] before:-z-20 before:rounded-[inherit] before:bg-[var(--btn-lip)]",
|
|
60
|
+
'before:translate-y-[var(--btn-yi)] before:transition-[translate,top] before:duration-(--motion-instant) before:ease-(--motion-ease-out) before:motion-reduce:transition-none',
|
|
61
|
+
'hover:before:translate-y-[var(--btn-yhi)] hover:before:top-[var(--btn-y-h)] active:before:translate-y-0 active:before:top-0',
|
|
62
|
+
// ::before tracks the FOCUS lift so the slab stays anchored when the key pops (mobile); on web the
|
|
63
|
+
// focus vars equal the rest values, so it doesn't budge.
|
|
64
|
+
'focus-visible:before:top-[var(--btn-y-focus)] focus-visible:before:translate-y-[var(--btn-yi-focus)]',
|
|
65
|
+
'disabled:before:translate-y-0 disabled:before:top-0 aria-disabled:before:translate-y-0 aria-disabled:before:top-0',
|
|
66
|
+
// Focus ring rides on ::before — which now spans the full silhouette — NOT the button, so it wraps
|
|
67
|
+
// the whole key with an EVEN 2px gap on all sides in every state. outline-none kills the button's
|
|
68
|
+
// UA default ring; rounded-[inherit] keeps the corners matched.
|
|
69
|
+
'focus-visible:outline-none focus-visible:before:outline-[length:var(--btn-ring-w)] focus-visible:before:outline-offset-2 focus-visible:before:outline-[var(--btn-ring)]',
|
|
70
|
+
].join(' ');
|
|
71
|
+
|
|
72
|
+
export const PAD = [
|
|
73
|
+
// Quiet tactile key — the KEYCAP engine fed the TINT ramp instead of the solid face: tint fill
|
|
74
|
+
// (t1→t2→t3), a line-colour border + lip, dark text. Same lift/press/anchored focus ring as the
|
|
75
|
+
// keycap, lower emphasis (subtle's calm + the keycap's feel). Each remap is declared TWICE — plain
|
|
76
|
+
// AND `dark:` — because the colour PALs set `dark:--btn-*`, and a `.dark`-scoped rule outranks a
|
|
77
|
+
// plain one; without the dark: copy the PAL's dark face beats the remap (pad == keycap in dark).
|
|
78
|
+
// var(--btn-t1) etc. are already mode-aware, so both copies point at the live tint value.
|
|
79
|
+
'[--btn-face:var(--btn-t1)] dark:[--btn-face:var(--btn-t1)] [--btn-hover:var(--btn-t2)] dark:[--btn-hover:var(--btn-t2)] [--btn-press:var(--btn-t3)] dark:[--btn-press:var(--btn-t3)] [--btn-lip:var(--btn-line)] dark:[--btn-lip:var(--btn-line)] [--btn-ink:var(--btn-text)] dark:[--btn-ink:var(--btn-text)]',
|
|
80
|
+
KEYCAP,
|
|
81
|
+
].join(' ');
|
|
82
|
+
|
|
83
|
+
export const FRAMED = [
|
|
84
|
+
// Filled face + an INSET RING border (Nuxt-style: a box-shadow, NOT a CSS border — so it adds no
|
|
85
|
+
// height, unlike `border-2`, keeping every variant the same size). The lip demotes to the ring. No
|
|
86
|
+
// lip, no motion; leans on face→hover→press. Keeps light-face fills (amber, secondary) visible.
|
|
87
|
+
'ring-[length:var(--btn-edge,1px)] ring-inset ring-[var(--btn-lip)] text-[var(--btn-ink)] bg-[var(--btn-face)]',
|
|
88
|
+
'hover:bg-[var(--btn-hover)] active:bg-[var(--btn-press)]',
|
|
89
|
+
FOCUS,
|
|
90
|
+
].join(' ');
|
|
91
|
+
|
|
92
|
+
export const SOLID = [
|
|
93
|
+
// Plain filled face — no lip, no border, no motion (the standard Nuxt-style solid). Feedback
|
|
94
|
+
// comes from the face→hover→press colour steps. A light face (amber, secondary) has no edge on a
|
|
95
|
+
// light surface by design — reach for `framed` when a light solid needs a border.
|
|
96
|
+
'border-0 text-[var(--btn-ink)] bg-[var(--btn-face)]',
|
|
97
|
+
'hover:bg-[var(--btn-hover)] active:bg-[var(--btn-press)]',
|
|
98
|
+
FOCUS,
|
|
99
|
+
].join(' ');
|
|
100
|
+
|
|
101
|
+
export const GLOSS = [
|
|
102
|
+
// Web-only "gloss" emphasis — framed's lip outline with a line of light ONE pixel inside it along the
|
|
103
|
+
// top (the shine sits below the border, over the face — never on the border itself). The line is the
|
|
104
|
+
// face's OWN colour lightened (oklch l + --btn-shine-l, default 0.25), not white, so the sheen is
|
|
105
|
+
// on-brand per intent and works in both modes. Neutral's near-black face needs a bigger lift to read
|
|
106
|
+
// (PAL_NEUTRAL bumps --btn-shine-l to 0.5 in light; dark stays 0.25 — its near-white face would clip).
|
|
107
|
+
// One box-shadow, two inset layers: layer 1 (FRONT) the lip outline, --btn-edge
|
|
108
|
+
// wide; layer 2 (BEHIND) a light band offset calc(edge + 1px), so the outline covers its top `edge` px
|
|
109
|
+
// and only the next 1px shows. Both inset → footprint identical to framed.
|
|
110
|
+
'border-0 text-[var(--btn-ink)] bg-[var(--btn-face)]',
|
|
111
|
+
'shadow-[inset_0_0_0_var(--btn-edge,1px)_var(--btn-lip),inset_0_calc(var(--btn-edge,1px)_+_1px)_0_0_oklch(from_var(--btn-face)_calc(l_+_var(--btn-shine-l,0.25))_c_h)]',
|
|
112
|
+
// hover/press shift --btn-face itself (not just bg), so BOTH the fill and the shine (derived from
|
|
113
|
+
// --btn-face) track the current state — the line stays +0.25 lighter than whatever face is showing.
|
|
114
|
+
'hover:[--btn-face:var(--btn-hover)] active:[--btn-face:var(--btn-press)]',
|
|
115
|
+
FOCUS,
|
|
116
|
+
].join(' ');
|
|
117
|
+
|
|
118
|
+
export const SUBTLE = [
|
|
119
|
+
// A ramp-pinned INSET RING (a box-shadow, not a border → no added height) over a plain surface fill.
|
|
120
|
+
// Our width + colour override Nuxt's native subtle ring (`ring ring-inset ring-{color}/25`), so
|
|
121
|
+
// there's a single edge; `ring-inset` itself comes from that stock compound (deltas only). Same
|
|
122
|
+
// ring approach as OUTLINE. Rest sits on `bg-default` (not the --btn-t1 tint) so it reads like a
|
|
123
|
+
// select's outline field — white at rest, tinting to --btn-t2 on hover — rather than a filled slab.
|
|
124
|
+
'ring-[length:var(--btn-edge,1px)] ring-[var(--btn-line)] text-[var(--btn-text)] bg-default',
|
|
125
|
+
'hover:bg-[var(--btn-t2)] active:bg-[var(--btn-t3)]',
|
|
126
|
+
FOCUS,
|
|
127
|
+
].join(' ');
|
|
128
|
+
|
|
129
|
+
export const OUTLINE = [
|
|
130
|
+
// Inset ring (box-shadow), NOT a CSS border — overrides Nuxt's native outline ring's width and
|
|
131
|
+
// colour and adds no height, so every variant stays the same size. `ring-inset` itself comes
|
|
132
|
+
// from the stock outline compound (deltas only); FRAMED keeps its own — no stock counterpart.
|
|
133
|
+
'ring-[length:var(--btn-edge,1px)] ring-[var(--btn-line)] text-[var(--btn-text)] bg-transparent',
|
|
134
|
+
// Border holds at --btn-line through hover — only the fill tints — so it reads like an input, not a
|
|
135
|
+
// ring that darkens on approach.
|
|
136
|
+
'hover:bg-[var(--btn-t1)] active:bg-[var(--btn-t2)]',
|
|
137
|
+
FOCUS,
|
|
138
|
+
].join(' ');
|
|
139
|
+
|
|
140
|
+
export const SOFT = [
|
|
141
|
+
// Tinted fill: rest t1 → hover t2 → press t3.
|
|
142
|
+
'text-[var(--btn-text)] bg-[var(--btn-t1)]',
|
|
143
|
+
'hover:bg-[var(--btn-t2)] active:bg-[var(--btn-t3)]',
|
|
144
|
+
FOCUS,
|
|
145
|
+
].join(' ');
|
|
146
|
+
|
|
147
|
+
export const GHOST = [
|
|
148
|
+
// Transparent rest → hover t1 → press t2.
|
|
149
|
+
'text-[var(--btn-text)] bg-transparent',
|
|
150
|
+
'hover:bg-[var(--btn-t1)] active:bg-[var(--btn-t2)]',
|
|
151
|
+
FOCUS,
|
|
152
|
+
].join(' ');
|
|
153
|
+
|
|
154
|
+
export const LINK = [
|
|
155
|
+
// Text-only, link-styled — no fill, no border. Underlines on hover and deepens to the press
|
|
156
|
+
// colour. Tertiary actions ("Learn more"). Inherits the size padding so it lines up in a button row.
|
|
157
|
+
'border-0 bg-transparent text-[var(--btn-text)] underline-offset-4',
|
|
158
|
+
'hover:underline hover:text-[var(--btn-press)] active:text-[var(--btn-press)]',
|
|
159
|
+
FOCUS,
|
|
160
|
+
].join(' ');
|
|
161
|
+
|
|
162
|
+
// Docs mirror: apps/docs/src/pages/components/ButtonPage.vue (Specs + Engineering notes) restates
|
|
163
|
+
// these recipes in prose — keep both in sync.
|
|
164
|
+
// Per-colour --btn-* palettes. LITERAL strings (Tailwind-scannable), sourced from the OKLCH ramp
|
|
165
|
+
// (--<family>-50..950). Solids step one level lighter in dark so they pop on a dark surface; the
|
|
166
|
+
// tint / text / line layers flip too, since the ramp is monotonic. Ramp levels — light / dark:
|
|
167
|
+
// face 500/400 · hover 600/500 · press 700/600 · lip 700/600 · ring 500 · t1 100/900 · t2 200/800 · t3 300/700 · line 400/600 · text 700/300 · ink 0/950
|
|
168
|
+
// On-solid ink follows FACE lightness, not mode: a dark face takes the light 0, a light face the dark 950.
|
|
169
|
+
// Solids use 50 in light / 950 in dark; amber's face is light in BOTH modes, so it's 950 in both — same
|
|
170
|
+
// rule, not a special case. Secondary→warm is a standard colour key (face 500/400). Neutral is the
|
|
171
|
+
// exception: Nuxt-style monochrome — face-based variants invert (900/50), the rest are surface greys.
|
|
172
|
+
export const PAL_FOREST = '[--btn-face:var(--forest-500)] dark:[--btn-face:var(--forest-400)] [--btn-hover:var(--forest-600)] dark:[--btn-hover:var(--forest-500)] [--btn-press:var(--forest-700)] dark:[--btn-press:var(--forest-600)] [--btn-lip:var(--forest-700)] dark:[--btn-lip:var(--forest-600)] [--btn-ring:var(--forest-500)] [--btn-t1:var(--forest-100)] dark:[--btn-t1:var(--forest-900)] [--btn-t2:var(--forest-200)] dark:[--btn-t2:var(--forest-800)] [--btn-t3:var(--forest-300)] dark:[--btn-t3:var(--forest-700)] [--btn-line:var(--forest-400)] dark:[--btn-line:var(--forest-600)] [--btn-text:var(--forest-700)] dark:[--btn-text:var(--forest-300)] [--btn-ink:var(--forest-0)] dark:[--btn-ink:var(--forest-950)]';
|
|
173
|
+
export const PAL_RIVER = '[--btn-face:var(--river-500)] dark:[--btn-face:var(--river-400)] [--btn-hover:var(--river-600)] dark:[--btn-hover:var(--river-500)] [--btn-press:var(--river-700)] dark:[--btn-press:var(--river-600)] [--btn-lip:var(--river-700)] dark:[--btn-lip:var(--river-600)] [--btn-ring:var(--river-500)] [--btn-t1:var(--river-100)] dark:[--btn-t1:var(--river-900)] [--btn-t2:var(--river-200)] dark:[--btn-t2:var(--river-800)] [--btn-t3:var(--river-300)] dark:[--btn-t3:var(--river-700)] [--btn-line:var(--river-400)] dark:[--btn-line:var(--river-600)] [--btn-text:var(--river-700)] dark:[--btn-text:var(--river-300)] [--btn-ink:var(--river-0)] dark:[--btn-ink:var(--river-950)]';
|
|
174
|
+
export const PAL_MAPLE = '[--btn-face:var(--maple-500)] dark:[--btn-face:var(--maple-400)] [--btn-hover:var(--maple-600)] dark:[--btn-hover:var(--maple-500)] [--btn-press:var(--maple-700)] dark:[--btn-press:var(--maple-600)] [--btn-lip:var(--maple-700)] dark:[--btn-lip:var(--maple-600)] [--btn-ring:var(--maple-500)] [--btn-t1:var(--maple-100)] dark:[--btn-t1:var(--maple-900)] [--btn-t2:var(--maple-200)] dark:[--btn-t2:var(--maple-800)] [--btn-t3:var(--maple-300)] dark:[--btn-t3:var(--maple-700)] [--btn-line:var(--maple-400)] dark:[--btn-line:var(--maple-600)] [--btn-text:var(--maple-700)] dark:[--btn-text:var(--maple-300)] [--btn-ink:var(--maple-0)] dark:[--btn-ink:var(--maple-950)]';
|
|
175
|
+
// Warning = light solid: a 300 face with frozen dark ink (a dark amber would fail dark-ink contrast).
|
|
176
|
+
export const PAL_AMBER = '[--btn-face:var(--amber-300)] [--btn-hover:var(--amber-400)] [--btn-press:var(--amber-500)] [--btn-lip:var(--amber-600)] [--btn-ring:var(--amber-500)] [--btn-t1:var(--amber-100)] dark:[--btn-t1:var(--amber-900)] [--btn-t2:var(--amber-200)] dark:[--btn-t2:var(--amber-800)] [--btn-t3:var(--amber-300)] dark:[--btn-t3:var(--amber-700)] [--btn-line:var(--amber-400)] dark:[--btn-line:var(--amber-600)] [--btn-text:var(--amber-700)] dark:[--btn-text:var(--amber-300)] [--btn-ink:var(--amber-950)]';
|
|
177
|
+
// Secondary = warm intent — standard colour key (face 500/400 like the brand families); white ink on
|
|
178
|
+
// the dark 500 face, dark ink on the lighter 400 in dark mode. Own warm focus ring.
|
|
179
|
+
export const PAL_SECONDARY = '[--btn-face:var(--warm-500)] dark:[--btn-face:var(--warm-400)] [--btn-hover:var(--warm-600)] dark:[--btn-hover:var(--warm-500)] [--btn-press:var(--warm-700)] dark:[--btn-press:var(--warm-600)] [--btn-lip:var(--warm-700)] dark:[--btn-lip:var(--warm-600)] [--btn-ring:var(--warm-500)] [--btn-t1:var(--warm-100)] dark:[--btn-t1:var(--warm-900)] [--btn-t2:var(--warm-200)] dark:[--btn-t2:var(--warm-800)] [--btn-t3:var(--warm-300)] dark:[--btn-t3:var(--warm-700)] [--btn-line:var(--warm-400)] dark:[--btn-line:var(--warm-600)] [--btn-text:var(--warm-700)] dark:[--btn-text:var(--warm-300)] [--btn-ink:var(--warm-0)] dark:[--btn-ink:var(--warm-950)]';
|
|
180
|
+
// Neutral = Nuxt-style monochrome (color="neutral"). Face-based variants (keycap/framed/solid) are the
|
|
181
|
+
// INVERTED key — near-black face + white ink in light, near-white face + dark ink in dark (high
|
|
182
|
+
// contrast, like Nuxt's bg-inverted/text-inverted). Tint/text variants (subtle/soft/outline/ghost/
|
|
183
|
+
// link) fall to surface greys (t 100–300 / text 900). Grey focus/hover ring (neutral-500), so the
|
|
184
|
+
// outline hover edge stays monochrome instead of leaking brand green via the shared --btn-ring.
|
|
185
|
+
// Rest --btn-line is 300/700 (not the ramp's 400/600) to match UInput's resting border
|
|
186
|
+
// (ring-accented → --ui-border-accented = neutral-300/700), so a neutral button sits flush with a field.
|
|
187
|
+
export const PAL_NEUTRAL = '[--btn-face:var(--neutral-900)] dark:[--btn-face:var(--neutral-50)] [--btn-hover:var(--neutral-800)] dark:[--btn-hover:var(--neutral-100)] [--btn-press:var(--neutral-700)] dark:[--btn-press:var(--neutral-200)] [--btn-lip:var(--neutral-400)] dark:[--btn-lip:var(--neutral-300)] [--btn-ring:var(--neutral-500)] [--btn-t1:var(--neutral-50)] dark:[--btn-t1:var(--neutral-950)] [--btn-t2:var(--neutral-100)] dark:[--btn-t2:var(--neutral-900)] [--btn-t3:var(--neutral-200)] dark:[--btn-t3:var(--neutral-800)] [--btn-line:var(--neutral-300)] dark:[--btn-line:var(--neutral-700)] [--btn-text:var(--neutral-900)] dark:[--btn-text:var(--neutral-100)] [--btn-ink:var(--neutral-0)] dark:[--btn-ink:var(--neutral-900)] [--btn-shine-l:0.5] dark:[--btn-shine-l:0.25]';
|
|
188
|
+
|
|
189
|
+
// All seven intents share the colour-keyed solid/flat/outline/soft/ghost classes.
|
|
190
|
+
export const ALL = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'];
|
|
191
|
+
export const emphasis = (variant: string, cls: string) => ALL.map((color) => ({ color, variant, class: cls }));
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forest theme fragment for UButton — the 9-rung emphasis ladder. The default is the signature
|
|
3
|
+
* tactile "keycap" (à la Sentry): a coloured face on a darker bottom lip (pseudo-layers behind the
|
|
4
|
+
* label) that lifts on hover and collapses flush on press.
|
|
5
|
+
*
|
|
6
|
+
* Intent × emphasis taxonomy, mapped onto Nuxt UI's color × variant axes:
|
|
7
|
+
*
|
|
8
|
+
* color (intent) | variant (emphasis, high → low)
|
|
9
|
+
* ----------------------------------- | -------------------------------------------------
|
|
10
|
+
* primary primary forest | keycap tactile lip (lifts on hover, flush on press)
|
|
11
|
+
* success success forest | pad tint + border + lip — quiet but tactile
|
|
12
|
+
* error danger maple | framed filled face + border (light faces stay visible)
|
|
13
|
+
* info info river | solid filled face, no border, no lip
|
|
14
|
+
* warning warning amber (ink) | subtle tint + border
|
|
15
|
+
* secondary secondary warm | soft tint, no border
|
|
16
|
+
* neutral neutral neutral | outline border only
|
|
17
|
+
* | ghost text, bg tint on hover
|
|
18
|
+
* | link text-only, underline on hover
|
|
19
|
+
*
|
|
20
|
+
* Each `color` sets per-family CSS custom properties (--btn-*) consumed by the shared emphasis
|
|
21
|
+
* classes in ./button.mechanics, so each variant is written once. Buttons override their own
|
|
22
|
+
* colours via these vars, independent of the preset's semantic role map.
|
|
23
|
+
*
|
|
24
|
+
* On-face ink follows FACE lightness, not mode: a dark face takes the family's 0, a light face its
|
|
25
|
+
* 950 (so amber — a light face in both modes — is 950 in both). Neutral is Nuxt-style monochrome:
|
|
26
|
+
* its face-based variants are the inverted key (face 900/50, ink 0/900); the rest fall to greys.
|
|
27
|
+
* Token source of truth: forest-web.DESIGN.md › Components.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
KEYCAP,
|
|
32
|
+
PAD,
|
|
33
|
+
FRAMED,
|
|
34
|
+
SOLID,
|
|
35
|
+
SUBTLE,
|
|
36
|
+
SOFT,
|
|
37
|
+
OUTLINE,
|
|
38
|
+
GHOST,
|
|
39
|
+
LINK,
|
|
40
|
+
PAL_FOREST,
|
|
41
|
+
PAL_RIVER,
|
|
42
|
+
PAL_MAPLE,
|
|
43
|
+
PAL_AMBER,
|
|
44
|
+
PAL_SECONDARY,
|
|
45
|
+
PAL_NEUTRAL,
|
|
46
|
+
emphasis,
|
|
47
|
+
} from './button.mechanics';
|
|
48
|
+
|
|
49
|
+
export const buttonTheme = {
|
|
50
|
+
slots: {
|
|
51
|
+
// motion-reduce: colour fades may stay; transform/box-shadow movement must not animate.
|
|
52
|
+
base: 'font-bold transition-[transform,box-shadow,background-color,border-color,color] duration-(--motion-fast) motion-reduce:transition-[background-color,border-color,color]',
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
variants: {
|
|
56
|
+
// `color` sets the per-family --btn-* vars (string → applies to the base slot).
|
|
57
|
+
color: {
|
|
58
|
+
primary: PAL_FOREST,
|
|
59
|
+
success: PAL_FOREST,
|
|
60
|
+
info: PAL_RIVER,
|
|
61
|
+
error: PAL_MAPLE,
|
|
62
|
+
warning: PAL_AMBER,
|
|
63
|
+
secondary: PAL_SECONDARY,
|
|
64
|
+
neutral: PAL_NEUTRAL,
|
|
65
|
+
},
|
|
66
|
+
// Custom emphasis keys not in Nuxt UI's native variant set — registered so the
|
|
67
|
+
// compoundVariants below bind. subtle/link are registered too, so binding never depends on
|
|
68
|
+
// the base preset's variant list. All are styled via the compounds.
|
|
69
|
+
variant: {
|
|
70
|
+
keycap: '',
|
|
71
|
+
pad: '',
|
|
72
|
+
framed: '',
|
|
73
|
+
gloss: '',
|
|
74
|
+
subtle: '',
|
|
75
|
+
link: '',
|
|
76
|
+
},
|
|
77
|
+
// Size-scaled padding, radius, and lip elevation.
|
|
78
|
+
size: {
|
|
79
|
+
// --btn-y = key lift (lip thickness), --btn-y-h = hover lift; --btn-yi/--btn-yhi = the exact
|
|
80
|
+
// inverses, applied to the base slab so it cancels the lift and stays anchored.
|
|
81
|
+
// Radius on the system's named scale (rounded-sm 8 · rounded-md 12 · rounded-lg 16 — the
|
|
82
|
+
// --radius-* tokens) instead of arbitrary [Npx]. The scale is coarser than the old 2px ladder,
|
|
83
|
+
// so sm snaps 10→8 and lg snaps 14→16, each sharing a step with a neighbour; xs/md/xl are
|
|
84
|
+
// unchanged (8/12/16). --btn-y* are lip mechanics, not a scale — left as-is.
|
|
85
|
+
// TWO LADDERS, selected by pointer, not by context:
|
|
86
|
+
// fine 24/28/32/36/40 (xs→xl) — the pointer ladder, matching UInput at every size.
|
|
87
|
+
// touch 28/36/40/44/52 — bigger box + a type step, so lg is Apple's 44 and xl 52.
|
|
88
|
+
// A control's height changes ONLY with its `size` and with the ladder in force. Per-ladder
|
|
89
|
+
// height does NOT break a mixed <UFieldGroup>: every field member carries the same `touch:`
|
|
90
|
+
// ladder, so button==input at every rung on either side.
|
|
91
|
+
// `min-h` is RETAINED (not removed): it is the height driver `aspect-square` mirrors into width
|
|
92
|
+
// for icon-only buttons (stock supplies the equal `p-1.5` square padding). A prior attempt that
|
|
93
|
+
// removed min-h collapsed every icon button — only its VALUE moved down to the input's.
|
|
94
|
+
// touch: = the touch ladder (custom variant, styles/forest.css). ONE set of numbers, fired
|
|
95
|
+
// either by a coarse primary pointer (any context — a touched web page counts) or by the mobile
|
|
96
|
+
// cut's baked `.cut-fixed` marker. Same values under both, so a real phone can't step twice and
|
|
97
|
+
// the desktop mock of the mobile app shows true size with no pointer to query.
|
|
98
|
+
// height min-h 28/36/40/44/52 — min-h is the authority, and the ONLY height driver for
|
|
99
|
+
// icon-only buttons (the square compound zeroes the padding, which would skew them).
|
|
100
|
+
// type sm/lg/xl gain a step; xs/md keep their pair's text (12·14·14·16·18 across the ladder).
|
|
101
|
+
// icons size-5/6/7 follow the box, so the glyph doesn't strand inside a bigger target.
|
|
102
|
+
// Radius is deliberately absent — the mobile cut is already rounded-full, and the web radius
|
|
103
|
+
// ramp reads correctly at both ladders. Lip vars (--btn-y*, keycap-only, ~1px) stay on `coarse:`:
|
|
104
|
+
// they're depth, not size, and the mobile cut sets its own flat lip in mobile-theme.ts.
|
|
105
|
+
xs: { base: 'min-h-6 px-2.5 rounded-sm touch:min-h-7 [--btn-y:-1px] [--btn-yi:1px] [--btn-y-h:-2px] [--btn-yhi:2px] coarse:[--btn-y:-2px] coarse:[--btn-yi:2px] coarse:[--btn-y-h:-3px] coarse:[--btn-yhi:3px]' },
|
|
106
|
+
sm: { base: 'min-h-7 px-3 rounded-md touch:min-h-9 touch:text-sm [--btn-y:-2px] [--btn-yi:2px] [--btn-y-h:-3px] [--btn-yhi:3px]', leadingIcon: 'touch:size-5', trailingIcon: 'touch:size-5' },
|
|
107
|
+
md: { base: 'min-h-8 px-3.5 rounded-md touch:min-h-10 [--btn-y:-2px] [--btn-yi:2px] [--btn-y-h:-3px] [--btn-yhi:3px] coarse:[--btn-y:-3px] coarse:[--btn-yi:3px] coarse:[--btn-y-h:-4px] coarse:[--btn-yhi:4px]' },
|
|
108
|
+
lg: { base: 'min-h-9 px-4 rounded-lg touch:min-h-11 touch:text-base [--btn-y:-3px] [--btn-yi:3px] [--btn-y-h:-4px] [--btn-yhi:4px]', leadingIcon: 'touch:size-6', trailingIcon: 'touch:size-6' },
|
|
109
|
+
xl: { base: 'min-h-10 px-4 rounded-lg touch:min-h-13 touch:text-lg [--btn-y:-3px] [--btn-yi:3px] [--btn-y-h:-4px] [--btn-yhi:4px]', leadingIcon: 'touch:size-7', trailingIcon: 'touch:size-7' },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
compoundVariants: [
|
|
114
|
+
...emphasis('keycap', KEYCAP),
|
|
115
|
+
...emphasis('pad', PAD),
|
|
116
|
+
...emphasis('framed', FRAMED),
|
|
117
|
+
// ...emphasis('gloss', GLOSS),
|
|
118
|
+
...emphasis('solid', SOLID),
|
|
119
|
+
...emphasis('subtle', SUBTLE),
|
|
120
|
+
...emphasis('soft', SOFT),
|
|
121
|
+
...emphasis('outline', OUTLINE),
|
|
122
|
+
...emphasis('ghost', GHOST),
|
|
123
|
+
...emphasis('link', LINK),
|
|
124
|
+
// Icon-only: `min-h` floors the height, `aspect-square` mirrors it into width; stock supplies the
|
|
125
|
+
// equal `p-*` square padding, which asymmetric touch padding would skew — so zero it and let the
|
|
126
|
+
// size's `touch:min-h-*` rung carry the growth on its own. That rung must therefore exist for
|
|
127
|
+
// every size: without it a touched icon button grows its glyph while the box holds still.
|
|
128
|
+
{ square: true, class: 'justify-center aspect-square touch:px-0 touch:py-0' },
|
|
129
|
+
],
|
|
130
|
+
|
|
131
|
+
defaultVariants: {
|
|
132
|
+
color: 'primary',
|
|
133
|
+
variant: 'solid',
|
|
134
|
+
size: 'md',
|
|
135
|
+
},
|
|
136
|
+
} as const
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forest theme fragment — UCalendar
|
|
3
|
+
*
|
|
4
|
+
* Deltas only — every token here changes stock. The card shell, the confident heading weight, and
|
|
5
|
+
* the offset brand focus ring are the whole opinion; shape, colour intents, sizes and the range /
|
|
6
|
+
* today fills are stock Nuxt UI on the Forest ramps.
|
|
7
|
+
*
|
|
8
|
+
* Slots touched: root, headingLabel, cellTrigger.
|
|
9
|
+
* Stock recipe (for the diff): apps/docs/node_modules/.nuxt-ui/ui/calendar.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const calendarTheme = {
|
|
13
|
+
slots: {
|
|
14
|
+
// Card-style container — stock `root` is empty; Forest frames the inline grid as a contained
|
|
15
|
+
// card (soft radius + border + gentle padding) rather than a raw grid floating in white space.
|
|
16
|
+
root: 'rounded-xl border border-default bg-elevated/30 p-4 inline-block',
|
|
17
|
+
|
|
18
|
+
// Month / year label — the actual text span (nested inside `heading`, which is why the weight
|
|
19
|
+
// must land here, not on the wrapper). Confident weight replaces stock's `font-medium` (banned
|
|
20
|
+
// by the two-weight system) and picks up display tracking without the full Mohr uppercase.
|
|
21
|
+
headingLabel: 'font-bold tracking-tight text-highlighted',
|
|
22
|
+
|
|
23
|
+
// Date cell — the only cell delta: a brand focus ring pushed out 2px (replaces stock's flat
|
|
24
|
+
// `focus-visible:outline-3`) so it clears the circle edge, plus a bolder today marker
|
|
25
|
+
// (`font-bold` over stock `font-semibold`). Shape (rounded-full), sizes and the colour-variant
|
|
26
|
+
// fills stay stock.
|
|
27
|
+
cellTrigger: 'focus-visible:ring-2 focus-visible:ring-offset-2 focus:outline-none data-today:font-bold',
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
@@ -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
|