Skip to content

Commit f0d4f6f

Browse files
committed
polish(desktop): refine main shell — wordmark header, starter cards, send button, custom empty-state mark
- Sidebar header uses Wordmark with pre-alpha pill instead of generic Sparkles - Starter prompt cards: hover-lift, accent on title hover, more vertical breathing room - Chat input is now a textarea with auto-grow, soft focus glow, kbd hints (Enter/Shift+Enter) - Send button: circular terracotta, hover scale, active press feedback - Empty state replaces generic Sparkles with a custom dotted-canvas SVG mark and warmer copy - Preview toolbar: tighter button height, elevated dropdown shadow Signed-off-by: hqhq1025 <1506751656@qq.com>
1 parent 98e79ba commit f0d4f6f

2 files changed

Lines changed: 168 additions & 71 deletions

File tree

Lines changed: 161 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { buildSrcdoc } from '@open-codesign/runtime';
22
import { BUILTIN_DEMOS } from '@open-codesign/templates';
3-
import { Button } from '@open-codesign/ui';
4-
import { Send, Sparkles } from 'lucide-react';
5-
import { useEffect, useState } from 'react';
3+
import { Wordmark } from '@open-codesign/ui';
4+
import { ArrowUp } from 'lucide-react';
5+
import { useEffect, useRef, useState } from 'react';
66
import { PreviewToolbar } from './components/PreviewToolbar';
77
import { Onboarding } from './onboarding';
88
import { useCodesignStore } from './store';
@@ -16,21 +16,36 @@ export function App() {
1616
const configLoaded = useCodesignStore((s) => s.configLoaded);
1717
const loadConfig = useCodesignStore((s) => s.loadConfig);
1818
const [prompt, setPrompt] = useState('');
19+
const taRef = useRef<HTMLTextAreaElement>(null);
1920

2021
useEffect(() => {
2122
void loadConfig();
2223
}, [loadConfig]);
2324

25+
useEffect(() => {
26+
const ta = taRef.current;
27+
if (!ta) return;
28+
ta.style.height = 'auto';
29+
ta.style.height = `${Math.min(ta.scrollHeight, 160)}px`;
30+
}, []);
31+
2432
function handleSubmit(e: React.FormEvent) {
2533
e.preventDefault();
2634
if (!prompt.trim() || isGenerating) return;
2735
void sendPrompt(prompt);
2836
setPrompt('');
2937
}
3038

39+
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
40+
if (e.key === 'Enter' && !e.shiftKey) {
41+
e.preventDefault();
42+
handleSubmit(e);
43+
}
44+
}
45+
3146
if (!configLoaded) {
3247
return (
33-
<div className="h-full flex items-center justify-center bg-[var(--color-background)] text-sm text-[var(--color-text-muted)]">
48+
<div className="h-full flex items-center justify-center bg-[var(--color-background)] text-[13px] text-[var(--color-text-muted)]">
3449
Loading…
3550
</div>
3651
);
@@ -40,35 +55,36 @@ export function App() {
4055
return <Onboarding />;
4156
}
4257

58+
const canSend = prompt.trim().length > 0 && !isGenerating;
59+
4360
return (
44-
<div className="h-full grid grid-cols-[380px_1fr] bg-[var(--color-background)]">
61+
<div className="h-full grid grid-cols-[360px_1fr] bg-[var(--color-background)]">
4562
<aside className="flex flex-col border-r border-[var(--color-border)] bg-[var(--color-background-secondary)]">
46-
<header className="px-5 py-4 border-b border-[var(--color-border)]">
47-
<div className="flex items-center gap-2">
48-
<Sparkles className="w-5 h-5 text-[var(--color-accent)]" />
49-
<span className="font-semibold text-[var(--color-text-primary)]">open-codesign</span>
50-
<span className="ml-auto text-xs text-[var(--color-text-muted)]">pre-alpha</span>
51-
</div>
63+
<header className="px-5 h-[52px] flex items-center border-b border-[var(--color-border-muted)]">
64+
<Wordmark badge="pre-alpha" />
5265
</header>
5366

54-
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4">
67+
<div className="flex-1 overflow-y-auto px-5 py-6 space-y-6">
5568
{messages.length === 0 ? (
56-
<div>
57-
<p className="text-sm text-[var(--color-text-secondary)] mb-3">
58-
Try a starter prompt:
59-
</p>
60-
<ul className="space-y-2">
69+
<div className="flex flex-col gap-3">
70+
<span
71+
className="text-[10px] uppercase tracking-[0.08em] text-[var(--color-text-muted)] font-medium"
72+
style={{ fontFamily: 'var(--font-mono)' }}
73+
>
74+
Starter prompts
75+
</span>
76+
<ul className="flex flex-col gap-2">
6177
{BUILTIN_DEMOS.map((demo) => (
6278
<li key={demo.id}>
6379
<button
6480
type="button"
6581
onClick={() => setPrompt(demo.prompt)}
66-
className="w-full text-left px-3 py-2 rounded-[var(--radius-md)] bg-[var(--color-surface)] border border-[var(--color-border)] hover:bg-[var(--color-surface-hover)] transition-colors"
82+
className="group w-full text-left px-4 py-3 rounded-[var(--radius-lg)] bg-[var(--color-surface)] border border-[var(--color-border)] shadow-[var(--shadow-soft)] hover:-translate-y-[1px] hover:border-[var(--color-border-strong)] hover:shadow-[var(--shadow-card)] transition-[transform,box-shadow,border-color] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]"
6783
>
68-
<div className="text-sm font-medium text-[var(--color-text-primary)]">
84+
<div className="text-[13px] font-semibold text-[var(--color-text-primary)] tracking-[-0.005em] group-hover:text-[var(--color-accent)] transition-colors duration-150">
6985
{demo.title}
7086
</div>
71-
<div className="text-xs text-[var(--color-text-muted)] mt-0.5">
87+
<div className="text-[12px] text-[var(--color-text-muted)] mt-[3px] leading-[1.5]">
7288
{demo.description}
7389
</div>
7490
</button>
@@ -77,45 +93,84 @@ export function App() {
7793
</ul>
7894
</div>
7995
) : (
80-
messages.map((m, i) => (
81-
<div
82-
key={`${m.role}-${i}`}
83-
className={`px-3 py-2 rounded-[var(--radius-md)] text-sm ${
84-
m.role === 'user'
85-
? 'bg-[var(--color-accent-muted)] text-[var(--color-text-primary)]'
86-
: 'bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-text-primary)]'
87-
}`}
88-
>
89-
{m.content}
90-
</div>
91-
))
96+
<div className="flex flex-col gap-3">
97+
{messages.map((m, i) => (
98+
<div
99+
key={`${m.role}-${i}-${m.content.slice(0, 8)}`}
100+
className={`px-4 py-3 rounded-[var(--radius-lg)] text-[13px] leading-[1.55] ${
101+
m.role === 'user'
102+
? 'bg-[var(--color-accent-soft)] text-[var(--color-text-primary)] border border-[var(--color-accent-muted)]'
103+
: 'bg-[var(--color-surface)] border border-[var(--color-border-muted)] text-[var(--color-text-primary)]'
104+
}`}
105+
>
106+
{m.content}
107+
</div>
108+
))}
109+
</div>
92110
)}
93111
</div>
94112

95-
<form
96-
onSubmit={handleSubmit}
97-
className="border-t border-[var(--color-border)] p-3 flex gap-2"
98-
>
99-
<input
100-
type="text"
101-
value={prompt}
102-
onChange={(e) => setPrompt(e.target.value)}
103-
placeholder="Describe what to design…"
104-
disabled={isGenerating}
105-
className="flex-1 px-3 py-2 rounded-[var(--radius-md)] bg-[var(--color-surface)] border border-[var(--color-border)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-accent)]"
106-
/>
107-
<Button type="submit" size="md" disabled={isGenerating || !prompt.trim()}>
108-
<Send className="w-4 h-4" />
109-
</Button>
113+
<form onSubmit={handleSubmit} className="border-t border-[var(--color-border-muted)] p-4">
114+
<div className="relative flex items-end gap-2 p-2 rounded-[var(--radius-lg)] bg-[var(--color-surface)] border border-[var(--color-border)] focus-within:border-[var(--color-accent)] focus-within:shadow-[0_0_0_3px_var(--color-focus-ring)] transition-[box-shadow,border-color] duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]">
115+
<textarea
116+
ref={taRef}
117+
value={prompt}
118+
onChange={(e) => {
119+
setPrompt(e.target.value);
120+
e.currentTarget.style.height = 'auto';
121+
e.currentTarget.style.height = `${Math.min(e.currentTarget.scrollHeight, 160)}px`;
122+
}}
123+
onKeyDown={handleKeyDown}
124+
placeholder="Describe what to design…"
125+
disabled={isGenerating}
126+
rows={1}
127+
className="flex-1 resize-none bg-transparent px-2 py-1 text-[13px] leading-[1.5] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] focus:outline-none min-h-[24px] max-h-[160px]"
128+
/>
129+
<button
130+
type="submit"
131+
disabled={!canSend}
132+
aria-label="Send prompt"
133+
className="shrink-0 inline-flex items-center justify-center w-8 h-8 rounded-[var(--radius-md)] bg-[var(--color-accent)] text-white shadow-[var(--shadow-soft)] hover:bg-[var(--color-accent-hover)] hover:scale-[1.04] active:scale-[0.96] disabled:opacity-30 disabled:hover:scale-100 disabled:pointer-events-none transition-[transform,background-color,opacity] duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]"
134+
>
135+
<ArrowUp className="w-4 h-4" strokeWidth={2.4} />
136+
</button>
137+
</div>
138+
<div className="mt-2 px-1 text-[11px] text-[var(--color-text-muted)] flex items-center justify-between">
139+
<span>
140+
<kbd
141+
className="px-[5px] py-[1px] rounded-[4px] bg-[var(--color-surface-active)] text-[10px] text-[var(--color-text-secondary)]"
142+
style={{ fontFamily: 'var(--font-mono)' }}
143+
>
144+
Enter
145+
</kbd>{' '}
146+
to send ·{' '}
147+
<kbd
148+
className="px-[5px] py-[1px] rounded-[4px] bg-[var(--color-surface-active)] text-[10px] text-[var(--color-text-secondary)]"
149+
style={{ fontFamily: 'var(--font-mono)' }}
150+
>
151+
Shift+Enter
152+
</kbd>{' '}
153+
for newline
154+
</span>
155+
{isGenerating ? (
156+
<span className="inline-flex items-center gap-1.5 text-[var(--color-accent)]">
157+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-accent)] animate-pulse" />
158+
Generating
159+
</span>
160+
) : null}
161+
</div>
110162
</form>
111163
</aside>
112164

113165
<main className="flex flex-col">
114-
<header className="h-12 px-5 border-b border-[var(--color-border)] flex items-center justify-between">
115-
<span className="text-sm text-[var(--color-text-secondary)]">
166+
<header className="h-[52px] px-6 border-b border-[var(--color-border-muted)] flex items-center justify-between">
167+
<span className="text-[13px] text-[var(--color-text-secondary)] tracking-[-0.005em]">
116168
{previewHtml ? 'Preview' : 'No design yet'}
117169
</span>
118-
<span className="text-xs text-[var(--color-text-muted)]">
170+
<span
171+
className="text-[10px] uppercase tracking-[0.08em] text-[var(--color-text-muted)] font-medium"
172+
style={{ fontFamily: 'var(--font-mono)' }}
173+
>
119174
BYOK · local-first · multi-model
120175
</span>
121176
</header>
@@ -130,23 +185,65 @@ export function App() {
130185
className="w-full h-full bg-white rounded-[var(--radius-2xl)] shadow-[var(--shadow-card)] border border-[var(--color-border)]"
131186
/>
132187
) : (
133-
<div className="h-full flex items-center justify-center">
134-
<div className="text-center max-w-md">
135-
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[var(--color-surface)] border border-[var(--color-border)] flex items-center justify-center">
136-
<Sparkles className="w-7 h-7 text-[var(--color-accent)]" />
137-
</div>
138-
<h2 className="text-lg font-semibold text-[var(--color-text-primary)] mb-2">
139-
Design with AI
140-
</h2>
141-
<p className="text-sm text-[var(--color-text-secondary)]">
142-
Pick a starter on the left, or describe what you want to design. The result
143-
renders here in a sandboxed preview.
144-
</p>
145-
</div>
146-
</div>
188+
<EmptyState />
147189
)}
148190
</div>
149191
</main>
150192
</div>
151193
);
152194
}
195+
196+
function EmptyState() {
197+
return (
198+
<div className="h-full flex items-center justify-center">
199+
<div className="text-center max-w-[360px] flex flex-col items-center gap-5">
200+
<EmptyMark />
201+
<div className="flex flex-col gap-2">
202+
<h2 className="text-[20px] font-semibold text-[var(--color-text-primary)] tracking-[-0.01em] leading-[1.2]">
203+
A blank canvas, ready when you are.
204+
</h2>
205+
<p className="text-[13px] text-[var(--color-text-secondary)] leading-[1.6]">
206+
Pick a starter on the left, or describe what you want to design. The result renders here
207+
in a sandboxed preview.
208+
</p>
209+
</div>
210+
</div>
211+
</div>
212+
);
213+
}
214+
215+
function EmptyMark() {
216+
return (
217+
<svg
218+
width="56"
219+
height="56"
220+
viewBox="0 0 56 56"
221+
fill="none"
222+
xmlns="http://www.w3.org/2000/svg"
223+
aria-hidden="true"
224+
>
225+
<title>open-codesign canvas</title>
226+
<rect
227+
x="8.5"
228+
y="8.5"
229+
width="39"
230+
height="39"
231+
rx="6"
232+
stroke="var(--color-border-strong)"
233+
strokeDasharray="3 4"
234+
strokeWidth="1"
235+
/>
236+
<rect
237+
x="20"
238+
y="20"
239+
width="16"
240+
height="16"
241+
rx="3"
242+
fill="var(--color-accent-soft)"
243+
stroke="var(--color-accent)"
244+
strokeWidth="1.4"
245+
/>
246+
<circle cx="28" cy="28" r="3" fill="var(--color-accent)" />
247+
</svg>
248+
);
249+
}

apps/desktop/src/renderer/src/components/PreviewToolbar.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ export function PreviewToolbar(): ReactElement {
4343
const disabled = !previewHtml;
4444

4545
return (
46-
<div className="flex items-center justify-end gap-2 px-5 py-2 border-b border-[var(--color-border)] bg-[var(--color-background-secondary)]">
46+
<div className="flex items-center justify-end gap-2 px-6 py-2 border-b border-[var(--color-border-muted)] bg-[var(--color-background-secondary)]">
4747
{toastMessage && (
48-
<output className="mr-auto text-xs text-[var(--color-text-secondary)] truncate max-w-[60%]">
48+
<output className="mr-auto text-[12px] text-[var(--color-text-secondary)] truncate max-w-[60%]">
4949
{toastMessage}
5050
</output>
5151
)}
@@ -55,18 +55,18 @@ export function PreviewToolbar(): ReactElement {
5555
type="button"
5656
disabled={disabled}
5757
onClick={() => setOpen((v) => !v)}
58-
className="inline-flex items-center gap-1.5 h-8 px-3 rounded-[var(--radius-md)] text-sm font-medium border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] disabled:opacity-50 disabled:pointer-events-none transition-colors"
58+
className="inline-flex items-center gap-1.5 h-[30px] px-3 rounded-[var(--radius-md)] text-[13px] font-medium border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] hover:border-[var(--color-border-strong)] disabled:opacity-40 disabled:pointer-events-none transition-[background-color,border-color] duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]"
5959
aria-haspopup="menu"
6060
aria-expanded={open}
6161
>
62-
<Download className="w-4 h-4" aria-hidden="true" />
62+
<Download className="w-[14px] h-[14px]" aria-hidden="true" />
6363
Export
6464
</button>
6565

6666
{open && (
6767
<div
6868
role="menu"
69-
className="absolute right-0 top-full mt-1 min-w-[180px] rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[var(--shadow-card)] py-1 z-10"
69+
className="absolute right-0 top-full mt-2 min-w-[200px] rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[var(--shadow-elevated)] py-1 z-10"
7070
>
7171
{EXPORT_ITEMS.map((item) => (
7272
<button
@@ -79,11 +79,11 @@ export function PreviewToolbar(): ReactElement {
7979
setOpen(false);
8080
void exportActive(item.format);
8181
}}
82-
className="w-full flex items-center justify-between gap-3 px-3 py-2 text-sm text-left text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] disabled:opacity-50 disabled:hover:bg-transparent disabled:cursor-not-allowed transition-colors"
82+
className="w-full flex items-center justify-between gap-3 px-3 py-2 text-[13px] text-left text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] disabled:opacity-50 disabled:hover:bg-transparent disabled:cursor-not-allowed transition-colors duration-100"
8383
>
8484
<span>{item.label}</span>
8585
{!item.ready && (
86-
<span className="text-xs text-[var(--color-text-muted)]">{item.hint}</span>
86+
<span className="text-[11px] text-[var(--color-text-muted)]">{item.hint}</span>
8787
)}
8888
</button>
8989
))}

0 commit comments

Comments
 (0)