From 22b30436ed3f70ac1bbcc0fb059db20911c592c0 Mon Sep 17 00:00:00 2001 From: Muhammed Sanjid Date: Sat, 21 Mar 2026 20:36:51 +0530 Subject: [PATCH 1/2] feat ui update --- apps/platform/app/(docs)/cli/page.tsx | 16 +- .../components/{[slug] => [...slug]}/page.tsx | 63 ++-- apps/platform/app/(docs)/components/page.tsx | 14 +- .../platform/app/(docs)/installation/page.tsx | 27 +- .../platform/app/(docs)/introduction/page.tsx | 141 ++++++++ apps/platform/app/(docs)/layout.tsx | 46 +-- apps/platform/app/(docs)/registry/page.tsx | 163 +++++++++ apps/platform/app/dashboard/page.tsx | 55 +++ apps/platform/app/globals.css | 101 ++++++ apps/platform/app/layout.tsx | 2 - apps/platform/app/page.tsx | 108 ++++-- apps/platform/components.json | 3 +- .../animate-ui/components/buttons/copy.tsx | 52 +-- apps/platform/components/app-sidebar.tsx | 110 ++++++ .../platform/components/core/3d-container.tsx | 44 +++ .../platform/components/core/docs-sidebar.tsx | 32 +- apps/platform/components/core/docs-toc.tsx | 339 ++++++++++++++++++ .../components/core/landing-navbar.tsx | 58 +++ apps/platform/components/core/logo.tsx | 7 +- apps/platform/components/core/navbar.tsx | 132 +++---- apps/platform/components/core/toc-context.tsx | 41 +++ .../components/mdx/component-preview.tsx | 120 +++++++ .../components/mdx/installation-cmd.tsx | 165 +++++++++ .../components/mdx/manual-installation.tsx | 126 +++++++ apps/platform/components/nav-main.tsx | 86 +++++ apps/platform/components/nav-projects.tsx | 86 +++++ apps/platform/components/nav-secondary.tsx | 41 +++ apps/platform/components/nav-user.tsx | 107 ++++++ .../scroll-fade-effect/scroll-fade-effect.tsx | 29 ++ .../components/showcase/code-block.tsx | 95 ++--- .../components/showcase/component-docs.tsx | 18 +- .../showcase/component-installation.tsx | 103 ++++++ .../components/showcase/docs-primitives.tsx | 132 +++---- .../components/showcase/docs-steps.tsx | 67 ++++ .../components/showcase/video-card.tsx | 2 +- apps/platform/components/ui/avatar.tsx | 112 ++++++ apps/platform/components/ui/breadcrumb.tsx | 122 +++++++ apps/platform/components/ui/command.tsx | 14 +- apps/platform/components/ui/dropdown-menu.tsx | 268 ++++++++++++++ .../components/ui/progressive-blur.tsx | 63 ++++ apps/platform/components/ui/sheet.tsx | 17 +- apps/platform/components/ui/sidebar.tsx | 284 +++++++-------- apps/platform/components/ui/skeleton.tsx | 10 +- apps/platform/content/components/button.mdx | 78 ++-- apps/platform/content/components/text.mdx | 53 +-- apps/platform/lib/component-docs.ts | 23 +- apps/platform/lib/component-index.ts | 3 +- apps/platform/package.json | 3 + 48 files changed, 3236 insertions(+), 545 deletions(-) rename apps/platform/app/(docs)/components/{[slug] => [...slug]}/page.tsx (63%) create mode 100644 apps/platform/app/(docs)/introduction/page.tsx create mode 100644 apps/platform/app/(docs)/registry/page.tsx create mode 100644 apps/platform/app/dashboard/page.tsx create mode 100644 apps/platform/components/app-sidebar.tsx create mode 100644 apps/platform/components/core/3d-container.tsx create mode 100644 apps/platform/components/core/docs-toc.tsx create mode 100644 apps/platform/components/core/landing-navbar.tsx create mode 100644 apps/platform/components/core/toc-context.tsx create mode 100644 apps/platform/components/mdx/component-preview.tsx create mode 100644 apps/platform/components/mdx/installation-cmd.tsx create mode 100644 apps/platform/components/mdx/manual-installation.tsx create mode 100644 apps/platform/components/nav-main.tsx create mode 100644 apps/platform/components/nav-projects.tsx create mode 100644 apps/platform/components/nav-secondary.tsx create mode 100644 apps/platform/components/nav-user.tsx create mode 100644 apps/platform/components/scroll-fade-effect/scroll-fade-effect.tsx create mode 100644 apps/platform/components/showcase/component-installation.tsx create mode 100644 apps/platform/components/showcase/docs-steps.tsx create mode 100644 apps/platform/components/ui/avatar.tsx create mode 100644 apps/platform/components/ui/breadcrumb.tsx create mode 100644 apps/platform/components/ui/dropdown-menu.tsx create mode 100644 apps/platform/components/ui/progressive-blur.tsx diff --git a/apps/platform/app/(docs)/cli/page.tsx b/apps/platform/app/(docs)/cli/page.tsx index aad537e..6c1c5ec 100644 --- a/apps/platform/app/(docs)/cli/page.tsx +++ b/apps/platform/app/(docs)/cli/page.tsx @@ -1,6 +1,12 @@ import { PageHeader } from "@/components/core/typography"; import { CodeBlock } from "@/components/showcase/code-block"; import MotionDiv from "@/components/core/motion-div"; +import { + DocSection, + OnThisPage, +} from "@/components/showcase/docs-primitives"; + +const toc = [{ id: "commands", title: "Commands" }]; export default function CliPage() { return ( @@ -11,17 +17,21 @@ export default function CliPage() { transition={{ duration: 0.3 }} className="mr-auto max-w-5xl space-y-8" > + + - - {`watermelon init + + + {`watermelon init watermelon add button watermelon add button text`} - + + ); } diff --git a/apps/platform/app/(docs)/components/[slug]/page.tsx b/apps/platform/app/(docs)/components/[...slug]/page.tsx similarity index 63% rename from apps/platform/app/(docs)/components/[slug]/page.tsx rename to apps/platform/app/(docs)/components/[...slug]/page.tsx index 51a1e6e..766baac 100644 --- a/apps/platform/app/(docs)/components/[slug]/page.tsx +++ b/apps/platform/app/(docs)/components/[...slug]/page.tsx @@ -7,6 +7,8 @@ import { OnThisPage, PreviewCard, } from "@/components/showcase/docs-primitives"; +import { ComponentPreview } from "@/components/mdx/component-preview"; +import { ComponentInstallation } from "@/components/showcase/component-installation"; import { getComponentDoc, getComponentDocPager, @@ -17,21 +19,23 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { ComponentVideoPreview } from "@/components/showcase/component-video-preview"; import MotionDiv from "@/components/core/motion-div"; export function generateStaticParams() { - return getComponentDocSlugs().map((slug) => ({ slug })); + return getComponentDocSlugs().map((slug) => ({ + slug: [slug], + })); } export async function generateMetadata({ params, }: { - params: Promise<{ slug: string }>; + params: Promise<{ slug: string[] }>; }): Promise { const { slug } = await params; + const componentSlug = slug[0]; try { - const { meta } = await getComponentDoc(slug); + const { meta } = await getComponentDoc(componentSlug); return { title: `${meta.title} | Watermelon RN`, description: meta.description, @@ -44,17 +48,18 @@ export async function generateMetadata({ export default async function ComponentPage({ params, }: { - params: Promise<{ slug: string }>; + params: Promise<{ slug: string[] }>; }) { const { slug } = await params; - const doc = await getComponentDoc(slug).catch(() => null); + const componentSlug = slug[0]; + const doc = await getComponentDoc(componentSlug).catch(() => null); if (!doc) { notFound(); } - const { Content, meta } = doc; - const pager = getComponentDocPager(slug); + const { Content, meta, component } = doc; + const pager = getComponentDocPager(componentSlug); return ( -
-
+ +
+

{meta.category}

-

{meta.title}

-

+

+ {meta.title} +

+

{meta.description}

@@ -87,8 +95,8 @@ export default async function ComponentPage({
-
-
+
+
-
-
+ + +
- - ); } diff --git a/apps/platform/app/(docs)/components/page.tsx b/apps/platform/app/(docs)/components/page.tsx index 7fd2633..5b5cfdd 100644 --- a/apps/platform/app/(docs)/components/page.tsx +++ b/apps/platform/app/(docs)/components/page.tsx @@ -1,5 +1,17 @@ import { ComponentsIndexView } from "@/components/showcase/component-docs"; +import { OnThisPage } from "@/components/showcase/docs-primitives"; + +const toc = [ + { id: "component-overview", title: "Overview" }, + { id: "buttons", title: "Buttons" }, + { id: "typography", title: "Typography" }, +]; export default function ComponentsPage() { - return ; + return ( + <> + + + + ); } diff --git a/apps/platform/app/(docs)/installation/page.tsx b/apps/platform/app/(docs)/installation/page.tsx index 5d256e2..1e85b87 100644 --- a/apps/platform/app/(docs)/installation/page.tsx +++ b/apps/platform/app/(docs)/installation/page.tsx @@ -1,6 +1,15 @@ import { PageHeader } from "@/components/core/typography"; import { CodeBlock } from "@/components/showcase/code-block"; import MotionDiv from "@/components/core/motion-div"; +import { + DocSection, + OnThisPage, +} from "@/components/showcase/docs-primitives"; + +const toc = [ + { id: "initialize", title: "Initialize" }, + { id: "config-file", title: "Config file" }, +]; export default function InstallationPage() { return ( @@ -11,19 +20,24 @@ export default function InstallationPage() { transition={{ duration: 0.3 }} className="mr-auto max-w-5xl space-y-8" > + + - - {`watermelon init + + + {`watermelon init watermelon add button text`} - + + - - {`{ + + + {`{ "style": "default", "tailwind": { "config": "tailwind.config.js", @@ -35,7 +49,8 @@ watermelon add button text`} "utils": "@/lib/utils" } }`} - + + ); } diff --git a/apps/platform/app/(docs)/introduction/page.tsx b/apps/platform/app/(docs)/introduction/page.tsx new file mode 100644 index 0000000..bc1e30e --- /dev/null +++ b/apps/platform/app/(docs)/introduction/page.tsx @@ -0,0 +1,141 @@ +import MotionDiv from "@/components/core/motion-div"; +import { PageHeader } from "@/components/core/typography"; +import { CodeBlock } from "@/components/showcase/code-block"; +import { + DocSection, + DocSubsection, + OnThisPage, +} from "@/components/showcase/docs-primitives"; + +const toc = [ + { id: "what-is-watermelon", title: "What is Watermelon?" }, + { id: "why-it-exists", title: "Why it exists" }, + { id: "workflow", title: "Typical workflow" }, + { id: "pick-components", title: "Pick components", depth: 3 }, + { id: "install-with-cli", title: "Install with CLI", depth: 3 }, + { id: "customize-output", title: "Customize output", depth: 3 }, + { id: "project-shape", title: "Project shape" }, + { id: "next-steps", title: "Next steps" }, +]; + +export default function IntroductionPage() { + return ( + + + + + + +

+ Watermelon is a React Native component registry and CLI. Instead of + shipping one giant UI dependency, it lets you pull installable + primitives like button and text directly + into your project. +

+

+ The result is a docs-and-registry workflow that feels familiar if you + have used shadcn on the web, but tuned for native app structure, + NativeWind styling, and Expo-friendly local development. +

+
+ + +
+
+

Own the code

+

+ Installed components live in your codebase, so refactors, + animations, and design tweaks stay under your control. +

+
+
+

Install incrementally

+

+ Start with a couple of primitives, then layer in more only when + the product actually needs them. +

+
+
+

Keep docs close

+

+ Every component has install commands, examples, previews, and API + notes in the same documentation flow. +

+
+
+

Stay native-first

+

+ The primitives are designed for React Native and Expo rather than + being thin ports of web-only assumptions. +

+
+
+
+ + + +

+ Browse the registry, open the docs for a primitive, and verify that + its API and visual behavior match the surface you are building. +

+
+ + +

+ Initialize your project once, then add primitives whenever you need + them. +

+ + {`watermelon init +watermelon add button text`} + +
+ + +

+ After installation, the files are yours. Adjust variants, spacing, + tokens, or composition patterns to fit your product instead of + waiting on a package release. +

+
+
+ + +

+ A typical setup keeps UI primitives in a local component directory, + uses a shared utility helper, and points Tailwind or NativeWind to the + right sources. +

+ + {`app/ +components/ + ui/ + button.tsx + text.tsx +lib/ + utils.ts +global.css +watermelon.json`} + +
+ + +

+ Start with the installation guide if you are setting up a fresh app, + or jump into the components catalog if you already want to pull in a + primitive and begin customizing it. +

+
+
+ ); +} diff --git a/apps/platform/app/(docs)/layout.tsx b/apps/platform/app/(docs)/layout.tsx index 713247f..9ff5a94 100644 --- a/apps/platform/app/(docs)/layout.tsx +++ b/apps/platform/app/(docs)/layout.tsx @@ -1,6 +1,9 @@ -import type { CSSProperties, ReactNode } from "react"; -import { DocsSidebar } from "@/components/core/docs-sidebar"; -import { SidebarProvider } from "@/components/ui/sidebar"; +import type { ReactNode } from "react"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/app-sidebar"; +import { Navbar } from "@/components/core/navbar"; +import { TOCProvider } from "@/components/core/toc-context"; +import { DocsTOC } from "@/components/core/docs-toc"; export default function ComponentsLayout({ children, @@ -8,23 +11,24 @@ export default function ComponentsLayout({ children: ReactNode; }) { return ( - -
-
- -
- -
-
{children}
-
-
-
+ + + + +
+ +
+
+ +
+
{children}
+
+
+
+ +
+
); } diff --git a/apps/platform/app/(docs)/registry/page.tsx b/apps/platform/app/(docs)/registry/page.tsx new file mode 100644 index 0000000..9b77ebd --- /dev/null +++ b/apps/platform/app/(docs)/registry/page.tsx @@ -0,0 +1,163 @@ +import MotionDiv from "@/components/core/motion-div"; +import { PageHeader } from "@/components/core/typography"; +import { CodeBlock } from "@/components/showcase/code-block"; +import { + DocSection, + OnThisPage, +} from "@/components/showcase/docs-primitives"; + +const toc = [ + { id: "overview", title: "Overview" }, + { id: "directory-entry", title: "Directory entry" }, + { id: "component-manifest", title: "Component manifest" }, + { id: "hosting-behavior", title: "Hosting behavior" }, + { id: "install-from-registry", title: "Install from registry" }, + { id: "pr-checklist", title: "PR checklist" }, +]; + +export default function RegistryPage() { + return ( + + + + + + +
+

+ The CLI failed earlier because the default registry URL was pointing + to a domain that did not exist yet. The fix is not just “host some + JSON somewhere”. A registry needs a stable manifest URL pattern, + file hosting that matches the manifest, and a scope entry we can + review and approve. +

+

+ If you want your website to work with Watermelon, publish a + registry on your own domain, make sure the component manifests + resolve correctly, and then submit a PR that adds your directory + entry to our approved registry list. +

+
+
+ + + + {`{ + "name": "@your-scope", + "homepage": "https://your-site.com", + "url": "https://your-site.com/r/{name}.json", + "description": "Short summary of your component registry." +}`} + + +
+

+ The name is the namespace users type in the CLI, for + example watermelon add @your-scope/card. The{" "} + url field is a template. Watermelon replaces{" "} + {"{name}"} with the requested component name. +

+

+ If your registry does not use a template and instead exposes a flat + base URL, you can still host manifests at /button.json + , /card.json, and so on. The shadcn-style template + above is the preferred format because it is more explicit and + easier to review. +

+
+
+ + + + {`{ + "name": "button", + "dependencies": ["clsx", "tailwind-merge"], + "registryDependencies": ["text"], + "files": [ + { + "path": "components/ui/button.tsx", + "url": "https://your-site.com/files/components/ui/button.tsx" + } + ] +}`} + + +
+

+ Each manifest must include a valid name and at least + one files entry. dependencies are npm + packages the CLI installs automatically.{" "} + registryDependencies are other registry components + that should be installed first. +

+

+ Each file needs a target path. You can also provide an + explicit url. If you omit the file URL, Watermelon + falls back to the conventional /files/<path>{" "} + pattern. +

+
+
+ + + + {`GET https://your-site.com/r/button.json +GET https://your-site.com/r/card.json +GET https://your-site.com/files/components/ui/button.tsx +GET https://your-site.com/files/components/ui/card.tsx`} + + +
+

+ Your hosted responses should be public, stable, and return valid + JSON or file contents without requiring a browser session. Avoid + URLs that depend on temporary tokens, client-side rendering, or + HTML wrappers. +

+
+
+ + + + {`watermelon add @your-scope/button +watermelon add @your-scope/card --dry-run +watermelon add @your-scope/button @your-scope/text`} + + + + + + {`- Host your manifests on a public domain you control. +- Serve a directory entry with a stable \`url\` template. +- Make every component manifest return valid JSON. +- Make every file URL downloadable directly. +- Include clear descriptions and ownership details. +- Open a PR adding your registry entry to the approved directory. +- We test the URLs, review the output, and approve if everything resolves correctly.`} + + +
+ ); +} diff --git a/apps/platform/app/dashboard/page.tsx b/apps/platform/app/dashboard/page.tsx new file mode 100644 index 0000000..a1f879f --- /dev/null +++ b/apps/platform/app/dashboard/page.tsx @@ -0,0 +1,55 @@ +import { AppSidebar } from "@/components/app-sidebar" +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" + +export default function Page() { + return ( + + + +
+
+ + + + + + + Build Your Application + + + + + Data Fetching + + + +
+
+
+
+
+
+
+
+
+
+ + + ) +} diff --git a/apps/platform/app/globals.css b/apps/platform/app/globals.css index d45eb9a..30cdafe 100644 --- a/apps/platform/app/globals.css +++ b/apps/platform/app/globals.css @@ -7,6 +7,7 @@ --color-foreground: var(--foreground); --font-sans: var(--font-sans); --font-mono: var(--font-geist-mono); + --shadow-3d: inset 0 5px 6px var(--color-border); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -124,4 +125,104 @@ html { @apply font-sans; } + +@keyframes show-top-mask { + to { + --top-mask-height: var(--mask-height); + } + } + +@keyframes hide-bottom-mask { + to { + --bottom-mask-height: 0px; + } + } + +@keyframes show-left-mask { + to { + --left-mask-width: var(--mask-width); + } + } + +@keyframes hide-right-mask { + to { + --right-mask-width: 0px; + } + } +} + +@property --top-mask-height { + syntax: ""; + inherits: true; + initial-value: 0px; +} + +@property --bottom-mask-height { + syntax: ""; + inherits: true; + initial-value: 64px; +} + +@property --left-mask-width { + syntax: ""; + inherits: true; + initial-value: 0px; } + +@property --right-mask-width { + syntax: ""; + inherits: true; + initial-value: 64px; +} + +@utility scroll-fade-effect-y { + --mask-height: 64px; + --mask-offset-top: 0px; + --mask-offset-bottom: 0px; + --scroll-buffer: 2rem; + mask-image: linear-gradient(to top, transparent, black 90%), linear-gradient(to bottom, transparent 0%, black 100%), linear-gradient(black, black); + mask-size: 100% var(--top-mask-height), 100% var(--bottom-mask-height), 100% 100%; + mask-repeat: no-repeat, no-repeat, no-repeat; + mask-position: 0 var(--mask-offset-top), 0 calc(100% - var(--mask-offset-bottom)), 0 0; + mask-composite: exclude; + animation-name: show-top-mask, hide-bottom-mask; + animation-timeline: scroll(self), scroll(self); + animation-range: 0 var(--scroll-buffer), calc(100% - var(--scroll-buffer)) 100%; + animation-fill-mode: both; +} + +@utility scroll-fade-effect-x { + --mask-width: 64px; + --mask-offset-left: 0px; + --mask-offset-right: 0px; + --scroll-buffer: 2rem; + mask-image: linear-gradient(to left, transparent, black 90%), linear-gradient(to right, transparent 0%, black 100%), linear-gradient(black, black); + mask-size: var(--left-mask-width) 100%, var(--right-mask-width) 100%, 100% 100%; + mask-repeat: no-repeat, no-repeat, no-repeat; + mask-position: var(--mask-offset-left) 0, calc(100% - var(--mask-offset-right)) 0, 0 0; + mask-composite: exclude; + animation-name: show-left-mask, hide-right-mask; + animation-timeline: scroll(self inline), scroll(self inline); + animation-range: 0 var(--scroll-buffer), calc(100% - var(--scroll-buffer)) 100%; + animation-fill-mode: both; +} + +/* Hide scrollbar globally */ +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + +/* Apply to all scrollable elements */ +* { + scrollbar-width: none; + -ms-overflow-style: none; +} + +*::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/apps/platform/app/layout.tsx b/apps/platform/app/layout.tsx index 11f8f28..3b2085d 100644 --- a/apps/platform/app/layout.tsx +++ b/apps/platform/app/layout.tsx @@ -3,7 +3,6 @@ import { Geist_Mono, Newsreader, Roboto } from "next/font/google"; import "./globals.css"; import { cn } from "@/lib/utils"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { Navbar } from "@/components/core/navbar"; import { ThemeProvider } from "next-themes"; const roboto = Roboto({ @@ -48,7 +47,6 @@ export default function RootLayout({ enableSystem > -
{children}
diff --git a/apps/platform/app/page.tsx b/apps/platform/app/page.tsx index 94d18ff..d2e284c 100644 --- a/apps/platform/app/page.tsx +++ b/apps/platform/app/page.tsx @@ -1,6 +1,7 @@ import { getRegistryCatalog } from "@/lib/registry-catalog"; import Link from "next/link"; import { Button } from "@/components/ui/button"; +import { LandingNavbar } from "@/components/core/landing-navbar"; export default async function Home() { const categories = await getRegistryCatalog(); @@ -10,33 +11,84 @@ export default async function Home() { ); return ( -
-
-
-

- React Native Registry -

-

- Watermelon RN -

-

- A minimal registry for installable React Native components. -

-
- - -
-
- -
-

{count} components

-

CLI installation

-
-
-
+ <> +
+ + +
+
+
+

+ React Native Registry +

+

+ Watermelon RN +

+

+ A minimal registry for installable React Native components. +

+
+ + +
+
+ +
+

{count} components

+

CLI installation

+
+
+ +
+
+

+ Why Watermelon +

+

+ Copy the component, own the code, ship faster. +

+
+ +
+
+

Registry-first workflow

+

+ Install only what you need and keep the generated files inside + your app. +

+
+ +
+

Native-first primitives

+

+ Build React Native interfaces with components designed for + real mobile product surfaces. +

+
+ +
+

Docs + CLI together

+

+ Browse examples, install commands, and API details in the same + place. +

+
+ +
+

Composable by default

+

+ Start with a tiny base and adapt each primitive to your design + system over time. +

+
+
+
+
+
+ ); } diff --git a/apps/platform/components.json b/apps/platform/components.json index 54acf35..bb90b58 100644 --- a/apps/platform/components.json +++ b/apps/platform/components.json @@ -23,6 +23,7 @@ }, "registries": { "@magicui": "https://magicui.design/r/{name}", - "@animate-ui": "https://animate-ui.com/r/{name}.json" + "@animate-ui": "https://animate-ui.com/r/{name}.json", + "@ncdai": "https://chanhdai.com/r/{name}.json" } } diff --git a/apps/platform/components/animate-ui/components/buttons/copy.tsx b/apps/platform/components/animate-ui/components/buttons/copy.tsx index b9728c8..e5a8c34 100644 --- a/apps/platform/components/animate-ui/components/buttons/copy.tsx +++ b/apps/platform/components/animate-ui/components/buttons/copy.tsx @@ -1,16 +1,16 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { AnimatePresence, motion } from 'motion/react'; -import { CheckIcon, CopyIcon } from 'lucide-react'; +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { AnimatePresence, motion } from "motion/react"; +import { CheckIcon, CopyIcon } from "lucide-react"; import { Button as ButtonPrimitive, type ButtonProps as ButtonPrimitiveProps, -} from '@/components/animate-ui/primitives/buttons/button'; -import { cn } from '@/lib/utils'; -import { useControlledState } from '@/hooks/use-controlled-state'; +} from "@/components/animate-ui/primitives/buttons/button"; +import { cn } from "@/lib/utils"; +import { useControlledState } from "@/hooks/use-controlled-state"; const buttonVariants = cva( "flex items-center justify-center rounded-md transition-[box-shadow,_color,_background-color,_border-color,_outline-color,_text-decoration-color,_fill,_stroke] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -18,33 +18,33 @@ const buttonVariants = cva( variants: { variant: { default: - 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', - accent: 'bg-accent text-accent-foreground shadow-xs hover:bg-accent/90', + "bg-primary border shadow-[inset_0px_3px_3px_var(--color-neutral-300)] dark:shadow-[inset_0px_3px_8px_var(--color-neutral-950)] border-neutral-300 dark:border-neutral-700 text-primary-foreground hover:bg-primary/90", + accent: "bg-accent text-accent-foreground hover:bg-accent/90", destructive: - 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + "bg-background hover:bg-accent border shadow-[inset_0px_3px_3px_var(--color-neutral-300)] dark:shadow-[inset_0px_3px_8px_var(--color-neutral-950)] border-neutral-300 dark:border-neutral-700 hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: - 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + "bg-secondary border shadow-[inset_0px_3px_3px_var(--color-neutral-300)] dark:shadow-[inset_0px_3px_8px_var(--color-neutral-950)] border-neutral-300 dark:border-neutral-700 text-secondary-foreground hover:bg-secondary/80", ghost: - 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', - link: 'text-primary underline-offset-4 hover:underline', + "border-transparent bg-transparent shadow-none hover:bg-accent hover:text-accent-foreground active:translate-y-0 active:shadow-none dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: 'size-9', + default: "size-9", xs: "size-7 [&_svg:not([class*='size-'])]:size-3.5 rounded-md", - sm: 'size-8 rounded-md', - lg: 'size-10 rounded-md', + sm: "size-8 rounded-md", + lg: "size-10 rounded-md", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, }, ); -type CopyButtonProps = Omit & +type CopyButtonProps = Omit & VariantProps & { content: string; copied?: boolean; @@ -85,7 +85,7 @@ function CopyButton({ }, delay); }) .catch((error) => { - console.error('Error copying command', error); + console.error("Error copying command", error); }); } }, @@ -104,11 +104,11 @@ function CopyButton({ > diff --git a/apps/platform/components/app-sidebar.tsx b/apps/platform/components/app-sidebar.tsx new file mode 100644 index 0000000..90c81db --- /dev/null +++ b/apps/platform/components/app-sidebar.tsx @@ -0,0 +1,110 @@ +"use client"; + +import * as React from "react"; + +import { NavMain } from "@/components/nav-main"; +import { NavSecondary } from "@/components/nav-secondary"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { + ComputerTerminalIcon, + BookOpen02Icon, + ChartRingIcon, + SentIcon, +} from "@hugeicons/core-free-icons"; +import { getComponentGroups } from "@/lib/component-index"; +import { Logo } from "./core/logo"; +import { Socials } from "./core/socials"; + +const data = { + navMain: [ + { + title: "Getting Started", + url: "#", + icon: , + isActive: true, + items: [ + { + title: "Introduction", + url: "/introduction", + }, + { + title: "Installation", + url: "/installation", + }, + { + title: "CLI", + url: "/cli", + }, + { + title: "Registry", + url: "/registry", + }, + ], + }, + ], + navSecondary: [ + { + title: "Support", + url: "#", + icon: , + }, + { + title: "Feedback", + url: "#", + icon: , + }, + ], +}; + +export function AppSidebar({ ...props }: React.ComponentProps) { + const componentGroups = getComponentGroups(); + + const formattedComponentGroups = componentGroups.map((group) => ({ + title: group.title, + url: "#", + icon: , + items: group.items.map((item) => ({ + title: item.title, + url: `/components/${item.slug}`, + })), + })); + + return ( + + + + +
+ + + + +
+
+
+
+ + + + + + + + +
+ ); +} diff --git a/apps/platform/components/core/3d-container.tsx b/apps/platform/components/core/3d-container.tsx new file mode 100644 index 0000000..a623df9 --- /dev/null +++ b/apps/platform/components/core/3d-container.tsx @@ -0,0 +1,44 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; + +type GlassContainerProps = React.HTMLAttributes & { + variant?: "default" | "soft" | "strong"; +}; + +export function GlassContainer({ + className, + variant = "strong", + children, + ...props +}: GlassContainerProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/platform/components/core/docs-sidebar.tsx b/apps/platform/components/core/docs-sidebar.tsx index d163731..e35f9a9 100644 --- a/apps/platform/components/core/docs-sidebar.tsx +++ b/apps/platform/components/core/docs-sidebar.tsx @@ -22,28 +22,22 @@ export function DocsSidebar({ const componentGroups = getComponentGroups(); return ( - - - - + + +
+ + + Getting Started - + {DOC_SECTIONS.map(({ name, href }) => ( {name} @@ -54,18 +48,18 @@ export function DocsSidebar({ {componentGroups.map((group) => ( - - + + {group.title} - + {group.items.map((component) => ( {component.title} diff --git a/apps/platform/components/core/docs-toc.tsx b/apps/platform/components/core/docs-toc.tsx new file mode 100644 index 0000000..afac36a --- /dev/null +++ b/apps/platform/components/core/docs-toc.tsx @@ -0,0 +1,339 @@ +"use client"; + +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { AnimatePresence, motion } from "motion/react"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { ArrowUp01Icon, Menu01Icon } from "@hugeicons/core-free-icons"; +import { cn } from "@/lib/utils"; +import { useTOC } from "./toc-context"; +import type { TocItem } from "./toc-context"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; + +type ItemPosition = { + top: number; + depth: number; +}; + +type ActivePosition = { + x: number; + y: number; +}; + +function getX(depth?: number) { + const level = depth === 3 ? 1 : 0; + return level * 16 + 4; +} + +function getScrollContainer(element?: HTMLElement | null) { + return ( + element?.closest('[data-slot="sidebar-inset"]') ?? + document.querySelector('[data-slot="sidebar-inset"]') + ); +} + +function DocsTOCNav({ + items, + activeId, + itemPositions, + activePos, + containerRef, + compact = false, + onItemSelect, +}: { + items: TocItem[]; + activeId: string | null; + itemPositions: Record; + activePos: ActivePosition | null; + containerRef: React.RefObject; + compact?: boolean; + onItemSelect?: () => void; +}) { + const svgPath = useMemo(() => { + if (items.length < 2) return ""; + + let path = ""; + + items.forEach((item, index) => { + const pos = itemPositions[item.id]; + if (!pos) return; + + const x = getX(item.depth); + const y = pos.top + 0.5; + + if (index === 0) { + path += `M ${x} ${y}`; + return; + } + + const prev = items[index - 1]; + const prevPos = itemPositions[prev.id]; + if (!prevPos) return; + + const prevX = getX(prev.depth); + const prevY = prevPos.top + 0.5; + + if (x === prevX) { + path += ` L ${x} ${y}`; + return; + } + + const midY = prevY + (y - prevY) / 2; + path += ` L ${prevX} ${midY - 4} L ${x} ${midY + 4} L ${x} ${y}`; + }); + + return path; + }, [items, itemPositions]); + + return ( +
+ + + + + + {activePos ? ( + +
+ + ) : null} + + + +
+ ); +} + +export function DocsTOC({ mobile = false }: { mobile?: boolean }) { + const { items, activeId, setActiveId } = useTOC(); + const observer = useRef(null); + const containerRef = useRef(null); + + const [itemPositions, setItemPositions] = useState< + Record + >({}); + const [open, setOpen] = useState(false); + + useEffect(() => { + const headings = items + .map((item) => document.getElementById(item.id)) + .filter(Boolean) as HTMLElement[]; + const scrollContainer = getScrollContainer(headings[0]); + + if (observer.current) observer.current.disconnect(); + + observer.current = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort( + (left, right) => + left.boundingClientRect.top - right.boundingClientRect.top, + ); + + if (visible.length > 0) { + setActiveId(visible[0].target.id); + } + }, + { + root: scrollContainer, + rootMargin: "-80px 0px -60% 0px", + threshold: [0, 1], + }, + ); + + headings.forEach((heading) => observer.current?.observe(heading)); + + return () => observer.current?.disconnect(); + }, [items, setActiveId]); + + useEffect(() => { + const scrollContainer = getScrollContainer(containerRef.current); + + const update = () => { + if (!containerRef.current) return; + + const positions: Record = {}; + const links = containerRef.current.querySelectorAll("a[data-toc-id]"); + + links.forEach((link) => { + const id = link.getAttribute("data-toc-id"); + if (!id) return; + + const depth = Number.parseInt( + link.getAttribute("data-depth") || "1", + 10, + ); + const rect = link.getBoundingClientRect(); + const containerRect = containerRef.current!.getBoundingClientRect(); + + positions[id] = { + top: rect.top - containerRect.top + rect.height / 2, + depth, + }; + }); + + setItemPositions(positions); + }; + + update(); + scrollContainer?.addEventListener("scroll", update, { passive: true }); + window.addEventListener("resize", update); + + return () => { + scrollContainer?.removeEventListener("scroll", update); + window.removeEventListener("resize", update); + }; + }, [items]); + + const activePos = useMemo(() => { + if (!activeId || !itemPositions[activeId]) return null; + const pos = itemPositions[activeId]; + + return { + x: getX(pos.depth), + y: pos.top, + }; + }, [activeId, itemPositions]); + + if (items.length === 0) return null; + + if (mobile) { + const activeItem = items.find((item) => item.id === activeId) ?? items[0]; + + return ( + + +
+ +
+

+ {activeItem?.title} +

+

+ On this page +

+
+
+ +
+ + +
+ setOpen(false)} + /> +
+
+
+ ); + } + + return ( +
+
+ + + On this page + +
+ +
+ ); +} diff --git a/apps/platform/components/core/landing-navbar.tsx b/apps/platform/components/core/landing-navbar.tsx new file mode 100644 index 0000000..ca35da8 --- /dev/null +++ b/apps/platform/components/core/landing-navbar.tsx @@ -0,0 +1,58 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; +import { Logo } from "./logo"; +import ThemeToggle from "./theme-toggle"; +import { ProgressiveBlur } from "../ui/progressive-blur"; + +const navLinks = [ + { label: "Components", href: "/components" }, + { label: "Installation", href: "/installation" }, + { label: "CLI", href: "/cli" }, + { label: "Registry", href: "/registry" }, +]; + +export function LandingNavbar() { + const pathname = usePathname(); + + return ( +
+ + + +
+ ); +} diff --git a/apps/platform/components/core/logo.tsx b/apps/platform/components/core/logo.tsx index a9d8e71..63fe565 100644 --- a/apps/platform/components/core/logo.tsx +++ b/apps/platform/components/core/logo.tsx @@ -3,11 +3,14 @@ import Link from "next/link"; export const Logo = () => { return ( - +
Logo
- + Watermelon RN diff --git a/apps/platform/components/core/navbar.tsx b/apps/platform/components/core/navbar.tsx index b2e78cc..89fefdd 100644 --- a/apps/platform/components/core/navbar.tsx +++ b/apps/platform/components/core/navbar.tsx @@ -3,101 +3,83 @@ import * as React from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { Menu01Icon, Cancel01Icon } from "@hugeicons/core-free-icons"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { CommandMenu } from "./command-pallete"; import { Logo } from "./logo"; import ThemeToggle from "./theme-toggle"; +import { ProgressiveBlur } from "../ui/progressive-blur"; +import { SidebarTrigger, useSidebar } from "@/components/ui/sidebar"; +import { useIsMobile } from "@/hooks/use-mobile"; const navLinks = [ { label: "Components", href: "/components" }, { label: "Installation", href: "/installation" }, { label: "CLI", href: "/cli" }, + { label: "Registry", href: "/registry" }, ]; // ─── Navbar ────────────────────────────────────────────────────────────────── export function Navbar() { const pathname = usePathname(); - const [mobileOpen, setMobileOpen] = React.useState(false); + const { state } = useSidebar(); + const isOpen = state === "expanded"; + const isMobile = useIsMobile(); return ( -
-
- {/* Logo */} - +
+ {/* Progressive blur effect - fades from top (blurry) to bottom (clear) */} + - {/* Desktop nav links */} - + {/* Navbar content */} +
); } diff --git a/apps/platform/components/core/toc-context.tsx b/apps/platform/components/core/toc-context.tsx new file mode 100644 index 0000000..4d45228 --- /dev/null +++ b/apps/platform/components/core/toc-context.tsx @@ -0,0 +1,41 @@ +"use client"; + +import React, { createContext, useContext, useState, useCallback } from "react"; + +export type TocItem = { + id: string; + title: string; + depth?: number; +}; + +type TOCContextType = { + items: TocItem[]; + setItems: (items: TocItem[]) => void; + activeId: string | null; + setActiveId: (id: string | null) => void; +}; + +const TOCContext = createContext(undefined); + +export function TOCProvider({ children }: { children: React.ReactNode }) { + const [items, setItemsState] = useState([]); + const [activeId, setActiveId] = useState(null); + + const setItems = useCallback((newItems: TocItem[]) => { + setItemsState(newItems); + }, []); + + return ( + + {children} + + ); +} + +export function useTOC() { + const context = useContext(TOCContext); + if (!context) { + throw new Error("useTOC must be used within a TOCProvider"); + } + return context; +} diff --git a/apps/platform/components/mdx/component-preview.tsx b/apps/platform/components/mdx/component-preview.tsx new file mode 100644 index 0000000..d376f1f --- /dev/null +++ b/apps/platform/components/mdx/component-preview.tsx @@ -0,0 +1,120 @@ +"use client"; + +import React, { useState } from "react"; +import { cn } from "@/lib/utils"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { ViewIcon, FileCodeCornerIcon } from "@hugeicons/core-free-icons"; +import { CodeBlock } from "../showcase/code-block"; +import { GlassContainer } from "../core/3d-container"; + +interface ComponentPreviewProps { + children?: React.ReactNode; + code?: string; + className?: string; + video?: string; + poster?: string; +} + +export function ComponentPreview({ + children, + code, + className, + video, + poster, +}: ComponentPreviewProps) { + const [activeTab, setActiveTab] = useState<"preview" | "code">("preview"); + + return ( + +
+ {/* Header with tabs and actions */} +
+
+ {/* Preview Tab */} + + + {/* Code Tab */} + {code && ( + + )} +
+
+ + {/* Content */} +
+ {/* Preview Panel */} + {activeTab === "preview" && ( +
+
+ +
+ Loading... +
+ } + > + {video ? ( +
+
+ ) : ( + children + )} +
+
+
+ )} + + {/* Code Panel */} + {activeTab === "code" && code && ( +
+ {code} +
+ )} +
+
+
+ ); +} + +// Export a simpler version for MDX usage +export function CompPreview(props: ComponentPreviewProps) { + return ; +} diff --git a/apps/platform/components/mdx/installation-cmd.tsx b/apps/platform/components/mdx/installation-cmd.tsx new file mode 100644 index 0000000..10d8bf8 --- /dev/null +++ b/apps/platform/components/mdx/installation-cmd.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { CopyButton } from "../animate-ui/components/buttons/copy"; +import { AnimatePresence, motion } from "motion/react"; +import { ScrollFadeEffect } from "../scroll-fade-effect/scroll-fade-effect"; +import { GlassContainer } from "../core/3d-container"; +type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; +const PM_LIST = ["npm", "pnpm", "yarn", "bun"] as PackageManager[]; + +type InstallTrackingContext = { + component_slug?: string; + component_name?: string; + category?: string; + source?: string; +}; + +export interface InstallationItem { + install: string[]; + slug: string; + name: string; + category: string; +} + +export const InstallationCmd = ({ + activePackageManager, + setActivePackageManager, + item, + trackingContext, +}: { + activePackageManager: PackageManager; + setActivePackageManager: (pm: PackageManager) => void; + item: InstallationItem; + trackingContext?: InstallTrackingContext; +}) => { + void trackingContext; + + const getInstallCommand = (pm: PackageManager, baseCommand: string) => { + // Check if it's a shadcn add command + if ( + baseCommand.startsWith("npx shadcn") || + baseCommand.includes("shadcn@latest add") + ) { + const parts = baseCommand.split(" "); + const componentName = parts[parts.length - 1]; + switch (pm) { + case "npm": + return `npx shadcn@latest add ${componentName}`; + case "yarn": + return `npx shadcn@latest add ${componentName}`; + case "pnpm": + return `pnpm dlx shadcn@latest add ${componentName}`; + case "bun": + return `bunx --bun shadcn@latest add ${componentName}`; + default: + return baseCommand; + } + } + // For npm install commands + if ( + baseCommand.startsWith("npm install") || + baseCommand.startsWith("npm i ") + ) { + const packages = baseCommand.replace(/^npm (install|i) /, ""); + switch (pm) { + case "npm": + return `npm install ${packages}`; + case "yarn": + return `yarn add ${packages}`; + case "pnpm": + return `pnpm add ${packages}`; + case "bun": + return `bun add ${packages}`; + default: + return baseCommand; + } + } + return baseCommand; + }; + + return ( +
+ {/* Package Manager Tabs */} +
+ +
+ {PM_LIST.map((pm) => { + const isActive = activePackageManager === pm; + + return ( + + ); + })} +
+
+
+ + {/* Install Commands */} +
+ {item.install.map((cmd: string, idx: number) => { + const command = getInstallCommand(activePackageManager, cmd); + + return ( +
+ +
+ + + + {command} + + + +
+
+ + +
+ ); + })} +
+
+ ); +}; diff --git a/apps/platform/components/mdx/manual-installation.tsx b/apps/platform/components/mdx/manual-installation.tsx new file mode 100644 index 0000000..57c3a73 --- /dev/null +++ b/apps/platform/components/mdx/manual-installation.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { AnimatePresence, motion } from "motion/react"; +import { CopyButton } from "../animate-ui/components/buttons/copy"; +import { ScrollFadeEffect } from "../scroll-fade-effect/scroll-fade-effect"; +import { GlassContainer } from "../core/3d-container"; + +type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; +const PM_LIST = ["npm", "pnpm", "yarn", "bun"] as PackageManager[]; + +type InstallTrackingContext = { + component_slug?: string; + component_name?: string; + category?: string; + source?: string; +}; + +export function ManualInstallationCmd({ + activePackageManager, + setActivePackageManager, + dependencies, + trackingContext, +}: { + activePackageManager: PackageManager; + setActivePackageManager: (pm: PackageManager) => void; + dependencies?: string[]; + trackingContext?: InstallTrackingContext; +}) { + if (!dependencies || dependencies.length === 0) return null; + void trackingContext; + + const getCommand = (pm: PackageManager) => { + const pkgs = dependencies.join(" "); + switch (pm) { + case "npm": + return `npm install ${pkgs}`; + case "yarn": + return `yarn add ${pkgs}`; + case "pnpm": + return `pnpm add ${pkgs}`; + case "bun": + return `bun add ${pkgs}`; + } + }; + + const command = getCommand(activePackageManager); + + return ( +
+ {/* PM Switcher */} +
+ +
+ {PM_LIST.map((pm) => { + const isActive = pm === activePackageManager; + + return ( + + ); + })} +
+
+
+ + {/* Command */} +
+ +
+ + + + {command} + + + +
+
+ + { + if (!command) return; + }} + className="absolute top-2 right-2" + /> +
+
+ ); +} diff --git a/apps/platform/components/nav-main.tsx b/apps/platform/components/nav-main.tsx new file mode 100644 index 0000000..8808e61 --- /dev/null +++ b/apps/platform/components/nav-main.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@/components/ui/sidebar"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { ArrowRight01Icon } from "@hugeicons/core-free-icons"; + +export function NavMain({ + items, + label = "Platform", + expandAll = false, +}: { + items: { + title: string; + url: string; + icon: React.ReactNode; + isActive?: boolean; + items?: { + title: string; + url: string; + }[]; + }[]; + label?: string; + expandAll?: boolean; +}) { + return ( + + {label} + + {items.map((item) => ( + + + + + {item.icon} + {item.title} + + + {item.items?.length ? ( + <> + + + + Toggle + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + ) : null} + + + ))} + + + ); +} diff --git a/apps/platform/components/nav-projects.tsx b/apps/platform/components/nav-projects.tsx new file mode 100644 index 0000000..8d6db72 --- /dev/null +++ b/apps/platform/components/nav-projects.tsx @@ -0,0 +1,86 @@ +"use client" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" +import { HugeiconsIcon } from "@hugeicons/react" +import { MoreHorizontalCircle01Icon, FolderIcon, Share03Icon, Delete02Icon } from "@hugeicons/core-free-icons" + +export function NavProjects({ + projects, +}: { + projects: { + name: string + url: string + icon: React.ReactNode + }[] +}) { + const { isMobile } = useSidebar() + + return ( + + Projects + + {projects.map((item) => ( + + + + {item.icon} + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + ) +} diff --git a/apps/platform/components/nav-secondary.tsx b/apps/platform/components/nav-secondary.tsx new file mode 100644 index 0000000..3afbe30 --- /dev/null +++ b/apps/platform/components/nav-secondary.tsx @@ -0,0 +1,41 @@ +"use client" + +import * as React from "react" + +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +export function NavSecondary({ + items, + ...props +}: { + items: { + title: string + url: string + icon: React.ReactNode + }[] +} & React.ComponentPropsWithoutRef) { + return ( + + + + {items.map((item) => ( + + + + {item.icon} + {item.title} + + + + ))} + + + + ) +} diff --git a/apps/platform/components/nav-user.tsx b/apps/platform/components/nav-user.tsx new file mode 100644 index 0000000..02ba547 --- /dev/null +++ b/apps/platform/components/nav-user.tsx @@ -0,0 +1,107 @@ +"use client" + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" +import { HugeiconsIcon } from "@hugeicons/react" +import { UnfoldMoreIcon, SparklesIcon, CheckmarkBadgeIcon, CreditCardIcon, NotificationIcon, LogoutIcon } from "@hugeicons/core-free-icons" + +export function NavUser({ + user, +}: { + user: { + name: string + email: string + avatar: string + } +}) { + const { isMobile } = useSidebar() + + return ( + + + + + + + + CN + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + CN + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ) +} diff --git a/apps/platform/components/scroll-fade-effect/scroll-fade-effect.tsx b/apps/platform/components/scroll-fade-effect/scroll-fade-effect.tsx new file mode 100644 index 0000000..ad4d16e --- /dev/null +++ b/apps/platform/components/scroll-fade-effect/scroll-fade-effect.tsx @@ -0,0 +1,29 @@ +import type { ComponentProps } from "react" + +import { cn } from "@/lib/utils" + +export type ScrollFadeEffectProps = ComponentProps<"div"> & { + /** + * Scroll direction to apply the fade effect. + * @defaultValue "vertical" + * */ + orientation?: "horizontal" | "vertical" +} + +export function ScrollFadeEffect({ + className, + orientation = "vertical", + ...props +}: ScrollFadeEffectProps) { + return ( +
+ ) +} diff --git a/apps/platform/components/showcase/code-block.tsx b/apps/platform/components/showcase/code-block.tsx index 636b5e7..db56025 100644 --- a/apps/platform/components/showcase/code-block.tsx +++ b/apps/platform/components/showcase/code-block.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import { useTheme } from "next-themes"; import { cn } from "@/lib/utils"; import { CopyButton } from "../animate-ui/components/buttons/copy"; +import { GlassContainer } from "../core/3d-container"; type HighlighterProps = Record & { children?: string; @@ -140,53 +141,61 @@ export function CodeBlock({ }, []); return ( -
-
-

- {title || language} -

- - +
+
+

+ {title || language} +

+ + +
+ + {syntax ? ( + + {code} + + ) : ( +
+            
+              {code}
+            
+          
+ )}
- - {syntax ? ( - - {code} - - ) : ( -
-          {code}
-        
- )} -
+ ); } diff --git a/apps/platform/components/showcase/component-docs.tsx b/apps/platform/components/showcase/component-docs.tsx index f4b96f2..6c685aa 100644 --- a/apps/platform/components/showcase/component-docs.tsx +++ b/apps/platform/components/showcase/component-docs.tsx @@ -9,14 +9,20 @@ export async function ComponentsIndexView() { return (
- +
+ +
{categories.map((category) => ( -
+

{category.title} diff --git a/apps/platform/components/showcase/component-installation.tsx b/apps/platform/components/showcase/component-installation.tsx new file mode 100644 index 0000000..b990a62 --- /dev/null +++ b/apps/platform/components/showcase/component-installation.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useState } from "react"; +import { + InstallationCmd, + type InstallationItem, +} from "@/components/mdx/installation-cmd"; +import { ManualInstallationCmd } from "@/components/mdx/manual-installation"; +import { CodeBlock } from "@/components/showcase/code-block"; +import { DocsStep, DocsSteps } from "@/components/showcase/docs-steps"; + +type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; + +interface ComponentInstallationProps { + item: InstallationItem; + dependencies?: string[]; +} + +export function ComponentInstallation({ + item, + dependencies, +}: ComponentInstallationProps) { + const [activePackageManager, setActivePackageManager] = + useState("npm"); + const importSnippet = `import { ${item.name} } from "@/components/ui/${item.slug}";`; + const steps = [ + { + title: "Install the component", + description: ( +

+ Run the registry command below to add {item.slug} to + your project. +

+ ), + content: ( + + ), + }, + ...(dependencies?.length + ? [ + { + title: "Install manual dependencies", + description: ( +

+ If you are wiring the component manually, install the package + dependencies shown below. +

+ ), + content: ( + + ), + }, + ] + : []), + { + title: "Import the component", + description: ( +

+ Import {item.name} from your local UI registry output. +

+ ), + content: ( + + {importSnippet} + + ), + }, + ]; + + return ( +
+
+

Installation

+

+ Install the registry item directly, then add any package dependencies + if you are setting the component up manually. +

+
+ + + {steps.map((step, index) => ( + + {step.content} + + ))} + +
+ ); +} diff --git a/apps/platform/components/showcase/docs-primitives.tsx b/apps/platform/components/showcase/docs-primitives.tsx index 0fdb3f1..74f161c 100644 --- a/apps/platform/components/showcase/docs-primitives.tsx +++ b/apps/platform/components/showcase/docs-primitives.tsx @@ -1,10 +1,16 @@ +"use client"; + import Link from "next/link"; import { QRCodeSVG } from "qrcode.react"; import { cn } from "@/lib/utils"; +import { useEffect } from "react"; +import { useTOC } from "@/components/core/toc-context"; +import { GlassContainer } from "@/components/core/3d-container"; export type TocItem = { id: string; title: string; + depth?: number; }; export function DocSection({ @@ -19,7 +25,7 @@ export function DocSection({ className?: string; }) { return ( -
+

{title}

{children}
@@ -38,7 +44,7 @@ export function DocSubsection({ className?: string; }) { return ( -
+

{title}

{children}
@@ -56,60 +62,52 @@ export function ApiTable({ }>; }) { return ( -
- - - - - - - - - - - {rows.map((row) => ( - - - - - + +
+
proptypedefaultdescription
- - {row.prop} - - - {row.type} - - {row.default || "-"} - - {row.description} -
+ + + + + + - ))} - -
proptypedefaultdescription
-
+ + + {rows.map((row) => ( + + + + {row.prop} + + + + {row.type} + + + {row.default || "-"} + + + {row.description} + + + ))} + + +
+ ); } export function OnThisPage({ items }: { items: TocItem[] }) { - return ( -
-

- On this page -

- -
- ); + const { setItems } = useTOC(); + + useEffect(() => { + setItems(items); + return () => setItems([]); + }, [items, setItems]); + + return null; } export function PreviewCard({ @@ -174,24 +172,28 @@ export function DocsPager({ return (
{previous ? ( - -

Previous

-

{previous.title}

- + + +

Previous

+

{previous.title}

+ +
) : (
)} {next ? ( - -

Next

-

{next.title}

- + + +

Next

+

{next.title}

+ +
) : null}
); diff --git a/apps/platform/components/showcase/docs-steps.tsx b/apps/platform/components/showcase/docs-steps.tsx new file mode 100644 index 0000000..d3a13a0 --- /dev/null +++ b/apps/platform/components/showcase/docs-steps.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { GlassContainer } from "../core/3d-container"; + +export function DocsSteps({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return
{children}
; +} + +export function DocsStep({ + index, + title, + description, + children, + isLast = false, + className, +}: { + index: number; + title: string; + description?: React.ReactNode; + children: React.ReactNode; + isLast?: boolean; + className?: string; +}) { + return ( +
+
+ +
+ {index} +
+
+ {!isLast ? ( +
+ ) : null} +
+ +
+
+

+ {title} +

+ {description ? ( +
+ {description} +
+ ) : null} +
+
{children}
+
+
+ ); +} diff --git a/apps/platform/components/showcase/video-card.tsx b/apps/platform/components/showcase/video-card.tsx index d9fae44..a8439cc 100644 --- a/apps/platform/components/showcase/video-card.tsx +++ b/apps/platform/components/showcase/video-card.tsx @@ -35,7 +35,7 @@ export function CardCard({ item, onClick, trackType = "Card" }: CardProps) { onClick(item); }} className={cn( - "group relative flex flex-col", + "group relative flex min-w-0 max-w-full flex-col", "border-border/70 bg-card/80 rounded-lg border p-1 shadow-sm", "transition-all duration-200", item.comingSoon diff --git a/apps/platform/components/ui/avatar.tsx b/apps/platform/components/ui/avatar.tsx new file mode 100644 index 0000000..47ff2e2 --- /dev/null +++ b/apps/platform/components/ui/avatar.tsx @@ -0,0 +1,112 @@ +"use client" + +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/apps/platform/components/ui/breadcrumb.tsx b/apps/platform/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..1f4b75a --- /dev/null +++ b/apps/platform/components/ui/breadcrumb.tsx @@ -0,0 +1,122 @@ +import * as React from "react" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" +import { HugeiconsIcon } from "@hugeicons/react" +import { ArrowRight01Icon, MoreHorizontalCircle01Icon } from "@hugeicons/core-free-icons" + +function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) { + return ( +