- Map Justine tokens to shadcn CSS variables (--vibn-* aliases) - Switch fonts to Inter + Lora via next/font (IBM Plex Mono for code) - Base typography: body Inter, h1–h3 Lora; marketing hero + wordmark serif - Project shell and global chrome use semantic colors - Replace Outfit/Newsreader references across TSX inline styles Made-with: Cursor
154 lines
4.3 KiB
TypeScript
154 lines
4.3 KiB
TypeScript
"use client";
|
||
|
||
import { ReactNode, CSSProperties } from "react";
|
||
|
||
export interface SetupProps {
|
||
workspace: string;
|
||
onClose: () => void;
|
||
onBack: () => void;
|
||
}
|
||
|
||
// Shared modal header
|
||
export function SetupHeader({
|
||
icon,
|
||
label,
|
||
tagline,
|
||
accent,
|
||
onBack,
|
||
onClose,
|
||
}: {
|
||
icon: string;
|
||
label: string;
|
||
tagline: string;
|
||
accent: string;
|
||
onBack: () => void;
|
||
onClose: () => void;
|
||
}) {
|
||
return (
|
||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 28 }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||
<button
|
||
onClick={onBack}
|
||
style={{
|
||
background: "none", border: "none", cursor: "pointer",
|
||
color: "#b5b0a6", fontSize: "1rem", padding: "3px 5px",
|
||
borderRadius: 4, lineHeight: 1, flexShrink: 0,
|
||
}}
|
||
onMouseEnter={e => (e.currentTarget.style.color = "#1a1a1a")}
|
||
onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")}
|
||
>
|
||
←
|
||
</button>
|
||
<div>
|
||
<h2 style={{
|
||
fontFamily: "var(--font-lora), ui-serif, serif", fontSize: "1.3rem", fontWeight: 400,
|
||
color: "#1a1a1a", margin: 0, marginBottom: 3,
|
||
}}>
|
||
{label}
|
||
</h2>
|
||
<p style={{ fontSize: "0.72rem", fontWeight: 600, color: accent, textTransform: "uppercase", letterSpacing: "0.04em", margin: 0 }}>
|
||
{tagline}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
style={{
|
||
background: "none", border: "none", cursor: "pointer",
|
||
color: "#b5b0a6", fontSize: "1.2rem", lineHeight: 1,
|
||
padding: "2px 5px", borderRadius: 4, flexShrink: 0,
|
||
}}
|
||
onMouseEnter={e => (e.currentTarget.style.color = "#6b6560")}
|
||
onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function FieldLabel({ children }: { children: ReactNode }) {
|
||
return (
|
||
<label style={{ display: "block", fontSize: "0.72rem", fontWeight: 600, color: "#6b6560", marginBottom: 6, letterSpacing: "0.02em" }}>
|
||
{children}
|
||
</label>
|
||
);
|
||
}
|
||
|
||
export function TextInput({
|
||
value,
|
||
onChange,
|
||
placeholder,
|
||
onKeyDown,
|
||
autoFocus,
|
||
inputRef,
|
||
}: {
|
||
value: string;
|
||
onChange: (v: string) => void;
|
||
placeholder?: string;
|
||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||
autoFocus?: boolean;
|
||
inputRef?: React.RefObject<HTMLInputElement>;
|
||
}) {
|
||
const base: CSSProperties = {
|
||
width: "100%", padding: "11px 14px", marginBottom: 16,
|
||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||
background: "#faf8f5", fontSize: "0.9rem",
|
||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", color: "#1a1a1a",
|
||
outline: "none", boxSizing: "border-box",
|
||
};
|
||
return (
|
||
<input
|
||
ref={inputRef}
|
||
type="text"
|
||
value={value}
|
||
onChange={e => onChange(e.target.value)}
|
||
onKeyDown={onKeyDown}
|
||
placeholder={placeholder}
|
||
autoFocus={autoFocus}
|
||
style={base}
|
||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||
/>
|
||
);
|
||
}
|
||
|
||
export function PrimaryButton({
|
||
onClick,
|
||
disabled,
|
||
loading,
|
||
children,
|
||
}: {
|
||
onClick: () => void;
|
||
disabled?: boolean;
|
||
loading?: boolean;
|
||
children: ReactNode;
|
||
}) {
|
||
const active = !disabled && !loading;
|
||
return (
|
||
<button
|
||
onClick={onClick}
|
||
disabled={!active}
|
||
style={{
|
||
width: "100%", padding: "12px",
|
||
borderRadius: 8, border: "none",
|
||
background: active ? "#1a1a1a" : "#e0dcd4",
|
||
color: active ? "#fff" : "#b5b0a6",
|
||
fontSize: "0.88rem", fontWeight: 600,
|
||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||
cursor: active ? "pointer" : "not-allowed",
|
||
display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
|
||
}}
|
||
onMouseEnter={e => { if (active) e.currentTarget.style.opacity = "0.85"; }}
|
||
onMouseLeave={e => { e.currentTarget.style.opacity = "1"; }}
|
||
>
|
||
{loading ? (
|
||
<>
|
||
<span style={{ width: 14, height: 14, borderRadius: "50%", border: "2px solid #fff4", borderTopColor: "#fff", animation: "vibn-spin 0.7s linear infinite", display: "inline-block" }} />
|
||
Creating…
|
||
</>
|
||
) : children}
|
||
</button>
|
||
);
|
||
}
|