Skip to content

Commit 7c15f0f

Browse files
committed
feat: implement loading and empty states in viewport, add toast notifications and skeleton components
1 parent e6d2ec0 commit 7c15f0f

File tree

6 files changed

+403
-152
lines changed

6 files changed

+403
-152
lines changed

src/lib/components/panels/GuardianPanel.svelte

Lines changed: 183 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { onMount } from 'svelte';
33
import ThemeSelector from '../ui/ThemeSelector.svelte';
44
import DataManager from '../ui/DataManager.svelte';
5-
import { User, Key, Settings, CheckCircle, AlertCircle } from 'lucide-svelte';
5+
import { ChevronLeft, ChevronRight, User, Key, Settings, CheckCircle, AlertCircle, Terminal } from 'lucide-svelte';
66
import { guardianHelpers } from '$lib/stores/guardian.js';
77
import { aiAnalysisHelpers } from '$lib/stores/ai-analysis.js';
88
@@ -16,6 +16,12 @@
1616
1717
let apiKeyStatus = 'unchecked'; // unchecked, checking, valid, invalid
1818
let showDataManager = false;
19+
20+
// CLI-style editing states
21+
let editingField: string | null = null;
22+
let themes = ['everforest', 'gruvbox', 'tokyo-night', 'nord'];
23+
let currentTheme = 'everforest';
24+
let themeIndex = 0;
1925
2026
onMount(() => {
2127
// Load saved guardian data
@@ -25,8 +31,51 @@
2531
apiKeyInput = saved.apiKey || '';
2632
preferences = { ...preferences, ...saved.preferences };
2733
}
34+
35+
// Get current theme from document
36+
const savedTheme = localStorage.getItem('petalytics-theme') || 'everforest';
37+
currentTheme = savedTheme;
38+
themeIndex = themes.indexOf(savedTheme);
2839
});
2940
41+
function toggleTheme(direction: 'prev' | 'next') {
42+
if (direction === 'next') {
43+
themeIndex = (themeIndex + 1) % themes.length;
44+
} else {
45+
themeIndex = (themeIndex - 1 + themes.length) % themes.length;
46+
}
47+
currentTheme = themes[themeIndex];
48+
49+
// Apply theme
50+
document.documentElement.className = currentTheme;
51+
localStorage.setItem('petalytics-theme', currentTheme);
52+
}
53+
54+
function togglePreference(key: keyof typeof preferences) {
55+
preferences[key] = !preferences[key];
56+
handlePreferenceChange(key);
57+
}
58+
59+
function startEdit(field: string) {
60+
editingField = field;
61+
}
62+
63+
function stopEdit() {
64+
editingField = null;
65+
saveGuardianInfo();
66+
if (editingField === 'apiKey') {
67+
validateApiKey();
68+
}
69+
}
70+
71+
function handleKeydown(event: KeyboardEvent, field: string) {
72+
if (event.key === 'Enter') {
73+
stopEdit();
74+
} else if (event.key === 'Escape') {
75+
editingField = null;
76+
}
77+
}
78+
3079
async function validateApiKey() {
3180
if (!apiKeyInput.trim()) {
3281
apiKeyStatus = 'unchecked';
@@ -75,163 +124,162 @@
75124
}
76125
</script>
77126

78-
<div class="panel-container h-full flex flex-col">
79-
<!-- Panel Header -->
80-
<div class="panel-header">
81-
<div class="flex items-center space-x-2">
82-
<User size={18} style="color: var(--petalytics-accent);" />
83-
<h2 class="text-lg font-semibold">Guardian Settings</h2>
127+
<div class="guardian-panel h-full" style="background: var(--petalytics-bg);">
128+
<!-- CLI-style header -->
129+
<div class="cli-header p-3 border-b font-mono text-sm" style="border-color: var(--petalytics-border); background: var(--petalytics-surface);">
130+
<div class="flex items-center space-x-2" style="color: var(--petalytics-pine);">
131+
<Terminal size={14} />
132+
<span>guardian@petalytics:~$</span>
84133
</div>
85134
</div>
86135

87-
<!-- Panel Content -->
88-
<div class="panel-content flex-1 p-4 space-y-4 overflow-y-auto">
89-
<!-- Guardian Info -->
90-
<div class="section">
91-
<label
92-
for="guardian-name"
93-
class="block text-sm font-medium mb-2"
94-
style="color: var(--petalytics-subtle);"
95-
>
96-
Your Name
97-
</label>
98-
<input
99-
id="guardian-name"
100-
type="text"
101-
bind:value={guardianName}
102-
on:blur={saveGuardianInfo}
103-
class="input w-full"
104-
placeholder="Pet Guardian Name"
105-
/>
136+
<div class="cli-content p-3 space-y-1 font-mono text-sm overflow-y-auto" style="color: var(--petalytics-text);">
137+
138+
<!-- Guardian name row -->
139+
<div class="cli-row flex items-center space-x-2 hover:bg-gray-800/20 px-2 py-1 rounded transition-colors cursor-pointer" on:click={() => startEdit('guardian')}>
140+
<span style="color: var(--petalytics-subtle);">></span>
141+
<span style="color: var(--petalytics-foam);">guardian:</span>
142+
{#if editingField === 'guardian'}
143+
<input
144+
bind:value={guardianName}
145+
on:blur={stopEdit}
146+
on:keydown={(e) => handleKeydown(e, 'guardian')}
147+
class="bg-transparent border-none outline-none flex-1"
148+
style="color: var(--petalytics-text);"
149+
placeholder="Pet Guardian Name"
150+
autofocus
151+
/>
152+
{:else}
153+
<span class="flex-1" style="color: var(--petalytics-text);">
154+
{guardianName || 'Not set'}
155+
</span>
156+
{/if}
106157
</div>
107158

108-
<!-- API Key Section -->
109-
<div class="section">
110-
<label class="block text-sm font-medium mb-2" style="color: var(--petalytics-subtle);">
111-
<Key size={16} class="inline mr-1" />
112-
OpenRouter API Key
113-
</label>
114-
<div class="flex space-x-2">
159+
<!-- API Key row -->
160+
<div class="cli-row flex items-center space-x-2 hover:bg-gray-800/20 px-2 py-1 rounded transition-colors cursor-pointer" on:click={() => startEdit('apiKey')}>
161+
<span style="color: var(--petalytics-subtle);">></span>
162+
<span style="color: var(--petalytics-foam);">api_key:</span>
163+
{#if editingField === 'apiKey'}
115164
<input
116165
type="password"
117166
bind:value={apiKeyInput}
118-
on:blur={validateApiKey}
119-
class="input flex-1"
167+
on:blur={stopEdit}
168+
on:keydown={(e) => handleKeydown(e, 'apiKey')}
169+
class="bg-transparent border-none outline-none flex-1"
170+
style="color: var(--petalytics-text);"
120171
placeholder="sk-or-..."
172+
autofocus
121173
/>
122-
<div class="flex items-center">
123-
{#if apiKeyStatus === 'checking'}
124-
<div
125-
class="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full"
126-
style="color: var(--petalytics-subtle);"
127-
></div>
128-
{:else if apiKeyStatus === 'valid'}
129-
<CheckCircle size={16} style="color: var(--petalytics-pine);" />
130-
{:else if apiKeyStatus === 'invalid'}
131-
<AlertCircle size={16} style="color: var(--petalytics-love);" />
132-
{/if}
133-
</div>
134-
</div>
135-
<p class="text-xs mt-1" style="color: var(--petalytics-muted);">
136-
Required for AI analysis features
137-
</p>
138-
{#if apiKeyStatus === 'invalid'}
139-
<p class="text-xs mt-1" style="color: var(--petalytics-love);">
140-
Invalid API key. Please check your key and try again.
141-
</p>
174+
{:else}
175+
<span class="flex-1" style="color: var(--petalytics-text);">
176+
{apiKeyInput ? `${apiKeyInput.slice(0, 8)}****` : 'Not set'}
177+
</span>
142178
{/if}
179+
<div class="flex items-center">
180+
{#if apiKeyStatus === 'checking'}
181+
<span style="color: var(--petalytics-gold);">●</span>
182+
{:else if apiKeyStatus === 'valid'}
183+
<span style="color: var(--petalytics-pine);">●</span>
184+
{:else if apiKeyStatus === 'invalid'}
185+
<span style="color: var(--petalytics-love);">●</span>
186+
{:else}
187+
<span style="color: var(--petalytics-subtle);">○</span>
188+
{/if}
189+
</div>
143190
</div>
144191

145-
<!-- Theme Selection -->
146-
<div class="section">
147-
<label
148-
for="theme-selector"
149-
class="block text-sm font-medium mb-2"
150-
style="color: var(--petalytics-subtle);"
151-
>
152-
Theme
153-
</label>
154-
<ThemeSelector id="theme-selector" />
192+
<!-- Theme row -->
193+
<div class="cli-row flex items-center space-x-2 hover:bg-gray-800/20 px-2 py-1 rounded transition-colors">
194+
<span style="color: var(--petalytics-subtle);">></span>
195+
<span style="color: var(--petalytics-foam);">theme:</span>
196+
<button on:click={() => toggleTheme('prev')} class="hover:opacity-70">
197+
<ChevronLeft size={14} style="color: var(--petalytics-subtle);" />
198+
</button>
199+
<span class="flex-1" style="color: var(--petalytics-text);">
200+
{currentTheme}
201+
</span>
202+
<button on:click={() => toggleTheme('next')} class="hover:opacity-70">
203+
<ChevronRight size={14} style="color: var(--petalytics-subtle);" />
204+
</button>
155205
</div>
156206

157-
<!-- Settings -->
158-
<div class="section">
159-
<label class="block text-sm font-medium mb-2" style="color: var(--petalytics-subtle);">
160-
<Settings size={16} class="inline mr-1" />
161-
Preferences
162-
</label>
163-
<div class="space-y-2">
164-
<label class="flex items-center space-x-2 cursor-pointer">
165-
<input
166-
type="checkbox"
167-
bind:checked={preferences.dailyReminders}
168-
on:change={() => handlePreferenceChange('dailyReminders')}
169-
class="w-4 h-4"
170-
/>
171-
<span class="text-sm" style="color: var(--petalytics-text);"> Daily reminders </span>
172-
</label>
173-
<label class="flex items-center space-x-2 cursor-pointer">
174-
<input
175-
type="checkbox"
176-
bind:checked={preferences.aiInsights}
177-
on:change={() => handlePreferenceChange('aiInsights')}
178-
class="w-4 h-4"
179-
/>
180-
<span class="text-sm" style="color: var(--petalytics-text);"> AI insights </span>
181-
</label>
182-
<label class="flex items-center space-x-2 cursor-pointer">
183-
<input
184-
type="checkbox"
185-
bind:checked={preferences.notifications}
186-
on:change={() => handlePreferenceChange('notifications')}
187-
class="w-4 h-4"
188-
/>
189-
<span class="text-sm" style="color: var(--petalytics-text);">
190-
Browser notifications
191-
</span>
192-
</label>
207+
<!-- Preferences section -->
208+
<div class="cli-section mt-4">
209+
<div class="cli-row flex items-center space-x-2 px-2 py-1">
210+
<span style="color: var(--petalytics-subtle);">#</span>
211+
<span style="color: var(--petalytics-gold);">preferences</span>
212+
</div>
213+
214+
<div class="cli-row flex items-center space-x-2 hover:bg-gray-800/20 px-2 py-1 rounded transition-colors cursor-pointer" on:click={() => togglePreference('dailyReminders')}>
215+
<span style="color: var(--petalytics-subtle);">></span>
216+
<span style="color: var(--petalytics-foam);">daily_reminders:</span>
217+
<span style="color: var(--petalytics-text);">
218+
{preferences.dailyReminders ? 'enabled' : 'disabled'}
219+
</span>
220+
<span style="color: {preferences.dailyReminders ? 'var(--petalytics-pine)' : 'var(--petalytics-subtle)'};">
221+
{preferences.dailyReminders ? '' : ''}
222+
</span>
223+
</div>
224+
225+
<div class="cli-row flex items-center space-x-2 hover:bg-gray-800/20 px-2 py-1 rounded transition-colors cursor-pointer" on:click={() => togglePreference('aiInsights')}>
226+
<span style="color: var(--petalytics-subtle);">></span>
227+
<span style="color: var(--petalytics-foam);">ai_insights:</span>
228+
<span style="color: var(--petalytics-text);">
229+
{preferences.aiInsights ? 'enabled' : 'disabled'}
230+
</span>
231+
<span style="color: {preferences.aiInsights ? 'var(--petalytics-pine)' : 'var(--petalytics-subtle)'};">
232+
{preferences.aiInsights ? '' : ''}
233+
</span>
234+
</div>
235+
236+
<div class="cli-row flex items-center space-x-2 hover:bg-gray-800/20 px-2 py-1 rounded transition-colors cursor-pointer" on:click={() => togglePreference('notifications')}>
237+
<span style="color: var(--petalytics-subtle);">></span>
238+
<span style="color: var(--petalytics-foam);">notifications:</span>
239+
<span style="color: var(--petalytics-text);">
240+
{preferences.notifications ? 'enabled' : 'disabled'}
241+
</span>
242+
<span style="color: {preferences.notifications ? 'var(--petalytics-pine)' : 'var(--petalytics-subtle)'};">
243+
{preferences.notifications ? '' : ''}
244+
</span>
193245
</div>
194246
</div>
195247

196-
<!-- Status Section -->
197-
<div class="section">
198-
<div class="text-xs" style="color: var(--petalytics-muted);">
199-
<div class="flex justify-between">
200-
<span>API Status:</span>
201-
<span
202-
class:text-green-400={apiKeyStatus === 'valid'}
203-
class:text-red-400={apiKeyStatus === 'invalid'}
204-
class:text-yellow-400={apiKeyStatus === 'checking'}
205-
>
206-
{apiKeyStatus === 'valid'
207-
? 'Connected'
208-
: apiKeyStatus === 'invalid'
209-
? 'Invalid'
210-
: apiKeyStatus === 'checking'
211-
? 'Checking...'
212-
: 'Not set'}
213-
</span>
214-
</div>
248+
<!-- Status section -->
249+
<div class="cli-section mt-4">
250+
<div class="cli-row flex items-center space-x-2 px-2 py-1">
251+
<span style="color: var(--petalytics-subtle);">#</span>
252+
<span style="color: var(--petalytics-gold);">status</span>
253+
</div>
254+
255+
<div class="cli-row flex items-center space-x-2 px-2 py-1">
256+
<span style="color: var(--petalytics-subtle);">></span>
257+
<span style="color: var(--petalytics-foam);">api_status:</span>
258+
<span style="color: {apiKeyStatus === 'valid' ? 'var(--petalytics-pine)' : apiKeyStatus === 'invalid' ? 'var(--petalytics-love)' : 'var(--petalytics-gold)'};">
259+
{apiKeyStatus === 'valid' ? 'connected' : apiKeyStatus === 'invalid' ? 'invalid' : apiKeyStatus === 'checking' ? 'checking...' : 'not_set'}
260+
</span>
215261
</div>
216262
</div>
217263

218-
<!-- Data Management Section -->
219-
<div class="section">
220-
<div class="flex items-center justify-between mb-2">
221-
<span class="text-sm font-medium" style="color: var(--petalytics-subtle);"
222-
>Data Management</span
223-
>
224-
<button
225-
on:click={() => (showDataManager = !showDataManager)}
226-
class="text-xs px-2 py-1 rounded bg-gray-100 hover:bg-gray-200 text-gray-700 transition-colors"
227-
>
228-
{showDataManager ? 'Hide' : 'Show'} Export/Import
229-
</button>
264+
<!-- Data management toggle -->
265+
<div class="cli-section mt-4">
266+
<div class="cli-row flex items-center space-x-2 hover:bg-gray-800/20 px-2 py-1 rounded transition-colors cursor-pointer" on:click={() => showDataManager = !showDataManager}>
267+
<span style="color: var(--petalytics-subtle);">></span>
268+
<span style="color: var(--petalytics-foam);">data_manager:</span>
269+
<span style="color: var(--petalytics-text);">
270+
{showDataManager ? 'show' : 'hidden'}
271+
</span>
272+
<ChevronRight
273+
size={14}
274+
style="color: var(--petalytics-subtle); transform: {showDataManager ? 'rotate(90deg)' : 'rotate(0deg)'}; transition: transform 0.2s;"
275+
/>
230276
</div>
277+
</div>
231278

232-
{#if showDataManager}
279+
{#if showDataManager}
280+
<div class="mt-2 p-2 rounded" style="background: var(--petalytics-overlay);">
233281
<DataManager />
234-
{/if}
235-
</div>
282+
</div>
283+
{/if}
236284
</div>
237285
</div>

0 commit comments

Comments
 (0)