P0: scaffold Vite + Vue 3 + Tauri v2

- pnpm + TS + Tailwind 3 + Pinia + Vue Router with hash history
- 6 placeholder views (Status, Peers, Routes, Messages, Topics, Settings)
  rendered via lucide-icon sidebar in App.vue
- Tauri v2: shell, store, log, dialog plugins; bundle targets deb +
  appimage; sidecar wired via externalBin = binaries/mycelium
- scripts/fetch-mycelium.sh pins v0.6.1, maps musl asset onto
  gnu target triple expected by Tauri bundler
- CI: pnpm typecheck + cargo fmt/clippy/test
This commit is contained in:
syoul
2026-04-25 22:12:48 +02:00
parent 0a2514ac93
commit d79300caf8
34 changed files with 8538 additions and 3 deletions

61
src/App.vue Normal file
View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { computed } from "vue";
import { RouterLink, RouterView, useRoute } from "vue-router";
import {
Activity,
Users,
Route as RouteIcon,
MessageSquare,
Hash,
Settings as SettingsIcon,
} from "lucide-vue-next";
const route = useRoute();
const navItems = [
{ to: "/status", label: "Status", icon: Activity },
{ to: "/peers", label: "Peers", icon: Users },
{ to: "/routes", label: "Routes", icon: RouteIcon },
{ to: "/messages", label: "Messages", icon: MessageSquare },
{ to: "/topics", label: "Topics", icon: Hash },
{ to: "/settings", label: "Settings", icon: SettingsIcon },
];
const currentTitle = computed(() => (route.meta?.title as string) ?? "Mycellium");
</script>
<template>
<div class="flex h-screen w-screen overflow-hidden bg-background text-foreground">
<aside
class="flex w-56 shrink-0 flex-col border-r border-border bg-card"
>
<div class="flex h-14 items-center px-4 border-b border-border">
<span class="font-semibold text-base">Mycellium</span>
</div>
<nav class="flex-1 overflow-y-auto px-2 py-3 space-y-1">
<RouterLink
v-for="item in navItems"
:key="item.to"
:to="item.to"
class="flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors"
active-class="bg-secondary text-secondary-foreground"
exact-active-class="bg-secondary text-secondary-foreground"
>
<component :is="item.icon" class="h-4 w-4" />
<span>{{ item.label }}</span>
</RouterLink>
</nav>
</aside>
<main class="flex flex-1 flex-col overflow-hidden">
<header
class="flex h-14 items-center border-b border-border px-6 shrink-0"
>
<h1 class="text-lg font-semibold">{{ currentTitle }}</h1>
</header>
<div class="flex-1 overflow-y-auto p-6">
<RouterView />
</div>
</main>
</div>
</template>

7
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<object, object, unknown>;
export default component;
}

23
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,23 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KiB", "MiB", "GiB", "TiB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(i === 0 ? 0 : 2)} ${sizes[i]}`;
}
export function formatRate(bytesPerSec: number): string {
return `${formatBytes(bytesPerSec)}/s`;
}
export function shortenIpv6(addr: string): string {
if (addr.length <= 24) return addr;
return `${addr.slice(0, 10)}${addr.slice(-8)}`;
}

10
src/main.ts Normal file
View File

@@ -0,0 +1,10 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import { router } from "./router";
import "./style.css";
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount("#app");

49
src/router/index.ts Normal file
View File

@@ -0,0 +1,49 @@
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
const routes: RouteRecordRaw[] = [
{
path: "/",
redirect: "/status",
},
{
path: "/status",
name: "status",
component: () => import("@/views/Status.vue"),
meta: { title: "Status" },
},
{
path: "/peers",
name: "peers",
component: () => import("@/views/Peers.vue"),
meta: { title: "Peers" },
},
{
path: "/routes",
name: "routes",
component: () => import("@/views/Routes.vue"),
meta: { title: "Routes" },
},
{
path: "/messages",
name: "messages",
component: () => import("@/views/Messages.vue"),
meta: { title: "Messages" },
},
{
path: "/topics",
name: "topics",
component: () => import("@/views/Topics.vue"),
meta: { title: "Topics" },
},
{
path: "/settings",
name: "settings",
component: () => import("@/views/Settings.vue"),
meta: { title: "Settings" },
},
];
export const router = createRouter({
history: createWebHashHistory(),
routes,
});

57
src/style.css Normal file
View File

@@ -0,0 +1,57 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-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 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial,
sans-serif;
}
}

5
src/views/Messages.vue Normal file
View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<p class="text-sm text-muted-foreground">Messages view wired in P4.</p>
</template>

5
src/views/Peers.vue Normal file
View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<p class="text-sm text-muted-foreground">Peers view wired in P2.</p>
</template>

5
src/views/Routes.vue Normal file
View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<p class="text-sm text-muted-foreground">Routes view wired in P3.</p>
</template>

5
src/views/Settings.vue Normal file
View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<p class="text-sm text-muted-foreground">Settings view wired in P5.</p>
</template>

9
src/views/Status.vue Normal file
View File

@@ -0,0 +1,9 @@
<script setup lang="ts"></script>
<template>
<div class="space-y-4">
<p class="text-sm text-muted-foreground">
Daemon status will appear here once the sidecar is wired up (P1).
</p>
</div>
</template>

5
src/views/Topics.vue Normal file
View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<p class="text-sm text-muted-foreground">Topics view wired in P4.</p>
</template>