diff --git a/pkgs/clan-app/process-compose-2d.yaml b/pkgs/clan-app/process-compose-2d.yaml
new file mode 100644
index 000000000..21bf4f224
--- /dev/null
+++ b/pkgs/clan-app/process-compose-2d.yaml
@@ -0,0 +1,39 @@
+version: "0.5"
+
+processes:
+ # App Dev
+
+ clan-app-ui:
+ namespace: "app"
+ command: |
+ cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
+ npm install
+ vite
+ ready_log_line: "VITE"
+
+ clan-app:
+ namespace: "app"
+ command: |
+ cd $(git rev-parse --show-toplevel)/pkgs/clan-app
+ ./bin/clan-app --debug --content-uri http://localhost:3000
+ depends_on:
+ clan-app-ui:
+ condition: "process_log_ready"
+ is_foreground: true
+ ready_log_line: "Debug mode enabled"
+
+ # Storybook Dev
+
+ storybook:
+ namespace: "storybook"
+ command: |
+ cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
+ npm run storybook-dev -- --ci
+ ready_log_line: "started"
+
+ luakit:
+ namespace: "storybook"
+ command: "luakit http://localhost:6006"
+ depends_on:
+ storybook:
+ condition: "process_log_ready"
diff --git a/pkgs/clan-app/ui-2d/.fonts b/pkgs/clan-app/ui-2d/.fonts
new file mode 120000
index 000000000..1622910be
--- /dev/null
+++ b/pkgs/clan-app/ui-2d/.fonts
@@ -0,0 +1 @@
+../ui/.fonts
\ No newline at end of file
diff --git a/pkgs/clan-app/ui-2d/.gitignore b/pkgs/clan-app/ui-2d/.gitignore
new file mode 100644
index 000000000..eed390273
--- /dev/null
+++ b/pkgs/clan-app/ui-2d/.gitignore
@@ -0,0 +1,5 @@
+app/api
+app/.fonts
+
+.vite
+storybook-static
\ No newline at end of file
diff --git a/pkgs/clan-app/ui-2d/.storybook b/pkgs/clan-app/ui-2d/.storybook
new file mode 120000
index 000000000..ab1c8fa2a
--- /dev/null
+++ b/pkgs/clan-app/ui-2d/.storybook
@@ -0,0 +1 @@
+../ui/.storybook
\ No newline at end of file
diff --git a/pkgs/clan-app/ui-2d/.vscode/settings.json b/pkgs/clan-app/ui-2d/.vscode/settings.json
new file mode 100644
index 000000000..f9c1a6bc2
--- /dev/null
+++ b/pkgs/clan-app/ui-2d/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "tailwindCSS.experimental.classRegex": [
+ ["cx\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]
+ ],
+ "editor.wordWrap": "on"
+}
diff --git a/pkgs/clan-app/ui-2d/README.md b/pkgs/clan-app/ui-2d/README.md
new file mode 100644
index 000000000..cd52c6e45
--- /dev/null
+++ b/pkgs/clan-app/ui-2d/README.md
@@ -0,0 +1,43 @@
+## Usage
+
+Those templates dependencies are maintained via [pnpm](https://pnpm.io) via
+`pnpm up -Lri`.
+
+This is the reason you see a `pnpm-lock.yaml`. That being said, any package
+manager will work. This file can be safely be removed once you clone a template.
+
+```bash
+$ npm install # or pnpm install or yarn install
+```
+
+### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm run dev` or `npm start`
+
+Runs the app in the development mode. Open
+[http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.
+
+### `npm run build`
+
+Builds the app for production to the `dist` folder. It correctly bundles
+Solid in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes. Your app is
+ready to be deployed!
+
+### `npm run storybook`
+
+Starts an instance of [storybook](https://storybook.js.org/).
+
+For more info on how to write stories, please [see here](https://storybook.js.org/docs).
+
+## Deployment
+
+You can deploy the `dist` folder to any static host provider (netlify, surge,
+now, etc.)
diff --git a/pkgs/clan-app/ui-2d/api b/pkgs/clan-app/ui-2d/api
new file mode 120000
index 000000000..75dd0c9cd
--- /dev/null
+++ b/pkgs/clan-app/ui-2d/api
@@ -0,0 +1 @@
+../ui/api
\ No newline at end of file
diff --git a/pkgs/clan-app/ui-2d/eslint.config.mjs b/pkgs/clan-app/ui-2d/eslint.config.mjs
new file mode 100644
index 000000000..44b2bae56
--- /dev/null
+++ b/pkgs/clan-app/ui-2d/eslint.config.mjs
@@ -0,0 +1,37 @@
+import eslint from "@eslint/js";
+import tseslint from "typescript-eslint";
+import tailwind from "eslint-plugin-tailwindcss";
+import pluginQuery from "@tanstack/eslint-plugin-query";
+import { globalIgnores } from "eslint/config";
+
+const config = tseslint.config(
+ eslint.configs.recommended,
+ ...pluginQuery.configs["flat/recommended"],
+ ...tseslint.configs.strict,
+ ...tseslint.configs.stylistic,
+ ...tailwind.configs["flat/recommended"],
+ globalIgnores(["src/types/index.d.ts"]),
+ {
+ rules: {
+ "tailwindcss/no-contradicting-classname": [
+ "error",
+ {
+ callees: ["cx"],
+ },
+ ],
+ "tailwindcss/no-custom-classname": [
+ "error",
+ {
+ callees: ["cx"],
+ whitelist: ["material-icons"],
+ },
+ ],
+ // TODO: make this more strict by removing later
+ "no-unused-vars": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ },
+ },
+);
+
+export default config;
diff --git a/pkgs/clan-app/ui-2d/gtk.webview.js b/pkgs/clan-app/ui-2d/gtk.webview.js
new file mode 100644
index 000000000..e75eecc12
--- /dev/null
+++ b/pkgs/clan-app/ui-2d/gtk.webview.js
@@ -0,0 +1,97 @@
+/**
+ * This script generates a custom index.html file for the webview UI.
+ * It reads the manifest.json file generated by Vite and uses it to generate the HTML file.
+ * It also processes the CSS files to rewrite the URLs in the CSS files to match the new location of the assets.
+ * The script is run after the Vite build is complete.
+ *
+ * This is necessary because the webview UI is loaded from the local file system and the URLs in the CSS files need to be rewritten to match the new location of the assets.
+ * The generated index.html file is then used as the entry point for the webview UI.
+ */
+import fs from "node:fs";
+import postcss from "postcss";
+import path from "node:path";
+import css_url from "postcss-url";
+
+const distPath = path.resolve("dist");
+const manifestPath = path.join(distPath, ".vite/manifest.json");
+const outputPath = path.join(distPath, "index.html");
+
+fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
+ if (err) {
+ return console.error("Failed to read manifest:", err);
+ }
+
+ const manifest = JSON.parse(data);
+ /** @type {{ file: string; name: string; src: string; isEntry: bool; css: string[]; } []} */
+ const assets = Object.values(manifest);
+
+ console.log(`Generate custom index.html from ${manifestPath} ...`);
+ // Start with a basic HTML structure
+ let htmlContent = `
+
+