11import { buildSrcdoc } from '@open-codesign/runtime' ;
22import { 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' ;
66import { PreviewToolbar } from './components/PreviewToolbar' ;
77import { Onboarding } from './onboarding' ;
88import { 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+ }
0 commit comments