From 39f86d9a6e23cfe0a349987dd367965c06d1bb16 Mon Sep 17 00:00:00 2001 From: Kian Bazarjani Date: Sat, 28 Mar 2026 16:36:13 -0400 Subject: [PATCH] updates --- apps/web/app/(docs)/docs/[[...slug]]/page.tsx | 25 +- apps/web/app/(docs)/layout.tsx | 9 +- .../app/llms.mdx/docs/[[...slug]]/route.ts | 6 +- apps/web/components/app-sidebar.tsx | 746 ++---------------- apps/web/components/sidebar/basics-group.tsx | 41 + .../web/components/sidebar/component-item.tsx | 103 +++ .../components/sidebar/components-group.tsx | 64 ++ apps/web/components/sidebar/data.tsx | 384 +++++++++ apps/web/components/sidebar/footer.tsx | 35 + apps/web/components/sidebar/header.tsx | 36 + apps/web/components/sidebar/link-item.tsx | 54 ++ apps/web/components/sidebar/menu-item.tsx | 73 ++ apps/web/components/sidebar/types.ts | 40 + apps/web/components/sidebar/utils.tsx | 109 +++ apps/web/content/docs/dropdown-menu/index.mdx | 5 +- apps/web/lib/env.ts | 4 + apps/web/lib/source.ts | 49 ++ apps/web/package.json | 1 + apps/web/source.config.ts | 1 + package.json | 1 + turbo.json | 5 + 21 files changed, 1075 insertions(+), 716 deletions(-) create mode 100644 apps/web/components/sidebar/basics-group.tsx create mode 100644 apps/web/components/sidebar/component-item.tsx create mode 100644 apps/web/components/sidebar/components-group.tsx create mode 100644 apps/web/components/sidebar/data.tsx create mode 100644 apps/web/components/sidebar/footer.tsx create mode 100644 apps/web/components/sidebar/header.tsx create mode 100644 apps/web/components/sidebar/link-item.tsx create mode 100644 apps/web/components/sidebar/menu-item.tsx create mode 100644 apps/web/components/sidebar/types.ts create mode 100644 apps/web/components/sidebar/utils.tsx diff --git a/apps/web/app/(docs)/docs/[[...slug]]/page.tsx b/apps/web/app/(docs)/docs/[[...slug]]/page.tsx index f98ae156..6dd48d6c 100644 --- a/apps/web/app/(docs)/docs/[[...slug]]/page.tsx +++ b/apps/web/app/(docs)/docs/[[...slug]]/page.tsx @@ -1,6 +1,10 @@ import type { Metadata } from 'next' import { notFound } from 'next/navigation' -import { docsSource } from '@/lib/source' +import { + type DocsAudience, + getVisibleDocsPage, + getVisibleDocsParams, +} from '@/lib/source' import 'rehype-callouts/theme/github' import { FlaskConicalIcon, TriangleDashedIcon } from 'lucide-react' import Link from 'next/link' @@ -15,7 +19,7 @@ export async function generateMetadata({ params: Promise<{ slug?: string[] }> }): Promise { const slug = (await params).slug || [] - const page = docsSource.getPage(slug) + const page = getVisibleDocsPage(slug) if (!page) { return {} @@ -26,6 +30,7 @@ export async function generateMetadata({ summary: string section: string badge?: 'alpha' | 'beta' + audience: DocsAudience image?: string body: React.ComponentType toc: unknown @@ -57,6 +62,16 @@ export async function generateMetadata({ return { title: pageTitle, description: metadata.summary, + robots: + metadata.audience === 'private' + ? { + index: false, + follow: false, + } + : undefined, + other: { + audience: metadata.audience, + }, openGraph: { title: `${pageTitle} — bazza/ui`, description: metadata.summary, @@ -132,7 +147,7 @@ export default async function Page({ params: Promise<{ slug?: string[] }> }) { const slug = (await params).slug || [] - const page = docsSource.getPage(slug) + const page = getVisibleDocsPage(slug) if (!page) { notFound() @@ -179,9 +194,7 @@ export default async function Page({ } export async function generateStaticParams() { - return docsSource.getPages().map((page) => ({ - slug: page.slugs, - })) + return getVisibleDocsParams() } export const dynamicParams = false diff --git a/apps/web/app/(docs)/layout.tsx b/apps/web/app/(docs)/layout.tsx index 0c97c022..29d76811 100644 --- a/apps/web/app/(docs)/layout.tsx +++ b/apps/web/app/(docs)/layout.tsx @@ -4,15 +4,22 @@ import { SidebarProvider, SidebarTrigger, } from '@/components/ui/sidebar' +import { getVisibleDocsUrls, getVisiblePrivateDocsUrls } from '@/lib/source' export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { + const visibleDocUrls = getVisibleDocsUrls() + const visiblePrivateDocUrls = getVisiblePrivateDocsUrls() + return ( - +
diff --git a/apps/web/app/llms.mdx/docs/[[...slug]]/route.ts b/apps/web/app/llms.mdx/docs/[[...slug]]/route.ts index e63a07cb..0e7a2801 100644 --- a/apps/web/app/llms.mdx/docs/[[...slug]]/route.ts +++ b/apps/web/app/llms.mdx/docs/[[...slug]]/route.ts @@ -1,5 +1,5 @@ import { notFound } from 'next/navigation' -import { docsSource } from '@/lib/source' +import { getVisibleDocsPage, getVisibleDocsParams } from '@/lib/source' export const revalidate = false @@ -8,7 +8,7 @@ export async function GET( { params }: { params: Promise<{ slug?: string[] }> }, ) { const { slug } = await params - const page = docsSource.getPage(slug ?? []) + const page = getVisibleDocsPage(slug ?? []) if (!page) notFound() // Get the raw MDX content using fumadocs getText API @@ -22,5 +22,5 @@ export async function GET( } export function generateStaticParams() { - return docsSource.generateParams() + return getVisibleDocsParams() } diff --git a/apps/web/components/app-sidebar.tsx b/apps/web/components/app-sidebar.tsx index 228a0506..ec7d1385 100644 --- a/apps/web/components/app-sidebar.tsx +++ b/apps/web/components/app-sidebar.tsx @@ -1,622 +1,45 @@ 'use client' -import { ChevronRight, TriangleDashedIcon } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' import { usePathname } from 'next/navigation' -import { useRef } from 'react' -import { DiscordIcon, GithubIcon } from '@/components/icons' -import { Button } from '@/components/ui/button' -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from '@/components/ui/collapsible' -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarTrigger, -} from '@/components/ui/sidebar' +import { type ComponentProps, useRef } from 'react' import { cn } from '@/lib/utils' -import logoSrc from '@/public/bazzaui-v3-color.png' import { FadeContainer } from './fade-container' -import { ThemeToggle } from './theme-toggle' - -const items = [ - { - title: 'Introduction', - url: '/docs/intro', - }, - { - title: 'Getting Started', - url: '/docs/getting-started', - }, - { - title: 'Feedback', - url: '/docs/feedback', - }, -] - -type MenuItem = - | { - type: 'link' - title: React.ReactNode - url: string - } - | { - type: 'collapsible' - title: React.ReactNode - items: MenuItem[] - } - -type ComponentItem = - | { - type: 'single' - title: React.ReactNode - url: string - badge?: React.ReactNode - } - | { - type: 'collapsible' - title: React.ReactNode - urlPrefix: string - badge?: React.ReactNode - groups: Array<{ - groupName: string - items: MenuItem[] - }> - } - -const componentItems: ComponentItem[] = [ - { - type: 'single', - title: 'Dropdown Menu', - url: '/docs/dropdown-menu', - }, - { - type: 'single', - title: 'Context Menu', - url: '/docs/context-menu', - }, - { - type: 'single', - title: 'Select', - url: '/docs/select', - }, - { - type: 'single', - title: 'Combobox', - url: '/docs/combobox', - }, - { - type: 'collapsible', - title: 'Filters', - urlPrefix: '/docs/filters', - groups: [ - { - groupName: 'Getting Started', - items: [ - { - type: 'link', - title: 'Introduction', - url: '/docs/filters/introduction', - }, - { - type: 'link', - title: 'Installation', - url: '/docs/filters/installation', - }, - // { - // type: 'link', - // title: 'Quick Start', - // url: '/docs/filters/quick-start', - // }, - { - type: 'link', - title: 'Examples', - url: '/docs/filters/examples', - }, - { - type: 'link', - title: 'Blocks', - url: '/docs/filters/blocks', - }, - ], - }, - { - groupName: 'Components', - items: [ - { - type: 'link', - title: 'Filter', - url: '/docs/filters/components/filter', - }, - ], - }, - { - groupName: 'Core', - items: [ - // { - // type: 'link', - // title: 'Overview', - // url: '/docs/filters/core/overview', - // }, - { - type: 'link', - title: 'Anatomy', - url: '/docs/filters/core/anatomy', - }, - { - type: 'link', - title: 'Concepts', - url: '/docs/filters/core/concepts', - }, - // { - // type: 'collapsible', - // title: 'Model', - // items: [ - // { - // type: 'link', - // title: 'Columns', - // url: '/docs/filters/core/model/columns', - // }, - // { - // type: 'link', - // title: 'Filters', - // url: '/docs/filters/core/model/filters', - // }, - // { - // type: 'link', - // title: 'Operators', - // url: '/docs/filters/core/model/operators', - // }, - // { - // type: 'link', - // title: 'Options', - // url: '/docs/filters/core/model/options', - // }, - // ], - // }, - ], - }, - { - groupName: 'Features', - items: [ - { - type: 'link', - title: 'Column Builder', - url: '/docs/filters/column-builder', - }, - { - type: 'link', - title: 'Instance', - url: '/docs/filters/instance', - }, - { - type: 'link', - title: 'State Management', - url: '/docs/filters/state-management', - }, - { - type: 'link', - title: 'Faceted Values', - url: '/docs/filters/faceted-values', - }, - { - type: 'link', - title: 'Option Columns', - url: '/docs/filters/option-columns', - }, - { - type: 'link', - title: 'Columns', - url: '/docs/filters/columns', - }, - { - type: 'link', - title: 'Operators', - url: '/docs/filters/operators', - }, - { - type: 'link', - title: 'Actions', - url: '/docs/filters/actions', - }, - { - type: 'link', - title: 'Filtering Data', - url: '/docs/filters/filtering-data', - }, - { - type: 'link', - title: 'Internationalization', - url: '/docs/filters/i18n', - }, - ], - }, - { - groupName: 'Integrations', - items: [ - { - type: 'link', - title: 'TanStack Table', - url: '/docs/filters/integrations/tanstack-table', - }, - { - type: 'link', - title: 'nuqs', - url: '/docs/filters/integrations/nuqs', - }, - ], - }, - ], - }, -] - -const archivedComponentItems: ComponentItem[] = [ - { - type: 'collapsible', - title: 'Action Menu', - urlPrefix: '/docs/action-menu', - badge: ( - - ), - groups: [ - { - groupName: 'Getting Started', - items: [ - { - type: 'link', - title: 'Introduction', - url: '/docs/action-menu/introduction', - }, - { - type: 'link', - title: 'Installation', - url: '/docs/action-menu/installation', - }, - { - type: 'link', - title: 'Quick Start', - url: '/docs/action-menu/quick-start', - }, - { - type: 'link', - title: 'Examples', - url: '/docs/action-menu/examples', - }, - ], - }, - { - groupName: 'Concepts', - items: [ - { - type: 'link', - title: 'Data-First API', - url: '/docs/action-menu/data-first-api', - }, - { - type: 'link', - title: 'Menu Structure', - url: '/docs/action-menu/menu-structure', - }, - { - type: 'link', - title: 'Node Types', - url: '/docs/action-menu/node-types', - }, - { - type: 'link', - title: 'State Management', - url: '/docs/action-menu/state-management', - }, - { - type: 'link', - title: 'Responsive Behavior', - url: '/docs/action-menu/responsive-behavior', - }, - ], - }, - { - groupName: 'Features', - items: [ - { - type: 'link', - title: 'Node Configuration', - url: '/docs/action-menu/nodes', - }, - { - type: 'link', - title: 'Async Loading', - url: '/docs/action-menu/async', - }, - { - type: 'link', - title: 'Search & Filtering', - url: '/docs/action-menu/search', - }, - { - type: 'link', - title: 'Keyboard Navigation', - url: '/docs/action-menu/keyboard', - }, - { - type: 'link', - title: 'Focus Management', - url: '/docs/action-menu/focus', - }, - { - type: 'link', - title: 'Positioning', - url: '/docs/action-menu/positioning', - }, - { - type: 'link', - title: 'Theming', - url: '/docs/action-menu/theming', - }, - { - type: 'link', - title: 'Virtualization', - url: '/docs/action-menu/virtualization', - }, - { - type: 'link', - title: 'Middleware', - url: '/docs/action-menu/middleware', - }, - { - type: 'link', - title: 'Extended Properties', - url: '/docs/action-menu/extended-properties', - }, - { - type: 'link', - title: 'Defaults', - url: '/docs/action-menu/defaults', - }, - ], - }, - { - groupName: 'Advanced', - items: [ - { - type: 'link', - title: 'Loader Adapters', - url: '/docs/action-menu/loader-adapters', - }, - { - type: 'link', - title: 'Deep Search', - url: '/docs/action-menu/deep-search', - }, - { - type: 'link', - title: 'Intent Zone', - url: '/docs/action-menu/intent-zone', - }, - { - type: 'link', - title: 'Custom Rendering', - url: '/docs/action-menu/custom-rendering', - }, - { - type: 'link', - title: 'Performance Optimization', - url: '/docs/action-menu/performance', - }, - { - type: 'link', - title: 'Accessibility', - url: '/docs/action-menu/accessibility', - }, - { - type: 'link', - title: 'RTL Support', - url: '/docs/action-menu/rtl', - }, - ], - }, - { - groupName: 'Components', - items: [ - { - type: 'link', - title: 'Select', - url: '/docs/action-menu/select', - }, - { - type: 'link', - title: 'MultiSelect', - url: '/docs/action-menu/multiselect', - }, - { - type: 'link', - title: 'Dropdown Menu', - url: '/docs/action-menu/dropdown-menu', - }, - { - type: 'link', - title: 'Context Menu', - url: '/docs/action-menu/context-menu', - }, - { - type: 'link', - title: 'Command Palette', - url: '/docs/action-menu/command-palette', - }, - ], - }, - { - groupName: 'Reference', - items: [ - { - type: 'link', - title: 'API Reference', - url: '/docs/action-menu/api-reference', - }, - { - type: 'link', - title: 'TypeScript Types', - url: '/docs/action-menu/typescript', - }, - ], - }, - ], - }, -] - -// Helper function to check if a menu item or any of its descendants contain the active URL -function containsActiveUrl(item: MenuItem, pathname: string): boolean { - if (item.type === 'link') { - return item.url === pathname - } - return item.items.some((child) => containsActiveUrl(child, pathname)) -} - -// Recursive component for rendering menu items -function MenuItemRenderer({ - item, - pathname, -}: { - item: MenuItem - pathname: string -}) { - if (item.type === 'link') { - return ( - - {item.title} - - ) - } - - // Collapsible menu item - const hasActiveChild = containsActiveUrl(item, pathname) - - return ( - - - - {item.title} - - - - - - {item.items.map((child, index) => ( - - ))} - - - - ) -} - -function ComponentItemRenderer({ - component, - pathname, -}: { - component: ComponentItem - pathname: string -}) { - if (component.type === 'single') { - return ( - - - - {component.title} - {component.badge} - - - - ) - } - - return ( - - - - -
- {component.title} - {component.badge} -
- -
-
- - - {component.groups.map((group, index) => ( -
- - {group.groupName} - -
- {group.items.map((item, itemIndex) => ( - - ))} -
-
- ))} -
-
-
-
- ) -} +import { SidebarBasicsGroup } from './sidebar/basics-group' +import { SidebarComponentsGroup } from './sidebar/components-group' +import { + archivedComponentItems, + basicItems, + componentItems, +} from './sidebar/data' +import { AppSidebarFooter } from './sidebar/footer' +import { AppSidebarHeader } from './sidebar/header' +import type { ComponentItem } from './sidebar/types' +import { filterComponentItem, isVisibleDocUrl } from './sidebar/utils' +import { Sidebar, SidebarContent } from './ui/sidebar' export function AppSidebar({ + visibleDocUrls, + privateDocUrls, className, variant = 'inset', ...props -}: React.ComponentProps) { +}: ComponentProps & { + visibleDocUrls?: string[] + privateDocUrls?: string[] +}) { const pathname = usePathname() const ref = useRef(null) + const visibleDocUrlSet = visibleDocUrls ? new Set(visibleDocUrls) : null + const privateDocUrlSet = privateDocUrls ? new Set(privateDocUrls) : null + const visibleItems = basicItems.filter((item) => + isVisibleDocUrl(item.url, visibleDocUrlSet), + ) + const visibleComponentItems = componentItems + .map((component) => filterComponentItem(component, visibleDocUrlSet)) + .filter((component): component is ComponentItem => component !== null) + const visibleArchivedComponentItems = archivedComponentItems + .map((component) => filterComponentItem(component, visibleDocUrlSet)) + .filter((component): component is ComponentItem => component !== null) return ( - - - - - bazza/ui - bazza - / - ui - - - - - + - - - Basics - - - {items.map((item) => ( - - - - {item.title} - - - - ))} - - - - - Components - - - {componentItems.map((component) => ( - - ))} - {archivedComponentItems.length > 0 && ( - - Archived - - )} - {archivedComponentItems.map((component) => ( - - ))} - - - + + + - - - - - - - - - - - - + ) } diff --git a/apps/web/components/sidebar/basics-group.tsx b/apps/web/components/sidebar/basics-group.tsx new file mode 100644 index 00000000..9967a2bf --- /dev/null +++ b/apps/web/components/sidebar/basics-group.tsx @@ -0,0 +1,41 @@ +'use client' + +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, +} from '@/components/ui/sidebar' +import { SidebarLinkItem } from './link-item' +import type { SidebarBasicItem } from './types' + +type SidebarBasicsGroupProps = { + items: SidebarBasicItem[] + pathname: string + privateDocUrls: Set | null +} + +export function SidebarBasicsGroup({ + items, + pathname, + privateDocUrls, +}: SidebarBasicsGroupProps) { + return ( + + Basics + + + {items.map((item) => ( + + ))} + + + + ) +} diff --git a/apps/web/components/sidebar/component-item.tsx b/apps/web/components/sidebar/component-item.tsx new file mode 100644 index 00000000..8da31fec --- /dev/null +++ b/apps/web/components/sidebar/component-item.tsx @@ -0,0 +1,103 @@ +'use client' + +import { ChevronRight } from 'lucide-react' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible' +import { + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, +} from '@/components/ui/sidebar' +import { cn } from '@/lib/utils' +import { SidebarLinkItem } from './link-item' +import { SidebarMenuItemRenderer } from './menu-item' +import type { ComponentItem } from './types' +import { + getComponentAudienceBadge, + getComponentAudienceClassName, +} from './utils' + +type SidebarComponentItemProps = { + component: ComponentItem + pathname: string + privateDocUrls: Set | null +} + +export function SidebarComponentItem({ + component, + pathname, + privateDocUrls, +}: SidebarComponentItemProps) { + if (component.type === 'single') { + return ( + + ) + } + + return ( + + + + +
+ {component.title} + {getComponentAudienceBadge(component.audience)} + {component.badge} +
+ +
+
+ + + {component.groups.map((group, index) => ( +
+ + {group.groupName} + +
+ {group.items.map((item, itemIndex) => ( + + ))} +
+
+ ))} +
+
+
+
+ ) +} diff --git a/apps/web/components/sidebar/components-group.tsx b/apps/web/components/sidebar/components-group.tsx new file mode 100644 index 00000000..38fcce63 --- /dev/null +++ b/apps/web/components/sidebar/components-group.tsx @@ -0,0 +1,64 @@ +'use client' + +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuItem, +} from '@/components/ui/sidebar' +import { SidebarComponentItem } from './component-item' +import type { ComponentItem } from './types' + +type SidebarComponentsGroupProps = { + items: ComponentItem[] + archivedItems: ComponentItem[] + pathname: string + privateDocUrls: Set | null +} + +export function SidebarComponentsGroup({ + items, + archivedItems, + pathname, + privateDocUrls, +}: SidebarComponentsGroupProps) { + return ( + + Components + + + {items.map((component) => ( + + ))} + {archivedItems.length > 0 ? ( + + Archived + + ) : null} + {archivedItems.map((component) => ( + + ))} + + + + ) +} diff --git a/apps/web/components/sidebar/data.tsx b/apps/web/components/sidebar/data.tsx new file mode 100644 index 00000000..ad3f5c17 --- /dev/null +++ b/apps/web/components/sidebar/data.tsx @@ -0,0 +1,384 @@ +import { TriangleDashedIcon } from 'lucide-react' +import type { ComponentItem, SidebarBasicItem } from './types' + +export const basicItems: SidebarBasicItem[] = [ + { + title: 'Introduction', + url: '/docs/intro', + }, + { + title: 'Getting Started', + url: '/docs/getting-started', + }, + { + title: 'Feedback', + url: '/docs/feedback', + }, +] + +export const componentItems: ComponentItem[] = [ + { + type: 'collapsible', + title: 'Filters', + urlPrefix: '/docs/filters', + groups: [ + { + groupName: 'Getting Started', + items: [ + { + type: 'link', + title: 'Introduction', + url: '/docs/filters/introduction', + }, + { + type: 'link', + title: 'Installation', + url: '/docs/filters/installation', + }, + { + type: 'link', + title: 'Examples', + url: '/docs/filters/examples', + }, + { + type: 'link', + title: 'Blocks', + url: '/docs/filters/blocks', + }, + ], + }, + { + groupName: 'Components', + items: [ + { + type: 'link', + title: 'Filter', + url: '/docs/filters/components/filter', + }, + ], + }, + { + groupName: 'Core', + items: [ + { + type: 'link', + title: 'Anatomy', + url: '/docs/filters/core/anatomy', + }, + { + type: 'link', + title: 'Concepts', + url: '/docs/filters/core/concepts', + }, + ], + }, + { + groupName: 'Features', + items: [ + { + type: 'link', + title: 'Column Builder', + url: '/docs/filters/column-builder', + }, + { + type: 'link', + title: 'Instance', + url: '/docs/filters/instance', + }, + { + type: 'link', + title: 'State Management', + url: '/docs/filters/state-management', + }, + { + type: 'link', + title: 'Faceted Values', + url: '/docs/filters/faceted-values', + }, + { + type: 'link', + title: 'Option Columns', + url: '/docs/filters/option-columns', + }, + { + type: 'link', + title: 'Columns', + url: '/docs/filters/columns', + }, + { + type: 'link', + title: 'Operators', + url: '/docs/filters/operators', + }, + { + type: 'link', + title: 'Actions', + url: '/docs/filters/actions', + }, + { + type: 'link', + title: 'Filtering Data', + url: '/docs/filters/filtering-data', + }, + { + type: 'link', + title: 'Internationalization', + url: '/docs/filters/i18n', + }, + ], + }, + { + groupName: 'Integrations', + items: [ + { + type: 'link', + title: 'TanStack Table', + url: '/docs/filters/integrations/tanstack-table', + }, + { + type: 'link', + title: 'nuqs', + url: '/docs/filters/integrations/nuqs', + }, + ], + }, + ], + }, + { + type: 'single', + title: 'Dropdown Menu', + url: '/docs/dropdown-menu', + audience: 'private', + }, + { + type: 'single', + title: 'Context Menu', + url: '/docs/context-menu', + audience: 'private', + }, + { + type: 'single', + title: 'Select', + url: '/docs/select', + audience: 'private', + }, + { + type: 'single', + title: 'Combobox', + url: '/docs/combobox', + audience: 'private', + }, +] + +export const archivedComponentItems: ComponentItem[] = [ + { + type: 'collapsible', + title: 'Action Menu', + urlPrefix: '/docs/action-menu', + badge: ( + + ), + groups: [ + { + groupName: 'Getting Started', + items: [ + { + type: 'link', + title: 'Introduction', + url: '/docs/action-menu/introduction', + }, + { + type: 'link', + title: 'Installation', + url: '/docs/action-menu/installation', + }, + { + type: 'link', + title: 'Quick Start', + url: '/docs/action-menu/quick-start', + }, + { + type: 'link', + title: 'Examples', + url: '/docs/action-menu/examples', + }, + ], + }, + { + groupName: 'Concepts', + items: [ + { + type: 'link', + title: 'Data-First API', + url: '/docs/action-menu/data-first-api', + }, + { + type: 'link', + title: 'Menu Structure', + url: '/docs/action-menu/menu-structure', + }, + { + type: 'link', + title: 'Node Types', + url: '/docs/action-menu/node-types', + }, + { + type: 'link', + title: 'State Management', + url: '/docs/action-menu/state-management', + }, + { + type: 'link', + title: 'Responsive Behavior', + url: '/docs/action-menu/responsive-behavior', + }, + ], + }, + { + groupName: 'Features', + items: [ + { + type: 'link', + title: 'Node Configuration', + url: '/docs/action-menu/nodes', + }, + { + type: 'link', + title: 'Async Loading', + url: '/docs/action-menu/async', + }, + { + type: 'link', + title: 'Search & Filtering', + url: '/docs/action-menu/search', + }, + { + type: 'link', + title: 'Keyboard Navigation', + url: '/docs/action-menu/keyboard', + }, + { + type: 'link', + title: 'Focus Management', + url: '/docs/action-menu/focus', + }, + { + type: 'link', + title: 'Positioning', + url: '/docs/action-menu/positioning', + }, + { + type: 'link', + title: 'Theming', + url: '/docs/action-menu/theming', + }, + { + type: 'link', + title: 'Virtualization', + url: '/docs/action-menu/virtualization', + }, + { + type: 'link', + title: 'Middleware', + url: '/docs/action-menu/middleware', + }, + { + type: 'link', + title: 'Extended Properties', + url: '/docs/action-menu/extended-properties', + }, + { + type: 'link', + title: 'Defaults', + url: '/docs/action-menu/defaults', + }, + ], + }, + { + groupName: 'Advanced', + items: [ + { + type: 'link', + title: 'Loader Adapters', + url: '/docs/action-menu/loader-adapters', + }, + { + type: 'link', + title: 'Deep Search', + url: '/docs/action-menu/deep-search', + }, + { + type: 'link', + title: 'Intent Zone', + url: '/docs/action-menu/intent-zone', + }, + { + type: 'link', + title: 'Custom Rendering', + url: '/docs/action-menu/custom-rendering', + }, + { + type: 'link', + title: 'Performance Optimization', + url: '/docs/action-menu/performance', + }, + { + type: 'link', + title: 'Accessibility', + url: '/docs/action-menu/accessibility', + }, + { + type: 'link', + title: 'RTL Support', + url: '/docs/action-menu/rtl', + }, + ], + }, + { + groupName: 'Components', + items: [ + { + type: 'link', + title: 'Select', + url: '/docs/action-menu/select', + }, + { + type: 'link', + title: 'MultiSelect', + url: '/docs/action-menu/multiselect', + }, + { + type: 'link', + title: 'Dropdown Menu', + url: '/docs/action-menu/dropdown-menu', + }, + { + type: 'link', + title: 'Context Menu', + url: '/docs/action-menu/context-menu', + }, + { + type: 'link', + title: 'Command Palette', + url: '/docs/action-menu/command-palette', + }, + ], + }, + { + groupName: 'Reference', + items: [ + { + type: 'link', + title: 'API Reference', + url: '/docs/action-menu/api-reference', + }, + { + type: 'link', + title: 'TypeScript Types', + url: '/docs/action-menu/typescript', + }, + ], + }, + ], + }, +] diff --git a/apps/web/components/sidebar/footer.tsx b/apps/web/components/sidebar/footer.tsx new file mode 100644 index 00000000..4462efaa --- /dev/null +++ b/apps/web/components/sidebar/footer.tsx @@ -0,0 +1,35 @@ +'use client' + +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { + SidebarFooter, + SidebarMenu, + SidebarMenuItem, +} from '@/components/ui/sidebar' +import { DiscordIcon, GithubIcon } from '../icons' +import { ThemeToggle } from '../theme-toggle' + +export function AppSidebarFooter() { + return ( + + + + + + + + + + + + ) +} diff --git a/apps/web/components/sidebar/header.tsx b/apps/web/components/sidebar/header.tsx new file mode 100644 index 00000000..590de622 --- /dev/null +++ b/apps/web/components/sidebar/header.tsx @@ -0,0 +1,36 @@ +'use client' + +import Image from 'next/image' +import Link from 'next/link' +import { + SidebarHeader, + SidebarMenu, + SidebarMenuItem, + SidebarTrigger, +} from '@/components/ui/sidebar' +import logoSrc from '@/public/bazzaui-v3-color.png' + +export function AppSidebarHeader() { + return ( + + + + + bazza/ui + bazza + / + ui + + + + + + ) +} diff --git a/apps/web/components/sidebar/link-item.tsx b/apps/web/components/sidebar/link-item.tsx new file mode 100644 index 00000000..91051022 --- /dev/null +++ b/apps/web/components/sidebar/link-item.tsx @@ -0,0 +1,54 @@ +'use client' + +import Link from 'next/link' +import type { ReactNode } from 'react' +import { SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar' +import { cn } from '@/lib/utils' +import type { SidebarAudience } from './types' +import { + getComponentAudienceBadge, + getComponentAudienceClassName, + isPrivateDocUrl, +} from './utils' + +type SidebarLinkItemProps = { + title: ReactNode + url: string + pathname: string + privateDocUrls: Set | null + audience?: SidebarAudience + badge?: ReactNode +} + +export function SidebarLinkItem({ + title, + url, + pathname, + privateDocUrls, + audience, + badge, +}: SidebarLinkItemProps) { + const privateDoc = isPrivateDocUrl(url, privateDocUrls) + const audienceBadge = getComponentAudienceBadge(audience) + + return ( + + + + {title} + {privateDoc + ? (audienceBadge ?? getComponentAudienceBadge('private')) + : audienceBadge} + {badge} + + + + ) +} diff --git a/apps/web/components/sidebar/menu-item.tsx b/apps/web/components/sidebar/menu-item.tsx new file mode 100644 index 00000000..00585796 --- /dev/null +++ b/apps/web/components/sidebar/menu-item.tsx @@ -0,0 +1,73 @@ +'use client' + +import { ChevronRight, LockKeyholeIcon } from 'lucide-react' +import Link from 'next/link' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible' +import { SidebarMenuSub, SidebarMenuSubButton } from '@/components/ui/sidebar' +import type { MenuItem } from './types' +import { containsActiveUrl, isPrivateDocUrl } from './utils' + +function PrivateDocIcon() { + return +} + +type SidebarMenuItemRendererProps = { + item: MenuItem + pathname: string + privateDocUrls: Set | null +} + +export function SidebarMenuItemRenderer({ + item, + pathname, + privateDocUrls, +}: SidebarMenuItemRendererProps) { + if (item.type === 'link') { + const privateDoc = isPrivateDocUrl(item.url, privateDocUrls) + + return ( + + + {item.title} + {privateDoc ? : null} + + + ) + } + + const hasActiveChild = containsActiveUrl(item, pathname) + + return ( + + + + {item.title} + + + + + + {item.items.map((child, index) => ( + + ))} + + + + ) +} diff --git a/apps/web/components/sidebar/types.ts b/apps/web/components/sidebar/types.ts new file mode 100644 index 00000000..30a2b07b --- /dev/null +++ b/apps/web/components/sidebar/types.ts @@ -0,0 +1,40 @@ +import type { ReactNode } from 'react' + +export type SidebarAudience = 'public' | 'private' | 'preview' + +export type SidebarBasicItem = { + title: string + url: string +} + +export type MenuItem = + | { + type: 'link' + title: ReactNode + url: string + } + | { + type: 'collapsible' + title: ReactNode + items: MenuItem[] + } + +type ComponentItemBase = { + title: ReactNode + badge?: ReactNode + audience?: SidebarAudience +} + +export type ComponentItem = + | (ComponentItemBase & { + type: 'single' + url: string + }) + | (ComponentItemBase & { + type: 'collapsible' + urlPrefix: string + groups: Array<{ + groupName: string + items: MenuItem[] + }> + }) diff --git a/apps/web/components/sidebar/utils.tsx b/apps/web/components/sidebar/utils.tsx new file mode 100644 index 00000000..2f45610d --- /dev/null +++ b/apps/web/components/sidebar/utils.tsx @@ -0,0 +1,109 @@ +import { FlaskConicalIcon, LockKeyholeIcon } from 'lucide-react' +import type { ComponentItem, MenuItem, SidebarAudience } from './types' + +export function isVisibleDocUrl( + url: string, + visibleDocUrls: Set | null, +) { + return visibleDocUrls ? visibleDocUrls.has(url) : true +} + +export function isPrivateDocUrl( + url: string, + privateDocUrls: Set | null, +) { + return privateDocUrls ? privateDocUrls.has(url) : false +} + +export function getComponentAudience( + audience: SidebarAudience | undefined, +): SidebarAudience { + return audience ?? 'public' +} + +export function getComponentAudienceClassName( + audience: SidebarAudience | undefined, +) { + switch (getComponentAudience(audience)) { + // case 'private': + // return 'text-amber-700 hover:text-amber-800 dark:text-amber-300 dark:hover:text-amber-200' + // case 'preview': + // return 'text-sky-700 hover:text-sky-800 dark:text-sky-300 dark:hover:text-sky-200' + default: + return 'text-muted-foreground' + } +} + +export function getComponentAudienceBadge( + audience: SidebarAudience | undefined, +) { + switch (getComponentAudience(audience)) { + case 'private': + return ( + + ) + case 'preview': + return ( + + ) + default: + return null + } +} + +export function filterMenuItem( + item: MenuItem, + visibleDocUrls: Set | null, +): MenuItem | null { + if (item.type === 'link') { + return isVisibleDocUrl(item.url, visibleDocUrls) ? item : null + } + + const filteredItems = item.items + .map((child) => filterMenuItem(child, visibleDocUrls)) + .filter((child): child is MenuItem => child !== null) + + if (filteredItems.length === 0) { + return null + } + + return { + ...item, + items: filteredItems, + } +} + +export function filterComponentItem( + component: ComponentItem, + visibleDocUrls: Set | null, +): ComponentItem | null { + if (component.type === 'single') { + return isVisibleDocUrl(component.url, visibleDocUrls) ? component : null + } + + const filteredGroups = component.groups + .map((group) => ({ + ...group, + items: group.items + .map((item) => filterMenuItem(item, visibleDocUrls)) + .filter((item): item is MenuItem => item !== null), + })) + .filter((group) => group.items.length > 0) + + if (filteredGroups.length === 0) { + return null + } + + return { + ...component, + groups: filteredGroups, + } +} + +export function containsActiveUrl(item: MenuItem, pathname: string): boolean { + if (item.type === 'link') { + return item.url === pathname + } + + return item.items.some((child) => containsActiveUrl(child, pathname)) +} diff --git a/apps/web/content/docs/dropdown-menu/index.mdx b/apps/web/content/docs/dropdown-menu/index.mdx index 1240a411..581e9f1b 100644 --- a/apps/web/content/docs/dropdown-menu/index.mdx +++ b/apps/web/content/docs/dropdown-menu/index.mdx @@ -2,6 +2,7 @@ title: Dropdown Menu section: Getting Started summary: Displays a menu to the user—such as a set of actions or functions—triggered by a button. Supports items, checkboxes, radio groups, submenus, and deep search. +audience: private --- ## Installation @@ -227,10 +228,6 @@ Use virtualization to efficiently render large lists with thousands of items. Th -### Async + Deep Search - - - ## API Reference For the full API docs, see [Dropdown Menu API Reference](/docs/dropdown-menu/api-reference). diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index 93521f5b..0f56fe76 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -3,6 +3,9 @@ import { z } from 'zod' export const env = createEnv({ skipValidation: true, + server: { + SHOW_PRIVATE_PAGES: z.enum(['true', 'false']).optional(), + }, client: { NEXT_PUBLIC_APP_URL: z.string().min(1), NEXT_PUBLIC_RELEASE_TYPE: z @@ -10,6 +13,7 @@ export const env = createEnv({ .default('canary'), }, runtimeEnv: { + SHOW_PRIVATE_PAGES: process.env.SHOW_PRIVATE_PAGES, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' ? `https://${process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}` diff --git a/apps/web/lib/source.ts b/apps/web/lib/source.ts index bc963043..1993a47b 100644 --- a/apps/web/lib/source.ts +++ b/apps/web/lib/source.ts @@ -2,6 +2,7 @@ import { changelog, docs } from 'fumadocs-mdx:collections/server' import { loader } from 'fumadocs-core/source' +import { env } from './env' export const docsSource = loader({ baseUrl: '/docs', @@ -12,3 +13,51 @@ export const changelogSource = loader({ baseUrl: '/changelog', source: changelog.toFumadocsSource(), }) + +export type DocsAudience = 'public' | 'preview' | 'private' + +function getDocsAudience(page: { data?: { audience?: DocsAudience } }) { + return page.data?.audience ?? 'public' +} + +export function shouldShowPrivateDocs() { + return ( + env.SHOW_PRIVATE_PAGES === 'true' || + process.env.NODE_ENV !== 'production' || + process.env.VERCEL_ENV === 'preview' + ) +} + +function isVisibleDocsPage(page: { data?: { audience?: DocsAudience } }) { + return shouldShowPrivateDocs() || getDocsAudience(page) !== 'private' +} + +export function getVisibleDocsPage(slug: string[]) { + const page = docsSource.getPage(slug) + + if (!page || !isVisibleDocsPage(page)) { + return undefined + } + + return page +} + +export function getVisibleDocsPages() { + return docsSource.getPages().filter(isVisibleDocsPage) +} + +export function getVisibleDocsParams() { + return getVisibleDocsPages().map((page) => ({ + slug: page.slugs, + })) +} + +export function getVisibleDocsUrls() { + return getVisibleDocsPages().map((page) => page.url) +} + +export function getVisiblePrivateDocsUrls() { + return getVisibleDocsPages() + .filter((page) => getDocsAudience(page) === 'private') + .map((page) => page.url) +} diff --git a/apps/web/package.json b/apps/web/package.json index b66f7489..e1600b8b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,6 +12,7 @@ "type-check": "tsc --noEmit", "vercel:install": "./vercel-submodule.sh && bun install", "start": "next start", + "start:portless": "portless bazza-ui next start", "lint": "next lint", "prepare": "husky", "check": "biome check", diff --git a/apps/web/source.config.ts b/apps/web/source.config.ts index 91273a8e..e42b8ba6 100644 --- a/apps/web/source.config.ts +++ b/apps/web/source.config.ts @@ -22,6 +22,7 @@ export const docs = defineDocs({ dir: 'content/docs', docs: { schema: frontmatterSchema.extend({ + audience: z.enum(['public', 'preview', 'private']).default('public'), component: z.string().optional(), summary: z.string(), section: z.string().optional(), diff --git a/package.json b/package.json index 09e6f1a7..ebd48255 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "registry:build": "turbo run registry:build", "docs:type-gen": "turbo run type-gen --filter web", "start": "turbo run start", + "start:portless": "turbo run start:portless", "prepare": "husky", "changeset": "changeset add", "ci:version": "changeset version && bun install", diff --git a/turbo.json b/turbo.json index 15e017ab..eb573dd7 100644 --- a/turbo.json +++ b/turbo.json @@ -61,6 +61,11 @@ "dependsOn": ["build", "registry:build"], "cache": false, "persistent": true + }, + "start:portless": { + "dependsOn": ["build", "registry:build"], + "cache": false, + "persistent": true } } }