Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions apps/docs/app/[lang]/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import Link from 'next/link'
import { notFound } from 'next/navigation'
import { PageNavigationArrows } from '@/components/docs-layout/page-navigation-arrows'
import { TOCFooter } from '@/components/docs-layout/toc-footer'
import { LLMCopyButton } from '@/components/page-actions'
import { StructuredData } from '@/components/structured-data'
import { CodeBlock } from '@/components/ui/code-block'
import { CopyPageButton } from '@/components/ui/copy-page-button'
import { Heading } from '@/components/ui/heading'
import { source } from '@/lib/source'

export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
Expand Down Expand Up @@ -202,7 +203,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
<div className='relative mt-6 sm:mt-0'>
<div className='absolute top-1 right-0 flex items-center gap-2'>
<div className='hidden sm:flex'>
<CopyPageButton markdownUrl={`${page.url}.mdx`} />
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
</div>
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
</div>
Expand All @@ -214,6 +215,12 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
components={{
...defaultMdxComponents,
CodeBlock,
h1: (props) => <Heading as='h1' {...props} />,
h2: (props) => <Heading as='h2' {...props} />,
h3: (props) => <Heading as='h3' {...props} />,
h4: (props) => <Heading as='h4' {...props} />,
h5: (props) => <Heading as='h5' {...props} />,
h6: (props) => <Heading as='h6' {...props} />,
}}
/>
</DocsBody>
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";

/* Prevent overscroll bounce effect on the page */
html,
body {
overscroll-behavior: none;
}

@theme {
--color-fd-primary: #802fff; /* Purple from control-bar component */
--font-geist-sans: var(--font-geist-sans);
Expand Down
12 changes: 11 additions & 1 deletion apps/docs/app/llms.mdx/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { notFound } from 'next/navigation'
import { type NextRequest, NextResponse } from 'next/server'
import { i18n } from '@/lib/i18n'
import { getLLMText } from '@/lib/llms'
import { source } from '@/lib/source'

export const revalidate = false

export async function GET(_req: NextRequest, { params }: { params: Promise<{ slug?: string[] }> }) {
const { slug } = await params
const page = source.getPage(slug)

let lang: (typeof i18n.languages)[number] = i18n.defaultLanguage
let pageSlug = slug

if (slug && slug.length > 0 && i18n.languages.includes(slug[0] as typeof lang)) {
lang = slug[0] as typeof lang
pageSlug = slug.slice(1)
}

const page = source.getPage(pageSlug, lang)
if (!page) notFound()

return new NextResponse(await getLLMText(page), {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,50 @@
'use client'

import { useState } from 'react'
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'
import { Check, Copy } from 'lucide-react'

const cache = new Map<string, string>()

interface CopyPageButtonProps {
export function LLMCopyButton({
markdownUrl,
}: {
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl: string
}

export function CopyPageButton({ markdownUrl }: CopyPageButtonProps) {
const [copied, setCopied] = useState(false)
}) {
const [isLoading, setLoading] = useState(false)

const handleCopy = async () => {
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl)
if (cached) {
await navigator.clipboard.writeText(cached)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
return
}
if (cached) return navigator.clipboard.writeText(cached)

setLoading(true)

try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': fetch(markdownUrl).then(async (res) => {
const content = await res.text()
cache.set(markdownUrl, content)

return content
}),
}),
])
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
} finally {
setLoading(false)
}
Comment on lines 22 to 37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: setLoading(true) is called but never reset to false when using cached content (line 20). This could leave the button in a loading state indefinitely if cache is hit.

Suggested change
setLoading(true)
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': fetch(markdownUrl).then(async (res) => {
const content = await res.text()
cache.set(markdownUrl, content)
return content
}),
}),
])
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
} finally {
setLoading(false)
}
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl)
if (cached) return navigator.clipboard.writeText(cached)
setLoading(true)
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': fetch(markdownUrl).then(async (res) => {
const content = await res.text()
cache.set(markdownUrl, content)
return content
}),
}),
])
} finally {
setLoading(false)
}
})
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/docs/components/page-actions.tsx
Line: 22:37

Comment:
**logic:** `setLoading(true)` is called but never reset to `false` when using cached content (line 20). This could leave the button in a loading state indefinitely if cache is hit.

```suggestion
  const [checked, onClick] = useCopyButton(async () => {
    const cached = cache.get(markdownUrl)
    if (cached) return navigator.clipboard.writeText(cached)

    setLoading(true)

    try {
      await navigator.clipboard.write([
        new ClipboardItem({
          'text/plain': fetch(markdownUrl).then(async (res) => {
            const content = await res.text()
            cache.set(markdownUrl, content)

            return content
          }),
        }),
      ])
    } finally {
      setLoading(false)
    }
  })
```

How can I resolve this? If you propose a fix, please make it concise.

}
})

return (
<button
disabled={isLoading}
onClick={handleCopy}
onClick={onClick}
className='flex cursor-pointer items-center gap-1.5 rounded-lg border border-border/40 bg-background px-2.5 py-2 text-muted-foreground/60 text-sm leading-none transition-all hover:border-border hover:bg-accent/50 hover:text-muted-foreground'
aria-label={copied ? 'Copied to clipboard' : 'Copy page content'}
aria-label={checked ? 'Copied to clipboard' : 'Copy page content'}
>
{copied ? (
{checked ? (
<>
<Check className='h-3.5 w-3.5' />
<span>Copied</span>
Expand Down
58 changes: 58 additions & 0 deletions apps/docs/components/ui/heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client'

import { type ComponentPropsWithoutRef, useState } from 'react'
import { Check, Link } from 'lucide-react'
import { cn } from '@/lib/utils'

type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'

interface HeadingProps extends ComponentPropsWithoutRef<'h1'> {
as?: HeadingTag
}

export function Heading({ as, className, ...props }: HeadingProps) {
const [copied, setCopied] = useState(false)
const As = as ?? 'h1'

if (!props.id) {
return <As className={className} {...props} />
}

const handleClick = async (e: React.MouseEvent) => {
e.preventDefault()

const url = `${window.location.origin}${window.location.pathname}#${props.id}`

try {
await navigator.clipboard.writeText(url)
setCopied(true)

// Update URL hash without scrolling
window.history.pushState(null, '', `#${props.id}`)

setTimeout(() => setCopied(false), 2000)
} catch {
// Fallback: just navigate to the anchor
window.location.hash = props.id as string
}
}

return (
<As className={cn('group flex scroll-m-28 flex-row items-center gap-2', className)} {...props}>
<a data-card='' href={`#${props.id}`} className='peer' onClick={handleClick}>
{props.children}
</a>
{copied ? (
<Check
aria-hidden
className='size-3.5 shrink-0 text-green-500 opacity-100 transition-opacity'
/>
) : (
<Link
aria-hidden
className='size-3.5 shrink-0 text-fd-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 peer-hover:opacity-100'
/>
)}
</As>
)
}
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"@tabler/icons-react": "^3.31.0",
"@vercel/og": "^0.6.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "16.2.3",
"fumadocs-mdx": "14.1.0",
Expand Down
2 changes: 2 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
Expand Down Expand Up @@ -42,6 +43,7 @@
"dependencies": {
"@tabler/icons-react": "^3.31.0",
"@vercel/og": "^0.6.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "16.2.3",
"fumadocs-mdx": "14.1.0",
Expand Down