Initial commit

This commit is contained in:
Markus Thielker 2025-07-24 15:08:39 +02:00
commit d9cff2e70c
72 changed files with 2878 additions and 0 deletions

26
packages/client/.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
/node_modules
# Idea
/.idea
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View file

@ -0,0 +1,59 @@
# Stage 1: Builder
# This stage installs all dependencies and builds all packages.
FROM oven/bun:1 AS builder
WORKDIR /app
# Copy dependency manifests
COPY package.json bun.lock ./
COPY tsconfig.base.json ./
# Copy package-specific manifests to leverage Docker cache
COPY packages/shared/package.json packages/shared/tsconfig.json ./packages/shared/
COPY packages/client/package.json packages/client/tsconfig.json ./packages/client/
# Install ALL workspace dependencies
RUN bun install
# Copy source code
COPY packages/shared ./packages/shared/
COPY packages/client ./packages/client/
# Build the client and its dependencies (i.e., 'shared')
RUN bun run --filter=@hnu.de/hl7v2-shared build
RUN bun run --filter=@hnu.de/hl7v2-client build
#---------------------------------------------------------------------
# Stage 2: Production
FROM oven/bun:1-slim
WORKDIR /app
# Create the full directory structure first
RUN mkdir -p packages/shared packages/client
# Copy workspace configuration
COPY --from=builder /app/package.json /app/bun.lock ./
# Set up shared package with its dist directory
COPY --from=builder /app/packages/shared/package.json ./packages/shared/
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
# Set up client package with its dist directory
COPY --from=builder /app/packages/client/package.json ./packages/client/
COPY --from=builder /app/packages/client/build ./packages/client/build
# Install ONLY production dependencies
ENV NODE_ENV=production
RUN bun install \
--frozen-lockfile \
--production
# Set environment variables from config.ts
ENV PUBLIC_SERVER=localhost:8080
EXPOSE 3000
# Run the client from its package directory
WORKDIR /app/packages/client
CMD ["bun", "-r", "dotenv/config", "build/index.js"]

View file

@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}

View file

@ -0,0 +1,41 @@
{
"name": "@hnu.de/hl7v2-client",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"dependencies": {
"@hnu.de/hl7v2-shared": "workspace:*",
"dotenv": "^17.2.0"
},
"devDependencies": {
"@internationalized/date": "^3.8.1",
"@lucide/svelte": "^0.515.0",
"@sveltejs/adapter-node": "^5.2.13",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/ws": "^8.18.1",
"bits-ui": "^2.8.6",
"clsx": "^2.1.1",
"formsnap": "^2.0.1",
"mode-watcher": "^1.1.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"sveltekit-superforms": "^2.26.1",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.5",
"typescript": "^5.0.0",
"vite": "^7.0.4",
"ws": "^8.18.3"
}
}

121
packages/client/src/app.css Normal file
View file

@ -0,0 +1,121 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

13
packages/client/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
import type { WebSocketServer } from 'ws';
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="%sveltekit.assets%/favicon.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,40 @@
<script lang="ts">
import { userPrefersMode, resetMode, setMode } from "mode-watcher"
import { buttonVariants } from "$lib/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "$lib/components/ui/dropdown-menu"
import { MoonIcon, SunIcon, SunMoonIcon } from "@lucide/svelte"
</script>
<DropdownMenu>
<DropdownMenuTrigger class={buttonVariants({ variant: "ghost", size: "icon" })}>
{#if userPrefersMode.current === "light"}
<SunIcon class="size-[1.2rem]"/>
{:else if userPrefersMode.current === "dark"}
<MoonIcon class="size-[1.2rem]"/>
{:else}
<SunMoonIcon class="size-[1.2rem]"/>
{/if}
<span class="sr-only">Toggle theme</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onclick={() => setMode("light")}>
<SunIcon/>
Light
</DropdownMenuItem>
<DropdownMenuItem onclick={() => setMode("dark")}>
<MoonIcon/>
Dark
</DropdownMenuItem>
<DropdownMenuItem onclick={() => resetMode()}>
<SunMoonIcon/>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View 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 { tv, type VariantProps } 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}

View file

@ -0,0 +1,12 @@
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,
};

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
};

View file

@ -0,0 +1,41 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 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({ checked, indeterminate })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<MinusIcon class="size-4" />
{:else}
<CheckIcon class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
class={cn(
"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-dropdown-menu-content-available-height) origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md outline-none",
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
data-slot="dropdown-menu-group-heading"
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: "default" | "destructive";
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_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]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 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({ checked })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<CircleIcon class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>

View 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<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}
</span>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
"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-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,29 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRightIcon class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
</script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />

View file

@ -0,0 +1,49 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
GroupHeading as DropdownMenuGroupHeading,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger,
};

View file

@ -0,0 +1,7 @@
import Root from './input.svelte';
export {
Root,
//
Root as Input,
};

View 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 pt-1.5 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}

View file

@ -0,0 +1,28 @@
import Root from './navigation-menu.svelte';
import Content from './navigation-menu-content.svelte';
import Indicator from './navigation-menu-indicator.svelte';
import Item from './navigation-menu-item.svelte';
import Link from './navigation-menu-link.svelte';
import List from './navigation-menu-list.svelte';
import Trigger from './navigation-menu-trigger.svelte';
import Viewport from './navigation-menu-viewport.svelte';
export {
Root,
Content,
Indicator,
Item,
Link,
List,
Trigger,
Viewport,
//
Root as NavigationMenuRoot,
Content as NavigationMenuContent,
Indicator as NavigationMenuIndicator,
Item as NavigationMenuItem,
Link as NavigationMenuLink,
List as NavigationMenuList,
Trigger as NavigationMenuTrigger,
Viewport as NavigationMenuViewport,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ContentProps = $props();
</script>
<NavigationMenuPrimitive.Content
bind:ref
data-slot="navigation-menu-content"
class={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 left-0 top-0 w-full md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.IndicatorProps = $props();
</script>
<NavigationMenuPrimitive.Indicator
bind:ref
data-slot="navigation-menu-indicator"
class={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...restProps}
>
<div class="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md"></div>
</NavigationMenuPrimitive.Indicator>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ItemProps = $props();
</script>
<NavigationMenuPrimitive.Item
bind:ref
data-slot="navigation-menu-item"
class={cn("relative", className)}
{...restProps}
/>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.LinkProps = $props();
</script>
<NavigationMenuPrimitive.Link
bind:ref
data-slot="navigation-menu-link"
class={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm outline-none transition-all focus-visible:outline-1 focus-visible:ring-[3px] [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ListProps = $props();
</script>
<NavigationMenuPrimitive.List
bind:ref
data-slot="navigation-menu-list"
class={cn("group flex flex-1 list-none items-center justify-center gap-1", className)}
{...restProps}
/>

View file

@ -0,0 +1,34 @@
<script lang="ts" module>
import { cn } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
export const navigationMenuTriggerStyle = tv({
base: 'bg-background hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50',
});
</script>
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from 'bits-ui';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: NavigationMenuPrimitive.TriggerProps = $props();
</script>
<NavigationMenuPrimitive.Trigger
bind:ref
data-slot="navigation-menu-trigger"
class={cn(navigationMenuTriggerStyle(), "group", className)}
{...restProps}
>
{@render children?.()}
<ChevronDownIcon
class="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ViewportProps = $props();
</script>
<div class={cn("absolute left-0 top-full isolate z-50 flex justify-center")}>
<NavigationMenuPrimitive.Viewport
bind:ref
data-slot="navigation-menu-viewport"
class={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--bits-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--bits-navigation-menu-viewport-width)]",
className
)}
{...restProps}
/>
</div>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import NavigationMenuViewport from './navigation-menu-viewport.svelte';
let {
ref = $bindable(null),
class: className,
viewport = true,
children,
...restProps
}: NavigationMenuPrimitive.RootProps & {
viewport?: boolean;
} = $props();
</script>
<NavigationMenuPrimitive.Root
bind:ref
data-slot="navigation-menu"
data-viewport={viewport}
class={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...restProps}
>
{@render children?.()}
{#if viewport}
<NavigationMenuViewport/>
{/if}
</NavigationMenuPrimitive.Root>

View file

@ -0,0 +1,21 @@
import { Tooltip as TooltipPrimitive } from "bits-ui";
import Trigger from "./tooltip-trigger.svelte";
import Content from "./tooltip-content.svelte";
const Root = TooltipPrimitive.Root;
const Provider = TooltipPrimitive.Provider;
const Portal = TooltipPrimitive.Portal;
export {
Root,
Trigger,
Content,
Provider,
Portal,
//
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
Provider as TooltipProvider,
Portal as TooltipPortal,
};

View file

@ -0,0 +1,47 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
sideOffset = 0,
side = "top",
children,
arrowClasses,
...restProps
}: TooltipPrimitive.ContentProps & {
arrowClasses?: string;
} = $props();
</script>
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
bind:ref
data-slot="tooltip-content"
{sideOffset}
{side}
class={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-tooltip-content-transform-origin) z-50 w-fit text-balance rounded-md px-3 py-1.5 text-xs",
className
)}
{...restProps}
>
{@render children?.()}
<TooltipPrimitive.Arrow>
{#snippet child({ props })}
<div
class={cn(
"bg-primary z-50 size-2.5 rotate-45 rounded-[2px]",
"data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]",
"data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]",
"data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2",
"data-[side=left]:-translate-y-[calc(50%_-_3px)]",
arrowClasses
)}
{...props}
></div>
{/snippet}
</TooltipPrimitive.Arrow>
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: TooltipPrimitive.TriggerProps = $props();
</script>
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />

View file

@ -0,0 +1,13 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };

View file

@ -0,0 +1,26 @@
<script lang="ts">
import '../app.css';
import ThemeSelector from '$lib/components/theme-selector.svelte';
import { ModeWatcher } from 'mode-watcher';
let { children } = $props();
</script>
<ModeWatcher/>
<!-- navigation bar -->
<header class="sticky top-0 right-0 left-0 flex justify-center z-50 backdrop-blur py-4 px-4 lg:px-8">
<div class="flex items-center justify-between w-full max-w-7xl">
<a href="/" class="cursor-pointer">
<img class="w-24 hidden dark:block" src="logo_white.svg" alt="HNU Logo"/>
<img class="w-24 block dark:hidden" src="logo_black.svg" alt="HNU Logo"/>
</a>
<div class="flex space-x-2">
<ThemeSelector/>
</div>
</div>
</header>
{@render children()}

View file

@ -0,0 +1,298 @@
<script lang="ts">
import {
CheckIcon,
ChevronRightIcon,
CopyIcon,
Loader2Icon,
MessageSquareIcon,
SendIcon,
UnplugIcon,
UserIcon,
} from '@lucide/svelte';
import { ConnectionState, type Message, MessageType, type ReceiveHl7v2Message } from '@hnu.de/hl7v2-shared';
import { Button } from '$lib/components/ui/button';
import { Card, CardContent, CardHeader } from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/dropdown-menu';
import { env } from '$env/dynamic/public';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '$lib/components/ui/tooltip';
// connection state
let ws = $state<WebSocket | undefined>(undefined);
let userId = $state<string | undefined>(undefined);
let connectionState = $state<ConnectionState>(ConnectionState.disconnected);
// client state
let composedMessage = $state('');
let sentMessages = $state<ReceiveHl7v2Message[]>([]); // storing sent messages with client-timestamp for now
let receivedMessages = $state<ReceiveHl7v2Message[]>([]);
let isSending = $state(false);
let copySuccess = $state(false);
let deliveryError = $state('');
// segment presets
const segmentTemplates = {
MSH: {
name: 'Message Header',
template: () => `MSH|^~\\&|SENDER_APP|SENDER_FACILITY|RECIPIENT_USER_ID|RECEIVER_FACILITY|${new Date().toISOString().replace(/[-:.]/g, '').slice(0, 14)}||ADT^A01|${Date.now()}|P|2.3`,
},
PID: {
name: 'Patient Identification',
template: () => `PID|||PATIENT_MRN||Doe^John^J||19900101|M`,
},
PV1: {
name: 'Patient Visit',
template: () => `PV1||I|ER^101^A|||1234^Welby^Marcus`,
},
AL1: {
name: 'Patient Allergy Information',
template: () => 'AL1|1|DA|12345^Penicillin|SV|Hives',
},
ITM: {
name: 'Material Item',
template: () => 'ITM|1|ITEM-789|Gauze Pads|Sterile Gauze Pads 4x4',
},
IVC: {
name: 'Invoice',
template: () => 'IVC|INV-987|ACCT-654|500.00|20230201',
},
};
const segmentTypes = Object.keys(segmentTemplates) as Array<keyof typeof segmentTemplates>;
function connectToServer() {
console.log('Connecting to server...');
connectionState = ConnectionState.connecting;
const socket = new WebSocket(`wss://${env.PUBLIC_SERVER}`);
socket.onopen = () => {
console.log('WebSocket connection established.');
ws = socket;
};
socket.onmessage = (event) => {
const message = JSON.parse(event.data) as Message;
console.log('Message received from server:', message);
switch (message.type) {
// initial message from server assigning ID
case MessageType.assign_id:
userId = message.payload.userId;
composedMessage = segmentTemplates.MSH.template();
connectionState = ConnectionState.connected;
break;
// message from another client
case MessageType.receive_hl7v2:
receivedMessages = [message, ...receivedMessages];
break;
// message from server due to delivery error
case MessageType.delivery_error:
deliveryError = message.payload.error;
setTimeout(() => deliveryError = '', 5000); // Clear error after 5 seconds
break;
}
};
socket.onclose = () => {
console.log('WebSocket connection closed.');
connectionState = ConnectionState.disconnected;
ws = undefined;
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
connectionState = ConnectionState.disconnected;
ws = undefined;
};
}
// clean up on close
$effect(() => {
return () => {
if (ws && ws.readyState === WebSocket.OPEN) {
connectionState = ConnectionState.disconnected;
ws.close();
}
};
});
function addSegment(type: keyof typeof segmentTemplates) {
const template = segmentTemplates[type].template();
composedMessage += `\r\n${template}`;
}
function handleSendMessage(message: string) {
if (!ws || ws.readyState !== WebSocket.OPEN || isSending || !userId) {
console.log('Socket not ready');
return;
}
isSending = true;
deliveryError = '';
const messageToSend = {
type: 'send_hl7v2',
payload: { message },
} as Message;
ws.send(JSON.stringify(messageToSend));
sentMessages = [{
type: MessageType.receive_hl7v2,
payload: { message, timestamp: new Date().toISOString() },
} as ReceiveHl7v2Message, ...sentMessages];
composedMessage = segmentTemplates.MSH.template();
isSending = false;
}
function copyUserId() {
if (userId) {
navigator.clipboard.writeText(userId).then(() => {
copySuccess = true;
setTimeout(() => copySuccess = false, 2000);
});
}
}
</script>
{#snippet message(msg: ReceiveHl7v2Message)}
<div class="bg-foreground/10 p-3 rounded-md space-y-1">
<p class="text-xs">{new Date(msg.payload.timestamp).toLocaleString()}</p>
<pre class="text-sm whitespace-pre-wrap break-all">{msg.payload.message}</pre>
</div>
{/snippet}
<div class="p-4 lg:p-8">
<div class="max-w-7xl mx-auto">
<main class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left Column: User Info & Logs -->
<div class="space-y-6">
<Card>
<CardHeader class="flex flex-row items-center">
<UserIcon class="mr-2"/>
Your Info
</CardHeader>
<CardContent>
{#if connectionState === ConnectionState.connected && userId}
<Label class="">Station ID</Label>
<div class="flex items-center space-x-2">
<Input disabled bind:value={userId}/>
<Button variant="outline" size="icon" onclick={copyUserId}>
{#if copySuccess}
<CheckIcon class={'text-green-400'}/>
{:else}
<CopyIcon/>
{/if}
</Button>
</div>
{:else if connectionState === ConnectionState.disconnected}
<div class="flex items-center space-x-2">
<UnplugIcon/>
<span>Disconnected</span>
</div>
{:else if connectionState === ConnectionState.connecting}
<div class="flex items-center space-x-2">
<Loader2Icon class="animate-spin"/>
<span>Connecting...</span>
</div>
{/if}
</CardContent>
</Card>
<Card>
<CardHeader class="flex items-center space-x-2">
<ChevronRightIcon class="text-green-400"/>
<span>Received Messages</span>
</CardHeader>
<CardContent class="space-y-2">
{#if receivedMessages.length === 0}
<p class="text-foreground/70 italic">Waiting for incoming messages...</p>
{/if}
{#each receivedMessages as msg (msg.payload.timestamp)}
{@render message(msg)}
{/each}
</CardContent>
</Card>
</div>
<!-- Right Column: Composer and Sent Log -->
<div class="lg:col-span-2 space-y-6">
<Card>
<CardHeader class="flex items-center space-x-2">
<MessageSquareIcon/>
<span>HL7v2 Message Editor</span>
</CardHeader>
<CardContent>
{#if connectionState === ConnectionState.connected && userId}
<div>
<div class="flex flex-wrap gap-2 mb-4">
{#each segmentTypes as type (type)}
{#if type !== 'MSH'}
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Button onclick={() => addSegment(type)}>
+ {type}
</Button>
</TooltipTrigger
>
<TooltipContent>
{segmentTemplates[type].name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/if}
{/each}
</div>
<textarea bind:value={composedMessage}
class="w-full h-64 bg-black font-mono text-sm text-green-400 p-4 rounded-md"></textarea>
<Button onclick={() => handleSendMessage(composedMessage)}
disabled={isSending || !composedMessage} class="w-full space-x-2">
<SendIcon/>
<span>Send Message</span>
</Button>
{#if deliveryError}
<p class="text-red-600 dark:text-red-400 text-center mt-2">{deliveryError}</p>
{/if}
</div>
{:else if connectionState === ConnectionState.disconnected}
<div class="flex flex-col items-center justify-center min-h-96 text-foreground/70 space-y-4">
<h3 class="text-xl font-bold">Connect as a client</h3>
<Button onclick={connectToServer}>Connect</Button>
</div>
{:else if connectionState === ConnectionState.connecting}
<div class="flex flex-col items-center justify-center min-h-96 text-foreground/70">
<Loader2Icon class="animate-spin mb-4" size={48}/>
<h3 class="text-xl font-bold">Connecting...</h3>
</div>
{/if}
</CardContent>
</Card>
<Card>
<CardHeader class="flex items-center space-x-2">
<SendIcon class="text-blue-400"/>
<span>Sent Messages Log</span>
</CardHeader>
<CardContent class="space-y-2">
{#if sentMessages.length === 0}
<p class="text-foreground/70 italic">Your sent messages will appear here.</p>
{/if}
{#each sentMessages as msg (msg.payload.timestamp)}
{@render message(msg)}
{/each}
</CardContent>
</Card>
</div>
</main>
</div>
</div>

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128">
<title>svelte-logo</title>
<path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116"
style="fill:#ff3e00"/>
<path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328"
style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,80 @@
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 188.3 100" style="enable-background:new 0 0 188.3 100;" xml:space="preserve">
<metadata>
<sfw xmlns="ns_sfw;">
<slices>
</slices>
<sliceSourceBounds bottomLeftOrigin="true" height="100" width="188.3" x="-68.6" y="1.5">
</sliceSourceBounds>
</sfw>
</metadata>
<g>
<path d="M7,76.3H1.3v-4.1H0v9.7h1.3v-4.4H7v4.4h1.3v-9.7H7V76.3z M13.1,75c-1.9,0-3.5,1.4-3.5,3.5s1.4,3.5,3.5,3.5s3.5-1.4,3.5-3.5
S15.2,75,13.1,75z M13.1,81c-1.4,0-2.1-1.1-2.1-2.5c0-1.6,0.8-2.5,2.1-2.5c1.4,0,2.1,1.1,2.1,2.5C15.3,80.1,14.6,81,13.1,81z
M21.2,75.9c1.3,0,1.9,0.8,2.1,2.1h1.1c-0.2-1.6-1.3-3-3.2-3c-2.1,0-3.5,1.4-3.5,3.5c0,2.2,1.4,3.5,3.5,3.5c1.9,0,3-1.3,3.2-3
l-1.1-0.2c-0.2,1.1-0.8,2.1-2.1,2.1c-1.3,0-2.1-0.9-2.1-2.5C19,76.9,19.9,75.9,21.2,75.9z M29,75.9c1.1,0,1.6,0.6,1.6,1.9V82h1.3
v-4.1c0-1.6-0.6-2.7-2.5-2.7c-0.9,0-1.9,0.6-2.2,1.4v-4.1h-1.3v9.7h1.3v-3.8C27.1,76.7,27.8,75.9,29,75.9z M36.2,81.2
c-1.1,0-1.9-0.5-2.1-1.6h-1.1c0.2,1.6,1.4,2.4,3,2.4s2.7-0.8,2.7-2.1c0-1.3-0.8-1.7-2.2-1.9l-1.3-0.2c-0.6-0.2-1.1-0.5-1.1-0.9
c0-0.6,0.5-0.9,1.4-0.9c1.1,0,1.7,0.5,1.9,1.6h1.1c-0.2-1.6-1.1-2.4-3-2.4c-1.6,0-2.5,0.8-2.5,2.1c0,1.1,0.8,1.7,2.1,1.9h1.3
c0.8,0.2,1.3,0.3,1.3,0.9C37.7,80.9,37,81.2,36.2,81.2z M43.4,75.9c1.3,0,1.9,0.8,2.1,2.1h1.1c-0.2-1.6-1.3-3-3.2-3
c-2.1,0-3.5,1.4-3.5,3.5c0,2.2,1.4,3.5,3.5,3.5c1.9,0,3-1.3,3.2-3l-1.1-0.2c-0.2,1.1-0.8,2.1-2.1,2.1c-1.3,0-2.1-0.9-2.1-2.5
C41.3,76.9,42.1,75.9,43.4,75.9z M51.1,75.9c1.1,0,1.6,0.6,1.6,1.9V82H54v-4.1c0-1.6-0.6-2.7-2.5-2.7c-0.9,0-1.9,0.6-2.2,1.4v-4.1
h-1.3v9.7h1.3v-3.8C49.4,76.7,50.2,75.9,51.1,75.9z M60.4,79c0,1.3-0.8,2.1-1.7,2.1c-1.1,0-1.6-0.8-1.6-1.9V75h-1.3v4.1
c0,1.6,0.6,2.7,2.4,2.7c0.9,0,1.9-0.6,2.2-1.4v1.1h1.3v-6.6h-1.3C60.4,74.8,60.4,79,60.4,79z M63.6,72.2h1.3v9.7h-1.3V72.2z
M69.8,82.1c1.7,0,2.8-0.9,3.2-2.4l-1.1-0.2c-0.3,1.1-0.9,1.6-2.1,1.6c-1.3,0-2.1-0.9-2.2-2.2h5.4v-0.5c0-1.9-1.1-3.3-3.2-3.3
c-1.9,0-3.3,1.6-3.3,3.6C66.3,80.7,67.7,82.1,69.8,82.1z M69.6,75.9c1.3,0,1.9,0.9,1.9,2.1h-4C67.7,76.9,68.5,75.9,69.6,75.9z
M85.3,81.8v-9.7H84v7.4l-5.2-7.4h-1.4v9.7h1.3v-7.8l5.5,7.8H85.3z M93.5,79.6l-1.1-0.2c-0.3,1.1-0.9,1.6-2.1,1.6
c-1.3,0-2.2-0.9-2.2-2.2h5.4v-0.5c0-1.9-1.1-3.3-3.2-3.3c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5C91.9,82.1,93,81.2,93.5,79.6z
M90.2,75.9c1.3,0,1.9,0.9,1.9,2.1h-4C88.3,76.9,89.1,75.9,90.2,75.9z M94.9,75.2v4.1c0,1.6,0.6,2.7,2.4,2.7c0.9,0,1.9-0.6,2.2-1.4
v1.1h1.3V75h-1.3v3.8c0,1.3-0.8,2.1-1.7,2.1c-1.1,0-1.6-0.8-1.6-1.9v-4.1h-1.3V75.2z M102.7,77.4h3.5v1.1h-3.5V77.4z M115.8,78.2
v-6h-1.3v6c0,1.9-0.9,2.7-2.7,2.7c-1.7,0-2.5-0.9-2.5-2.7v-6h-1.3v6c0,2.8,1.7,4,4,4S115.8,81,115.8,78.2z M117.7,72.2h1.3v9.7
h-1.3V72.2z M128.8,75c-1.1,0-1.9,0.6-2.2,1.4c-0.3-0.9-0.9-1.4-2.2-1.4c-1.1,0-1.7,0.6-2.1,1.4v-1.3H121v6.6h1.3V78
c0-1.3,0.8-2.1,1.7-2.1c1.1,0,1.4,0.6,1.4,1.9V82h1.1v-3.8c0-1.3,0.8-2.1,1.7-2.1c1.1,0,1.4,0.6,1.4,1.9v4.1h1.3V78
C131.2,76.3,130.5,75,128.8,75z M6.5,93.7c0,1.9-0.9,2.7-2.7,2.7s-2.5-0.9-2.5-2.7v-6H0v6c0,2.8,1.7,4,4,4s4-1.1,4-4v-6H6.5V93.7z
M13.1,90.5c-0.9,0-1.9,0.6-2.2,1.4v-1.3H9.7v6.6h1.3v-3.8c0-1.3,0.8-2.1,1.7-2.1c1.1,0,1.6,0.6,1.6,1.9v4.1h1.3v-4.1
C15.7,91.6,14.9,90.5,13.1,90.5z M17.4,90.7h1.3v6.6h-1.3V90.7z M17.4,87.7h1.3v1.4h-1.3V87.7z M23.4,95.9h-0.2l-2.1-5.2h-1.3
l2.7,6.6h1.3l2.7-6.6h-1.1L23.4,95.9z M30.7,90.5c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5c1.7,0,2.8-0.9,3.2-2.4l-1.3-0.3
c-0.3,1.1-0.9,1.6-2.1,1.6c-1.3,0-2.2-0.9-2.2-2.2h5.4v-0.5C33.9,91.9,32.8,90.5,30.7,90.5z M28.5,93.5c0.2-1.1,0.9-2.1,2.1-2.1
c1.3,0,1.9,0.9,1.9,2.1H28.5z M36.6,92.1v-1.4h-1.3v6.6h1.3v-3.5c0-1.3,0.8-2.2,1.7-2.2c0.5,0,0.6,0,1.1,0.3l0.2-1.1
c-0.2-0.2-0.5-0.3-0.9-0.3C37.5,90.5,36.9,91.3,36.6,92.1z M43.7,93.5l-1.3-0.2c-0.6-0.2-1.1-0.5-1.1-0.9c0-0.6,0.5-0.9,1.4-0.9
c1.1,0,1.7,0.5,1.9,1.6h1.1c-0.2-1.6-1.1-2.4-3-2.4c-1.6,0-2.5,0.8-2.5,2.1c0,1.1,0.8,1.7,2.1,1.9l1.1,0.2c0.8,0.2,1.3,0.3,1.3,0.9
c0,0.6-0.6,0.9-1.6,0.9c-1.1,0-1.9-0.5-2.1-1.6h-1.1c0.2,1.6,1.4,2.4,3,2.4c1.6,0,2.7-0.8,2.7-2.1C45.9,94.3,45.1,93.8,43.7,93.5z
M47.3,87.7h1.3v1.4h-1.3V87.7z M47.3,90.7h1.3v6.6h-1.3V90.7z M52.1,95.7v-4.1H54v-0.9h-1.9v-1.9h-0.8l-0.2,0.9
c-0.2,0.6-0.3,0.9-0.9,1.1h-0.5v0.6h0.9v4.3c0,1.3,0.6,1.7,1.6,1.7c0.8,0,1.3-0.2,1.7-0.6V96c-0.3,0.2-0.8,0.5-1.3,0.5
S52.1,96.2,52.1,95.7z M58.1,95.9l-2.2-5.2h-1.3l2.8,6.6l-1.1,2.7h1.3l3.8-9.3h-1.3L58.1,95.9z M68.5,90.5c-1.9,0-3.5,1.4-3.5,3.5
s1.4,3.5,3.5,3.5S72,96,72,94S70.4,90.5,68.5,90.5z M68.5,96.5c-1.4,0-2.1-1.1-2.1-2.5c0-1.6,0.8-2.5,2.1-2.5
c1.4,0,2.1,1.1,2.1,2.5C70.6,95.6,69.8,96.5,68.5,96.5z M75.9,87.5c-1.4,0-2.2,0.9-2.2,2.4v0.8h-1.1v0.9h1.1v5.9H75v-5.9h1.9v-0.9
H75v-0.9c0-0.8,0.3-1.3,1.1-1.3c0.6,0,0.9,0.5,1.1,0.9l0.9-0.3C77.7,88,77.1,87.5,75.9,87.5z M84.2,87.7l-4,9.7h1.3l1.1-2.8H87
l1.1,2.8h1.4l-4-9.7H84.2z M83.1,93.5l1.7-4.3l1.7,4.3C86.6,93.5,83.1,93.5,83.1,93.5z M94.5,90.5c-1.3,0-2.1,0.8-2.4,1.6v-1.4
h-1.3v9.3h1.3v-4c0.3,0.8,1.1,1.6,2.4,1.6c2.1,0,3-1.6,3-3.5C97.6,92.1,96.5,90.5,94.5,90.5z M94.3,96.5c-1.3,0-2.2-0.8-2.2-2.4
v-0.3c0-1.4,0.9-2.4,2.2-2.4c1.3,0,2.1,0.9,2.1,2.5C96.2,95.6,95.6,96.5,94.3,96.5z M102.7,90.5c-1.3,0-2.1,0.8-2.4,1.6v-1.4h-1.3
v9.3h1.3v-4c0.3,0.8,1.1,1.6,2.4,1.6c2.1,0,3-1.6,3-3.5C105.9,92.1,104.7,90.5,102.7,90.5z M102.5,96.5c-1.3,0-2.2-0.8-2.2-2.4
v-0.3c0-1.4,0.9-2.4,2.2-2.4c1.3,0,2.1,0.9,2.1,2.5C104.6,95.6,103.8,96.5,102.5,96.5z M107.3,87.7h1.3v9.7h-1.3V87.7z M110.4,87.7
h1.3v1.4h-1.3V87.7z M110.4,90.7h1.3v6.6h-1.3V90.7z M116.6,90.5c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5c1.7,0,2.8-0.9,3.2-2.4
l-1.1-0.3c-0.3,1.1-0.9,1.6-2.1,1.6c-1.3,0-2.1-0.9-2.2-2.2h5.4v-0.5C119.8,91.9,118.8,90.5,116.6,90.5z M114.6,93.5
c0.2-1.1,0.9-2.1,2.1-2.1c1.3,0,1.9,0.9,1.9,2.1H114.6z M126.4,92.1c-0.3-0.8-1.1-1.6-2.4-1.6c-2.1,0-3,1.6-3,3.5
c0,2.1,1.1,3.5,3,3.5c1.3,0,2.1-0.8,2.4-1.6v1.4h1.3v-9.7h-1.3C126.4,87.7,126.4,92.1,126.4,92.1z M126.4,94.1
c0,1.4-0.9,2.4-2.1,2.4c-1.3,0-2.1-0.9-2.1-2.5s0.8-2.5,2.1-2.5c1.3,0,2.1,0.8,2.1,2.4C126.4,93.8,126.4,94.1,126.4,94.1z
M137,92.1l-1.7-0.3c-1.1-0.2-1.7-0.5-1.7-1.4c0-1.1,0.8-1.7,2.2-1.7c1.6,0,2.5,0.9,2.7,2.5h1.3c0-2.2-1.4-3.5-3.8-3.5
c-2.1,0-3.5,1.1-3.5,2.8c0,1.6,0.9,2.2,2.4,2.5l1.9,0.3c1.3,0.3,1.9,0.6,1.9,1.6c0,1.1-0.9,1.7-2.4,1.7c-1.6,0-2.8-0.9-2.8-2.7
h-1.3c0,2.4,1.7,3.6,4.1,3.6c2.1,0,3.6-1.1,3.6-2.8C139.7,93.2,138.8,92.4,137,92.1z M144.3,91.5c1.3,0,1.9,0.8,2.1,2.1h1.1
c-0.2-1.6-1.3-3-3.2-3c-2.1,0-3.5,1.4-3.5,3.5c0,2.2,1.4,3.5,3.5,3.5c1.9,0,3-1.3,3.2-3l-1.1-0.2c0,1.4-0.8,2.2-2.1,2.2
c-1.3,0-2.1-0.9-2.1-2.5S143,91.5,144.3,91.5z M148.9,87.7h1.3v1.4h-1.3V87.7z M149.1,90.7h1.3v6.6h-1.3V90.7z M155.1,90.5
c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5c1.7,0,2.8-0.9,3.2-2.4l-1.1-0.3c-0.3,1.1-0.9,1.6-2.1,1.6c-1.3,0-2.2-0.9-2.2-2.2h5.4
v-0.5C158.4,91.9,157.3,90.5,155.1,90.5z M153,93.5c0.2-1.1,0.9-2.1,2.1-2.1c1.3,0,1.9,0.9,1.9,2.1H153z M163.3,90.5
c-0.9,0-1.9,0.6-2.2,1.4v-1.3h-1.3v6.6h1.3v-3.8c0-1.3,0.8-2.1,1.7-2.1c1.1,0,1.6,0.6,1.6,1.9v4.1h1.3v-4.1
C165.8,91.6,165,90.5,163.3,90.5z M170.6,91.5c1.3,0,1.9,0.8,2.1,2.1h1.1c-0.2-1.6-1.3-3-3.2-3c-2.1,0-3.5,1.4-3.5,3.5
c0,2.2,1.4,3.5,3.5,3.5c1.9,0,3-1.3,3.2-3l-1.1-0.2c-0.2,1.1-0.8,2.1-2.1,2.1s-2.1-0.9-2.1-2.5C168.5,92.4,169.3,91.5,170.6,91.5z
M178.3,90.5c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5c1.7,0,2.8-0.9,3.2-2.4l-1.3-0.3c-0.3,1.1-0.9,1.6-2.1,1.6
c-1.3,0-2.2-0.9-2.2-2.2h5.4v-0.5C181.5,91.9,180.4,90.5,178.3,90.5z M176.1,93.5c0.2-1.1,0.9-2.1,2.1-2.1c1.3,0,1.9,0.9,1.9,2.1
H176.1z M186.2,93.5l-1.3-0.2c-0.6-0.2-1.1-0.5-1.1-0.9c0-0.6,0.5-0.9,1.4-0.9c1.1,0,1.7,0.5,1.9,1.6h1.1c-0.2-1.6-1.1-2.4-3-2.4
c-1.6,0-2.5,0.8-2.5,2.1c0,1.1,0.8,1.7,2.1,1.9l1.1,0.2c0.8,0.2,1.3,0.3,1.3,0.9c0,0.6-0.6,0.9-1.6,0.9c-1.1,0-1.9-0.5-2.1-1.6
h-1.1c0.2,1.6,1.4,2.4,3,2.4c1.6,0,2.7-0.8,2.7-2.1C188.4,94.3,187.5,93.8,186.2,93.5z M6.2,0h11.7v19.8H6.2V0z M17.7,37.7h10.1
v-4.7H6.2V57H0v3.6h17.7V37.7z M55.2,0H43.7v57h-6.2v3.6h17.7V0z M149.2,44.3c1.7,0,3.3-0.2,4.6-0.6c-2.1-1.9-3-5.1-3-10V0h-11.6
v30.2C139.2,39.4,141.6,44.3,149.2,44.3z">
</path>
<path d="M144,57.8c4.4,2.5,10,4,16.6,4c17.6,0,27.7-9.3,27.7-25.5V0h-11.6v32.9c0,17.7-12,25.2-27.5,25.2
C147.3,58.1,145.6,57.9,144,57.8z M95.7,15.7L84.7,0H73.3l22.5,31.8V15.7z M122.3,60.6V0h-11.7v57h-5.5l2.7,3.6H122.3z M84.8,37.5
L72.3,20.6V57h-4.6v3.6h17.1V37.5z">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

@ -0,0 +1,83 @@
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 188.3 100" style="enable-background:new 0 0 188.3 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<metadata>
<sfw xmlns="ns_sfw;">
<slices>
</slices>
<sliceSourceBounds bottomLeftOrigin="true" height="100" width="188.3" x="-68.6" y="1.5">
</sliceSourceBounds>
</sfw>
</metadata>
<g>
<path class="st0" d="M7,76.3H1.3v-4.1H0v9.7h1.3v-4.4H7v4.4h1.3v-9.7H7V76.3z M13.1,75c-1.9,0-3.5,1.4-3.5,3.5s1.4,3.5,3.5,3.5
s3.5-1.4,3.5-3.5S15.2,75,13.1,75z M13.1,81c-1.4,0-2.1-1.1-2.1-2.5c0-1.6,0.8-2.5,2.1-2.5c1.4,0,2.1,1.1,2.1,2.5
C15.3,80.1,14.6,81,13.1,81z M21.2,75.9c1.3,0,1.9,0.8,2.1,2.1h1.1c-0.2-1.6-1.3-3-3.2-3c-2.1,0-3.5,1.4-3.5,3.5
c0,2.2,1.4,3.5,3.5,3.5c1.9,0,3-1.3,3.2-3l-1.1-0.2c-0.2,1.1-0.8,2.1-2.1,2.1c-1.3,0-2.1-0.9-2.1-2.5C19,76.9,19.9,75.9,21.2,75.9z
M29,75.9c1.1,0,1.6,0.6,1.6,1.9V82h1.3v-4.1c0-1.6-0.6-2.7-2.5-2.7c-0.9,0-1.9,0.6-2.2,1.4v-4.1h-1.3v9.7h1.3v-3.8
C27.1,76.7,27.8,75.9,29,75.9z M36.2,81.2c-1.1,0-1.9-0.5-2.1-1.6h-1.1c0.2,1.6,1.4,2.4,3,2.4s2.7-0.8,2.7-2.1
c0-1.3-0.8-1.7-2.2-1.9l-1.3-0.2c-0.6-0.2-1.1-0.5-1.1-0.9c0-0.6,0.5-0.9,1.4-0.9c1.1,0,1.7,0.5,1.9,1.6h1.1
c-0.2-1.6-1.1-2.4-3-2.4c-1.6,0-2.5,0.8-2.5,2.1c0,1.1,0.8,1.7,2.1,1.9h1.3c0.8,0.2,1.3,0.3,1.3,0.9C37.7,80.9,37,81.2,36.2,81.2z
M43.4,75.9c1.3,0,1.9,0.8,2.1,2.1h1.1c-0.2-1.6-1.3-3-3.2-3c-2.1,0-3.5,1.4-3.5,3.5c0,2.2,1.4,3.5,3.5,3.5c1.9,0,3-1.3,3.2-3
l-1.1-0.2c-0.2,1.1-0.8,2.1-2.1,2.1c-1.3,0-2.1-0.9-2.1-2.5C41.3,76.9,42.1,75.9,43.4,75.9z M51.1,75.9c1.1,0,1.6,0.6,1.6,1.9V82
H54v-4.1c0-1.6-0.6-2.7-2.5-2.7c-0.9,0-1.9,0.6-2.2,1.4v-4.1h-1.3v9.7h1.3v-3.8C49.4,76.7,50.2,75.9,51.1,75.9z M60.4,79
c0,1.3-0.8,2.1-1.7,2.1c-1.1,0-1.6-0.8-1.6-1.9V75h-1.3v4.1c0,1.6,0.6,2.7,2.4,2.7c0.9,0,1.9-0.6,2.2-1.4v1.1h1.3v-6.6h-1.3
C60.4,74.8,60.4,79,60.4,79z M63.6,72.2h1.3v9.7h-1.3V72.2z M69.8,82.1c1.7,0,2.8-0.9,3.2-2.4l-1.1-0.2c-0.3,1.1-0.9,1.6-2.1,1.6
c-1.3,0-2.1-0.9-2.2-2.2h5.4v-0.5c0-1.9-1.1-3.3-3.2-3.3c-1.9,0-3.3,1.6-3.3,3.6C66.3,80.7,67.7,82.1,69.8,82.1z M69.6,75.9
c1.3,0,1.9,0.9,1.9,2.1h-4C67.7,76.9,68.5,75.9,69.6,75.9z M85.3,81.8v-9.7H84v7.4l-5.2-7.4h-1.4v9.7h1.3v-7.8l5.5,7.8H85.3z
M93.5,79.6l-1.1-0.2c-0.3,1.1-0.9,1.6-2.1,1.6c-1.3,0-2.2-0.9-2.2-2.2h5.4v-0.5c0-1.9-1.1-3.3-3.2-3.3c-1.9,0-3.3,1.6-3.3,3.6
s1.4,3.5,3.5,3.5C91.9,82.1,93,81.2,93.5,79.6z M90.2,75.9c1.3,0,1.9,0.9,1.9,2.1h-4C88.3,76.9,89.1,75.9,90.2,75.9z M94.9,75.2
v4.1c0,1.6,0.6,2.7,2.4,2.7c0.9,0,1.9-0.6,2.2-1.4v1.1h1.3V75h-1.3v3.8c0,1.3-0.8,2.1-1.7,2.1c-1.1,0-1.6-0.8-1.6-1.9v-4.1h-1.3
V75.2z M102.7,77.4h3.5v1.1h-3.5V77.4z M115.8,78.2v-6h-1.3v6c0,1.9-0.9,2.7-2.7,2.7c-1.7,0-2.5-0.9-2.5-2.7v-6h-1.3v6
c0,2.8,1.7,4,4,4S115.8,81,115.8,78.2z M117.7,72.2h1.3v9.7h-1.3V72.2z M128.8,75c-1.1,0-1.9,0.6-2.2,1.4c-0.3-0.9-0.9-1.4-2.2-1.4
c-1.1,0-1.7,0.6-2.1,1.4v-1.3H121v6.6h1.3V78c0-1.3,0.8-2.1,1.7-2.1c1.1,0,1.4,0.6,1.4,1.9V82h1.1v-3.8c0-1.3,0.8-2.1,1.7-2.1
c1.1,0,1.4,0.6,1.4,1.9v4.1h1.3V78C131.2,76.3,130.5,75,128.8,75z M6.5,93.7c0,1.9-0.9,2.7-2.7,2.7s-2.5-0.9-2.5-2.7v-6H0v6
c0,2.8,1.7,4,4,4s4-1.1,4-4v-6H6.5V93.7z M13.1,90.5c-0.9,0-1.9,0.6-2.2,1.4v-1.3H9.7v6.6h1.3v-3.8c0-1.3,0.8-2.1,1.7-2.1
c1.1,0,1.6,0.6,1.6,1.9v4.1h1.3v-4.1C15.7,91.6,14.9,90.5,13.1,90.5z M17.4,90.7h1.3v6.6h-1.3V90.7z M17.4,87.7h1.3v1.4h-1.3V87.7z
M23.4,95.9h-0.2l-2.1-5.2h-1.3l2.7,6.6h1.3l2.7-6.6h-1.1L23.4,95.9z M30.7,90.5c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5
c1.7,0,2.8-0.9,3.2-2.4l-1.3-0.3c-0.3,1.1-0.9,1.6-2.1,1.6c-1.3,0-2.2-0.9-2.2-2.2h5.4v-0.5C33.9,91.9,32.8,90.5,30.7,90.5z
M28.5,93.5c0.2-1.1,0.9-2.1,2.1-2.1c1.3,0,1.9,0.9,1.9,2.1H28.5z M36.6,92.1v-1.4h-1.3v6.6h1.3v-3.5c0-1.3,0.8-2.2,1.7-2.2
c0.5,0,0.6,0,1.1,0.3l0.2-1.1c-0.2-0.2-0.5-0.3-0.9-0.3C37.5,90.5,36.9,91.3,36.6,92.1z M43.7,93.5l-1.3-0.2
c-0.6-0.2-1.1-0.5-1.1-0.9c0-0.6,0.5-0.9,1.4-0.9c1.1,0,1.7,0.5,1.9,1.6h1.1c-0.2-1.6-1.1-2.4-3-2.4c-1.6,0-2.5,0.8-2.5,2.1
c0,1.1,0.8,1.7,2.1,1.9l1.1,0.2c0.8,0.2,1.3,0.3,1.3,0.9c0,0.6-0.6,0.9-1.6,0.9c-1.1,0-1.9-0.5-2.1-1.6h-1.1c0.2,1.6,1.4,2.4,3,2.4
c1.6,0,2.7-0.8,2.7-2.1C45.9,94.3,45.1,93.8,43.7,93.5z M47.3,87.7h1.3v1.4h-1.3V87.7z M47.3,90.7h1.3v6.6h-1.3V90.7z M52.1,95.7
v-4.1H54v-0.9h-1.9v-1.9h-0.8l-0.2,0.9c-0.2,0.6-0.3,0.9-0.9,1.1h-0.5v0.6h0.9v4.3c0,1.3,0.6,1.7,1.6,1.7c0.8,0,1.3-0.2,1.7-0.6V96
c-0.3,0.2-0.8,0.5-1.3,0.5S52.1,96.2,52.1,95.7z M58.1,95.9l-2.2-5.2h-1.3l2.8,6.6l-1.1,2.7h1.3l3.8-9.3h-1.3L58.1,95.9z
M68.5,90.5c-1.9,0-3.5,1.4-3.5,3.5s1.4,3.5,3.5,3.5S72,96,72,94S70.4,90.5,68.5,90.5z M68.5,96.5c-1.4,0-2.1-1.1-2.1-2.5
c0-1.6,0.8-2.5,2.1-2.5c1.4,0,2.1,1.1,2.1,2.5C70.6,95.6,69.8,96.5,68.5,96.5z M75.9,87.5c-1.4,0-2.2,0.9-2.2,2.4v0.8h-1.1v0.9h1.1
v5.9H75v-5.9h1.9v-0.9H75v-0.9c0-0.8,0.3-1.3,1.1-1.3c0.6,0,0.9,0.5,1.1,0.9l0.9-0.3C77.7,88,77.1,87.5,75.9,87.5z M84.2,87.7
l-4,9.7h1.3l1.1-2.8H87l1.1,2.8h1.4l-4-9.7H84.2z M83.1,93.5l1.7-4.3l1.7,4.3C86.6,93.5,83.1,93.5,83.1,93.5z M94.5,90.5
c-1.3,0-2.1,0.8-2.4,1.6v-1.4h-1.3v9.3h1.3v-4c0.3,0.8,1.1,1.6,2.4,1.6c2.1,0,3-1.6,3-3.5C97.6,92.1,96.5,90.5,94.5,90.5z
M94.3,96.5c-1.3,0-2.2-0.8-2.2-2.4v-0.3c0-1.4,0.9-2.4,2.2-2.4c1.3,0,2.1,0.9,2.1,2.5C96.2,95.6,95.6,96.5,94.3,96.5z M102.7,90.5
c-1.3,0-2.1,0.8-2.4,1.6v-1.4h-1.3v9.3h1.3v-4c0.3,0.8,1.1,1.6,2.4,1.6c2.1,0,3-1.6,3-3.5C105.9,92.1,104.7,90.5,102.7,90.5z
M102.5,96.5c-1.3,0-2.2-0.8-2.2-2.4v-0.3c0-1.4,0.9-2.4,2.2-2.4c1.3,0,2.1,0.9,2.1,2.5C104.6,95.6,103.8,96.5,102.5,96.5z
M107.3,87.7h1.3v9.7h-1.3V87.7z M110.4,87.7h1.3v1.4h-1.3V87.7z M110.4,90.7h1.3v6.6h-1.3V90.7z M116.6,90.5
c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5c1.7,0,2.8-0.9,3.2-2.4l-1.1-0.3c-0.3,1.1-0.9,1.6-2.1,1.6c-1.3,0-2.1-0.9-2.2-2.2h5.4
v-0.5C119.8,91.9,118.8,90.5,116.6,90.5z M114.6,93.5c0.2-1.1,0.9-2.1,2.1-2.1c1.3,0,1.9,0.9,1.9,2.1H114.6z M126.4,92.1
c-0.3-0.8-1.1-1.6-2.4-1.6c-2.1,0-3,1.6-3,3.5c0,2.1,1.1,3.5,3,3.5c1.3,0,2.1-0.8,2.4-1.6v1.4h1.3v-9.7h-1.3
C126.4,87.7,126.4,92.1,126.4,92.1z M126.4,94.1c0,1.4-0.9,2.4-2.1,2.4c-1.3,0-2.1-0.9-2.1-2.5s0.8-2.5,2.1-2.5
c1.3,0,2.1,0.8,2.1,2.4C126.4,93.8,126.4,94.1,126.4,94.1z M137,92.1l-1.7-0.3c-1.1-0.2-1.7-0.5-1.7-1.4c0-1.1,0.8-1.7,2.2-1.7
c1.6,0,2.5,0.9,2.7,2.5h1.3c0-2.2-1.4-3.5-3.8-3.5c-2.1,0-3.5,1.1-3.5,2.8c0,1.6,0.9,2.2,2.4,2.5l1.9,0.3c1.3,0.3,1.9,0.6,1.9,1.6
c0,1.1-0.9,1.7-2.4,1.7c-1.6,0-2.8-0.9-2.8-2.7h-1.3c0,2.4,1.7,3.6,4.1,3.6c2.1,0,3.6-1.1,3.6-2.8C139.7,93.2,138.8,92.4,137,92.1z
M144.3,91.5c1.3,0,1.9,0.8,2.1,2.1h1.1c-0.2-1.6-1.3-3-3.2-3c-2.1,0-3.5,1.4-3.5,3.5c0,2.2,1.4,3.5,3.5,3.5c1.9,0,3-1.3,3.2-3
l-1.1-0.2c0,1.4-0.8,2.2-2.1,2.2c-1.3,0-2.1-0.9-2.1-2.5S143,91.5,144.3,91.5z M148.9,87.7h1.3v1.4h-1.3V87.7z M149.1,90.7h1.3v6.6
h-1.3V90.7z M155.1,90.5c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5c1.7,0,2.8-0.9,3.2-2.4l-1.1-0.3c-0.3,1.1-0.9,1.6-2.1,1.6
c-1.3,0-2.2-0.9-2.2-2.2h5.4v-0.5C158.4,91.9,157.3,90.5,155.1,90.5z M153,93.5c0.2-1.1,0.9-2.1,2.1-2.1c1.3,0,1.9,0.9,1.9,2.1H153
z M163.3,90.5c-0.9,0-1.9,0.6-2.2,1.4v-1.3h-1.3v6.6h1.3v-3.8c0-1.3,0.8-2.1,1.7-2.1c1.1,0,1.6,0.6,1.6,1.9v4.1h1.3v-4.1
C165.8,91.6,165,90.5,163.3,90.5z M170.6,91.5c1.3,0,1.9,0.8,2.1,2.1h1.1c-0.2-1.6-1.3-3-3.2-3c-2.1,0-3.5,1.4-3.5,3.5
c0,2.2,1.4,3.5,3.5,3.5c1.9,0,3-1.3,3.2-3l-1.1-0.2c-0.2,1.1-0.8,2.1-2.1,2.1s-2.1-0.9-2.1-2.5C168.5,92.4,169.3,91.5,170.6,91.5z
M178.3,90.5c-1.9,0-3.3,1.6-3.3,3.6s1.4,3.5,3.5,3.5c1.7,0,2.8-0.9,3.2-2.4l-1.3-0.3c-0.3,1.1-0.9,1.6-2.1,1.6
c-1.3,0-2.2-0.9-2.2-2.2h5.4v-0.5C181.5,91.9,180.4,90.5,178.3,90.5z M176.1,93.5c0.2-1.1,0.9-2.1,2.1-2.1c1.3,0,1.9,0.9,1.9,2.1
H176.1z M186.2,93.5l-1.3-0.2c-0.6-0.2-1.1-0.5-1.1-0.9c0-0.6,0.5-0.9,1.4-0.9c1.1,0,1.7,0.5,1.9,1.6h1.1c-0.2-1.6-1.1-2.4-3-2.4
c-1.6,0-2.5,0.8-2.5,2.1c0,1.1,0.8,1.7,2.1,1.9l1.1,0.2c0.8,0.2,1.3,0.3,1.3,0.9c0,0.6-0.6,0.9-1.6,0.9c-1.1,0-1.9-0.5-2.1-1.6
h-1.1c0.2,1.6,1.4,2.4,3,2.4c1.6,0,2.7-0.8,2.7-2.1C188.4,94.3,187.5,93.8,186.2,93.5z M6.2,0h11.7v19.8H6.2V0z M17.7,37.7h10.1
v-4.7H6.2V57H0v3.6h17.7V37.7z M55.2,0H43.7v57h-6.2v3.6h17.7V0z M149.2,44.3c1.7,0,3.3-0.2,4.6-0.6c-2.1-1.9-3-5.1-3-10V0h-11.6
v30.2C139.2,39.4,141.6,44.3,149.2,44.3z">
</path>
<path class="st0" d="M144,57.8c4.4,2.5,10,4,16.6,4c17.6,0,27.7-9.3,27.7-25.5V0h-11.6v32.9c0,17.7-12,25.2-27.5,25.2
C147.3,58.1,145.6,57.9,144,57.8z M95.7,15.7L84.7,0H73.3l22.5,31.8V15.7z M122.3,60.6V0h-11.7v57h-5.5l2.7,3.6H122.3z M84.8,37.5
L72.3,20.6V57h-4.6v3.6h17.1V37.5z">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,12 @@
import adapter from '@sveltejs/adapter-node';
import {vitePreprocess} from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
}
};
export default config;

View file

@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler",
},
}

View file

@ -0,0 +1,10 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
sveltekit(),
tailwindcss()
],
});