design(preview): match toggle button sizes to device toggles
This commit is contained in:
@@ -3,8 +3,13 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
Loader2, AlertCircle, ChevronDown, ChevronRight,
|
Loader2,
|
||||||
Box, Container, CircleDot,
|
AlertCircle,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
CircleDot,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { GiteaFileTree } from "@/components/project/gitea-file-tree";
|
import { GiteaFileTree } from "@/components/project/gitea-file-tree";
|
||||||
import { GiteaFileViewer } from "@/components/project/gitea-file-viewer";
|
import { GiteaFileViewer } from "@/components/project/gitea-file-viewer";
|
||||||
@@ -23,10 +28,7 @@ import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy";
|
|||||||
* part of the product surface.
|
* part of the product surface.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Selection =
|
type Selection = { type: "file"; codebaseId: string; path: string } | null;
|
||||||
| { type: "file"; codebaseId: string; path: string }
|
|
||||||
| { type: "image"; uuid: string }
|
|
||||||
| null;
|
|
||||||
|
|
||||||
export default function CodeTab() {
|
export default function CodeTab() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -34,32 +36,14 @@ export default function CodeTab() {
|
|||||||
const { anatomy, loading, error } = useAnatomy(projectId);
|
const { anatomy, loading, error } = useAnatomy(projectId);
|
||||||
|
|
||||||
const codebases = anatomy?.product.codebases ?? null;
|
const codebases = anatomy?.product.codebases ?? null;
|
||||||
const images = anatomy?.product.images ?? null;
|
const reason = anatomy?.codebasesReason;
|
||||||
const reason = anatomy?.codebasesReason;
|
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
||||||
const [selection, setSelection] = useState<Selection>(null);
|
const [selection, setSelection] = useState<Selection>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (codebases && codebases[0]) {
|
|
||||||
setExpanded(prev => (prev.size === 0 ? new Set([codebases[0].id]) : prev));
|
|
||||||
}
|
|
||||||
}, [codebases]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelection(null);
|
setSelection(null);
|
||||||
setExpanded(new Set());
|
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
const toggleCodebase = (id: string) => {
|
|
||||||
setExpanded(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (next.has(id)) next.delete(id);
|
|
||||||
else next.add(id);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const showLoading = loading && !anatomy;
|
const showLoading = loading && !anatomy;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -68,97 +52,78 @@ export default function CodeTab() {
|
|||||||
{/* ── Left rail ── */}
|
{/* ── Left rail ── */}
|
||||||
<section style={leftCol}>
|
<section style={leftCol}>
|
||||||
{showLoading && (
|
{showLoading && (
|
||||||
<Inline><Loader2 size={13} className="animate-spin" /> Loading…</Inline>
|
<Inline>
|
||||||
|
<Loader2 size={13} className="animate-spin" /> Loading…
|
||||||
|
</Inline>
|
||||||
)}
|
)}
|
||||||
{error && !showLoading && (
|
{error && !showLoading && (
|
||||||
<Inline><AlertCircle size={13} /> {error}</Inline>
|
<Inline>
|
||||||
|
<AlertCircle size={13} /> {error}
|
||||||
|
</Inline>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{anatomy && (
|
{anatomy && (
|
||||||
<>
|
<>
|
||||||
{/* Codebases */}
|
{/* Code Files */}
|
||||||
<RailGroup title="Codebases" count={codebases?.length ?? 0}>
|
<RailGroup title="Code files" count={codebases?.length ?? 0}>
|
||||||
{codebases && codebases.length === 0 && (
|
{codebases && codebases.length === 0 && (
|
||||||
<RailEmpty>
|
<RailEmpty>
|
||||||
{reason === "no_repo"
|
{reason === "no_repo" ? (
|
||||||
? <>No codebase yet. <span style={nudge}>Try: "Start building my app"</span></>
|
<>
|
||||||
: <>Repo is empty — push a first commit. <span style={nudge}>Try: "Scaffold a Next.js app"</span></>}
|
No codebase yet.{" "}
|
||||||
|
<span style={nudge}>
|
||||||
|
Try: "Start building my app"
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Repo is empty — push a first commit.{" "}
|
||||||
|
<span style={nudge}>
|
||||||
|
Try: "Scaffold a Next.js app"
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</RailEmpty>
|
</RailEmpty>
|
||||||
)}
|
)}
|
||||||
{codebases?.map(cb => {
|
{codebases?.map((cb) => {
|
||||||
const isOpen = expanded.has(cb.id);
|
|
||||||
return (
|
return (
|
||||||
<article key={cb.id} style={codebaseTile}>
|
<article key={cb.id} style={codebaseTile}>
|
||||||
<button
|
<div style={tileHeader}>
|
||||||
type="button"
|
|
||||||
onClick={() => toggleCodebase(cb.id)}
|
|
||||||
style={tileHeader}
|
|
||||||
aria-expanded={isOpen}
|
|
||||||
>
|
|
||||||
<span style={chevronCell}>
|
<span style={chevronCell}>
|
||||||
{isOpen
|
<ChevronDown size={13} style={{ color: INK.mid }} />
|
||||||
? <ChevronDown size={13} style={{ color: INK.mid }} />
|
|
||||||
: <ChevronRight size={13} style={{ color: INK.mid }} />}
|
|
||||||
</span>
|
</span>
|
||||||
<Box size={13} style={{ color: INK.mid, flexShrink: 0 }} />
|
<Box
|
||||||
|
size={13}
|
||||||
|
style={{ color: INK.mid, flexShrink: 0 }}
|
||||||
|
/>
|
||||||
<div style={{ minWidth: 0, textAlign: "left" }}>
|
<div style={{ minWidth: 0, textAlign: "left" }}>
|
||||||
<div style={tileLabel}>{cb.label}</div>
|
<div style={tileLabel}>{cb.label}</div>
|
||||||
{cb.hint && <div style={tileHint}>{cb.hint}</div>}
|
{cb.hint && <div style={tileHint}>{cb.hint}</div>}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
{isOpen && (
|
<div style={tileBody}>
|
||||||
<div style={tileBody}>
|
<GiteaFileTree
|
||||||
<GiteaFileTree
|
projectId={projectId}
|
||||||
projectId={projectId}
|
rootPath={cb.path}
|
||||||
rootPath={cb.path}
|
selectedPath={
|
||||||
selectedPath={
|
selection?.type === "file" &&
|
||||||
selection?.type === "file" && selection.codebaseId === cb.id
|
selection.codebaseId === cb.id
|
||||||
? selection.path
|
? selection.path
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onSelectFile={(p) =>
|
onSelectFile={(p) =>
|
||||||
setSelection({ type: "file", codebaseId: cb.id, path: p })
|
setSelection({
|
||||||
}
|
type: "file",
|
||||||
/>
|
codebaseId: cb.id,
|
||||||
</div>
|
path: p,
|
||||||
)}
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</RailGroup>
|
</RailGroup>
|
||||||
|
|
||||||
{/* Images */}
|
|
||||||
<RailGroup title="Images" count={images?.length ?? 0}>
|
|
||||||
{images && images.length === 0 && (
|
|
||||||
<RailEmpty>
|
|
||||||
Self-hosted tools (Twenty CRM, n8n, Plausible…) you run appear here.
|
|
||||||
<span style={nudge}>Try: "Install Twenty CRM for my project"</span>
|
|
||||||
</RailEmpty>
|
|
||||||
)}
|
|
||||||
{images?.map(img => (
|
|
||||||
<button
|
|
||||||
key={img.uuid}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelection({ type: "image", uuid: img.uuid })}
|
|
||||||
style={{
|
|
||||||
...flatTile,
|
|
||||||
borderColor: selection?.type === "image" && selection.uuid === img.uuid ? INK.ink : INK.borderSoft,
|
|
||||||
boxShadow: selection?.type === "image" && selection.uuid === img.uuid ? `0 0 0 1px ${INK.ink}` : "none",
|
|
||||||
background: selection?.type === "image" && selection.uuid === img.uuid ? "#fffdf8" : INK.cardBg,
|
|
||||||
}}
|
|
||||||
aria-pressed={selection?.type === "image" && selection.uuid === img.uuid}
|
|
||||||
>
|
|
||||||
<Container size={13} style={{ color: INK.mid, flexShrink: 0 }} />
|
|
||||||
<div style={{ minWidth: 0, textAlign: "left", flex: 1 }}>
|
|
||||||
<div style={tileLabel}>{img.name}</div>
|
|
||||||
<div style={tileHint}>
|
|
||||||
{img.image}{img.version ? `:${img.version}` : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{img.status && <CircleDot size={9} style={{ color: statusColor(img.status), flexShrink: 0 }} />}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</RailGroup>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -170,12 +135,7 @@ export default function CodeTab() {
|
|||||||
{selection?.type === "file" && (
|
{selection?.type === "file" && (
|
||||||
<GiteaFileViewer projectId={projectId} path={selection.path} />
|
<GiteaFileViewer projectId={projectId} path={selection.path} />
|
||||||
)}
|
)}
|
||||||
{selection?.type === "image" && anatomy && (
|
{!selection && <Empty>Pick a codebase file on the left.</Empty>}
|
||||||
<ImageDetail uuid={selection.uuid} anatomy={anatomy} />
|
|
||||||
)}
|
|
||||||
{!selection && (
|
|
||||||
<Empty>Pick a codebase file or an image on the left.</Empty>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,39 +143,19 @@ export default function CodeTab() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
// Image details (right pane)
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function ImageDetail({ uuid, anatomy }: { uuid: string; anatomy: Anatomy }) {
|
|
||||||
const img = anatomy.product.images.find(i => i.uuid === uuid);
|
|
||||||
if (!img) return <Empty>This image is no longer in the project.</Empty>;
|
|
||||||
const live = anatomy.hosting.live.find(l => l.uuid === uuid);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
||||||
<DetailRow label="Image" value={img.image} />
|
|
||||||
<DetailRow label="Version" value={img.version || "latest"} />
|
|
||||||
<DetailRow label="Type" value={img.serviceType ?? "—"} />
|
|
||||||
<DetailRow
|
|
||||||
label="Status"
|
|
||||||
value={img.status ?? "unknown"}
|
|
||||||
dot={statusColor(img.status ?? "")}
|
|
||||||
/>
|
|
||||||
{live?.fqdn && (
|
|
||||||
<DetailRow label="URL" value={live.fqdn} href={`https://${live.fqdn}`} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// Bits
|
// Bits
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
|
|
||||||
function RailGroup({
|
function RailGroup({
|
||||||
title, count, children,
|
title,
|
||||||
}: { title: string; count: number; children: React.ReactNode }) {
|
count,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
count: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div style={railGroup}>
|
<div style={railGroup}>
|
||||||
<header style={railGroupHeader}>
|
<header style={railGroupHeader}>
|
||||||
@@ -232,16 +172,28 @@ function RailEmpty({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DetailRow({
|
function DetailRow({
|
||||||
label, value, dot, href,
|
label,
|
||||||
}: { label: string; value: string; dot?: string; href?: string }) {
|
value,
|
||||||
|
dot,
|
||||||
|
href,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
dot?: string;
|
||||||
|
href?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div style={detailRow}>
|
<div style={detailRow}>
|
||||||
<span style={detailLabel}>{label}</span>
|
<span style={detailLabel}>{label}</span>
|
||||||
<span style={detailValue}>
|
<span style={detailValue}>
|
||||||
{dot && <CircleDot size={9} style={{ color: dot, marginRight: 6 }} />}
|
{dot && <CircleDot size={9} style={{ color: dot, marginRight: 6 }} />}
|
||||||
{href ? (
|
{href ? (
|
||||||
<a href={href} target="_blank" rel="noreferrer" style={detailLink}>{value}</a>
|
<a href={href} target="_blank" rel="noreferrer" style={detailLink}>
|
||||||
) : value}
|
{value}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
value
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -249,11 +201,19 @@ function DetailRow({
|
|||||||
|
|
||||||
function Inline({ children }: { children: React.ReactNode }) {
|
function Inline({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
display: "flex", alignItems: "center", gap: 8,
|
style={{
|
||||||
padding: "12px 14px", fontSize: "0.82rem", color: INK.mid,
|
display: "flex",
|
||||||
background: INK.cardBg, border: `1px solid ${INK.borderSoft}`, borderRadius: 8,
|
alignItems: "center",
|
||||||
}}>
|
gap: 8,
|
||||||
|
padding: "12px 14px",
|
||||||
|
fontSize: "0.82rem",
|
||||||
|
color: INK.mid,
|
||||||
|
background: INK.cardBg,
|
||||||
|
border: `1px solid ${INK.borderSoft}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -261,10 +221,18 @@ function Inline({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
function Empty({ children }: { children: React.ReactNode }) {
|
function Empty({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
flex: 1, display: "flex", alignItems: "center", justifyContent: "center",
|
style={{
|
||||||
color: INK.mid, fontSize: "0.85rem", padding: "32px 16px", textAlign: "center",
|
flex: 1,
|
||||||
}}>
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: INK.mid,
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
padding: "32px 16px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -275,7 +243,7 @@ function Empty({ children }: { children: React.ReactNode }) {
|
|||||||
function paneHeading(s: Selection): string {
|
function paneHeading(s: Selection): string {
|
||||||
if (!s) return "Preview";
|
if (!s) return "Preview";
|
||||||
if (s.type === "file") return `Preview · ${shortPath(s.path)}`;
|
if (s.type === "file") return `Preview · ${shortPath(s.path)}`;
|
||||||
return "Image";
|
return "Preview";
|
||||||
}
|
}
|
||||||
function shortPath(p: string) {
|
function shortPath(p: string) {
|
||||||
const parts = p.split("/");
|
const parts = p.split("/");
|
||||||
@@ -286,7 +254,8 @@ function statusColor(status: string) {
|
|||||||
const s = status.toLowerCase();
|
const s = status.toLowerCase();
|
||||||
if (s.includes("running") || s.includes("healthy")) return "#2e7d32";
|
if (s.includes("running") || s.includes("healthy")) return "#2e7d32";
|
||||||
if (s.includes("starting") || s.includes("deploying")) return "#d4a04a";
|
if (s.includes("starting") || s.includes("deploying")) return "#d4a04a";
|
||||||
if (s.includes("exit") || s.includes("fail") || s.includes("unhealthy")) return "#c5392b";
|
if (s.includes("exit") || s.includes("fail") || s.includes("unhealthy"))
|
||||||
|
return "#c5392b";
|
||||||
return "#a09a90";
|
return "#a09a90";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,79 +287,156 @@ const grid: React.CSSProperties = {
|
|||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
};
|
};
|
||||||
const leftCol: React.CSSProperties = {
|
const leftCol: React.CSSProperties = {
|
||||||
minWidth: 0, display: "flex", flexDirection: "column", gap: 18,
|
minWidth: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 18,
|
||||||
};
|
};
|
||||||
const rightCol: React.CSSProperties = {
|
const rightCol: React.CSSProperties = {
|
||||||
minWidth: 0, display: "flex", flexDirection: "column",
|
minWidth: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
};
|
};
|
||||||
const heading: React.CSSProperties = {
|
const heading: React.CSSProperties = {
|
||||||
fontSize: "0.72rem", fontWeight: 600, letterSpacing: "0.12em",
|
fontSize: "0.72rem",
|
||||||
textTransform: "uppercase", color: INK.muted, margin: "0 0 14px",
|
fontWeight: 600,
|
||||||
|
letterSpacing: "0.12em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: INK.muted,
|
||||||
|
margin: "0 0 14px",
|
||||||
|
};
|
||||||
|
const railGroup: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
};
|
};
|
||||||
const railGroup: React.CSSProperties = { display: "flex", flexDirection: "column" };
|
|
||||||
const railGroupHeader: React.CSSProperties = {
|
const railGroupHeader: React.CSSProperties = {
|
||||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
padding: "0 4px 8px",
|
padding: "0 4px 8px",
|
||||||
};
|
};
|
||||||
const railGroupTitle: React.CSSProperties = {
|
const railGroupTitle: React.CSSProperties = {
|
||||||
fontSize: "0.68rem", fontWeight: 600, letterSpacing: "0.12em",
|
fontSize: "0.68rem",
|
||||||
textTransform: "uppercase", color: INK.muted,
|
fontWeight: 600,
|
||||||
|
letterSpacing: "0.12em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: INK.muted,
|
||||||
};
|
};
|
||||||
const countPill: React.CSSProperties = {
|
const countPill: React.CSSProperties = {
|
||||||
fontSize: "0.7rem", fontWeight: 600, color: INK.mid,
|
fontSize: "0.7rem",
|
||||||
padding: "1px 7px", borderRadius: 999, background: "#f3eee4",
|
fontWeight: 600,
|
||||||
|
color: INK.mid,
|
||||||
|
padding: "1px 7px",
|
||||||
|
borderRadius: 999,
|
||||||
|
background: "#f3eee4",
|
||||||
|
};
|
||||||
|
const railItems: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 10,
|
||||||
};
|
};
|
||||||
const railItems: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 10 };
|
|
||||||
const railEmpty: React.CSSProperties = {
|
const railEmpty: React.CSSProperties = {
|
||||||
padding: "10px 12px", fontSize: "0.74rem", color: INK.muted,
|
padding: "10px 12px",
|
||||||
border: `1px dashed ${INK.borderSoft}`, borderRadius: 8,
|
fontSize: "0.74rem",
|
||||||
|
color: INK.muted,
|
||||||
|
border: `1px dashed ${INK.borderSoft}`,
|
||||||
|
borderRadius: 8,
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
};
|
};
|
||||||
const nudge: React.CSSProperties = {
|
const nudge: React.CSSProperties = {
|
||||||
display: "block", marginTop: 6, fontStyle: "normal",
|
display: "block",
|
||||||
background: "#f3eee4", borderRadius: 4, padding: "3px 8px",
|
marginTop: 6,
|
||||||
fontSize: "0.72rem", color: "#7a6a50",
|
fontStyle: "normal",
|
||||||
|
background: "#f3eee4",
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: "3px 8px",
|
||||||
|
fontSize: "0.72rem",
|
||||||
|
color: "#7a6a50",
|
||||||
};
|
};
|
||||||
const flatTile: React.CSSProperties = {
|
const flatTile: React.CSSProperties = {
|
||||||
display: "flex", alignItems: "center", gap: 10,
|
display: "flex",
|
||||||
width: "100%", padding: "12px 14px",
|
alignItems: "center",
|
||||||
background: INK.cardBg, border: `1px solid ${INK.borderSoft}`, borderRadius: 10,
|
gap: 10,
|
||||||
cursor: "pointer", font: "inherit", color: "inherit",
|
width: "100%",
|
||||||
|
padding: "12px 14px",
|
||||||
|
background: INK.cardBg,
|
||||||
|
border: `1px solid ${INK.borderSoft}`,
|
||||||
|
borderRadius: 10,
|
||||||
|
cursor: "pointer",
|
||||||
|
font: "inherit",
|
||||||
|
color: "inherit",
|
||||||
transition: "border-color 0.12s, background 0.12s, box-shadow 0.12s",
|
transition: "border-color 0.12s, background 0.12s, box-shadow 0.12s",
|
||||||
};
|
};
|
||||||
const codebaseTile: React.CSSProperties = {
|
const codebaseTile: React.CSSProperties = {
|
||||||
background: INK.cardBg, border: `1px solid ${INK.borderSoft}`, borderRadius: 10, overflow: "hidden",
|
background: INK.cardBg,
|
||||||
|
border: `1px solid ${INK.borderSoft}`,
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: "hidden",
|
||||||
};
|
};
|
||||||
const tileHeader: React.CSSProperties = {
|
const tileHeader: React.CSSProperties = {
|
||||||
display: "flex", alignItems: "center", gap: 8, width: "100%",
|
display: "flex",
|
||||||
padding: "12px 14px", background: "transparent", border: "none",
|
alignItems: "center",
|
||||||
cursor: "pointer", font: "inherit", color: "inherit",
|
gap: 8,
|
||||||
|
width: "100%",
|
||||||
|
padding: "12px 14px",
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
font: "inherit",
|
||||||
|
color: "inherit",
|
||||||
};
|
};
|
||||||
const tileLabel: React.CSSProperties = {
|
const tileLabel: React.CSSProperties = {
|
||||||
fontSize: "0.85rem", fontWeight: 600, color: INK.ink, marginBottom: 2,
|
fontSize: "0.85rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: INK.ink,
|
||||||
|
marginBottom: 2,
|
||||||
|
};
|
||||||
|
const tileHint: React.CSSProperties = {
|
||||||
|
fontSize: "0.74rem",
|
||||||
|
color: INK.mid,
|
||||||
|
lineHeight: 1.4,
|
||||||
};
|
};
|
||||||
const tileHint: React.CSSProperties = { fontSize: "0.74rem", color: INK.mid, lineHeight: 1.4 };
|
|
||||||
const tileBody: React.CSSProperties = {
|
const tileBody: React.CSSProperties = {
|
||||||
padding: "8px 10px 12px", borderTop: `1px solid ${INK.borderSoft}`,
|
padding: "8px 10px 12px",
|
||||||
|
borderTop: `1px solid ${INK.borderSoft}`,
|
||||||
};
|
};
|
||||||
const chevronCell: React.CSSProperties = {
|
const chevronCell: React.CSSProperties = {
|
||||||
width: 14, display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
width: 14,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
};
|
};
|
||||||
const panel: React.CSSProperties = {
|
const panel: React.CSSProperties = {
|
||||||
background: INK.cardBg, border: `1px solid ${INK.border}`, borderRadius: 10,
|
background: INK.cardBg,
|
||||||
padding: 16, flex: 1, minHeight: 0, display: "flex", flexDirection: "column",
|
border: `1px solid ${INK.border}`,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 16,
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
};
|
};
|
||||||
const detailRow: React.CSSProperties = {
|
const detailRow: React.CSSProperties = {
|
||||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
display: "flex",
|
||||||
padding: "12px 4px", borderBottom: `1px solid ${INK.borderSoft}`,
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "12px 4px",
|
||||||
|
borderBottom: `1px solid ${INK.borderSoft}`,
|
||||||
};
|
};
|
||||||
const detailLabel: React.CSSProperties = {
|
const detailLabel: React.CSSProperties = {
|
||||||
fontSize: "0.72rem", fontWeight: 600, letterSpacing: "0.06em",
|
fontSize: "0.72rem",
|
||||||
textTransform: "uppercase", color: INK.muted,
|
fontWeight: 600,
|
||||||
|
letterSpacing: "0.06em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: INK.muted,
|
||||||
};
|
};
|
||||||
const detailValue: React.CSSProperties = {
|
const detailValue: React.CSSProperties = {
|
||||||
fontSize: "0.85rem", color: INK.ink, display: "inline-flex", alignItems: "center",
|
fontSize: "0.85rem",
|
||||||
|
color: INK.ink,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
};
|
};
|
||||||
const detailLink: React.CSSProperties = {
|
const detailLink: React.CSSProperties = {
|
||||||
color: INK.ink, textDecoration: "underline",
|
color: INK.ink,
|
||||||
|
textDecoration: "underline",
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,233 +1,6 @@
|
|||||||
"use client";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { useState } from "react";
|
export default async function SettingsPage({ params }: { params: Promise<{ workspace: string; projectId: string }> }) {
|
||||||
import { useParams, useRouter } from "next/navigation";
|
const { workspace, projectId } = await params;
|
||||||
import { Settings, Trash2, AlertTriangle, Loader2, ArrowLeft } from "lucide-react";
|
redirect(`/${workspace}/project/${projectId}/settings/app`);
|
||||||
import { WorkspaceKeysPanel } from "@/components/workspace/WorkspaceKeysPanel";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project settings page.
|
|
||||||
* Accessible via the gear icon in the project header.
|
|
||||||
*
|
|
||||||
* Sections:
|
|
||||||
* - General (name, description — future)
|
|
||||||
* - Danger zone: delete project
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function ProjectSettingsPage() {
|
|
||||||
const params = useParams();
|
|
||||||
const router = useRouter();
|
|
||||||
const projectId = params.projectId as string;
|
|
||||||
const workspace = params.workspace as string;
|
|
||||||
|
|
||||||
const [deletePhase, setDeletePhase] = useState<"idle" | "confirm" | "deleting" | "done">("idle");
|
|
||||||
const [confirmInput, setConfirmInput] = useState("");
|
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const projectBackUrl = `/${workspace}/project/${projectId}/plan`;
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (deletePhase === "idle") {
|
|
||||||
setDeletePhase("confirm");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (deletePhase !== "confirm") return;
|
|
||||||
if (confirmInput.toLowerCase() !== "delete") return;
|
|
||||||
|
|
||||||
setDeletePhase("deleting");
|
|
||||||
setDeleteError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch("/api/projects/delete", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ projectId }),
|
|
||||||
});
|
|
||||||
const d = await r.json();
|
|
||||||
if (!r.ok) throw new Error(d.error || "Delete failed");
|
|
||||||
setDeletePhase("done");
|
|
||||||
setTimeout(() => router.push(`/${workspace}/projects`), 1500);
|
|
||||||
} catch (e) {
|
|
||||||
setDeleteError(e instanceof Error ? e.message : String(e));
|
|
||||||
setDeletePhase("confirm");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={pageWrap}>
|
|
||||||
{/* Back link */}
|
|
||||||
<Link href={projectBackUrl} style={backLink}>
|
|
||||||
<ArrowLeft size={14} /> Back to project
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<h1 style={pageTitle}>
|
|
||||||
<Settings size={18} /> Project settings
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 40 }}><WorkspaceKeysPanel workspaceSlug={workspace} /></div>
|
|
||||||
|
|
||||||
{/* ── Danger zone ── */}
|
|
||||||
<section style={dangerSection}>
|
|
||||||
<h2 style={sectionTitle}>
|
|
||||||
<AlertTriangle size={15} style={{ color: DANGER }} />
|
|
||||||
Danger zone
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div style={dangerCard}>
|
|
||||||
<div style={dangerCardBody}>
|
|
||||||
<div>
|
|
||||||
<div style={dangerItemTitle}>Delete this project</div>
|
|
||||||
<div style={dangerItemDesc}>
|
|
||||||
Removes all project data from Vibn. Coolify services and databases
|
|
||||||
are <strong>not</strong> automatically stopped — use the chat to clean those
|
|
||||||
up first, or remove them from Coolify directly.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{deletePhase === "idle" && (
|
|
||||||
<button onClick={handleDelete} style={dangerBtn}>
|
|
||||||
<Trash2 size={13} /> Delete project
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{deletePhase === "confirm" && (
|
|
||||||
<div style={confirmBox}>
|
|
||||||
<div style={{ fontSize: "0.82rem", color: DANGER, fontWeight: 600, marginBottom: 8 }}>
|
|
||||||
Type <strong>delete</strong> to confirm
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
value={confirmInput}
|
|
||||||
onChange={e => setConfirmInput(e.target.value)}
|
|
||||||
onKeyDown={e => e.key === "Enter" && confirmInput.toLowerCase() === "delete" && handleDelete()}
|
|
||||||
placeholder="delete"
|
|
||||||
style={confirmInput_}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleDelete}
|
|
||||||
disabled={confirmInput.toLowerCase() !== "delete"}
|
|
||||||
style={{
|
|
||||||
...dangerBtn,
|
|
||||||
opacity: confirmInput.toLowerCase() !== "delete" ? 0.4 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 size={13} /> Confirm delete
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => { setDeletePhase("idle"); setConfirmInput(""); setDeleteError(null); }}
|
|
||||||
style={cancelBtn}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{deleteError && (
|
|
||||||
<div style={{ marginTop: 8, fontSize: "0.8rem", color: DANGER }}>{deleteError}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{deletePhase === "deleting" && (
|
|
||||||
<button style={{ ...dangerBtn, opacity: 0.6 }} disabled>
|
|
||||||
<Loader2 size={13} className="animate-spin" /> Deleting…
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{deletePhase === "done" && (
|
|
||||||
<div style={{ fontSize: "0.85rem", color: "#2e7d32", fontWeight: 600 }}>
|
|
||||||
Project deleted. Redirecting…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
// Tokens
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const DANGER = "#c5392b";
|
|
||||||
|
|
||||||
const INK = {
|
|
||||||
ink: "#1a1a1a",
|
|
||||||
mid: "#5f5e5a",
|
|
||||||
muted: "#a09a90",
|
|
||||||
border: "#e8e4dc",
|
|
||||||
borderSoft: "#efebe1",
|
|
||||||
fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
// Styles
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const pageWrap: React.CSSProperties = {
|
|
||||||
padding: "28px 48px 64px",
|
|
||||||
fontFamily: INK.fontSans,
|
|
||||||
color: INK.ink,
|
|
||||||
maxWidth: 720,
|
|
||||||
};
|
|
||||||
const backLink: React.CSSProperties = {
|
|
||||||
display: "inline-flex", alignItems: "center", gap: 6,
|
|
||||||
fontSize: "0.8rem", color: INK.mid, textDecoration: "none",
|
|
||||||
marginBottom: 24,
|
|
||||||
};
|
|
||||||
const pageTitle: React.CSSProperties = {
|
|
||||||
display: "flex", alignItems: "center", gap: 10,
|
|
||||||
fontSize: "1.25rem", fontWeight: 700, color: INK.ink,
|
|
||||||
marginBottom: 36, marginTop: 0,
|
|
||||||
};
|
|
||||||
const dangerSection: React.CSSProperties = { marginTop: 32 };
|
|
||||||
const sectionTitle: React.CSSProperties = {
|
|
||||||
display: "flex", alignItems: "center", gap: 8,
|
|
||||||
fontSize: "0.72rem", fontWeight: 700, letterSpacing: "0.12em",
|
|
||||||
textTransform: "uppercase", color: INK.muted,
|
|
||||||
marginBottom: 12,
|
|
||||||
};
|
|
||||||
const dangerCard: React.CSSProperties = {
|
|
||||||
border: `1px solid #f0cac5`,
|
|
||||||
borderRadius: 10,
|
|
||||||
background: "#fffaf9",
|
|
||||||
};
|
|
||||||
const dangerCardBody: React.CSSProperties = {
|
|
||||||
padding: "18px 20px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: 24,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
};
|
|
||||||
const dangerItemTitle: React.CSSProperties = {
|
|
||||||
fontWeight: 600, fontSize: "0.9rem", color: INK.ink, marginBottom: 4,
|
|
||||||
};
|
|
||||||
const dangerItemDesc: React.CSSProperties = {
|
|
||||||
fontSize: "0.8rem", color: INK.mid, lineHeight: 1.55, maxWidth: 380,
|
|
||||||
};
|
|
||||||
const dangerBtn: React.CSSProperties = {
|
|
||||||
display: "inline-flex", alignItems: "center", gap: 6,
|
|
||||||
padding: "7px 14px", border: `1px solid ${DANGER}`,
|
|
||||||
borderRadius: 6, background: "#fff", cursor: "pointer",
|
|
||||||
font: "inherit", fontSize: "0.8rem", fontWeight: 600, color: DANGER,
|
|
||||||
whiteSpace: "nowrap", flexShrink: 0,
|
|
||||||
};
|
|
||||||
const cancelBtn: React.CSSProperties = {
|
|
||||||
display: "inline-flex", alignItems: "center",
|
|
||||||
padding: "7px 12px", border: `1px solid ${INK.border}`,
|
|
||||||
borderRadius: 6, background: "#fff", cursor: "pointer",
|
|
||||||
font: "inherit", fontSize: "0.8rem", color: INK.mid,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
};
|
|
||||||
const confirmBox: React.CSSProperties = { display: "flex", flexDirection: "column" };
|
|
||||||
const confirmInput_: React.CSSProperties = {
|
|
||||||
padding: "7px 10px",
|
|
||||||
border: `1px solid ${DANGER}`,
|
|
||||||
borderRadius: 6,
|
|
||||||
font: "inherit",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
outline: "none",
|
|
||||||
width: 100,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Users,
|
Users,
|
||||||
|
HardDrive,
|
||||||
|
Blocks,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { useAnatomy } from "@/components/project/use-anatomy";
|
||||||
|
|
||||||
export function DashboardSidebar({
|
export function DashboardSidebar({
|
||||||
workspace,
|
workspace,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -39,23 +43,40 @@ export function DashboardSidebar({
|
|||||||
Record<string, boolean>
|
Record<string, boolean>
|
||||||
>({
|
>({
|
||||||
settings: true,
|
settings: true,
|
||||||
|
data: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const { anatomy } = useAnatomy(projectId);
|
||||||
|
const databases = anatomy?.infrastructure?.databases ?? [];
|
||||||
|
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSection = (section: string) => {
|
const handleSectionClick = (segment: string) => {
|
||||||
setExpandedSections((prev) => ({ ...prev, [section]: !prev[section] }));
|
if (!expandedSections[segment]) {
|
||||||
|
setExpandedSections((prev) => ({ ...prev, [segment]: true }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ segment: "overview", label: "Overview", Icon: LayoutGrid },
|
{ segment: "overview", label: "Overview", Icon: LayoutGrid },
|
||||||
{ segment: "plan", label: "Plan & Specs", Icon: ClipboardList },
|
{ segment: "plan", label: "Plan & Specs", Icon: ClipboardList },
|
||||||
{ segment: "code", label: "Code", Icon: Code2 },
|
{ segment: "code", label: "Code", Icon: Code2 },
|
||||||
{ segment: "data", label: "Data", Icon: Database },
|
{
|
||||||
|
segment: "data",
|
||||||
|
label: "Data",
|
||||||
|
Icon: Database,
|
||||||
|
hasChildren: true,
|
||||||
|
children: databases.map((db) => ({
|
||||||
|
segment: `data/tables?db=${db.uuid}`,
|
||||||
|
label: db.name,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{ segment: "storage", label: "Storage", Icon: HardDrive },
|
||||||
|
{ segment: "services", label: "Services", Icon: Blocks },
|
||||||
{ segment: "users", label: "Auth / Users", Icon: Users },
|
{ segment: "users", label: "Auth / Users", Icon: Users },
|
||||||
{ segment: "integrations", label: "Integrations", Icon: Plug },
|
{ segment: "integrations", label: "Integrations", Icon: Plug },
|
||||||
{ segment: "security", label: "Security", Icon: ShieldCheck },
|
{ segment: "security", label: "Security", Icon: ShieldCheck },
|
||||||
@@ -67,6 +88,17 @@ export function DashboardSidebar({
|
|||||||
Icon: BarChart2,
|
Icon: BarChart2,
|
||||||
badge: "Soon",
|
badge: "Soon",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
segment: "marketing",
|
||||||
|
label: "Marketing",
|
||||||
|
Icon: BarChart2,
|
||||||
|
badge: "New",
|
||||||
|
hasChildren: true,
|
||||||
|
children: [
|
||||||
|
{ segment: "marketing/seo", label: "SEO & GEO" },
|
||||||
|
{ segment: "marketing/social", label: "Social content" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
segment: "settings",
|
segment: "settings",
|
||||||
label: "Settings",
|
label: "Settings",
|
||||||
@@ -170,9 +202,10 @@ export function DashboardSidebar({
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.hasChildren) {
|
if (item.hasChildren) {
|
||||||
toggleSection(item.segment);
|
setExpandedSections((prev) => ({
|
||||||
} else {
|
...prev,
|
||||||
// Navigate via link logic would happen here, we'll wrap the icon/label in a link
|
[item.segment]: !prev[item.segment],
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -245,28 +278,41 @@ export function DashboardSidebar({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.children.map((child) => {
|
{item.children.map((child) => {
|
||||||
const isChildActive =
|
const href = child.segment.includes("?")
|
||||||
pathname === `${projectBase}/${child.segment}`;
|
? `${projectBase}/${child.segment.split("?")[0]}?${child.segment.split("?")[1]}`
|
||||||
|
: `${projectBase}/${child.segment}`;
|
||||||
|
let isChildActive = false;
|
||||||
|
if (child.segment.includes("?")) {
|
||||||
|
const [basePath, searchStr] = child.segment.split("?");
|
||||||
|
isChildActive =
|
||||||
|
pathname === `${projectBase}/${basePath}` &&
|
||||||
|
(typeof window !== "undefined"
|
||||||
|
? window.location.search.includes(searchStr)
|
||||||
|
: false);
|
||||||
|
} else {
|
||||||
|
isChildActive =
|
||||||
|
pathname === `${projectBase}/${child.segment}`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={child.segment}
|
key={child.segment}
|
||||||
href={`${projectBase}/${child.segment}`}
|
href={href}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "6px 10px 6px 36px",
|
padding: "6px 10px 6px 14px",
|
||||||
borderRadius: 8,
|
marginLeft: "18px",
|
||||||
|
borderRadius: "0 8px 8px 0",
|
||||||
fontSize: "0.8rem",
|
fontSize: "0.8rem",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
color: isChildActive ? "#18181b" : "#52525b",
|
color: isChildActive ? "#18181b" : "#52525b",
|
||||||
background: isChildActive
|
background: "transparent",
|
||||||
? "#f4f4f5"
|
|
||||||
: "transparent",
|
|
||||||
transition: "all 0.1s ease",
|
transition: "all 0.1s ease",
|
||||||
border: isChildActive
|
borderLeft: isChildActive
|
||||||
? "1px solid #e4e4e7"
|
? "2px solid #18181b"
|
||||||
: "1px solid transparent",
|
: "2px solid transparent",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{child.label}
|
{child.label}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export function ProjectIconRail({ workspace, projectId, actions }: Props) {
|
|||||||
fontSize: "0.75rem",
|
fontSize: "0.75rem",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: 24, // Explicitly match device toggles height
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
background: isPreviewActive ? "#ffffff" : "transparent",
|
background: isPreviewActive ? "#ffffff" : "transparent",
|
||||||
color: isPreviewActive ? "#18181b" : "#71717a",
|
color: isPreviewActive ? "#18181b" : "#71717a",
|
||||||
@@ -53,6 +56,9 @@ export function ProjectIconRail({ workspace, projectId, actions }: Props) {
|
|||||||
fontSize: "0.75rem",
|
fontSize: "0.75rem",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: 24, // Explicitly match device toggles height
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
background: !isPreviewActive ? "#ffffff" : "transparent",
|
background: !isPreviewActive ? "#ffffff" : "transparent",
|
||||||
color: !isPreviewActive ? "#18181b" : "#71717a",
|
color: !isPreviewActive ? "#18181b" : "#71717a",
|
||||||
@@ -401,12 +407,11 @@ const bar: React.CSSProperties = {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
height: "56px", // Explicitly set height
|
height: "100%", // Inherit height from parent
|
||||||
padding: "0 16px",
|
padding: "0 16px",
|
||||||
gap: 12,
|
gap: 12,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
background: "#fafafa",
|
background: "#faf8f5",
|
||||||
borderBottom: "1px solid #e4e4e7",
|
|
||||||
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
|
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1027,16 +1027,11 @@ export function ChatPanel({
|
|||||||
);
|
);
|
||||||
const [showThreads, setShowThreads] = useState(false);
|
const [showThreads, setShowThreads] = useState(false);
|
||||||
const [mcpToken, setMcpToken] = useState<string | null>(null);
|
const [mcpToken, setMcpToken] = useState<string | null>(null);
|
||||||
const [isChatMinimized, setIsChatMinimized] = useState<boolean>(() => {
|
const [isChatMinimized, setIsChatMinimized] = useState<boolean>(false);
|
||||||
if (typeof window === "undefined") return false;
|
|
||||||
return !window.location.pathname.includes("/preview");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-minimize when navigating to dashboard, auto-open when navigating to preview
|
// Auto-minimize when navigating to dashboard, auto-open when navigating to preview
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
setIsChatMinimized(!pathname.includes("/preview"));
|
||||||
setIsChatMinimized(!window.location.pathname.includes("/preview"));
|
|
||||||
}
|
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -2101,12 +2096,9 @@ export function ChatPanel({
|
|||||||
}}
|
}}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
const el = e.currentTarget;
|
const el = e.currentTarget;
|
||||||
const newlines = (el.value.match(/\n/g) || []).length;
|
// Only resize if height actually changed
|
||||||
if ((el as any).lastNewlines !== newlines) {
|
el.style.height = "auto";
|
||||||
(el as any).lastNewlines = newlines;
|
el.style.height = Math.min(el.scrollHeight, 240) + "px";
|
||||||
el.style.height = "auto";
|
|
||||||
el.style.height = Math.min(el.scrollHeight, 240) + "px";
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -2332,7 +2324,7 @@ export function ChatPanel({
|
|||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
height: 48,
|
height: 48,
|
||||||
borderBottom: "1px solid #e8e4dc",
|
borderBottom: "1px solid #e4e4e7",
|
||||||
background: "#faf8f5",
|
background: "#faf8f5",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
}}
|
}}
|
||||||
@@ -2347,7 +2339,7 @@ export function ChatPanel({
|
|||||||
padding: "0 12px",
|
padding: "0 12px",
|
||||||
gap: 6,
|
gap: 6,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
borderRight: "1px solid #e8e4dc",
|
borderRight: "1px solid #e4e4e7",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -158,7 +158,10 @@ function renderDevCompose(projectSlug: string, projectId: string): string {
|
|||||||
// process is actually listening on the port — Traefik does the
|
// process is actually listening on the port — Traefik does the
|
||||||
// health check.
|
// health check.
|
||||||
const token = projectPreviewToken(projectId);
|
const token = projectPreviewToken(projectId);
|
||||||
const traefikLabels: string[] = ['"traefik.enable=true"'];
|
const traefikLabels: string[] = [
|
||||||
|
'"traefik.enable=true"',
|
||||||
|
'"traefik.docker.network=coolify"',
|
||||||
|
];
|
||||||
for (let i = 0; i < PREVIEW_PORT_COUNT; i++) {
|
for (let i = 0; i < PREVIEW_PORT_COUNT; i++) {
|
||||||
const port = PREVIEW_BASE_PORT + i;
|
const port = PREVIEW_BASE_PORT + i;
|
||||||
const router = `vibn-dev-${projectSlug}-${i}`;
|
const router = `vibn-dev-${projectSlug}-${i}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user