use:more shadcn components

This commit is contained in:
2025-10-03 15:45:57 +02:00
parent 00da815d52
commit d82f590fab
28 changed files with 695 additions and 218 deletions

View File

@@ -21,6 +21,7 @@
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.22.0",
"@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.544.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
@@ -28,6 +29,7 @@
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22",
"bits-ui": "^2.11.4",
"clsx": "^2.1.1",
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.40.0",

View File

@@ -1,54 +1,135 @@
@font-face {
font-family: 'Washington';
src: url('/fonts/Washington.ttf') format('truetype');
font-weight: normal;
font-style: normal;
@import 'tailwindcss';
@import 'tw-animate-css';
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
body {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
line-height: 1.5;
background-color: #f8f8f8;
color: #333;
margin: 0 auto;
}
@layer base {
@font-face {
font-family: 'Washington';
src: url('/fonts/Washington.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
h1 {
font-family: 'Washington', serif;
font-weight: normal;
}
* {
@apply border-border outline-ring/50;
box-sizing: border-box;
margin: 0;
padding: 0;
}
h2,
h3,
h4,
h5,
h6 {
font-family: 'Times New Roman', Times, serif;
font-weight: normal;
}
body {
@apply bg-background text-foreground;
button {
cursor: pointer;
font-family: inherit;
}
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
line-height: 1.5;
background-color: #f8f8f8;
color: #333;
margin: 0 auto;
}
input {
font-family: inherit;
}
h1 {
font-family: 'Washington', serif;
font-weight: normal;
font-size: 2rem;
}
h2,
h3,
h4,
h5,
h6 {
font-family: 'Times New Roman', Times, serif;
font-weight: normal;
}
a {
color: inherit;
text-decoration: none;
button {
cursor: pointer;
font-family: inherit;
}
input {
font-family: inherit;
}
a {
color: inherit;
text-decoration: none;
}
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { ProfileIcon, ProfilePanel } from '$lib';
import { ProfilePanel } from '$lib';
type User = {
id: string;
@@ -7,29 +7,13 @@
};
let { user }: { user: User } = $props();
let showProfilePanel = $state(false);
function toggleProfilePanel() {
showProfilePanel = !showProfilePanel;
}
function closeProfilePanel() {
showProfilePanel = false;
}
</script>
<header class="app-header">
<div class="header-content">
<h1 class="app-title">Serengo</h1>
<div class="profile-container">
<ProfileIcon username={user.username} onclick={toggleProfilePanel} />
<ProfilePanel
username={user.username}
id={user.id}
isOpen={showProfilePanel}
onClose={closeProfilePanel}
/>
<ProfilePanel username={user.username} id={user.id} />
</div>
</div>
</header>
@@ -39,7 +23,6 @@
width: 100%;
position: relative;
z-index: 10;
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}

View File

@@ -113,7 +113,6 @@
position: absolute;
top: 12px;
right: 12px;
z-index: 1000;
}
/* Location marker styles */

View File

@@ -1,59 +0,0 @@
<script lang="ts">
interface Props {
username: string;
onclick?: () => void;
}
let { username, onclick }: Props = $props();
// Get the first letter of username for avatar
const initial = username.charAt(0).toUpperCase();
</script>
<button class="profile-icon" {onclick} type="button">
<div class="avatar">
{initial}
</div>
</button>
<style>
.profile-icon {
background: none;
border: none;
padding: 0;
cursor: pointer;
border-radius: 50%;
transition: transform 0.2s ease;
}
.profile-icon:hover {
transform: scale(1.05);
}
.profile-icon:active {
transform: scale(0.95);
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: black;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
@media (max-width: 480px) {
.avatar {
width: 36px;
height: 36px;
font-size: 14px;
}
}
</style>

View File

@@ -1,128 +1,170 @@
<script lang="ts">
import { enhance } from '$app/forms';
import Modal from './Modal.svelte';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from './dropdown-menu';
import { Avatar, AvatarFallback } from './avatar';
interface Props {
username: string;
id: string;
isOpen: boolean;
onClose: () => void;
}
let { username, id, isOpen, onClose }: Props = $props();
let { username, id }: Props = $props();
// Create a bindable showModal that syncs with isOpen
let showModal = $derived(false);
// Sync showModal with isOpen prop
$effect(() => {
showModal = isOpen;
});
// Handle modal close and sync back to parent
$effect(() => {
if (!showModal && isOpen) {
onClose();
}
});
// Get the first letter of username for avatar
const initial = username.charAt(0).toUpperCase();
</script>
<Modal bind:showModal positioning="dropdown">
{#snippet header()}
<h2>Profile</h2>
{/snippet}
<DropdownMenu>
<DropdownMenuTrigger class="profile-trigger">
<Avatar class="profile-avatar">
<AvatarFallback class="profile-avatar-fallback">
{initial}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<div class="panel-content">
<div class="user-item">
<span class="label">Username</span>
<span class="value">{username}</span>
<DropdownMenuContent align="end" class="profile-dropdown-content">
<div class="profile-header">
<span class="profile-title">Profile</span>
</div>
<div class="user-item">
<span class="label">User ID</span>
<span class="value">{id}</span>
<DropdownMenuSeparator />
<div class="user-info-item">
<span class="info-label">Username</span>
<span class="info-value">{username}</span>
</div>
<div class="panel-actions">
<form method="post" action="/logout" use:enhance>
<button type="submit" class="signout-button"> Sign out </button>
</form>
<div class="user-info-item">
<span class="info-label">User ID</span>
<span class="info-value">{id}</span>
</div>
</div>
</Modal>
<DropdownMenuSeparator />
<form method="post" action="/logout" use:enhance>
<DropdownMenuItem variant="destructive" class="logout-item">
<button type="submit" class="logout-button">Sign out</button>
</DropdownMenuItem>
</form>
</DropdownMenuContent>
</DropdownMenu>
<style>
.panel-content {
:global(.profile-trigger) {
background: none;
border: none;
padding: 0;
cursor: pointer;
border-radius: 50%;
transition: transform 0.2s ease;
outline: none;
}
:global(.profile-trigger:hover) {
transform: scale(1.05);
}
:global(.profile-trigger:active) {
transform: scale(0.95);
}
:global(.profile-avatar) {
width: 40px;
height: 40px;
}
:global(.profile-avatar-fallback) {
background: black;
color: white;
font-weight: 600;
font-size: 16px;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
:global(.profile-dropdown-content) {
min-width: 200px;
max-width: 240px;
padding: 4px;
}
.profile-header {
padding: 8px 12px;
}
.profile-title {
font-size: 14px;
font-weight: 600;
color: #333;
}
.user-info-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 12px;
}
.user-item {
padding: 16px 20px;
border-bottom: 1px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-item:last-of-type {
border-bottom: none;
}
.label {
font-size: 14px;
.info-label {
font-size: 12px;
color: #666;
font-weight: 500;
}
.value {
font-size: 14px;
.info-value {
font-size: 13px;
color: #333;
font-family: monospace;
font-weight: 500;
word-break: break-all;
}
.panel-actions {
padding: 20px;
border-top: 1px solid #e5e5e5;
:global(.logout-item) {
padding: 0;
margin: 4px;
}
.signout-button {
background-color: #dc3545;
color: white;
.logout-button {
background: none;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
width: 100%;
padding: 6px 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.signout-button:hover {
background-color: #c82333;
}
.signout-button:active {
background-color: #bd2130;
}
:global(.modal h2) {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
color: inherit;
text-align: left;
border-radius: 4px;
}
@media (max-width: 480px) {
.user-item {
padding: 14px 16px;
:global(.profile-avatar) {
width: 36px;
height: 36px;
}
.panel-actions {
padding: 16px;
:global(.profile-avatar-fallback) {
font-size: 14px;
}
:global(.profile-dropdown-content) {
min-width: 180px;
max-width: 200px;
}
.profile-header {
padding: 6px 10px;
}
.user-info-item {
padding: 6px 10px;
}
}
</style>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
data-slot="avatar-fallback"
class={cn('flex size-full items-center justify-center rounded-full bg-muted', className)}
{...restProps}
/>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn('aspect-square size-full', className)}
{...restProps}
/>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
loadingStatus = $bindable('loading'),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
bind:ref
bind:loadingStatus
data-slot="avatar"
class={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
{...restProps}
/>

View File

@@ -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
};

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<MinusIcon class="size-4" />
{:else}
<CheckIcon class={cn('size-4', !checked && 'text-transparent')} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
class={cn(
'z-50 max-h-(--bits-dropdown-menu-content-available-height) min-w-[8rem] origin-(--bits-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
data-slot="dropdown-menu-group-heading"
data-inset={inset}
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)}
{...restProps}
/>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
inset,
variant = 'default',
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: 'default' | 'destructive';
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 data-[variant=destructive]:data-highlighted:text-destructive dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:!text-destructive",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CircleIcon from '@lucide/svelte/icons/circle';
import { cn, type WithoutChild } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<CircleIcon class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn('-mx-1 my-1 h-px bg-border', className)}
{...restProps}
/>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
{...restProps}
>
{@render children?.()}
</span>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
'z-50 min-w-[8rem] origin-(--bits-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRightIcon class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
</script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />

View File

@@ -0,0 +1,49 @@
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
import Content from './dropdown-menu-content.svelte';
import Group from './dropdown-menu-group.svelte';
import Item from './dropdown-menu-item.svelte';
import Label from './dropdown-menu-label.svelte';
import RadioGroup from './dropdown-menu-radio-group.svelte';
import RadioItem from './dropdown-menu-radio-item.svelte';
import Separator from './dropdown-menu-separator.svelte';
import Shortcut from './dropdown-menu-shortcut.svelte';
import Trigger from './dropdown-menu-trigger.svelte';
import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
import GroupHeading from './dropdown-menu-group-heading.svelte';
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
GroupHeading as DropdownMenuGroupHeading,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger
};

View File

@@ -0,0 +1,7 @@
import Root from './skeleton.svelte';
export {
Root,
//
Root as Skeleton
};

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
</script>
<div
bind:this={ref}
data-slot="skeleton"
class={cn('animate-pulse rounded-md bg-accent', className)}
{...restProps}
></div>

View File

@@ -2,7 +2,6 @@
export { default as Input } from './components/Input.svelte';
export { default as Button } from './components/Button.svelte';
export { default as ErrorMessage } from './components/ErrorMessage.svelte';
export { default as ProfileIcon } from './components/ProfileIcon.svelte';
export { default as ProfilePanel } from './components/ProfilePanel.svelte';
export { default as Header } from './components/Header.svelte';
export { default as Modal } from './components/Modal.svelte';

View File

@@ -199,17 +199,20 @@ export const getMapCenter = derived(coordinates, ($coordinates) => {
});
// Utility function to get appropriate zoom level based on accuracy
export const getMapZoom = derived([coordinates, shouldZoomToLocation], ([$coordinates, $shouldZoom]) => {
if ($coordinates?.accuracy) {
// More aggressive zoom levels when location button is clicked
const baseZoom = $shouldZoom ? 2 : 0; // Add 2 zoom levels when triggered by button
export const getMapZoom = derived(
[coordinates, shouldZoomToLocation],
([$coordinates, $shouldZoom]) => {
if ($coordinates?.accuracy) {
// More aggressive zoom levels when location button is clicked
const baseZoom = $shouldZoom ? 2 : 0; // Add 2 zoom levels when triggered by button
// Adjust zoom based on accuracy (lower accuracy = lower zoom)
if ($coordinates.accuracy < 10) return Math.min(20, 18 + baseZoom); // Very accurate
if ($coordinates.accuracy < 50) return Math.min(19, 16 + baseZoom); // Good accuracy
if ($coordinates.accuracy < 100) return Math.min(18, 14 + baseZoom); // Moderate accuracy
if ($coordinates.accuracy < 500) return Math.min(16, 12 + baseZoom); // Low accuracy
return Math.min(15, 10 + baseZoom); // Very low accuracy
// Adjust zoom based on accuracy (lower accuracy = lower zoom)
if ($coordinates.accuracy < 10) return Math.min(20, 18 + baseZoom); // Very accurate
if ($coordinates.accuracy < 50) return Math.min(19, 16 + baseZoom); // Good accuracy
if ($coordinates.accuracy < 100) return Math.min(18, 14 + baseZoom); // Moderate accuracy
if ($coordinates.accuracy < 500) return Math.min(16, 12 + baseZoom); // Low accuracy
return Math.min(15, 10 + baseZoom); // Very low accuracy
}
return $shouldZoom ? 16 : 13; // More aggressive default when triggered by button
}
return $shouldZoom ? 16 : 13; // More aggressive default when triggered by button
});
);