diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..bea462a --- /dev/null +++ b/app/globals.css @@ -0,0 +1,369 @@ +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap'); + +@import "tailwindcss"; + +/* ============================================================ + TAILWIND V4 THEME — tokens that generate utility classes + ============================================================ */ +@theme { + /* Original palette — retained */ + --color-pink: #B53082; + --color-purple: #381B57; + --color-orange: #F2963A; + --color-teal: #6DC3BB; + --color-beige: #C8B99A; + --color-navy: #0E1628; + --color-void: #07050F; + + /* New palette */ + --color-green: #00ed3f; + --color-amber: #ff8800; + --color-amber-light: #ffa238; + --color-red: #cc2200; + --color-blue: #4466cc; + --color-bone: #E0D5BE; + + /* Neutral dark backgrounds */ + --color-bg: #050505; + --color-surface: #0D0D0D; + --color-surface-raised: #141414; + + /* Typography */ + --font-mono: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'Courier New', monospace; + + /* Zero radius */ + --radius: 0px; +} + +/* ============================================================ + SEMANTIC TOKENS — contextual roles mapped to palette + ============================================================ */ +:root { + /* Backgrounds — neutral dark, no color tint */ + --background: #050505; + --surface: #0D0D0D; + --surface-raised: #141414; + + /* Borders */ + --border: #252525; + --border-active: #00ed3f; + + /* Text */ + --text-primary: #00ed3f; + --text-secondary: #E0D5BE; + /* SpaceWanderer tweak (v0.1.0): upstream default is #4A4A40 (HSL 60,7%,27%) — too dim for + secondary text on the near-black background. Brightened to HSL 60,8%,39% (+12pt lightness, + same warm-gray hue) so muted labels stay readable while remaining clearly muted. */ + --text-muted: #6B6B5C; + --text-warning: #ff8800; + --text-danger: #cc2200; + + /* Accent shortcuts */ + --accent-primary: #00ed3f; + --accent-secondary: #4466cc; + --accent-warning: #ff8800; + + /* Glow — original (kept for backward compat) */ + --glow-teal: 0 0 8px #6DC3BB66, 0 0 20px #6DC3BB33; + --glow-pink: 0 0 8px #B5308266, 0 0 20px #B5308233; + --glow-orange: 0 0 8px #F2963A66, 0 0 20px #F2963A33; + + /* Glow — new palette */ + --glow-green: 0 0 8px #00ed3f66, 0 0 20px #00ed3f33; + --glow-amber: 0 0 8px #ff880066, 0 0 20px #ff880033; + --glow-red: 0 0 8px #cc220066, 0 0 20px #cc220033; + --glow-blue: 0 0 8px #4466cc66, 0 0 20px #4466cc33; + + /* Text glow — original (kept) */ + --text-glow-teal: 0 0 6px #6DC3BB99, 0 0 14px #6DC3BB55; + --text-glow-orange: 0 0 6px #F2963A99, 0 0 14px #F2963A55; + --text-glow-pink: 0 0 6px #B5308299, 0 0 14px #B5308255; + + /* Text glow — new palette */ + --text-glow-green: 0 0 6px #00ed3f99, 0 0 14px #00ed3f55; + --text-glow-amber: 0 0 6px #ff880099, 0 0 14px #ff880055; + --text-glow-red: 0 0 6px #cc220099, 0 0 14px #cc220055; + --text-glow-blue: 0 0 6px #4466cc99, 0 0 14px #4466cc55; + + /* Corner notch clip-paths */ + --clip-corner-sm: polygon(6px 0%, 100% 0%, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0% 100%, 0% 6px); + --clip-corner-md: polygon(10px 0%, 100% 0%, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0% 100%, 0% 10px); + --clip-corner-lg: polygon(16px 0%, 100% 0%, 100% calc(100% - 16px), calc(100% - 16px) 100%, 0% 100%, 0% 16px); + + /* Geometry */ + --border-width: 1px; +} + +/* ============================================================ + KEYFRAME ANIMATIONS + ============================================================ */ +@keyframes flicker { + 0%, 89%, 91%, 93%, 100% { opacity: 1; } + 90% { opacity: 0.4; } + 92% { opacity: 0.8; } + 94% { opacity: 0.6; } +} + +@keyframes blink-cursor { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } +} + +@keyframes glitch { + 0%, 100% { transform: translate(0); } + 10% { transform: translate(-2px, 1px); } + 20% { transform: translate(2px, -1px); } + 30% { transform: translate(-1px, 2px); } + 40% { transform: translate(1px, -1px); } + 50% { transform: translate(0); } +} + +@keyframes scanline-move { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(100vh); } +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} + +@keyframes data-scroll { + 0% { transform: translateY(0); } + 100% { transform: translateY(-50%); } +} + +/* ============================================================ + BASE RESET + ============================================================ */ +*, +*::before, +*::after { + box-sizing: border-box; + border-radius: 0 !important; +} + +html { + font-size: 14px; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + padding: 0; + background-color: var(--background); + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 1rem; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + min-height: 100vh; +} + +button, +input, +textarea, +select { + font-family: var(--font-mono); + font-size: inherit; + border-radius: 0; +} + +a { + color: var(--text-primary); + text-decoration: none; +} + +a:hover { + text-shadow: var(--text-glow-green); +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-mono); + font-weight: 700; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin: 0; +} + +p { + margin: 0; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 5px; + height: 5px; +} +::-webkit-scrollbar-track { + background: var(--background); +} +::-webkit-scrollbar-thumb { + background: var(--border); +} +::-webkit-scrollbar-thumb:hover { + background: var(--border-active); + box-shadow: var(--glow-green); +} + +/* Text selection */ +::selection { + background: #00ed3f; + color: #050505; +} + +/* ============================================================ + UTILITY CLASSES + ============================================================ */ + +/* CRT scanline overlay — apply to panels/containers */ +.scanlines { + position: relative; + overflow: hidden; +} +.scanlines::after { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + to bottom, + transparent 0px, + transparent 3px, + rgba(0, 0, 0, 0.12) 3px, + rgba(0, 0, 0, 0.12) 4px + ); + pointer-events: none; + z-index: 10; +} + +/* Phosphor flicker text */ +.flicker { + animation: flicker 8s infinite; +} + +/* Glitch on hover */ +.glitch-hover:hover { + animation: glitch 0.3s ease-in-out; +} + +/* Pulsing glow */ +.pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; +} + +/* Corner notch variants */ +.clip-corner-sm { + clip-path: var(--clip-corner-sm); +} +.clip-corner-md { + clip-path: var(--clip-corner-md); +} +.clip-corner-lg { + clip-path: var(--clip-corner-lg); +} + +/* ============================================================ + THEME: STAR WARS + Holographic blue primary · bone/gold warnings · teal info + Deep blue-black backgrounds + ============================================================ */ +[data-theme="star-wars"] { + /* Remap color primitives used by components */ + --color-green: #1a6dff; /* blue hologram = "active" */ + --color-amber: #E0D5BE; /* bone/gold = "warning" */ + --color-red: #cc0000; /* Sith red = "danger" */ + --color-blue: #6DC3BB; /* teal = "info" */ + + /* Backgrounds — deep blue-black */ + --background: #020509; + --surface: #060E18; + --surface-raised: #0C1826; + + /* Borders */ + --border: #152030; + --border-active: #1a6dff; + + /* Text */ + --text-primary: #1a6dff; + --text-secondary: #E0D5BE; + --text-muted: #2A3A52; + --text-warning: #E0D5BE; + --text-danger: #cc0000; + + /* Accent shortcuts */ + --accent-primary: #1a6dff; + --accent-secondary: #6DC3BB; + --accent-warning: #E0D5BE; + + /* Glow — remap green→blue, amber→bone, blue→teal */ + --glow-green: 0 0 8px rgba(26,109,255,0.40), 0 0 20px rgba(26,109,255,0.20); + --glow-amber: 0 0 8px rgba(224,213,190,0.35), 0 0 20px rgba(224,213,190,0.15); + --glow-red: 0 0 8px rgba(204,0,0,0.40), 0 0 20px rgba(204,0,0,0.20); + --glow-blue: 0 0 8px rgba(109,195,187,0.40), 0 0 20px rgba(109,195,187,0.20); + + /* Text glow — remap */ + --text-glow-green: 0 0 6px rgba(26,109,255,0.60), 0 0 14px rgba(26,109,255,0.30); + --text-glow-amber: 0 0 6px rgba(224,213,190,0.55), 0 0 14px rgba(224,213,190,0.25); + --text-glow-red: 0 0 6px rgba(204,0,0,0.60), 0 0 14px rgba(204,0,0,0.30); + --text-glow-blue: 0 0 6px rgba(109,195,187,0.60), 0 0 14px rgba(109,195,187,0.30); +} + +[data-theme="star-wars"] ::selection { + background: #1a6dff; + color: #020509; +} + +/* ============================================================ + THEME: ALIEN + Bone/ivory primary · amber warnings · acid green info + Warm organic dark backgrounds + ============================================================ */ +[data-theme="alien"] { + /* Remap color primitives used by components */ + --color-green: #E0D5BE; /* bone = "active" (bleached, eerie) */ + --color-amber: #ff8800; /* amber = "warning" (Nostromo lights) */ + --color-red: #cc2200; /* blood red = "danger" */ + --color-blue: #00ed3f; /* acid green = "info" (bioluminescent) */ + + /* Backgrounds — warm organic dark */ + --background: #040302; + --surface: #080604; + --surface-raised: #0E0A06; + + /* Borders */ + --border: #201808; + --border-active: #E0D5BE; + + /* Text */ + --text-primary: #E0D5BE; + --text-secondary: #C8B99A; + --text-muted: #403020; + --text-warning: #ff8800; + --text-danger: #cc2200; + + /* Accent shortcuts */ + --accent-primary: #E0D5BE; + --accent-secondary: #00ed3f; + --accent-warning: #ff8800; + + /* Glow — remap green→bone, blue→acid-green */ + --glow-green: 0 0 8px rgba(224,213,190,0.35), 0 0 20px rgba(224,213,190,0.15); + --glow-amber: 0 0 8px rgba(255,136,0,0.40), 0 0 20px rgba(255,136,0,0.20); + --glow-red: 0 0 8px rgba(204,34,0,0.40), 0 0 20px rgba(204,34,0,0.20); + --glow-blue: 0 0 8px rgba(0,237,63,0.40), 0 0 20px rgba(0,237,63,0.20); + + /* Text glow — remap */ + --text-glow-green: 0 0 6px rgba(224,213,190,0.55), 0 0 14px rgba(224,213,190,0.25); + --text-glow-amber: 0 0 6px rgba(255,136,0,0.60), 0 0 14px rgba(255,136,0,0.30); + --text-glow-red: 0 0 6px rgba(204,34,0,0.60), 0 0 14px rgba(204,34,0,0.30); + --text-glow-blue: 0 0 6px rgba(0,237,63,0.60), 0 0 14px rgba(0,237,63,0.30); +} + +[data-theme="alien"] ::selection { + background: #E0D5BE; + color: #040302; +} diff --git a/app/icon.svg b/app/icon.svg new file mode 100644 index 0000000..34a4efa --- /dev/null +++ b/app/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..ce6dfd6 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'SpaceWanderer', + description: 'Turn-based space adventure — build & expand your ship, survive the void.', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..5cfd162 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,81 @@ +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Panel, PanelContent, PanelFooter, PanelHeader } from '@/components/ui/panel'; +import pkg from '@/package.json'; + +export default function Home() { + return ( +
+ + + SYS:ONLINE + + v{pkg.version} — STACK PROOF + + + +

+ SpaceWanderer +

+

+ Turn-based space adventure — build & expand your ship from found parts, survive the void. +

+ + + + Ship status + + +
+
+ HULL + NOMINAL +
+
+ CREW + — +
+
+ NEXT ENCOUNTER + SCANNING... +
+
+
+
+ + + Scaffold online + + Base stack + scificn-ui frontend integrated. No game systems yet — single player only, by design. + + +
+ + + + SCIFICN/UI v0.1.0 · SCI-FI THEME + + +
+
+ ); +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..0e4f310 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,119 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const alertVariants = cva( + [ + 'relative w-full', + 'border border-solid border-l-[3px]', + 'p-4', + 'font-mono', + 'rounded-none', + ], + { + variants: { + variant: { + STATUS: [ + 'border-[var(--border)]', + 'border-l-[var(--color-green)]', + 'bg-[var(--surface)]', + ], + WARNING: [ + 'border-[var(--border)]', + 'border-l-[var(--color-amber)]', + 'bg-[var(--surface)]', + ], + CRITICAL: [ + 'border-[var(--border)]', + 'border-l-[var(--color-red)]', + 'bg-[var(--surface)]', + ], + INFO: [ + 'border-[var(--border)]', + 'border-l-[var(--color-blue)]', + 'bg-[var(--surface)]', + ], + }, + }, + defaultVariants: { variant: 'STATUS' }, + } +) + +const prefixMap = { + STATUS: { symbol: '◈', color: 'var(--color-green)', glow: 'var(--text-glow-green)' }, + WARNING: { symbol: '⚠', color: 'var(--color-amber)', glow: 'var(--text-glow-amber)' }, + CRITICAL: { symbol: '✕', color: 'var(--color-red)', glow: 'var(--text-glow-red)' }, + INFO: { symbol: 'ℹ', color: 'var(--color-blue)', glow: 'var(--text-glow-blue)' }, +} + +export interface AlertProps + extends React.HTMLAttributes, + VariantProps {} + +const Alert = React.forwardRef( + ({ className, variant = 'STATUS', children, ...props }, ref) => { + const v = variant ?? 'STATUS' + const prefix = prefixMap[v] + + return ( +
+
+ + {prefix.symbol} + +
{children}
+
+
+ ) + } +) +Alert.displayName = 'Alert' + +/* ── AlertTitle ── */ +const AlertTitle = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +
+ ) +) +AlertTitle.displayName = 'AlertTitle' + +/* ── AlertDescription ── */ +const AlertDescription = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +

+ ) +) +AlertDescription.displayName = 'AlertDescription' + +export { Alert, AlertTitle, AlertDescription, alertVariants } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..29c7f69 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,83 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + [ + 'inline-flex items-center gap-1.5', + 'px-2 py-0.5', + 'text-[0.65rem] font-mono font-medium uppercase tracking-widest', + 'border border-solid', + 'rounded-none', + 'whitespace-nowrap', + ], + { + variants: { + variant: { + ACTIVE: [ + 'border-[var(--color-green)]', + 'text-[var(--color-green)]', + ], + OFFLINE: [ + 'border-[var(--border)]', + 'text-[var(--text-muted)]', + ], + WARNING: [ + 'border-[var(--color-amber)]', + 'text-[var(--color-amber)]', + ], + CRITICAL: [ + 'border-[var(--color-red)]', + 'text-[var(--color-red)]', + ], + SCANNING: [ + 'border-[var(--color-green)]', + 'text-[var(--color-green)]', + ], + }, + }, + defaultVariants: { + variant: 'ACTIVE', + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, children, ...props }: BadgeProps) { + const isScanning = variant === 'SCANNING' + + return ( + + {isScanning && ( + + ● + + )} + {!isScanning && variant === 'ACTIVE' && ●} + {!isScanning && variant === 'OFFLINE' && ○} + {!isScanning && variant === 'WARNING' && ⚠} + {!isScanning && variant === 'CRITICAL' && ✕} + {children} + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..a71ca92 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,91 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + [ + 'inline-flex items-center justify-center', + 'font-mono font-medium uppercase tracking-widest', + 'border border-solid', + 'cursor-pointer select-none', + 'transition-all duration-150', + 'focus-visible:outline-none', + 'disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none', + 'rounded-none', + 'whitespace-nowrap', + ], + { + variants: { + variant: { + EXEC: [ + 'bg-transparent', + 'border-[var(--color-green)]', + 'text-[var(--color-green)]', + 'hover:bg-[var(--color-green)]', + 'hover:text-[var(--color-bg)]', + 'hover:shadow-[var(--glow-green)]', + 'focus-visible:shadow-[var(--glow-green)]', + ], + GHOST: [ + 'bg-transparent', + 'border-transparent', + 'text-[var(--text-secondary)]', + 'hover:text-[var(--color-green)]', + 'hover:border-[var(--border)]', + 'focus-visible:border-[var(--border)]', + ], + OUTLINE: [ + 'bg-[var(--surface)]', + 'border-[var(--border)]', + 'text-[var(--text-secondary)]', + 'hover:border-[var(--border-active)]', + 'hover:text-[var(--color-green)]', + 'hover:shadow-[var(--glow-green)]', + 'focus-visible:border-[var(--border-active)]', + 'focus-visible:shadow-[var(--glow-green)]', + ], + ABORT: [ + 'bg-transparent', + 'border-[var(--color-red)]', + 'text-[var(--color-red)]', + 'hover:bg-[var(--color-red)]', + 'hover:text-[var(--color-bg)]', + 'hover:shadow-[var(--glow-red)]', + 'focus-visible:shadow-[var(--glow-red)]', + ], + }, + size: { + SM: 'h-7 px-3 text-[0.65rem] gap-1.5', + MD: 'h-9 px-4 text-[0.75rem] gap-2', + LG: 'h-11 px-6 text-[0.875rem] gap-2.5', + }, + }, + defaultVariants: { + variant: 'OUTLINE', + size: 'MD', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..4a959a5 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } diff --git a/components/ui/panel.tsx b/components/ui/panel.tsx new file mode 100644 index 0000000..189b63b --- /dev/null +++ b/components/ui/panel.tsx @@ -0,0 +1,112 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +/* ── Panel (outer container) ── */ +export interface PanelProps extends React.HTMLAttributes { + notch?: 'sm' | 'md' | 'lg' | 'none' +} + +const Panel = React.forwardRef( + ({ className, notch = 'md', style, ...props }, ref) => { + const clipMap = { + sm: 'var(--clip-corner-sm)', + md: 'var(--clip-corner-md)', + lg: 'var(--clip-corner-lg)', + none: 'none', + } + + return ( +
+ ) + } +) +Panel.displayName = 'Panel' + +/* ── PanelHeader ── */ +const PanelHeader = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +
+ ) +) +PanelHeader.displayName = 'PanelHeader' + +/* ── PanelTitle ── */ +const PanelTitle = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +

+ ) +) +PanelTitle.displayName = 'PanelTitle' + +/* ── PanelContent ── */ +const PanelContent = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +
+ ) +) +PanelContent.displayName = 'PanelContent' + +/* ── PanelFooter ── */ +const PanelFooter = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +
+ ) +) +PanelFooter.displayName = 'PanelFooter' + +export { Panel, PanelHeader, PanelTitle, PanelContent, PanelFooter } diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..b057456 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)) +}