Rebuild website from scratch on Tailwind v4 + shadcn/ui
- Fresh Astro 5 project with Tailwind v4 and shadcn/ui olive preset - All shadcn components installed (Card, Button, Badge, Separator, etc.) - Homepage with hero, terminal demo, workflows, agents, sources, compute - Full docs system with 24 markdown pages across 5 sections - Sidebar navigation with active state highlighting - Prose styles for markdown content using shadcn color tokens - Dark/light theme toggle with localStorage persistence - Shiki everforest syntax themes for code blocks - 404 page with VT323 font - /docs redirect to installation page - GitHub star count fetch - Earthy green/cream oklch color palette matching TUI theme Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
size?: 'nav' | 'hero';
|
||||
}
|
||||
|
||||
const { class: className = '', size = 'hero' } = Astro.props;
|
||||
|
||||
const sizeClasses = size === 'nav'
|
||||
? 'text-2xl'
|
||||
: 'text-6xl sm:text-7xl md:text-8xl';
|
||||
---
|
||||
|
||||
<span
|
||||
class:list={[
|
||||
"font-['VT323'] text-accent inline-block tracking-tighter",
|
||||
sizeClasses,
|
||||
className,
|
||||
]}
|
||||
aria-label="Feynman"
|
||||
>feynman</span>
|
||||
@@ -1,9 +0,0 @@
|
||||
<footer class="py-8 mt-16">
|
||||
<div class="max-w-6xl mx-auto px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<span class="text-sm text-text-dim">© 2026 Companion Inc.</span>
|
||||
<div class="flex gap-6">
|
||||
<a href="https://github.com/getcompanion-ai/feynman" target="_blank" rel="noopener" class="text-sm text-text-dim hover:text-text-primary transition-colors">GitHub</a>
|
||||
<a href="/docs/getting-started/installation" class="text-sm text-text-dim hover:text-text-primary transition-colors">Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
import AsciiLogo from './AsciiLogo.astro';
|
||||
|
||||
interface Props {
|
||||
active?: 'home' | 'docs';
|
||||
}
|
||||
|
||||
const { active = 'home' } = Astro.props;
|
||||
---
|
||||
|
||||
<nav class="sticky top-0 z-50 bg-bg">
|
||||
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
|
||||
<a href="/" class="hover:opacity-80 transition-opacity" aria-label="Feynman">
|
||||
<AsciiLogo size="nav" />
|
||||
</a>
|
||||
<div class="flex items-center gap-6">
|
||||
<a href="/docs/getting-started/installation"
|
||||
class:list={["text-sm transition-colors", active === 'docs' ? 'text-text-primary' : 'text-text-muted hover:text-text-primary']}>
|
||||
Docs
|
||||
</a>
|
||||
<a href="https://github.com/getcompanion-ai/feynman" target="_blank" rel="noopener"
|
||||
class="text-sm text-text-muted hover:text-text-primary transition-colors">
|
||||
GitHub
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
interface Props {
|
||||
currentSlug: string;
|
||||
}
|
||||
|
||||
const { currentSlug } = Astro.props;
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: 'Getting Started',
|
||||
items: [
|
||||
{ label: 'Installation', slug: 'getting-started/installation' },
|
||||
{ label: 'Quick Start', slug: 'getting-started/quickstart' },
|
||||
{ label: 'Setup', slug: 'getting-started/setup' },
|
||||
{ label: 'Configuration', slug: 'getting-started/configuration' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Workflows',
|
||||
items: [
|
||||
{ label: 'Deep Research', slug: 'workflows/deep-research' },
|
||||
{ label: 'Literature Review', slug: 'workflows/literature-review' },
|
||||
{ label: 'Peer Review', slug: 'workflows/review' },
|
||||
{ label: 'Code Audit', slug: 'workflows/audit' },
|
||||
{ label: 'Replication', slug: 'workflows/replication' },
|
||||
{ label: 'Source Comparison', slug: 'workflows/compare' },
|
||||
{ label: 'Draft Writing', slug: 'workflows/draft' },
|
||||
{ label: 'Autoresearch', slug: 'workflows/autoresearch' },
|
||||
{ label: 'Watch', slug: 'workflows/watch' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Agents',
|
||||
items: [
|
||||
{ label: 'Researcher', slug: 'agents/researcher' },
|
||||
{ label: 'Reviewer', slug: 'agents/reviewer' },
|
||||
{ label: 'Writer', slug: 'agents/writer' },
|
||||
{ label: 'Verifier', slug: 'agents/verifier' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tools',
|
||||
items: [
|
||||
{ label: 'AlphaXiv', slug: 'tools/alphaxiv' },
|
||||
{ label: 'Web Search', slug: 'tools/web-search' },
|
||||
{ label: 'Session Search', slug: 'tools/session-search' },
|
||||
{ label: 'Preview', slug: 'tools/preview' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Reference',
|
||||
items: [
|
||||
{ label: 'CLI Commands', slug: 'reference/cli-commands' },
|
||||
{ label: 'Slash Commands', slug: 'reference/slash-commands' },
|
||||
{ label: 'Package Stack', slug: 'reference/package-stack' },
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<aside id="sidebar" class="w-64 shrink-0 h-[calc(100vh-3.5rem)] sticky top-14 overflow-y-auto py-6 pr-4 hidden lg:block border-r border-border">
|
||||
{sections.map((section) => (
|
||||
<div class="mb-6">
|
||||
<div class="text-xs font-semibold text-accent uppercase tracking-wider px-3 mb-2">{section.title}</div>
|
||||
{section.items.map((item) => (
|
||||
<a
|
||||
href={`/docs/${item.slug}`}
|
||||
class:list={[
|
||||
'block px-3 py-1.5 text-sm border-l-[2px] transition-colors',
|
||||
currentSlug === item.slug
|
||||
? 'border-accent text-text-primary'
|
||||
: 'border-transparent text-text-muted hover:text-text-primary',
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</aside>
|
||||
@@ -1,53 +0,0 @@
|
||||
<button id="theme-toggle" class="p-1.5 rounded-md text-text-muted hover:text-text-primary hover:bg-surface transition-colors" aria-label="Toggle theme">
|
||||
<svg id="sun-icon" class="hidden w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||
</svg>
|
||||
<svg id="moon-icon" class="hidden w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script is:inline>
|
||||
(function() {
|
||||
var stored = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var dark = stored === 'dark' || (!stored && prefersDark);
|
||||
if (dark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
function updateIcons() {
|
||||
var isDark = document.documentElement.classList.contains('dark');
|
||||
var sun = document.getElementById('sun-icon');
|
||||
var moon = document.getElementById('moon-icon');
|
||||
if (sun) sun.style.display = isDark ? 'block' : 'none';
|
||||
if (moon) moon.style.display = isDark ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function bindToggle() {
|
||||
var btn = document.getElementById('theme-toggle');
|
||||
if (btn && !btn._bound) {
|
||||
btn._bound = true;
|
||||
btn.addEventListener('click', function() {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
var isDark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
updateIcons();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateIcons();
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateIcons();
|
||||
bindToggle();
|
||||
});
|
||||
document.addEventListener('astro:after-swap', function() {
|
||||
updateIcons();
|
||||
bindToggle();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
49
website/src/components/ui/badge.tsx
Normal file
49
website/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
67
website/src/components/ui/button.tsx
Normal file
67
website/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
||||
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-9",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
103
website/src/components/ui/card.tsx
Normal file
103
website/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-6 overflow-hidden rounded-xl bg-card py-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
26
website/src/components/ui/separator.tsx
Normal file
26
website/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
Reference in New Issue
Block a user