diff --git a/docs/app/docs/componentGroups.ts b/docs/app/docs/componentGroups.ts new file mode 100644 index 000000000..24e20bb9e --- /dev/null +++ b/docs/app/docs/componentGroups.ts @@ -0,0 +1,153 @@ +// Every documented component, grouped by what it is for. The sidebar, the +// /docs/components index, breadcrumbs and the search palette all read this, +// so a component is placed once and shows the same way everywhere. +// +// `docsNavigationSections` stays the flat, alphabetical list (llms.txt, the +// sitemap and previous/next links use it); this file only adds the grouping +// and a one-line purpose per component. + +export type ComponentGroupId = + | 'layout' + | 'forms' + | 'buttons' + | 'overlays' + | 'navigation' + | 'feedback' + | 'data' + | 'utilities' + +export type ComponentGroup = { + id: ComponentGroupId + title: string + /** One line under the group heading on the index page. */ + blurb: string +} + +export const componentGroups: readonly ComponentGroup[] = [ + { id: 'layout', title: 'Layout and typography', blurb: 'Containers, dividers and text, with the HTML semantics already right.' }, + { id: 'forms', title: 'Forms', blurb: 'Inputs that submit with the form, validate with the browser and reset with it.' }, + { id: 'buttons', title: 'Buttons and toggles', blurb: 'Pressable things, alone or in groups with one tab stop.' }, + { id: 'overlays', title: 'Overlays and menus', blurb: 'Dialogs, popups and menus with focus, dismissal and positioning handled.' }, + { id: 'navigation', title: 'Navigation and disclosure', blurb: 'Moving between views and revealing more without leaving the page.' }, + { id: 'feedback', title: 'Feedback and status', blurb: 'Telling the user what is happening, visibly and to assistive tech.' }, + { id: 'data', title: 'Data display', blurb: 'People, records and structured values.' }, + { id: 'utilities', title: 'Utilities', blurb: 'Providers and building blocks for your own components.' } +] + +export type ComponentMeta = { + /** URL slug under /docs/components/. */ + slug: string + /** Display name, as exported from `@radui/ui`. */ + name: string + group: ComponentGroupId + /** One sentence: what it is for. */ + purpose: string +} + +export const components: readonly ComponentMeta[] = [ + // Layout and typography + { slug: 'aspect-ratio', name: 'AspectRatio', group: 'layout', purpose: 'Keeps media at a fixed ratio while it scales.' }, + { slug: 'card', name: 'Card', group: 'layout', purpose: 'Groups related content and actions in one surface.' }, + { slug: 'separator', name: 'Separator', group: 'layout', purpose: 'A horizontal or vertical divider between sections.' }, + { slug: 'scroll-area', name: 'ScrollArea', group: 'layout', purpose: 'A scrollable container with custom scrollbars.' }, + { slug: 'splitter', name: 'Splitter', group: 'layout', purpose: 'Resizable panels you drag or resize with the keyboard.' }, + { slug: 'heading', name: 'Heading', group: 'layout', purpose: 'Titles and subtitles with the right heading level.' }, + { slug: 'text', name: 'Text', group: 'layout', purpose: 'Body copy with control over the element it renders.' }, + { slug: 'strong', name: 'Strong', group: 'layout', purpose: 'Marks text as important with a strong element.' }, + { slug: 'em', name: 'Em', group: 'layout', purpose: 'Emphasises text with an em element.' }, + { slug: 'quote', name: 'Quote', group: 'layout', purpose: 'An inline quotation with its own quotation marks.' }, + { slug: 'blockquote', name: 'BlockQuote', group: 'layout', purpose: 'A quoted passage set apart from the text around it.' }, + { slug: 'code', name: 'Code', group: 'layout', purpose: 'An inline code snippet.' }, + { slug: 'kbd', name: 'Kbd', group: 'layout', purpose: 'A keyboard key or shortcut.' }, + { slug: 'link', name: 'Link', group: 'layout', purpose: 'An anchor for pages and external URLs.' }, + { slug: 'label', name: 'Label', group: 'layout', purpose: 'A native label that names a form control.' }, + + // Forms + { slug: 'text-field', name: 'TextField', group: 'forms', purpose: 'A single-line input with leading and trailing slots.' }, + { slug: 'text-area', name: 'TextArea', group: 'forms', purpose: 'A multi-line text input.' }, + { slug: 'number-field', name: 'NumberField', group: 'forms', purpose: 'A numeric input with step buttons and a scrub area.' }, + { slug: 'otp-field', name: 'OTPField', group: 'forms', purpose: 'One-time code slots that fill left to right and accept paste.' }, + { slug: 'checkbox', name: 'Checkbox', group: 'forms', purpose: 'A single on, off or mixed choice.' }, + { slug: 'checkbox-group', name: 'CheckboxGroup', group: 'forms', purpose: 'Several checkboxes that share one value.' }, + { slug: 'checkbox-cards', name: 'CheckboxCards', group: 'forms', purpose: 'Multi-select where each option is a card.' }, + { slug: 'radio', name: 'Radio', group: 'forms', purpose: 'A single radio button for custom layouts.' }, + { slug: 'radio-group', name: 'RadioGroup', group: 'forms', purpose: 'Pick one option from a set.' }, + { slug: 'radio-cards', name: 'RadioCards', group: 'forms', purpose: 'Single-select where each option is a card.' }, + { slug: 'switch', name: 'Switch', group: 'forms', purpose: 'An on/off toggle that submits like a checkbox.' }, + { slug: 'slider', name: 'Slider', group: 'forms', purpose: 'Pick a number or a range by dragging.' }, + { slug: 'select', name: 'Select', group: 'forms', purpose: 'Choose one value from a dropdown list.' }, + { slug: 'combobox', name: 'Combobox', group: 'forms', purpose: 'A dropdown you can type into to filter the options.' }, + { slug: 'autocomplete', name: 'Autocomplete', group: 'forms', purpose: 'A text input with suggestions; the value is what you type.' }, + { slug: 'field', name: 'Field', group: 'forms', purpose: 'Wires a label, help text and an error to any control.' }, + { slug: 'fieldset', name: 'Fieldset', group: 'forms', purpose: 'Groups related controls under one legend.' }, + { slug: 'form', name: 'Form', group: 'forms', purpose: 'Validates its fields before submitting and focuses the first error.' }, + + // Buttons and toggles + { slug: 'button', name: 'Button', group: 'buttons', purpose: 'Triggers an action or submits a form.' }, + { slug: 'toggle', name: 'Toggle', group: 'buttons', purpose: 'A button that stays pressed.' }, + { slug: 'toggle-group', name: 'ToggleGroup', group: 'buttons', purpose: 'A set of toggles, single or multiple.' }, + { slug: 'toolbar', name: 'Toolbar', group: 'buttons', purpose: 'A row of controls that is one tab stop.' }, + + // Overlays and menus + { slug: 'dialog', name: 'Dialog', group: 'overlays', purpose: 'A modal window that traps focus until it closes.' }, + { slug: 'alert-dialog', name: 'AlertDialog', group: 'overlays', purpose: 'A dialog that asks the user to confirm or cancel.' }, + { slug: 'drawer', name: 'Drawer', group: 'overlays', purpose: 'A panel that slides in from an edge, with drag to close.' }, + { slug: 'popover', name: 'Popover', group: 'overlays', purpose: 'A floating surface anchored to a trigger.' }, + { slug: 'hover-card', name: 'HoverCard', group: 'overlays', purpose: 'A preview card on hover or focus.' }, + { slug: 'tooltip', name: 'Tooltip', group: 'overlays', purpose: 'A short hint on hover or focus.' }, + { slug: 'dropdown-menu', name: 'DropdownMenu', group: 'overlays', purpose: 'A menu opened from a button, with submenus.' }, + { slug: 'context-menu', name: 'ContextMenu', group: 'overlays', purpose: 'A menu opened with a right click or long press.' }, + { slug: 'menubar', name: 'Menubar', group: 'overlays', purpose: 'An application-style bar of menus.' }, + { slug: 'command', name: 'Command', group: 'overlays', purpose: 'A command palette for searchable actions.' }, + + // Navigation and disclosure + { slug: 'breadcrumb', name: 'Breadcrumb', group: 'navigation', purpose: 'Shows where the current page sits in the hierarchy.' }, + { slug: 'tabs', name: 'Tabs', group: 'navigation', purpose: 'Switch between views in place.' }, + { slug: 'tab-nav', name: 'TabNav', group: 'navigation', purpose: 'Tabs that are links, one URL per tab.' }, + { slug: 'navigation-menu', name: 'NavigationMenu', group: 'navigation', purpose: 'Site navigation with dropdown panels.' }, + { slug: 'steps', name: 'Steps', group: 'navigation', purpose: 'Progress through a multi-step flow.' }, + { slug: 'accordion', name: 'Accordion', group: 'navigation', purpose: 'Stacked sections that expand one or many at a time.' }, + { slug: 'collapsible', name: 'Collapsible', group: 'navigation', purpose: 'Show or hide one block of content.' }, + { slug: 'disclosure', name: 'Disclosure', group: 'navigation', purpose: 'Several show/hide items, composed or from an array.' }, + { slug: 'tree', name: 'Tree', group: 'navigation', purpose: 'Nested items like folders and files.' }, + { slug: 'minimap', name: 'Minimap', group: 'navigation', purpose: 'A scaled overview that jumps between sections.' }, + + // Feedback and status + { slug: 'badge', name: 'Badge', group: 'feedback', purpose: 'A small label for status or a count.' }, + { slug: 'callout', name: 'Callout', group: 'feedback', purpose: 'A message that needs attention.' }, + { slug: 'progress', name: 'Progress', group: 'feedback', purpose: 'How far a task has got.' }, + { slug: 'meter', name: 'Meter', group: 'feedback', purpose: 'A bounded measurement such as usage or a score.' }, + { slug: 'spinner', name: 'Spinner', group: 'feedback', purpose: 'Something is loading.' }, + { slug: 'skeleton', name: 'Skeleton', group: 'feedback', purpose: 'A placeholder in the shape of the content to come.' }, + { slug: 'toast', name: 'Toast', group: 'feedback', purpose: 'Stacked notifications with swipe to dismiss.' }, + { slug: 'live-region', name: 'LiveRegion', group: 'feedback', purpose: 'Announces updates to screen readers.' }, + + // Data display + { slug: 'avatar', name: 'Avatar', group: 'data', purpose: 'A person or brand, as an image or initials.' }, + { slug: 'avatar-group', name: 'AvatarGroup', group: 'data', purpose: 'Several avatars stacked with an overflow count.' }, + { slug: 'table', name: 'Table', group: 'data', purpose: 'Rows and columns of data.' }, + { slug: 'data-list', name: 'DataList', group: 'data', purpose: 'Key and value pairs, such as a profile or metadata.' }, + + // Utilities + { slug: 'theme', name: 'Theme', group: 'utilities', purpose: 'Sets appearance, accent, radius and scale for everything inside.' }, + { slug: 'direction-provider', name: 'DirectionProvider', group: 'utilities', purpose: 'Sets the reading direction once for right-to-left apps.' }, + { slug: 'slot', name: 'Slot', group: 'utilities', purpose: 'Slot, mergeProps and composeRefs for building your own parts.' }, + { slug: 'visually-hidden', name: 'VisuallyHidden', group: 'utilities', purpose: 'Content for screen readers only.' } +] + +const bySlug = new Map(components.map((component) => [component.slug, component])) +const groupById = new Map(componentGroups.map((group) => [group.id, group])) + +export const getComponentMeta = (slug: string) => bySlug.get(slug) + +export const getComponentGroup = (id: ComponentGroupId) => groupById.get(id) + +/** Components of one group, in the order they are listed above. */ +export const componentsInGroup = (id: ComponentGroupId) => components.filter((component) => component.group === id) + +/** The group a docs path belongs to, for breadcrumbs. */ +export const getGroupForPath = (pathname: string | null) => { + const slug = pathname?.match(/^\/docs\/components\/([^/]+)/)?.[1] + const meta = slug ? bySlug.get(slug) : undefined + return meta ? groupById.get(meta.group) : undefined +} diff --git a/docs/app/docs/components/[component]/changelog/page.tsx b/docs/app/docs/components/[component]/changelog/page.tsx index 604efcfa2..cbc06a097 100644 --- a/docs/app/docs/components/[component]/changelog/page.tsx +++ b/docs/app/docs/components/[component]/changelog/page.tsx @@ -82,7 +82,7 @@ export default async function ComponentChangelogPage({

Component-specific release notes collected from{" "} the full changelog @@ -91,13 +91,13 @@ export default async function ComponentChangelogPage({

Back to {target.title} docs
() +for (const section of docsNavigationSections as unknown as { items: NavLink[] }[]) { + for (const item of section.items) flags.set(item.path, item) +} + +const Tag = ({ children }: { children: string }) => ( + + {children} + +) + +/** Every component on one page, in the sidebar's groups, each with its one-line purpose. */ +export default function ComponentsIndex() { + return ( +
+
+

Components

+ + + {components.length} components, one import each + + + + Every component owns its roles, keyboard handling and focus. Style it with the Clarity theme, with + your own CSS on its data-* attributes, or not at all. + Each page has a live preview, the parts and props, the keyboard map and the ARIA pattern it follows. + +
+ + Open the playground + + + + Install Rad UI + +
+
+ +
+ {componentGroups.map((group) => { + const items = componentsInGroup(group.id) + return ( +
+
+ + {group.title} + + {group.blurb} +
+
    + {items.map((component) => { + const path = `/docs/components/${component.slug}` + const flag = flags.get(path) + return ( +
  • + + + {component.name} + {flag?.is_new ? New : flag?.is_preview ? Preview : null} + + + {component.purpose} + +
  • + ) + })} +
+
+ ) + })} +
+
+ ) +} diff --git a/docs/app/docs/components/shared/ariaReferences.js b/docs/app/docs/components/shared/ariaReferences.js index 1f1830b48..d13d9334a 100644 --- a/docs/app/docs/components/shared/ariaReferences.js +++ b/docs/app/docs/components/shared/ariaReferences.js @@ -166,7 +166,7 @@ export const createAriaReferenceRow = (pattern, description) => { id: pattern.id, reference: (
( . Minor and patch sections follow{" "} . Source:{" "} + +Every component has its own entry point (`@radui/ui/Button`, `@radui/ui/Dialog`), so your bundle only carries what you import. + +## Pick a setup + +Components work without any CSS. The theme is a choice, not a requirement. + +| Setup | Choose it when | What to add | +| --- | --- | --- | +| **Clarity theme** | You want finished-looking components today and will retheme with tokens later | `import "@radui/ui/themes/default.css"` and wrap the app in `Theme classNamespace="rad-ui"` | +| **Tailwind preset** | You style with Tailwind and want Rad UI's colour scales as utilities | `@import "@radui/ui/themes/tailwind-presets/default.css"` after `tailwindcss` in your global CSS | +| **Fully headless** | You own every pixel | Nothing. Import components and style their `data-*` attributes with any CSS | + +The three combine: most apps use the theme plus the Tailwind preset, and drop the theme on components they have restyled. + +## Set up the app + +For the Clarity theme in a Next.js App Router project, two lines in the root layout do it. The `Theme` wrapper sets appearance and accent and turns on the generated class names the stylesheet targets. + +```tsx +// app/layout.tsx +import "@radui/ui/themes/default.css"; +import Theme from "@radui/ui/Theme"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} +``` + +Going headless? Skip both lines. Using Vite or Remix? The same wrapper goes in your root component; see [Framework usage](/docs/guides/framework-usage). + +## Render a component + +```tsx +import Button from "@radui/ui/Button"; + +export function SaveButton() { + return ; +} +``` + +Interactive components are client components. In the App Router, add `"use client"` to the file that renders them. + +## Troubleshooting + +- **Components render unstyled.** The theme CSS is imported but `Theme` has no `classNamespace`. Pass `classNamespace="rad-ui"`; without it the library emits no classes for the stylesheet to match. +- **Your Tailwind utilities lose to the theme.** Load the theme before your own CSS, or override on the component's `data-*` attributes. [Styling and customization](/docs/guides/styling-and-customization) covers the cascade. +- **TypeScript complains about the CSS import.** Add a `declare module "*.css";` in a `.d.ts` file if your config checks side-effect imports. + +More cases: [Troubleshooting](/docs/guides/troubleshooting). + +## Next steps + +- [Usage](/docs/first-steps/usage): `Theme`, `classNamespace`, the Tailwind preset, wrapping components in your own. +- [All components](/docs/components): what each one is for, grouped. +- [Styling and customization](/docs/guides/styling-and-customization): tokens, `data-*` attributes and the headless path. +- [Design system integration](/docs/guides/design-system-integration): building your own system on Rad UI. +- Working with an AI agent? Point it at [llms.txt](https://www.rad-ui.com/llms.txt). diff --git a/docs/app/docs/first-steps/introduction/content.mdx b/docs/app/docs/first-steps/introduction/content.mdx index dd60b4452..be56bdf11 100644 --- a/docs/app/docs/first-steps/introduction/content.mdx +++ b/docs/app/docs/first-steps/introduction/content.mdx @@ -1,103 +1,44 @@ # Introduction -Rad UI is a **headless, accessible, and performance focused React component system** built for products that expect to live for years. +Rad UI is a headless, accessible React component library for products that expect to live for years. It owns behaviour: roles, keyboard handling, focus, dismissal, positioning. You own the look: style every state through stable `data-*` attributes, use the included Clarity theme, or mix both. -It is designed for teams building real design systems, scaling production applications, or shipping serious side projects that cannot afford fragile UI foundations. +It is infrastructure for a design system, not a starter kit. -Rad UI gives you control over structure, styling, and behavior while handling the hard parts for you: accessibility, composition, and predictable interactions. +## What ships -This is not a starter kit. -It is UI infrastructure. +- **70+ components**, each its own import (`@radui/ui/Dialog`), so a Dialog never pays for a Table. +- **Accessibility by default.** Composite widgets follow WAI-ARIA patterns: one tab stop per group, arrow keys inside it, focus returned on close, live announcements where they belong. +- **A styling contract.** Every state is a `data-*` attribute. Generated class names are opt-in through `Theme classNamespace`, so nothing leaks into a codebase that did not ask for it. +- **Clarity**, an optional theme driven by tokens: appearance, accent, radius and scale, with Tailwind presets for the colour scales. +- **TypeScript end to end**, server-rendering safe, React 18 and 19, Next.js, Remix and Vite. ---- +## Why Rad UI -## Why Rad UI? +### Headless at the core -### Design System First +Components ship with no visual styling. They integrate with your tokens, spacing and typography instead of imposing their own, which is what a design system needs. When you want to move fast, turn on Clarity; when a component needs its own look, detach it from the theme and style it directly. Both paths use the same markup. -Rad UI is built for design systems, not demos. +### Accessibility without ceremony -Every component is unstyled by default, making it trivial to integrate with your tokens, spacing scales, typography, and theming strategy. Whether you are building a design system from scratch or standardizing an existing one, Rad UI adapts to your rules instead of imposing its own. +Keyboard navigation, focus management and ARIA are built into each component and documented on its page: the keyboard map, the pattern it follows, the references. You get accessible behaviour on day one instead of accessibility debt later. -You keep ownership of your visual language. Rad UI stays out of the way. +### Composable primitives ---- +Each component is a set of parts (`Dialog.Root`, `Dialog.Trigger`, `Dialog.Content`) with a small, predictable API. Compose them into your own components, swap the rendered element with `asChild`, and build new parts with `Slot`, `mergeProps` and `composeRefs`. -### Gradual, Intentional Adoption +### Adopt one component at a time -Rad UI does not require a rewrite. +Rad UI does not ask for a rewrite. Bring it into an existing app next to what is already there, replace the hardest widgets first, and keep going at your own pace. -You can introduce it incrementally alongside existing components and styles. Adopt it one component at a time, modernize critical flows first, or replace legacy UI at your own pace. +## Who it is for -This makes Rad UI practical for real world codebases, not just greenfield projects. +- **Design system builders** who want solid primitives under their own language. +- **Product teams** who need consistent interactions across a large app. +- **Solo developers** who want polished, accessible UI without building it from scratch. ---- +## Start here -### Accessibility Without Ceremony - -Accessibility is not optional and it is not an afterthought. - -Rad UI components ship with keyboard navigation, focus management, and correct ARIA behavior baked in. They are designed to meet WCAG requirements wherever possible without forcing you to memorize specs or reinvent interaction logic. - -You get accessible behavior by default, not accessibility debt later. - ---- - -### Fully Composable Primitives - -Rad UI is built from low level primitives, not rigid components. - -Each piece exposes a clean, predictable API that can be composed into complex interfaces without hidden behavior or styling assumptions. You decide structure, layout, and presentation at every layer. - -Inspired by the Radix philosophy and extended with better ergonomics, customization hooks, and modern styling support, Rad UI prioritizes flexibility without chaos. - ---- - -### Performance by Design - -Rad UI is engineered to stay fast as your application grows. - -Components minimize unnecessary DOM updates, reduce re-renders through careful state management, and avoid heavyweight abstractions. The goal is simple: predictable performance that does not degrade as complexity increases. - ---- - -### Developer Experience That Respects You - -Rad UI is built by developers who have shipped and maintained large UIs. - -Expect: -- Clear and predictable APIs -- First class TypeScript support -- Consistent patterns across components -- Documentation written for understanding, not marketing - -You should spend your time building features, not decoding library behavior. - ---- - -### Styling Freedom, With an Escape Hatch - -Rad UI does not force a styling system. - -Use Tailwind, CSS Modules, SCSS, vanilla CSS, or anything else. Components ship without styles or resets, so they integrate cleanly into any stack without side effects. - -For teams that want to move fast, Rad UI also provides default CSS and theming out of the box. When you are ready, you can **detach components from the default styles** and progressively adopt your own design system without friction. - -This is intentional. Escape hatches are a feature. - ---- - -## Who is Rad UI for? - -Rad UI is for developers who want **control without chaos**. - -- **Design System Builders** - Build and enforce your own design language on top of solid, accessible primitives without fighting library opinions. - -- **Product Teams** - Scale complex applications with predictable UI behavior, consistent interactions, and long term maintainability. - -- **Solo Developers and Side Projects** - Ship polished interfaces quickly without compromising on accessibility, performance, or future flexibility. - -Rad UI is built on proven design system principles +1. [Installation](/docs/first-steps/installation): install, pick a setup, render a component. +2. [Usage](/docs/first-steps/usage): `Theme`, class namespaces and the Tailwind preset. +3. [All components](/docs/components): what each one is for. +4. [Playground](/playground): every component with its variants, sizes and states side by side. diff --git a/docs/app/globals.css b/docs/app/globals.css index 4086c7285..8a34fc854 100644 --- a/docs/app/globals.css +++ b/docs/app/globals.css @@ -843,3 +843,117 @@ pre.docs-syntax-pre code.docs-code-block { /* Avoid `transition: none !important` on the whole docs shell: it overrides inline height transitions on Rad UI components (e.g. Accordion) in demos. Prefer `prefers-reduced-motion` in component CSS when reducing motion. */ + +/* ------------------------------------------------------------------ + Docs shell: sidebar, search, phone drawer + ------------------------------------------------------------------ */ + +/* The active sidebar row carries a short accent bar on its left edge. */ +.docs-nav-item[data-active]::before { + content: ""; + position: absolute; + left: 0; + top: 0.45rem; + bottom: 0.45rem; + width: 2px; + border-radius: 9999px; + background: var(--rad-ui-color-green-900); +} + +/* Collapsible groups in the sidebar render no theme classes (customRootClass="docs"), + so the only motion is the chevron; content shows and hides at once. */ +.docs-collapsible-content[data-state="closed"] { + display: none; +} + +.docs-kbd-hint { + font-size: 0.65rem; + line-height: 1; + padding: 0.2rem 0.35rem; + color: var(--rad-ui-text-secondary); + border: 1px solid var(--rad-ui-border-soft); + border-radius: 0.3rem; + background: var(--rad-ui-surface-canvas); +} + +.docs-search-dialog { + width: min(600px, calc(100vw - 2rem)); +} + +.docs-search-list { + max-height: min(60vh, 440px); +} + +.docs-nav-drawer-overlay { + position: fixed; + inset: 0; + z-index: 60; + background: color-mix(in oklab, var(--rad-ui-color-gray-1000) 40%, transparent); +} + +.docs-nav-drawer { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 61; + width: min(300px, 85vw); + background: var(--rad-ui-color-gray-50); + border-right: 1px solid var(--rad-ui-color-gray-300); + outline: none; +} + +@media (width >= 768px) { + .docs-nav-drawer, + .docs-nav-drawer-overlay { + display: none; + } +} + +/* ------------------------------------------------------------------ + Component page: preview stage, code gutter, API tables + ------------------------------------------------------------------ */ + +.docs-preview-stage { + background-image: radial-gradient( + circle, + color-mix(in oklab, var(--rad-ui-color-gray-700) 45%, transparent) 1px, + transparent 1px + ); + background-size: 18px 18px; + background-position: center; +} + +.docs-code-body { + padding: 0.75rem 0; +} + +.docs-code-gutter { + flex: none; + padding: 0 0.75rem 0 1rem; + font-family: var(--font-mono), monospace; + font-size: 12px; + line-height: 1.7; + text-align: right; + white-space: pre; + color: var(--rad-ui-text-secondary); +} + +[data-rad-ui-theme="dark"] .docs-code-gutter { + color: #a1a1aa; +} + +pre.docs-syntax-pre .docs-code-body code.docs-code-block { + padding: 0 1rem 0 0; +} + +pre.docs-syntax-pre .docs-code-body .docs-code-gutter + code.docs-code-block { + padding-left: 0; +} + +.docs-api-table th, +.docs-api-table td { + padding-top: 0.6rem; + padding-bottom: 0.6rem; + vertical-align: top; +} diff --git a/docs/app/landingComponents/ThemePlayground.tsx b/docs/app/landingComponents/ThemePlayground.tsx index ad5f7bb15..2dc54a5a4 100644 --- a/docs/app/landingComponents/ThemePlayground.tsx +++ b/docs/app/landingComponents/ThemePlayground.tsx @@ -17,6 +17,7 @@ import Theme from '@radui/ui/Theme' import ToggleGroup from '@radui/ui/ToggleGroup' import { NavBarContext } from '@/components/Main/NavBar/NavBarContext' +import { useLightTokens } from '@/utils/useLightTokens' import Unthemed from './Unthemed' @@ -55,37 +56,6 @@ function ControlLabel({ children }: { children: string }) { } -const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 1000] -const RAMPS = ['gray', 'blue', 'violet', 'crimson', 'orange', 'jade', 'red', 'green', 'amber'] -const LIGHT_TOKENS = [ - ...RAMPS.flatMap((ramp) => STEPS.map((step) => `--rad-ui-color-${ramp}-${step}`)), - ...['xs', 'sm', 'md', 'lg', 'xl'].map((size) => `--rad-ui-shadow-${size}`) -] - -/** - * Clarity declares its light ramps on :root and only overrides them under - * [data-rad-ui-theme="dark"], so a light Theme nested in a dark page would - * inherit dark values. When that happens, re-declare the root (light) values - * on the stage so the nested appearance is honest. - */ -function useLightTokens(enabled: boolean) { - const [vars, setVars] = useState>({}) - useEffect(() => { - if (!enabled) { - setVars({}) - return - } - const root = getComputedStyle(document.documentElement) - const next: Record = {} - for (const name of LIGHT_TOKENS) { - const value = root.getPropertyValue(name).trim() - if (value) next[name] = value - } - setVars(next) - }, [enabled]) - return vars -} - export default function ThemePlayground() { const { darkMode } = useContext(NavBarContext) const [accent, setAccent] = useState('blue') diff --git a/docs/components/Main/Main.js b/docs/components/Main/Main.js index 0ae17eea2..764d6f6b3 100644 --- a/docs/components/Main/Main.js +++ b/docs/components/Main/Main.js @@ -6,6 +6,7 @@ import Cookies from 'js-cookie'; import { NavBarContext } from '@/components/Main/NavBar/NavBarContext'; import NavBar from './NavBar'; +import SearchPalette from '@/components/navigation/SearchPalette'; import Theme from '@radui/ui/Theme'; @@ -27,6 +28,7 @@ const THEME_SCRIPT = `try{if(document.cookie.split('; ').indexOf('${DARK_MODE_CO const MainLayout = ({ children }) => { const [darkMode, setDarkMode] = useState(true); const [isDocsNavOpen, setIsDocsNavOpen] = useState(false); + const [isSearchOpen, setIsSearchOpen] = useState(false); const [previewAppearance, setPreviewAppearance] = useState(null); useEffect(() => { @@ -44,6 +46,8 @@ const MainLayout = ({ children }) => { const sendValues = { isDocsNavOpen, setIsDocsNavOpen, + isSearchOpen, + setIsSearchOpen, darkMode, }; @@ -68,6 +72,7 @@ const MainLayout = ({ children }) => { {children} + diff --git a/docs/components/Main/NavBar/NavBarContext.tsx b/docs/components/Main/NavBar/NavBarContext.tsx index 1ad8f59cc..84dde070c 100644 --- a/docs/components/Main/NavBar/NavBarContext.tsx +++ b/docs/components/Main/NavBar/NavBarContext.tsx @@ -4,11 +4,15 @@ import { createContext } from "react"; type NavBarContextType = { isDocsNavOpen: boolean; setIsDocsNavOpen: (isDocsNavOpen: boolean) => void; + isSearchOpen: boolean; + setIsSearchOpen: (isSearchOpen: boolean) => void; darkMode?: boolean; } export const NavBarContext = createContext({ isDocsNavOpen: false, setIsDocsNavOpen: () => {}, + isSearchOpen: false, + setIsSearchOpen: () => {}, darkMode: false, }); diff --git a/docs/components/Main/NavBar/helpers/NavRoot/index.tsx b/docs/components/Main/NavBar/helpers/NavRoot/index.tsx index 48300d133..a687afc1e 100644 --- a/docs/components/Main/NavBar/helpers/NavRoot/index.tsx +++ b/docs/components/Main/NavBar/helpers/NavRoot/index.tsx @@ -8,6 +8,8 @@ import { usePathname } from "next/navigation"; import Link from "next/link"; import { getActiveNavLink, getActiveProduct, getDocsProduct } from "@/components/navigation/products"; import ProductSwitcher from "@/components/navigation/ProductSwitcher"; +import SearchTrigger from "@/components/navigation/SearchTrigger"; +import { RAD_UI_VERSION } from "@/utils/radVersion"; import { Menu, Moon, Sun } from "lucide-react"; const RadUILogo = () => { @@ -111,6 +113,15 @@ const NavBar = ({ darkMode, setDarkMode, setThemeCookie }) => { + {product.id === "ui" ? ( + + v{RAD_UI_VERSION} + + ) : null} {/* The centre cell stays in the grid at every breakpoint. Hiding it below `lg` would leave two children in a `1fr auto 1fr` track, pulling the @@ -136,6 +147,7 @@ const NavBar = ({ darkMode, setDarkMode, setThemeCookie }) => {
+
} diff --git a/docs/components/layout/Documentation/helpers/ComponentHero/ComponentHero.js b/docs/components/layout/Documentation/helpers/ComponentHero/ComponentHero.js index d43ea8c95..79c5d5ff3 100644 --- a/docs/components/layout/Documentation/helpers/ComponentHero/ComponentHero.js +++ b/docs/components/layout/Documentation/helpers/ComponentHero/ComponentHero.js @@ -1,54 +1,127 @@ "use client" import CodeBlock from '../CodeBlock'; -import { useMemo } from 'react' +import { useContext, useEffect, useMemo, useState } from 'react' +import Link from 'next/link' +import { ArrowUpRight, Moon, Sun } from 'lucide-react' import Heading from "@radui/ui/Heading" +import Theme from '@radui/ui/Theme' +import ToggleGroup from '@radui/ui/ToggleGroup' + +import { NavBarContext } from '@/components/Main/NavBar/NavBarContext' import { BookMarkLink } from '@/components/layout/Documentation/utils'; -import { docsSectionBlockClassName, docsSectionHeadingClassName, docsSurfaceClassName } from '../../shared'; +import { useLightTokens } from '@/utils/useLightTokens' +import { toSectionId } from '@/app/playground/helpers/PlaygroundContext' +import { docsHeadingClassFor, docsSectionBlockClassName, docsSurfaceClassName } from '../../shared'; +import { useDocsPage } from '../../DocsPageContext' import CodeTabs from './CodeTabs'; -const ComponentHero = ({ children, title='', codeUsage = {} }) => { - - const initializeTabs = (codeUsage) => { - const tabs = [] - for (const key in codeUsage) { - if (Object.hasOwnProperty.call(codeUsage, key)) { - let language = key - if(key === 'javascript') { - language = 'tsx' - } - tabs.push({ - label: key, - value: key, - content: {codeUsage[key]?.code}, - }) - } - } - return tabs - } - - const data = useMemo(() => initializeTabs(codeUsage), [codeUsage]); +const ACCENTS = ['gray', 'blue', 'violet', 'crimson', 'orange', 'jade'] +const TAB_LABELS = { javascript: 'React', scss: 'Styles' } + +function firstValue(value) { + if (Array.isArray(value)) return value[0] + return typeof value === 'string' && value ? value : undefined +} + +const toggleItemClass = 'grid h-6 min-w-6 place-items-center rounded-[5px] px-1 text-gray-950 outline-none transition-colors hover:text-gray-1000 focus-visible:ring-2 focus-visible:ring-gray-1000 data-[state=on]:bg-gray-50 data-[state=on]:text-gray-1000 data-[state=on]:shadow-sm' + +/** + * The live example at the top of a component page. The toolbar drives a nested + * Theme around the example only, so a reader can check the component in the + * other appearance and with another accent without leaving the page. + */ +const ComponentHero = ({ children, title = '', codeUsage = {} }) => { + const { darkMode } = useContext(NavBarContext) + const page = useDocsPage() + const [accent, setAccent] = useState('gray') + const [appearance, setAppearance] = useState(darkMode ? 'dark' : 'light') + const [pinned, setPinned] = useState(false) + useEffect(() => { + if (!pinned) setAppearance(darkMode ? 'dark' : 'light') + }, [darkMode, pinned]) + const lightTokens = useLightTokens(appearance === 'light' && Boolean(darkMode)) + + const data = useMemo(() => Object.keys(codeUsage).map((key) => ({ + label: TAB_LABELS[key] ?? key, + value: key, + content: {codeUsage[key]?.code} + })), [codeUsage]); + + const playgroundHref = page.title ? `/playground#${toSectionId(page.title.replace(/\s+/g, ''))}` : '/playground' return
{title && ( - {title} + {title} )}
-
- - - - - preview - +
+ Preview +
+ { + const next = firstValue(value) + if (next) setAccent(next) + }} + aria-label="Accent color" + className="flex items-center gap-1 rounded-md bg-gray-200 p-1" + > + {ACCENTS.map((name) => ( + + + + ))} + + { + const next = firstValue(value) + if (next === 'light' || next === 'dark') { + setPinned(true) + setAppearance(next) + } + }} + aria-label="Preview appearance" + className="flex items-center rounded-md bg-gray-200 p-0.5" + > + + + + + + + + + Playground + + +
-
+ {children} -
-
- + +
+
diff --git a/docs/components/layout/Documentation/helpers/DocsTable.js b/docs/components/layout/Documentation/helpers/DocsTable.js index 412e442ae..910ee4268 100644 --- a/docs/components/layout/Documentation/helpers/DocsTable.js +++ b/docs/components/layout/Documentation/helpers/DocsTable.js @@ -5,19 +5,13 @@ import Heading from '@radui/ui/Heading'; import Text from '@radui/ui/Text'; import Tooltip from '@radui/ui/Tooltip'; import { Info } from 'lucide-react'; -import { docsSectionBlockClassName, docsSectionHeadingClassName, docsSurfaceClassName } from '../shared'; +import { docsBodyClassName, docsHeadingClassFor, docsSectionBlockClassName, docsSurfaceClassName } from '../shared'; -const InlineCode = ({ children, tone = 'neutral' }) => { - const toneClassName = tone === 'accent' - ? 'border-green-500 bg-green-50 text-green-1000' - : 'border-gray-300 bg-gray-50 text-gray-950'; - - return ( - - {children} - - ); -}; +const Mono = ({ children, tone = 'primary' }) => ( + + {children} + +); const InfoButton = ({ infoText }) => { if (!infoText) { @@ -30,7 +24,7 @@ const InfoButton = ({ infoText }) => { @@ -50,8 +44,8 @@ const InfoRenderer = ({ row }) => { const prop = row.prop.name || row.prop; const infoText = row.prop.info_tooltips; return ( -
- {prop} +
+ {prop}
); @@ -60,8 +54,8 @@ const InfoRenderer = ({ row }) => { const EnumRenderer = ({ enumValues = [] }) => { const enumValuesString = enumValues.map(value => `'${value}'`).join(" | ") return ( -
- enum +
+ enum
); @@ -77,24 +71,25 @@ const renderCellValue = (row, columnType, value) => { } if (columnType === "type") { - return {value}; + return {value}; } if (columnType === "default") { - const tone = value === "--" ? 'neutral' : 'accent'; - return {value}; + return value === "--" + ? — + : {value}; } if (columnType === "description") { - return {value}; + return {value}; } if (value === "boolean" || value === "false" || value === "true") { - return {value}; + return {value}; } if (typeof value === 'string') { - return {value}; + return {value}; } return value; @@ -103,18 +98,18 @@ const renderCellValue = (row, columnType, value) => { const DocsTable = ({ title = 'API Documentation', as = "h3", description = '', columns = [], data = [] }) => { return (
-
+
- {title} + {title} - {description ? {description} : null} + {description ? {description} : null}
- + {columns.map((column, idx) => ( - + {column.name} @@ -130,9 +125,7 @@ const DocsTable = ({ title = 'API Documentation', as = "h3", description = '', c return ( -
- {itemToRender} -
+ {itemToRender}
); })} diff --git a/docs/components/layout/Documentation/shared.js b/docs/components/layout/Documentation/shared.js index 7765dc50f..665aa6a45 100644 --- a/docs/components/layout/Documentation/shared.js +++ b/docs/components/layout/Documentation/shared.js @@ -1,11 +1,17 @@ -export const docsSectionStackClassName = 'mt-10 flex flex-col gap-14' +// One type scale for every docs page. MDX headings (mdx-components.tsx) use the same values. +export const docsSectionStackClassName = 'mt-10 flex flex-col gap-12' export const docsSectionBlockClassName = 'space-y-4' export const docsSectionHeadingClassName = - 'mb-2 text-2xl font-semibold tracking-[-0.03em] text-gray-1000 sm:text-[1.65rem]' -export const docsSectionIntroClassName = 'space-y-3 pb-2' + 'mb-2 text-[1.5rem] font-semibold leading-[1.2] tracking-[-0.025em] text-gray-1000' +export const docsSubheadingClassName = + 'mb-1 text-[1.125rem] font-semibold leading-[1.3] tracking-[-0.015em] text-gray-1000' +export const docsSectionIntroClassName = 'pb-2' export const docsSectionDividerClassName = 'mt-14 opacity-30' export const docsEyebrowClassName = - 'font-mono text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-green-1000' -export const docsBodyClassName = 'text-[0.98rem] leading-7 text-gray-950' + 'font-mono text-[0.68rem] font-semibold uppercase tracking-[0.14em] text-green-1000' +export const docsBodyClassName = 'text-[0.95rem] leading-7 text-gray-950' export const docsSurfaceClassName = 'overflow-hidden rounded-xl border border-gray-400 bg-gray-50' + +/** Heading class by level, so a table titled with h3 reads smaller than a section h2. */ +export const docsHeadingClassFor = (as = 'h2') => (as === 'h2' ? docsSectionHeadingClassName : docsSubheadingClassName) diff --git a/docs/components/navigation/Category.tsx b/docs/components/navigation/Category.tsx deleted file mode 100644 index e514114a6..000000000 --- a/docs/components/navigation/Category.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import NavItem from './NavItem' - -const Category = ({ categoryItem, pathname, setIsDocsNavOpen }) => { - return
-
{categoryItem.title}
-
    - {categoryItem.items.map((item, itemKey) => { - return
  • setIsDocsNavOpen(false)}> - -
  • - })} -
-
-} - - -export default Category; diff --git a/docs/components/navigation/NavItem.js b/docs/components/navigation/NavItem.js deleted file mode 100644 index 5a3a6fa8b..000000000 --- a/docs/components/navigation/NavItem.js +++ /dev/null @@ -1,29 +0,0 @@ - - -import Link from 'next/link' - -import Badge from "@radui/ui/Badge" - - -const NavItem = ({ item, path, setIsDocsNavOpen }) => { - - - const isCurrentPath = path === item?.path; - const baseItemClasses = "mb-0.5 block rounded-md border px-3 py-1.5 text-[0.84rem] font-medium transition-colors" - const activeClasses = isCurrentPath - ? 'border-green-500 bg-green-100 text-green-1000' - : 'border-transparent text-gray-950 hover:bg-gray-100 hover:text-gray-1000' - - return -
-
- {item?.title} -
- {item.is_new?
New
:null} - {item.is_preview?
Preview
:null} -
- - -} - -export default NavItem; diff --git a/docs/components/navigation/Navigation.tsx b/docs/components/navigation/Navigation.tsx index 85b102dc3..e99663155 100644 --- a/docs/components/navigation/Navigation.tsx +++ b/docs/components/navigation/Navigation.tsx @@ -1,61 +1,208 @@ 'use client' -import { usePathname } from 'next/navigation'; -import { useContext } from 'react'; +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { useContext } from 'react' +import { ChevronRight } from 'lucide-react' -import { NavBarContext } from '@/components/Main/NavBar/NavBarContext'; -import ScrollArea from "@radui/ui/ScrollArea" -import Category from './Category' +import Collapsible from '@radui/ui/Collapsible' +import ScrollArea from '@radui/ui/ScrollArea' + +import { NavBarContext } from '@/components/Main/NavBar/NavBarContext' +import { componentGroups, components, type ComponentGroupId } from '@/app/docs/componentGroups' +import { docsNavigationSections } from '@/app/docs/docsNavigationSections' +import SearchTrigger from './SearchTrigger' import { getDocsProduct } from './products' +type NavLink = { title: string; path: string; is_new?: boolean; is_preview?: boolean } +type NavSection = { type: string; title: string; items: NavLink[] } + +const flatSections = docsNavigationSections as NavSection[] +const sectionByTitle = (title: string) => flatSections.find((section) => section.title === title) + +/** New and Preview flags live on the flat list; look them up by path. */ +const flagsByPath = new Map() +for (const section of flatSections) for (const item of section.items) flagsByPath.set(item.path, item) + +const groupedComponents = componentGroups.map((group) => ({ + ...group, + items: components + .filter((component) => component.group === group.id) + .map((component) => { + const path = `/docs/components/${component.slug}` + const flags = flagsByPath.get(path) + return { title: component.name, path, is_new: flags?.is_new, is_preview: flags?.is_preview } + }) +})) +const Tag = ({ children }: { children: string }) => ( + + {children} + +) + +const NavItem = ({ item, pathname, onNavigate }: { item: NavLink; pathname: string | null; onNavigate: () => void }) => { + const isActive = pathname === item.path + return ( +
  • + + {item.title} + {item.is_new ? New : item.is_preview ? Preview : null} + +
  • + ) +} + +const SectionLabel = ({ children, href }: { children: string; href?: string }) => { + const className = 'block px-3 pb-1.5 font-mono text-[0.62rem] font-semibold uppercase tracking-[0.18em] text-gray-950' + return href ? ( + + {children} + + ) : ( +

    {children}

    + ) +} +/** A collapsible run of links. Open when the current page is inside it, or when asked. */ +const NavGroup = ({ + title, + items, + pathname, + defaultOpen, + onNavigate +}: { + title: string + items: NavLink[] + pathname: string | null + defaultOpen: boolean + onNavigate: () => void +}) => { + const containsActive = items.some((item) => item.path === pathname) + return ( + + + + {title} + {items.length} + + +
      + {items.map((item) => ( + + ))} +
    +
    +
    + ) +} + +const FlatSection = ({ section, pathname, onNavigate }: { section: NavSection; pathname: string | null; onNavigate: () => void }) => ( +
    + {section.title} +
      + {section.items.map((item) => ( + + ))} +
    +
    +) + +const UiDocsNavigation = ({ pathname, onNavigate }: { pathname: string | null; onNavigate: () => void }) => { + const firstSteps = sectionByTitle('First Steps') + const guides = sectionByTitle('Guides') + const contributing = sectionByTitle('Contributing') + const onComponentsIndex = pathname === '/docs/components' + + return ( + <> + {firstSteps ? : null} + +
    + Components +
      +
    • + + All components + {components.length} + +
    • +
    + {groupedComponents.map((group) => ( + + ))} +
    + + {guides ? ( +
    + Guides + +
    + ) : null} + {contributing ? ( +
    + Contributing + +
    + ) : null} + + ) +} -const Navigation = ({ customSections }: { customSections?: any }) => { - const defaultSections = [ - { - type: "CATEGORY", - title: "Main", - items: [ - { title: "Documentation", path: "/docs/first-steps/introduction" }, - { title: "Playground", path: "/playground" }, - { title: "Colors", path: "/colors" }, - { title: "Showcase", path: "/showcase/music-app" } - ] - } - ] - // get path from ssr - const pathname = usePathname(); - const { setIsDocsNavOpen } = useContext(NavBarContext) as { isDocsNavOpen: boolean, setIsDocsNavOpen: (isDocsNavOpen: boolean) => void }; - - const product = getDocsProduct(pathname); - const sections = product ? product.sections : defaultSections; - - - return - -
    -
    - {sections.map((section, i) => { - const isCategory = section.type === "CATEGORY"; - if (isCategory) { - return - } - else{ - return
    - Hello -
    - } - })} -
    -
    - -
    - - - -
    +/** Groups most readers look for first stay open; the rest open when their page is current. */ +const OPEN_BY_DEFAULT = new Set(['forms', 'overlays']) +const Navigation = () => { + const pathname = usePathname() + const { setIsDocsNavOpen } = useContext(NavBarContext) + const product = getDocsProduct(pathname) + const closeMobileNav = () => setIsDocsNavOpen(false) + return ( + + + + + + + + + ) } -export default Navigation; +export default Navigation diff --git a/docs/components/navigation/SearchPalette.tsx b/docs/components/navigation/SearchPalette.tsx new file mode 100644 index 000000000..5f742d408 --- /dev/null +++ b/docs/components/navigation/SearchPalette.tsx @@ -0,0 +1,114 @@ +'use client' +import { useContext, useEffect, useMemo } from 'react' +import { useRouter } from 'next/navigation' +import { BookOpen, Box, Compass, GitPullRequest, Palette, Sparkles, SwatchBook } from 'lucide-react' +import Command from '@radui/ui/Command' + +import { NavBarContext } from '@/components/Main/NavBar/NavBarContext' +import { componentGroups, components } from '@/app/docs/componentGroups' +import { docsNavigationSections } from '@/app/docs/docsNavigationSections' +import { fxNavigationSections } from '@/app/fx/fxNavigationSections' + +type Entry = { title: string; path: string; hint?: string; keywords?: string[] } +type Section = { title: string; items: { title: string; path: string }[] } + +const sectionItems = (sections: readonly Section[], title: string) => + sections.find((section) => section.title === title)?.items ?? [] + +const SITE: Entry[] = [ + { title: 'Playground', path: '/playground', hint: 'Every component, every variant' }, + { title: 'Colors', path: '/colors', hint: 'The 12-step scales' }, + { title: 'Showcase', path: '/showcase/music-app', hint: 'Real screens built with Rad UI' }, + { title: 'Case studies', path: '/built-with', hint: 'Products that run on Rad UI' }, + { title: 'Rad UI FX', path: '/fx', hint: 'Accessible motion components' } +] + +const GROUP_TITLE = new Map(componentGroups.map((group) => [group.id, group.title])) + +/** + * Site-wide search over every docs page, opened from the sidebar, the top bar + * or Cmd/Ctrl+K. Filtering is the Command component's own; each component + * carries its one-line purpose as keywords so "modal" finds Dialog. + */ +const SearchPalette = () => { + const { isSearchOpen, setIsSearchOpen } = useContext(NavBarContext) + const router = useRouter() + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { + event.preventDefault() + setIsSearchOpen(!isSearchOpen) + } + } + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, [isSearchOpen, setIsSearchOpen]) + + const groups = useMemo(() => { + const docs = docsNavigationSections as unknown as Section[] + const fx = fxNavigationSections as unknown as Section[] + return [ + { title: 'Get started', icon: BookOpen, items: sectionItems(docs, 'First Steps') as Entry[] }, + { + title: 'Components', + icon: Box, + items: components.map((component) => ({ + title: component.name, + path: `/docs/components/${component.slug}`, + hint: component.purpose, + keywords: [GROUP_TITLE.get(component.group) ?? '', component.purpose] + })) + }, + { title: 'Guides', icon: Compass, items: sectionItems(docs, 'Guides') as Entry[] }, + { title: 'Contributing', icon: GitPullRequest, items: sectionItems(docs, 'Contributing') as Entry[] }, + { title: 'Site', icon: SwatchBook, items: SITE }, + { + title: 'FX', + icon: Sparkles, + items: fx.flatMap((section) => section.items).map((item) => ({ title: item.title, path: item.path })) + } + ].filter((group) => group.items.length > 0) + }, []) + + const go = (path: string) => { + setIsSearchOpen(false) + router.push(path) + } + + return ( + setIsSearchOpen(open)} + contentClassName="docs-search-dialog" + label="Search documentation" + > + + + Nothing matches. Try a component name or what it does, like “modal”. + {groups.map((group) => ( + + {group.items.map((item) => ( + go(item.path)} + > + + + + {item.title} + {item.hint ? {item.hint} : null} + + + + ))} + + ))} + + + ) +} + +export default SearchPalette diff --git a/docs/components/navigation/SearchTrigger.tsx b/docs/components/navigation/SearchTrigger.tsx new file mode 100644 index 000000000..66e8d9ab6 --- /dev/null +++ b/docs/components/navigation/SearchTrigger.tsx @@ -0,0 +1,41 @@ +'use client' +import { useContext } from 'react' +import { Search } from 'lucide-react' +import Kbd from '@radui/ui/Kbd' + +import { NavBarContext } from '@/components/Main/NavBar/NavBarContext' + +/** + * Opens the search palette. `compact` is the icon-only form for the top bar; + * the default is the input-shaped field at the top of the sidebar. + */ +const SearchTrigger = ({ compact = false }: { compact?: boolean }) => { + const { setIsSearchOpen } = useContext(NavBarContext) + + if (compact) { + return ( + + ) + } + + return ( + + ) +} + +export default SearchTrigger diff --git a/docs/mdx-components.tsx b/docs/mdx-components.tsx index 21391cf99..e80393fca 100644 --- a/docs/mdx-components.tsx +++ b/docs/mdx-components.tsx @@ -17,12 +17,12 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { ), h2: ({ children }) => ( - + {children} ), h3: ({ children }) => ( - + {children} ), @@ -42,7 +42,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { ), p: ({ children }) => ( - + {children} ), @@ -79,7 +79,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { ), li: ({ children }) => ( -
  • {children}
  • +
  • {children}
  • ), a: ({ children, href, ...props }) => ( ), blockquote: ({ children }) => ( -
    +
    {children}
    ), diff --git a/docs/utils/radVersion.ts b/docs/utils/radVersion.ts new file mode 100644 index 000000000..1d238eb90 --- /dev/null +++ b/docs/utils/radVersion.ts @@ -0,0 +1,4 @@ +import pkg from '../package.json' + +/** The `@radui/ui` version the docs are pinned to, without the range prefix. */ +export const RAD_UI_VERSION = String(pkg.dependencies['@radui/ui']).replace(/^[^\d]*/, '') diff --git a/docs/utils/useLightTokens.ts b/docs/utils/useLightTokens.ts new file mode 100644 index 000000000..13784ab31 --- /dev/null +++ b/docs/utils/useLightTokens.ts @@ -0,0 +1,33 @@ +'use client' +import { useEffect, useState } from 'react' + +const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 1000] +const RAMPS = ['gray', 'blue', 'violet', 'crimson', 'orange', 'jade', 'red', 'green', 'amber'] +const LIGHT_TOKENS = [ + ...RAMPS.flatMap((ramp) => STEPS.map((step) => `--rad-ui-color-${ramp}-${step}`)), + ...['xs', 'sm', 'md', 'lg', 'xl'].map((size) => `--rad-ui-shadow-${size}`) +] + +/** + * Clarity declares its light ramps on :root and only overrides them under + * [data-rad-ui-theme="dark"], so a light Theme nested in a dark page would + * inherit dark values. When that happens, re-declare the root (light) values + * on the stage so the nested appearance is honest. + */ +export function useLightTokens(enabled: boolean) { + const [vars, setVars] = useState>({}) + useEffect(() => { + if (!enabled) { + setVars({}) + return + } + const root = getComputedStyle(document.documentElement) + const next: Record = {} + for (const name of LIGHT_TOKENS) { + const value = root.getPropertyValue(name).trim() + if (value) next[name] = value + } + setVars(next) + }, [enabled]) + return vars +}