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
231 lines
6.3 KiB
TypeScript
231 lines
6.3 KiB
TypeScript
"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>
|
||
);
|
||
}
|