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:
52
.github/workflows/ci.yml
vendored
Normal file
52
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm typecheck
|
||||
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install system deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libsoup-3.0-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libxdo-dev \
|
||||
pkg-config \
|
||||
build-essential
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
- run: cargo fmt --all -- --check
|
||||
working-directory: src-tauri
|
||||
- run: cargo clippy --all-targets --locked -- -D warnings
|
||||
working-directory: src-tauri
|
||||
- run: cargo test --locked
|
||||
working-directory: src-tauri
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -12,9 +12,9 @@ dist-ssr
|
||||
*.local
|
||||
|
||||
# Tauri
|
||||
/app/src-tauri/target
|
||||
/app/src-tauri/binaries/mycelium*
|
||||
/app/src-tauri/gen/schemas
|
||||
/src-tauri/target
|
||||
/src-tauri/binaries/mycelium-*
|
||||
/src-tauri/gen
|
||||
|
||||
# Editor / OS
|
||||
.DS_Store
|
||||
|
||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mycellium UI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
39
package.json
Normal file
39
package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "mycellium-ui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-log": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": "^2.2.0",
|
||||
"@tauri-apps/plugin-store": "^2.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.469.0",
|
||||
"pinia": "^2.3.0",
|
||||
"radix-vue": "^1.9.11",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.2.0",
|
||||
"@types/node": "^22.10.5",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.7",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
1980
pnpm-lock.yaml
generated
Normal file
1980
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
65
scripts/fetch-mycelium.sh
Executable file
65
scripts/fetch-mycelium.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Fetches the official mycelium release binary and places it in src-tauri/binaries/
|
||||
# with the target-triple suffix expected by Tauri's externalBin bundler.
|
||||
#
|
||||
# Usage: scripts/fetch-mycelium.sh [VERSION]
|
||||
# VERSION defaults to MYCELIUM_VERSION below.
|
||||
|
||||
MYCELIUM_VERSION="${1:-${MYCELIUM_VERSION:-v0.6.1}}"
|
||||
REPO="threefoldtech/mycelium"
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DEST_DIR="${ROOT_DIR}/src-tauri/binaries"
|
||||
mkdir -p "${DEST_DIR}"
|
||||
|
||||
detect_target_triple() {
|
||||
local arch os
|
||||
arch="$(uname -m)"
|
||||
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
case "${os}-${arch}" in
|
||||
linux-x86_64) echo "x86_64-unknown-linux-gnu" ;;
|
||||
linux-aarch64) echo "aarch64-unknown-linux-gnu" ;;
|
||||
*) echo "unsupported: ${os}-${arch}" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Map our target triple to the asset name pattern used by upstream releases.
|
||||
asset_for_triple() {
|
||||
case "$1" in
|
||||
x86_64-unknown-linux-gnu) echo "mycelium-x86_64-unknown-linux-musl.tar.gz" ;;
|
||||
aarch64-unknown-linux-gnu) echo "mycelium-aarch64-unknown-linux-musl.tar.gz" ;;
|
||||
*) echo "unsupported triple: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
TRIPLE="$(detect_target_triple)"
|
||||
ASSET="$(asset_for_triple "${TRIPLE}")"
|
||||
URL="https://github.com/${REPO}/releases/download/${MYCELIUM_VERSION}/${ASSET}"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
|
||||
echo "→ downloading ${URL}"
|
||||
curl -fsSL "${URL}" -o "${TMP_DIR}/mycelium.tar.gz"
|
||||
|
||||
echo "→ extracting"
|
||||
tar -xzf "${TMP_DIR}/mycelium.tar.gz" -C "${TMP_DIR}"
|
||||
|
||||
# The archive contains a single 'mycelium' binary at the root.
|
||||
SRC="${TMP_DIR}/mycelium"
|
||||
if [[ ! -f "${SRC}" ]]; then
|
||||
# Some releases nest the binary; find it.
|
||||
SRC="$(find "${TMP_DIR}" -name 'mycelium' -type f -executable | head -n1)"
|
||||
fi
|
||||
if [[ -z "${SRC}" || ! -f "${SRC}" ]]; then
|
||||
echo "could not locate mycelium binary in archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEST="${DEST_DIR}/mycelium-${TRIPLE}"
|
||||
install -m 0755 "${SRC}" "${DEST}"
|
||||
echo "✓ installed ${DEST}"
|
||||
echo " version: ${MYCELIUM_VERSION}"
|
||||
echo " size: $(stat -c%s "${DEST}") bytes"
|
||||
5900
src-tauri/Cargo.lock
generated
Normal file
5900
src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
src-tauri/Cargo.toml
Normal file
33
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "mycellium-ui"
|
||||
version = "0.1.0"
|
||||
description = "Mycelium overlay network desktop client"
|
||||
authors = ["syoul"]
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
|
||||
[lib]
|
||||
name = "mycellium_ui_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-store = "2"
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
thiserror = "2"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
portpicker = "0.1"
|
||||
parking_lot = "0.12"
|
||||
|
||||
[features]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
3
src-tauri/build.rs
Normal file
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
12
src-tauri/capabilities/default.json
Normal file
12
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capabilities for the main window. The mycelium sidecar is spawned from Rust setup and does not require JS-side shell permissions.",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"store:default",
|
||||
"log:default",
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
BIN
src-tauri/icons/128x128@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
BIN
src-tauri/icons/32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/icon.png
Normal file
BIN
src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
21
src-tauri/src/lib.rs
Normal file
21
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
pub fn run() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
||||
)
|
||||
.with_target(false)
|
||||
.compact()
|
||||
.try_init()
|
||||
.ok();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_log::Builder::new().build())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.invoke_handler(tauri::generate_handler![])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
5
src-tauri/src/main.rs
Normal file
5
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
mycellium_ui_lib::run();
|
||||
}
|
||||
47
src-tauri/tauri.conf.json
Normal file
47
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Mycellium UI",
|
||||
"version": "0.1.0",
|
||||
"identifier": "tech.threefold.mycellium-ui",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Mycellium",
|
||||
"width": 1100,
|
||||
"height": 720,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600,
|
||||
"resizable": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "appimage"],
|
||||
"category": "Network",
|
||||
"shortDescription": "Mycelium overlay network client",
|
||||
"longDescription": "Desktop GUI for the Mycelium end-to-end encrypted IPv6 overlay network.",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png"
|
||||
],
|
||||
"externalBin": ["binaries/mycelium"],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["policykit-1"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/App.vue
Normal file
61
src/App.vue
Normal 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
7
src/env.d.ts
vendored
Normal 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
23
src/lib/utils.ts
Normal 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
10
src/main.ts
Normal 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
49
src/router/index.ts
Normal 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
57
src/style.css
Normal 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
5
src/views/Messages.vue
Normal 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
5
src/views/Peers.vue
Normal 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
5
src/views/Routes.vue
Normal 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
5
src/views/Settings.vue
Normal 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
9
src/views/Status.vue
Normal 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
5
src/views/Topics.vue
Normal 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>
|
||||
50
tailwind.config.ts
Normal file
50
tailwind.config.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
darkMode: "class",
|
||||
content: ["./index.html", "./src/**/*.{vue,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ["ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "monospace"],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
30
vite.config.ts
Normal file
30
vite.config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import path from "node:path";
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user