diff --git a/.changeset/add-nav-recipe.md b/.changeset/add-nav-recipe.md new file mode 100644 index 00000000..87916a17 --- /dev/null +++ b/.changeset/add-nav-recipe.md @@ -0,0 +1,10 @@ +--- +"@styleframe/theme": minor +"styleframe": minor +--- + +Add Nav recipe with list-style utility + +- Add `useNavRecipe` and `useNavItemRecipe` multi-part recipes with size (xs–xl), color, and variant (pills, underline, default) support +- Add `useListStyleUtility` shorthand utility for the `list-style` CSS property +- Add Nav storybook components, grid previews, and stories diff --git a/.claude/styleframe-recipe-prompt.md b/.claude/styleframe-recipe-prompt.md index 74208a97..50b52930 100644 --- a/.claude/styleframe-recipe-prompt.md +++ b/.claude/styleframe-recipe-prompt.md @@ -670,6 +670,29 @@ defaultVariants: { Custom axes should be simple enums that map to a small number of CSS properties. Complex conditional behavior belongs in compound variants instead. +### Boolean State Axes (active, disabled, block, etc.) + +Boolean component states MUST be modeled as variant axes with string `"true"` / `"false"` keys, not as manual class additions with `selector()` callbacks. Place the state-specific CSS directly in the variant value: + +```ts +active: { + true: { fontWeight: "@font-weight.semibold" }, + false: {}, +}, +disabled: { + true: { cursor: "not-allowed", opacity: "0.5", pointerEvents: "none" }, + false: {}, +}, +``` + +Default to `"false"` in `defaultVariants`. The consuming component passes the state as a string variant prop: + +```ts +recipe({ active: isActive ? "true" : "false" }) +``` + +**NEVER** use `selector()` with class-based modifiers (e.g., `.component.-active`) for states that apply to the element itself. Use `className` compound variants + `selector()` only when the state styles target **child elements** (e.g., `.button-group.-horizontal > .button`). + --- ## Compound Variants diff --git a/apps/docs/content/docs/06.components/02.composables/06.nav.md b/apps/docs/content/docs/06.components/02.composables/06.nav.md new file mode 100644 index 00000000..9d6cc6d7 --- /dev/null +++ b/apps/docs/content/docs/06.components/02.composables/06.nav.md @@ -0,0 +1,572 @@ +--- +title: Nav +description: A navigation component for horizontal and vertical link lists. Supports multiple colors, visual styles, sizes, and active/disabled states through a two-part recipe system. +--- + +## Overview + +The **Nav** is a navigation component used for building horizontal and vertical link lists such as navbars, sidebars, and tab bars. It is composed of two recipe parts: `useNavRecipe()` for the container that controls layout direction and spacing, and `useNavItemRecipe()` for individual navigation links with color, variant, and interactive state options. Each composable creates a fully configured [recipe](/docs/api/recipes) with compound variants that handle the color-variant combinations automatically. + +The Nav recipes integrate directly with the default [design tokens preset](/docs/design-tokens/presets) and generate type-safe utility classes at build time with zero runtime CSS. + +## Why use the Nav recipe? + +The Nav recipe helps you: + +- **Ship faster with sensible defaults**: Get 2 orientations, 3 colors, 2 visual styles, and 3 sizes out of the box with a pair of composable calls. +- **Compose flexible layouts**: Two coordinated recipes (container + item) share the size axis, so your navigation stays internally consistent. +- **Maintain consistency**: Compound variants ensure every color-variant combination follows the same design rules, including hover, focus, active, and dark mode states. +- **Customize without forking**: Override base styles, default variants, or filter out options you don't need — all through the options API. +- **Stay type-safe**: Full TypeScript support means your editor catches invalid color, variant, or size values at compile time. +- **Integrate with your tokens**: Every value references the design tokens preset, so theme changes propagate automatically. + +## Usage + +::steps{level="4"} + +#### Register the recipes + +Add the Nav recipes to a local Styleframe instance. The global `styleframe.config.ts` provides design tokens and utilities, while the component-level file registers the recipes themselves: + +:::code-tree{default-value="src/components/nav.styleframe.ts"} + +```ts [src/components/nav.styleframe.ts] +import { styleframe } from 'virtual:styleframe'; +import { useNavRecipe, useNavItemRecipe } from '@styleframe/theme'; + +const s = styleframe(); + +const nav = useNavRecipe(s); +const navItem = useNavItemRecipe(s); + +export default s; +``` + +```ts [styleframe.config.ts] +import { styleframe } from 'styleframe'; +import { useDesignTokensPreset, useUtilitiesPreset } from '@styleframe/theme'; + +const s = styleframe(); + +useDesignTokensPreset(s); +useUtilitiesPreset(s); + +export default s; +``` + +::: + +#### Build the component + +Import the `nav` and `navItem` runtime functions from the virtual module and pass variant props to compute class names: + +:::tabs +::::tabs-item{icon="i-devicon-react" label="React"} + +```ts [src/components/Nav.tsx] +import { nav, navItem } from "virtual:styleframe"; + +interface NavItemProps { + color?: "light" | "dark" | "neutral"; + variant?: "ghost" | "link"; + size?: "sm" | "md" | "lg"; + active?: boolean; + disabled?: boolean; + href?: string; + children?: React.ReactNode; +} + +interface NavProps { + orientation?: "horizontal" | "vertical"; + size?: "sm" | "md" | "lg"; + children?: React.ReactNode; +} + +export function Nav({ + orientation = "horizontal", + size = "md", + children, +}: NavProps) { + return ( + + ); +} + +export function NavItem({ + color = "neutral", + variant = "ghost", + size = "md", + active = false, + disabled = false, + href, + children, +}: NavItemProps) { + return ( + + {children} + + ); +} +``` + +:::: +::::tabs-item{icon="i-devicon-vuejs" label="Vue"} + +```vue [src/components/Nav.vue] + + + +``` + +```vue [src/components/NavItem.vue] + + + +``` + +:::: +::: + +#### See it in action + +:::story-preview +--- +story: theme-recipes-nav--default +panel: true +--- +::: + +:: + +## Orientation + +The Nav container recipe supports two orientations that control the flex layout direction. Horizontal arranges items in a row (ideal for top navbars and tab bars), while vertical stacks items in a column (ideal for sidebars and dropdown menus). + +::story-preview +--- +story: theme-recipes-nav--vertical +panel: true +--- +:: + +### Orientation Reference + +| Orientation | Flex Direction | Alignment | Use Case | +|-------------|---------------|-----------|----------| +| `horizontal` | `row` | `center` | Top-level navbars, tab bars, breadcrumbs | +| `vertical` | `column` | `flex-start` | Sidebars, dropdown menus, stacked navigation | + +::tip +**Pro tip:** Use `horizontal` for top-level navigation and `vertical` for sidebar navigation. The orientation only affects the container — individual items render the same regardless of direction. +:: + +## Colors + +The Nav item recipe includes 3 color variants: `light`, `dark`, and `neutral`. Like the Card recipe, Nav uses neutral-spectrum colors designed for structural navigation elements rather than status communication. Each color is combined with every visual style variant through compound variants, so you get consistent, predictable styling across all combinations — including dark mode overrides. + +The `neutral` color adapts automatically: it uses dark text in light mode and light text in dark mode, making it the safest default for general-purpose navigation. + +::story-preview +--- +story: theme-recipes-nav--dark +panel: true +--- +:: + +### Color Reference + +::story-preview +--- +story: theme-recipes-nav--all-variants +height: 420 +--- +:: + +| Color | Token | Use Case | +|-------|-------|----------| +| `light` | `@color.text` / `@color.gray-*` | Navigation on light backgrounds, stays light-text in dark mode | +| `dark` | `@color.gray-200` | Navigation on dark backgrounds, stays dark appearance in light mode | +| `neutral` | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme | + +::tip +**Pro tip:** Use `neutral` as your default nav item color. It adapts automatically to the user's color scheme, so you don't need to manage light and dark variants separately. +:: + +## Variants + +Two visual style variants control how nav items are rendered. Each variant is combined with the selected color through [compound variants](/docs/api/recipes#compound-variants), so you always get the correct text color and hover behavior for your chosen color. + +### Ghost + +Transparent background that reveals a tinted background on hover. The most common style for navigation, ideal for navbars and sidebars where items should be subtle at rest but clearly interactive on hover. + +::story-preview +--- +story: theme-recipes-nav--ghost +panel: true +--- +:: + +### Link + +Transparent background with colored text that gains an underline on hover. Use for secondary navigation, inline link lists, or when items should look like standard hyperlinks. + +::story-preview +--- +story: theme-recipes-nav--link +panel: true +--- +:: + +## Sizes + +Three size variants from `sm` to `lg` control the font size, gap, and padding of the navigation. The `size` prop affects both the container (font size and gap between items) and individual items (font size and padding). + +::story-preview +--- +story: theme-recipes-nav--large +panel: true +--- +:: + +### Size Reference + +::story-preview +--- +story: theme-recipes-nav--all-sizes +height: 480 +--- +:: + +**Nav Container:** + +| Size | Font Size | Gap | +|------|-----------|-----| +| `sm` | `@font-size.xs` | `@0.25` | +| `md` | `@font-size.sm` | `@0.5` | +| `lg` | `@font-size.md` | `@0.75` | + +**Nav Item:** + +| Size | Font Size | Padding (V / H) | +|------|-----------|-----------------| +| `sm` | `@font-size.xs` | `@0.25` / `@0.5` | +| `md` | `@font-size.sm` | `@0.375` / `@0.75` | +| `lg` | `@font-size.md` | `@0.5` / `@1` | + +::note +**Good to know:** The `size` prop must be passed to both the nav container and each nav item individually. The container controls font size and gap between items, while items control their own padding. +:: + +## Active + +The Nav item recipe includes an `active` boolean variant. Active items receive `font-weight: semibold` to visually distinguish the current page or section from other navigation links. + +::story-preview +--- +story: theme-recipes-nav--active +panel: true +--- +:: + +```ts +// Active item via variant prop +navItem({ color: "neutral", variant: "ghost", size: "md", active: "true" }) +``` + +::tip +**Good practice:** Always pair the active variant with `aria-current="page"` so both sighted users and screen reader users know which page is currently active. +:: + +## Disabled + +The Nav item recipe includes a built-in disabled state through two mechanisms: the `&:disabled` pseudo-class for native ` +``` + +::tip +**Good practice:** If your page has multiple `