diff --git a/REVIEWERS.md b/REVIEWERS.md index ab123b75..f65321de 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -4,7 +4,7 @@ This file is intended to used as **review guidance** for automated/agent code re ## CodeBlock component / shiki codeblock styling divergence -The react codeblock component should avoid creating custom styles for itself, prefer styling shiki and it's elements from app/styles/shiki.css and and lib/shiki.ts +The react codeblock component should avoid creating custom styles for itself, prefer styling shiki and it's elements from app/styles/shiki.css and and lib/mdx.ts ### Why diff --git a/app/(registry)/[[...slug]]/page.tsx b/app/(registry)/[[...slug]]/page.tsx index 85e39647..78b29340 100644 --- a/app/(registry)/[[...slug]]/page.tsx +++ b/app/(registry)/[[...slug]]/page.tsx @@ -35,6 +35,8 @@ import { cn } from '@/lib/utils' import { RegistryMetaProvider } from '@/components/registry-meta' import { PageGithubLinkButton } from '@/components/page-github-link-button' import { PageViews } from '@/components/layout/views' +import { MDXContent } from '@/components/content' +import { getLibraryReadme } from '@/lib/libraries' const getComponentSlug = (page: InferPageType) => { if (page.slugs[0] !== 'components') return undefined @@ -91,9 +93,11 @@ export default async function Page(props: PageProps<'/[[...slug]]'>) { })() const componentSlug = getComponentSlug(page) - const [downloadStats, pageViews] = await Promise.all([ + const isLibrary = page.data.type === 'library' && page.data.repo + const [downloadStats, pageViews, libraryReadme] = await Promise.all([ componentSlug ? getComponentDownloadStats(componentSlug) : null, isLog ? getPageViews(`/logs/${page.slugs[page.slugs.length - 1]}`) : null, + isLibrary ? getLibraryReadme(page.data.repo!) : null, ]) const componentSource = await getComponentSource(componentSlug) const docLinks = [...page.data.docLinks] @@ -101,7 +105,9 @@ export default async function Page(props: PageProps<'/[[...slug]]'>) { const llmUrl = page.slugs.length === 0 ? null : `/${page.slugs.join('/')}.md` const relatedItems = getRelatedPages(page, 3) - const toc = page.data.toc + const toc = libraryReadme + ? [...page.data.toc, ...libraryReadme.toc] + : page.data.toc const hasToc = toc.length > 0 const counts = getRegistryCounts() @@ -187,6 +193,7 @@ export default async function Page(props: PageProps<'/[[...slug]]'>) { a: createRelativeLink(source, page), })} /> + {libraryReadme && } {!isTopCategoryPage && relatedItems.length > 0 && ( ) { - const cookieStore = await cookies() - const isTeam = cookieStore.has('joyco-team') - - const itemMeta: Record< - string, - { - badge?: 'new' | 'updated' | 'internal' - dot?: 'red' | 'blue' | 'green' | 'yellow' - hidden?: boolean - } - > = { - '/toolbox/skills': { badge: 'new' }, - '/toolbox/ui': isTeam ? { badge: 'internal' } : { hidden: true }, +const itemMeta: Record< + string, + { + badge?: 'new' | 'updated' | 'internal' + dot?: 'red' | 'blue' | 'green' | 'yellow' + hidden?: boolean } +> = { + '/toolbox/atlas-cropper': { badge: 'new' }, + '/toolbox/skills': { badge: 'new' }, + '/toolbox/ui': { hidden: true }, +} + +export default async function Layout({ children }: LayoutProps<'/'>) { const gameSlugs = getGameSlugs() const effectSlugs = getEffectSlugs() const canvasSlugs = getCanvasSlugs() + const librarySlugs = getLibrarySlugs() const { experiments } = await getExperiments() return ( @@ -58,6 +58,7 @@ export default async function Layout({ children }: LayoutProps<'/'>) { gameSlugs={gameSlugs} effectSlugs={effectSlugs} canvasSlugs={canvasSlugs} + librarySlugs={librarySlugs} experiments={experiments} /> {children} diff --git a/app/api/screenshot/route.ts b/app/api/screenshot/route.ts index 1ff8f3fc..fc16a754 100644 --- a/app/api/screenshot/route.ts +++ b/app/api/screenshot/route.ts @@ -108,7 +108,7 @@ const getCachedExperimentScreenshot = unstable_cache( ) }, ['experiment-screenshot'], - { revalidate: 86400 } + { revalidate: 604800 } ) export async function GET(request: NextRequest) { @@ -139,7 +139,7 @@ export async function GET(request: NextRequest) { return new NextResponse(imageBuffer, { headers: { 'Content-Type': 'image/png', - 'Cache-Control': 'public, s-maxage=31536000', + 'Cache-Control': 'public, max-age=604800, s-maxage=604800, stale-while-revalidate=86400', }, }) } catch (error) { diff --git a/components/category-index.tsx b/components/category-index.tsx index c38660fd..d80df5c8 100644 --- a/components/category-index.tsx +++ b/components/category-index.tsx @@ -10,6 +10,24 @@ import { Badge } from './ui/badge' import { EyeIcon } from 'lucide-react' import { Fragment } from 'react' +type PageTreeNode = { + type?: string + $id?: string + url?: string + children?: PageTreeNode[] +} + +function getPageTreeOrder(category: string): string[] { + const children = source.pageTree.children as unknown as PageTreeNode[] + const folder = children.find( + (child) => child.type === 'folder' && child.$id?.split(':')[1] === category + ) + if (!folder?.children) return [] + return folder.children + .filter((child) => child.type === 'page' && child.url) + .map((child) => child.url!) +} + export async function CategoryIndex({ category, }: { @@ -18,6 +36,18 @@ export async function CategoryIndex({ const pages = source .getPages() .filter((page) => page.slugs[0] === category && page.slugs.length > 1) + + // Sort pages based on meta.json order (reflected in the page tree) + const pageTreeOrder = getPageTreeOrder(category) + if (pageTreeOrder.length > 0) { + const orderMap = new Map(pageTreeOrder.map((url, i) => [url, i])) + pages.sort((a, b) => { + const aIndex = orderMap.get(a.url) ?? Infinity + const bIndex = orderMap.get(b.url) ?? Infinity + return aIndex - bIndex + }) + } + if (category === 'logs') pages.reverse() const typeMap: Record = { components: 'component', diff --git a/components/code-source.tsx b/components/code-source.tsx index 50dbfef3..5a7d9388 100644 --- a/components/code-source.tsx +++ b/components/code-source.tsx @@ -4,7 +4,7 @@ import { highlightCode, getLanguageFromExtension, stripFrontmatter, -} from '@/lib/shiki' +} from '@/lib/mdx' import { CodeBlock } from '@/components/code-block' export async function FileCodeblock({ diff --git a/components/content.tsx b/components/content.tsx new file mode 100644 index 00000000..dd2f3f24 --- /dev/null +++ b/components/content.tsx @@ -0,0 +1,15 @@ +import { Markdown } from 'fumadocs-core/content' +import { getMDXComponents } from '@/mdx-components' +import { rehypePlugins, remarkPlugins } from '@/lib/mdx' + +export function MDXContent({ content }: { content: string }) { + return ( + + {content} + + ) +} diff --git a/components/layout/mobile-nav.tsx b/components/layout/mobile-nav.tsx index 396fb97e..4482c853 100644 --- a/components/layout/mobile-nav.tsx +++ b/components/layout/mobile-nav.tsx @@ -16,6 +16,7 @@ import FileIcon from '@/components/icons/file' import FlaskIcon from '@/components/icons/flask' import type { SidebarItemMeta } from './sidebar/section' import type { Experiment } from '@/lib/lab' +import { useIsTeam } from '@/hooks/use-team-cookie' import { MetaBadge } from '@/components/layout/meta-badge' import { SearchResults } from './sidebar/search-results' import { NoResults } from './sidebar/no-results' @@ -52,9 +53,17 @@ const sectionIcons: Record< export function MobileNav({ tree, - itemMeta = {}, + itemMeta: itemMetaProp = {}, experiments = [], }: MobileNavProps) { + const isTeam = useIsTeam() + const itemMeta = React.useMemo( + () => + isTeam + ? { ...itemMetaProp, '/toolbox/ui': { badge: 'internal' as const } } + : itemMetaProp, + [isTeam, itemMetaProp] + ) const pathname = usePathname() const router = useRouter() const inputRef = React.useRef(null) diff --git a/components/layout/sidebar/index.tsx b/components/layout/sidebar/index.tsx index cddf5a14..b418b414 100644 --- a/components/layout/sidebar/index.tsx +++ b/components/layout/sidebar/index.tsx @@ -13,6 +13,7 @@ import { LabSidebarSection } from './lab-section' import { SocialLinks } from './social-links' import { NavAside } from '../nav-aside' import { useSearch } from '@/hooks/use-search' +import { useIsTeam } from '@/hooks/use-team-cookie' import type { Experiment } from '@/lib/lab' export type { SidebarItemMeta } @@ -23,6 +24,7 @@ type RegistrySidebarProps = { gameSlugs?: string[] effectSlugs?: string[] canvasSlugs?: string[] + librarySlugs?: string[] experiments?: Experiment[] } @@ -32,8 +34,17 @@ export function RegistrySidebar({ gameSlugs = [], effectSlugs = [], canvasSlugs = [], + librarySlugs = [], experiments = [], }: RegistrySidebarProps) { + const isTeam = useIsTeam() + const resolvedMeta = React.useMemo( + () => + isTeam + ? { ...itemMeta, '/toolbox/ui': { badge: 'internal' as const } } + : itemMeta, + [isTeam, itemMeta] + ) const pathname = usePathname() const router = useRouter() const { @@ -106,10 +117,11 @@ export function RegistrySidebar({ ) diff --git a/components/layout/sidebar/section.tsx b/components/layout/sidebar/section.tsx index 09b1e7d8..7659beff 100644 --- a/components/layout/sidebar/section.tsx +++ b/components/layout/sidebar/section.tsx @@ -11,7 +11,7 @@ import FileIcon from '@/components/icons/file' import FlaskIcon from '@/components/icons/flask' import GamepadIcon from '@/components/icons/gamepad' import TextScanIcon from '@/components/icons/text-scan' -import { Frame, Minus, Plus } from 'lucide-react' +import { Frame, Library, Minus, Plus } from 'lucide-react' import { getLogNumber, stripLogPrefixFromTitle } from '@/lib/log-utils' import { MetaBadge } from '@/components/layout/meta-badge' @@ -214,6 +214,7 @@ type SidebarSectionProps = { gameSlugs?: string[] effectSlugs?: string[] canvasSlugs?: string[] + librarySlugs?: string[] } /** @@ -227,6 +228,7 @@ export function SidebarSection({ gameSlugs = [], effectSlugs = [], canvasSlugs = [], + librarySlugs = [], }: SidebarSectionProps) { const [isOpen, setIsOpen] = React.useState(defaultOpen) const pathname = usePathname() @@ -309,7 +311,44 @@ export function SidebarSection({ ) } - // For other sections (Toolbox, Logs), render with collapsible header + // For toolbox section, split into General and Libraries + if (sectionId === 'toolbox' && librarySlugs.length > 0) { + const isLibrary = (url: string) => { + const slug = url.split('/').pop() ?? '' + return librarySlugs.includes(slug) + } + + const pages = folder.children.filter( + (child): child is PageTree.Item => child.type === 'page' + ) + const generalPages = pages.filter((page) => !isLibrary(page.url)) + const libraryPages = pages.filter((page) => isLibrary(page.url)) + + return ( +
+ + {libraryPages.length > 0 && ( + + )} +
+ ) + } + + // For other sections (Logs), render with collapsible header return (
\n \n )\n}\n\ntype ScrollAreaContentProps = React.ComponentProps<'div'>\n\nexport function ScrollAreaContent({\n className,\n children,\n ref,\n ...props\n}: ScrollAreaContentProps) {\n const { scrollRef, orientation } = useScrollAreaContext()\n\n const combinedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n scrollRef.current = node\n if (typeof ref === 'function') ref(node)\n else if (ref)\n (ref as React.RefObject).current = node\n },\n [ref, scrollRef]\n )\n\n const overflowClasses =\n orientation === 'horizontal'\n ? 'overflow-x-auto overflow-y-hidden'\n : 'overflow-y-auto h-full'\n\n const paddingClasses =\n orientation === 'horizontal'\n ? 'pb-2' // Padding bottom para empujar el scrollbar horizontal hacia abajo\n : 'pr-2' // Padding right para empujar el scrollbar vertical hacia afuera\n\n return (\n \n {children}\n \n )\n}\n", + "content": "'use client'\n\nimport * as React from 'react'\nimport { Presence } from '@radix-ui/react-presence'\nimport { cn } from '@/lib/utils'\n\ninterface ScrollAreaContextValue {\n scrollRef: React.RefObject\n hasScroll: boolean\n orientation: 'vertical' | 'horizontal'\n}\n\nconst ScrollAreaContext = React.createContext(\n null\n)\n\nfunction useScrollAreaContext() {\n const ctx = React.useContext(ScrollAreaContext)\n if (!ctx) {\n throw new Error('ScrollArea components must be used within ScrollArea Root')\n }\n return ctx\n}\n\ninterface ScrollAreaViewportProps extends React.ComponentProps<'div'> {\n orientation?: 'vertical' | 'horizontal'\n topShadowGradient?: string\n bottomShadowGradient?: string\n leftShadowGradient?: string\n rightShadowGradient?: string\n}\n\nexport function ScrollAreaViewport({\n className,\n children,\n orientation = 'vertical',\n topShadowGradient,\n bottomShadowGradient,\n leftShadowGradient,\n rightShadowGradient,\n ref,\n ...props\n}: ScrollAreaViewportProps) {\n const scrollRef = React.useRef(null)\n const [hasScrollTop, setHasScrollTop] = React.useState(false)\n const [hasScrollBottom, setHasScrollBottom] = React.useState(false)\n const [hasScrollLeft, setHasScrollLeft] = React.useState(false)\n const [hasScrollRight, setHasScrollRight] = React.useState(false)\n\n const update = React.useCallback(() => {\n const el = scrollRef.current\n if (!el) return\n\n if (orientation === 'vertical') {\n // Vertical scroll detection\n const scrollY = Math.ceil(el.scrollTop)\n const scrollHeight = el.scrollHeight\n const clientHeight = el.clientHeight\n\n const newHasScrollTop = scrollY > 0\n const newHasScrollBottom = scrollY + clientHeight < scrollHeight\n\n setHasScrollTop(newHasScrollTop)\n setHasScrollBottom(newHasScrollBottom)\n } else {\n const scrollX = Math.ceil(el.scrollLeft)\n const scrollWidth = el.scrollWidth\n const clientWidth = el.clientWidth\n\n const newHasScrollLeft = scrollX > 0\n const newHasScrollRight = scrollX + clientWidth < scrollWidth\n\n setHasScrollLeft(newHasScrollLeft)\n setHasScrollRight(newHasScrollRight)\n }\n }, [orientation])\n\n React.useEffect(() => {\n const el = scrollRef.current\n if (!el) return\n\n const handleScroll = () => requestAnimationFrame(update)\n el.addEventListener('scroll', handleScroll, { passive: true })\n\n const ro = new ResizeObserver(update)\n ro.observe(el)\n\n const mo = new MutationObserver(update)\n mo.observe(el, { childList: true })\n\n update()\n\n return () => {\n el.removeEventListener('scroll', handleScroll)\n ro.disconnect()\n mo.disconnect()\n }\n }, [update])\n\n const hasScroll =\n hasScrollTop || hasScrollBottom || hasScrollLeft || hasScrollRight\n\n const ctxValue = React.useMemo(\n () => ({\n scrollRef,\n hasScroll,\n orientation,\n }),\n [hasScroll, orientation]\n )\n\n const shadowBaseClasses =\n 'pointer-events-none absolute z-20 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 duration-300 ease-out'\n\n const shadows =\n orientation === 'vertical'\n ? [\n {\n present: hasScrollTop,\n position: 'left-0 top-0 right-0 h-8',\n slide:\n 'data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2',\n gradient:\n topShadowGradient ||\n 'bg-linear-to-b from-background to-transparent',\n },\n {\n present: hasScrollBottom,\n position: 'left-0 bottom-0 right-0 h-8',\n slide:\n 'data-[state=closed]:slide-out-to-bottom-2 data-[state=open]:slide-in-from-bottom-2',\n gradient:\n bottomShadowGradient ||\n 'bg-linear-to-t from-background to-transparent',\n },\n ]\n : [\n {\n present: hasScrollLeft,\n position: 'inset-y-0 left-0 bottom-0 w-8',\n slide:\n 'data-[state=closed]:slide-out-to-left-2 data-[state=open]:slide-in-from-left-2',\n gradient:\n leftShadowGradient ||\n 'bg-linear-to-r from-background to-transparent',\n },\n {\n present: hasScrollRight,\n position: 'inset-y-0 right-0 bottom-0 w-8',\n slide:\n 'data-[state=closed]:slide-out-to-right-2 data-[state=open]:slide-in-from-right-2',\n gradient:\n rightShadowGradient ||\n 'bg-linear-to-l from-background to-transparent',\n },\n ]\n\n const dataAttributes =\n orientation === 'vertical'\n ? {\n 'data-scroll-top': hasScrollTop,\n 'data-scroll-bottom': hasScrollBottom,\n }\n : {\n 'data-scroll-left': hasScrollLeft,\n 'data-scroll-right': hasScrollRight,\n }\n\n return (\n \n \n {children}\n\n {shadows.map((shadow, index) => (\n \n \n \n ))}\n \n \n )\n}\n\ntype ScrollAreaContentProps = React.ComponentProps<'div'>\n\nexport function ScrollAreaContent({\n className,\n children,\n ref,\n ...props\n}: ScrollAreaContentProps) {\n const { scrollRef, orientation } = useScrollAreaContext()\n\n const combinedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n scrollRef.current = node\n if (typeof ref === 'function') ref(node)\n else if (ref)\n (ref as React.RefObject).current = node\n },\n [ref, scrollRef]\n )\n\n const overflowClasses =\n orientation === 'horizontal'\n ? 'overflow-x-auto overflow-y-hidden'\n : 'overflow-y-auto h-full'\n\n const paddingClasses = orientation === 'horizontal'\n\n return (\n \n {children}\n \n )\n}\n", "type": "registry:component" } ] diff --git a/registry/components/scroll-area.tsx b/registry/components/scroll-area.tsx index 5216315a..5108b91d 100644 --- a/registry/components/scroll-area.tsx +++ b/registry/components/scroll-area.tsx @@ -117,7 +117,7 @@ export function ScrollAreaViewport({ ? [ { present: hasScrollTop, - position: 'left-0 top-0 right-2 h-8', + position: 'left-0 top-0 right-0 h-8', slide: 'data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2', gradient: @@ -126,7 +126,7 @@ export function ScrollAreaViewport({ }, { present: hasScrollBottom, - position: 'left-0 bottom-0 right-2 h-8', + position: 'left-0 bottom-0 right-0 h-8', slide: 'data-[state=closed]:slide-out-to-bottom-2 data-[state=open]:slide-in-from-bottom-2', gradient: @@ -137,7 +137,7 @@ export function ScrollAreaViewport({ : [ { present: hasScrollLeft, - position: 'inset-y-0 left-0 bottom-2 w-8', + position: 'inset-y-0 left-0 bottom-0 w-8', slide: 'data-[state=closed]:slide-out-to-left-2 data-[state=open]:slide-in-from-left-2', gradient: @@ -146,7 +146,7 @@ export function ScrollAreaViewport({ }, { present: hasScrollRight, - position: 'inset-y-0 right-0 bottom-2 w-8', + position: 'inset-y-0 right-0 bottom-0 w-8', slide: 'data-[state=closed]:slide-out-to-right-2 data-[state=open]:slide-in-from-right-2', gradient: @@ -221,10 +221,7 @@ export function ScrollAreaContent({ ? 'overflow-x-auto overflow-y-hidden' : 'overflow-y-auto h-full' - const paddingClasses = - orientation === 'horizontal' - ? 'pb-2' // Padding bottom para empujar el scrollbar horizontal hacia abajo - : 'pr-2' // Padding right para empujar el scrollbar vertical hacia afuera + const paddingClasses = orientation === 'horizontal' return (