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
29 changes: 29 additions & 0 deletions cypress/e2e/features/link-cards.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
describe('LinkCards', () => {
beforeEach(() => {
cy.visit('/quickstart');
});

it('should render cards with icon, title, and description', () => {
cy.get('.grid a.no-underline')
.first()
.within(() => {
cy.get('svg').should('exist');
cy.get('h3').should('exist').and('not.be.empty');
cy.get('p').should('exist').and('not.be.empty');
});
});

it('should have valid navigation links', () => {
cy.get('.grid a.no-underline')
.first()
.then(($link) => {
const href = $link.attr('href');
// Verify href exists and is an internal link
expect(href).to.match(/^\//);
// Verify linked page exists (returns 200)
cy.request(href as string)
.its('status')
.should('eq', 200);
});
});
});
12 changes: 12 additions & 0 deletions src/components/CardGrid.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
interface Props {
cols?: 2 | 3;
}

const { cols = 2 } = Astro.props;
const gridCols = cols === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2';
---

<div class={`grid grid-cols-1 ${gridCols} gap-4 not-content`}>
<slot />
</div>
25 changes: 17 additions & 8 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Default from '@astrojs/starlight/components/Header.astro';
link.className = 'dashboard-link';
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.innerHTML = `Dashboard <svg class="external-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z" clip-rule="evenodd" /></svg>`;
link.innerHTML = `Dashboard <svg class="pixel-arrow" viewBox="0 0 36 60" fill="currentColor"><path d="M24 48V36H12v12h12M0 48v12h12V48H0m12-24h12V12H12v12m24 0H24v12h12V24M12 12V0H0v12h12z" /></svg>`;
rightGroup.appendChild(link);
}
}
Expand Down Expand Up @@ -64,17 +64,26 @@ import Default from '@astrojs/starlight/components/Header.astro';
letter-spacing: -0.01em;
color: white;
text-decoration: none;
background-color: #16a34a;
border-radius: 0.25rem;
background: linear-gradient(180deg, #22c55e 0%, #16a34a 100%);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 0.375rem;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}

.dashboard-link:hover {
background-color: #22c55e;
background: linear-gradient(180deg, #2dd464 0%, #1eb854 100%);
}

.dashboard-link .external-icon {
width: 0.875rem;
height: 0.875rem;
opacity: 0.8;
.dashboard-link .pixel-arrow {
width: 0.625rem;
height: 0.625rem;
opacity: 0.6;
transition: opacity 0.15s ease;
}

.dashboard-link:hover .pixel-arrow {
opacity: 1;
}
</style>
15 changes: 15 additions & 0 deletions src/components/LinkCard.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
import { LinkCard as LinkCardReact } from './react';

interface Props {
href: string;
title: string;
description: string;
icon?: string;
external?: boolean;
}

const props = Astro.props;
---

<LinkCardReact {...props} client:load />
71 changes: 71 additions & 0 deletions src/components/SpotlightCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type React from 'react';
import { useRef, useState } from 'react';

interface Position {
x: number;
y: number;
}

interface SpotlightCardProps extends React.PropsWithChildren {
className?: string;
spotlightColor?: string;
}

const SpotlightCard: React.FC<SpotlightCardProps> = ({
children,
className = '',
spotlightColor = 'rgba(255, 255, 255, 0.25)',
}) => {
const divRef = useRef<HTMLDivElement>(null);
const [isFocused, setIsFocused] = useState<boolean>(false);
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
const [opacity, setOpacity] = useState<number>(0);

const handleMouseMove: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!divRef.current || isFocused) return;

const rect = divRef.current.getBoundingClientRect();
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
};

const handleFocus = () => {
setIsFocused(true);
setOpacity(0.6);
};

const handleBlur = () => {
setIsFocused(false);
setOpacity(0);
};

const handleMouseEnter = () => {
setOpacity(0.6);
};

const handleMouseLeave = () => {
setOpacity(0);
};

return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`relative rounded-3xl border border-border bg-card overflow-hidden p-8 ${className}`}
>
<div
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-500 ease-in-out"
style={{
opacity,
background: `radial-gradient(circle at ${position.x}px ${position.y}px, ${spotlightColor}, transparent 80%)`,
}}
/>
{children}
</div>
);
};

export default SpotlightCard;
18 changes: 18 additions & 0 deletions src/components/react/CardGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type React from 'react';

interface CardGridProps {
children: React.ReactNode;
cols?: 2 | 3;
}

const CardGrid: React.FC<CardGridProps> = ({ children, cols = 2 }) => {
const gridCols = cols === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2';

return (
<div className={`grid grid-cols-1 ${gridCols} gap-4 not-content`}>
{children}
</div>
);
};

export default CardGrid;
95 changes: 95 additions & 0 deletions src/components/react/LinkCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
ArrowUpRight,
BookOpen,
Code,
Cog,
FileText,
Folder,
HelpCircle,
Layers,
LifeBuoy,
type LucideIcon,
Play,
Rocket,
Settings,
Terminal,
Wrench,
Zap,
} from 'lucide-react';
import type React from 'react';
import SpotlightCard from '../SpotlightCard';

const iconMap: Record<string, LucideIcon> = {
'arrow-up-right': ArrowUpRight,
'book-open': BookOpen,
code: Code,
cog: Cog,
'file-text': FileText,
folder: Folder,
'help-circle': HelpCircle,
layers: Layers,
'life-buoy': LifeBuoy,
play: Play,
rocket: Rocket,
settings: Settings,
terminal: Terminal,
wrench: Wrench,
zap: Zap,
};

interface LinkCardProps {
href: string;
title: string;
description: string;
icon?: keyof typeof iconMap;
external?: boolean;
}

const LinkCard: React.FC<LinkCardProps> = ({
href,
title,
description,
icon,
external = false,
}) => {
const isExternal = external || href.startsWith('http');
const IconComponent = icon ? iconMap[icon] : null;

return (
<a
href={href}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
className="block no-underline group"
>
<SpotlightCard
className="h-full"
spotlightColor="color-mix(in oklch, var(--primary) 15%, transparent)"
>
<div className="flex flex-col gap-3">
{IconComponent && (
<div className="text-primary">
<IconComponent size={28} strokeWidth={1.5} />
</div>
)}
<div className="flex items-start justify-between gap-2">
<h3 className="text-lg font-semibold text-card-foreground m-0 group-hover:text-primary transition-colors">
{title}
</h3>
{isExternal && (
<ArrowUpRight
size={16}
className="text-muted-foreground flex-shrink-0 mt-1"
/>
)}
</div>
<p className="text-muted-foreground text-sm m-0 leading-relaxed">
{description}
</p>
</div>
</SpotlightCard>
</a>
);
};

export default LinkCard;
2 changes: 2 additions & 0 deletions src/components/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ export { BillingCalculator } from './BillingCalculator';
export { BillingFAQ } from './BillingFAQ';
export { ContentBreadcrumbs } from './Breadcrumbs';
export { Callout } from './Callout';
export { default as CardGrid } from './CardGrid';
export { CodeTabs, Snippet } from './CodeTabs';
export { CopyPageButton } from './CopyPageButton';
export { DotPattern } from './DotPattern';
export { LifecycleDiagram } from './LifecycleDiagram';
export { default as LinkCard } from './LinkCard';
export { Param, ParamInline, ParamTable } from './ParamTable';
export { PricingRates } from './PricingRates';
export { SearchDialog } from './SearchDialog';
Expand Down
35 changes: 31 additions & 4 deletions src/content/docs/api/rest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: HTTP API for managing Sprites programmatically
---

import APIEndpoint from '@/components/APIEndpoint.astro';
import { Callout, ParamTable, Param, StatusCodes, StatusBadge, APIBody } from '@/components/react';
import { Callout, ParamTable, Param, StatusCodes, StatusBadge, APIBody, LinkCard, CardGrid } from '@/components/react';

The Sprites REST API allows you to manage Sprites programmatically via HTTP requests.

Expand Down Expand Up @@ -351,6 +351,33 @@ done

## Related Documentation

- [JavaScript SDK](/sdks/javascript) - Programmatic access
- [Go SDK](/sdks/go) - Native Go client
- [CLI Reference](/cli/commands) - Command-line interface
<CardGrid client:load>
<LinkCard
href="/sdks/javascript"
title="JavaScript SDK"
description="Programmatic access with JavaScript and TypeScript"
icon="code"
client:load
/>
<LinkCard
href="/sdks/go"
title="Go SDK"
description="Native Go client for the Sprites API"
icon="code"
client:load
/>
<LinkCard
href="/cli/commands"
title="CLI Reference"
description="Command-line interface documentation"
icon="terminal"
client:load
/>
<LinkCard
href="/sprites"
title="Sprites Guide"
description="Comprehensive guide to working with Sprites"
icon="book-open"
client:load
/>
</CardGrid>
20 changes: 18 additions & 2 deletions src/content/docs/cli/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ title: CLI Authentication
description: Authenticate the Sprites CLI with your Fly.io account
---

import { LinkCard, CardGrid } from '@/components/react';

Sprites uses your Fly.io account for authentication. This guide covers setting up authentication and managing API tokens.

## Quick Setup
Expand Down Expand Up @@ -239,5 +241,19 @@ If you get permission errors after authentication:

## Next Steps

- [Commands Reference](/cli/commands) - Full CLI documentation
- [Quickstart](/quickstart) - Create your first Sprite
<CardGrid client:load>
<LinkCard
href="/cli/commands"
title="Commands Reference"
description="Full CLI documentation and command examples"
icon="terminal"
client:load
/>
<LinkCard
href="/quickstart"
title="Quickstart"
description="Create your first Sprite in minutes"
icon="rocket"
client:load
/>
</CardGrid>
Loading
Loading