feat(spa): topbar logo + 4-section sakai sidebar with primeicons

- manager.html: add favicon/apple-touch-icon/manifest link tags
- AppTopbar: favicon SVG brand logo, version badge, settings cog + theme toggle
- AppMenu: 4 sections (Home, Assets, Library, Get Started) with soon-disabled placeholders
- AppMenuItem: 3 render branches (soon/disabled, external link, RouterLink)
- vite.config.mts: fix renameManagerHtml plugin to handle Vite 5.4 output path
- use import.meta.env.BASE_URL for logo src to avoid Rollup static-asset resolution
This commit is contained in:
DumiFlex
2026-04-26 21:14:58 +03:00
parent f3a10664ab
commit 42f30efbe7
6 changed files with 185 additions and 54 deletions
+3
View File
@@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wildcard Pipeline Manager</title>
<link rel="icon" type="image/svg+xml" href="/wp/images/favicon.svg" />
<link rel="apple-touch-icon" href="/wp/images/apple-touch-icon.png" />
<link rel="manifest" href="/wp/images/site.webmanifest" />
</head>
<body>
<div id="app"></div>
+50 -18
View File
@@ -1,42 +1,74 @@
<script setup lang="ts">
import AppMenuItem from "./AppMenuItem.vue";
interface MenuItem { label: string; icon: string; to: string; }
interface MenuSection { label: string; items: MenuItem[]; }
interface Item {
label: string;
icon: string;
to?: string;
url?: string;
soon?: boolean;
}
interface Section { label: string; items: Item[]; }
const sections: MenuSection[] = [
const sections: Section[] = [
{
label: "Modules",
label: "Home",
items: [{ label: "Dashboard", icon: "pi pi-home", to: "/dashboard" }],
},
{
label: "Assets",
items: [
{ label: "All modules", icon: "pi pi-list", to: "/modules" },
{ label: "+ New wildcard", icon: "pi pi-plus", to: "/modules/wildcard/new" },
{ label: "+ New fixed values", icon: "pi pi-plus", to: "/modules/fixed-values/new" },
{ label: "Wildcards", icon: "pi pi-th-large", to: "/wildcards" },
{ label: "Fixed Values", icon: "pi pi-tag", to: "/fixed-values" },
{ label: "Combines", icon: "pi pi-link", soon: true },
{ label: "Derivations", icon: "pi pi-code", soon: true },
],
},
{
label: "Library",
items: [
{ label: "Categories", icon: "pi pi-tag", to: "/categories" },
{ label: "Categories", icon: "pi pi-bookmark", to: "/categories" },
{ label: "Import / Export", icon: "pi pi-arrow-right-arrow-left", to: "/import-export" },
{ label: "Test runner", icon: "pi pi-bolt", to: "/test" },
{ label: "Test Runner", icon: "pi pi-bolt", to: "/test" },
],
},
{
label: "App",
items: [{ label: "Settings", icon: "pi pi-cog", to: "/settings" }],
label: "Get Started",
items: [
{
label: "Documentation", icon: "pi pi-book",
url: "https://github.com/DumiFlex/ComfyUI-WildcardPipeline/wiki",
},
{
label: "View Source", icon: "pi pi-github",
url: "https://github.com/DumiFlex/ComfyUI-WildcardPipeline",
},
],
},
];
</script>
<template>
<nav>
<div v-for="section in sections" :key="section.label" class="mb-3">
<div class="px-4 py-1 text-[10px] uppercase tracking-wider text-wp-text3 font-semibold">
{{ section.label }}
</div>
<ul class="list-none p-0 m-0">
<AppMenuItem v-for="item in section.items" :key="item.to" :item="item" />
<nav class="app-menu">
<div v-for="section in sections" :key="section.label" class="app-menu__section">
<div class="app-menu__section-label">{{ section.label }}</div>
<ul class="app-menu__list">
<AppMenuItem v-for="item in section.items" :key="item.label" :item="item" />
</ul>
</div>
</nav>
</template>
<style scoped>
.app-menu { padding: 12px 0; }
.app-menu__section { margin-bottom: 12px; }
.app-menu__section-label {
padding: 4px 16px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--wp-text3);
font-weight: 600;
}
.app-menu__list { list-style: none; padding: 0; margin: 0; }
</style>
+60 -7
View File
@@ -2,19 +2,72 @@
import { RouterLink } from "vue-router";
defineProps<{
item: { label: string; icon: string; to: string };
item: {
label: string;
icon: string;
to?: string;
url?: string;
soon?: boolean;
};
}>();
</script>
<template>
<li>
<RouterLink
:to="item.to"
class="flex items-center gap-2 px-4 py-2 text-wp-text hover:bg-wp-bg2 border-l-2 border-transparent"
exact-active-class="!bg-wp-accent/15 !border-l-[color:var(--wp-accent)] !text-white"
<span v-if="item.soon" class="sidebar-item sidebar-item--disabled">
<i :class="['pi', 'pi-fw', item.icon]" aria-hidden="true" />
<span class="sidebar-item__label">{{ item.label }}</span>
<span class="sidebar-item__badge">soon</span>
</span>
<a
v-else-if="item.url"
:href="item.url"
target="_blank" rel="noopener noreferrer"
class="sidebar-item"
>
<i :class="['pi', item.icon, 'text-wp-text2']" aria-hidden="true" />
<span class="text-sm">{{ item.label }}</span>
<i :class="['pi', 'pi-fw', item.icon]" aria-hidden="true" />
<span class="sidebar-item__label">{{ item.label }}</span>
<i class="pi pi-external-link sidebar-item__external" aria-hidden="true" />
</a>
<RouterLink
v-else-if="item.to"
:to="item.to"
exact-active-class="sidebar-item--active"
class="sidebar-item"
>
<i :class="['pi', 'pi-fw', item.icon]" aria-hidden="true" />
<span class="sidebar-item__label">{{ item.label }}</span>
</RouterLink>
</li>
</template>
<style scoped>
.sidebar-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
color: var(--wp-text);
text-decoration: none;
border-left: 2px solid transparent;
font-size: 13px;
}
.sidebar-item:hover { background: var(--wp-bg2); }
.sidebar-item--active {
background: var(--wp-accent-glow);
border-left-color: var(--wp-accent);
color: #fff;
}
.sidebar-item--active :deep(.pi) { color: var(--wp-accent); }
.sidebar-item__label { flex: 1; }
.sidebar-item__badge {
font-size: 9.5px;
padding: 1px 5px;
background: var(--wp-bg3);
color: var(--wp-text3);
border-radius: 3px;
}
.sidebar-item__external { font-size: 10px; color: var(--wp-text3); }
.sidebar-item--disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }
.sidebar-item :deep(.pi) { color: var(--wp-text2); }
</style>
+47 -19
View File
@@ -1,39 +1,67 @@
<script setup lang="ts">
import Button from "primevue/button";
import { useRouter } from "vue-router";
import { useUiStore } from "../stores/uiStore";
const ui = useUiStore();
const router = useRouter();
const version = "1.4.0-dev";
const logoSrc = `${import.meta.env.BASE_URL}images/favicon.svg`;
</script>
<template>
<header class="h-12 px-4 flex items-center justify-between bg-wp-bg border-b border-wp-border sticky top-0 z-10">
<div class="flex items-center gap-3">
<header class="topbar">
<div class="topbar-left">
<Button
icon="pi pi-bars"
text
rounded
severity="secondary"
icon="pi pi-bars" text rounded severity="secondary"
aria-label="Toggle menu"
@click="ui.toggleSidebar"
/>
<div class="flex items-center gap-2 font-semibold">
<div
class="w-5 h-5 rounded"
style="background: var(--wp-brand-gradient)"
aria-hidden="true"
/>
<span>Wildcard Pipeline</span>
</div>
<RouterLink to="/" class="brand">
<img :src="logoSrc" alt="" class="brand-logo" />
<span class="brand-name">Wildcard Pipeline</span>
<span class="brand-version">v{{ version }}</span>
</RouterLink>
</div>
<div class="flex items-center gap-1">
<div class="topbar-right">
<Button
icon="pi pi-moon"
text
rounded
severity="secondary"
icon="pi pi-cog" text rounded severity="secondary"
aria-label="Settings"
@click="router.push('/settings')"
/>
<Button
icon="pi pi-moon" text rounded severity="secondary"
aria-label="Toggle theme"
@click="ui.toggleTheme"
/>
</div>
</header>
</template>
<style scoped>
.topbar {
height: 56px;
background: var(--wp-bg);
border-bottom: 1px solid var(--wp-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
position: sticky;
top: 0;
z-index: 10;
}
.topbar-left, .topbar-right { display: flex; align-items: center; gap: 8px; }
.brand {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: var(--wp-text);
font-weight: 600;
}
.brand:hover { color: #fff; }
.brand-logo { width: 24px; height: 24px; }
.brand-name { letter-spacing: 0.2px; }
.brand-version { font-size: 11px; color: var(--wp-text3); font-weight: 400; }
</style>
+20 -8
View File
@@ -2,7 +2,7 @@ import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
import { resolve } from "node:path";
import { copyFileSync, rmSync } from "node:fs";
import { copyFileSync, existsSync, rmSync } from "node:fs";
// Two build targets controlled by --mode flag:
// pnpm build:extension → js/main.js (ComfyUI web extension, critical-path bundle)
@@ -71,18 +71,30 @@ export default defineConfig(({ mode }) => {
}
// Manager SPA — Vue 3 + PrimeVue + Tailwind, served by aiohttp at /wp/
// The inline plugin renames src/manager.html → index.html so aiohttp's
// The inline plugin renames the emitted HTML → web/index.html so aiohttp's
// SPA fallback (which serves web/index.html) picks it up correctly.
// Post-write plugin: Vite preserves the input path for HTML outputs
// (src/manager.html → web/src/manager.html). After the write, promote it
// to web/index.html so aiohttp's SPA fallback can serve it.
// Vite ≥5.4 emits the HTML directly to web/manager.html (basename only);
// older versions used web/src/manager.html (full relative path). Handle both.
// If Vite already emitted to web/index.html (future-proof), skip the copy.
const renameManagerHtml = {
name: "rename-manager-html",
closeBundle() {
const src = resolve(__dirname, "web/src/manager.html");
const dest = resolve(__dirname, "web/index.html");
copyFileSync(src, dest);
rmSync(resolve(__dirname, "web/src"), { recursive: true, force: true });
const candidates = [
resolve(__dirname, "web/manager.html"),
resolve(__dirname, "web/src/manager.html"),
];
for (const src of candidates) {
if (existsSync(src)) {
copyFileSync(src, dest);
rmSync(resolve(__dirname, src.endsWith("src/manager.html") ? "web/src" : src), {
recursive: true,
force: true,
});
return;
}
}
// Vite already wrote web/index.html directly — nothing to do.
},
};
+5 -2
View File
@@ -4,8 +4,11 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wildcard Pipeline Manager</title>
<script type="module" crossorigin src="/wp/assets/manager-DCWwvajO.js"></script>
<link rel="stylesheet" crossorigin href="/wp/assets/manager-id15NJ7-.css">
<link rel="icon" type="image/svg+xml" href="/wp/images/favicon.svg" />
<link rel="apple-touch-icon" href="/wp/images/apple-touch-icon.png" />
<link rel="manifest" href="/wp/images/site.webmanifest" />
<script type="module" crossorigin src="/wp/assets/manager-Bo0Ofrce.js"></script>
<link rel="stylesheet" crossorigin href="/wp/assets/manager-BYxj3PXg.css">
</head>
<body>
<div id="app"></div>