diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte new file mode 100644 index 0000000..992593e --- /dev/null +++ b/src/lib/components/Header.svelte @@ -0,0 +1,105 @@ + + +
+
+ + +
+ +
+ + + + + + + + {#if user} + + + {#snippet child({ props })} + + {/snippet} + + + +
+

+ {user.name || 'User'} +

+

+ {user.email} +

+
+
+ + + + Profile + + + + Settings + + + + + Log out + +
+
+ {:else} + + + {/if} +
+
\ No newline at end of file diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..61e9d65 --- /dev/null +++ b/src/lib/components/Sidebar.svelte @@ -0,0 +1,76 @@ + + + + + + + +
+ +

MyApp

+
+
+
+
+ + + Application + + + {#each items as item (item.title)} + + + {#snippet child({ props })} + + + {item.title} + + {/snippet} + + + {/each} + + + + + + + +
diff --git a/src/lib/components/SidebarUser.svelte b/src/lib/components/SidebarUser.svelte new file mode 100644 index 0000000..c88c106 --- /dev/null +++ b/src/lib/components/SidebarUser.svelte @@ -0,0 +1,124 @@ + + + + + + + {#snippet child({ props })} + + + + {userInitials} + +
+ {user.name} + {user.email} +
+ +
+ {/snippet} +
+ + +
+ + + {userInitials} + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + +
+ + +
+ +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/ThemeSelector.svelte b/src/lib/components/ThemeSelector.svelte new file mode 100644 index 0000000..9b2c8e8 --- /dev/null +++ b/src/lib/components/ThemeSelector.svelte @@ -0,0 +1,59 @@ + + + + + + + +
+

Primary Color

+ +
+ {#each primaryColors as color} + + {/each} +
+ + +
+
+
diff --git a/src/lib/components/connection-status.svelte b/src/lib/components/connection-status.svelte new file mode 100644 index 0000000..514232e --- /dev/null +++ b/src/lib/components/connection-status.svelte @@ -0,0 +1,119 @@ + + +
+
+ +
+
+
+ + {#if initialConnectionAttemptTime !== null && !hasTimedOut} +
+ {/if} + + {#if connection.status === 'OPEN'} +
+ {/if} +
+ + + {#snippet iconSnippet()} + {@const IconComponent = statusInfo().icon} + + {/snippet} + {@render iconSnippet()} + + + + {statusInfo().text} + +
+
\ No newline at end of file diff --git a/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..249d4a4 --- /dev/null +++ b/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..2bb9db4 --- /dev/null +++ b/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..e37214d --- /dev/null +++ b/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/avatar/index.ts b/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..d06457b --- /dev/null +++ b/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,13 @@ +import Root from "./avatar.svelte"; +import Image from "./avatar-image.svelte"; +import Fallback from "./avatar-fallback.svelte"; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, +}; diff --git a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte index 0acd1ec..2ee7d7c 100644 --- a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte +++ b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -2,7 +2,7 @@ import { tv, type VariantProps } from "tailwind-variants"; export const sidebarMenuButtonVariants = tv({ - base: "peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-start text-sm outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pe-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + base: "peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-clip rounded-md p-2 text-start text-sm outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pe-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", variants: { variant: { default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", diff --git a/src/lib/context/theme.svelte.ts b/src/lib/context/theme.svelte.ts new file mode 100644 index 0000000..10991df --- /dev/null +++ b/src/lib/context/theme.svelte.ts @@ -0,0 +1,93 @@ +import { getContext, setContext } from 'svelte'; +import { browser } from '$app/environment'; +import { parse, formatHex, oklch } from 'culori'; + +const THEME_STATE_KEY = Symbol('THEME_STATE'); + +// Predefined primary color options +export const primaryColors = [ + { name: 'Blue', value: 'oklch(0.6 0.2 260)' }, + { name: 'Emerald', value: 'oklch(0.6 0.2 160)' }, + { name: 'Rose', value: 'oklch(0.6 0.2 0)' }, + { name: 'Violet', value: 'oklch(0.6 0.2 290)' }, + { name: 'Orange', value: 'oklch(0.65 0.2 50)' }, + { name: 'Cyan', value: 'oklch(0.7 0.15 200)' } +] as const; + +export class ThemeState { + primaryColor = $state(null); + + constructor() { + if (browser) { + const saved = localStorage.getItem('primary-color'); + if (saved) { + this.primaryColor = saved; + this.applyPrimaryColor(saved); + } + } + } + + setPrimaryColor(color: string | null) { + this.primaryColor = color; + + if (browser) { + if (color) { + localStorage.setItem('primary-color', color); + this.applyPrimaryColor(color); + } else { + localStorage.removeItem('primary-color'); + this.clearPrimaryColor(); + } + } + } + + private applyPrimaryColor(color: string) { + if (!browser) return; + document.documentElement.style.setProperty('--primary', color); + } + + private clearPrimaryColor() { + if (!browser) return; + document.documentElement.style.removeProperty('--primary'); + } + + // Convert hex to OKLCH + hexToOklch(hex: string): string { + try { + const color = parse(hex); + if (color) { + const oklchColor = oklch(color); + if (oklchColor) { + const l = oklchColor.l?.toFixed(3) ?? '0'; + const c = oklchColor.c?.toFixed(3) ?? '0'; + const h = isNaN(oklchColor.h!) ? '0' : oklchColor.h!.toFixed(0); + return `oklch(${l} ${c} ${h})`; + } + } + } catch (e) { + console.warn('Failed to convert hex to OKLCH:', hex, e); + } + return 'oklch(0.6 0.2 260)'; + } + + // Convert OKLCH to hex + oklchToHex(oklchStr: string): string { + try { + const color = parse(oklchStr); + if (color) { + return formatHex(color) || '#3b82f6'; + } + } catch (e) { + console.warn('Failed to parse OKLCH color:', oklchStr, e); + } + return '#3b82f6'; + } +} + +export function setThemeState(): ThemeState { + return setContext(THEME_STATE_KEY, new ThemeState()); +} + +export function getThemeState(): ThemeState { + return getContext(THEME_STATE_KEY); +} diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte new file mode 100644 index 0000000..1bc5986 --- /dev/null +++ b/src/routes/(app)/+layout.svelte @@ -0,0 +1,25 @@ + + + + +
+
+
+ {@render children?.()} +
+
+
\ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 0d8eb03..0df932c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,10 @@ + {@render children()}