feat(web): Next.js 15 shell — design tokens, landing, auth pages
This commit is contained in:
65
apps/web/components/code-block.tsx
Normal file
65
apps/web/components/code-block.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
export interface CodeBlockProps {
|
||||
code: string;
|
||||
language?: string;
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, language, label, className }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function onCopy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('panel-subtle relative overflow-hidden', className)}>
|
||||
{(label || language) && (
|
||||
<div className="flex items-center justify-between border-b border-[--color-border] px-3 py-2">
|
||||
<span className="mono text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
|
||||
{label ?? language}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopy}
|
||||
aria-label="Copy"
|
||||
className="inline-flex items-center gap-1.5 rounded-sm text-[11px] text-[--color-fg-subtle] transition-colors duration-200 ease-out hover:text-[--color-fg]"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={12} /> copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={12} /> copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!label && !language && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopy}
|
||||
aria-label="Copy"
|
||||
className="absolute right-2 top-2 inline-flex items-center gap-1.5 rounded-sm border border-[--color-border] bg-[--color-bg-elevated] px-2 py-1 text-[11px] text-[--color-fg-subtle] transition-colors duration-200 ease-out hover:text-[--color-fg]"
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</button>
|
||||
)}
|
||||
<pre className="mono overflow-x-auto px-3 py-3 text-[12.5px] leading-relaxed text-[--color-fg]">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
apps/web/components/input.tsx
Normal file
53
apps/web/components/input.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||
function Input({ className, ...props }, ref) {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-8 w-full rounded-md border border-[--color-border] bg-[--color-bg-subtle] px-2.5 text-[13px] text-[--color-fg] placeholder:text-[--color-fg-subtle] transition-colors duration-200 ease-out focus:border-[--color-accent] focus:outline-none focus:ring-1 focus:ring-[--color-accent]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
|
||||
function Textarea({ className, ...props }, ref) {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mono w-full rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-3 text-[13px] leading-relaxed text-[--color-fg] placeholder:text-[--color-fg-subtle] transition-colors duration-200 ease-out focus:border-[--color-accent] focus:outline-none focus:ring-1 focus:ring-[--color-accent]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export function Label({
|
||||
children,
|
||||
hint,
|
||||
className,
|
||||
htmlFor,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
hint?: string;
|
||||
className?: string;
|
||||
htmlFor?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('flex items-baseline justify-between', className)}>
|
||||
<label htmlFor={htmlFor} className="text-[12px] font-medium text-[--color-fg]">
|
||||
{children}
|
||||
</label>
|
||||
{hint && <span className="mono text-[11px] text-[--color-fg-subtle]">{hint}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
apps/web/components/logo.tsx
Normal file
14
apps/web/components/logo.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Link from 'next/link';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
export function Logo({ className }: { className?: string }) {
|
||||
return (
|
||||
<Link href="/" className={cn('inline-flex items-center gap-2 text-[--color-fg]', className)}>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden>
|
||||
<rect x="1" y="1" width="16" height="16" rx="3" stroke="currentColor" strokeWidth="1.25" />
|
||||
<path d="M5 12V6L9 9.5L13 6V12" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span className="text-[13px] font-semibold tracking-tight">BuildMyMCPServer</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
38
apps/web/components/status-pill.tsx
Normal file
38
apps/web/components/status-pill.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { ServerStatus, BuildStatus } from '@bmm/types';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
type AnyStatus = ServerStatus | BuildStatus;
|
||||
|
||||
const map: Record<string, { label: string; dot: string; fg: string; bg: string; pulse?: boolean }> = {
|
||||
draft: { label: 'Draft', dot: 'bg-zinc-500', fg: 'text-zinc-400', bg: 'bg-zinc-500/10' },
|
||||
queued: { label: 'Queued', dot: 'bg-zinc-400', fg: 'text-zinc-300', bg: 'bg-zinc-400/10' },
|
||||
generating: { label: 'Generating', dot: 'bg-amber-400', fg: 'text-amber-300', bg: 'bg-amber-400/10', pulse: true },
|
||||
building: { label: 'Building', dot: 'bg-amber-400', fg: 'text-amber-300', bg: 'bg-amber-400/10', pulse: true },
|
||||
deploying: { label: 'Deploying', dot: 'bg-indigo-400', fg: 'text-indigo-300', bg: 'bg-indigo-400/10', pulse: true },
|
||||
live: { label: 'Live', dot: 'bg-emerald-400', fg: 'text-emerald-300', bg: 'bg-emerald-400/10', pulse: true },
|
||||
success: { label: 'Success', dot: 'bg-emerald-400', fg: 'text-emerald-300', bg: 'bg-emerald-400/10' },
|
||||
failed: { label: 'Failed', dot: 'bg-red-400', fg: 'text-red-300', bg: 'bg-red-400/10' },
|
||||
cancelled: { label: 'Cancelled', dot: 'bg-zinc-500', fg: 'text-zinc-400', bg: 'bg-zinc-500/10' },
|
||||
paused: { label: 'Paused', dot: 'bg-zinc-500', fg: 'text-zinc-400', bg: 'bg-zinc-500/10' },
|
||||
};
|
||||
|
||||
export function StatusPill({ status, className }: { status: AnyStatus; className?: string }) {
|
||||
const s = map[status] ?? map.draft;
|
||||
if (!s) return null;
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-full border border-[--color-border] px-2 py-0.5 text-[11px] font-medium tracking-tight',
|
||||
s.bg,
|
||||
s.fg,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn('size-1.5 rounded-full', s.dot)}
|
||||
style={s.pulse ? { animation: 'pulse-dot 1.6s ease-in-out infinite' } : undefined}
|
||||
/>
|
||||
{s.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
45
apps/web/components/ui/button.tsx
Normal file
45
apps/web/components/ui/button.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
type Variant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
type Size = 'sm' | 'md' | 'lg';
|
||||
|
||||
const variants: Record<Variant, string> = {
|
||||
primary:
|
||||
'bg-[--color-accent] text-[--color-accent-fg] hover:bg-[#5557e8] active:bg-[#4f51d8] border border-transparent',
|
||||
secondary:
|
||||
'bg-[--color-bg-elevated] text-[--color-fg] hover:bg-[--color-bg-subtle] border border-[--color-border]',
|
||||
ghost:
|
||||
'bg-transparent text-[--color-fg-muted] hover:text-[--color-fg] hover:bg-[--color-bg-subtle] border border-transparent',
|
||||
danger:
|
||||
'bg-transparent text-[--color-danger] hover:bg-[rgba(239,68,68,0.08)] border border-[--color-border]',
|
||||
};
|
||||
|
||||
const sizes: Record<Size, string> = {
|
||||
sm: 'h-7 px-2.5 text-xs gap-1.5',
|
||||
md: 'h-8 px-3 text-[13px] gap-2',
|
||||
lg: 'h-10 px-4 text-sm gap-2',
|
||||
};
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
{ className, variant = 'secondary', size = 'md', ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md font-medium tracking-tight transition-colors duration-200 ease-out disabled:opacity-50 disabled:pointer-events-none select-none',
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user