@humanforest/nuxt-layer 0.1.3 → 0.3.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.
@@ -54,7 +54,12 @@ const props = withDefaults(
54
54
  // navigation and the weight split between two edges. It is what a console with a left rail does
55
55
  // everywhere (Linear, Figma, Retool). Pass `side="start"` where the panel IS the navigation for its
56
56
  // view, which in practice means a settings sub-nav.
57
- { behaviour: 'push', side: 'end', size: 'md' },
57
+ // ★ `close: undefined` is load-bearing, not noise. A type-declared `close?: boolean` compiles to
58
+ // `{ type: Boolean }`, and Vue casts an ABSENT boolean prop to `false` unless the options carry a
59
+ // `default` key — so without this `props.close ?? overlay` never sees `undefined`, and an overlay
60
+ // panel drew no close at all unless a consumer asked for one. Declaring the default keeps the three
61
+ // states the prop is written for: unset (follow the behaviour), true, false.
62
+ { behaviour: 'push', side: 'end', size: 'md', close: undefined },
58
63
  );
59
64
 
60
65
  const open = defineModel<boolean>('open', { default: true });
@@ -214,6 +219,20 @@ const sidebarUi = computed(() => ({
214
219
  // panel title and a card title share a baseline instead of nearly sharing one.
215
220
  header: 'min-h-0 p-4 pb-0',
216
221
  inner: overlay.value ? 'forest-canvas-chrome divide-y-0' : '',
222
+ // ★ THE PANEL OWNS DISMISS — the button it renders into `#actions`, wired to its own `open`. Stock adds a
223
+ // SECOND one under 1024px and it is inert: `canClose` is `close && collapsible !== 'none' ||
224
+ // isMobile`, and that `|| isMobile` survives this component's `collapsible="none"`, while the click
225
+ // writes USidebar's own `open` model — which this never binds and which changes nothing in the
226
+ // `collapsible="none"` branch (no `data-state`, no mobile menu). So it is a dead control, and on a
227
+ // phone it sat next to a live one.
228
+ //
229
+ // ★ Hidden through the theme rather than through `<template #close />`, which LOOKS like the tighter
230
+ // fix and does not work: Vue falls back to a slot's default content when the passed slot renders no
231
+ // valid vnode, so an empty template hands back exactly the button it was meant to suppress (measured
232
+ // — stock's close was still in the DOM). And not `:open` either: USidebar's
233
+ // `watch(isMobile, …, { immediate: true })` sets that model to `false` on crossing 1024px, so a bound
234
+ // panel would shut itself on a phone.
235
+ close: 'hidden',
217
236
  }));
218
237
  </script>
219
238
 
@@ -7,16 +7,24 @@
7
7
  * while aligning with Forest's overall soft-radius vocabulary (--ui-radius = 0.5 rem).
8
8
  * - `label`: font-medium is already stock; we keep it and add nothing — the Nuxt UI default is
9
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.
10
+ * - focus ring: Forest's house convention `outline-2 outline-offset-2` (an offset ring), over
11
+ * stock's flush `outline-3`. The outline COLOUR is stock's semantic value, resolved to the
12
+ * Forest OKLCH ramp via docs.css / forest-preset — no colour override needed, only the width
13
+ * and offset. Where that ring has to be written is the awkward part; see below.
14
14
  * - `legend` (CheckboxGroup): font-bold — bold against the regular item labels, giving the
15
15
  * group label a clear hierarchy above the individual checkbox labels.
16
16
  *
17
- * The card variant is left entirely stock no radius override.
17
+ * THE RING IS A COMPOUND, NOT A SLOT. Stock paints it in two places and neither is a plain slot:
18
+ * on `base` for the `list` variant with a visible box, and on `root` (as `has-focus-visible:`) for
19
+ * `card`, and for `list` once the box is hidden. Both live in stock's compoundVariants, which tv
20
+ * appends AFTER the slots — so a `slots.base` override loses to them and silently does nothing.
21
+ * The width therefore has to be restated at the same layer, in the entries below.
18
22
  *
19
- * Slots touched: base.
23
+ * The offset is the exception: no stock rule sets `outline-offset`, so the one on `base` still
24
+ * reaches from `slots` and is not repeated. The root entries carry their own, `has-focus-visible:`
25
+ * being a different property from the bare `outline-offset-2` on base.
26
+ *
27
+ * Slots touched: base, root, icon.
20
28
  * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/checkbox.ts
21
29
  *
22
30
  * The CheckboxGroup legend opinion lives in its own sibling fragment (checkbox-group.ts),
@@ -26,22 +34,32 @@
26
34
  export const checkboxTheme = {
27
35
  slots: {
28
36
  // 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',
37
+ // ring's offset. The WIDTH is not here it would be overridden; see the compounds below.
38
+ base: 'rounded focus-visible:outline-offset-2',
32
39
  },
33
40
  // 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.
41
+ // a coarse (finger) pointer each size grows one size up — box (base), tick (icon), row height
42
+ // (container) and label text (wrapper) — so the row becomes a taller, easier target. No sliding
43
+ // part, so the bump is clean (unlike the switch thumb). xl is the ceiling; each size lists only
44
+ // its deltas up, which is why container and wrapper appear on two rungs and the box on four.
45
+ //
46
+ // ★ THE TICK IS SIZED SEPARATELY FROM THE BOX. Stock pins `icon` per size rather than filling the
47
+ // box, so growing `base` alone leaves the tick at its fine-pointer size inside a bigger square.
37
48
  // `coarse:` carries the `:not(.cut-fixed)` escape so the mobile cut (baked size lg) opts out.
38
49
  variants: {
39
50
  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' },
51
+ xs: { base: 'coarse:size-3.5', icon: 'coarse:size-3' },
52
+ sm: { base: 'coarse:size-4', icon: 'coarse:size-3.5', container: 'coarse:h-5', wrapper: 'coarse:text-sm' },
53
+ md: { base: 'coarse:size-4.5', icon: 'coarse:size-4' },
54
+ lg: { base: 'coarse:size-5', icon: 'coarse:size-4.5', container: 'coarse:h-6', wrapper: 'coarse:text-base' },
44
55
  // xl: ceiling
45
56
  },
46
57
  },
58
+ // The house width, restated where stock's own compounds would otherwise win. Colour is left to
59
+ // stock, so these carry no `color` discriminator and apply across every ramp.
60
+ compoundVariants: [
61
+ { variant: 'list', indicator: ['start', 'end'], class: { base: 'focus-visible:outline-2' } },
62
+ { variant: 'card', class: { root: 'has-focus-visible:outline-2 has-focus-visible:outline-offset-2' } },
63
+ { variant: 'list', indicator: 'hidden', class: { root: 'has-focus-visible:outline-2 has-focus-visible:outline-offset-2' } },
64
+ ],
47
65
  } as const;
@@ -73,6 +73,7 @@ export { paginationTheme as pagination } from './pagination';
73
73
  export { pinInputTheme as pinInput } from './pin-input';
74
74
  export { popoverTheme as popover } from './popover';
75
75
  export { progressTheme as progress } from './progress';
76
+ export { progressGroupTheme as progressGroup } from './progress-group';
76
77
  // Prose components are namespaced under ui.prose.* — the fragment runs stock, carrying only the
77
78
  // `ed-demo` marker the docs editorial grid reads off a demo pane.
78
79
  export { proseTheme as prose } from './prose';
@@ -85,6 +86,7 @@ export { sidebarTheme as sidebar } from './sidebar';
85
86
  export { skeletonTheme as skeleton } from './skeleton';
86
87
  export { slideoverTheme as slideover } from './slideover';
87
88
  export { sliderTheme as slider } from './slider';
89
+ export { splitterTheme as splitter } from './splitter';
88
90
  export { stepperTheme as stepper } from './stepper';
89
91
  export { switchTheme as switch } from './switch';
90
92
  export { tableTheme as table } from './table';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Forest theme fragment — UProgressGroup (several values as segments of one track, plus a legend).
3
+ *
4
+ * New in Nuxt UI 4.11, and it reaches a Forest consumer whether or not we document it: the layer
5
+ * registers @nuxt/ui wholesale, so the component is typeable the moment the dependency resolves.
6
+ * This fragment exists so that what arrives is not off-brand, not because the system recommends it.
7
+ *
8
+ * Forest opinions applied:
9
+ *
10
+ * segment, status — the motion tokens, replacing stock's hard-coded `duration-200 ease-out`.
11
+ * UProgress next to it already animates on `--motion-slow` / `--motion-ease-out` (progress.ts),
12
+ * and two bars of the same family easing differently is the kind of seam that only ever shows
13
+ * up once both are on screen.
14
+ *
15
+ * We do NOT override:
16
+ * - `base` (the track) — stock's `bg-accented` groove and `rounded-full` are already the Forest
17
+ * reading; radius follows --ui-radius through the preset.
18
+ * - Colour variants — `bg-primary`, `bg-error` and the Forest ramps (forest/maple/river/warm/acid)
19
+ * are registered by forest-preset, and a per-item `color` that is not a registered name is
20
+ * passed through as an inline background, so `var(--dataviz-categorical-safe-N)` works as-is.
21
+ * - `motion-reduce:transition-none` — stock sets it, and the global reduced-motion floor in
22
+ * packages/tokens/motion supersedes it regardless. Restating it would be drift.
23
+ * - The size ladder, the legend slots, and `list`/`item` layout — no Forest opinion here.
24
+ *
25
+ * ★ NOT A REPLACEMENT FOR FDistributionBar. Its bar segments carry no slot, so nothing can hang a
26
+ * tooltip or a focus stop on them, and each segment is its own `role="progressbar"` — a five-part
27
+ * figure announces as five progress bars rather than one. FDistributionBar exists for the
28
+ * composition read: one `role="img"`, one sentence, and optional interactive segments.
29
+ *
30
+ * Slots touched: segment, status.
31
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/progress-group.ts
32
+ */
33
+
34
+ export const progressGroupTheme = {
35
+ slots: {
36
+ segment: 'duration-(--motion-slow) ease-(--motion-ease-out)',
37
+ status: 'duration-(--motion-slow) ease-(--motion-ease-out)',
38
+ },
39
+ } as const;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Forest theme fragment — USplitter (resizable panels with a draggable handle).
3
+ *
4
+ * New in Nuxt UI 4.11. Nothing in Forest uses it yet — FAppSidebar resizes through
5
+ * UDashboardSidebar's own `resizable`, and FSidePanel is deliberately two fixed rungs rather than a
6
+ * drag. The fragment is here for the same reason as progress-group's: the component is typeable by
7
+ * any consumer on the layer, so its focus ring should be the house one on arrival.
8
+ *
9
+ * Forest opinions applied:
10
+ *
11
+ * handle — the house focus ring: offset, and pinned to the ramp var rather than the semantic
12
+ * `outline-primary` token, matching link.ts, accordion.ts and carousel.ts. Stock's width is
13
+ * already `outline-2`, so only the offset and the colour are stated here.
14
+ *
15
+ * We do NOT override:
16
+ * - The handle's resting appearance. Stock paints nothing until focus, and that is right for a
17
+ * seam between two panels — see dashboard-resize-handle.ts for the case where Forest does draw
18
+ * a grab affordance, and note it does not port: that handle's box is zero-wide and sits ON the
19
+ * seam, so its pill is positioned against a zero-width edge. This one is a real 2-unit column.
20
+ * - `panel`, `root`, and the orientation cursors — layout and affordance, no Forest opinion.
21
+ *
22
+ * ★ If a split pane is ever built in Forest, it should be built on this rather than hand-rolled,
23
+ * and this fragment will need the resting/hover treatment that decision implies.
24
+ *
25
+ * Slots touched: handle.
26
+ * Slot names sourced from: apps/docs/node_modules/.nuxt-ui/ui/splitter.ts
27
+ */
28
+
29
+ export const splitterTheme = {
30
+ slots: {
31
+ handle: 'focus-visible:outline-offset-2 focus-visible:outline-[var(--forest-500)]',
32
+ },
33
+ } as const;
@@ -1297,6 +1297,20 @@
1297
1297
  --forest-card-l2: color-mix(in oklab, var(--ui-bg), var(--ui-text-highlighted) 8%);
1298
1298
  --forest-card-l3: color-mix(in oklab, var(--ui-bg), var(--ui-text-highlighted) 13%);
1299
1299
  }
1300
+ /* ── Inset surface — a RAISED panel sitting on a card, the inverse of the ramp above ──────────────
1301
+ The ramp steps AWAY from the page (light darker, dark lighter), which reads as a recess. A panel
1302
+ inset into a card's own ground is the opposite gesture: it should read as lifted, so it steps
1303
+ TOWARD white in both modes — i.e. always lighter than the card it sits in.
1304
+ Light lands back on the page plane (a white panel on a grey card, the dashboard look); dark cannot
1305
+ do that, because the page is the darkest thing there, so it borrows the ramp's own next level.
1306
+ Measured against a card at L1 (.967 light / .229 dark): this is .994 / .268 — lighter in both.
1307
+ ⚠ Declared on EVERY mode-boundary selector for the same reason the levels above are: the value
1308
+ derives from --ui-bg, a custom property resolves ONCE where it is declared, and on :root alone a
1309
+ nested force-light or force-dark island would inherit the outer mode's already-resolved colour.
1310
+ ⚠ NOT --ui-bg-elevated or --ui-bg-muted, which sound right and are not: both are DARKER than the
1311
+ Forest card surface in dark (.156 against the card's .229), so a panel on them reads as a hole. */
1312
+ :root, .light { --forest-card-inset: var(--ui-bg); }
1313
+ .dark { --forest-card-inset: var(--forest-card-l2); }
1300
1314
  /* Map — WHICH PALETTE OWNS WHICH REGION (José, 2026-07-26). Two rules, and they do not overlap:
1301
1315
  the DISC and the BADGE take the VIVID set (plus neutrals). They are categorical: they say WHICH
1302
1316
  kind of thing this is, and vivid separates further than the CVD-gated set does at marker size.