chore: init
This commit is contained in:
169
src/lib/components/lnv/AddVehicleDrawer.svelte
Normal file
169
src/lib/components/lnv/AddVehicleDrawer.svelte
Normal file
@@ -0,0 +1,169 @@
|
||||
<script lang="ts">
|
||||
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||
import { BikeIcon, CarIcon, PlusCircleIcon, SaveIcon, TractorIcon, TruckIcon, XIcon } from "@lucide/svelte";
|
||||
import Button, { buttonVariants } from "../ui/button/button.svelte";
|
||||
import { DefaultVehicle, isValidFuel, selectVehicle, setVehicles, vehicles, type Vehicle, type VehicleType } from "$lib/vehicles/vehicles.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Select from "../ui/select";
|
||||
import Input from "../ui/input/input.svelte";
|
||||
import EvConnectorSelect from "./EVConnectorSelect.svelte";
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
|
||||
function getVehicleIcon(type: VehicleType) {
|
||||
switch (type) {
|
||||
case "car":
|
||||
return CarIcon;
|
||||
case "motor_scooter":
|
||||
return TractorIcon;
|
||||
case "bicycle":
|
||||
return BikeIcon;
|
||||
case "motorcycle":
|
||||
return BikeIcon;
|
||||
case "truck":
|
||||
return TruckIcon;
|
||||
default:
|
||||
return TractorIcon; // Default icon if no match
|
||||
}
|
||||
}
|
||||
|
||||
let vehicle: Vehicle = $state({
|
||||
name: "",
|
||||
type: "motor_scooter",
|
||||
legalMaxSpeed: 45,
|
||||
actualMaxSpeed: 45,
|
||||
emissionClass: "euro_5",
|
||||
fuelType: "diesel",
|
||||
preferredFuel: "Diesel"
|
||||
});
|
||||
</script>
|
||||
|
||||
<Drawer.Root bind:open={open}>
|
||||
<Drawer.Trigger class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}>
|
||||
{@render children()}
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Add Vehicle</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<div class="p-4 pt-0 flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<Select.Root type="single" bind:value={vehicle.type}>
|
||||
<Select.Trigger class="w-[180px]">
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.type === "car" ? "Car" : vehicle.type === "motor_scooter" ? "Moped" : "?"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="car">
|
||||
<CarIcon />
|
||||
Car
|
||||
</Select.Item>
|
||||
<Select.Item value="motor_scooter">
|
||||
<TractorIcon />
|
||||
Moped
|
||||
</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Vehicle Name"
|
||||
bind:value={vehicle.name}
|
||||
class="w-full"
|
||||
aria-label="Vehicle Name"
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-around mt-4">
|
||||
<span>Legal Speed</span>
|
||||
<span>/</span>
|
||||
<span>Actual Speed</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Legal Speed"
|
||||
bind:value={vehicle.legalMaxSpeed}
|
||||
class="w-full text-center"
|
||||
aria-label="Legal Max Speed"
|
||||
aria-required="true"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Actual Speed"
|
||||
bind:value={vehicle.actualMaxSpeed}
|
||||
class="w-full text-center"
|
||||
aria-label="Actual Max Speed"
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-around mt-4">
|
||||
<span>Fuel Type</span>
|
||||
<span>/</span>
|
||||
<span>Preferred Fuel</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Select.Root type="single" bind:value={vehicle.fuelType}>
|
||||
<Select.Trigger class="w-full">
|
||||
{vehicle.fuelType === "diesel" ? "Diesel" : vehicle.fuelType === "petrol" ? "Petrol" : "Electric"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="diesel">Diesel</Select.Item>
|
||||
<Select.Item value="petrol">Petrol</Select.Item>
|
||||
<Select.Item value="electric">Electric</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<Select.Root type="single" bind:value={vehicle.preferredFuel}>
|
||||
<Select.Trigger class="w-full">
|
||||
{vehicle.preferredFuel}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#if vehicle.fuelType === "diesel"}
|
||||
<Select.Item value="Diesel">Diesel</Select.Item>
|
||||
{:else if vehicle.fuelType === "petrol"}
|
||||
<Select.Item value="Super">Super</Select.Item>
|
||||
<Select.Item value="Super E10">Super E10</Select.Item>
|
||||
{:else if vehicle.fuelType === "electric"}
|
||||
<EvConnectorSelect />
|
||||
{/if}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
<Drawer.Footer>
|
||||
<Button onclick={() => {
|
||||
open = false;
|
||||
if (vehicle.name.trim() === "") {
|
||||
alert("Please enter a vehicle name.");
|
||||
return;
|
||||
}
|
||||
if (vehicle.legalMaxSpeed <= 0 || vehicle.actualMaxSpeed <= 0) {
|
||||
alert("Please enter valid speeds.");
|
||||
return;
|
||||
}
|
||||
if(!isValidFuel(vehicle)) {
|
||||
alert("Please select a valid fuel type and preferred fuel.");
|
||||
return;
|
||||
}
|
||||
setVehicles([...vehicles, vehicle]);
|
||||
selectVehicle(vehicle);
|
||||
location.reload(); // TODO
|
||||
}}>
|
||||
<SaveIcon />
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="secondary" onclick={() => {
|
||||
open = false;
|
||||
}}>
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
10
src/lib/components/lnv/EVConnectorSelect.svelte
Normal file
10
src/lib/components/lnv/EVConnectorSelect.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
import { EVConnectors } from "$lib/vehicles/vehicles.svelte";
|
||||
import * as Select from "../ui/select";
|
||||
</script>
|
||||
|
||||
{#each EVConnectors as connector}
|
||||
<Select.Item value={connector}>
|
||||
{connector}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
111
src/lib/components/lnv/LocationSelect.svelte
Normal file
111
src/lib/components/lnv/LocationSelect.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
|
||||
import { tick } from "svelte";
|
||||
import * as Command from "$lib/components/ui/command/index.js";
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
value: "sveltekit",
|
||||
label: "SvelteKit",
|
||||
},
|
||||
{
|
||||
value: "next.js",
|
||||
label: "Next.js",
|
||||
},
|
||||
{
|
||||
value: "nuxt.js",
|
||||
label: "Nuxt.js",
|
||||
},
|
||||
{
|
||||
value: "remix",
|
||||
label: "Remix",
|
||||
},
|
||||
{
|
||||
value: "astro",
|
||||
label: "Astro",
|
||||
},
|
||||
];
|
||||
|
||||
let open = $state(false);
|
||||
let value = $state("");
|
||||
let triggerRef = $state<HTMLButtonElement>(null!);
|
||||
|
||||
const selectedValue = $derived(
|
||||
value === "location" ? "My Location" : value
|
||||
);
|
||||
|
||||
// We want to refocus the trigger button when the user selects
|
||||
// an item from the list so users can continue navigating the
|
||||
// rest of the form with the keyboard.
|
||||
function closeAndFocusTrigger() {
|
||||
open = false;
|
||||
tick().then(() => {
|
||||
triggerRef.focus();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger bind:ref={triggerRef}>
|
||||
{#snippet child({ props }: { props: Record<string, any> })}
|
||||
<Button
|
||||
variant="outline"
|
||||
class="justify-between"
|
||||
{...props}
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
{selectedValue || "Select a location..."}
|
||||
<ChevronsUpDownIcon class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-[200px] p-0">
|
||||
<Command.Root>
|
||||
<Command.Input placeholder="Search..." />
|
||||
<Command.List>
|
||||
<Command.Empty>No location found.</Command.Empty>
|
||||
<Command.Group>
|
||||
<Command.Item
|
||||
value={"location"}
|
||||
onSelect={() => {
|
||||
value = "location";
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
value !== "location" && "text-transparent"
|
||||
)}
|
||||
/>
|
||||
My Location
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group>
|
||||
{#each frameworks as framework}
|
||||
<Command.Item
|
||||
value={framework.value}
|
||||
onSelect={() => {
|
||||
value = framework.value;
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
value !== framework.value && "text-transparent"
|
||||
)}
|
||||
/>
|
||||
{framework.label}
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.Group>
|
||||
</Command.List>
|
||||
</Command.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
180
src/lib/components/lnv/Map.svelte
Normal file
180
src/lib/components/lnv/Map.svelte
Normal file
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
GeoJSONSource,
|
||||
GeolocateControl,
|
||||
Hash,
|
||||
LineLayer,
|
||||
MapLibre,
|
||||
Marker,
|
||||
Protocol,
|
||||
} from "svelte-maplibre-gl";
|
||||
import { view } from "./sidebar.svelte";
|
||||
import { geolocate, map, pin } from "./map.svelte";
|
||||
import {
|
||||
drawAllRoutes,
|
||||
fetchRoute,
|
||||
routing,
|
||||
} from "$lib/services/navigation/routing.svelte";
|
||||
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
|
||||
import { ROUTING_SERVER } from "$lib/services/hosts";
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("resize", map.updateMapPadding);
|
||||
map.updateMapPadding();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Protocol
|
||||
scheme="tiles"
|
||||
loadFn={async (params) => {
|
||||
console.log(params.url);
|
||||
const url = params.url.replace("tiles://", "").replace("tiles.openfreemap.org/", "");
|
||||
const path = url.split("/")[0];
|
||||
if (path == "natural_earth") {
|
||||
const t = await fetch("https://tiles.openfreemap.org/" + url);
|
||||
if (t.status == 200) {
|
||||
const buffer = await t.arrayBuffer();
|
||||
return { data: buffer };
|
||||
} else {
|
||||
throw new Error(`Tile fetch error: ${t.statusText}`);
|
||||
}
|
||||
} else if (path == "planet") {
|
||||
const t = await fetch("https://tiles.openfreemap.org/" + url);
|
||||
if (t.status == 200) {
|
||||
const buffer = await t.arrayBuffer();
|
||||
return { data: buffer };
|
||||
} else {
|
||||
throw new Error(`Tile fetch error: ${t.statusText}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid tiles protocol path");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<MapLibre
|
||||
class="w-full h-full"
|
||||
style="/style.json"
|
||||
bind:map={map.value}
|
||||
padding={map.padding}
|
||||
onload={async () => {
|
||||
map.updateMapPadding();
|
||||
}}
|
||||
onclick={(e) => {
|
||||
if (view.current.type == "main" || view.current.type == "info") {
|
||||
pin.dropPin(e.lngLat.lat, e.lngLat.lng);
|
||||
pin.showInfo();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<!-- <Hash /> -->
|
||||
<GeolocateControl
|
||||
positionOptions={{
|
||||
enableHighAccuracy: true,
|
||||
}}
|
||||
trackUserLocation={true}
|
||||
autoTrigger={true}
|
||||
ongeolocate={(e: GeolocationPosition) => {
|
||||
const speed = Math.round((e.coords.speed || 0) * 3.6); // In km/h
|
||||
const accuracy = Math.round(e.coords.accuracy);
|
||||
geolocate.currentLocation = {
|
||||
lat: e.coords.latitude,
|
||||
lon: e.coords.longitude,
|
||||
};
|
||||
// $inspect(`Geolocation: ${e.coords.latitude}, ${e.coords.longitude} (Speed: ${speed} km/h, Accuracy: ${accuracy} m)`);
|
||||
}}
|
||||
/>
|
||||
{#if pin.isDropped}
|
||||
<Marker lnglat={{ lat: pin.lat, lng: pin.lng }} />
|
||||
{/if}
|
||||
|
||||
{#if routing.geojson.routePast}
|
||||
<GeoJSONSource id="route-past" data={routing.geojson.routePast}>
|
||||
<LineLayer
|
||||
id="route-past-border"
|
||||
source="route-past"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 13,
|
||||
}}
|
||||
></LineLayer>
|
||||
<LineLayer
|
||||
id="route-past"
|
||||
source="route-past"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#acacac",
|
||||
"line-width": 8,
|
||||
}}
|
||||
></LineLayer>
|
||||
</GeoJSONSource>
|
||||
{/if}
|
||||
{#if routing.geojson.al0}
|
||||
<GeoJSONSource id="al0" data={routing.geojson.al0}>
|
||||
<LineLayer
|
||||
id="al0-border"
|
||||
source="al0"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 13,
|
||||
}}
|
||||
></LineLayer>
|
||||
<LineLayer
|
||||
id="al0"
|
||||
source="al0"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#94aad4",
|
||||
"line-width": 8,
|
||||
}}
|
||||
></LineLayer>
|
||||
</GeoJSONSource>
|
||||
{/if}
|
||||
{#if routing.geojson.al1}
|
||||
<GeoJSONSource id="al1" data={routing.geojson.al1}>
|
||||
<LineLayer
|
||||
id="al1-border"
|
||||
source="al1"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 13,
|
||||
}}
|
||||
></LineLayer>
|
||||
<LineLayer
|
||||
id="al1"
|
||||
source="al1"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#94aad4",
|
||||
"line-width": 8,
|
||||
}}
|
||||
></LineLayer>
|
||||
</GeoJSONSource>
|
||||
{/if}
|
||||
{#if routing.geojson.route}
|
||||
<GeoJSONSource id="route" data={routing.geojson.route}>
|
||||
<LineLayer
|
||||
id="route-border"
|
||||
source="route"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 13,
|
||||
}}
|
||||
></LineLayer>
|
||||
<LineLayer
|
||||
id="route"
|
||||
source="route"
|
||||
layout={{ "line-join": "round", "line-cap": "round" }}
|
||||
paint={{
|
||||
"line-color": "#3478f6",
|
||||
"line-width": 8,
|
||||
}}
|
||||
></LineLayer>
|
||||
</GeoJSONSource>
|
||||
{/if}
|
||||
</MapLibre>
|
||||
0
src/lib/components/lnv/Post.svelte
Normal file
0
src/lib/components/lnv/Post.svelte
Normal file
17
src/lib/components/lnv/RequiresCapability.svelte
Normal file
17
src/lib/components/lnv/RequiresCapability.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { hasCapability, type Capabilities } from "$lib/services/lnv";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let { capability, children }: {
|
||||
capability: Capabilities[number];
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#await hasCapability(capability) then has}
|
||||
{#if has}
|
||||
{@render children()}
|
||||
{/if}
|
||||
{:catch error}
|
||||
<!-- user is likely offline -->
|
||||
{/await}
|
||||
204
src/lib/components/lnv/Sidebar.svelte
Normal file
204
src/lib/components/lnv/Sidebar.svelte
Normal file
@@ -0,0 +1,204 @@
|
||||
<script lang="ts">
|
||||
import { onMount, type Component } from "svelte";
|
||||
import InvalidSidebar from "./sidebar/InvalidSidebar.svelte";
|
||||
import { searchbar, view } from "./sidebar.svelte";
|
||||
import MainSidebar from "./sidebar/MainSidebar.svelte";
|
||||
import InfoSidebar from "./sidebar/InfoSidebar.svelte";
|
||||
import RouteSidebar from "./sidebar/RouteSidebar.svelte";
|
||||
import { map } from "./map.svelte";
|
||||
import TripSidebar from "./sidebar/TripSidebar.svelte";
|
||||
import Input from "../ui/input/input.svelte";
|
||||
import { HomeIcon, SettingsIcon, UserIcon } from "@lucide/svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
import { search, type Feature } from "$lib/services/Search";
|
||||
import SearchSidebar from "./sidebar/SearchSidebar.svelte";
|
||||
|
||||
const views: {[key: string]: Component<any>} = {
|
||||
main: MainSidebar,
|
||||
info: InfoSidebar,
|
||||
route: RouteSidebar,
|
||||
trip: TripSidebar,
|
||||
search: SearchSidebar
|
||||
};
|
||||
|
||||
let isDragging = false;
|
||||
let startY = 0;
|
||||
let startHeight = 0;
|
||||
let sidebarHeight = $state(200);
|
||||
|
||||
let CurrentView = $derived(views[view.current.type] || InvalidSidebar);
|
||||
|
||||
function debounce<T>(getter: () => T, delay: number): () => T | undefined {
|
||||
let value = $state<T>();
|
||||
let timer: NodeJS.Timeout;
|
||||
$effect(() => {
|
||||
const newValue = getter(); // read here to subscribe to it
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => value = newValue, delay);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
return () => value;
|
||||
}
|
||||
|
||||
let loading = $state(false);
|
||||
|
||||
let searchText = $derived.by(debounce(() => searchbar.text, 300));
|
||||
let searchResults: Feature[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
if(!searchText) {
|
||||
searchResults = [];
|
||||
if(view.current.type == "search") view.switch("main");
|
||||
return;
|
||||
}
|
||||
if (searchText.length > 0) {
|
||||
loading = true;
|
||||
search(searchText, 0, 0).then(results => {
|
||||
searchResults = results;
|
||||
loading = false;
|
||||
view.switch("search", {
|
||||
results: searchResults,
|
||||
query: searchText
|
||||
})
|
||||
});
|
||||
} else {
|
||||
searchResults = [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="floating-search">
|
||||
<Input class="h-10"
|
||||
placeholder="Search..." bind:value={searchbar.text} />
|
||||
</div>
|
||||
<div id="sidebar" style={window.innerWidth < 768 ? `height: ${sidebarHeight}px` : ""}>
|
||||
{#if window.innerWidth < 768}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
||||
<div role="button" id="grabber" style="height: 10px; cursor: grab; display: flex; justify-content: center; align-items: center; touch-action: none;" ontouchstart={(e) => {
|
||||
isDragging = true;
|
||||
startY = e.touches[0].clientY;
|
||||
startHeight = sidebarHeight;
|
||||
}} ontouchmove={(e) => {
|
||||
if(!isDragging) return;
|
||||
e.preventDefault();
|
||||
const deltaY = e.touches[0].clientY - startY;
|
||||
let newHeight = Math.max(100, startHeight - deltaY);
|
||||
|
||||
const snapPoint = 200;
|
||||
const snapThreshold = 20;
|
||||
if (Math.abs(newHeight - snapPoint) < snapThreshold) {
|
||||
newHeight = snapPoint;
|
||||
}
|
||||
sidebarHeight = newHeight;
|
||||
|
||||
map.updateMapPadding();
|
||||
}} ontouchend={() => {
|
||||
if(!isDragging) return;
|
||||
isDragging = false;
|
||||
}}>
|
||||
<div style="height: 8px; background-color: #acacac; width: 40%; border-radius: 15px;"></div>
|
||||
</div>
|
||||
{/if}
|
||||
<CurrentView {...view.current.props}></CurrentView>
|
||||
</div>
|
||||
<div id="navigation">
|
||||
<button>
|
||||
<HomeIcon />
|
||||
</button>
|
||||
<button>
|
||||
<UserIcon />
|
||||
</button>
|
||||
<button>
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#sidebar {
|
||||
background-color: hsla(0, 0%, 5%, 0.6);
|
||||
backdrop-filter: blur(5px);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
width: calc(25% - 20px);
|
||||
max-width: calc(25% - 20px);
|
||||
overflow-y: scroll;
|
||||
height: calc(100vh - 20px - 40px - 10px - 50px);
|
||||
max-height: calc(100vh - 20px);
|
||||
|
||||
position: fixed;
|
||||
top: calc(40px + 10px);
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
margin: 10px;
|
||||
border-radius: 15px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
#floating-search {
|
||||
position: fixed;
|
||||
margin: 10px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
width: calc(25% - 20px);
|
||||
background-color: hsla(0, 0%, 5%, 0.6);
|
||||
backdrop-filter: blur(5px);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
background-color: hsla(0, 0%, 5%, 0.9);
|
||||
backdrop-filter: blur(5px);
|
||||
margin-bottom: 0;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
width: calc(25% - 20px);
|
||||
/* border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px; */
|
||||
height: 50px;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
top: unset;
|
||||
bottom: 50px;
|
||||
left: 0;
|
||||
/* min-width: calc(100% - 20px);
|
||||
max-width: calc(100% - 20px); */
|
||||
min-width: calc(100%);
|
||||
max-width: calc(100%);
|
||||
width: calc(100% - 20px);
|
||||
height: 200px;
|
||||
margin: unset;
|
||||
/* margin-left: 10px;
|
||||
margin-right: 10px; */
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
padding-top: 5px; /* for the grabber */
|
||||
}
|
||||
|
||||
#floating-search {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
#navigation {
|
||||
margin: 0;
|
||||
width: calc(100%);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
src/lib/components/lnv/VehicleSelector.svelte
Normal file
57
src/lib/components/lnv/VehicleSelector.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||
import { BikeIcon, CarIcon, PlusCircleIcon, TractorIcon, TruckIcon } from "@lucide/svelte";
|
||||
import Button, { buttonVariants } from "../ui/button/button.svelte";
|
||||
import { DefaultVehicle, selectedVehicle, selectVehicle, vehicles, type VehicleType } from "$lib/vehicles/vehicles.svelte";
|
||||
import AddVehicleDrawer from "./AddVehicleDrawer.svelte";
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
function getVehicleIcon(type: VehicleType) {
|
||||
switch (type) {
|
||||
case "car":
|
||||
return CarIcon;
|
||||
case "motor_scooter":
|
||||
return TractorIcon;
|
||||
case "bicycle":
|
||||
return BikeIcon;
|
||||
case "motorcycle":
|
||||
return BikeIcon;
|
||||
case "truck":
|
||||
return TruckIcon;
|
||||
default:
|
||||
return TractorIcon; // Default icon if no match
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer.Root bind:open={open}>
|
||||
<Drawer.Trigger class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}>
|
||||
{@const vehicle = selectedVehicle() ?? DefaultVehicle}
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.name}
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Vehicle Selector</Drawer.Title>
|
||||
<Drawer.Description>Select your vehicle to customize routing just for you.</Drawer.Description>
|
||||
</Drawer.Header>
|
||||
<div class="p-4 pt-0 flex flex-col gap-2">
|
||||
{#each vehicles as vehicle}
|
||||
<Button variant={selectedVehicle() === vehicle ? "default" : "secondary"} class="w-full p-5" onclick={() => {selectVehicle(vehicle); open = false;}}>
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.name}
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
<AddVehicleDrawer>
|
||||
<Button variant="secondary" class="w-full p-5">
|
||||
<PlusCircleIcon />
|
||||
Add Vehicle
|
||||
</Button>
|
||||
</AddVehicleDrawer>
|
||||
</div>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
41
src/lib/components/lnv/info/FuelStation.svelte
Normal file
41
src/lib/components/lnv/info/FuelStation.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script>
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import { getStations } from "$lib/services/MTSK";
|
||||
|
||||
let { tags, lat, lng } = $props();
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">Fuel Types</h3>
|
||||
<ul class="flex gap-2 flex-wrap">
|
||||
{#each Object.entries(tags).filter(([key]) => key.startsWith("fuel:")) as [key, tag]}
|
||||
<!-- <li>{key.replace("fuel:", "")}: {tag}</li> -->
|
||||
<Badge>
|
||||
{key.replace("fuel:", "")}
|
||||
{#if tag !== "yes"}
|
||||
({tag})
|
||||
{/if}
|
||||
</Badge>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">Prices</h3>
|
||||
{#await getStations(lat, lng)}
|
||||
<p>Loading fuel prices...</p>
|
||||
{:then stations}
|
||||
{#if stations.stations.length > 0}
|
||||
{@const station = stations.stations[0]}
|
||||
{#if station.diesel}
|
||||
<p>Diesel: {station.diesel}</p>
|
||||
{/if}
|
||||
{#if station.e10}
|
||||
<p>E10: {station.e10}</p>
|
||||
{/if}
|
||||
{#if station.e5}
|
||||
<p>E5: {station.e5}</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>No fuel prices available.</p>
|
||||
{/if}
|
||||
{:catch err}
|
||||
<p>Error loading fuel prices: {err.message}</p>
|
||||
{/await}
|
||||
41
src/lib/components/lnv/info/MapAI.svelte
Normal file
41
src/lib/components/lnv/info/MapAI.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import { LNV_SERVER } from "$lib/services/hosts";
|
||||
import { ai } from "$lib/services/lnv";
|
||||
import { SparklesIcon } from "@lucide/svelte";
|
||||
|
||||
let { lat, lon } = $props();
|
||||
let question = $state("");
|
||||
|
||||
function getText(res: string) {
|
||||
console.log("Response from MapAI:", res);
|
||||
const chunks = res.split("\n");
|
||||
let text = "";
|
||||
for (const chunk of chunks) {
|
||||
if(chunk.startsWith("0:")) {
|
||||
text += JSON.parse(chunk.substring(2).trim());
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 mt-2 p-2 border-border border-solid border-2 rounded-lg">
|
||||
<SparklesIcon />
|
||||
<div class="flex gap-2 flex-col w-full">
|
||||
{#await ai(question, { lat, lon })}
|
||||
<p>Loading...</p>
|
||||
{:then data}
|
||||
{@const text = getText(data)}
|
||||
<p>{text}</p>
|
||||
{:catch error}
|
||||
<p>Error: {error.message}</p>
|
||||
{/await}
|
||||
<Input
|
||||
type="text"
|
||||
value={""}
|
||||
placeholder="Ask a question about this place..." onchange={(e) => {
|
||||
question = (e.target! as any).value;
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
27
src/lib/components/lnv/info/OpeningHours.svelte
Normal file
27
src/lib/components/lnv/info/OpeningHours.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import opening_hours from "opening_hours";
|
||||
|
||||
let { hours, lat, lon }: { hours: string, lat: number, lon: number } = $props();
|
||||
|
||||
const oh = $derived.by(() => {
|
||||
return new opening_hours(hours, {
|
||||
lat, lon, address: {
|
||||
country_code: "de", // Default to Germany, can be overridden if needed
|
||||
state: "NRW", // Default to North Rhine-Westphalia, can be overridden if needed
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">
|
||||
Opening Hours
|
||||
{#if oh.getState()}
|
||||
<Badge>Open</Badge>
|
||||
{:else}
|
||||
<Badge variant="destructive">Closed</Badge>
|
||||
{/if}
|
||||
</h3>
|
||||
|
||||
<p>{hours}</p>
|
||||
<!-- todo -->
|
||||
46
src/lib/components/lnv/info/Reviews.svelte
Normal file
46
src/lib/components/lnv/info/Reviews.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import * as Avatar from "$lib/components/ui/avatar";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { getReviews, postReview } from "$lib/services/lnv";
|
||||
import Stars from "./Stars.svelte";
|
||||
|
||||
let { lat, lng }: { lat: number, lng: number } = $props();
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">Reviews</h3>
|
||||
{#await getReviews({lat, lon: lng}) then reviews}
|
||||
{#if reviews.length > 0}
|
||||
<ul class="list-disc pl-5">
|
||||
{#each reviews as review}
|
||||
<li class="flex justify-center gap-2 mb-2 flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar.Root>
|
||||
<!-- <Avatar.Image src="https://github.com/shadcn.png" alt="@shadcn" /> -->
|
||||
<Avatar.Fallback>{review.username}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<Stars rating={review.rating} />
|
||||
</div>
|
||||
<span>{review.comment}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p>No reviews available.</p>
|
||||
{/if}
|
||||
<Button variant="secondary" onclick={async () => {
|
||||
const rating = prompt("Enter your rating (1-5):");
|
||||
const comment = prompt("Enter your review comment:");
|
||||
if (rating && comment) {
|
||||
console.log(`Rating: ${rating}, Comment: ${comment}`);
|
||||
await postReview({ lat, lon: lng }, {
|
||||
rating: parseInt(rating, 10),
|
||||
comment
|
||||
})
|
||||
alert("Thank you for your review!");
|
||||
} else {
|
||||
alert("Review submission cancelled.");
|
||||
}
|
||||
}} disabled>Write a review</Button><br>
|
||||
{:catch error}
|
||||
<p>Error loading reviews: {error.message}</p>
|
||||
{/await}
|
||||
43
src/lib/components/lnv/info/Stars.svelte
Normal file
43
src/lib/components/lnv/info/Stars.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { StarIcon } from "@lucide/svelte";
|
||||
|
||||
let { rating }: { rating: number } = $props();
|
||||
</script>
|
||||
|
||||
{#if rating == 0}
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
{:else if rating == 1}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
{:else if rating == 2}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
{:else if rating == 3}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon />
|
||||
<StarIcon />
|
||||
{:else if rating == 4}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon />
|
||||
{:else if rating == 5}
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
{/if}
|
||||
76
src/lib/components/lnv/map.svelte.ts
Normal file
76
src/lib/components/lnv/map.svelte.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { reverseGeocode } from "$lib/services/Search";
|
||||
import { view } from "./sidebar.svelte";
|
||||
|
||||
export const geolocate = $state({
|
||||
currentLocation: null as WorldLocation | null,
|
||||
})
|
||||
|
||||
export const map = $state({
|
||||
value: undefined as maplibregl.Map | undefined,
|
||||
updateMapPadding: () => {
|
||||
if(document.querySelector<HTMLDivElement>("#sidebar") == null) {
|
||||
map._setPadding({
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("Updating map padding");
|
||||
if (window.innerWidth < 768) {
|
||||
const calculatedSidebarHeight = document.querySelector<HTMLDivElement>("#sidebar")!.getBoundingClientRect().height;
|
||||
map._setPadding({
|
||||
top: 50,
|
||||
right: 0,
|
||||
bottom: calculatedSidebarHeight + 50,
|
||||
left: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
const calculatedSidebarWidth = document.querySelector<HTMLDivElement>("#sidebar")!.getBoundingClientRect().width;
|
||||
map._setPadding({
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: calculatedSidebarWidth,
|
||||
});
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
},
|
||||
_setPadding: (_padding: { top: number, right: number, bottom: number, left: number }) => {
|
||||
map.padding = _padding;
|
||||
if (map.value) {
|
||||
map.value.setPadding(map.padding);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const pin = $state({
|
||||
isDropped: false,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
dropPin: (lat: number, lng: number) => {
|
||||
pin.isDropped = true;
|
||||
pin.lat = lat;
|
||||
pin.lng = lng;
|
||||
},
|
||||
liftPin: () => {
|
||||
pin.isDropped = false;
|
||||
pin.lat = 0;
|
||||
pin.lng = 0;
|
||||
},
|
||||
showInfo: async () => {
|
||||
if(!pin.isDropped) return;
|
||||
// const res = await reverseGeocode({ lat: pin.lat, lon: pin.lng });
|
||||
// if(res.length > 0) {
|
||||
// const feature = res[0];
|
||||
// view.switch("info", { feature });
|
||||
// }
|
||||
view.switch("info", { lat: pin.lat, lng: pin.lng });
|
||||
}
|
||||
})
|
||||
26
src/lib/components/lnv/sidebar.svelte.ts
Normal file
26
src/lib/components/lnv/sidebar.svelte.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export type View = {
|
||||
type: string;
|
||||
props?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const view = $state({
|
||||
current: { type: "main" } as View,
|
||||
history: [] as View[],
|
||||
back: () => {
|
||||
if (view.history.length > 0) {
|
||||
view.current = view.history.pop()!;
|
||||
} else {
|
||||
view.current = { type: "main" } as View; // Reset to main view if history is empty
|
||||
}
|
||||
},
|
||||
switch: (to: string, props?: Record<string, any>) => {
|
||||
if (view.current.type !== to) {
|
||||
view.history.push(view.current);
|
||||
}
|
||||
view.current = { type: to, props } as View;
|
||||
}
|
||||
});
|
||||
|
||||
export const searchbar = $state({
|
||||
text: ""
|
||||
})
|
||||
196
src/lib/components/lnv/sidebar/InfoSidebar.svelte
Normal file
196
src/lib/components/lnv/sidebar/InfoSidebar.svelte
Normal file
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { POIIcons } from "$lib/POIIcons";
|
||||
import { OVERPASS_SERVER } from "$lib/services/hosts";
|
||||
import { BriefcaseIcon, EllipsisIcon, GlobeIcon, HomeIcon, MailIcon, PhoneIcon, RouteIcon } from "@lucide/svelte";
|
||||
import { pin } from "../map.svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { fetchPOI, type OverpassElement } from "$lib/services/Overpass";
|
||||
import OpeningHours from "../info/OpeningHours.svelte";
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import FuelStation from "../info/FuelStation.svelte";
|
||||
import { view } from "../sidebar.svelte";
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import Reviews from "../info/Reviews.svelte";
|
||||
import MapAi from "../info/MapAI.svelte";
|
||||
import { hasCapability } from "$lib/services/lnv";
|
||||
import RequiresCapability from "../RequiresCapability.svelte";
|
||||
|
||||
// let { feature }: { feature: Feature } = $props();
|
||||
|
||||
// let Icon = $derived(POIIcons[feature.properties.osm_key + "=" + feature.properties.osm_value]);
|
||||
|
||||
let { lat, lng }: { lat: number, lng: number } = $props();
|
||||
|
||||
function getIcon(tags: Record<string, string>): typeof POIIcons[keyof typeof POIIcons] | null {
|
||||
const key = Object.keys(tags).find(k => k.startsWith("amenity") || k.startsWith("shop"));
|
||||
if (key && POIIcons[key + "=" + tags[key]]) {
|
||||
return POIIcons[key + "=" + tags[key]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDistance(aLat: number, aLon: number, lat: number, lon: number): number {
|
||||
const R = 6371e3; // Earth radius in meters
|
||||
const φ1 = lat * Math.PI / 180;
|
||||
const φ2 = aLat * Math.PI / 180;
|
||||
const Δφ = (aLat - lat) * Math.PI / 180;
|
||||
const Δλ = (aLon - lon) * Math.PI / 180;
|
||||
|
||||
const a = Math.sin(Δφ/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2;
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
function sortByDistance(elements: OverpassElement[], lat: number, lng: number): OverpassElement[] {
|
||||
return elements.sort((a: OverpassElement, b: OverpassElement) => {
|
||||
const aLoc = a.center || a;
|
||||
const bLoc = b.center || b;
|
||||
return getDistance(aLoc.lat!, aLoc.lon!, lat, lng) - getDistance(bLoc.lat!, bLoc.lon!, lat, lng);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await fetchPOI(lat, lng, 20)}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<p>Loading...</p>
|
||||
{:then res}
|
||||
{#if res.elements.length === 0}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<span style="color: #acacac;">© OpenStreetMap</span>
|
||||
<pre>{JSON.stringify(res, null, 2)}</pre>
|
||||
{:else}
|
||||
{@const elements = sortByDistance(res.elements, lat, lng)}
|
||||
{@const tags = elements[0].tags}
|
||||
{@const firstElement = elements[0]}
|
||||
{@const ellat = firstElement.center?.lat || firstElement.lat!}
|
||||
{@const ellng = firstElement.center?.lon || firstElement.lon!}
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
{#if getIcon(tags)}
|
||||
{@const Icon = getIcon(tags)}
|
||||
<Icon />
|
||||
{/if}
|
||||
{tags.name || (tags["addr:street"] ? (tags["addr:street"] + " " + tags["addr:housenumber"]) : "")}
|
||||
</SidebarHeader>
|
||||
<div id="actions">
|
||||
<Button onclick={() => {
|
||||
view.switch("route", {
|
||||
to: lat + "," + lng,
|
||||
})
|
||||
}}>
|
||||
<RouteIcon />
|
||||
Route
|
||||
</Button>
|
||||
{#if tags.email || tags["contact:email"]}
|
||||
<Button variant="secondary" href={`mailto:${tags.email || tags["contact:email"]}`} target="_blank">
|
||||
<MailIcon />
|
||||
Email
|
||||
</Button>
|
||||
{/if}
|
||||
{#if tags.website || tags["contact:website"]}
|
||||
<Button variant="secondary" href={tags.website || tags["contact:website"]} target="_blank">
|
||||
<GlobeIcon />
|
||||
Website
|
||||
</Button>
|
||||
{/if}
|
||||
{#if tags.phone || tags["contact:phone"]}
|
||||
<Button variant="secondary" href={`tel:${tags.phone || tags["contact:phone"]}`} target="_blank">
|
||||
<PhoneIcon />
|
||||
Call
|
||||
</Button>
|
||||
{/if}
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button variant="secondary">
|
||||
<EllipsisIcon />
|
||||
More
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button variant="outline" onclick={() => {
|
||||
localStorage.setItem("saved.home", JSON.stringify({ lat, lon: lng }));
|
||||
}}>
|
||||
<HomeIcon />
|
||||
Set as Home
|
||||
</Button>
|
||||
<Button variant="outline" onclick={() => {
|
||||
localStorage.setItem("saved.work", JSON.stringify({ lat, lon: lng }));
|
||||
}}>
|
||||
<BriefcaseIcon />
|
||||
Set as Work
|
||||
</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
|
||||
<RequiresCapability capability="ai">
|
||||
<MapAi lat={ellat} lon={ellng} />
|
||||
</RequiresCapability>
|
||||
|
||||
<!--
|
||||
"addr:city": "Hagen",
|
||||
"addr:housenumber": "12",
|
||||
"addr:postcode": "58135",
|
||||
"addr:street": -->
|
||||
<p class="mt-2">{tags["addr:street"]} {tags["addr:housenumber"]}</p>
|
||||
<p>{tags["addr:postcode"]} {tags["addr:city"]}</p>
|
||||
|
||||
{#if tags.opening_hours}
|
||||
<OpeningHours hours={tags.opening_hours} {lat} lon={lng} />
|
||||
{/if}
|
||||
|
||||
{#if tags.amenity == "fuel"}
|
||||
<RequiresCapability capability="fuel">
|
||||
<FuelStation {tags} {lat} {lng} />
|
||||
</RequiresCapability>
|
||||
{/if}
|
||||
|
||||
<!-- any payment:* tag -->
|
||||
{#if Object.keys(tags).some(key => key.startsWith("payment:"))}
|
||||
<h3 class="text-lg font-bold mt-2">Payment Methods</h3>
|
||||
<ul style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
{#each Object.entries(tags).filter(([key]) => key.startsWith("payment:")) as [key, value]}
|
||||
<!-- <li>{key.replace("payment:", "")}: {value}</li> -->
|
||||
<Badge>{key.replace("payment:", "")}</Badge>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<RequiresCapability capability="reviews">
|
||||
<Reviews lat={ellat} lng={ellng} />
|
||||
</RequiresCapability>
|
||||
|
||||
<span style="color: #acacac;">© OpenStreetMap</span>
|
||||
|
||||
<pre>{JSON.stringify(elements, null, 2)}</pre>
|
||||
{/if}
|
||||
{:catch err}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<p>Error: {err.message}</p>
|
||||
{/await}
|
||||
|
||||
<style>
|
||||
#actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
2
src/lib/components/lnv/sidebar/InvalidSidebar.svelte
Normal file
2
src/lib/components/lnv/sidebar/InvalidSidebar.svelte
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Error</h1>
|
||||
<p>Invalid sidebar configuration.</p>
|
||||
117
src/lib/components/lnv/sidebar/MainSidebar copy.svelte
Normal file
117
src/lib/components/lnv/sidebar/MainSidebar copy.svelte
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { BriefcaseIcon, HomeIcon } from "@lucide/svelte";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { fly } from "svelte/transition";
|
||||
import { circInOut } from "svelte/easing";
|
||||
import { search, type Feature } from "$lib/services/Search";
|
||||
import { view } from "../sidebar.svelte";
|
||||
import { map, pin } from "../map.svelte";
|
||||
|
||||
function debounce<T>(getter: () => T, delay: number): () => T | undefined {
|
||||
let value = $state<T>();
|
||||
let timer: NodeJS.Timeout;
|
||||
$effect(() => {
|
||||
const newValue = getter(); // read here to subscribe to it
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => value = newValue, delay);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
return () => value;
|
||||
}
|
||||
|
||||
let typedText = $state("");
|
||||
let loading = $state(false);
|
||||
|
||||
let searchText = $derived.by(debounce(() => typedText, 300));
|
||||
let searchResults: Feature[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
if(!searchText) {
|
||||
searchResults = [];
|
||||
return;
|
||||
}
|
||||
if (searchText.length > 0) {
|
||||
loading = true;
|
||||
search(searchText, 0, 0).then(results => {
|
||||
searchResults = results;
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
searchResults = [];
|
||||
}
|
||||
});
|
||||
|
||||
$inspect("searchText", searchText);
|
||||
</script>
|
||||
|
||||
<div id="search-progress" style="min-height: calc(3px + 3px); width: 100%; min-height: 3ch;">
|
||||
{#if loading}
|
||||
LOADING
|
||||
{/if}
|
||||
</div>
|
||||
<Input placeholder="Search..." bind:value={typedText} class="mb-2" />
|
||||
{#if searchResults.length == 0}
|
||||
<div id="saved" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const home = localStorage.getItem("saved.home");
|
||||
if(!home) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(home);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<HomeIcon />
|
||||
Home
|
||||
</Button>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const work = localStorage.getItem("saved.work");
|
||||
if(!work) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(work);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<BriefcaseIcon />
|
||||
Work
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div id="results" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
{#each searchResults as result}
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
// view.switch("info", { feature: result });
|
||||
pin.dropPin(result.geometry.coordinates[1], result.geometry.coordinates[0]);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [result.geometry.coordinates[0], result.geometry.coordinates[1]],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
{result.properties.name}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#saved {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
/* justify-content: space-evenly; */
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
64
src/lib/components/lnv/sidebar/MainSidebar.svelte
Normal file
64
src/lib/components/lnv/sidebar/MainSidebar.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { BriefcaseIcon, HomeIcon } from "@lucide/svelte";
|
||||
import { Button } from "../../ui/button";
|
||||
import { fly } from "svelte/transition";
|
||||
import { circInOut } from "svelte/easing";
|
||||
import { map, pin } from "../map.svelte";
|
||||
import VehicleSelector from "../VehicleSelector.svelte";
|
||||
import Post from "../Post.svelte";
|
||||
</script>
|
||||
|
||||
<div id="saved" class="mt-2 mb-2" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const home = localStorage.getItem("saved.home");
|
||||
if(!home) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(home);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<HomeIcon />
|
||||
Home
|
||||
</Button>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const work = localStorage.getItem("saved.work");
|
||||
if(!work) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(work);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<BriefcaseIcon />
|
||||
Work
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<VehicleSelector />
|
||||
|
||||
<div>
|
||||
<h2>In your area</h2>
|
||||
|
||||
<Post />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#saved {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
/* justify-content: space-evenly; */
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
92
src/lib/components/lnv/sidebar/RouteSidebar.svelte
Normal file
92
src/lib/components/lnv/sidebar/RouteSidebar.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { CircleArrowDown, CircleDotIcon, StarIcon } from "@lucide/svelte";
|
||||
import LocationSelect from "../LocationSelect.svelte";
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
|
||||
import { drawAllRoutes, fetchRoute, removeAllRoutes, zoomToPoints } from "$lib/services/navigation/routing.svelte";
|
||||
import { ROUTING_SERVER } from "$lib/services/hosts";
|
||||
import { map } from "../map.svelte";
|
||||
import { view } from "../sidebar.svelte";
|
||||
import { DefaultVehicle, selectedVehicle } from "$lib/vehicles/vehicles.svelte";
|
||||
|
||||
let { from, to }: {
|
||||
from?: string,
|
||||
to?: string
|
||||
} = $props();
|
||||
|
||||
let fromLocation = $state(from || "");
|
||||
let toLocation = $state(to || "");
|
||||
|
||||
let routes: Trip[] | null = $state(null);
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
// const secs = seconds % 60;
|
||||
return `${hours != 0 ? hours + "h " : ""}${minutes}min`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
removeAllRoutes();
|
||||
}}>
|
||||
Route
|
||||
</SidebarHeader>
|
||||
|
||||
<span>Driving with <strong>{(selectedVehicle() ?? DefaultVehicle).name}</strong></span>
|
||||
<div class="flex flex-col gap-2 w-full mb-2">
|
||||
<div class="flex gap-2 items-center w-full">
|
||||
<CircleDotIcon />
|
||||
<Input bind:value={fromLocation} />
|
||||
</div>
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<CircleArrowDown />
|
||||
</div>
|
||||
<div class="flex gap-2 items-center w-full">
|
||||
<CircleDotIcon />
|
||||
<Input bind:value={toLocation} />
|
||||
</div>
|
||||
</div>
|
||||
<Button onclick={async () => {
|
||||
const FROM: WorldLocation = fromLocation == "home" ? JSON.parse(localStorage.getItem("saved.home")!)
|
||||
: fromLocation == "work" ? JSON.parse(localStorage.getItem("saved.work")!)
|
||||
: {
|
||||
lat: parseFloat(fromLocation.split(",")[0]),
|
||||
lon: parseFloat(fromLocation.split(",")[1])
|
||||
};
|
||||
const TO: WorldLocation = toLocation == "home" ? JSON.parse(localStorage.getItem("saved.home")!)
|
||||
: toLocation == "work" ? JSON.parse(localStorage.getItem("saved.work")!)
|
||||
: {
|
||||
lat: parseFloat(toLocation.split(",")[0]),
|
||||
lon: parseFloat(toLocation.split(",")[1])
|
||||
};
|
||||
const req = createValhallaRequest(selectedVehicle() ?? DefaultVehicle, [FROM, TO]);
|
||||
const res = await fetchRoute(ROUTING_SERVER, req);
|
||||
routes = [
|
||||
res.trip,
|
||||
];
|
||||
for(const alternate of res.alternates) {
|
||||
if(alternate.trip) {
|
||||
routes.push(alternate.trip);
|
||||
}
|
||||
}
|
||||
drawAllRoutes(routes);
|
||||
zoomToPoints(FROM, TO, map.value!);
|
||||
}}>Calculate</Button>
|
||||
|
||||
{#if routes}
|
||||
<div class="mt-2 flex gap-2 flex-col">
|
||||
{#each routes as route, i (route?.summary?.length)}
|
||||
<Button variant="secondary" onclick={() => {
|
||||
view.switch("trip", { route });
|
||||
}}>
|
||||
{#if i == 0}
|
||||
<StarIcon />
|
||||
{/if}
|
||||
{route.summary.length}km - {formatTime(Math.round(route.summary.time))}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
35
src/lib/components/lnv/sidebar/SearchSidebar.svelte
Normal file
35
src/lib/components/lnv/sidebar/SearchSidebar.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import { circInOut } from "svelte/easing";
|
||||
import { fly } from "svelte/transition";
|
||||
import { map, pin } from "../map.svelte";
|
||||
import type { Feature } from "$lib/services/Search";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { searchbar } from "../sidebar.svelte";
|
||||
|
||||
let { results, query }: {
|
||||
results: Feature[],
|
||||
query: string
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
searchbar.text = "";
|
||||
}}>
|
||||
Search Results
|
||||
</SidebarHeader>
|
||||
<div id="results" class="mt-2" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
{#each results as result}
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
// view.switch("info", { feature: result });
|
||||
pin.dropPin(result.geometry.coordinates[1], result.geometry.coordinates[0]);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [result.geometry.coordinates[0], result.geometry.coordinates[1]],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
{result.properties.name}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
20
src/lib/components/lnv/sidebar/SidebarHeader.svelte
Normal file
20
src/lib/components/lnv/sidebar/SidebarHeader.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { view } from "../sidebar.svelte";
|
||||
|
||||
let { children, onback }: {
|
||||
children: Snippet,
|
||||
onback?: () => void,
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<Button variant="outline" onclick={() => {
|
||||
view.back();
|
||||
if (onback) {
|
||||
onback();
|
||||
}
|
||||
}}><</Button>
|
||||
<h2 class="text-lg font-bold flex gap-2 items-center">{@render children?.()}</h2>
|
||||
</div>
|
||||
51
src/lib/components/lnv/sidebar/TripSidebar.svelte
Normal file
51
src/lib/components/lnv/sidebar/TripSidebar.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { drawRoute, removeAllRoutes, startRoute } from "$lib/services/navigation/routing.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { RouteIcon, SaveIcon, SendIcon } from "@lucide/svelte";
|
||||
import { map } from "../map.svelte";
|
||||
|
||||
let { route }: {
|
||||
route: Trip
|
||||
} = $props();
|
||||
|
||||
onMount(() => {
|
||||
removeAllRoutes();
|
||||
drawRoute(route);
|
||||
})
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
removeAllRoutes();
|
||||
}}>
|
||||
Trip Details
|
||||
</SidebarHeader>
|
||||
|
||||
<div id="actions" class="flex gap-2">
|
||||
<Button onclick={async () => {
|
||||
await startRoute(route);
|
||||
requestAnimationFrame(() => {
|
||||
map.updateMapPadding();
|
||||
})
|
||||
}}>
|
||||
<RouteIcon />
|
||||
Start Navigation
|
||||
</Button>
|
||||
<Button variant="secondary" disabled>
|
||||
<SaveIcon />
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="secondary" disabled>
|
||||
<SendIcon />
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
{#each route.legs[0].maneuvers as maneuver}
|
||||
<li>
|
||||
{maneuver.instruction}
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
17
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal file
17
src/lib/components/ui/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("bg-muted flex size-full items-center justify-center rounded-full", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/avatar/avatar-image.svelte
Normal file
17
src/lib/components/ui/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}
|
||||
/>
|
||||
17
src/lib/components/ui/avatar/avatar.svelte
Normal file
17
src/lib/components/ui/avatar/avatar.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.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="avatar"
|
||||
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
13
src/lib/components/ui/avatar/index.ts
Normal file
13
src/lib/components/ui/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,
|
||||
};
|
||||
50
src/lib/components/ui/badge/badge.svelte
Normal file
50
src/lib/components/ui/badge/badge.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||
destructive:
|
||||
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? "a" : "span"}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||
80
src/lib/components/ui/button/button.svelte
Normal file
80
src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||
outline:
|
||||
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
17
src/lib/components/ui/button/index.ts
Normal file
17
src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
20
src/lib/components/ui/card/card-action.svelte
Normal file
20
src/lib/components/ui/card/card-action.svelte
Normal file
@@ -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>
|
||||
15
src/lib/components/ui/card/card-content.svelte
Normal file
15
src/lib/components/ui/card/card-content.svelte
Normal file
@@ -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>
|
||||
20
src/lib/components/ui/card/card-description.svelte
Normal file
20
src/lib/components/ui/card/card-description.svelte
Normal 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<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
20
src/lib/components/ui/card/card-footer.svelte
Normal file
20
src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -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("[.border-t]:pt-6 flex items-center px-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/card/card-header.svelte
Normal file
23
src/lib/components/ui/card/card-header.svelte
Normal file
@@ -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 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/card/card-title.svelte
Normal file
20
src/lib/components/ui/card/card-title.svelte
Normal 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<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/card/card.svelte
Normal file
23
src/lib/components/ui/card/card.svelte
Normal file
@@ -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>
|
||||
25
src/lib/components/ui/card/index.ts
Normal file
25
src/lib/components/ui/card/index.ts
Normal file
@@ -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,
|
||||
};
|
||||
40
src/lib/components/ui/command/command-dialog.svelte
Normal file
40
src/lib/components/ui/command/command-dialog.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import Command from "./command.svelte";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run",
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
|
||||
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
title?: string;
|
||||
description?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open {...restProps}>
|
||||
<Dialog.Header class="sr-only">
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>{description}</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Content class="overflow-hidden p-0" {portalProps}>
|
||||
<Command
|
||||
class="**:data-[slot=command-input-wrapper]:h-12 [&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-group]]:px-2 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
|
||||
{...restProps}
|
||||
bind:value
|
||||
bind:ref
|
||||
{children}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
17
src/lib/components/ui/command/command-empty.svelte
Normal file
17
src/lib/components/ui/command/command-empty.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.EmptyProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Empty
|
||||
bind:ref
|
||||
data-slot="command-empty"
|
||||
class={cn("py-6 text-center text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
32
src/lib/components/ui/command/command-group.svelte
Normal file
32
src/lib/components/ui/command/command-group.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive, useId } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
heading,
|
||||
value,
|
||||
...restProps
|
||||
}: CommandPrimitive.GroupProps & {
|
||||
heading?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="command-group"
|
||||
class={cn("text-foreground overflow-hidden p-1", className)}
|
||||
value={value ?? heading ?? `----${useId()}`}
|
||||
{...restProps}
|
||||
>
|
||||
{#if heading}
|
||||
<CommandPrimitive.GroupHeading
|
||||
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
|
||||
>
|
||||
{heading}
|
||||
</CommandPrimitive.GroupHeading>
|
||||
{/if}
|
||||
<CommandPrimitive.GroupItems {children} />
|
||||
</CommandPrimitive.Group>
|
||||
26
src/lib/components/ui/command/command-input.svelte
Normal file
26
src/lib/components/ui/command/command-input.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(""),
|
||||
...restProps
|
||||
}: CommandPrimitive.InputProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-9 items-center gap-2 border-b px-3" data-slot="command-input-wrapper">
|
||||
<SearchIcon class="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
class={cn(
|
||||
"placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:ref
|
||||
{...restProps}
|
||||
bind:value
|
||||
/>
|
||||
</div>
|
||||
20
src/lib/components/ui/command/command-item.svelte
Normal file
20
src/lib/components/ui/command/command-item.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/command/command-link-item.svelte
Normal file
20
src/lib/components/ui/command/command-link-item.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.LinkItemProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.LinkItem
|
||||
bind:ref
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/command/command-list.svelte
Normal file
17
src/lib/components/ui/command/command-list.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.ListProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.List
|
||||
bind:ref
|
||||
data-slot="command-list"
|
||||
class={cn("max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/command/command-separator.svelte
Normal file
17
src/lib/components/ui/command/command-separator.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="command-separator"
|
||||
class={cn("bg-border -mx-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/command/command-shortcut.svelte
Normal file
20
src/lib/components/ui/command/command-shortcut.svelte
Normal file
@@ -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<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="command-shortcut"
|
||||
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
22
src/lib/components/ui/command/command.svelte
Normal file
22
src/lib/components/ui/command/command.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Root
|
||||
bind:value
|
||||
bind:ref
|
||||
data-slot="command"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
40
src/lib/components/ui/command/index.ts
Normal file
40
src/lib/components/ui/command/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
|
||||
import Root from "./command.svelte";
|
||||
import Dialog from "./command-dialog.svelte";
|
||||
import Empty from "./command-empty.svelte";
|
||||
import Group from "./command-group.svelte";
|
||||
import Item from "./command-item.svelte";
|
||||
import Input from "./command-input.svelte";
|
||||
import List from "./command-list.svelte";
|
||||
import Separator from "./command-separator.svelte";
|
||||
import Shortcut from "./command-shortcut.svelte";
|
||||
import LinkItem from "./command-link-item.svelte";
|
||||
|
||||
const Loading = CommandPrimitive.Loading;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Dialog,
|
||||
Empty,
|
||||
Group,
|
||||
Item,
|
||||
LinkItem,
|
||||
Input,
|
||||
List,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Loading,
|
||||
//
|
||||
Root as Command,
|
||||
Dialog as CommandDialog,
|
||||
Empty as CommandEmpty,
|
||||
Group as CommandGroup,
|
||||
Item as CommandItem,
|
||||
LinkItem as CommandLinkItem,
|
||||
Input as CommandInput,
|
||||
List as CommandList,
|
||||
Separator as CommandSeparator,
|
||||
Shortcut as CommandShortcut,
|
||||
Loading as CommandLoading,
|
||||
};
|
||||
7
src/lib/components/ui/dialog/dialog-close.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
39
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
39
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
17
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
17
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
@@ -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="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal 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<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
17
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||
37
src/lib/components/ui/dialog/index.ts
Normal file
37
src/lib/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
|
||||
const Root = DialogPrimitive.Root;
|
||||
const Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
7
src/lib/components/ui/drawer/drawer-close.svelte
Normal file
7
src/lib/components/ui/drawer/drawer-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Close bind:ref data-slot="drawer-close" {...restProps} />
|
||||
37
src/lib/components/ui/drawer/drawer-content.svelte
Normal file
37
src/lib/components/ui/drawer/drawer-content.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import DrawerOverlay from "./drawer-overlay.svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: DrawerPrimitive.ContentProps & {
|
||||
portalProps?: DrawerPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Portal {...portalProps}>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="drawer-content"
|
||||
class={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block"
|
||||
></div>
|
||||
{@render children?.()}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPrimitive.Portal>
|
||||
17
src/lib/components/ui/drawer/drawer-description.svelte
Normal file
17
src/lib/components/ui/drawer/drawer-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="drawer-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
20
src/lib/components/ui/drawer/drawer-footer.svelte
Normal file
@@ -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="drawer-footer"
|
||||
class={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/drawer/drawer-header.svelte
Normal file
20
src/lib/components/ui/drawer/drawer-header.svelte
Normal 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<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="drawer-header"
|
||||
class={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
12
src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
12
src/lib/components/ui/drawer/drawer-nested.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
20
src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
20
src/lib/components/ui/drawer/drawer-overlay.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="drawer-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/drawer/drawer-title.svelte
Normal file
17
src/lib/components/ui/drawer/drawer-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="drawer-title"
|
||||
class={cn("text-foreground font-semibold", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
src/lib/components/ui/drawer/drawer-trigger.svelte
Normal file
7
src/lib/components/ui/drawer/drawer-trigger.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Trigger bind:ref data-slot="drawer-trigger" {...restProps} />
|
||||
12
src/lib/components/ui/drawer/drawer.svelte
Normal file
12
src/lib/components/ui/drawer/drawer.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
41
src/lib/components/ui/drawer/index.ts
Normal file
41
src/lib/components/ui/drawer/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
import Root from "./drawer.svelte";
|
||||
import Content from "./drawer-content.svelte";
|
||||
import Description from "./drawer-description.svelte";
|
||||
import Overlay from "./drawer-overlay.svelte";
|
||||
import Footer from "./drawer-footer.svelte";
|
||||
import Header from "./drawer-header.svelte";
|
||||
import Title from "./drawer-title.svelte";
|
||||
import NestedRoot from "./drawer-nested.svelte";
|
||||
import Close from "./drawer-close.svelte";
|
||||
import Trigger from "./drawer-trigger.svelte";
|
||||
|
||||
const Portal: typeof DrawerPrimitive.Portal = DrawerPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
NestedRoot,
|
||||
Content,
|
||||
Description,
|
||||
Overlay,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Trigger,
|
||||
Portal,
|
||||
Close,
|
||||
|
||||
//
|
||||
Root as Drawer,
|
||||
NestedRoot as DrawerNestedRoot,
|
||||
Content as DrawerContent,
|
||||
Description as DrawerDescription,
|
||||
Overlay as DrawerOverlay,
|
||||
Footer as DrawerFooter,
|
||||
Header as DrawerHeader,
|
||||
Title as DrawerTitle,
|
||||
Trigger as DrawerTrigger,
|
||||
Portal as DrawerPortal,
|
||||
Close as DrawerClose,
|
||||
};
|
||||
7
src/lib/components/ui/input/index.ts
Normal file
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
51
src/lib/components/ui/input/input.svelte
Normal file
51
src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
17
src/lib/components/ui/popover/index.ts
Normal file
17
src/lib/components/ui/popover/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import Content from "./popover-content.svelte";
|
||||
import Trigger from "./popover-trigger.svelte";
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
};
|
||||
29
src/lib/components/ui/popover/popover-content.svelte
Normal file
29
src/lib/components/ui/popover/popover-content.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: PopoverPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="popover-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 origin-(--bits-popover-content-transform-origin) outline-hidden z-50 w-72 rounded-md border p-4 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
17
src/lib/components/ui/popover/popover-trigger.svelte
Normal file
17
src/lib/components/ui/popover/popover-trigger.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: PopoverPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="popover-trigger"
|
||||
class={cn("", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
37
src/lib/components/ui/select/index.ts
Normal file
37
src/lib/components/ui/select/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
import Group from "./select-group.svelte";
|
||||
import Label from "./select-label.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Content from "./select-content.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import GroupHeading from "./select-group-heading.svelte";
|
||||
|
||||
const Root = SelectPrimitive.Root;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
};
|
||||
40
src/lib/components/ui/select/select-content.svelte
Normal file
40
src/lib/components/ui/select/select-content.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: SelectPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1"
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.GroupHeading>
|
||||
7
src/lib/components/ui/select/select-group.svelte
Normal file
7
src/lib/components/ui/select/select-group.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group data-slot="select-group" {...restProps} />
|
||||
38
src/lib/components/ui/select/select-item.svelte
Normal file
38
src/lib/components/ui/select/select-item.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="size-4" />
|
||||
{/if}
|
||||
</span>
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
20
src/lib/components/ui/select/select-label.svelte
Normal file
20
src/lib/components/ui/select/select-label.svelte
Normal file
@@ -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="select-label"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUpIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
18
src/lib/components/ui/select/select-separator.svelte
Normal file
18
src/lib/components/ui/select/select-separator.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 shadow-xs flex w-fit select-none items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="size-4 opacity-50" />
|
||||
</SelectPrimitive.Trigger>
|
||||
7
src/lib/components/ui/separator/index.ts
Normal file
7
src/lib/components/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
20
src/lib/components/ui/separator/separator.svelte
Normal file
20
src/lib/components/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="separator"
|
||||
class={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
Reference in New Issue
Block a user