Files
vibn-frontend/components/project-creation/setup-shared.tsx
Mark Henderson 6f79a88abd fix(gitea-bot): add write:organization scope so bot can create repos
Without this the bot PAT 403s on POST /orgs/{org}/repos, which is
the single most important operation — creating new project repos
inside the workspace's Gitea org.

Made-with: Cursor
2026-04-21 11:05:55 -07:00

231 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { ReactNode, CSSProperties } from "react";
import { JM } from "./modal-theme";
export interface SetupProps {
workspace: string;
onClose: () => void;
onBack: () => void;
}
export function FieldLabel({ children }: { children: ReactNode }) {
return (
<label style={{
display: "block", fontSize: 12, fontWeight: 600, color: JM.mid,
marginBottom: 6, fontFamily: JM.fontSans,
}}>
{children}
</label>
);
}
export function ForWhomSelector({
value,
onChange,
}: {
value: "personal" | "client";
onChange: (v: "personal" | "client") => void;
}) {
const cardBase: CSSProperties = {
flex: 1,
border: `1px solid ${JM.border}`,
borderRadius: 9,
padding: 14,
cursor: "pointer",
textAlign: "center" as const,
background: JM.inputBg,
transition: "all 0.15s",
fontFamily: JM.fontSans,
};
const row = (key: "personal" | "client", emoji: string, title: string, sub: string) => {
const sel = value === key;
return (
<button
type="button"
key={key}
onClick={() => onChange(key)}
style={{
...cardBase,
borderColor: sel ? JM.indigo : JM.border,
background: sel ? JM.cream : JM.inputBg,
boxShadow: sel ? "0 0 0 1px rgba(99,102,241,0.2)" : undefined,
}}
>
<div style={{ fontSize: 20, marginBottom: 5 }}>{emoji}</div>
<div style={{ fontSize: 12.5, fontWeight: 600, color: JM.ink }}>{title}</div>
<div style={{ fontSize: 11, color: JM.muted, marginTop: 2 }}>{sub}</div>
</button>
);
};
return (
<div style={{ marginBottom: 22 }}>
<FieldLabel>This project is for</FieldLabel>
<div style={{ display: "flex", gap: 8 }}>
{row("personal", "🧑‍💻", "Myself", "My own product")}
{row("client", "🤝", "A client", "Client project")}
</div>
</div>
);
}
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: 22 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<button
type="button"
onClick={onBack}
style={{
background: "none", border: "none", cursor: "pointer",
color: JM.muted, fontSize: "1rem", padding: "3px 5px",
borderRadius: 4, lineHeight: 1, flexShrink: 0,
fontFamily: JM.fontSans,
}}
onMouseEnter={e => (e.currentTarget.style.color = JM.ink)}
onMouseLeave={e => (e.currentTarget.style.color = JM.muted)}
>
</button>
<div>
<h2 style={{
fontFamily: JM.fontDisplay, fontSize: 18, fontWeight: 700,
color: JM.ink, margin: 0, marginBottom: 3, letterSpacing: "-0.02em",
}}>
{label}
</h2>
<p style={{
fontSize: 10.5, fontWeight: 600, color: accent, textTransform: "uppercase",
letterSpacing: "0.07em", margin: 0, fontFamily: JM.fontSans,
}}>
{tagline}
</p>
</div>
</div>
<button
type="button"
onClick={onClose}
style={{
background: "none", border: "none", cursor: "pointer",
color: JM.muted, fontSize: 20, lineHeight: 1,
padding: 4, flexShrink: 0, fontFamily: JM.fontSans,
}}
onMouseEnter={e => (e.currentTarget.style.color = JM.mid)}
onMouseLeave={e => (e.currentTarget.style.color = JM.muted)}
>
×
</button>
</div>
);
}
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 | null> | React.RefObject<HTMLInputElement>;
}) {
const base: CSSProperties = {
width: "100%", padding: "10px 13px", marginBottom: 16,
borderRadius: 8, border: `1px solid ${JM.border}`,
background: JM.inputBg, fontSize: 14,
fontFamily: JM.fontSans, color: JM.ink,
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 = JM.indigo)}
onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
/>
);
}
export function PrimaryButton({
onClick,
disabled,
loading,
children,
}: {
onClick: () => void;
disabled?: boolean;
loading?: boolean;
children: ReactNode;
}) {
const active = !disabled && !loading;
return (
<button
type="button"
onClick={onClick}
disabled={!active}
style={{
width: "100%", padding: "12px",
borderRadius: 8, border: "none",
background: active ? JM.primaryGradient : "#E5E7EB",
color: active ? "#fff" : JM.muted,
fontSize: 14, fontWeight: 600,
fontFamily: JM.fontSans,
cursor: active ? "pointer" : "not-allowed",
display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
boxShadow: active ? JM.primaryShadow : "none",
transition: "box-shadow 0.2s, transform 0.15s, opacity 0.15s",
}}
onMouseEnter={e => {
if (active) {
e.currentTarget.style.boxShadow = JM.primaryShadowHover;
e.currentTarget.style.transform = "translateY(-1px)";
}
}}
onMouseLeave={e => {
e.currentTarget.style.boxShadow = active ? JM.primaryShadow : "none";
e.currentTarget.style.transform = "none";
}}
>
{loading ? (
<>
<span style={{
width: 14, height: 14, borderRadius: "50%",
border: "2px solid rgba(255,255,255,0.35)", borderTopColor: "#fff",
animation: "vibn-spin 0.7s linear infinite", display: "inline-block",
}} />
Creating
</>
) : (
children
)}
</button>
);
}