style(studio): fix spacing — generous padding, centered content, breathing room

Sidebar: 200px→220px, generous logo area (pt-7 pb-6), wider item padding
Main area: max-w-4xl→max-w-3xl, px-6→px-10, py-8→py-12
Empty state: min-h-[60vh] vertical centering, larger ornament spacing
Chat bar: centered to max-w-3xl matching content, py-2.5→py-3.5
Header strip: px-4→px-8, h-11→h-12
Book cards: px-5→px-6, py-4→py-5
All borders softened to /20-/30 opacity for subtlety
This commit is contained in:
Ma
2026-03-18 16:02:45 +08:00
parent fd4889979c
commit 5e465b6326
7 changed files with 73 additions and 77 deletions
@@ -1 +0,0 @@
{"type":"server-started","port":60485,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:60485","screen_dir":"/Users/majunxian/Desktop/PyProject/inkos-studio/.superpowers/brainstorm/33312-1773817943"}
@@ -0,0 +1 @@
{"reason":"owner process exited","timestamp":1773820583908}
@@ -1,3 +1,4 @@
{"type":"server-started","port":60485,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:60485","screen_dir":"/Users/majunxian/Desktop/PyProject/inkos-studio/.superpowers/brainstorm/33312-1773817943"}
{"type":"screen-added","file":"/Users/majunxian/Desktop/PyProject/inkos-studio/.superpowers/brainstorm/33312-1773817943/layout-concept.html"}
{"type":"screen-added","file":"/Users/majunxian/Desktop/PyProject/inkos-studio/.superpowers/brainstorm/33312-1773817943/layout-refined.html"}
{"type":"server-stopped","reason":"owner process exited"}
+9 -16
View File
@@ -41,12 +41,9 @@ export function App() {
document.documentElement.classList.toggle("dark", isDark);
}, [isDark]);
// Check if language has been set
useEffect(() => {
if (project) {
setLanguageSet(!!project.language && project.language !== "zh" ? true : true);
// For now, always consider language "set" — the selector is for first-time experience
// In a real implementation, we'd check a "hasCompletedSetup" flag
setLanguageSet(true);
}
}, [project]);
@@ -63,42 +60,38 @@ export function App() {
toLogs: () => setRoute({ page: "logs" }),
};
// Derive active page for sidebar highlighting
const activePage =
route.page === "book" || route.page === "chapter" || route.page === "truth" || route.page === "analytics"
? `book:${(route as { bookId: string }).bookId}`
: route.page;
// Language selector for first-time setup
if (languageSet === null) {
return <div className="min-h-screen bg-background" />; // Loading
return <div className="min-h-screen bg-background" />;
}
return (
<div className="h-screen bg-background text-foreground flex overflow-hidden">
{/* Sidebar */}
<Sidebar nav={nav} activePage={activePage} t={t} />
{/* Main area + Chat bar */}
<div className="flex-1 flex flex-col min-w-0">
{/* Header strip */}
<div className="h-11 shrink-0 border-b border-border/30 flex items-center justify-end px-4 gap-3">
{/* Thin utility strip — pushed inward */}
<div className="h-12 shrink-0 border-b border-border/20 flex items-center justify-end px-8 gap-4">
<button
onClick={() => setTheme(isDark ? "light" : "dark")}
className="w-7 h-7 flex items-center justify-center rounded-full bg-secondary/50 text-muted-foreground hover:text-foreground transition-colors text-xs"
className="w-7 h-7 flex items-center justify-center rounded-full bg-secondary/40 text-muted-foreground/60 hover:text-foreground hover:bg-secondary transition-all text-xs"
>
{isDark ? "☀" : "☽"}
</button>
{sse.connected && (
<span className="text-[10px] text-muted-foreground/40 uppercase tracking-wider">
<span className="text-[10px] text-muted-foreground/30 uppercase tracking-[0.15em]">
{t("nav.connected")}
</span>
)}
</div>
{/* Scrollable main content */}
{/* Scrollable main — centered with generous inset */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-4xl mx-auto px-6 py-8">
<div className="max-w-3xl mx-auto px-10 py-12">
{route.page === "dashboard" && <Dashboard nav={nav} sse={sse} theme={theme} t={t} />}
{route.page === "book" && <BookDetail bookId={route.bookId} nav={nav} theme={theme} t={t} />}
{route.page === "book-create" && <BookCreate nav={nav} theme={theme} t={t} />}
@@ -111,7 +104,7 @@ export function App() {
</div>
</div>
{/* Chat bar - always at bottom */}
{/* Chat bar — inset to match content width */}
<ChatBar t={t} sse={sse} />
</div>
</div>
+33 -31
View File
@@ -118,38 +118,40 @@ export function ChatBar({ t, sse }: {
: "Tell InkOS what to do...";
return (
<div className="border-t border-border/40 bg-card/80 backdrop-blur-sm">
<div className="border-t border-border/20 bg-card/60 backdrop-blur-md">
{/* Expanded message area */}
{expanded && messages.length > 0 && (
<div
ref={scrollRef}
className="max-h-[200px] overflow-y-auto px-4 py-3 space-y-2 border-b border-border/20"
>
{messages.map((msg, i) => (
<div
key={i}
className={`text-[13px] leading-relaxed ${
msg.role === "user"
? "text-foreground"
: msg.content.startsWith("✗")
? "text-destructive"
: msg.content.startsWith("⋯")
? "text-muted-foreground"
: "text-primary"
}`}
>
{msg.role === "user" && <span className="text-muted-foreground/50 mr-1.5"></span>}
{msg.content}
</div>
))}
{loading && !messages.some((m) => m.content.startsWith("⋯")) && (
<div className="text-[13px] text-muted-foreground animate-pulse"></div>
)}
<div className="max-w-3xl mx-auto">
<div
ref={scrollRef}
className="max-h-[200px] overflow-y-auto px-10 py-4 space-y-2.5"
>
{messages.map((msg, i) => (
<div
key={i}
className={`text-[13px] leading-relaxed ${
msg.role === "user"
? "text-foreground"
: msg.content.startsWith("✗")
? "text-destructive"
: msg.content.startsWith("⋯")
? "text-muted-foreground"
: "text-primary"
}`}
>
{msg.role === "user" && <span className="text-muted-foreground/40 mr-2"></span>}
{msg.content}
</div>
))}
{loading && !messages.some((m) => m.content.startsWith("⋯")) && (
<div className="text-[13px] text-muted-foreground animate-pulse"></div>
)}
</div>
</div>
)}
{/* Input bar */}
<div className="px-4 py-2.5 flex items-center gap-3">
{/* Input bar — centered to match main content */}
<div className="max-w-3xl mx-auto px-10 py-3.5 flex items-center gap-4">
<div className="flex-1 relative">
<input
ref={inputRef}
@@ -160,14 +162,14 @@ export function ChatBar({ t, sse }: {
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
placeholder={chatPlaceholder}
disabled={loading}
className="w-full bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground/30 focus:outline-none disabled:opacity-50"
className="w-full bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground/25 focus:outline-none disabled:opacity-50"
/>
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="flex items-center gap-3 shrink-0">
{expanded && messages.length > 0 && (
<button
onClick={() => { setExpanded(false); setMessages([]); }}
className="text-[10px] text-muted-foreground/40 hover:text-muted-foreground transition-colors"
className="text-[10px] text-muted-foreground/30 hover:text-muted-foreground transition-colors"
>
Clear
</button>
@@ -175,7 +177,7 @@ export function ChatBar({ t, sse }: {
<button
onClick={handleSubmit}
disabled={!input.trim() || loading}
className="w-6 h-6 rounded bg-primary/80 text-primary-foreground flex items-center justify-center text-xs hover:bg-primary transition-colors disabled:opacity-20"
className="w-7 h-7 rounded-md bg-primary/70 text-primary-foreground flex items-center justify-center text-xs hover:bg-primary transition-all disabled:opacity-15"
>
</button>
+22 -22
View File
@@ -27,36 +27,36 @@ export function Sidebar({ nav, activePage, t }: {
const { data: daemon } = useApi<{ running: boolean }>("/daemon");
return (
<aside className="w-[200px] shrink-0 border-r border-border/40 bg-card/50 flex flex-col h-full overflow-y-auto">
{/* Logo */}
<div className="px-4 py-5 border-b border-border/30">
<button onClick={nav.toDashboard} className="flex items-baseline gap-0.5 hover:opacity-80 transition-opacity">
<span className="font-serif text-xl italic text-primary">Ink</span>
<span className="text-base font-semibold tracking-tight">OS</span>
<aside className="w-[220px] shrink-0 border-r border-border/30 bg-card/30 flex flex-col h-full overflow-y-auto">
{/* Logo — generous vertical breathing room */}
<div className="px-6 pt-7 pb-6">
<button onClick={nav.toDashboard} className="flex items-baseline gap-0.5 hover:opacity-70 transition-opacity">
<span className="font-serif text-2xl italic text-primary">Ink</span>
<span className="text-lg font-semibold tracking-tight">OS</span>
</button>
</div>
{/* Books section */}
<div className="flex-1 py-3">
<div className="px-4 mb-2 flex items-center justify-between">
<span className="text-[10px] uppercase tracking-widest text-muted-foreground/50">{t("nav.books")}</span>
<div className="flex-1 px-4">
<div className="px-2 mb-3 flex items-center justify-between">
<span className="text-[10px] uppercase tracking-[0.15em] text-muted-foreground/40 font-medium">{t("nav.books")}</span>
<button
onClick={nav.toBookCreate}
className="text-[10px] text-primary/60 hover:text-primary transition-colors"
className="w-5 h-5 flex items-center justify-center rounded text-[11px] text-muted-foreground/40 hover:text-primary hover:bg-primary/10 transition-all"
>
+
</button>
</div>
<div className="space-y-0.5 px-2">
<div className="space-y-1">
{data?.books.map((book) => (
<button
key={book.id}
onClick={() => nav.toBook(book.id)}
className={`w-full text-left px-2 py-1.5 rounded text-[13px] truncate transition-colors ${
className={`w-full text-left px-3 py-2 rounded-md text-[13px] truncate transition-all duration-150 ${
activePage === `book:${book.id}`
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted/30"
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
}`}
>
{book.title}
@@ -64,15 +64,15 @@ export function Sidebar({ nav, activePage, t }: {
))}
{(!data?.books || data.books.length === 0) && (
<div className="px-2 py-3 text-[11px] text-muted-foreground/40 italic">
<div className="px-3 py-4 text-[11px] text-muted-foreground/30 italic leading-relaxed">
{t("dash.noBooks")}
</div>
)}
</div>
</div>
{/* System nav */}
<div className="border-t border-border/30 py-3 px-2 space-y-0.5">
{/* System nav — generous spacing from book list */}
<div className="border-t border-border/20 mt-4 pt-4 pb-5 px-4 space-y-1">
<SidebarItem
label={t("nav.config")}
icon="⚙"
@@ -109,15 +109,15 @@ function SidebarItem({ label, icon, active, onClick, badge, badgeColor }: {
return (
<button
onClick={onClick}
className={`w-full text-left px-2 py-1.5 rounded text-[13px] flex items-center gap-2 transition-colors ${
className={`w-full text-left px-3 py-2 rounded-md text-[13px] flex items-center gap-2.5 transition-all duration-150 ${
active
? "bg-secondary text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted/30"
? "bg-secondary text-foreground font-medium"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
}`}
>
<span className="text-xs w-4 text-center opacity-50">{icon}</span>
<span className="text-[11px] w-4 text-center opacity-40">{icon}</span>
<span className="flex-1">{label}</span>
{badge && <span className={`text-[10px] ${badgeColor ?? ""}`}>{badge}</span>}
{badge && <span className={`text-[9px] ${badgeColor ?? ""}`}>{badge}</span>}
</button>
);
}
+7 -7
View File
@@ -47,16 +47,16 @@ export function Dashboard({ nav, sse, theme, t }: { nav: Nav; sse: { messages: R
if (loading) return <div className="text-muted-foreground py-20 text-center text-sm">Loading...</div>;
if (error) return <div className="text-destructive py-20 text-center">Error: {error}</div>;
/* ── Empty state ── */
/* ── Empty state — vertically centered in the available viewport ── */
if (!data?.books.length) {
return (
<div className="flex flex-col items-center justify-center py-32">
<div className="text-6xl mb-6 opacity-20"></div>
<h2 className="font-serif text-3xl italic text-foreground/50 mb-3">{t("dash.noBooks")}</h2>
<p className="text-sm text-muted-foreground mb-8">{t("dash.createFirst")}</p>
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<div className="text-5xl mb-8 opacity-15 select-none"></div>
<h2 className="font-serif text-3xl italic text-foreground/40 mb-3">{t("dash.noBooks")}</h2>
<p className="text-sm text-muted-foreground/60 mb-10">{t("dash.createFirst")}</p>
<button
onClick={nav.toBookCreate}
className="px-6 py-3 rounded-md text-sm font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
className="px-7 py-3 rounded-md text-sm font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
>
{t("nav.newBook")}
</button>
@@ -85,7 +85,7 @@ export function Dashboard({ nav, sse, theme, t }: { nav: Nav; sse: { messages: R
key={book.id}
className={`group border ${c.card} rounded-lg overflow-hidden`}
>
<div className="px-5 py-4 flex items-start justify-between">
<div className="px-6 py-5 flex items-start justify-between">
<div className="min-w-0">
<button
onClick={() => nav.toBook(book.id)}