diff --git a/assets/logo.svg b/assets/logo.svg
new file mode 100644
index 0000000..166d7b2
--- /dev/null
+++ b/assets/logo.svg
@@ -0,0 +1,24 @@
+
+ PiStation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 0000000..f5dd434
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@pistation/ui",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "svelte": "./src/index.ts",
+ "exports": {
+ ".": {
+ "svelte": "./src/index.ts",
+ "types": "./src/index.ts",
+ "default": "./src/index.ts"
+ }
+ },
+ "dependencies": {
+ "@pistation/client-core": "workspace:*",
+ "@pistation/shared-types": "workspace:*"
+ },
+ "peerDependencies": {
+ "@excalidraw/excalidraw": "^0.17.6",
+ "@iconify/svelte": "^4.0.2",
+ "livekit-client": "^2.7.2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "svelte": "^5.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1"
+ }
+}
diff --git a/packages/ui/src/AnnotationOverlay.svelte b/packages/ui/src/AnnotationOverlay.svelte
new file mode 100644
index 0000000..9cdea4d
--- /dev/null
+++ b/packages/ui/src/AnnotationOverlay.svelte
@@ -0,0 +1,119 @@
+
+
+
diff --git a/packages/ui/src/Logo.svelte b/packages/ui/src/Logo.svelte
new file mode 100644
index 0000000..d4c43d2
--- /dev/null
+++ b/packages/ui/src/Logo.svelte
@@ -0,0 +1,35 @@
+
+
+
+ PiStation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/NightSurface.svelte b/packages/ui/src/NightSurface.svelte
new file mode 100644
index 0000000..8d26a1a
--- /dev/null
+++ b/packages/ui/src/NightSurface.svelte
@@ -0,0 +1,58 @@
+
+
+
+
+ {timeLabel}
+
+
+ {#if layout.nightMode.showPin && pin}
+
+ {formatPin(pin)}
+
+ {/if}
+
diff --git a/packages/ui/src/TrackVideo.svelte b/packages/ui/src/TrackVideo.svelte
new file mode 100644
index 0000000..04e5a18
--- /dev/null
+++ b/packages/ui/src/TrackVideo.svelte
@@ -0,0 +1,26 @@
+
+
+
diff --git a/packages/ui/src/WhiteboardCanvas.svelte b/packages/ui/src/WhiteboardCanvas.svelte
new file mode 100644
index 0000000..0f7bc71
--- /dev/null
+++ b/packages/ui/src/WhiteboardCanvas.svelte
@@ -0,0 +1,124 @@
+
+
+ (isDrawing = true)}
+ onpointerupcapture={() => (isDrawing = false)}
+ onpointercancelcapture={() => (isDrawing = false)}
+>
diff --git a/packages/ui/src/WidgetRenderer.svelte b/packages/ui/src/WidgetRenderer.svelte
new file mode 100644
index 0000000..b557fad
--- /dev/null
+++ b/packages/ui/src/WidgetRenderer.svelte
@@ -0,0 +1,57 @@
+
+
+{#if widget.kind === "clock"}
+
+{:else if widget.kind === "weather"}
+
+{:else if widget.kind === "pin"}
+
+{:else if widget.kind === "text"}
+
+{:else if widget.kind === "image"}
+
+{:else if widget.kind === "agenda"}
+
+{:else if widget.kind === "custom"}
+
+{/if}
diff --git a/packages/ui/src/WidgetSurface.svelte b/packages/ui/src/WidgetSurface.svelte
new file mode 100644
index 0000000..af8d702
--- /dev/null
+++ b/packages/ui/src/WidgetSurface.svelte
@@ -0,0 +1,146 @@
+
+
+
+ {#if hasWallpaper}
+ {#each wallpapers as wallpaper, index (wallpaper)}
+
+ {/each}
+
+ {/if}
+
+
+ {#each visibleWidgets as widget (widget.widgetId)}
+ {@const style = styleOf(widget)}
+ {@const interaction = onSelect
+ ? {
+ role: "button",
+ tabindex: 0,
+ onclick: () => onSelect(widget.widgetId),
+ onkeydown: (event: KeyboardEvent) => {
+ if (event.key === "Enter") onSelect(widget.widgetId);
+ }
+ }
+ : {}}
+
+ {/each}
+
+
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
new file mode 100644
index 0000000..107ca4f
--- /dev/null
+++ b/packages/ui/src/index.ts
@@ -0,0 +1,14 @@
+export { default as AnnotationOverlay } from "./AnnotationOverlay.svelte";
+export { default as Logo } from "./Logo.svelte";
+export { default as NightSurface } from "./NightSurface.svelte";
+export { default as TrackVideo } from "./TrackVideo.svelte";
+export { default as WhiteboardCanvas } from "./WhiteboardCanvas.svelte";
+export { default as WidgetSurface } from "./WidgetSurface.svelte";
+export { default as WidgetRenderer } from "./WidgetRenderer.svelte";
+export { default as ClockWidget } from "./widgets/ClockWidget.svelte";
+export { default as WeatherWidget } from "./widgets/WeatherWidget.svelte";
+export { default as PinWidget } from "./widgets/PinWidget.svelte";
+export { default as TextWidget } from "./widgets/TextWidget.svelte";
+export { default as ImageWidget } from "./widgets/ImageWidget.svelte";
+export { default as AgendaWidget } from "./widgets/AgendaWidget.svelte";
+export { default as CustomWidget } from "./widgets/CustomWidget.svelte";
diff --git a/packages/ui/src/widgets/AgendaWidget.svelte b/packages/ui/src/widgets/AgendaWidget.svelte
new file mode 100644
index 0000000..8510953
--- /dev/null
+++ b/packages/ui/src/widgets/AgendaWidget.svelte
@@ -0,0 +1,31 @@
+
+
+
+ {#if settings.heading}
+
+ {settings.heading}
+
+ {/if}
+
+
+ {#each settings.items as item}
+
+
+ {item}
+
+ {/each}
+
+
diff --git a/packages/ui/src/widgets/ClockWidget.svelte b/packages/ui/src/widgets/ClockWidget.svelte
new file mode 100644
index 0000000..0866619
--- /dev/null
+++ b/packages/ui/src/widgets/ClockWidget.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+ {timeLabel}
+
+ {#if settings.showDate}
+
+ {dateLabel}
+
+ {/if}
+
diff --git a/packages/ui/src/widgets/CustomWidget.svelte b/packages/ui/src/widgets/CustomWidget.svelte
new file mode 100644
index 0000000..17665db
--- /dev/null
+++ b/packages/ui/src/widgets/CustomWidget.svelte
@@ -0,0 +1,92 @@
+
+
+{#if !definition}
+
+ Widget definition missing
+
+{:else}
+
+ {#each definition.blocks as block (block.blockId)}
+ {#if block.kind === "heading"}
+
+ {renderTemplate(block.template, data)}
+
+ {:else if block.kind === "text"}
+
+ {renderTemplate(block.template, data)}
+
+ {:else if block.kind === "metric"}
+
+
+ {renderTemplate(block.labelTemplate, data)}
+
+
+ {renderTemplate(block.valueTemplate, data)}{block.unit}
+
+
+ {:else if block.kind === "list"}
+
+ {#each listItems(block.sourcePath, block.maxItems) as item}
+
+ {renderTemplate(block.itemTemplate, item)}
+
+ {/each}
+
+ {:else if block.kind === "image"}
+
+ {:else if block.kind === "divider"}
+
+ {/if}
+ {/each}
+
+{/if}
diff --git a/packages/ui/src/widgets/ImageWidget.svelte b/packages/ui/src/widgets/ImageWidget.svelte
new file mode 100644
index 0000000..6943589
--- /dev/null
+++ b/packages/ui/src/widgets/ImageWidget.svelte
@@ -0,0 +1,17 @@
+
+
+{#if settings.imageUrl}
+
+{:else}
+ No image set
+{/if}
diff --git a/packages/ui/src/widgets/PinWidget.svelte b/packages/ui/src/widgets/PinWidget.svelte
new file mode 100644
index 0000000..96924ac
--- /dev/null
+++ b/packages/ui/src/widgets/PinWidget.svelte
@@ -0,0 +1,48 @@
+
+
+
+
+ {settings.label}
+
+
+ {#if settings.showJoinUrl}
+
+ {joinUrl}
+
+ {/if}
+
+ {#if pin}
+
+ {formatPin(pin)}
+
+ {:else}
+
+ ------
+
+ {/if}
+
diff --git a/packages/ui/src/widgets/TextWidget.svelte b/packages/ui/src/widgets/TextWidget.svelte
new file mode 100644
index 0000000..9dbabd1
--- /dev/null
+++ b/packages/ui/src/widgets/TextWidget.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {#if settings.heading}
+
+ {settings.heading}
+
+ {/if}
+ {#if settings.body}
+
+ {settings.body}
+
+ {/if}
+
diff --git a/packages/ui/src/widgets/WeatherWidget.svelte b/packages/ui/src/widgets/WeatherWidget.svelte
new file mode 100644
index 0000000..b641506
--- /dev/null
+++ b/packages/ui/src/widgets/WeatherWidget.svelte
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+ {#if reading}
+
+ {reading.temperature}°{unitLabel}
+
+ {:else}
+
+ {hasFailed ? "Weather unavailable" : "Loading"}
+
+ {/if}
+
+ {settings.locationLabel}
+
+
+