initial cleanup
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||
|
||||
import { type GeoLocation } from '$lib/stores/settings';
|
||||
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
|
||||
export let label: string = 'Search Locations...';
|
||||
export let placeholder: string = 'Enter city name...';
|
||||
|
||||
interface ResultSet {
|
||||
results: GeoLocation[] | undefined;
|
||||
@@ -80,7 +79,6 @@
|
||||
};
|
||||
}
|
||||
|
||||
// Always set format=json to fetch data
|
||||
const url = 'https://geocoding-api.open-meteo.com/v1/search';
|
||||
const fetchUrl = `${url}?${new URLSearchParams({ name: searchQuery })}`;
|
||||
const result = await fetch(fetchUrl);
|
||||
@@ -97,169 +95,180 @@
|
||||
|
||||
<Dialog.Root bind:open={dialogOpen}>
|
||||
<Dialog.Trigger
|
||||
id="location_search"
|
||||
class="group flex h-14 w-full cursor-pointer items-center justify-start rounded-xl border-2 border-gray-200 bg-white px-6 transition-all duration-200 hover:border-blue-400 hover:shadow-md focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-700"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
dialogOpen = !dialogOpen;
|
||||
}}
|
||||
class="flex h-12 w-full cursor-pointer items-center justify-center rounded-md border border-border px-5 pr-6 duration-200 hover:bg-accent"
|
||||
><svg
|
||||
class="lucide lucide-search mr-[5px]"
|
||||
>
|
||||
<svg
|
||||
class="mr-3 h-5 w-5 text-gray-400 transition-colors group-hover:text-blue-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
{label}</Dialog.Trigger
|
||||
>
|
||||
<span
|
||||
class="text-gray-600 transition-colors group-hover:text-gray-900 dark:text-gray-300 dark:group-hover:text-white"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="bg-black/5" />
|
||||
<Dialog.Overlay class="bg-black/20 backdrop-blur-sm" />
|
||||
|
||||
<Dialog.Content
|
||||
class="top-[10%] flex max-h-[calc(100vh-10%)] min-h-[400px] translate-y-0 flex-col overflow-y-auto border-border sm:max-w-[600px]"
|
||||
class="top-[10%] flex max-h-[calc(100vh-10%)] min-h-[500px] translate-y-0 flex-col overflow-hidden rounded-2xl border-border bg-white shadow-2xl sm:max-w-[700px] dark:bg-gray-800"
|
||||
>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Search Locations</Dialog.Title>
|
||||
<Dialog.Header class="pb-6">
|
||||
<Dialog.Title class="text-center text-2xl font-bold">Find Your Location</Dialog.Title>
|
||||
<p class="text-center text-gray-600 dark:text-gray-300">
|
||||
Search for a city or use GPS to detect your location
|
||||
</p>
|
||||
</Dialog.Header>
|
||||
<div>
|
||||
<div class="flex gap-3">
|
||||
<Input
|
||||
type="search"
|
||||
id="location_search_input"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-label="Search Location"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
<Button
|
||||
id="location_search_gps"
|
||||
variant="outline"
|
||||
title="Detect Location via GPS"
|
||||
onclick={() => (searchQuery = 'GPS')}
|
||||
><svg
|
||||
class="lucide lucide-mouse-pointer-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="px-6">
|
||||
<div class="mb-6 flex gap-3">
|
||||
<div class="flex-1">
|
||||
<Input
|
||||
type="search"
|
||||
{placeholder}
|
||||
class="h-12 text-lg"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-label="Search Location"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
</svg></Button
|
||||
>
|
||||
</div>
|
||||
{#await results}
|
||||
<div class="mt-4 flex h-full items-center justify-center">
|
||||
<Alert.Root class="my-auto w-[unset] border-border">
|
||||
<Alert.Description>Loading...</Alert.Description>
|
||||
</Alert.Root>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="px-4"
|
||||
title="Use GPS Location"
|
||||
onclick={() => (searchQuery = 'GPS')}
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
{:then results}
|
||||
{#if results.results && results.results.length === 0}
|
||||
{#if searchQuery.length < 2}
|
||||
<Alert.Root class="my-auto mt-4 w-[unset] border-border">
|
||||
<Alert.Description>Start typing to search for locations</Alert.Description>
|
||||
</Alert.Root>
|
||||
{:else}
|
||||
<Alert.Root class="my-auto !mt-4 w-[unset] border-border">
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-6 pb-6">
|
||||
{#await results}
|
||||
<div class="flex h-32 items-center justify-center">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="h-6 w-6 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
||||
<span class="text-gray-600 dark:text-gray-300">Searching...</span>
|
||||
</div>
|
||||
</div>
|
||||
{:then results}
|
||||
{#if results.results && results.results.length === 0}
|
||||
{#if searchQuery.length < 2}
|
||||
<Alert.Root class="border-blue-200 bg-blue-50 dark:bg-blue-900/20">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Alert.Description class="text-blue-700 dark:text-blue-300">
|
||||
Start typing to search for locations or use GPS to detect your current position
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{:else}
|
||||
<Alert.Root class="border-orange-200 bg-orange-50 dark:bg-orange-900/20">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<Alert.Description class="text-orange-700 dark:text-orange-300">
|
||||
No locations found for "{searchQuery}". Try a different search term.
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
{:else if !results.results}
|
||||
<Alert.Root variant="destructive">
|
||||
<Alert.Description>No locations found</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
{:else if !results.results}
|
||||
<Alert.Root class="my-auto !mt-4 w-[unset] border-border">
|
||||
<Alert.Description>No locations found</Alert.Description>
|
||||
</Alert.Root>
|
||||
{:else}
|
||||
<div class="list-group mt-4">
|
||||
<div id="location_search_results" class="rounded-lg border border-border">
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each results.results || [] as location, i (i)}
|
||||
<Button
|
||||
variant="outline"
|
||||
class="location-search-result flex h-[unset] w-full justify-between gap-0 rounded-none py-2 pr-1 pl-3 not-last:border-b md:pr-2 {i ===
|
||||
0
|
||||
? 'rounded-t-md'
|
||||
: ''} {results.results && i === results.results.length - 1
|
||||
? 'rounded-b-md'
|
||||
: ''}"
|
||||
<button
|
||||
class="group w-full rounded-xl border border-gray-200 p-4 text-left transition-all duration-200 hover:border-blue-400 hover:bg-blue-50 dark:border-gray-600 dark:hover:bg-blue-900/20"
|
||||
onclick={() => selectLocation(location)}
|
||||
>
|
||||
<div class="pointer-events-none flex flex-col gap-1 truncate">
|
||||
<div class="flex items-center gap-2 truncate text-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-1 items-center space-x-4">
|
||||
<img
|
||||
height="24"
|
||||
width="24"
|
||||
class="h-10 w-10 rounded-full shadow-md"
|
||||
src="/images/country-flags/{(
|
||||
location.country_code || 'united_nations'
|
||||
).toLowerCase()}.svg"
|
||||
title={location.country}
|
||||
alt={location.country_code}
|
||||
alt={location.country}
|
||||
/>
|
||||
{location.name}
|
||||
<div class="flex-1">
|
||||
<h3
|
||||
class="font-semibold text-gray-900 group-hover:text-blue-600 dark:text-white dark:group-hover:text-blue-400"
|
||||
>
|
||||
{location.name}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{location.admin1 || ''}
|
||||
{location.country || ''}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{location.latitude.toFixed(2)}°N {location.longitude.toFixed(2)}°E
|
||||
{#if location.elevation}• {location.elevation.toFixed(0)}m{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="truncate text-left text-sm text-muted-foreground">
|
||||
{location.admin1 || ''} ({location.latitude.toFixed(2)}°N {location.longitude.toFixed(
|
||||
2
|
||||
)}°E{#if location.elevation}{' ' +
|
||||
location.elevation.toFixed(0) +
|
||||
'm asl'}{/if})
|
||||
</div>
|
||||
</div>
|
||||
<div class="-mr-1 flex justify-self-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="px-2 duration-200 hover:brightness-[140%] md:px-3"
|
||||
href="https://www.openstreetmap.org/#map=13/{location.latitude}/{location.longitude}"
|
||||
target="_blank"
|
||||
title="Show on map"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 group-hover:text-blue-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-map"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"
|
||||
/>
|
||||
<path d="M15 5.764v15" />
|
||||
<path d="M9 3.236v15" />
|
||||
</svg>
|
||||
</Button>
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Button>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Alert.Root variant="destructive" class="my-auto mt-4 w-[unset]">
|
||||
<Alert.Description>{error.message}</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/await}
|
||||
{/if}
|
||||
{:catch error}
|
||||
<Alert.Root variant="destructive">
|
||||
<Alert.Description>Error: {error.message}</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: 'Current Weather',
|
||||
url: '/weather/week',
|
||||
description: 'Current conditions and overview',
|
||||
icon: '🌡️'
|
||||
},
|
||||
{
|
||||
title: 'Model Comparison',
|
||||
url: '/weather/compare',
|
||||
description: 'Compare multiple weather models',
|
||||
icon: '📊'
|
||||
},
|
||||
{
|
||||
title: '14 Day Forecast',
|
||||
url: '/weather/14-day',
|
||||
description: 'Extended forecast with uncertainty',
|
||||
icon: '📅'
|
||||
}
|
||||
];
|
||||
|
||||
let mobileNavOpened = $state(false);
|
||||
let currentPath = $derived($page.url.pathname);
|
||||
|
||||
const isActive = (url: string) => {
|
||||
return currentPath === url || currentPath.startsWith(url + '/');
|
||||
};
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="sticky top-0 z-50 border-b border-gray-200/50 bg-white/90 shadow-sm backdrop-blur-lg dark:border-gray-700/50 dark:bg-gray-900/90"
|
||||
>
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 p-2">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<a
|
||||
href={resolve('/')}
|
||||
class="text-xl font-bold text-gray-900 transition-colors hover:text-blue-600 dark:text-white"
|
||||
>
|
||||
Open-Meteo Weather
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden items-center space-x-2 md:flex">
|
||||
{#each links as link (link.title)}
|
||||
<Button
|
||||
href={link.url}
|
||||
variant={isActive(link.url) ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
class="group relative px-4 py-2 {isActive(link.url)
|
||||
? 'bg-blue-600 text-white shadow-md'
|
||||
: 'hover:bg-blue-50 dark:hover:bg-blue-900/20'}"
|
||||
>
|
||||
<span class="mr-1">{link.icon}</span>
|
||||
{link.title}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<div class="md:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="rounded-lg"
|
||||
onclick={() => (mobileNavOpened = !mobileNavOpened)}
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{#if mobileNavOpened}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
{#if mobileNavOpened}
|
||||
<div class="border-t border-gray-200 py-4 md:hidden dark:border-gray-700">
|
||||
<div class="space-y-2">
|
||||
{#each links as link (link.title)}
|
||||
<Button
|
||||
href={link.url}
|
||||
variant={isActive(link.url) ? 'default' : 'ghost'}
|
||||
class="w-full justify-start py-3 {isActive(link.url) ? 'bg-blue-600 text-white' : ''}"
|
||||
onclick={() => (mobileNavOpened = false)}
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-lg">{link.icon}</span>
|
||||
<div class="text-left">
|
||||
<div class="font-medium">{link.title}</div>
|
||||
<div
|
||||
class="text-xs {isActive(link.url)
|
||||
? 'text-blue-100'
|
||||
: 'text-gray-500 dark:text-gray-400'}"
|
||||
>
|
||||
{link.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
:global(.container) {
|
||||
max-width: 1200px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<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<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -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<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -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<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("leading-none font-semibold", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<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<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { writable } from 'svelte/store';
|
||||
// Placeholder function for urlHashStore
|
||||
// In a real application, this would handle URL hash parameters
|
||||
// and return a Svelte store that reflects those parameters.
|
||||
export function urlHashStore(initialValue: Record<string, any>) {
|
||||
export function urlHashStore(initialValue: Record<string, unknown>) {
|
||||
const { subscribe, set, update } = writable(initialValue);
|
||||
|
||||
// In a full implementation, you would add logic here to:
|
||||
@@ -18,6 +18,7 @@ export function urlHashStore(initialValue: Record<string, any>) {
|
||||
set,
|
||||
update,
|
||||
// You might want to add methods to easily update specific hash parameters
|
||||
updateParam: (key: string, value: any) => update((current) => ({ ...current, [key]: value }))
|
||||
updateParam: (key: string, value: unknown) =>
|
||||
update((current) => ({ ...current, [key]: value }))
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user