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
107 changes: 8 additions & 99 deletions src/components/Pagination.astro
Original file line number Diff line number Diff line change
@@ -1,104 +1,13 @@
---
import { Pagination as PaginationReact } from './react/Pagination';

const { dir, pagination } = Astro.locals.starlightRoute;
const { prev, next } = pagination;
const isRtl = dir === 'rtl';
---

<div class="pagination-links print:hidden" dir={dir}>
{
prev && (
<a href={prev.href} rel="prev" class="pagination-pill">
<svg class="pixel-arrow pixel-arrow-left" viewBox="0 0 36 60" fill="currentColor" aria-hidden="true">
<path d="M24 48V36H12v12h12M0 48v12h12V48H0m12-24h12V12H12v12m24 0H24v12h12V24M12 12V0H0v12h12z" />
</svg>
<span class="link-title">{prev.label}</span>
</a>
)
}
{
next && (
<a href={next.href} rel="next" class="pagination-pill">
<span class="link-title">{next.label}</span>
<svg class="pixel-arrow" viewBox="0 0 36 60" fill="currentColor" aria-hidden="true">
<path d="M24 48V36H12v12h12M0 48v12h12V48H0m12-24h12V12H12v12m24 0H24v12h12V24M12 12V0H0v12h12z" />
</svg>
</a>
)
}
</div>

<style>
.pagination-links {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-top: 2rem;
}

.pagination-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--sl-color-gray-5);
border-radius: var(--radius, 0.5rem);
text-decoration: none;
color: var(--sl-color-white);
font-size: 0.9375rem;
font-weight: 500;
transition: all 0.15s ease;
white-space: nowrap;
}

.pagination-pill:hover {
border-color: var(--sl-color-gray-3);
background: var(--sl-color-gray-6);
}

[rel='prev'] {
margin-right: auto;
}

[rel='next'] {
margin-left: auto;
}

.link-title {
line-height: 1;
}

.pixel-arrow {
width: 0.625rem;
height: 0.625rem;
flex-shrink: 0;
opacity: 0.6;
transition: opacity 0.15s ease, transform 0.15s ease;
}

.pixel-arrow-left {
transform: scaleX(-1);
}

.pagination-pill:hover .pixel-arrow {
opacity: 1;
}

[rel='prev']:hover .pixel-arrow {
transform: scaleX(-1) translateX(2px);
}

[rel='next']:hover .pixel-arrow {
transform: translateX(2px);
}

:global([data-theme='light']) .pagination-pill {
color: var(--sl-color-gray-1);
border-color: var(--sl-color-gray-4);
}

:global([data-theme='light']) .pagination-pill:hover {
border-color: var(--sl-color-gray-3);
background: var(--sl-color-gray-6);
}
</style>
<PaginationReact
client:load
prev={prev ? { href: prev.href, label: prev.label } : undefined}
next={next ? { href: next.href, label: next.label } : undefined}
dir={dir}
/>
90 changes: 72 additions & 18 deletions src/components/react/CopyPageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ interface CopyPageButtonProps {

// ChatGPT logo
const ChatGPTIcon = () => (
<svg viewBox="0 0 24 24" className="size-4" fill="currentColor">
<svg
viewBox="0 0 24 24"
className="size-4"
fill="currentColor"
aria-hidden="true"
>
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.896zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />
</svg>
);
Expand All @@ -33,7 +38,12 @@ const ClaudeIcon = () => (

// Markdown logo
const MarkdownIcon = () => (
<svg viewBox="0 0 208 128" className="size-4" fill="currentColor">
<svg
viewBox="0 0 208 128"
className="size-4"
fill="currentColor"
aria-hidden="true"
>
<rect
width="198"
height="118"
Expand All @@ -53,6 +63,11 @@ const CursorIcon = () => (
<img src="/cursor.svg" alt="" className="size-4 dark:invert" />
);

// Visually hidden text for screen readers
const VisuallyHidden = ({ children }: { children: React.ReactNode }) => (
<span className="sr-only">{children}</span>
);

export function CopyPageButton({
pageMarkdown,
pageUrl,
Expand Down Expand Up @@ -106,15 +121,30 @@ export function CopyPageButton({
size="sm"
className="gap-1 !text-xs h-7"
onClick={handleCopy}
aria-label={copied ? 'Copied to clipboard' : 'Copy page to clipboard'}
>
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
<span className="hidden sm:inline">Copy page</span>
{copied ? (
<Check className="size-3" aria-hidden="true" />
) : (
<Copy className="size-3" aria-hidden="true" />
)}
<span className="hidden sm:inline" aria-hidden="true">
Copy page
</span>
<span className="sr-only" aria-live="polite">
{copied ? 'Copied to clipboard' : ''}
</span>
</Button>
<ButtonGroupSeparator />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="px-1.5 h-7">
<Ellipsis className="size-3" />
<Button
variant="outline"
size="sm"
className="px-1.5 h-7"
aria-label="More options"
>
<Ellipsis className="size-3" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
Expand All @@ -124,10 +154,16 @@ export function CopyPageButton({
>
<div className="flex items-center gap-2">
<ChatGPTIcon />
<span className="font-medium">Open in ChatGPT</span>
<ArrowUpRight className="size-3 opacity-50" />
<span className="font-medium">
Open in ChatGPT
<VisuallyHidden> (opens in new tab)</VisuallyHidden>
</span>
<ArrowUpRight className="size-3 opacity-50" aria-hidden="true" />
</div>
<span className="text-xs text-muted-foreground pl-6">
<span
className="text-xs text-muted-foreground pl-6"
aria-hidden="true"
>
Ask questions about this page
</span>
</DropdownMenuItem>
Expand All @@ -137,10 +173,16 @@ export function CopyPageButton({
>
<div className="flex items-center gap-2">
<ClaudeIcon />
<span className="font-medium">Open in Claude</span>
<ArrowUpRight className="size-3 opacity-50" />
<span className="font-medium">
Open in Claude
<VisuallyHidden> (opens in new tab)</VisuallyHidden>
</span>
<ArrowUpRight className="size-3 opacity-50" aria-hidden="true" />
</div>
<span className="text-xs text-muted-foreground pl-6">
<span
className="text-xs text-muted-foreground pl-6"
aria-hidden="true"
>
Ask questions about this page
</span>
</DropdownMenuItem>
Expand All @@ -150,10 +192,16 @@ export function CopyPageButton({
>
<div className="flex items-center gap-2">
<CursorIcon />
<span className="font-medium">Open in Cursor</span>
<ArrowUpRight className="size-3 opacity-50" />
<span className="font-medium">
Open in Cursor
<VisuallyHidden> (opens in Cursor app)</VisuallyHidden>
</span>
<ArrowUpRight className="size-3 opacity-50" aria-hidden="true" />
</div>
<span className="text-xs text-muted-foreground pl-6">
<span
className="text-xs text-muted-foreground pl-6"
aria-hidden="true"
>
Open page context in Cursor
</span>
</DropdownMenuItem>
Expand All @@ -164,10 +212,16 @@ export function CopyPageButton({
>
<div className="flex items-center gap-2">
<MarkdownIcon />
<span className="font-medium">View as Markdown</span>
<ArrowUpRight className="size-3 opacity-50" />
<span className="font-medium">
View as Markdown
<VisuallyHidden> (opens in new tab)</VisuallyHidden>
</span>
<ArrowUpRight className="size-3 opacity-50" aria-hidden="true" />
</div>
<span className="text-xs text-muted-foreground pl-6">
<span
className="text-xs text-muted-foreground pl-6"
aria-hidden="true"
>
Open raw markdown in new tab
</span>
</DropdownMenuItem>
Expand Down
55 changes: 55 additions & 0 deletions src/components/react/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Button } from '@/components/ui/button';

interface PaginationLink {
href: string;
label: string;
}

interface PaginationProps {
prev?: PaginationLink;
next?: PaginationLink;
dir?: 'ltr' | 'rtl';
}

const PixelArrow = ({
direction = 'right',
}: {
direction?: 'left' | 'right';
}) => (
<svg
className={`size-2.5 shrink-0 opacity-60 transition-transform group-hover:translate-x-0.5 ${
direction === 'left' ? '-scale-x-100 group-hover:-translate-x-0.5' : ''
}`}
viewBox="0 0 36 60"
fill="currentColor"
aria-hidden="true"
>
<path d="M24 48V36H12v12h12M0 48v12h12V48H0m12-24h12V12H12v12m24 0H24v12h12V24M12 12V0H0v12h12z" />
</svg>
);

export function Pagination({ prev, next, dir = 'ltr' }: PaginationProps) {
return (
<div
className="flex justify-between items-center gap-4 mt-8 print:hidden"
dir={dir}
>
{prev && (
<Button variant="outline" size="sm" asChild className="group mr-auto">
<a href={prev.href} rel="prev">
<PixelArrow direction="left" />
<span className="leading-none">{prev.label}</span>
</a>
</Button>
)}
{next && (
<Button variant="outline" size="sm" asChild className="group ml-auto">
<a href={next.href} rel="next">
<span className="leading-none">{next.label}</span>
<PixelArrow direction="right" />
</a>
</Button>
)}
</div>
);
}
1 change: 1 addition & 0 deletions src/components/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export { CopyPageButton } from './CopyPageButton';
export { DotPattern } from './DotPattern';
export { LifecycleDiagram } from './LifecycleDiagram';
export { default as LinkCard } from './LinkCard';
export { Pagination } from './Pagination';
export { Param, ParamInline, ParamTable } from './ParamTable';
export { PricingRates } from './PricingRates';
export { SearchDialog } from './SearchDialog';
Expand Down
1 change: 1 addition & 0 deletions src/styles/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ body.search-dialog-open .page {
[data-theme='dark'] body:has(.page)::after {
bottom: 0;
mask-image: linear-gradient(to top right, rgb(0 0 0 / 1), rgb(0 0 0 / 0) 20%);
opacity: 0.6;
}
}

Expand Down