Compare commits

...

3 Commits

Author SHA1 Message Date
4f5b301e7d Fix a thing 2025-03-09 17:31:09 +01:00
8fedbc34fa tmp 2025-03-09 17:13:31 +01:00
47d4c7926e api endpoints for adding and removing trackers 2025-03-09 16:16:26 +01:00
26 changed files with 797 additions and 15 deletions

View File

@@ -247,6 +247,35 @@ def osc_tracker_updater(address, fixed_args, *args) -> None:
asyncio.ensure_future(update_all_clients(app))
async def handle_add_tracker(request):
# Add tracker with id from request
try:
trackers = request.app["trackers"]
request_data = await request.json()
tracker_id = request_data["id"]
if tracker_id in trackers:
return web.Response(text="Tracker already exists", status=400)
trackers[tracker_id] = TrackerData(tracker_id, *START_POSITION_INTERNAL)
return web.Response(text="OK", status=200)
except Exception as e:
return web.Response(text=f"Error: {e}", status=400)
async def handler_delete_tracker(request):
# Delete tracker with id from request
try:
trackers = request.app["trackers"]
request_data = await request.json()
tracker_id = request_data["id"]
if tracker_id not in trackers:
return web.Response(text="Tracker does not exist", status=400)
del trackers[tracker_id]
return web.Response(text="OK", status=200)
except Exception as e:
return web.Response(text=f"Error: {e}", status=400)
async def receive_osc_data(app):
dispatcher = Dispatcher()
@@ -265,8 +294,13 @@ def create_app():
app.router.add_get("/", handle_root)
app.router.add_get("/ws", handle_websocket)
app.router.add_get("/background_image", handle_background_image)
app.router.add_post("/mode", handle_set_mode)
app.router.add_get("/mode", handlet_get_mode)
app.router.add_post("/tracker", handle_add_tracker)
app.router.add_delete("/tracker", handler_delete_tracker)
app.router.add_static("/", "./static")
# Setup app state

View File

@@ -8,5 +8,10 @@ import tailwind from "@astrojs/tailwind";
// https://astro.build/config
export default defineConfig({
integrations: [svelte(), tailwind()],
integrations: [
svelte(),
tailwind({
applyBaseStyles: false,
}),
],
});

14
frontend/components.json Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.mjs",
"css": "src/styles/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

View File

@@ -19,10 +19,15 @@
"typescript": "^5.8.2"
},
"devDependencies": {
"bits-ui": "0.22.0",
"clsx": "^2.1.1",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11"
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwind-merge": "^3.0.2",
"tailwind-variants": "^1.0.0",
"vaul-svelte": "^0.3.2"
},
"pnpm": {
"onlyBuiltDependencies": [

133
frontend/pnpm-lock.yaml generated
View File

@@ -30,6 +30,12 @@ importers:
specifier: ^5.8.2
version: 5.8.2
devDependencies:
bits-ui:
specifier: 0.22.0
version: 0.22.0(svelte@5.22.6)
clsx:
specifier: ^2.1.1
version: 2.1.1
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -42,6 +48,15 @@ importers:
prettier-plugin-tailwindcss:
specifier: ^0.6.11
version: 0.6.11(prettier-plugin-astro@0.14.1)(prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.22.6))(prettier@3.5.3)
tailwind-merge:
specifier: ^3.0.2
version: 3.0.2
tailwind-variants:
specifier: ^1.0.0
version: 1.0.0(tailwindcss@3.4.17)
vaul-svelte:
specifier: ^0.3.2
version: 0.3.2(svelte@5.22.6)
packages:
@@ -296,6 +311,15 @@ packages:
cpu: [x64]
os: [win32]
'@floating-ui/core@1.6.9':
resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
'@floating-ui/dom@1.6.13':
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -401,6 +425,9 @@ packages:
cpu: [x64]
os: [win32]
'@internationalized/date@3.7.0':
resolution: {integrity: sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -423,6 +450,11 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@melt-ui/svelte@0.76.2':
resolution: {integrity: sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA==}
peerDependencies:
svelte: '>=3 <5'
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -587,6 +619,9 @@ packages:
svelte: ^5.0.0
vite: ^6.0.0
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
@@ -719,6 +754,16 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
bits-ui@0.21.16:
resolution: {integrity: sha512-XFZ7/bK7j/K+5iktxX/ZpmoFHjYjpPzP5EOO/4bWiaFg5TG1iMcfjDhlBTQnJxD6BoVoHuqeZPHZvaTgF4Iv3Q==}
peerDependencies:
svelte: ^4.0.0 || ^5.0.0-next.118
bits-ui@0.22.0:
resolution: {integrity: sha512-r7Fw1HNgA4YxZBRcozl7oP0bheQ8EHh+kfMBZJgyFISix8t4p/nqDcHLmBgIiJ3T5XjYnJRorYDjIWaCfhb5fw==}
peerDependencies:
svelte: ^4.0.0 || ^5.0.0
boxen@8.0.1:
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
engines: {node: '>=18'}
@@ -990,6 +1035,9 @@ packages:
resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
engines: {node: '>=8'}
focus-trap@7.6.4:
resolution: {integrity: sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
@@ -1362,6 +1410,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.1.3:
resolution: {integrity: sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==}
engines: {node: ^18 || >=20}
hasBin: true
neotraverse@0.6.18:
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
engines: {node: '>= 10'}
@@ -1814,6 +1867,18 @@ packages:
resolution: {integrity: sha512-dxHyh3USJyayafSt5I5QD7KuoCM5ZGdIOtLQiKHEro7tymdh0jMcNkiSBVHW+LOA2jEqZEHhyfwN6/pCjx0Fug==}
engines: {node: '>=18'}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tailwind-merge@3.0.2:
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
tailwind-variants@1.0.0:
resolution: {integrity: sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==}
engines: {node: '>=16.x', pnpm: '>=7.x'}
peerDependencies:
tailwindcss: '*'
tailwindcss@3.4.17:
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
engines: {node: '>=14.0.0'}
@@ -1981,6 +2046,11 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vaul-svelte@0.3.2:
resolution: {integrity: sha512-X4OGWttSTVUl417qGDsSFgOvIx24DoiMRY/jaP9z0v9FL8LQQJ0RQ1ZM0QpdyQPRlNd24ewjNQHh5EgYDtfNpw==}
peerDependencies:
svelte: ^4.0.0 || ^5.0.0-next.1
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@@ -2480,6 +2550,17 @@ snapshots:
'@esbuild/win32-x64@0.25.0':
optional: true
'@floating-ui/core@1.6.9':
dependencies:
'@floating-ui/utils': 0.2.9
'@floating-ui/dom@1.6.13':
dependencies:
'@floating-ui/core': 1.6.9
'@floating-ui/utils': 0.2.9
'@floating-ui/utils@0.2.9': {}
'@img/sharp-darwin-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
@@ -2555,6 +2636,10 @@ snapshots:
'@img/sharp-win32-x64@0.33.5':
optional: true
'@internationalized/date@3.7.0':
dependencies:
'@swc/helpers': 0.5.15
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -2581,6 +2666,16 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@melt-ui/svelte@0.76.2(svelte@5.22.6)':
dependencies:
'@floating-ui/core': 1.6.9
'@floating-ui/dom': 1.6.13
'@internationalized/date': 3.7.0
dequal: 2.0.3
focus-trap: 7.6.4
nanoid: 5.1.3
svelte: 5.22.6
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -2724,6 +2819,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
'@types/cookie@0.6.0': {}
'@types/debug@4.1.12':
@@ -2959,6 +3058,20 @@ snapshots:
binary-extensions@2.3.0: {}
bits-ui@0.21.16(svelte@5.22.6):
dependencies:
'@internationalized/date': 3.7.0
'@melt-ui/svelte': 0.76.2(svelte@5.22.6)
nanoid: 5.1.3
svelte: 5.22.6
bits-ui@0.22.0(svelte@5.22.6):
dependencies:
'@internationalized/date': 3.7.0
'@melt-ui/svelte': 0.76.2(svelte@5.22.6)
nanoid: 5.1.3
svelte: 5.22.6
boxen@8.0.1:
dependencies:
ansi-align: 3.0.1
@@ -3217,6 +3330,10 @@ snapshots:
flattie@1.1.1: {}
focus-trap@7.6.4:
dependencies:
tabbable: 6.2.0
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
@@ -3810,6 +3927,8 @@ snapshots:
nanoid@3.3.9: {}
nanoid@5.1.3: {}
neotraverse@0.6.18: {}
nlcst-to-string@4.0.0:
@@ -4297,6 +4416,15 @@ snapshots:
magic-string: 0.30.17
zimmerframe: 1.1.2
tabbable@6.2.0: {}
tailwind-merge@3.0.2: {}
tailwind-variants@1.0.0(tailwindcss@3.4.17):
dependencies:
tailwind-merge: 3.0.2
tailwindcss: 3.4.17
tailwindcss@3.4.17:
dependencies:
'@alloc/quick-lru': 5.2.0
@@ -4442,6 +4570,11 @@ snapshots:
util-deprecate@1.0.2: {}
vaul-svelte@0.3.2(svelte@5.22.6):
dependencies:
bits-ui: 0.21.16(svelte@5.22.6)
svelte: 5.22.6
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3

View File

@@ -3,6 +3,7 @@
import type { TrackerData } from "../utils/types";
import Drag from "./Drag.svelte";
import Slider from "./Slider.svelte";
import Settings from "./Settings.svelte";
let ws: WebSocket | null = $state(null);
let image: HTMLImageElement | null = $state(null);
@@ -65,7 +66,9 @@
<svelte:window onresize={resize} />
<div class="flex h-screen w-screen items-center justify-center">
<div class="mr-auto w-20"></div>
<div class="mr-auto w-20">
<Settings />
</div>
<div class="relative">
<img
src="/background_image?342038402"

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import * as Drawer from "$lib/components/ui/drawer/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import TrackerSetting from "./TrackerSetting.svelte";
const addTracker = async () => {
const response = await fetch("/tracker", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: 4,
}),
});
console.log(response);
};
const deleteTracker = async () => {
const response = await fetch("/tracker", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: 4,
}),
});
console.log(response);
};
</script>
<Drawer.Root>
<Drawer.Trigger asChild let:builder>
<Button builders={[builder]} variant="outline">Settings</Button>
</Drawer.Trigger>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Are you sure absolutely sure?</Drawer.Title>
<Drawer.Description>This action cannot be undone.</Drawer.Description>
</Drawer.Header>
<div class="flex items-center justify-evenly">
<TrackerSetting action={addTracker} text="Add Tracker" />
<TrackerSetting action={deleteTracker} text="Delete Tracker" />
</div>
</Drawer.Content>
</Drawer.Root>

View File

@@ -20,15 +20,19 @@
let arena_state: "full_arena" | "scene_only" = $state("scene_only");
let tracker = trackers.find((ball) => {
ball.id === selected;
});
const onchange = () => {
const x_send = trackers[selected].x / width;
const y_send = trackers[selected].y / height;
const x_send = tracker!.x / width;
const y_send = tracker!.y / height;
ws?.send(
JSON.stringify({
id: trackers[selected].id,
id: tracker!.id,
x: x_send,
y: y_send,
z: trackers[selected].z,
z: tracker!.z,
}),
);
};
@@ -52,7 +56,7 @@
});
};
let z_viz = $derived(trackers[selected].z.toFixed(2));
let z_viz = $derived(tracker?.z.toFixed(2));
</script>
<div class="slider-container ml-auto">
@@ -64,7 +68,6 @@
max="4"
step="0.01"
oninput={onchange}
bind:value={trackers[selected].z}
/>
<output for="z">{z_viz} m</output>
</div>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import Button from "./ui/button/button.svelte";
import Input from "./ui/input/input.svelte";
type Props = {
text: string;
action: () => void;
};
let { action, text }: Props = $props();
</script>
<div>
<h2>{text}</h2>
<Input placeholder={"1"} type="tel" />
<Button onclick={action}>Submit</Button>
</div>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View File

@@ -0,0 +1,49 @@
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import DrawerOverlay from "./drawer-overlay.svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Portal>
<DrawerOverlay />
<DrawerPrimitive.Content
class={cn(
"bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border",
className
)}
{...$$restProps}
>
<div class="bg-muted mx-auto mt-4 h-2 w-[100px] rounded-full"></div>
<slot />
</DrawerPrimitive.Content>
</DrawerPrimitive.Portal>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.DescriptionProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Description
bind:el
class={cn("text-muted-foreground text-sm", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Description>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div bind:this={el} class={cn("mt-auto flex flex-col gap-2 p-4", className)} {...$$restProps}>
<slot />
</div>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
bind:this={el}
class={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...$$restProps}
>
<slot />
</div>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.NestedRoot>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.OverlayProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Overlay
bind:el
class={cn("fixed inset-0 z-50 bg-black/80", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Overlay>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.TitleProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Title
bind:el
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Title>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.Root>

View 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";
const Trigger = DrawerPrimitive.Trigger;
const Portal = DrawerPrimitive.Portal;
const Close = DrawerPrimitive.Close;
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,
};

View File

@@ -0,0 +1,29 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
};

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<input
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View File

@@ -1,5 +1,6 @@
---
import Container from "../components/Container.svelte";
import "$lib/styles/app.css";
---
<html lang="en">

View File

@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

62
frontend/src/utils.ts Normal file
View File

@@ -0,0 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

View File

@@ -1,8 +1,64 @@
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {},
},
plugins: [],
const config = {
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
}
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
},
fontFamily: {
sans: [...fontFamily.sans]
}
}
},
};
export default config;