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
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { JM } from "./modal-theme";
|
||||
import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
|
||||
|
||||
export function ChatImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
@@ -45,10 +46,10 @@ export function ChatImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
<div style={{ padding: 28 }}>
|
||||
<SetupHeader
|
||||
icon="⌁" label="Import Chats" tagline="You've been thinking"
|
||||
accent="#2e5a4a" onBack={onBack} onClose={onClose}
|
||||
accent="#059669" onBack={onBack} onClose={onClose}
|
||||
/>
|
||||
|
||||
<FieldLabel>Project name</FieldLabel>
|
||||
@@ -66,14 +67,14 @@ export function ChatImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
placeholder={"Paste conversations from ChatGPT, Claude, Gemini, or any AI tool.\n\nVibn will extract decisions, ideas, open questions, and architecture notes."}
|
||||
rows={8}
|
||||
style={{
|
||||
width: "100%", padding: "12px 14px", marginBottom: 20,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.85rem", lineHeight: 1.55,
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", color: "#1a1a1a",
|
||||
width: "100%", padding: "10px 13px", marginBottom: 20,
|
||||
borderRadius: 8, border: `1px solid ${JM.border}`,
|
||||
background: JM.inputBg, fontSize: 13, lineHeight: 1.55,
|
||||
fontFamily: JM.fontSans, color: JM.ink,
|
||||
outline: "none", resize: "vertical", boxSizing: "border-box",
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
|
||||
/>
|
||||
|
||||
<PrimaryButton onClick={handleCreate} disabled={!canCreate} loading={loading}>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { JM } from "./modal-theme";
|
||||
import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
|
||||
|
||||
export function CodeImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
@@ -47,10 +48,10 @@ export function CodeImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
<div style={{ padding: 28 }}>
|
||||
<SetupHeader
|
||||
icon="⌘" label="Import Code" tagline="Already have a repo"
|
||||
accent="#1a3a5c" onBack={onBack} onClose={onClose}
|
||||
accent="#1D4ED8" onBack={onBack} onClose={onClose}
|
||||
/>
|
||||
|
||||
<FieldLabel>Project name</FieldLabel>
|
||||
@@ -70,7 +71,7 @@ export function CodeImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
|
||||
<FieldLabel>
|
||||
Personal Access Token{" "}
|
||||
<span style={{ color: "#b5b0a6", fontWeight: 400 }}>(required for private repos)</span>
|
||||
<span style={{ color: JM.muted, fontWeight: 400 }}>(required for private repos)</span>
|
||||
</FieldLabel>
|
||||
<input
|
||||
type="password"
|
||||
@@ -78,17 +79,21 @@ export function CodeImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
onChange={e => setPat(e.target.value)}
|
||||
placeholder="ghp_… or similar"
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px", marginBottom: 20,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.9rem",
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", color: "#1a1a1a",
|
||||
width: "100%", padding: "10px 13px", marginBottom: 20,
|
||||
borderRadius: 8, border: `1px solid ${JM.border}`,
|
||||
background: JM.inputBg, fontSize: 14,
|
||||
fontFamily: JM.fontSans, color: JM.ink,
|
||||
outline: "none", boxSizing: "border-box",
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
|
||||
/>
|
||||
|
||||
<div style={{ fontSize: "0.75rem", color: "#a09a90", marginBottom: 20, lineHeight: 1.5, padding: "12px 14px", background: "#faf8f5", borderRadius: 8, border: "1px solid #f0ece4" }}>
|
||||
<div style={{
|
||||
fontSize: 12, color: JM.mid, marginBottom: 20, lineHeight: 1.5,
|
||||
padding: "12px 14px", background: JM.cream, borderRadius: 8,
|
||||
border: `1px solid ${JM.border}`, fontFamily: JM.fontSans,
|
||||
}}>
|
||||
Vibn will clone your repo, read key files, and build a full architecture map — tech stack, routes, database, auth, and third-party integrations. Tokens are used only for cloning and are not stored.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,11 +2,20 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Plus_Jakarta_Sans } from "next/font/google";
|
||||
import { TypeSelector } from "./TypeSelector";
|
||||
import { FreshIdeaSetup } from "./FreshIdeaSetup";
|
||||
import { ChatImportSetup } from "./ChatImportSetup";
|
||||
import { CodeImportSetup } from "./CodeImportSetup";
|
||||
import { MigrateSetup } from "./MigrateSetup";
|
||||
import { JM } from "./modal-theme";
|
||||
|
||||
const modalFont = Plus_Jakarta_Sans({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700", "800"],
|
||||
variable: "--font-justine-jakarta",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export type CreationMode = "fresh" | "chat-import" | "code-import" | "migration";
|
||||
|
||||
@@ -62,29 +71,31 @@ export function CreateProjectFlow({ open, onOpenChange, workspace }: CreateProje
|
||||
<div
|
||||
onClick={() => onOpenChange(false)}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 50,
|
||||
background: "rgba(26,26,26,0.38)",
|
||||
position: "fixed", inset: 0, zIndex: 200,
|
||||
background: JM.overlay,
|
||||
backdropFilter: "blur(2px)",
|
||||
WebkitBackdropFilter: "blur(2px)",
|
||||
animation: "vibn-fadeIn 0.15s ease",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Modal container */}
|
||||
{/* Modal container — matches justine/03_dashboard.html #modal-new */}
|
||||
<div style={{
|
||||
position: "fixed", inset: 0, zIndex: 51,
|
||||
position: "fixed", inset: 0, zIndex: 201,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
padding: 24, pointerEvents: "none",
|
||||
}}>
|
||||
<div
|
||||
onClick={e => e.stopPropagation()}
|
||||
className={modalFont.variable}
|
||||
style={{
|
||||
background: "#fff", borderRadius: 16,
|
||||
boxShadow: "0 12px 48px rgba(26,26,26,0.16)",
|
||||
boxShadow: JM.cardShadow,
|
||||
width: "100%",
|
||||
maxWidth: 520,
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
maxWidth: JM.cardMaxWidth,
|
||||
fontFamily: JM.fontSans,
|
||||
pointerEvents: "all",
|
||||
animation: "vibn-slideUp 0.18s cubic-bezier(0.4,0,0.2,1)",
|
||||
transition: "max-width 0.2s ease",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
|
||||
import { JM } from "./modal-theme";
|
||||
import { FieldLabel, TextInput, PrimaryButton, ForWhomSelector, type SetupProps } from "./setup-shared";
|
||||
|
||||
export function FreshIdeaSetup({ workspace, onClose }: SetupProps) {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
const [forWhom, setForWhom] = useState<"personal" | "client">("personal");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -24,7 +26,7 @@ export function FreshIdeaSetup({ workspace, onClose }: SetupProps) {
|
||||
projectName: name.trim(),
|
||||
projectType: "web-app",
|
||||
slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
|
||||
product: { name: name.trim() },
|
||||
product: { name: name.trim(), isForClient: forWhom === "client" },
|
||||
creationMode: "fresh",
|
||||
sourceData: {},
|
||||
}),
|
||||
@@ -45,29 +47,45 @@ export function FreshIdeaSetup({ workspace, onClose }: SetupProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 28 }}>
|
||||
<div style={{ fontSize: "1.15rem", fontWeight: 600, color: "#1a1a1a", fontFamily: "var(--font-lora), ui-serif, serif" }}>
|
||||
<div style={{ padding: 28 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 22 }}>
|
||||
<h3 style={{
|
||||
fontFamily: JM.fontDisplay, fontSize: 18, fontWeight: 700,
|
||||
color: JM.ink, margin: 0, letterSpacing: "-0.02em",
|
||||
}}>
|
||||
New project
|
||||
</div>
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", color: "#a09a90", fontSize: "1.1rem", padding: "2px 6px", lineHeight: 1 }}
|
||||
>×</button>
|
||||
style={{
|
||||
background: "none", border: "none", cursor: "pointer",
|
||||
color: JM.muted, fontSize: 20, padding: 4, lineHeight: 1,
|
||||
fontFamily: JM.fontSans,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = JM.mid)}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = JM.muted)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<FieldLabel>Project name</FieldLabel>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={setName}
|
||||
placeholder="e.g. Foxglove, Meridian, OpsAI…"
|
||||
onKeyDown={e => { if (e.key === "Enter" && canCreate) handleCreate(); }}
|
||||
inputRef={nameRef}
|
||||
autoFocus
|
||||
/>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<FieldLabel>Project name</FieldLabel>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={setName}
|
||||
placeholder="e.g. My SaaS App"
|
||||
onKeyDown={e => { if (e.key === "Enter" && canCreate) void handleCreate(); }}
|
||||
inputRef={nameRef}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PrimaryButton onClick={handleCreate} disabled={!canCreate} loading={loading}>
|
||||
Start →
|
||||
<ForWhomSelector value={forWhom} onChange={setForWhom} />
|
||||
|
||||
<PrimaryButton onClick={() => { void handleCreate(); }} disabled={!canCreate} loading={loading}>
|
||||
Create project →
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { JM } from "./modal-theme";
|
||||
import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
|
||||
|
||||
const HOSTING_OPTIONS = [
|
||||
@@ -70,7 +71,7 @@ export function MigrateSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
<div style={{ padding: 28 }}>
|
||||
<SetupHeader
|
||||
icon="⇢" label="Migrate Product" tagline="Move an existing product"
|
||||
accent="#4a2a5a" onBack={onBack} onClose={onClose}
|
||||
@@ -86,7 +87,7 @@ export function MigrateSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
|
||||
<FieldLabel>
|
||||
Repository URL{" "}
|
||||
<span style={{ color: "#b5b0a6", fontWeight: 400 }}>(recommended)</span>
|
||||
<span style={{ color: JM.muted, fontWeight: 400 }}>(recommended)</span>
|
||||
</FieldLabel>
|
||||
<TextInput
|
||||
value={repoUrl}
|
||||
@@ -96,7 +97,7 @@ export function MigrateSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
|
||||
<FieldLabel>
|
||||
Live URL{" "}
|
||||
<span style={{ color: "#b5b0a6", fontWeight: 400 }}>(optional)</span>
|
||||
<span style={{ color: JM.muted, fontWeight: 400 }}>(optional)</span>
|
||||
</FieldLabel>
|
||||
<TextInput
|
||||
value={liveUrl}
|
||||
@@ -111,14 +112,16 @@ export function MigrateSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
value={hosting}
|
||||
onChange={e => setHosting(e.target.value)}
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px", marginBottom: 16,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.88rem",
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", color: hosting ? "#1a1a1a" : "#a09a90",
|
||||
width: "100%", padding: "10px 13px", marginBottom: 16,
|
||||
borderRadius: 8, border: `1px solid ${JM.border}`,
|
||||
background: JM.inputBg, fontSize: 13,
|
||||
fontFamily: JM.fontSans, color: hosting ? JM.ink : JM.muted,
|
||||
outline: "none", boxSizing: "border-box", appearance: "none",
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23a09a90' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round'/%3E%3C/svg%3E")`,
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%239CA3AF' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: "no-repeat", backgroundPosition: "right 12px center",
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
|
||||
>
|
||||
{HOSTING_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
@@ -127,7 +130,7 @@ export function MigrateSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>
|
||||
PAT{" "}<span style={{ color: "#b5b0a6", fontWeight: 400 }}>(private repos)</span>
|
||||
PAT{" "}<span style={{ color: JM.muted, fontWeight: 400 }}>(private repos)</span>
|
||||
</FieldLabel>
|
||||
<input
|
||||
type="password"
|
||||
@@ -135,20 +138,24 @@ export function MigrateSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
onChange={e => setPat(e.target.value)}
|
||||
placeholder="ghp_…"
|
||||
style={{
|
||||
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",
|
||||
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",
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: "0.75rem", color: "#a09a90", marginBottom: 20, lineHeight: 1.5, padding: "12px 14px", background: "#faf8f5", borderRadius: 8, border: "1px solid #f0ece4" }}>
|
||||
<strong style={{ color: "#4a2a5a" }}>Non-destructive.</strong> Vibn builds a full audit and migration plan. Your existing product stays live throughout the entire migration process.
|
||||
<div style={{
|
||||
fontSize: 12, color: JM.mid, marginBottom: 20, lineHeight: 1.5,
|
||||
padding: "12px 14px", background: JM.cream, borderRadius: 8,
|
||||
border: `1px solid ${JM.border}`, fontFamily: JM.fontSans,
|
||||
}}>
|
||||
<strong style={{ color: "#5B21B6" }}>Non-destructive.</strong> Vibn builds a full audit and migration plan. Your existing product stays live throughout the entire migration process.
|
||||
</div>
|
||||
|
||||
<PrimaryButton onClick={handleCreate} disabled={!canCreate} loading={loading}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { CreationMode } from "./CreateProjectFlow";
|
||||
import { JM } from "./modal-theme";
|
||||
|
||||
interface TypeSelectorProps {
|
||||
onSelect: (mode: CreationMode) => void;
|
||||
@@ -22,7 +23,7 @@ const ALL_FLOW_TYPES: {
|
||||
label: "Fresh Idea",
|
||||
tagline: "Start from scratch",
|
||||
desc: "Talk through your idea with Vibn. We'll explore it together and shape it into a full product plan.",
|
||||
accent: "#4a3728",
|
||||
accent: "#4338CA",
|
||||
},
|
||||
{
|
||||
id: "chat-import",
|
||||
@@ -30,7 +31,7 @@ const ALL_FLOW_TYPES: {
|
||||
label: "Import Chats",
|
||||
tagline: "You've been thinking",
|
||||
desc: "Paste conversations from ChatGPT or Claude. Vibn extracts your decisions, ideas, and open questions.",
|
||||
accent: "#2e5a4a",
|
||||
accent: "#059669",
|
||||
},
|
||||
{
|
||||
id: "code-import",
|
||||
@@ -38,7 +39,7 @@ const ALL_FLOW_TYPES: {
|
||||
label: "Import Code",
|
||||
tagline: "Already have a repo",
|
||||
desc: "Point Vibn at your GitHub or Bitbucket repo. We'll map your stack and show what's missing.",
|
||||
accent: "#1a3a5c",
|
||||
accent: "#1D4ED8",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
@@ -47,7 +48,7 @@ const ALL_FLOW_TYPES: {
|
||||
label: "Migrate Product",
|
||||
tagline: "Move an existing product",
|
||||
desc: "Bring your live product into the VIBN infrastructure. Vibn builds a safe, phased migration plan.",
|
||||
accent: "#4a2a5a",
|
||||
accent: "#7C3AED",
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
@@ -56,89 +57,88 @@ const FLOW_TYPES = ALL_FLOW_TYPES.filter(t => !t.hidden);
|
||||
|
||||
export function TypeSelector({ onSelect, onClose }: TypeSelectorProps) {
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 28 }}>
|
||||
<div style={{ padding: 28 }}>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 22 }}>
|
||||
<div>
|
||||
<h2 style={{
|
||||
fontFamily: "var(--font-lora), ui-serif, serif", fontSize: "1.4rem", fontWeight: 400,
|
||||
color: "#1a1a1a", margin: 0, marginBottom: 4,
|
||||
fontFamily: JM.fontDisplay, fontSize: 18, fontWeight: 700,
|
||||
color: JM.ink, margin: 0, marginBottom: 4, letterSpacing: "-0.02em",
|
||||
}}>
|
||||
Start a new project
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.78rem", color: "#a09a90", margin: 0 }}>
|
||||
<p style={{ fontSize: 13, color: JM.mid, margin: 0, fontFamily: JM.fontSans, lineHeight: 1.45 }}>
|
||||
How would you like to begin?
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: "none", border: "none", cursor: "pointer",
|
||||
color: "#b5b0a6", fontSize: "1.2rem", lineHeight: 1,
|
||||
padding: "2px 5px", borderRadius: 4,
|
||||
color: JM.muted, fontSize: 20, lineHeight: 1,
|
||||
padding: 4, fontFamily: JM.fontSans,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = "#6b6560")}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = JM.mid)}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = JM.muted)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Type cards */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr", gap: 10 }}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr", gap: 8 }}>
|
||||
{FLOW_TYPES.map(type => (
|
||||
<button
|
||||
key={type.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(type.id)}
|
||||
style={{
|
||||
display: "flex", flexDirection: "column", alignItems: "flex-start",
|
||||
gap: 0, padding: "20px", borderRadius: 12, textAlign: "left",
|
||||
border: "1px solid #e8e4dc",
|
||||
background: "#faf8f5",
|
||||
gap: 0, padding: 16, borderRadius: 12, textAlign: "left",
|
||||
border: `1px solid ${JM.border}`,
|
||||
background: JM.inputBg,
|
||||
cursor: "pointer",
|
||||
transition: "all 0.14s",
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
transition: "border-color 0.15s, background 0.15s, box-shadow 0.15s",
|
||||
fontFamily: JM.fontSans,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.borderColor = "#d0ccc4";
|
||||
e.currentTarget.style.background = "#fff";
|
||||
e.currentTarget.style.boxShadow = "0 2px 12px rgba(26,26,26,0.07)";
|
||||
e.currentTarget.style.borderColor = JM.indigo;
|
||||
e.currentTarget.style.background = JM.cream;
|
||||
e.currentTarget.style.boxShadow = "0 2px 12px rgba(99,102,241,0.1)";
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.borderColor = "#e8e4dc";
|
||||
e.currentTarget.style.background = "#faf8f5";
|
||||
e.currentTarget.style.borderColor = JM.border;
|
||||
e.currentTarget.style.background = JM.inputBg;
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div style={{
|
||||
width: 36, height: 36, borderRadius: 9, marginBottom: 14,
|
||||
background: `${type.accent}10`,
|
||||
width: 36, height: 36, borderRadius: 9, marginBottom: 12,
|
||||
background: "rgba(99,102,241,0.12)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "1.1rem", color: type.accent,
|
||||
fontSize: "1.05rem", color: type.accent,
|
||||
}}>
|
||||
{type.icon}
|
||||
</div>
|
||||
|
||||
{/* Label + tagline */}
|
||||
<div style={{ fontSize: "0.88rem", fontWeight: 700, color: "#1a1a1a", marginBottom: 2 }}>
|
||||
<div style={{ fontSize: 13.5, fontWeight: 600, color: JM.ink, marginBottom: 2 }}>
|
||||
{type.label}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.68rem", fontWeight: 600, color: type.accent, letterSpacing: "0.03em", marginBottom: 8, textTransform: "uppercase" }}>
|
||||
<div style={{
|
||||
fontSize: 10.5, fontWeight: 600, color: type.accent,
|
||||
letterSpacing: "0.07em", marginBottom: 8, textTransform: "uppercase",
|
||||
}}>
|
||||
{type.tagline}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div style={{ fontSize: "0.75rem", color: "#8a8478", lineHeight: 1.5 }}>
|
||||
<div style={{ fontSize: 12.5, color: JM.mid, lineHeight: 1.55 }}>
|
||||
{type.desc}
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<div style={{
|
||||
position: "absolute", right: 16, bottom: 16,
|
||||
fontSize: "0.85rem", color: "#c5c0b8",
|
||||
fontSize: 14, color: JM.muted,
|
||||
}}>
|
||||
→
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, CSSProperties } from "react";
|
||||
import { JM } from "./modal-theme";
|
||||
|
||||
export interface SetupProps {
|
||||
workspace: string;
|
||||
@@ -8,7 +9,68 @@ export interface SetupProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
// Shared modal header
|
||||
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,
|
||||
@@ -25,41 +87,47 @@ export function SetupHeader({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 28 }}>
|
||||
<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: "#b5b0a6", fontSize: "1rem", padding: "3px 5px",
|
||||
color: JM.muted, fontSize: "1rem", padding: "3px 5px",
|
||||
borderRadius: 4, lineHeight: 1, flexShrink: 0,
|
||||
fontFamily: JM.fontSans,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = "#1a1a1a")}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = JM.ink)}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = JM.muted)}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<div>
|
||||
<h2 style={{
|
||||
fontFamily: "var(--font-lora), ui-serif, serif", fontSize: "1.3rem", fontWeight: 400,
|
||||
color: "#1a1a1a", margin: 0, marginBottom: 3,
|
||||
fontFamily: JM.fontDisplay, fontSize: 18, fontWeight: 700,
|
||||
color: JM.ink, margin: 0, marginBottom: 3, letterSpacing: "-0.02em",
|
||||
}}>
|
||||
{label}
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.72rem", fontWeight: 600, color: accent, textTransform: "uppercase", letterSpacing: "0.04em", margin: 0 }}>
|
||||
<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: "#b5b0a6", fontSize: "1.2rem", lineHeight: 1,
|
||||
padding: "2px 5px", borderRadius: 4, flexShrink: 0,
|
||||
color: JM.muted, fontSize: 20, lineHeight: 1,
|
||||
padding: 4, flexShrink: 0, fontFamily: JM.fontSans,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = "#6b6560")}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = JM.mid)}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = JM.muted)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -67,14 +135,6 @@ export function SetupHeader({
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -88,13 +148,13 @@ export function TextInput({
|
||||
placeholder?: string;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
autoFocus?: boolean;
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
inputRef?: React.RefObject<HTMLInputElement | null> | 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",
|
||||
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 (
|
||||
@@ -107,8 +167,8 @@ export function TextInput({
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
style={base}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -127,27 +187,44 @@ export function PrimaryButton({
|
||||
const active = !disabled && !loading;
|
||||
return (
|
||||
<button
|
||||
type="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",
|
||||
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: 8,
|
||||
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";
|
||||
}}
|
||||
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" }} />
|
||||
<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}
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user