use:more shadcn components
This commit is contained in:
@@ -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",
|
||||
|
||||
169
src/app.css
169
src/app.css
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Location marker styles */
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
17
src/lib/components/avatar/avatar-fallback.svelte
Normal file
17
src/lib/components/avatar/avatar-fallback.svelte
Normal 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}
|
||||
/>
|
||||
17
src/lib/components/avatar/avatar-image.svelte
Normal file
17
src/lib/components/avatar/avatar-image.svelte
Normal 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}
|
||||
/>
|
||||
19
src/lib/components/avatar/avatar.svelte
Normal file
19
src/lib/components/avatar/avatar.svelte
Normal 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}
|
||||
/>
|
||||
13
src/lib/components/avatar/index.ts
Normal file
13
src/lib/components/avatar/index.ts
Normal 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
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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} />
|
||||
27
src/lib/components/dropdown-menu/dropdown-menu-item.svelte
Normal file
27
src/lib/components/dropdown-menu/dropdown-menu-item.svelte
Normal 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}
|
||||
/>
|
||||
24
src/lib/components/dropdown-menu/dropdown-menu-label.svelte
Normal file
24
src/lib/components/dropdown-menu/dropdown-menu-label.svelte
Normal 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>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
49
src/lib/components/dropdown-menu/index.ts
Normal file
49
src/lib/components/dropdown-menu/index.ts
Normal 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
|
||||
};
|
||||
7
src/lib/components/skeleton/index.ts
Normal file
7
src/lib/components/skeleton/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './skeleton.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Skeleton
|
||||
};
|
||||
17
src/lib/components/skeleton/skeleton.svelte
Normal file
17
src/lib/components/skeleton/skeleton.svelte
Normal 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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
});
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user