diff --git a/opal-web/src/app.css b/opal-web/src/app.css new file mode 100644 index 0000000..20f40c8 --- /dev/null +++ b/opal-web/src/app.css @@ -0,0 +1,186 @@ +/* Global Styles - Mobile-First */ + +:root { + /* Colors */ + --color-primary: #4f46e5; + --color-primary-dark: #4338ca; + --color-secondary: #6b7280; + --color-success: #10b981; + --color-danger: #ef4444; + --color-warning: #f59e0b; + + /* Backgrounds */ + --bg-primary: #ffffff; + --bg-secondary: #f9fafb; + --bg-tertiary: #f3f4f6; + + /* Text */ + --text-primary: #111827; + --text-secondary: #6b7280; + --text-tertiary: #9ca3af; + + /* Borders */ + --border-color: #e5e7eb; + --border-radius: 0.5rem; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + + /* Typography */ + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + + /* Layout */ + --nav-height: 60px; + --content-max-width: 768px; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); +} + +/* Reset & Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + background-color: var(--bg-secondary); + color: var(--text-primary); + min-height: 100vh; + overflow-x: hidden; +} + +/* Typography */ +h1 { + font-size: var(--font-size-2xl); + font-weight: 700; + line-height: 1.2; + margin-bottom: var(--spacing-md); +} + +h2 { + font-size: var(--font-size-xl); + font-weight: 600; + line-height: 1.3; + margin-bottom: var(--spacing-sm); +} + +h3 { + font-size: var(--font-size-lg); + font-weight: 600; + line-height: 1.4; + margin-bottom: var(--spacing-sm); +} + +p { + margin-bottom: var(--spacing-md); +} + +a { + color: var(--color-primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Layout Helpers */ +.container { + max-width: var(--content-max-width); + margin: 0 auto; + padding: 0 var(--spacing-md); +} + +.page { + min-height: calc(100vh - var(--nav-height)); + padding-bottom: calc(var(--nav-height) + var(--spacing-md)); +} + +/* Utility Classes */ +.text-center { + text-align: center; +} + +.text-sm { + font-size: var(--font-size-sm); +} + +.text-secondary { + color: var(--text-secondary); +} + +.mt-sm { margin-top: var(--spacing-sm); } +.mt-md { margin-top: var(--spacing-md); } +.mt-lg { margin-top: var(--spacing-lg); } +.mb-sm { margin-bottom: var(--spacing-sm); } +.mb-md { margin-bottom: var(--spacing-md); } +.mb-lg { margin-bottom: var(--spacing-lg); } + +.hidden { + display: none; +} + +/* Loading State */ +.loading { + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid var(--color-primary); + border-radius: 50%; + border-top-color: transparent; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Touch Targets - Mobile First */ +button, +a, +input, +select, +textarea { + min-height: 44px; + min-width: 44px; +} + +/* Focus Styles */ +:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Smooth Scrolling */ +html { + scroll-behavior: smooth; +} + +/* Safe Area Padding (for mobile notches) */ +@supports (padding: env(safe-area-inset-bottom)) { + .page { + padding-bottom: calc(var(--nav-height) + var(--spacing-md) + env(safe-area-inset-bottom)); + } +} diff --git a/opal-web/src/lib/components/BottomNav.svelte b/opal-web/src/lib/components/BottomNav.svelte new file mode 100644 index 0000000..c655a09 --- /dev/null +++ b/opal-web/src/lib/components/BottomNav.svelte @@ -0,0 +1,91 @@ + + + + + diff --git a/opal-web/src/lib/components/ui/Button.svelte b/opal-web/src/lib/components/ui/Button.svelte new file mode 100644 index 0000000..1867e07 --- /dev/null +++ b/opal-web/src/lib/components/ui/Button.svelte @@ -0,0 +1,115 @@ + + + + + diff --git a/opal-web/src/lib/components/ui/Checkbox.svelte b/opal-web/src/lib/components/ui/Checkbox.svelte new file mode 100644 index 0000000..ae1d926 --- /dev/null +++ b/opal-web/src/lib/components/ui/Checkbox.svelte @@ -0,0 +1,86 @@ + + + + + diff --git a/opal-web/src/lib/components/ui/Input.svelte b/opal-web/src/lib/components/ui/Input.svelte new file mode 100644 index 0000000..c56298e --- /dev/null +++ b/opal-web/src/lib/components/ui/Input.svelte @@ -0,0 +1,87 @@ + + +
+ {#if label} + + {/if} + + + + {#if error} + {error} + {/if} +
+ + diff --git a/opal-web/src/lib/components/ui/Select.svelte b/opal-web/src/lib/components/ui/Select.svelte new file mode 100644 index 0000000..47aaf60 --- /dev/null +++ b/opal-web/src/lib/components/ui/Select.svelte @@ -0,0 +1,73 @@ + + +
+ {#if label} + + {/if} + + +
+ + diff --git a/opal-web/src/routes/+layout.svelte b/opal-web/src/routes/+layout.svelte index 5c4f0f7..66a03c8 100644 --- a/opal-web/src/routes/+layout.svelte +++ b/opal-web/src/routes/+layout.svelte @@ -1,11 +1,32 @@ - - - +
+
+ +
+ + {#if $authStore.isAuthenticated && !isAuthPage} + + {/if} +
-{@render children()} + diff --git a/opal-web/src/routes/auth/callback/+page.svelte b/opal-web/src/routes/auth/callback/+page.svelte new file mode 100644 index 0000000..191b262 --- /dev/null +++ b/opal-web/src/routes/auth/callback/+page.svelte @@ -0,0 +1,83 @@ + + +
+
+
+ {#if error} +

Authentication Failed

+

{error}

+ Try Again + {:else} +
+
+
+

{status}

+ {/if} +
+
+
+ + diff --git a/opal-web/src/routes/auth/login/+page.svelte b/opal-web/src/routes/auth/login/+page.svelte new file mode 100644 index 0000000..889a1e5 --- /dev/null +++ b/opal-web/src/routes/auth/login/+page.svelte @@ -0,0 +1,79 @@ + + +
+
+ +
+
+ + diff --git a/opal-web/src/routes/projects/+page.svelte b/opal-web/src/routes/projects/+page.svelte new file mode 100644 index 0000000..14893e5 --- /dev/null +++ b/opal-web/src/routes/projects/+page.svelte @@ -0,0 +1,6 @@ +
+
+

Projects

+

Projects view coming soon...

+
+
diff --git a/opal-web/src/routes/settings/+page.svelte b/opal-web/src/routes/settings/+page.svelte new file mode 100644 index 0000000..a0d2654 --- /dev/null +++ b/opal-web/src/routes/settings/+page.svelte @@ -0,0 +1,192 @@ + + +
+
+

Settings

+ + {#if $authStore.isAuthenticated} +
+

Account

+
+ Username: + {$authStore.user?.username || 'Unknown'} +
+ {#if $authStore.user?.email} +
+ Email: + {$authStore.user.email} +
+ {/if} +
+ +
+

Sync

+
+ Status: + {$syncStore.status} +
+
+ Queue: + {$syncStore.queueSize} changes +
+ {#if $syncStore.lastSync} +
+ Last Sync: + {new Date($syncStore.lastSync * 1000).toLocaleString()} +
+ {/if} + + +
+ +
+

Actions

+ +
+ {:else} +
+

API Key Authentication

+

+ For testing, you can authenticate with an API key. Generate a key using: + opal server keygen --name "Web" +

+ + + + + +
+

+ Or login with OAuth +

+
+
+ {/if} +
+
+ + diff --git a/opal-web/src/routes/tags/+page.svelte b/opal-web/src/routes/tags/+page.svelte new file mode 100644 index 0000000..04308d3 --- /dev/null +++ b/opal-web/src/routes/tags/+page.svelte @@ -0,0 +1,6 @@ +
+
+

Tags

+

Tags view coming soon...

+
+