fix: Alpine double-load, FOUC, purple accent, logo/favicon

- Remove alpinejs devDep; Livewire v3 owns Alpine lifecycle
- Register sidebar + theme stores via alpine:init event
- @persist sidebar across wire:navigate to prevent re-render
- Inline critical CSS for FOUC prevention on initial load
- Purple #7c3aed accent + pink #ec4899 gradient in both themes
- Light-mode border visibility fix (solid color, not opacity)
- bg-accent-gradient / text-accent-gradient / border-accent-gradient utilities
- Logo mark SVG + full wordmark variants (dark/light/neutral)
- favicon.svg, favicon PNGs, apple-touch-icon, site.webmanifest
- Sidebar uses logo-mark.svg + text-accent-gradient wordmark

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 12:26:14 +02:00
parent d0365fd34f
commit 00673f3119
12 changed files with 142 additions and 61 deletions

28
package-lock.json generated
View File

@ -6,7 +6,6 @@
"": {
"devDependencies": {
"@tailwindcss/vite": "^4.3.0",
"alpinejs": "^3.15.12",
"axios": "^1.11.0",
"chart.js": "^4.5.1",
"concurrently": "^9.0.1",
@ -1154,23 +1153,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@vue/reactivity": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/shared": "3.1.5"
}
},
"node_modules/@vue/shared": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
"dev": true,
"license": "MIT"
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -1184,16 +1166,6 @@
"node": ">= 6.0.0"
}
},
"node_modules/alpinejs": {
"version": "3.15.12",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz",
"integrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",

View File

@ -11,7 +11,6 @@
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.0",
"alpinejs": "^3.15.12",
"axios": "^1.11.0",
"chart.js": "^4.5.1",
"concurrently": "^9.0.1",

View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 40" fill="none">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
</defs>
<rect x="0" y="5" width="5.5" height="30" rx="2.75" fill="url(#g)"/>
<rect x="22" y="5" width="5.5" height="30" rx="2.75" fill="url(#g)"/>
<path d="M5.5 7.5L22 32.5" stroke="url(#g)" stroke-width="5.5" stroke-linecap="round"/>
<text x="36" y="29" font-family="Inter, ui-sans-serif, sans-serif" font-weight="700" font-size="22" letter-spacing="-0.5" fill="#f0f0f5">nimuli</text>
</svg>

After

Width:  |  Height:  |  Size: 660 B

View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 40" fill="none">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
</defs>
<rect x="0" y="5" width="5.5" height="30" rx="2.75" fill="url(#g)"/>
<rect x="22" y="5" width="5.5" height="30" rx="2.75" fill="url(#g)"/>
<path d="M5.5 7.5L22 32.5" stroke="url(#g)" stroke-width="5.5" stroke-linecap="round"/>
<text x="36" y="29" font-family="Inter, ui-sans-serif, sans-serif" font-weight="700" font-size="22" letter-spacing="-0.5" fill="#111827">nimuli</text>
</svg>

After

Width:  |  Height:  |  Size: 660 B

View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
</defs>
<!-- n-shaped mark: two vertical bars connected by diagonal -->
<rect x="4" y="6" width="5" height="20" rx="2.5" fill="url(#g)"/>
<rect x="23" y="6" width="5" height="20" rx="2.5" fill="url(#g)"/>
<path d="M9 8.5L23 23.5" stroke="url(#g)" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 562 B

14
public/brand/logo.svg Normal file
View File

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 40" fill="none">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
</defs>
<!-- Mark -->
<rect x="0" y="5" width="5.5" height="30" rx="2.75" fill="url(#g)"/>
<rect x="22" y="5" width="5.5" height="30" rx="2.75" fill="url(#g)"/>
<path d="M5.5 7.5L22 32.5" stroke="url(#g)" stroke-width="5.5" stroke-linecap="round"/>
<!-- Wordmark: nimuli -->
<text x="36" y="29" font-family="Inter, ui-sans-serif, sans-serif" font-weight="700" font-size="22" letter-spacing="-0.5" fill="url(#g)">nimuli</text>
</svg>

After

Width:  |  Height:  |  Size: 704 B

11
public/favicon.svg Normal file
View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="100%" stop-color="#ec4899"/>
</linearGradient>
</defs>
<rect x="4" y="6" width="5" height="20" rx="2.5" fill="url(#g)"/>
<rect x="23" y="6" width="5" height="20" rx="2.5" fill="url(#g)"/>
<path d="M9 8.5L23 23.5" stroke="url(#g)" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

13
public/site.webmanifest Normal file
View File

@ -0,0 +1,13 @@
{
"name": "Nimuli",
"short_name": "Nimuli",
"description": "Short links, QR codes, and bio pages — all in one place.",
"start_url": "/dashboard",
"display": "standalone",
"background_color": "#0a0a0f",
"theme_color": "#7c3aed",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}

View File

@ -6,19 +6,26 @@
@source '../**/*.js';
@theme {
/* Dark mode (default) */
--color-bg: #0a0a0f;
--color-s1: #111117;
--color-s2: #16161e;
--color-s3: #1c1c26;
--color-b1: rgb(255 255 255 / 6%);
--color-b2: rgb(255 255 255 / 10%);
--color-b1: rgb(255 255 255 / 8%);
--color-b2: rgb(255 255 255 / 14%);
--color-t1: #f0f0f5;
--color-t2: #8888a0;
--color-t3: #44445a;
--color-blue: #4f8ef7;
/* Accent — purple-pink gradient, consistent across both themes */
--color-blue: #7c3aed;
--color-accent: #7c3aed;
--color-accent-2: #ec4899;
--color-green: #3ecf8e;
--color-amber: #f5a623;
--color-red: #f05252;
--font-mono: 'JetBrains Mono', monospace;
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
@ -33,8 +40,30 @@
--color-t1: #111827;
--color-t2: #6b7280;
--color-t3: #9ca3af;
--color-blue: #7c3aed;
--color-accent: #7c3aed;
--color-accent-2: #ec4899;
--color-green: #10b981;
--color-amber: #f59e0b;
--color-red: #ef4444;
}
/* Visible borders on all themes */
*, ::before, ::after { border-color: var(--color-b1); }
@layer utilities {
.bg-accent-gradient {
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-2));
}
.text-accent-gradient {
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.border-accent-gradient {
border-image: linear-gradient(135deg, var(--color-accent), var(--color-accent-2)) 1;
}
}

View File

@ -1,34 +1,23 @@
import '../css/app.css';
import './bootstrap';
import Alpine from 'alpinejs';
import Chart from 'chart.js/auto';
window.Chart = Chart;
// Theme store — reads localStorage, applies to <html data-theme>
Alpine.store('theme', {
// Alpine stores registered via alpine:init — Livewire v3 owns Alpine lifecycle
document.addEventListener('alpine:init', () => {
Alpine.store('theme', {
current: localStorage.getItem('nimuli_theme') || document.documentElement.dataset.theme || 'dark',
init() {
this.apply(this.current);
},
init() { this.apply(this.current); },
apply(theme) {
this.current = theme;
document.documentElement.dataset.theme = theme;
localStorage.setItem('nimuli_theme', theme);
},
cycle() {
const order = ['dark', 'light', 'system'];
const next = order[(order.indexOf(this.current) + 1) % order.length];
this.apply(next);
this.apply(order[(order.indexOf(this.current) + 1) % order.length]);
},
});
});
// Sidebar drawer store — mobile open/close
Alpine.store('sidebar', {
open: false,
Alpine.store('sidebar', { open: false });
});
window.Alpine = Alpine;
Alpine.start();

View File

@ -100,8 +100,9 @@
@click="wsOpen = !wsOpen"
class="w-full flex items-center gap-3 px-4 py-4 hover:bg-s2 transition-colors border-b border-white/[.06]"
>
<img src="/brand/logo-mark.svg" alt="Nimuli" class="w-7 h-7 flex-shrink-0">
<div class="flex-1 text-left min-w-0">
<div class="text-sm font-bold text-t1 tracking-tight">nimuli.</div>
<div class="text-sm font-bold tracking-tight text-accent-gradient">nimuli</div>
@if($workspace)
<div class="text-xs text-t2 truncate mt-0.5">{{ $workspace->name }}</div>
@else

View File

@ -6,11 +6,26 @@
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ ($title ?? '') ? $title . ' — Nimuli' : 'Nimuli' }}</title>
<!-- Prevent FOUC: inline critical theme vars -->
<style>
html { background: #0a0a0f; color: #f0f0f5; }
[data-theme="light"] { background: #ffffff; color: #111827; }
</style>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="theme-color" content="#0a0a0f" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
@vite(['resources/js/app.js'])
@livewireStyles
</head>
@ -30,13 +45,15 @@
aria-hidden="true"
></div>
<!-- Sidebar: always visible on md+, drawer on mobile -->
<!-- Sidebar: persisted across wire:navigate to prevent re-render -->
@persist('sidebar')
<div
class="fixed inset-y-0 left-0 z-30 md:relative md:z-auto md:flex md:flex-shrink-0 transition-transform duration-200 ease-in-out"
:class="$store.sidebar?.open ? 'translate-x-0' : '-translate-x-full md:translate-x-0'"
>
<x-sidebar />
</div>
@endpersist
<!-- Main Area -->
<div class="flex-1 flex flex-col overflow-hidden min-w-0">