|
1 | 1 | <script lang="ts"> |
2 | 2 | import { onMount } from 'svelte'; |
3 | | - import { Plus, Heart, ArrowLeft, Upload } from 'lucide-svelte'; |
| 3 | + import { Upload, Terminal } from 'lucide-svelte'; |
4 | 4 | import { petStore, selectedPetStore, petHelpers, selectedPetHelpers } from '$lib/stores/pets.js'; |
5 | | - import { fade } from 'svelte/transition'; |
6 | 5 | import type { PetPanelData } from '$lib/types/Pet.js'; |
7 | 6 |
|
8 | 7 | let pets: PetPanelData[] = []; |
|
54 | 53 | const file = target.files?.[0]; |
55 | 54 | if (file) { |
56 | 55 | if (file.size > 5 * 1024 * 1024) { |
57 | | - // 5MB limit |
58 | 56 | formErrors.image = 'Image must be less than 5MB'; |
59 | 57 | return; |
60 | 58 | } |
|
113 | 111 | function selectPet(petId: string) { |
114 | 112 | selectedPetHelpers.select(petId); |
115 | 113 | } |
| 114 | +
|
| 115 | + function handleActivate(e: KeyboardEvent, action: () => void) { |
| 116 | + if (e.key === 'Enter' || e.key === ' ') { |
| 117 | + e.preventDefault(); |
| 118 | + action(); |
| 119 | + } |
| 120 | + } |
116 | 121 | </script> |
117 | 122 |
|
118 | | -<div class="panel-container h-full flex flex-col"> |
119 | | - <div class="panel-header"> |
120 | | - <div class="flex items-center justify-between"> |
121 | | - <div class="flex items-center space-x-2"> |
122 | | - {#if showCreateForm} |
123 | | - <button on:click={toggleCreateForm} class="p-1 hover:opacity-70 transition-opacity"> |
124 | | - <ArrowLeft size={16} style="color: var(--petalytics-accent);" /> |
125 | | - </button> |
126 | | - {:else} |
127 | | - <Heart size={18} style="color: var(--petalytics-accent);" /> |
128 | | - {/if} |
129 | | - <h2 class="text-lg font-semibold"> |
130 | | - {showCreateForm ? 'Add New Pet' : 'Your Pets'} |
131 | | - </h2> |
132 | | - </div> |
133 | | - {#if !showCreateForm} |
134 | | - <button |
135 | | - on:click={toggleCreateForm} |
136 | | - class="flex items-center space-x-1 px-2 py-1 rounded-md button-secondary" |
137 | | - > |
138 | | - <Plus size={16} /> |
139 | | - <span class="text-sm">Add</span> |
140 | | - </button> |
141 | | - {/if} |
| 123 | +<div class="pet-panel h-full" style="background: var(--petalytics-bg);"> |
| 124 | + <div class="cli-header p-3 border-b font-mono text-sm" style="border-color: var(--petalytics-border); background: var(--petalytics-surface);"> |
| 125 | + <div class="flex items-center space-x-2" style="color: var(--petalytics-pine);"> |
| 126 | + <Terminal size={14} /> |
| 127 | + <span>pets@petalytics:~$</span> |
142 | 128 | </div> |
143 | 129 | </div> |
144 | 130 |
|
145 | | - <div class="panel-content flex-1 p-4 overflow-y-auto"> |
| 131 | + <div class="cli-content p-3 font-mono text-sm overflow-y-auto" style="color: var(--petalytics-text);"> |
| 132 | + <!-- Toggle create form --> |
| 133 | + <div class="cli-row px-2 py-1" role="button" tabindex="0" aria-expanded={showCreateForm} onclick={toggleCreateForm} onkeydown={(e) => handleActivate(e, toggleCreateForm)}> |
| 134 | + <span class="label">add_pet</span> |
| 135 | + <span class="value">{showCreateForm ? 'show' : 'hidden'}</span> |
| 136 | + </div> |
| 137 | + |
146 | 138 | {#if showCreateForm} |
147 | | - <!-- Pet Creation Form --> |
148 | | - <div class="create-form space-y-4" transition:fade={{ duration: 200 }}> |
149 | | - <!-- Profile Image Upload --> |
150 | | - <div class="section"> |
151 | | - <label |
152 | | - for="pet-profile-image" |
153 | | - class="block text-sm font-medium mb-2" |
154 | | - style="color: var(--petalytics-subtle);" |
155 | | - > |
156 | | - Profile Photo |
157 | | - </label> |
158 | | - <div class="flex flex-col items-center space-y-3"> |
159 | | - {#if newPet.profileImageUrl} |
160 | | - <img |
161 | | - src={newPet.profileImageUrl} |
162 | | - alt="Pet preview" |
163 | | - class="w-24 h-24 rounded-full object-cover border-2" |
164 | | - style="border-color: var(--petalytics-border);" |
165 | | - /> |
166 | | - {:else} |
167 | | - <div |
168 | | - class="w-24 h-24 rounded-full border-2 border-dashed flex items-center justify-center cursor-pointer hover:opacity-70 transition-opacity" |
169 | | - style="border-color: var(--petalytics-border);" |
170 | | - on:click={() => imageInput.click()} |
171 | | - on:keydown={(e) => e.key === 'Enter' && imageInput.click()} |
172 | | - role="button" |
173 | | - tabindex="0" |
174 | | - > |
175 | | - <Upload size={24} style="color: var(--petalytics-subtle);" /> |
176 | | - </div> |
177 | | - {/if} |
178 | | - <input |
179 | | - id="pet-profile-image" |
180 | | - bind:this={imageInput} |
181 | | - type="file" |
182 | | - accept="image/*" |
183 | | - on:change={handleImageUpload} |
184 | | - class="hidden" |
185 | | - /> |
186 | | - {#if formErrors.image} |
187 | | - <p class="text-sm text-red-400">{formErrors.image}</p> |
188 | | - {/if} |
189 | | - </div> |
| 139 | + <div class="mt-2 p-2 rounded" style="background: var(--petalytics-overlay);"> |
| 140 | + <!-- name --> |
| 141 | + <div class="cli-row px-2 py-1"> |
| 142 | + <span class="label">name</span> |
| 143 | + <input class="value bg-transparent border-none outline-none input-inline" bind:value={newPet.name} placeholder="Pet Name" /> |
190 | 144 | </div> |
| 145 | + {#if formErrors.name} |
| 146 | + <p class="px-2 text-xs" style="color: var(--petalytics-love);">{formErrors.name}</p> |
| 147 | + {/if} |
191 | 148 |
|
192 | | - <!-- Pet Name --> |
193 | | - <div class="section"> |
194 | | - <input type="text" bind:value={newPet.name} class="input w-full" placeholder="Pet Name" /> |
195 | | - {#if formErrors.name} |
196 | | - <p class="text-sm text-red-400 mt-1">{formErrors.name}</p> |
197 | | - {/if} |
| 149 | + <!-- breed --> |
| 150 | + <div class="cli-row px-2 py-1"> |
| 151 | + <span class="label">breed</span> |
| 152 | + <input class="value bg-transparent border-none outline-none input-inline" bind:value={newPet.breed} placeholder="Breed" /> |
198 | 153 | </div> |
| 154 | + {#if formErrors.breed} |
| 155 | + <p class="px-2 text-xs" style="color: var(--petalytics-love);">{formErrors.breed}</p> |
| 156 | + {/if} |
199 | 157 |
|
200 | | - <!-- Breed --> |
201 | | - <div class="section"> |
202 | | - <input type="text" bind:value={newPet.breed} class="input w-full" placeholder="Breed" /> |
203 | | - {#if formErrors.breed} |
204 | | - <p class="text-sm text-red-400 mt-1">{formErrors.breed}</p> |
205 | | - {/if} |
| 158 | + <!-- age --> |
| 159 | + <div class="cli-row px-2 py-1"> |
| 160 | + <span class="label">age</span> |
| 161 | + <input class="value bg-transparent border-none outline-none input-inline" type="number" min="0" max="30" bind:value={newPet.age} placeholder="Age" /> |
206 | 162 | </div> |
| 163 | + {#if formErrors.age} |
| 164 | + <p class="px-2 text-xs" style="color: var(--petalytics-love);">{formErrors.age}</p> |
| 165 | + {/if} |
207 | 166 |
|
208 | | - <!-- Age and Gender --> |
209 | | - <div class="grid grid-cols-2 gap-3"> |
210 | | - <div> |
211 | | - <input |
212 | | - type="number" |
213 | | - bind:value={newPet.age} |
214 | | - class="input w-full" |
215 | | - placeholder="Age" |
216 | | - min="0" |
217 | | - max="30" |
218 | | - /> |
219 | | - {#if formErrors.age} |
220 | | - <p class="text-xs text-red-400 mt-1">{formErrors.age}</p> |
221 | | - {/if} |
222 | | - </div> |
| 167 | + <!-- gender --> |
| 168 | + <div class="cli-row px-2 py-1"> |
| 169 | + <span class="label">gender</span> |
| 170 | + <select class="value bg-transparent border-none outline-none input-inline" bind:value={newPet.gender}> |
| 171 | + <option value="">Select gender</option> |
| 172 | + <option value="male">male</option> |
| 173 | + <option value="female">female</option> |
| 174 | + </select> |
| 175 | + </div> |
| 176 | + {#if formErrors.gender} |
| 177 | + <p class="px-2 text-xs" style="color: var(--petalytics-love);">{formErrors.gender}</p> |
| 178 | + {/if} |
223 | 179 |
|
224 | | - <div> |
225 | | - <select bind:value={newPet.gender} class="input w-full"> |
226 | | - <option value="">Gender</option> |
227 | | - <option value="male">Male</option> |
228 | | - <option value="female">Female</option> |
229 | | - </select> |
230 | | - {#if formErrors.gender} |
231 | | - <p class="text-xs text-red-400 mt-1">{formErrors.gender}</p> |
| 180 | + <!-- profile image upload --> |
| 181 | + <div class="cli-row px-2 py-1" style="align-items: flex-start;"> |
| 182 | + <span class="label">profile_image</span> |
| 183 | + <div class="value flex items-center justify-end gap-2"> |
| 184 | + {#if newPet.profileImageUrl} |
| 185 | + <img src={newPet.profileImageUrl} alt="preview" class="w-10 h-10 rounded-full object-cover border" style="border-color: var(--petalytics-border);" /> |
232 | 186 | {/if} |
| 187 | + <button type="button" class="arrow-btn" onclick={() => imageInput.click()}>upload</button> |
| 188 | + <input id="pet-profile-image" bind:this={imageInput} type="file" accept="image/*" onchange={handleImageUpload} class="hidden" /> |
233 | 189 | </div> |
234 | 190 | </div> |
| 191 | + {#if formErrors.image} |
| 192 | + <p class="px-2 text-xs" style="color: var(--petalytics-love);">{formErrors.image}</p> |
| 193 | + {/if} |
235 | 194 |
|
236 | | - <!-- Form Actions --> |
237 | | - <div class="flex space-x-3 pt-4"> |
238 | | - <button on:click={createPet} class="button flex-1">Create Pet</button> |
239 | | - <button on:click={toggleCreateForm} class="button-secondary flex-1">Cancel</button> |
| 195 | + <div class="flex gap-2 px-2 pt-2"> |
| 196 | + <button class="button flex-1" type="button" onclick={createPet}>create</button> |
| 197 | + <button class="button-secondary flex-1" type="button" onclick={toggleCreateForm}>cancel</button> |
240 | 198 | </div> |
241 | 199 | </div> |
| 200 | + {/if} |
| 201 | + |
| 202 | + <!-- Separator --> |
| 203 | + <div class="my-3"><div class="border-t" style="border-color: var(--petalytics-border);"></div></div> |
| 204 | + |
| 205 | + <!-- Pets list --> |
| 206 | + <div class="cli-row px-2 py-1"> |
| 207 | + <span style="color: var(--petalytics-subtle);">#</span> |
| 208 | + <span class="ml-2" style="color: var(--petalytics-gold);">pets</span> |
| 209 | + </div> |
| 210 | + |
| 211 | + {#if pets.length === 0} |
| 212 | + <div class="px-2 py-4" style="color: var(--petalytics-subtle);">no pets yet — use add_pet to create one</div> |
242 | 213 | {:else} |
243 | | - <!-- Pet Grid --> |
244 | | - <div class="pets-grid"> |
245 | | - {#if pets.length === 0} |
246 | | - <div class="empty-state text-center py-8"> |
247 | | - <Heart size={48} style="color: var(--petalytics-subtle); margin: 0 auto 1rem;" /> |
248 | | - <p class="text-lg font-medium mb-2" style="color: var(--petalytics-text);"> |
249 | | - No pets yet |
250 | | - </p> |
251 | | - <p class="text-sm mb-4" style="color: var(--petalytics-subtle);"> |
252 | | - Add your first pet to get started with tracking their journal |
253 | | - </p> |
254 | | - <button on:click={toggleCreateForm} class="button">Add Your First Pet</button> |
255 | | - </div> |
256 | | - {:else} |
257 | | - <div class="grid grid-cols-2 gap-3"> |
258 | | - {#each pets as pet} |
259 | | - <div |
260 | | - class="pet-card p-3 rounded-lg border cursor-pointer transition-all hover:opacity-80" |
261 | | - class:selected={selectedPetId === pet.id} |
262 | | - style=" |
263 | | - background: var(--petalytics-surface); |
264 | | - border-color: {selectedPetId === pet.id |
265 | | - ? 'var(--petalytics-accent)' |
266 | | - : 'var(--petalytics-border)'}; |
267 | | - " |
268 | | - on:click={() => selectPet(pet.id)} |
269 | | - on:keydown={(e) => e.key === 'Enter' && selectPet(pet.id)} |
270 | | - role="button" |
271 | | - tabindex="0" |
272 | | - > |
273 | | - <div class="flex flex-col items-center space-y-2"> |
274 | | - <img |
275 | | - src={pet.profileImageUrl || '/images/default-pet.png'} |
276 | | - alt={pet.name} |
277 | | - class="w-16 h-16 rounded-full object-cover" |
278 | | - /> |
279 | | - <div class="text-center"> |
280 | | - <p class="font-medium text-sm truncate" style="color: var(--petalytics-text);"> |
281 | | - {pet.name} |
282 | | - </p> |
283 | | - <p class="text-xs truncate" style="color: var(--petalytics-subtle);"> |
284 | | - {pet.breed} |
285 | | - </p> |
286 | | - <p class="text-xs" style="color: var(--petalytics-subtle);"> |
287 | | - {pet.age} |
288 | | - {pet.age === 1 ? 'year' : 'years'} old |
289 | | - </p> |
290 | | - </div> |
291 | | - </div> |
292 | | - </div> |
293 | | - {/each} |
294 | | - </div> |
295 | | - {/if} |
296 | | - </div> |
| 214 | + {#each pets as pet} |
| 215 | + <div class="cli-row px-2 py-1" role="button" tabindex="0" data-selected={selectedPetId === pet.id} onclick={() => selectPet(pet.id)} onkeydown={(e) => handleActivate(e, () => selectPet(pet.id))}> |
| 216 | + <span class="label" style="color: var(--petalytics-text);">{pet.name}</span> |
| 217 | + <span class="value" style="color: var(--petalytics-subtle);"> |
| 218 | + {pet.breed} | {pet.age}{pet.age === 1 ? 'y' : 'y'} |
| 219 | + </span> |
| 220 | + </div> |
| 221 | + {/each} |
297 | 222 | {/if} |
298 | 223 | </div> |
299 | 224 | </div> |
300 | 225 |
|
301 | 226 | <style> |
302 | | - .pet-card.selected { |
303 | | - box-shadow: 0 0 0 2px var(--petalytics-accent); |
304 | | - } |
305 | | -
|
306 | | - .panel-header { |
307 | | - background: var(--petalytics-overlay); |
308 | | - border-bottom: 1px solid var(--petalytics-border); |
309 | | - padding: 0.75rem 1rem; |
310 | | - font-weight: 500; |
311 | | - color: var(--petalytics-text); |
312 | | - } |
313 | | -
|
314 | | - .panel-content { |
315 | | - background: var(--petalytics-surface); |
316 | | - } |
317 | | -
|
318 | | - .section { |
319 | | - margin-bottom: 1rem; |
320 | | - } |
| 227 | +.cli-row { |
| 228 | + display: flex; |
| 229 | + align-items: center; |
| 230 | + border: 1px solid transparent; |
| 231 | + border-radius: 6px; |
| 232 | + transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease; |
| 233 | +} |
| 234 | +.cli-row[role="button"] { cursor: pointer; } |
| 235 | +.cli-row:hover { background: var(--petalytics-highlight-low); border-color: var(--petalytics-border); } |
| 236 | +.cli-row:focus-within, .cli-row[role="button"]:focus-visible { outline: none; background: var(--petalytics-highlight-med); border-color: var(--petalytics-accent); box-shadow: 0 0 0 2px color-mix(in oklab, var(--petalytics-accent) 40%, transparent); } |
| 237 | +.cli-row[data-selected="true"], .cli-row[aria-expanded="true"] { background: var(--petalytics-highlight-high); border-color: var(--petalytics-accent); } |
| 238 | +.label { color: var(--petalytics-foam); } |
| 239 | +.value { margin-left: auto; text-align: right; flex: 1 1 auto; } |
| 240 | +.input-inline { padding: 0; } |
| 241 | +.arrow-btn { font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; line-height: 1rem; background: transparent; border: 1px solid var(--petalytics-border); color: var(--petalytics-subtle); padding: 0.15rem 0.5rem; border-radius: 4px; cursor: pointer; } |
| 242 | +.arrow-btn:hover { background: var(--petalytics-highlight-low); color: var(--petalytics-text); } |
| 243 | +.arrow-btn:focus-visible { outline: none; border-color: var(--petalytics-accent); box-shadow: 0 0 0 2px color-mix(in oklab, var(--petalytics-accent) 35%, transparent); } |
321 | 244 | </style> |
0 commit comments