init
This commit is contained in:
		
							
								
								
									
										179
									
								
								src/app.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/app.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,179 @@
 | 
			
		||||
@import "tailwindcss";
 | 
			
		||||
 | 
			
		||||
@plugin 'tailwindcss-animate';
 | 
			
		||||
 | 
			
		||||
@custom-variant dark (&:where(.dark, .dark *));
 | 
			
		||||
 | 
			
		||||
@theme {
 | 
			
		||||
  --color-background: hsl(var(--background));
 | 
			
		||||
  --color-foreground: hsl(var(--foreground));
 | 
			
		||||
 | 
			
		||||
  --color-card: hsl(var(--card));
 | 
			
		||||
  --color-card-foreground: hsl(var(--card-foreground));
 | 
			
		||||
 | 
			
		||||
  --color-popover: hsl(var(--popover));
 | 
			
		||||
  --color-popover-foreground: hsl(var(--popover-foreground));
 | 
			
		||||
 | 
			
		||||
  --color-primary: hsl(var(--primary));
 | 
			
		||||
  --color-primary-foreground: hsl(var(--primary-foreground));
 | 
			
		||||
 | 
			
		||||
  --color-secondary: hsl(var(--secondary));
 | 
			
		||||
  --color-secondary-foreground: hsl(var(--secondary-foreground));
 | 
			
		||||
 | 
			
		||||
  --color-muted: hsl(var(--muted));
 | 
			
		||||
  --color-muted-foreground: hsl(var(--muted-foreground));
 | 
			
		||||
 | 
			
		||||
  --color-accent: hsl(var(--accent));
 | 
			
		||||
  --color-accent-foreground: hsl(var(--accent-foreground));
 | 
			
		||||
 | 
			
		||||
  --color-destructive: hsl(var(--destructive));
 | 
			
		||||
  --color-destructive-foreground: hsl(var(--destructive-foreground));
 | 
			
		||||
 | 
			
		||||
  --color-border: hsl(var(--border));
 | 
			
		||||
  --color-input: hsl(var(--input));
 | 
			
		||||
  --color-ring: hsl(var(--ring));
 | 
			
		||||
 | 
			
		||||
  --color-chart-1: hsl(var(--chart-1));
 | 
			
		||||
  --color-chart-2: hsl(var(--chart-2));
 | 
			
		||||
  --color-chart-3: hsl(var(--chart-3));
 | 
			
		||||
  --color-chart-4: hsl(var(--chart-4));
 | 
			
		||||
  --color-chart-5: hsl(var(--chart-5));
 | 
			
		||||
 | 
			
		||||
  --color-sidebar: hsl(var(--sidebar-background));
 | 
			
		||||
  --color-sidebar-foreground: hsl(var(--sidebar-foreground));
 | 
			
		||||
  --color-sidebar-primary: hsl(var(--sidebar-primary));
 | 
			
		||||
  --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
 | 
			
		||||
  --color-sidebar-accent: hsl(var(--sidebar-accent));
 | 
			
		||||
  --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
 | 
			
		||||
  --color-sidebar-border: hsl(var(--sidebar-border));
 | 
			
		||||
  --color-sidebar-ring: hsl(var(--sidebar-ring));
 | 
			
		||||
 | 
			
		||||
  --radius-lg: var(--radius);
 | 
			
		||||
  --radius-md: calc(var(--radius) - 2px);
 | 
			
		||||
  --radius-sm: calc(var(--radius) - 4px);
 | 
			
		||||
 | 
			
		||||
  --animate-accordion-down: accordion-down 0.2s ease-out;
 | 
			
		||||
  --animate-accordion-up: accordion-up 0.2s ease-out;
 | 
			
		||||
 | 
			
		||||
  @keyframes accordion-down {
 | 
			
		||||
    from {
 | 
			
		||||
      height: 0;
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
      height: var(--radix-accordion-content-height);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  @keyframes accordion-up {
 | 
			
		||||
    from {
 | 
			
		||||
      height: var(--radix-accordion-content-height);
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
      height: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
  The default border color has changed to `currentColor` in Tailwind CSS v4,
 | 
			
		||||
  so we've added these compatibility styles to make sure everything still
 | 
			
		||||
  looks the same as it did with Tailwind CSS v3.
 | 
			
		||||
 | 
			
		||||
  If we ever want to remove these styles, we need to add an explicit border
 | 
			
		||||
  color utility to any element that depends on these defaults.
 | 
			
		||||
*/
 | 
			
		||||
@layer base {
 | 
			
		||||
  *,
 | 
			
		||||
  ::after,
 | 
			
		||||
  ::before,
 | 
			
		||||
  ::backdrop,
 | 
			
		||||
  ::file-selector-button {
 | 
			
		||||
    border-color: var(--color-gray-200, currentColor);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer utilities {
 | 
			
		||||
  body {
 | 
			
		||||
    font-family: Arial, Helvetica, sans-serif;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  :root {
 | 
			
		||||
    --background: 0 0% 100%;
 | 
			
		||||
    --foreground: 240 10% 3.9%;
 | 
			
		||||
    --card: 0 0% 100%;
 | 
			
		||||
    --card-foreground: 240 10% 3.9%;
 | 
			
		||||
    --popover: 0 0% 100%;
 | 
			
		||||
    --popover-foreground: 240 10% 3.9%;
 | 
			
		||||
    --primary: 240 5.9% 10%;
 | 
			
		||||
    --primary-foreground: 0 0% 98%;
 | 
			
		||||
    --secondary: 240 4.8% 95.9%;
 | 
			
		||||
    --secondary-foreground: 240 5.9% 10%;
 | 
			
		||||
    --muted: 240 4.8% 95.9%;
 | 
			
		||||
    --muted-foreground: 240 3.8% 46.1%;
 | 
			
		||||
    --accent: 240 4.8% 95.9%;
 | 
			
		||||
    --accent-foreground: 240 5.9% 10%;
 | 
			
		||||
    --destructive: 0 84.2% 60.2%;
 | 
			
		||||
    --destructive-foreground: 0 0% 98%;
 | 
			
		||||
    --border: 240 5.9% 90%;
 | 
			
		||||
    --input: 240 5.9% 90%;
 | 
			
		||||
    --ring: 240 10% 3.9%;
 | 
			
		||||
    --chart-1: 12 76% 61%;
 | 
			
		||||
    --chart-2: 173 58% 39%;
 | 
			
		||||
    --chart-3: 197 37% 24%;
 | 
			
		||||
    --chart-4: 43 74% 66%;
 | 
			
		||||
    --chart-5: 27 87% 67%;
 | 
			
		||||
    --radius: 0.5rem;
 | 
			
		||||
    --sidebar-background: 0 0% 98%;
 | 
			
		||||
    --sidebar-foreground: 240 5.3% 26.1%;
 | 
			
		||||
    --sidebar-primary: 240 5.9% 10%;
 | 
			
		||||
    --sidebar-primary-foreground: 0 0% 98%;
 | 
			
		||||
    --sidebar-accent: 240 4.8% 95.9%;
 | 
			
		||||
    --sidebar-accent-foreground: 240 5.9% 10%;
 | 
			
		||||
    --sidebar-border: 220 13% 91%;
 | 
			
		||||
    --sidebar-ring: 217.2 91.2% 59.8%;
 | 
			
		||||
  }
 | 
			
		||||
  .dark {
 | 
			
		||||
    --background: 240 10% 3.9%;
 | 
			
		||||
    --foreground: 0 0% 98%;
 | 
			
		||||
    --card: 240 10% 3.9%;
 | 
			
		||||
    --card-foreground: 0 0% 98%;
 | 
			
		||||
    --popover: 240 10% 3.9%;
 | 
			
		||||
    --popover-foreground: 0 0% 98%;
 | 
			
		||||
    --primary: 0 0% 98%;
 | 
			
		||||
    --primary-foreground: 240 5.9% 10%;
 | 
			
		||||
    --secondary: 240 3.7% 15.9%;
 | 
			
		||||
    --secondary-foreground: 0 0% 98%;
 | 
			
		||||
    --muted: 240 3.7% 15.9%;
 | 
			
		||||
    --muted-foreground: 240 5% 64.9%;
 | 
			
		||||
    --accent: 240 3.7% 15.9%;
 | 
			
		||||
    --accent-foreground: 0 0% 98%;
 | 
			
		||||
    --destructive: 0 62.8% 30.6%;
 | 
			
		||||
    --destructive-foreground: 0 0% 98%;
 | 
			
		||||
    --border: 240 3.7% 15.9%;
 | 
			
		||||
    --input: 240 3.7% 15.9%;
 | 
			
		||||
    --ring: 240 4.9% 83.9%;
 | 
			
		||||
    --chart-1: 220 70% 50%;
 | 
			
		||||
    --chart-2: 160 60% 45%;
 | 
			
		||||
    --chart-3: 30 80% 55%;
 | 
			
		||||
    --chart-4: 280 65% 60%;
 | 
			
		||||
    --chart-5: 340 75% 55%;
 | 
			
		||||
    --sidebar-background: 240 5.9% 10%;
 | 
			
		||||
    --sidebar-foreground: 240 4.8% 95.9%;
 | 
			
		||||
    --sidebar-primary: 224.3 76.3% 48%;
 | 
			
		||||
    --sidebar-primary-foreground: 0 0% 100%;
 | 
			
		||||
    --sidebar-accent: 240 3.7% 15.9%;
 | 
			
		||||
    --sidebar-accent-foreground: 240 4.8% 95.9%;
 | 
			
		||||
    --sidebar-border: 240 3.7% 15.9%;
 | 
			
		||||
    --sidebar-ring: 217.2 91.2% 59.8%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  * {
 | 
			
		||||
    @apply border-border;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    @apply bg-background text-foreground;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/app.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
// See https://svelte.dev/docs/kit/types#app.d.ts
 | 
			
		||||
// for information about these interfaces
 | 
			
		||||
declare global {
 | 
			
		||||
	namespace App {
 | 
			
		||||
		// interface Error {}
 | 
			
		||||
		// interface Locals {}
 | 
			
		||||
		// interface PageData {}
 | 
			
		||||
		// interface PageState {}
 | 
			
		||||
		// interface Platform {}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {};
 | 
			
		||||
							
								
								
									
										12
									
								
								src/app.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/app.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
	<head>
 | 
			
		||||
		<meta charset="utf-8" />
 | 
			
		||||
		<link rel="icon" href="%sveltekit.assets%/favicon.png" />
 | 
			
		||||
		<meta name="viewport" content="initial-scale=1, viewport-fit=cover" />
 | 
			
		||||
		%sveltekit.head%
 | 
			
		||||
	</head>
 | 
			
		||||
	<body data-sveltekit-preload-data="hover">
 | 
			
		||||
		<div style="display: contents">%sveltekit.body%</div>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										14
									
								
								src/hooks.client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/hooks.client.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
import type { ClientInit } from '@sveltejs/kit';
 | 
			
		||||
import { SafeArea } from '@capacitor-community/safe-area';
 | 
			
		||||
 | 
			
		||||
export const init: ClientInit = async () => {
 | 
			
		||||
    await SafeArea.enable({
 | 
			
		||||
        config: {
 | 
			
		||||
            customColorsForSystemBars: true,
 | 
			
		||||
            statusBarColor: '#00000000', // transparent
 | 
			
		||||
            statusBarContent: 'light',
 | 
			
		||||
            navigationBarColor: '#00000000', // transparent
 | 
			
		||||
            navigationBarContent: 'light',
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										76
									
								
								src/lib/ScanCapture.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/lib/ScanCapture.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import { Haptics, ImpactStyle } from '@capacitor/haptics';
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		focused = $bindable(false),
 | 
			
		||||
		scanning = $bindable(false),
 | 
			
		||||
		onscan
 | 
			
		||||
	}: {
 | 
			
		||||
		focused?: boolean;
 | 
			
		||||
		scanning?: boolean;
 | 
			
		||||
		onscan: (value: string) => void;
 | 
			
		||||
	} = $props();
 | 
			
		||||
 | 
			
		||||
	// Data for manual inputs
 | 
			
		||||
	let inputElement: HTMLInputElement | null = null;
 | 
			
		||||
	let unfocusTimeout: NodeJS.Timeout;
 | 
			
		||||
 | 
			
		||||
	async function onKeydown(event: KeyboardEvent) {
 | 
			
		||||
		// If we this is a manual input
 | 
			
		||||
		if (focused) {
 | 
			
		||||
			// Auto unfocus after 5 seconds of inactivity
 | 
			
		||||
			clearTimeout(unfocusTimeout);
 | 
			
		||||
			unfocusTimeout = setTimeout(() => {
 | 
			
		||||
				inputElement?.blur();
 | 
			
		||||
			}, 10000);
 | 
			
		||||
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Check if this is an indicator that scanning has been initiated
 | 
			
		||||
		if (event.key == 'Unidentified') {
 | 
			
		||||
			scanning = true;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Send the key to onscan
 | 
			
		||||
		if ((event.key == 'Tab' || event.key == 'Enter' || event.key == 'Backspace' || event.key == "Home" || event.key == "Delete")) {
 | 
			
		||||
			// Prevent tab or enter from doing anything
 | 
			
		||||
			event.preventDefault();
 | 
			
		||||
 | 
			
		||||
			// Send key as text
 | 
			
		||||
			onscan(event.key);
 | 
			
		||||
		} else if (event.key.length == 1) {
 | 
			
		||||
			onscan(event.key);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Haptics.impact({ style: ImpactStyle.Light });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function onKeyup(event: KeyboardEvent) {
 | 
			
		||||
		if (event.key == 'Unidentified' && scanning) {
 | 
			
		||||
			scanning = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function focusIn(e: any) {
 | 
			
		||||
		if (e.target?.nodeName == 'INPUT') {
 | 
			
		||||
			focused = true;
 | 
			
		||||
			inputElement = e.target;
 | 
			
		||||
            Haptics.selectionStart();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function focusOut() {
 | 
			
		||||
		focused = false;
 | 
			
		||||
		inputElement = null;
 | 
			
		||||
        Haptics.selectionEnd();
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:window
 | 
			
		||||
	on:keydown={onKeydown}
 | 
			
		||||
	on:keyup={onKeyup}
 | 
			
		||||
	on:focusin={focusIn}
 | 
			
		||||
	on:focusout={focusOut}
 | 
			
		||||
/>
 | 
			
		||||
							
								
								
									
										74
									
								
								src/lib/components/ui/button/button.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/lib/components/ui/button/button.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
<script lang="ts" module>
 | 
			
		||||
	import type { WithElementRef } from "bits-ui";
 | 
			
		||||
	import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
 | 
			
		||||
	import { type VariantProps, tv } from "tailwind-variants";
 | 
			
		||||
 | 
			
		||||
	export const buttonVariants = tv({
 | 
			
		||||
		base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
 | 
			
		||||
		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",
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	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">
 | 
			
		||||
	import { cn } from "$lib/utils.js";
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		class: className,
 | 
			
		||||
		variant = "default",
 | 
			
		||||
		size = "default",
 | 
			
		||||
		ref = $bindable(null),
 | 
			
		||||
		href = undefined,
 | 
			
		||||
		type = "button",
 | 
			
		||||
		children,
 | 
			
		||||
		...restProps
 | 
			
		||||
	}: ButtonProps = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if href}
 | 
			
		||||
	<a
 | 
			
		||||
		bind:this={ref}
 | 
			
		||||
		class={cn(buttonVariants({ variant, size }), className)}
 | 
			
		||||
		{href}
 | 
			
		||||
		{...restProps}
 | 
			
		||||
	>
 | 
			
		||||
		{@render children?.()}
 | 
			
		||||
	</a>
 | 
			
		||||
{:else}
 | 
			
		||||
	<button
 | 
			
		||||
		bind:this={ref}
 | 
			
		||||
		class={cn(buttonVariants({ variant, size }), className)}
 | 
			
		||||
		{type}
 | 
			
		||||
		{...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,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										38
									
								
								src/lib/components/ui/dialog/dialog-content.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/lib/components/ui/dialog/dialog-content.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
 | 
			
		||||
	import X from "lucide-svelte/icons/x";
 | 
			
		||||
	import type { Snippet } from "svelte";
 | 
			
		||||
	import * as Dialog from "./index.js";
 | 
			
		||||
	import { cn } 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
 | 
			
		||||
		class={cn(
 | 
			
		||||
			"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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
 | 
			
		||||
			className
 | 
			
		||||
		)}
 | 
			
		||||
		{...restProps}
 | 
			
		||||
	>
 | 
			
		||||
		{@render children?.()}
 | 
			
		||||
		<DialogPrimitive.Close
 | 
			
		||||
			class="ring-offset-background focus:ring-ring absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
 | 
			
		||||
		>
 | 
			
		||||
			<X class="size-4" />
 | 
			
		||||
			<span class="sr-only">Close</span>
 | 
			
		||||
		</DialogPrimitive.Close>
 | 
			
		||||
	</DialogPrimitive.Content>
 | 
			
		||||
</Dialog.Portal>
 | 
			
		||||
							
								
								
									
										16
									
								
								src/lib/components/ui/dialog/dialog-description.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/lib/components/ui/dialog/dialog-description.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<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
 | 
			
		||||
	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 type { WithElementRef } from "bits-ui";
 | 
			
		||||
	import type { HTMLAttributes } from "svelte/elements";
 | 
			
		||||
	import { cn } from "$lib/utils.js";
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		ref = $bindable(null),
 | 
			
		||||
		class: className,
 | 
			
		||||
		children,
 | 
			
		||||
		...restProps
 | 
			
		||||
	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	bind:this={ref}
 | 
			
		||||
	class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 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 type { WithElementRef } from "bits-ui";
 | 
			
		||||
	import { cn } from "$lib/utils.js";
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		ref = $bindable(null),
 | 
			
		||||
		class: className,
 | 
			
		||||
		children,
 | 
			
		||||
		...restProps
 | 
			
		||||
	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	bind:this={ref}
 | 
			
		||||
	class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
 | 
			
		||||
	{...restProps}
 | 
			
		||||
>
 | 
			
		||||
	{@render children?.()}
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										19
									
								
								src/lib/components/ui/dialog/dialog-overlay.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/lib/components/ui/dialog/dialog-overlay.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
<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
 | 
			
		||||
	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/80",
 | 
			
		||||
		className
 | 
			
		||||
	)}
 | 
			
		||||
	{...restProps}
 | 
			
		||||
/>
 | 
			
		||||
							
								
								
									
										16
									
								
								src/lib/components/ui/dialog/dialog-title.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/lib/components/ui/dialog/dialog-title.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<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
 | 
			
		||||
	class={cn("text-lg font-semibold leading-none tracking-tight", className)}
 | 
			
		||||
	{...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";
 | 
			
		||||
 | 
			
		||||
const Root = DialogPrimitive.Root;
 | 
			
		||||
const Trigger = DialogPrimitive.Trigger;
 | 
			
		||||
const Close = DialogPrimitive.Close;
 | 
			
		||||
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/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,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										22
									
								
								src/lib/components/ui/input/input.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/lib/components/ui/input/input.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import type { HTMLInputAttributes } from "svelte/elements";
 | 
			
		||||
	import type { WithElementRef } from "bits-ui";
 | 
			
		||||
	import { cn } from "$lib/utils.js";
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		ref = $bindable(null),
 | 
			
		||||
		value = $bindable(),
 | 
			
		||||
		class: className,
 | 
			
		||||
		...restProps
 | 
			
		||||
	}: WithElementRef<HTMLInputAttributes> = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<input
 | 
			
		||||
	bind:this={ref}
 | 
			
		||||
	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-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
 | 
			
		||||
		className
 | 
			
		||||
	)}
 | 
			
		||||
	bind:value
 | 
			
		||||
	{...restProps}
 | 
			
		||||
/>
 | 
			
		||||
							
								
								
									
										7
									
								
								src/lib/components/ui/label/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/lib/components/ui/label/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import Root from "./label.svelte";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	Root,
 | 
			
		||||
	//
 | 
			
		||||
	Root as Label,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										19
									
								
								src/lib/components/ui/label/label.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/lib/components/ui/label/label.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { Label as LabelPrimitive } from "bits-ui";
 | 
			
		||||
	import { cn } from "$lib/utils.js";
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		ref = $bindable(null),
 | 
			
		||||
		class: className,
 | 
			
		||||
		...restProps
 | 
			
		||||
	}: LabelPrimitive.RootProps = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<LabelPrimitive.Root
 | 
			
		||||
	bind:ref
 | 
			
		||||
	class={cn(
 | 
			
		||||
		"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
 | 
			
		||||
		className
 | 
			
		||||
	)}
 | 
			
		||||
	{...restProps}
 | 
			
		||||
/>
 | 
			
		||||
							
								
								
									
										1
									
								
								src/lib/components/ui/sonner/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/components/ui/sonner/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export { default as Toaster } from "./sonner.svelte";
 | 
			
		||||
							
								
								
									
										19
									
								
								src/lib/components/ui/sonner/sonner.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/lib/components/ui/sonner/sonner.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
 | 
			
		||||
 | 
			
		||||
	let restProps: SonnerProps = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Sonner
 | 
			
		||||
	theme={"light"}
 | 
			
		||||
	class="toaster group"
 | 
			
		||||
	toastOptions={{
 | 
			
		||||
		classes: {
 | 
			
		||||
			toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
 | 
			
		||||
			description: "group-[.toast]:text-muted-foreground",
 | 
			
		||||
			actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
 | 
			
		||||
			cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
 | 
			
		||||
		},
 | 
			
		||||
	}}
 | 
			
		||||
	{...restProps}
 | 
			
		||||
/>
 | 
			
		||||
							
								
								
									
										17
									
								
								src/lib/cutils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/lib/cutils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import { Filesystem } from "@capacitor/filesystem";
 | 
			
		||||
import type { GetUriOptions, MkdirOptions } from "@capacitor/filesystem";
 | 
			
		||||
 | 
			
		||||
export async function checkFileExists(getUriOptions: GetUriOptions): Promise<boolean> {
 | 
			
		||||
    try {
 | 
			
		||||
        await Filesystem.stat(getUriOptions);
 | 
			
		||||
        return true;
 | 
			
		||||
    } catch (_) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function createDirectory(mkdirOptions: MkdirOptions) {
 | 
			
		||||
    try {
 | 
			
		||||
        await Filesystem.mkdir(mkdirOptions);
 | 
			
		||||
    } catch (_) { }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/lib/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
// place files you want to import through the `$lib` alias in this folder.
 | 
			
		||||
							
								
								
									
										70
									
								
								src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
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,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										30
									
								
								src/routes/+layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/routes/+layout.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import '../app.css';
 | 
			
		||||
	import { Toaster } from '$lib/components/ui/sonner';
 | 
			
		||||
 | 
			
		||||
	let { children } = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Toaster />
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col h-screen">
 | 
			
		||||
	<div id="header"></div>
 | 
			
		||||
 | 
			
		||||
	<div class="overflow-y-auto">
 | 
			
		||||
		{@render children()}
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div id="footer"></div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	#header {
 | 
			
		||||
		height: var(--safe-area-inset-top);
 | 
			
		||||
		background-color: white;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	#footer {
 | 
			
		||||
		height: var(--safe-area-inset-bottom);
 | 
			
		||||
		background-color: white;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										1
									
								
								src/routes/+layout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/routes/+layout.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export const prerender = true;
 | 
			
		||||
							
								
								
									
										316
									
								
								src/routes/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								src/routes/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,316 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import * as Dialog from '$lib/components/ui/dialog';
 | 
			
		||||
	import { Input } from '$lib/components/ui/input';
 | 
			
		||||
	import { Label } from '$lib/components/ui/label';
 | 
			
		||||
	import { Button, buttonVariants } from '$lib/components/ui/button';
 | 
			
		||||
	import ScanCapture from '$lib/ScanCapture.svelte';
 | 
			
		||||
	import { cn } from '$lib/utils';
 | 
			
		||||
	import { toast } from 'svelte-sonner';
 | 
			
		||||
	import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
 | 
			
		||||
	import { checkFileExists, createDirectory } from '$lib/cutils';
 | 
			
		||||
	import { Haptics, ImpactStyle } from '@capacitor/haptics';
 | 
			
		||||
	import { SvelteMap } from 'svelte/reactivity';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	let showdialog = $state(false);
 | 
			
		||||
 | 
			
		||||
	let focused = $state(false);
 | 
			
		||||
	let confirmation = $state(false);
 | 
			
		||||
 | 
			
		||||
	const formValues: SvelteMap<string, string | undefined> = new SvelteMap();
 | 
			
		||||
	let sessioncount = $state(0);
 | 
			
		||||
	let daycount = $state(0);
 | 
			
		||||
 | 
			
		||||
	let selected = $state('service');
 | 
			
		||||
 | 
			
		||||
	function onscan(value: string) {
 | 
			
		||||
		// Set as empty if undefined
 | 
			
		||||
		if (formValues.get(selected) == undefined) {
 | 
			
		||||
			formValues.set(selected, '');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (value == 'Home') {
 | 
			
		||||
			showdialog = !showdialog;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (value == 'Delete') {
 | 
			
		||||
			reset();
 | 
			
		||||
			toast.success('Cleared');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Advance to next input field
 | 
			
		||||
		if (value == 'Tab' || value == 'Enter') {
 | 
			
		||||
			if (selected == 'service') {
 | 
			
		||||
				selected = 'serial';
 | 
			
		||||
			} else if (selected == 'serial') {
 | 
			
		||||
				selected = 'description';
 | 
			
		||||
			} else if (selected == 'description') {
 | 
			
		||||
				selected = 'manufacturer';
 | 
			
		||||
			} else if (selected == 'manufacturer') {
 | 
			
		||||
				selected = 'model';
 | 
			
		||||
			} else if (selected == 'model') {
 | 
			
		||||
				selected = 'condition';
 | 
			
		||||
			} else if (confirmation == false) {
 | 
			
		||||
				confirmation = true;
 | 
			
		||||
			} else if (confirmation == true) {
 | 
			
		||||
				save();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Cancel confirmation
 | 
			
		||||
		if (value == 'Backspace' && confirmation == true) {
 | 
			
		||||
			confirmation = false;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Go back to the previous input field
 | 
			
		||||
		if (value == 'Backspace' && formValues.get(selected)?.length == 0) {
 | 
			
		||||
			if (selected == 'condition') {
 | 
			
		||||
				selected = 'model';
 | 
			
		||||
			} else if (selected == 'model') {
 | 
			
		||||
				selected = 'manufacturer';
 | 
			
		||||
			} else if (selected == 'manufacturer') {
 | 
			
		||||
				selected = 'description';
 | 
			
		||||
			} else if (selected == 'description') {
 | 
			
		||||
				selected = 'serial';
 | 
			
		||||
			} else if (selected == 'serial') {
 | 
			
		||||
				selected = 'service';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Subtract one from the current input
 | 
			
		||||
		if (value == 'Backspace') {
 | 
			
		||||
			formValues.set(selected, formValues.get(selected)?.slice(0, -1));
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Append current input with new text
 | 
			
		||||
		formValues.set(selected, formValues.get(selected) + value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function onsubmit(event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }) {
 | 
			
		||||
		event.preventDefault();
 | 
			
		||||
		Haptics.impact({ style: ImpactStyle.Medium });
 | 
			
		||||
		confirmation = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function save() {
 | 
			
		||||
		Haptics.impact({ style: ImpactStyle.Medium });
 | 
			
		||||
 | 
			
		||||
		// Create directory
 | 
			
		||||
		await createDirectory({
 | 
			
		||||
			path: 'inventory',
 | 
			
		||||
			directory: Directory.Documents
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const filename = `inventory/${new Date().toLocaleDateString().replaceAll('/', '-')}.csv`;
 | 
			
		||||
 | 
			
		||||
		// Check if CSV file exists
 | 
			
		||||
		const exists = await checkFileExists({
 | 
			
		||||
			path: filename,
 | 
			
		||||
			directory: Directory.Documents
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Create file if it does not exist
 | 
			
		||||
		if (!exists) {
 | 
			
		||||
			await Filesystem.writeFile({
 | 
			
		||||
				path: filename,
 | 
			
		||||
				data: '',
 | 
			
		||||
				directory: Directory.Documents,
 | 
			
		||||
				encoding: Encoding.UTF8
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Append data to file
 | 
			
		||||
		await Filesystem.appendFile({
 | 
			
		||||
			path: filename,
 | 
			
		||||
			data: `${formValues.get('service')},${formValues.get('serial')},${formValues.get('manufacturer')},${formValues.get('model')},${formValues.get('description')},${formValues.get('condition')},${new Date().toLocaleDateString()}\n`,
 | 
			
		||||
			directory: Directory.Documents,
 | 
			
		||||
			encoding: Encoding.UTF8
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		sessioncount = sessioncount + 1;
 | 
			
		||||
		daycount = daycount + 1;
 | 
			
		||||
		toast.success('Added item to inventory');
 | 
			
		||||
 | 
			
		||||
		// Reset values
 | 
			
		||||
		reset();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function reset() {
 | 
			
		||||
		confirmation = false;
 | 
			
		||||
		selected = 'service';
 | 
			
		||||
		formValues.set('serial', '');
 | 
			
		||||
		formValues.set('description', '');
 | 
			
		||||
		formValues.set('service', '');
 | 
			
		||||
		formValues.set('manufacturer', '');
 | 
			
		||||
		formValues.set('model', '');
 | 
			
		||||
		formValues.set('condition', '');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		const filename = `inventory/${new Date().toLocaleDateString().replaceAll('/', '-')}.csv`;
 | 
			
		||||
 | 
			
		||||
		// Check if CSV file exists
 | 
			
		||||
		const exists = await checkFileExists({
 | 
			
		||||
			path: filename,
 | 
			
		||||
			directory: Directory.Documents
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (!exists) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Read CSV file
 | 
			
		||||
		const file = await Filesystem.readFile({
 | 
			
		||||
			path: filename,
 | 
			
		||||
			directory: Directory.Documents,
 | 
			
		||||
			encoding: Encoding.UTF8
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Parse CSV file
 | 
			
		||||
		if (typeof file.data !== 'string') {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const lines = file.data.split("\n");
 | 
			
		||||
		daycount = lines.length - 1;
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<ScanCapture {onscan} bind:focused />
 | 
			
		||||
 | 
			
		||||
<Dialog.Root bind:open={showdialog}>
 | 
			
		||||
	<Dialog.Content
 | 
			
		||||
		onEscapeKeydown={(e) => {
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		}}
 | 
			
		||||
	>
 | 
			
		||||
		<Dialog.Header>
 | 
			
		||||
			<Dialog.Title>Stats</Dialog.Title>
 | 
			
		||||
			<div class="grid grid-cols-2 gap-2 py-4">
 | 
			
		||||
				<p>day count:</p>
 | 
			
		||||
				<p>{daycount}</p>
 | 
			
		||||
				<p>session count:</p>
 | 
			
		||||
				<p>{sessioncount}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
			<Button
 | 
			
		||||
				type="button"
 | 
			
		||||
				class={cn(buttonVariants({ variant: 'outline' }), 'text-black cursor-pointer')}
 | 
			
		||||
				onclick={() => {
 | 
			
		||||
					sessioncount = 0;
 | 
			
		||||
					toast.success('Session count reset');
 | 
			
		||||
				}}>Reset session count</Button
 | 
			
		||||
			>
 | 
			
		||||
		</Dialog.Header>
 | 
			
		||||
	</Dialog.Content>
 | 
			
		||||
</Dialog.Root>
 | 
			
		||||
 | 
			
		||||
<form class="flex flex-col gap-4 py-2 px-3 py-4" {onsubmit}>
 | 
			
		||||
	<div class="flex gap-2 items-center">
 | 
			
		||||
		<Label class="basis-1/4" for="service">Asset Tag</Label>
 | 
			
		||||
		<Input
 | 
			
		||||
			id="service"
 | 
			
		||||
			name="service"
 | 
			
		||||
			type="text"
 | 
			
		||||
			disabled={confirmation}
 | 
			
		||||
			class={cn(!focused && selected == 'service' && 'ring-offset-2 ring-2 ring-green-600')}
 | 
			
		||||
			bind:value={() => formValues.get('service'), (v) => formValues.set('service', v)}
 | 
			
		||||
		/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="flex gap-2 items-center">
 | 
			
		||||
		<Label class="basis-1/4" for="serial">Serial #</Label>
 | 
			
		||||
		<Input
 | 
			
		||||
			id="serial"
 | 
			
		||||
			name="serial"
 | 
			
		||||
			type="text"
 | 
			
		||||
			disabled={confirmation}
 | 
			
		||||
			class={cn(!focused && selected == 'serial' && 'ring-offset-2 ring-2 ring-green-600')}
 | 
			
		||||
			bind:value={() => formValues.get('serial'), (v) => formValues.set('serial', v)}
 | 
			
		||||
		/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="flex gap-2 items-center">
 | 
			
		||||
		<Label class="basis-1/4" for="description">Machine Type</Label>
 | 
			
		||||
		<Input
 | 
			
		||||
			id="description"
 | 
			
		||||
			name="description"
 | 
			
		||||
			type="text"
 | 
			
		||||
			disabled={confirmation}
 | 
			
		||||
			class={cn(!focused && selected == 'description' && 'ring-offset-2 ring-2 ring-green-600')}
 | 
			
		||||
			bind:value={() => formValues.get('description'), (v) => formValues.set('description', v)}
 | 
			
		||||
		/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="flex gap-2 items-center">
 | 
			
		||||
		<Label class="basis-1/4" for="manufacturer">Maker</Label>
 | 
			
		||||
		<Input
 | 
			
		||||
			id="manufacturer"
 | 
			
		||||
			name="manufacturer"
 | 
			
		||||
			type="text"
 | 
			
		||||
			disabled={confirmation}
 | 
			
		||||
			class={cn(!focused && selected == 'manufacturer' && 'ring-offset-2 ring-2 ring-green-600')}
 | 
			
		||||
			bind:value={() => formValues.get('manufacturer'), (v) => formValues.set('manufacturer', v)}
 | 
			
		||||
		/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="flex gap-2 items-center">
 | 
			
		||||
		<Label class="basis-1/4" for="model">Model</Label>
 | 
			
		||||
		<Input
 | 
			
		||||
			id="model"
 | 
			
		||||
			name="model"
 | 
			
		||||
			type="text"
 | 
			
		||||
			disabled={confirmation}
 | 
			
		||||
			class={cn(!focused && selected == 'model' && 'ring-offset-2 ring-2 ring-green-600')}
 | 
			
		||||
			bind:value={() => formValues.get('model'), (v) => formValues.set('model', v)}
 | 
			
		||||
		/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="flex gap-2 items-center">
 | 
			
		||||
		<Label class="basis-1/4" for="condition">Condition</Label>
 | 
			
		||||
		<Input
 | 
			
		||||
			id="condition"
 | 
			
		||||
			name="condition"
 | 
			
		||||
			type="text"
 | 
			
		||||
			disabled={confirmation}
 | 
			
		||||
			class={cn(!focused && selected == 'condition' && 'ring-offset-2 ring-2 ring-green-600')}
 | 
			
		||||
			bind:value={() => formValues.get('condition'), (v) => formValues.set('condition', v)}
 | 
			
		||||
		/>
 | 
			
		||||
	</div>
 | 
			
		||||
	{#if confirmation}
 | 
			
		||||
		<div class="flex flex-col gap-2">
 | 
			
		||||
			<Button
 | 
			
		||||
				type="button"
 | 
			
		||||
				class={cn(buttonVariants({ variant: 'outline' }), 'text-black cursor-pointer')}
 | 
			
		||||
				onclick={() => {
 | 
			
		||||
					reset();
 | 
			
		||||
					toast.success('Canceled');
 | 
			
		||||
				}}>Cancel</Button
 | 
			
		||||
			>
 | 
			
		||||
			<Button type="button" class="cursor-pointer" onclick={() => save()}>Confirm</Button>
 | 
			
		||||
		</div>
 | 
			
		||||
	{:else}
 | 
			
		||||
		<div class="flex flex-col gap-2">
 | 
			
		||||
			<div class="flex justify-center gap-1">
 | 
			
		||||
				<Button
 | 
			
		||||
					type="button"
 | 
			
		||||
					class={cn(buttonVariants({ variant: 'outline' }), 'text-black cursor-pointer grow')}
 | 
			
		||||
					onclick={() => {
 | 
			
		||||
						showdialog = !showdialog;
 | 
			
		||||
					}}>Stats</Button
 | 
			
		||||
				>
 | 
			
		||||
				<Button
 | 
			
		||||
					type="button"
 | 
			
		||||
					class={cn(buttonVariants({ variant: 'outline' }), 'text-black cursor-pointer grow')}
 | 
			
		||||
					onclick={() => {
 | 
			
		||||
						reset();
 | 
			
		||||
						toast.success('Cleared');
 | 
			
		||||
					}}>Reset</Button
 | 
			
		||||
				>
 | 
			
		||||
			</div>
 | 
			
		||||
			<Button type="submit" class="cursor-pointer">Submit</Button>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
</form>
 | 
			
		||||
		Reference in New Issue
	
	Block a user