diff --git a/cypress/e2e/features/link-cards.cy.ts b/cypress/e2e/features/link-cards.cy.ts new file mode 100644 index 0000000..2c0b4f1 --- /dev/null +++ b/cypress/e2e/features/link-cards.cy.ts @@ -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); + }); + }); +}); diff --git a/src/components/CardGrid.astro b/src/components/CardGrid.astro new file mode 100644 index 0000000..2639bc7 --- /dev/null +++ b/src/components/CardGrid.astro @@ -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'; +--- + +
+ +
diff --git a/src/components/Header.astro b/src/components/Header.astro index db287e1..0ac3d99 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -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 `; + link.innerHTML = `Dashboard `; rightGroup.appendChild(link); } } @@ -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; } diff --git a/src/components/LinkCard.astro b/src/components/LinkCard.astro new file mode 100644 index 0000000..23a3c0f --- /dev/null +++ b/src/components/LinkCard.astro @@ -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; +--- + + diff --git a/src/components/SpotlightCard.tsx b/src/components/SpotlightCard.tsx new file mode 100644 index 0000000..fa6f9a0 --- /dev/null +++ b/src/components/SpotlightCard.tsx @@ -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 = ({ + children, + className = '', + spotlightColor = 'rgba(255, 255, 255, 0.25)', +}) => { + const divRef = useRef(null); + const [isFocused, setIsFocused] = useState(false); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [opacity, setOpacity] = useState(0); + + const handleMouseMove: React.MouseEventHandler = (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 ( +
+
+ {children} +
+ ); +}; + +export default SpotlightCard; diff --git a/src/components/react/CardGrid.tsx b/src/components/react/CardGrid.tsx new file mode 100644 index 0000000..8ccddad --- /dev/null +++ b/src/components/react/CardGrid.tsx @@ -0,0 +1,18 @@ +import type React from 'react'; + +interface CardGridProps { + children: React.ReactNode; + cols?: 2 | 3; +} + +const CardGrid: React.FC = ({ children, cols = 2 }) => { + const gridCols = cols === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2'; + + return ( +
+ {children} +
+ ); +}; + +export default CardGrid; diff --git a/src/components/react/LinkCard.tsx b/src/components/react/LinkCard.tsx new file mode 100644 index 0000000..69b58b5 --- /dev/null +++ b/src/components/react/LinkCard.tsx @@ -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 = { + '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 = ({ + href, + title, + description, + icon, + external = false, +}) => { + const isExternal = external || href.startsWith('http'); + const IconComponent = icon ? iconMap[icon] : null; + + return ( + + +
+ {IconComponent && ( +
+ +
+ )} +
+

+ {title} +

+ {isExternal && ( + + )} +
+

+ {description} +

+
+
+
+ ); +}; + +export default LinkCard; diff --git a/src/components/react/index.ts b/src/components/react/index.ts index 1f3c4a2..d2c1a64 100644 --- a/src/components/react/index.ts +++ b/src/components/react/index.ts @@ -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'; diff --git a/src/content/docs/api/rest.mdx b/src/content/docs/api/rest.mdx index ba6fbb0..b00292a 100644 --- a/src/content/docs/api/rest.mdx +++ b/src/content/docs/api/rest.mdx @@ -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. @@ -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 + + + + + + diff --git a/src/content/docs/cli/authentication.mdx b/src/content/docs/cli/authentication.mdx index fb39a41..6ea8e59 100644 --- a/src/content/docs/cli/authentication.mdx +++ b/src/content/docs/cli/authentication.mdx @@ -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 @@ -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 + + + + diff --git a/src/content/docs/cli/commands.mdx b/src/content/docs/cli/commands.mdx index ddd2106..785bf32 100644 --- a/src/content/docs/cli/commands.mdx +++ b/src/content/docs/cli/commands.mdx @@ -3,7 +3,7 @@ title: CLI Commands Reference description: Complete reference for all Sprites CLI commands --- -import { Callout } from '@/components/react'; +import { Callout, LinkCard, CardGrid } from '@/components/react'; Complete reference for all `sprite` CLI commands. @@ -447,3 +447,36 @@ sprite exec "python experiment.py" # Restore to clean state sprite restore ``` + +## Related Documentation + + + + + + + diff --git a/src/content/docs/cli/installation.mdx b/src/content/docs/cli/installation.mdx index 6d69d51..a121b36 100644 --- a/src/content/docs/cli/installation.mdx +++ b/src/content/docs/cli/installation.mdx @@ -3,7 +3,7 @@ title: CLI Installation description: Install the Sprites CLI on macOS, Linux, or Windows --- -import { Callout } from '@/components/react'; +import { Callout, LinkCard, CardGrid } from '@/components/react'; The Sprites CLI (`sprite`) is available for macOS, Linux, and Windows. @@ -161,5 +161,19 @@ xattr -d com.apple.quarantine /usr/local/bin/sprite ## Next Steps -- [Authentication](/cli/authentication) - Set up your Fly.io account -- [Commands Reference](/cli/commands) - Full CLI documentation + + + + diff --git a/src/content/docs/concepts/checkpoints.mdx b/src/content/docs/concepts/checkpoints.mdx index d099761..c749009 100644 --- a/src/content/docs/concepts/checkpoints.mdx +++ b/src/content/docs/concepts/checkpoints.mdx @@ -4,7 +4,7 @@ description: Save and restore Sprite state with checkpoints --- import { Tabs, TabItem } from '@astrojs/starlight/components'; -import { Snippet } from '@/components/react'; +import { Snippet, LinkCard, CardGrid } from '@/components/react'; Checkpoints allow you to save the complete state of a Sprite and restore it later. This is useful for creating clean states, recovering from errors, or sharing environment configurations. @@ -563,6 +563,33 @@ sprite exec -detachable "npm run dev" # Restart services ## Related Documentation -- [Lifecycle and Persistence](/concepts/lifecycle) - How persistence works -- [CLI Commands](/cli/commands) - Checkpoint commands -- [Billing](/reference/billing) - Storage costs + + + + + + diff --git a/src/content/docs/concepts/lifecycle.mdx b/src/content/docs/concepts/lifecycle.mdx index 87342bb..a3d6ec5 100644 --- a/src/content/docs/concepts/lifecycle.mdx +++ b/src/content/docs/concepts/lifecycle.mdx @@ -4,7 +4,7 @@ description: Understanding Sprite hibernation, persistence, and state management --- import LifecycleDiagram from '@/components/LifecycleDiagram.astro'; -import { Callout } from '@/components/react'; +import { Callout, LinkCard, CardGrid } from '@/components/react'; This page is about what happens to a Sprite after you create it. Where it lives, how it sleeps, when it wakes up, and what it remembers. @@ -128,7 +128,33 @@ If you're running something where latency matters and you want to keep a Sprite ## Related Documentation -- [Services](/concepts/services/) — Run persistent processes that restart automatically on resume. -- [Checkpoints](/concepts/checkpoints) - Save and restore state -- [Configuration](/reference/configuration) - All configuration options -- [Billing](/reference/billing) - Pricing details + + + + + + diff --git a/src/content/docs/concepts/networking.mdx b/src/content/docs/concepts/networking.mdx index c123e78..1be32d8 100644 --- a/src/content/docs/concepts/networking.mdx +++ b/src/content/docs/concepts/networking.mdx @@ -4,7 +4,7 @@ description: HTTP access, URLs, port forwarding, and network configuration --- import { Tabs, TabItem } from '@astrojs/starlight/components'; -import { Snippet } from '@/components/react'; +import { Snippet, LinkCard, CardGrid } from '@/components/react'; Sprites run your code in the cloud. This page is about how to talk to that code via HTTPS URLs and port forwarding that make remote services feel local. You get a public endpoint to hit your app over the Internet, and the ability to proxy ports straight to your laptop so you can test, debug, and connect tools like you're running everything on localhost. @@ -204,6 +204,33 @@ We don't add firewall rules or block inbound traffic to forwarded ports, but we ## Related Documentation -- [Working with Sprites](/working-with-sprites) - Beyond the basics guide to using Sprites -- [CLI Commands](/cli/commands) - Command reference -- [Configuration](/reference/configuration) - Network settings \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/src/content/docs/concepts/services.mdx b/src/content/docs/concepts/services.mdx index d9d7ce1..43975a6 100644 --- a/src/content/docs/concepts/services.mdx +++ b/src/content/docs/concepts/services.mdx @@ -3,6 +3,8 @@ title: Services description: Long-running processes that persist across Sprite reboots --- +import { LinkCard, CardGrid } from '@/components/react'; + Services are long-running processes managed by the Sprite runtime that automatically restart when your Sprite boots. Unlike detachable sessions, services survive full Sprite restarts and are ideal for background daemons, development servers, and databases. ## Overview @@ -238,6 +240,33 @@ tail -f /var/log/syslog ## Related Documentation -- [Lifecycle and Persistence](/concepts/lifecycle) - Sprite lifecycle overview -- [Checkpoints](/concepts/checkpoints) - Save and restore state -- [Base Images](/reference/base-images) - Pre-installed tools + + + + + + diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index 0774ead..67ef378 100644 --- a/src/content/docs/index.mdx +++ b/src/content/docs/index.mdx @@ -4,6 +4,8 @@ description: Persistent, hardware-isolated execution environments for arbitrary tableOfContents: false --- +import { LinkCard, CardGrid } from '@/components/react'; + **Sprites are persistent, hardware-isolated Linux environments for running arbitrary code.** It's like having a small, stateful computer you can spin up on demand. Unlike serverless functions, Sprites keep their filesystem and memory between runs. They're useful for things like AI agents, persistent development environments, or user-submitted code. ## What makes Sprites different? @@ -42,7 +44,33 @@ Every Sprite gets a unique URL, making it easy to expose web services or APIs ru ## Get Started -- **[Quickstart](/quickstart)** - Install the CLI, create your first Sprite, and learn about all the features -- **[JavaScript SDK](/sdks/javascript)** - Build with Sprites in Node.js applications -- **[Go SDK](/sdks/go)** - Use the native Go client mirroring os/exec -- **[CLI Reference](/cli/commands)** -Review the complete command documentation + + + + + + diff --git a/src/content/docs/quickstart.mdx b/src/content/docs/quickstart.mdx index 038d868..cff8ea6 100644 --- a/src/content/docs/quickstart.mdx +++ b/src/content/docs/quickstart.mdx @@ -4,7 +4,7 @@ description: Get up and running with Sprites in 5 minutes --- import { Tabs, TabItem } from '@astrojs/starlight/components'; -import { Callout } from '@/components/react'; +import { Callout, LinkCard, CardGrid } from '@/components/react'; This guide shows you how to install the CLI, create your first Sprite, and start running commands. By the end, you'll have a working environment that sticks around between runs, serves traffic over HTTP, and wakes up automatically when you need it. @@ -183,7 +183,34 @@ func main() { ## Next Steps -- **[Working with Sprites](/working-with-sprites)** - Find out about sessions, ports, persistence, and everything beyond the basics -- **[Core concepts: Lifecycle](/concepts/lifecycle)** - Take a deeper dive into Sprites core concepts, beginning with Lifecycle and Persistence -- **[Billing and Cost Optimization](/reference/billing)** - Learn how Sprites are billed, estimate your costs, and try cost optimization strategies + + + + + + diff --git a/src/content/docs/reference/base-images.mdx b/src/content/docs/reference/base-images.mdx index 985ff6c..e7715a4 100644 --- a/src/content/docs/reference/base-images.mdx +++ b/src/content/docs/reference/base-images.mdx @@ -3,6 +3,8 @@ title: Base Images description: Pre-configured development environments for Sprites --- +import { LinkCard, CardGrid } from '@/components/react'; + Sprites run on pre-configured base images that include common development tools, language runtimes, and AI coding agents. The base image is upgraded automatically without affecting your overlay filesystem. ## Ubuntu DevTools (Default) @@ -382,3 +384,36 @@ cat /.sprite/policy/network.json ``` Network policies must be updated externally via the Sprite API—the container cannot modify its own policy. See [Networking](/concepts/networking) for details. + +## Related Documentation + + + + + + + diff --git a/src/content/docs/reference/billing.mdx b/src/content/docs/reference/billing.mdx index 750ef0f..c7b5a32 100644 --- a/src/content/docs/reference/billing.mdx +++ b/src/content/docs/reference/billing.mdx @@ -6,6 +6,7 @@ description: Sprites pricing and billing details import BillingCalculator from '@/components/BillingCalculator.astro'; import BillingFAQ from '@/components/BillingFAQ.astro'; import PricingRates from '@/components/PricingRates.astro'; +import { LinkCard, CardGrid } from '@/components/react'; Sprites offers flexible billing: **pay-as-you-go** for usage-based pricing with no commitment, or a **subscription plan** with included credits for predictable costs. @@ -135,6 +136,33 @@ Usage and billing information is available through your Fly.io dashboard and org ## Related Documentation -- [Lifecycle](/concepts/lifecycle) - Hibernation behavior -- [Configuration](/reference/configuration) - Resource options -- [Checkpoints](/concepts/checkpoints) - Checkpoint storage + + + + + + diff --git a/src/content/docs/reference/configuration.mdx b/src/content/docs/reference/configuration.mdx index c19d093..e729e6d 100644 --- a/src/content/docs/reference/configuration.mdx +++ b/src/content/docs/reference/configuration.mdx @@ -3,6 +3,8 @@ title: Configuration Reference description: All configuration options for Sprites --- +import { LinkCard, CardGrid } from '@/components/react'; + This page documents all configuration options available for Sprites, including environment settings and client configuration. ## URL Settings @@ -251,6 +253,33 @@ cmd := sprite.CommandContext(ctx, "long-command") ## Related Documentation -- [Lifecycle](/concepts/lifecycle) - Hibernation and persistence -- [Billing](/reference/billing) - Pricing details -- [CLI Commands](/cli/commands) - Full CLI reference + + + + + + diff --git a/src/content/docs/sdks/go.mdx b/src/content/docs/sdks/go.mdx index fc49bf8..92097f6 100644 --- a/src/content/docs/sdks/go.mdx +++ b/src/content/docs/sdks/go.mdx @@ -3,6 +3,8 @@ title: Go SDK description: Use Sprites programmatically with the Go SDK --- +import { LinkCard, CardGrid } from '@/components/react'; + The Sprites Go SDK provides an idiomatic Go interface for managing Sprites programmatically. The SDK mirrors the `os/exec` package API, making it a near drop-in replacement for local command execution. ## Installation @@ -522,6 +524,33 @@ func main() { ## Related Documentation -- [Sprites Guide](/sprites) - Comprehensive guide -- [JavaScript SDK](/sdks/javascript) - JavaScript SDK reference -- [REST API](/api/rest) - HTTP API reference + + + + + + diff --git a/src/content/docs/sdks/javascript.mdx b/src/content/docs/sdks/javascript.mdx index adc98bf..b94d75d 100644 --- a/src/content/docs/sdks/javascript.mdx +++ b/src/content/docs/sdks/javascript.mdx @@ -3,6 +3,8 @@ title: JavaScript SDK description: Use Sprites programmatically with the JavaScript/TypeScript SDK --- +import { LinkCard, CardGrid } from '@/components/react'; + The Sprites JavaScript SDK (`@fly/sprites`) provides a Node.js interface for managing Sprites programmatically. The SDK mirrors the Node.js `child_process` API, making it familiar and easy to use. ## Installation @@ -400,6 +402,33 @@ main(); ## Related Documentation -- [Sprites Guide](/sprites) - Comprehensive guide -- [Go SDK](/sdks/go) - Go SDK reference -- [REST API](/api/rest) - HTTP API reference + + + + + + diff --git a/src/content/docs/sprites.mdx b/src/content/docs/sprites.mdx index 6022e9f..902e0a3 100644 --- a/src/content/docs/sprites.mdx +++ b/src/content/docs/sprites.mdx @@ -4,7 +4,7 @@ description: Create and manage persistent execution environments programmaticall --- import { Tabs, TabItem } from '@astrojs/starlight/components'; -import { Snippet } from '@/components/react'; +import { Snippet, LinkCard, CardGrid } from '@/components/react'; Sprites provides a direct interface for defining containers at runtime and securely running arbitrary code inside them. @@ -630,9 +630,47 @@ err := client.DeleteSprite(ctx, "my-sprite") ## Related Documentation -- [Lifecycle and Persistence](/concepts/lifecycle) -- [Networking and URLs](/concepts/networking) -- [Checkpoints](/concepts/checkpoints) -- [JavaScript SDK Reference](/sdks/javascript) -- [Go SDK Reference](/sdks/go) -- [CLI Reference](/cli/commands) + + + + + + + + diff --git a/src/content/docs/working-with-sprites.mdx b/src/content/docs/working-with-sprites.mdx index c64a1a5..5c0ceb9 100644 --- a/src/content/docs/working-with-sprites.mdx +++ b/src/content/docs/working-with-sprites.mdx @@ -4,7 +4,7 @@ description: Beyond the basics, sessions, ports, persistence, checkpoints --- import { Tabs, TabItem } from '@astrojs/starlight/components'; -import { Callout } from '@/components/react'; +import { Callout, LinkCard, CardGrid } from '@/components/react'; After you've made it through the Quickstart, you've got a working Sprite and a sense of how to use it. This page covers what comes next: how Sprites behave over time, how to run persistent processes, how networking works, and how to shape the environment to fit your stack. Use it as a reference as you start building more with Sprites. @@ -587,8 +587,47 @@ sc my-sprite # Mounts and cd's to the sprite's filesystem ## Next Steps -- **[CLI Reference](/cli/commands)** - Full command documentation -- **[JavaScript SDK](/sdks/javascript)** - Complete SDK reference -- **[Go SDK](/sdks/go)** - Native Go integration -- **[Lifecycle & Persistence](/concepts/lifecycle)** - Deep dive into how Sprites work -- **[Networking](/concepts/networking)** - URLs, ports, and connectivity + + + + + + + + diff --git a/src/pages/[...slug].md.ts b/src/pages/[...slug].md.ts index ee59d4a..bd00b62 100644 --- a/src/pages/[...slug].md.ts +++ b/src/pages/[...slug].md.ts @@ -34,6 +34,22 @@ function cleanMdxContent(content: string): string { return results.length > 0 ? results.join('\n\n') : ''; }); + // Process CardGrid/LinkCard components - convert to markdown links + content = content.replace(/]*>[\s\S]*?<\/CardGrid>/g, (match) => { + const links: string[] = []; + const linkCardRegex = + /]*href="([^"]*)"[^>]*title="([^"]*)"[^>]*description="([^"]*)"[^>]*\/>/g; + + for (const [, href, title, description] of match.matchAll(linkCardRegex)) { + const fullUrl = href.startsWith('/') + ? `https://docs.sprites.dev${href}` + : href; + links.push(`- [${title}](${fullUrl}) - ${description}`); + } + + return links.length > 0 ? links.join('\n') : ''; + }); + // Remove self-closing JSX/MDX components content = content.replace(/<[A-Z][a-zA-Z]*\s+[^>]*\/>/g, ''); diff --git a/src/styles/custom.css b/src/styles/custom.css index ec58a90..f4a34d2 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -96,18 +96,18 @@ --card-foreground: oklch(0.985 0.001 106.423); --popover: oklch(0.216 0.006 56.043); --popover-foreground: oklch(0.985 0.001 106.423); - --primary: oklch(0.648 0.2 131.684); + --primary: oklch(0.723 0.191 145.579); --primary-foreground: oklch(0.986 0.031 120.757); --secondary: oklch(0.274 0.006 286.033); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.268 0.007 34.298); --muted-foreground: oklch(0.709 0.01 56.259); - --accent: oklch(0.25 0.05 131.684); - --accent-foreground: oklch(0.95 0.01 131.684); + --accent: oklch(0.25 0.05 145.579); + --accent-foreground: oklch(0.95 0.01 145.579); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); - --ring: oklch(0.648 0.2 131.684); + --ring: oklch(0.723 0.191 145.579); --chart-1: oklch(0.871 0.15 154.449); --chart-2: oklch(0.723 0.219 149.579); --chart-3: oklch(0.627 0.194 149.214); @@ -115,12 +115,12 @@ --chart-5: oklch(0.448 0.119 151.328); --sidebar: oklch(0.216 0.006 56.043); --sidebar-foreground: oklch(0.985 0.001 106.423); - --sidebar-primary: oklch(0.768 0.233 130.85); + --sidebar-primary: oklch(0.723 0.191 145.579); --sidebar-primary-foreground: oklch(0.986 0.031 120.757); - --sidebar-accent: oklch(0.25 0.05 131.684); - --sidebar-accent-foreground: oklch(0.95 0.01 131.684); + --sidebar-accent: oklch(0.25 0.05 145.579); + --sidebar-accent-foreground: oklch(0.95 0.01 145.579); --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.648 0.2 131.684); + --sidebar-ring: oklch(0.723 0.191 145.579); } /* Tailwind v4 theme inline */ @@ -167,10 +167,10 @@ /* Starlight CSS Custom Properties */ :root, [data-theme='dark'] { - /* Primary accent color - Teal */ - --sl-color-accent-low: oklch(0.3 0.1 131.684); - --sl-color-accent: oklch(0.648 0.2 131.684); - --sl-color-accent-high: oklch(0.8 0.15 131.684); + /* Primary accent color - Sprites Green (#22c55e) */ + --sl-color-accent-low: oklch(0.3 0.1 145.579); + --sl-color-accent: oklch(0.723 0.191 145.579); + --sl-color-accent-high: oklch(0.8 0.15 145.579); /* Text colors */ --sl-color-white: oklch(0.985 0.001 106.423); @@ -185,7 +185,7 @@ /* Background colors */ --sl-color-bg: oklch(0.147 0.004 49.25); --sl-color-bg-nav: oklch(0.18 0.005 49.25); - --sl-color-bg-sidebar: oklch(0.16 0.004 49.25); + --sl-color-bg-sidebar: oklch(0.147 0.004 49.25); --sl-color-bg-inline-code: oklch(0.25 0.007 34.298); /* Border colors */ @@ -317,6 +317,27 @@ header.header { border-bottom-color: var(--sl-color-hairline); } +/* Dark mode - remove borders between sections (except header) */ +[data-theme='dark'] .sidebar-pane { + border-inline-end: none; +} + +[data-theme='dark'] .right-sidebar { + border-inline-start: none; +} + +[data-theme='dark'] .content-panel { + border-top: none; +} + +[data-theme='dark'] nav.sidebar { + border: none; +} + +[data-theme='dark'] .sidebar-content { + border: none; +} + /* Table styling */ table { width: 100%; @@ -364,22 +385,54 @@ h1, h2, h3, h4, h5, h6 { background-color: oklch(0.97 0 0) !important; /* #f5f5f5 neutral gray */ } -[data-theme='light'] nav[aria-label="Main"] a[aria-current="page"] { +[data-theme='light'] nav[aria-label="Main"] a[aria-current="page"], +[data-theme='light'] nav[aria-label="Main"] a[aria-current="page"]:hover, +[data-theme='light'] nav[aria-label="Main"] a[aria-current="page"]:focus { color: oklch(0.15 0 0) !important; - background-color: oklch(0.97 0 0) !important; /* #f5f5f5 neutral gray */ + background-color: transparent !important; font-weight: 600; } +/* Nested sidebar items in light mode */ +[data-theme='light'] nav[aria-label="Main"] ul ul li { + border-inline-start: 1px solid oklch(0 0 0 / 10%); + border-radius: 0 0.5rem 0.5rem 0; +} + +[data-theme='light'] nav[aria-label="Main"] ul ul li:has(> a[aria-current="page"]) { + border-inline-start: 2px solid oklch(0.5 0.2 285); +} + +[data-theme='light'] nav[aria-label="Main"] ul ul li:has(> a[aria-current="page"]) > a { + margin-left: -1px; +} + /* Sidebar navigation in dark mode */ -[data-theme='dark'] nav[aria-label="Main"] a:hover { +[data-theme='dark'] nav[aria-label="Main"] a:hover, +[data-theme='dark'] nav[aria-label="Main"] li:has(> a:hover) { color: oklch(0.98 0 0) !important; - background-color: oklch(0.22 0 0) !important; /* lighter gray for visibility */ + background-color: oklch(0.22 0 0) !important; } -[data-theme='dark'] nav[aria-label="Main"] a[aria-current="page"] { +[data-theme='dark'] nav[aria-label="Main"] a[aria-current="page"], +[data-theme='dark'] nav[aria-label="Main"] a[aria-current="page"]:hover, +[data-theme='dark'] nav[aria-label="Main"] a[aria-current="page"]:focus { color: oklch(0.98 0 0) !important; - background-color: oklch(0.22 0 0) !important; /* lighter gray for visibility */ - font-weight: 600; + background-color: transparent !important; +} + +/* Nested sidebar items get a left border indicator */ +[data-theme='dark'] nav[aria-label="Main"] ul ul li { + border-inline-start: 1px solid oklch(1 0 0 / 10%); + border-radius: 0 0.5rem 0.5rem 0; +} + +[data-theme='dark'] nav[aria-label="Main"] ul ul li:has(> a[aria-current="page"]) { + border-inline-start: 2px solid var(--sl-color-accent); +} + +[data-theme='dark'] nav[aria-label="Main"] ul ul li:has(> a[aria-current="page"]) > a { + margin-left: -1px; } /* Sidebar group labels in light mode */ @@ -434,22 +487,22 @@ header.header .site-title img { /* Link styling - dotted underline always visible, violet on hover */ /* Exclude: badges, buttons, cards, edit link, pagination */ -main .sl-markdown-content a:not(.sl-badge):not([class*="button"]):not([class*="btn"]) { +main .sl-markdown-content a:not(.sl-badge):not([class*="button"]):not([class*="btn"]):not(.no-underline) { text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px; } /* Links inherit text color by default, violet on hover */ -main .sl-markdown-content a:not(.sl-badge):not([class*="button"]):not([class*="btn"]) { +main .sl-markdown-content a:not(.sl-badge):not([class*="button"]):not([class*="btn"]):not(.no-underline) { color: inherit; } -main .sl-markdown-content a:not(.sl-badge):not([class*="button"]):not([class*="btn"]):hover { +main .sl-markdown-content a:not(.sl-badge):not([class*="button"]):not([class*="btn"]):not(.no-underline):hover { color: oklch(0.55 0.25 285) !important; } -[data-theme='dark'] main .sl-markdown-content a:not(.sl-badge):not([class*="button"]):not([class*="btn"]):hover { +[data-theme='dark'] main .sl-markdown-content a:not(.sl-badge):not([class*="button"]):not([class*="btn"]):not(.no-underline):hover { color: oklch(0.7 0.2 285) !important; }