diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c1a16c8b..6961c0eb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -62,6 +62,28 @@ jobs:
- name: Lint
run: pnpm check
+ typecheck:
+ name: Typecheck
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Typecheck
+ run: pnpm typecheck
+
test:
name: Test
runs-on: ubuntu-latest
@@ -84,6 +106,10 @@ jobs:
- name: Test
run: pnpm test
+ - name: Verify registry output (no workspace import leaks)
+ working-directory: apps/docs
+ run: npx tsx scripts/verify-registry.mts
+
build:
name: Build
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 09cec6be..fccf8ca2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,7 @@ node_modules
.yarn/install-state.gz
# testing
-/coverage
+coverage/
# next.js
.next/
@@ -51,3 +51,7 @@ AGENT.md
# internal planning docs (never committed)
planning/
+.agents/
+node-compile-cache/
+skills-lock.json
+.claude/skills/
diff --git a/.ui-craft/brief.md b/.ui-craft/brief.md
new file mode 100644
index 00000000..58bc8717
--- /dev/null
+++ b/.ui-craft/brief.md
@@ -0,0 +1,63 @@
+# SmoothUI — Design Brief
+
+Surface: the SmoothUI public site (landing + docs + Theme Studio at smoothui.dev).
+Scoped to the marketing/docs surface, not the installed components themselves.
+Created 2026-06-12. Append-mostly — date every change, never delete principles.
+
+## 1. Product purpose
+
+The place where React developers and AI agents discover, preview live, and
+install animated shadcn-compatible components in one command.
+
+## 2. Primary user
+
+A React developer already using shadcn/ui, evaluating components on a desktop,
+who wants to preview the motion, then `npx shadcn add` and ship in minutes — and
+the AI coding agents that install on their behalf. Both are first-class.
+
+## 3. Principles (conflict-resolution order — higher wins)
+
+1. **Show the component, not the chrome.** The live demos are the product; site
+ UI retreats so the preview is the loudest thing on screen. Decoration loses to
+ the demo every time.
+2. **Installable beats impressive.** The install command is never more than one
+ click from the preview. Optimize for "copy and ship", not for browsing.
+3. **Machine-readable is a first-class surface.** Anything a human can find
+ (component, props, install), an agent can fetch as structured data. No
+ human-only affordance for core discovery.
+4. **Motion proves the point, never decorates.** Animation demonstrates a
+ component's behavior; site chrome animates only to communicate state or space,
+ and always honors `prefers-reduced-motion`.
+5. **One signature, applied everywhere.** The dotted-frame motif and the brand
+ pink are the identity; consistency across landing, docs and studio beats
+ per-page novelty.
+
+> Override example: if the dotted-frame motif (P5) competes with a component
+> preview for attention (P1), P1 wins — the frame retreats.
+
+## 4. Success metric
+
+A developer lands on a component page and has the install command copied (or the
+code panel open) within ~15 seconds — and an agent can resolve the same
+component's install JSON in a single request.
+
+## 5. Out of scope
+
+- Does not persist user state server-side — Theme Studio presets are URL-encoded, not accounts.
+- Does not re-explain shadcn basics — links out rather than duplicating shadcn's docs.
+- Does not gate core component discovery behind JS-only interactions.
+- Does not blur the free/Pro line — Pro content lives on pro.smoothui.dev.
+- Does not become a generic theming playground — the studio themes SmoothUI's own surface.
+
+## Visual direction (durable decisions, 2026-06-12)
+
+- **Style: Soft Modern, refined.** Evolve the current look — rounded cards, soft
+ layered shadows (ambient + direct, no heavy glow), generous whitespace, neutrals
+ with a subtle tint. Polish and consistency over reinvention.
+- **Accent: brand pink, kept.** `oklch(0.72 0.2 352.53)`. Budget it to 3–5
+ placements per above-the-fold viewport; lower chroma on secondary uses.
+- **Signature: the dotted frame** (`.frame-box`) is the recurring motif.
+- **Type: Poppins display / Inter body** — keep, but unify into a shared type
+ scale consumed by landing AND docs prose (today they diverge).
+- **Audience: humans + machines equally** — keep the AI-native surface
+ (llms.txt, skill, structured endpoints) first-class alongside the visual design.
diff --git a/.ui-craft/tokens.md b/.ui-craft/tokens.md
new file mode 100644
index 00000000..7ab75f55
--- /dev/null
+++ b/.ui-craft/tokens.md
@@ -0,0 +1,80 @@
+# SmoothUI — Token Spine
+
+Audit + decisions for the design-token spine. Anchored to `brief.md`.
+Created 2026-06-12. The project already has a Tailwind v4 `@theme inline` system
+in `apps/docs/app/smoothui.css` + `global.css` — this **extends**, never replaces.
+
+## Layer status (audit)
+
+| Category | State | Source |
+|---|---|---|
+| Neutral ramp | ✅ rebuilt, perceptually uniform, WCAG AA (f1ce982) | `smoothui.css` `--color-smooth-*` |
+| Accent (brand) | ✅ pink `oklch(0.72 0.2 352.53)` + secondary/light/lighter | `smoothui.css` `--color-brand*` |
+| Semantic color | ✅ shadcn-style (background/foreground/muted/border/input/sidebar/chart) | `smoothui.css` `@theme inline` |
+| Spacing | ✅ Tailwind v4 defaults — **no gap**, issue is uniform *usage* (py-24 everywhere) | Tailwind |
+| Radius | ⚠️ `--radius` + sm/md/lg/xl exist, but applied uniformly (`rounded-xl` everywhere) | `smoothui.css` lines 48-51 |
+| Type scale | ❌ no shared semantic roles; landing (Poppins) / docs prose / studio (13px) diverge; `prose h2` cliffs to 18px | ad-hoc |
+| Shadows | ✅ `--shadow-custom`, `--shadow-custom-brand` (layered) | `global.css` |
+| Motion | ❌ no duration/easing tokens; components hardcode (0.2-0.3s, cubic-beziers) | ad-hoc |
+| Z-index | ⚠️ ad-hoc; not tokenized | — |
+
+## Decisions
+
+### Type scale (the unification core)
+
+One scale, consumed by landing AND docs prose AND studio. Poppins = display,
+Inter = body/UI, mono for code/values. Roles (apply consistently, stop ad-hoc sizes):
+
+| Role | Size | Weight | Tracking | Use |
+|---|---|---|---|---|
+| display-xl | 60px (text-6xl) | 600 | -0.02em | hero headline |
+| display-lg | 48px (text-5xl) | 600 | -0.02em | section headlines |
+| heading | 30px (text-3xl) | 600 | -0.01em | page / prose h1 |
+| subheading | 24px (text-2xl) | 600 | -0.01em | **prose h2 (fixes 18px cliff)** |
+| title | 19px (text-lg/xl) | 600 | normal | prose h3, card titles |
+| body | 16px (text-base) | 400 | normal | prose p, descriptions |
+| label | 13px | 500 | normal | UI labels, studio inspector |
+| eyebrow | 12-13px | 600 | 0.05em, uppercase | category labels (the ONE allowed uppercase) |
+
+Prose heading scale in `global.css` is corrected to follow this (h2 → subheading,
+not text-lg).
+
+### Radius — vary by element (kill uniform `rounded-xl`)
+
+| Token | px | Element |
+|---|---|---|
+| `--radius-sm` (calc -4) | ~6px | inputs, badges, segmented controls |
+| `--radius-md` (calc -2) | ~8px | buttons, small panels, studio rows |
+| `--radius-lg` (`--radius` 10px) | 10px | cards |
+| `--radius-xl` (calc +4) | ~14px | modals, large panels, hero frame |
+| `--radius-full` | pill | tags, avatars, the float nav |
+
+Base `--radius` stays `0.625rem` (10px). Soft Modern = lean toward the larger end
+for cards (`rounded-2xl`/14px) where it reads friendly, per brief visual direction.
+
+### Motion tokens (new category)
+
+Match the existing `.cursor/rules/animations.mdc` convention — don't invent new curves.
+
+```css
+--duration-fast: 0.15s; /* color/opacity, hover */
+--duration-normal: 0.25s; /* standard UI: dropdowns, toggles */
+--duration-slow: 0.4s; /* modals, drawers, page-level */
+--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* entering */
+--ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); /* moving */
+```
+
+Exit ≈ 75% of enter. Always honor `prefers-reduced-motion`. Spring bounce ≤ 0.1 for UI.
+
+### Accent budget (brief P-color)
+
+Brand pink: max 3-5 placements per above-the-fold viewport. Secondary uses drop
+chroma (`--color-brand-light/lighter`). Today the pink appears in logo + sidebar
+active + TOC active + eyebrows + CTA simultaneously — trim per viewport.
+
+## Scope boundary
+
+This spine doc + the additive **motion tokens** + the **prose type-scale fix** are
+the token-phase deliverables. Applying the full type/radius scale across every
+landing section and component is the **visual redesign phase** (next), done with
+the browser in the loop.
diff --git a/apps/docs/app/(home)/docs/page.tsx b/apps/docs/app/(home)/docs/page.tsx
index 0ba34bb6..f69b9894 100644
--- a/apps/docs/app/(home)/docs/page.tsx
+++ b/apps/docs/app/(home)/docs/page.tsx
@@ -1,7 +1,7 @@
import { cn } from "@repo/shadcn-ui/lib/utils";
-import { Library, Pencil } from "lucide-react";
import type { Metadata } from "next";
import Link, { type LinkProps } from "next/link";
+import { IconBooksFill24, IconPencilFill24 } from "nucleo-core-fill-24";
export const metadata: Metadata = {
alternates: {
@@ -23,19 +23,19 @@ export default function DocsPage() {
{
name: "Documentation",
description: "The guides for SmoothUI.",
- icon: ,
+ icon: ,
href: "/docs/guides",
},
{
name: "Components",
description: "The library of SmoothUI components.",
- icon: ,
+ icon: ,
href: "/docs/components",
},
{
name: "Blocks",
description: "The library of SmoothUI blocks.",
- icon: ,
+ icon: ,
href: "/docs/blocks",
},
].map((item) => (
diff --git a/apps/docs/app/(home)/page.tsx b/apps/docs/app/(home)/page.tsx
index 3ede73e0..3d0a74aa 100644
--- a/apps/docs/app/(home)/page.tsx
+++ b/apps/docs/app/(home)/page.tsx
@@ -1,13 +1,15 @@
import { AISection } from "@docs/components/landing/ai-section";
import { BlockCategories } from "@docs/components/landing/block-categories";
import { ComponentsSlideshow } from "@docs/components/landing/components-slideshow";
+import { Coverage } from "@docs/components/landing/coverage";
import { FAQ } from "@docs/components/landing/faqs";
import { Features } from "@docs/components/landing/features";
import Footer from "@docs/components/landing/footer";
import { Hero } from "@docs/components/landing/hero";
+import { LatestPosts } from "@docs/components/landing/latest-posts";
import { SkillsSection } from "@docs/components/landing/skills-section";
+import { SocialProof } from "@docs/components/landing/social-proof";
import { WhatTheySay } from "@docs/components/landing/what-they-say";
-import { getSmoothuiStats } from "@docs/lib/smoothui-stats";
import type { Metadata } from "next";
import { Suspense } from "react";
@@ -29,22 +31,26 @@ function SectionSkeleton({ minHeight = "400px" }: { minHeight?: string }) {
}
export default function Home() {
- const stats = getSmoothuiStats();
return (
<>
-
+
}>
+
-
}>
-
+
+
+ }>
+
+
+
>
);
}
diff --git a/apps/docs/app/(studio)/layout.tsx b/apps/docs/app/(studio)/layout.tsx
new file mode 100644
index 00000000..4d9ecdf4
--- /dev/null
+++ b/apps/docs/app/(studio)/layout.tsx
@@ -0,0 +1,14 @@
+import { FloatNav } from "@docs/components/float-nav";
+import Navbar from "@docs/components/landing/navbar/navbar";
+
+// Full-bleed layout for studio-style screens (no max-w container, unlike
+// the (home) landing layout): the canvas must reach the viewport edges.
+export default function Layout({ children }: LayoutProps<"/">) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/docs/app/(studio)/themes/page.tsx b/apps/docs/app/(studio)/themes/page.tsx
new file mode 100644
index 00000000..402909c6
--- /dev/null
+++ b/apps/docs/app/(studio)/themes/page.tsx
@@ -0,0 +1,19 @@
+import { ThemeStudio } from "@docs/components/themes/theme-studio";
+import { createMetadata } from "@docs/lib/metadata";
+import type { Metadata } from "next";
+
+export const metadata: Metadata = createMetadata({
+ title: "Themes — SmoothUI",
+ description:
+ "Installable SmoothUI color themes for shadcn projects. Six palettes with light and dark mode, applied with a single CLI command.",
+ alternates: {
+ canonical: "/themes",
+ },
+ openGraph: {
+ url: "/themes",
+ },
+});
+
+export default function ThemesPage() {
+ return ;
+}
diff --git a/apps/docs/app/blocks/preview/[blockId]/page.tsx b/apps/docs/app/blocks/preview/[blockId]/page.tsx
index 132ada9e..1253dfa7 100644
--- a/apps/docs/app/blocks/preview/[blockId]/page.tsx
+++ b/apps/docs/app/blocks/preview/[blockId]/page.tsx
@@ -25,6 +25,10 @@ export default async function BlockPreviewPage({ params }: PageProps) {
(mod) => mod.default
);
+ if (typeof BlockExample !== "function") {
+ notFound();
+ }
+
return (
diff --git a/apps/docs/app/blog/[slug]/page.tsx b/apps/docs/app/blog/[slug]/page.tsx
index 4dc4fed0..8f90fe9f 100644
--- a/apps/docs/app/blog/[slug]/page.tsx
+++ b/apps/docs/app/blog/[slug]/page.tsx
@@ -1,4 +1,5 @@
import { ArticleSchema } from "@docs/components/article-schema";
+import { PostCover } from "@docs/components/post-cover";
import { RelatedPosts } from "@docs/components/related-posts";
import { ShareButtons } from "@docs/components/share-buttons";
import { createMetadata } from "@docs/lib/metadata";
@@ -43,7 +44,37 @@ export default async function BlogPostPage({ params }: PageProps) {
title={post.data.title}
url={post.url}
/>
-
+
+
+
+
{post.data.title}
@@ -63,7 +102,11 @@ export default async function BlogPostPage({ params }: PageProps) {
{post.data.author as string}
diff --git a/apps/docs/app/blog/layout.tsx b/apps/docs/app/blog/layout.tsx
index ccb33742..26438b6e 100644
--- a/apps/docs/app/blog/layout.tsx
+++ b/apps/docs/app/blog/layout.tsx
@@ -4,15 +4,12 @@ import { BgLines } from "@docs/components/landing/bg-lines";
import Divider from "@docs/components/landing/divider";
import Footer from "@docs/components/landing/footer";
import Navbar from "@docs/components/landing/navbar/navbar";
-import { getSmoothuiStats } from "@docs/lib/smoothui-stats";
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
- const stats = getSmoothuiStats();
-
return (
@@ -37,7 +34,7 @@ export default function BlogLayout({
/>
-
+
);
}
diff --git a/apps/docs/app/blog/page.tsx b/apps/docs/app/blog/page.tsx
index f0a31889..68e9fd49 100644
--- a/apps/docs/app/blog/page.tsx
+++ b/apps/docs/app/blog/page.tsx
@@ -1,13 +1,17 @@
+import { PostCover } from "@docs/components/post-cover";
import { createMetadata } from "@docs/lib/metadata";
import { blogSource, formatDate, getReadingTime } from "@docs/lib/source";
-import { ChevronRight } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
+import { IconChevronRightFill24 } from "nucleo-core-fill-24";
export const metadata: Metadata = createMetadata({
- title: "Blog",
+ title: "React Animation Tutorials & Guides",
description:
"Tutorials, tips, and case studies on building beautiful animated React components with SmoothUI and Tailwind CSS.",
+ alternates: {
+ canonical: "/blog",
+ },
openGraph: {
url: "/blog",
},
@@ -40,42 +44,14 @@ function ReadLink({ title }: { title: string }) {
className="flex items-center gap-1 font-medium text-brand text-sm transition-colors duration-200 group-hover:text-foreground"
>
Read
-
);
}
-/** Abstract decorative illustration for the featured post. Mesh gradient, CSS-only. */
-function FeaturedIllustration() {
- return (
-
- );
-}
-
export default async function BlogPage() {
const posts = blogSource.getPages().sort((a, b) => {
const dateA = new Date(a.data.date as string);
@@ -98,7 +74,7 @@ export default async function BlogPage() {
{/* Header */}
- Blog
+ React Animation Tutorials & Guides
Tutorials, deep dives, and case studies on building beautiful
@@ -111,7 +87,13 @@ export default async function BlogPage() {
{/* Featured post */}
0 && (
- {rest.map(async (post) => {
- const content = await post.data.getText("processed");
- const readingTime = getReadingTime(content);
-
- return (
-
-
-
-
-
- {post.data.title}
-
-
- {post.data.description}
-
-
-
- {post.data.author && (
-
- )}
-
-
-
-
- );
- })}
+ {rest.map((post) => (
+
+
+
+
+
+ {post.data.title}
+
+
+ {post.data.description}
+
+
+
+ {post.data.author && (
+
+ )}
+
+
+
+
+ ))}
)}
diff --git a/apps/docs/app/docs/[...slug]/page.tsx b/apps/docs/app/docs/[...slug]/page.tsx
index db3f2dc7..b2e4119b 100644
--- a/apps/docs/app/docs/[...slug]/page.tsx
+++ b/apps/docs/app/docs/[...slug]/page.tsx
@@ -1,3 +1,4 @@
+import { AddToKitButton } from "@docs/components/add-to-kit-button";
import { BodyText } from "@docs/components/body-text";
import { BreadcrumbSchema } from "@docs/components/breadcrumb-schema";
import { BundleSizeBadge } from "@docs/components/bundle-size-badge";
@@ -45,6 +46,11 @@ const AutoTypeTableWithGenerator = (
props: React.ComponentProps
) => ;
+// Renders BodyText as div to avoid -in-
hydration errors when MDX wraps content in
+const BodyTextAsDiv = (props: React.ComponentProps) => (
+
+);
+
export default async function Page(props: PageProps<"/docs/[...slug]">) {
const params = await props.params;
const page = source.getPage(params.slug);
@@ -147,7 +153,7 @@ export default async function Page(props: PageProps<"/docs/[...slug]">) {
) {
markdownUrl={`${page.url}.mdx`}
/>
{registryUrl && }
- {componentName && }
- {lastModified && (
-
)}
+ {(componentName || lastModified) && (
+
+ {componentName && }
+ {lastModified && }
+
+ )}
{page.data.installer && (
<>
Installation
-
+
>
)}
) {
PoweredBy,
Reference,
Contributor,
- BodyText,
+ BodyText: BodyTextAsDiv,
FeatureCard,
FeatureCardHover,
Divider,
@@ -228,9 +240,18 @@ export async function generateMetadata(
title: "Not Found",
});
+ // Per-page fallback so pages without a frontmatter description don't all share
+ // one generic (thin/duplicate) meta description.
+ const section = slug[0];
+ const kind =
+ section === "blocks"
+ ? "block"
+ : section === "guides"
+ ? "guide"
+ : "component";
const description =
page.data.description ??
- "Beautiful React components with smooth animations";
+ `${page.data.title} — an animated React ${kind} for shadcn/ui, built with Motion and Tailwind CSS. Copy, paste, and ship.`;
const image = {
url: getPageImage(page).url,
diff --git a/apps/docs/app/docs/layout.tsx b/apps/docs/app/docs/layout.tsx
index daba429a..b53cde6f 100644
--- a/apps/docs/app/docs/layout.tsx
+++ b/apps/docs/app/docs/layout.tsx
@@ -1,29 +1,37 @@
+import DocsFooter from "@docs/components/docs-footer";
import { FloatNav } from "@docs/components/float-nav";
-import Footer from "@docs/components/landing/footer";
import { NavSponsorCard } from "@docs/components/nav-sponsor-card";
import { decorateNewPages } from "@docs/lib/decorate-new";
import { baseOptions } from "@docs/lib/layout.shared";
-import { getSmoothuiStats } from "@docs/lib/smoothui-stats";
import { source } from "@docs/lib/source";
-import { DocsLayout } from "fumadocs-ui/layouts/docs";
+import { DocsLayout } from "fumadocs-ui/layouts/notebook";
export default function Layout({ children }: LayoutProps<"/docs">) {
const tree = decorateNewPages(source.pageTree, source);
- const stats = getSmoothuiStats();
+ const options = baseOptions();
return (
- <>
- ,
- }}
- >
- {children}
-
-
-
- >
+ ,
+ }}
+ tabMode="navbar"
+ >
+ {children}
+
+ {/* SEO footer inside the scroll flow, spanning the full grid width
+ so it lands at the bottom without breaking the 100dvh layout.
+ DocsFooter constrains its content to the main column band. */}
+
+
+
+
);
}
diff --git a/apps/docs/app/fonts.ts b/apps/docs/app/fonts.ts
index 48efc6d1..b8544abe 100644
--- a/apps/docs/app/fonts.ts
+++ b/apps/docs/app/fonts.ts
@@ -1,4 +1,4 @@
-import { Inter, Poppins } from "next/font/google";
+import { Inter, Plus_Jakarta_Sans, Poppins } from "next/font/google";
export const inter = Inter({
subsets: ["latin"],
@@ -12,3 +12,10 @@ export const poppins = Poppins({
variable: "--font-poppins",
weight: ["400", "500", "600", "700", "800", "900"],
});
+
+export const plusJakartaSans = Plus_Jakarta_Sans({
+ subsets: ["latin"],
+ display: "swap",
+ variable: "--font-plus-jakarta-sans",
+ weight: ["400", "500", "600", "700", "800"],
+});
diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css
index 559a5abd..039fca2e 100644
--- a/apps/docs/app/global.css
+++ b/apps/docs/app/global.css
@@ -11,7 +11,7 @@
@theme inline {
--font-body: var(--font-inter);
- --font-title: var(--font-poppins);
+ --font-title: var(--font-plus-jakarta-sans);
--shiki-theme: var(--shiki--light);
--shiki--light: var(--shiki-theme);
--shiki--dark: var(--shiki-theme);
@@ -36,6 +36,13 @@
--lines-page: url("/lines-b.png");
--color-fd-primary: var(--color-brand);
--color-fd-secondary: var(--color-smooth-50);
+ /* Motion spine — match .cursor/rules/animations.mdc. Exit ~75% of enter;
+ always honor prefers-reduced-motion. See .ui-craft/tokens.md. */
+ --duration-fast: 0.15s;
+ --duration-normal: 0.25s;
+ --duration-slow: 0.4s;
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
+ --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
}
.dark {
@@ -56,10 +63,12 @@
border-color: var(--color-gray-200, currentColor);
}
* {
+ scrollbar-color: gray transparent;
+ scrollbar-width: thin;
@apply border-border outline-ring/50;
}
body {
- @apply bg-background text-foreground;
+ @apply bg-background text-foreground antialiased;
}
.dark {
@@ -90,6 +99,16 @@
@apply from-brand-secondary to-brand bg-gradient-to-bl;
}
+/* Nucleo duo-tone: color-2 inherits parent icon color at reduced opacity */
+[data-color="color-2"] {
+ color: color-mix(in oklch, currentColor 25%, transparent);
+}
+
+/* Active sidebar items: color-2 uses brand tint */
+[data-active="true"] [data-color="color-2"] {
+ color: color-mix(in oklch, var(--color-brand) 35%, transparent);
+}
+
.shadow-custom-btgray {
box-shadow:
0px 1px 2px rgba(0, 0, 0, 0.4),
@@ -128,7 +147,7 @@
}
.frame-box {
- @apply bg-primary inset-ring-background border inset-ring-2 before:absolute before:inset-0 before:bg-[radial-gradient(var(--dots-color)_1px,transparent_1px)] before:[background-size:16px_16px] before:[--dots-color:--alpha(var(--color-foreground)/5%)];
+ @apply bg-primary inset-ring-background border inset-ring-2 before:pointer-events-none before:absolute before:inset-0 before:bg-[radial-gradient(var(--dots-color)_1px,transparent_1px)] before:[background-size:16px_16px] before:[--dots-color:--alpha(var(--color-foreground)/5%)];
}
/* Price Flow Animations */
@@ -199,22 +218,22 @@
@apply font-semibold text-foreground tracking-tight;
}
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
- @apply -tracking-wide font-bold text-3xl;
+ @apply font-extrabold text-4xl leading-tight;
}
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
- @apply font-semibold text-lg;
+ @apply font-bold text-2xl leading-tight;
}
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
- @apply font-semibold text-base;
+ @apply font-semibold text-xl leading-tight;
}
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
- @apply font-semibold text-sm;
+ @apply font-semibold text-lg leading-tight;
}
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
- @apply font-semibold text-xs;
+ @apply font-semibold text-base;
}
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
- @apply font-medium text-xs;
+ @apply font-semibold text-sm;
}
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
@apply text-base;
@@ -253,3 +272,135 @@
.animate-accordion-up {
animation: accordion-up 0.2s ease-out;
}
+
+/* Center the docs content column within its (full-width) grid track,
+ mirroring ElevenLabs' `w-content-width mx-auto` article. */
+#nd-page {
+ width: 100%;
+ max-width: calc(900px + 2 * 2rem);
+ margin-inline: auto;
+}
+
+/* Full pages (no TOC) stretch to the whole main track; also lift the
+ *:max-w-[1285px] child cap fumadocs applies in full mode. Unlayered
+ rules win over the layered utilities. */
+#nd-page[data-full="true"] {
+ max-width: none;
+}
+
+#nd-page[data-full="true"] > * {
+ max-width: none;
+}
+
+/* Docs headings use the title font (Plus Jakarta Sans). */
+#nd-page :is(h1, h2, h3, h4, h5, h6) {
+ font-family: var(--font-title);
+}
+
+/* Override fumadocs utility classes on headings — unlayered beats @layer utilities */
+#nd-page h1 {
+ font-size: var(--text-4xl);
+ font-weight: 800;
+ line-height: var(--leading-tight);
+}
+#nd-page h2 {
+ font-size: var(--text-2xl);
+ font-weight: 700;
+ line-height: var(--leading-tight);
+}
+#nd-page h3 {
+ font-size: var(--text-xl);
+ font-weight: 600;
+ line-height: var(--leading-tight);
+}
+#nd-page h4 {
+ font-size: var(--text-lg);
+ font-weight: 600;
+ line-height: var(--leading-tight);
+}
+#nd-page h5 {
+ font-size: var(--text-base);
+ font-weight: 600;
+}
+#nd-page h6 {
+ font-size: var(--text-sm);
+ font-weight: 600;
+}
+
+/* ElevenLabs-style docs chrome: clean white surfaces + hairline borders,
+ built from our own tokens (no hardcoded colors). */
+#nd-notebook-layout,
+#nd-sidebar {
+ --fd-sidebar-width: 240px;
+}
+
+/* The fumadocs notebook container sets grid-template via inline style using
+ `calc(var(--fd-layout-width,97rem) - sidebar - toc)` for the main track.
+ Percentage values in CSS vars don't resolve against the grid container's
+ inline size (circular), so the outer 1fr gutter columns end up non-zero
+ even with --fd-layout-width:100%. Override the column template directly
+ to collapse them to 0px.
+
+ Mobile: sidebar is max-md:hidden but with collapsible:false collapsed=false
+ always, so --fd-sidebar-col stays at 240px. Force column 2 to 0px on mobile
+ so the hidden sidebar doesn't occupy grid space. */
+@media (max-width: 767px) {
+ #nd-notebook-layout {
+ grid-template-columns: 0px 0px 1fr 0px 0px !important;
+ }
+}
+@media (min-width: 768px) and (max-width: 1279px) {
+ #nd-notebook-layout {
+ grid-template-columns: 0px var(--fd-sidebar-col) 1fr var(--fd-toc-width) 0px !important;
+ }
+
+ #nd-notebook-layout:has(#nd-page[data-full="true"]) {
+ grid-template-columns: 0px var(--fd-sidebar-col) 1fr 0px 0px !important;
+ }
+}
+
+/* At xl+, always reserve 268px for the toc column so content width is identical
+ on pages with and without a TOC. --fd-toc-width on #nd-notebook-layout itself
+ is overridden by the [--fd-toc-width:0px] utility class on the same element,
+ so hardcode the column value directly instead of relying on the variable.
+ Exception: full pages (no TOC by design, e.g. the components gallery)
+ collapse the toc column and let the main track take the whole width. */
+@media (min-width: 1280px) {
+ #nd-notebook-layout {
+ grid-template-columns: 0px var(--fd-sidebar-col) 1fr 240px 0px !important;
+ }
+
+ #nd-notebook-layout:has(#nd-page[data-full="true"]) {
+ grid-template-columns: 0px var(--fd-sidebar-col) 1fr 0px 0px !important;
+ }
+}
+
+/* Top nav: solid surface instead of the translucent gray. The inner
+ tabs row already carries its own border-b, so no border here. */
+#nd-subnav {
+ background-color: var(--color-background);
+}
+
+/* Sidebar: hairline separator from the content column. */
+#nd-sidebar {
+ border-inline-end: 1px solid var(--color-border);
+}
+
+/* Match TOC column width to sidebar (fumadocs default 268px → 240px).
+ xl:layout:[--fd-toc-width:268px] sets it on #nd-notebook-layout via :has();
+ this unlayered rule wins over @layer utilities. */
+#nd-notebook-layout {
+ --fd-toc-width: 240px;
+}
+
+/* Three sources of top gap in the sidebar when navMode="top" + collapsible=false:
+ 1. Header div (p-4 pb-2) has no visible content → collapse it
+ 2. ScrollViewport inner div has p-4 top padding → remove it
+ 3. Hidden lg:hidden menu items precede the first separator so first:mt-0 never
+ fires and the separator keeps mt-6; pt-0 on the viewport absorbs that. */
+#nd-sidebar > div:first-child {
+ padding-block: 0 !important;
+}
+#nd-sidebar [data-radix-scroll-area-viewport] > div {
+ padding-top: 0 !important;
+}
diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx
index 453175d7..ef29bc3b 100644
--- a/apps/docs/app/layout.tsx
+++ b/apps/docs/app/layout.tsx
@@ -1,10 +1,11 @@
+import { SoundProvider } from "@docs/components/sound-provider";
+import { KitProvider } from "@docs/lib/kit-context";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { RootProvider } from "fumadocs-ui/provider/next";
import type { Metadata } from "next";
-import Script from "next/script";
import "./global.css";
-import { inter, poppins } from "./fonts";
+import { inter, plusJakartaSans, poppins } from "./fonts";
import { smoothUISchema } from "./utils/schema";
const enableVercelAnalytics =
@@ -84,7 +85,7 @@ export const metadata: Metadata = {
export default function Layout({ children }: LayoutProps<"/">) {
return (
@@ -96,20 +97,21 @@ export default function Layout({ children }: LayoutProps<"/">) {
title="SmoothUI Blog"
type="application/rss+xml"
/>
-
-
-
+
+
{enableVercelAnalytics && }
{enableVercelAnalytics && }
- {children}
+
+
+ {children}
+
+