mirror of
https://github.com/aculix/negotium.git
synced 2026-09-11 07:28:17 +00:00
perf: drop lottie, remove splash, fix theme flash
Bundle goes from 337 kB to 52 kB, or 89 kB to 20 kB gzipped. Almost all of it was lottie-web, imported at the top level to draw one decorative empty state, so every visitor paid 305 kB whether they ever saw it or not. It's an inline SVG now, and it respects prefers-reduced-motion. The 500ms splash is gone. There was no async work behind it. Dark mode is applied by a small script in the head before first paint, so dark-mode users stop getting a white flash on load. The theme class moves to <html>, and App.svelte reads its initial state from that class instead of re-reading storage after mount. publicDir moves from assets/ to public/, which stops shipping the 200 kB README screenshot to everyone.
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 195 KiB |
+17
-3
@@ -6,6 +6,22 @@
|
||||
<title>Negotium - Your Productivity Companion</title>
|
||||
<meta name="description" content="A clean, minimal to-do list application with smooth animations and dark/light mode support">
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'><path d='M12.37 8.87988H17.62' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><path d='M6.38 8.87988L7.13 9.62988L9.38 7.37988' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><path d='M12.37 15.8799H17.62' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><path d='M6.38 15.8799L7.13 16.6299L9.38 14.3799' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><path d='M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>">
|
||||
<script>
|
||||
// Runs before first paint so dark-mode users never see a light flash.
|
||||
// App.svelte seeds its own state from the class this sets, keeping one
|
||||
// source of truth.
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem('negotium-theme');
|
||||
var dark = stored
|
||||
? stored === 'dark'
|
||||
: window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (dark) document.documentElement.classList.add('dark');
|
||||
} catch (e) {
|
||||
// Storage unavailable; fall through to the light default.
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
/* Prevent flash of unstyled content */
|
||||
body {
|
||||
@@ -14,14 +30,12 @@
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background-color: #F8FAFB;
|
||||
color: #1A1A1A;
|
||||
transition: background-color 300ms ease, color 300ms ease;
|
||||
}
|
||||
|
||||
.dark {
|
||||
html.dark body {
|
||||
background-color: #121212;
|
||||
color: #E0E0E0;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Generated
-9
@@ -8,9 +8,6 @@
|
||||
"name": "negotium-todo",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lottie-web": "^5.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
||||
"svelte": "^5.56.9",
|
||||
@@ -985,12 +982,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lottie-web": {
|
||||
"version": "5.13.0",
|
||||
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz",
|
||||
"integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.0.tgz",
|
||||
|
||||
+1
-4
@@ -24,8 +24,5 @@
|
||||
"minimal"
|
||||
],
|
||||
"author": "Negotium",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lottie-web": "^5.13.0"
|
||||
}
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 908 B After Width: | Height: | Size: 908 B |
+20
-59
@@ -2,7 +2,6 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { fly, fade } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import lottie from 'lottie-web';
|
||||
import './style.css';
|
||||
|
||||
import { toKey, addDays, fromKey, labelFor, formatLong } from './lib/dates.js';
|
||||
@@ -14,8 +13,9 @@
|
||||
|
||||
let tasks = $state([]);
|
||||
let newTask = $state('');
|
||||
let darkMode = $state(false);
|
||||
let isLoading = $state(true);
|
||||
// Seeded from the class the pre-paint script in index.html already set, so
|
||||
// there is one source of truth and no post-mount correction to flash.
|
||||
let darkMode = $state(document.documentElement.classList.contains('dark'));
|
||||
let isInitialized = false;
|
||||
let todayKey = $state(toKey(new Date()));
|
||||
let selectedKey = $state(toKey(new Date()));
|
||||
@@ -145,52 +145,15 @@
|
||||
const completedTasks = $derived(tasks.filter(task => task.completed).length);
|
||||
|
||||
$effect(() => {
|
||||
document.documentElement.classList.toggle('dark', darkMode);
|
||||
if (!isInitialized) return;
|
||||
storage.saveTheme(darkMode ? 'dark' : 'light');
|
||||
});
|
||||
|
||||
function initLottie(node) {
|
||||
let instance = null;
|
||||
|
||||
async function loadAnimation() {
|
||||
try {
|
||||
const response = await fetch('/lottie_empty_state.json');
|
||||
const animationData = await response.json();
|
||||
|
||||
instance = lottie.loadAnimation({
|
||||
container: node,
|
||||
renderer: 'svg',
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
animationData: animationData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load Lottie animation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadAnimation();
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
if (instance) {
|
||||
instance.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
storage.migrateLegacyKeys();
|
||||
runRollover();
|
||||
|
||||
const savedTheme = storage.loadTheme();
|
||||
darkMode = savedTheme ? savedTheme === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
setTimeout(() => {
|
||||
isLoading = false;
|
||||
isInitialized = true;
|
||||
}, 500);
|
||||
isInitialized = true;
|
||||
|
||||
// Four triggers, because no single one is sufficient. The timer covers a
|
||||
// pinned tab crossing midnight unattended; visibility and focus cover
|
||||
@@ -209,21 +172,7 @@
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="app" class:dark={darkMode}>
|
||||
{#if isLoading}
|
||||
<div class="loading-overlay" transition:fade={{ duration: 300 }}>
|
||||
<div class="loading">
|
||||
<svg class="loading-logo" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.37 8.87988H17.62" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.38 8.87988L7.13 9.62988L9.38 7.37988" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.37 15.8799H17.62" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.38 15.8799L7.13 16.6299L9.38 14.3799" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div class="loading-text">Loading Negotium...</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="app">
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<div class="logo-section">
|
||||
@@ -305,7 +254,20 @@
|
||||
{#key selectedKey}
|
||||
{#if tasks.length === 0}
|
||||
<div class="empty-state" transition:fade={{ duration: 200 }}>
|
||||
<div class="lottie-animation" use:initLottie></div>
|
||||
<svg class="empty-art" viewBox="0 0 120 120" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="empty-art-motes" stroke="currentColor" stroke-width="3" stroke-linecap="round">
|
||||
<path d="M40 34 L40 34" />
|
||||
<path d="M60 26 L60 26" />
|
||||
<path d="M80 34 L80 34" />
|
||||
</g>
|
||||
<g class="empty-art-box" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M30 58 L30 92 L90 92 L90 58" />
|
||||
<path d="M24 58 L96 58" />
|
||||
<path d="M30 58 L18 45" />
|
||||
<path d="M90 58 L102 45" />
|
||||
<path d="M48 74 L72 74" opacity="0.35" />
|
||||
</g>
|
||||
</svg>
|
||||
<p>No tasks yet. Add one above to get started!</p>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -361,5 +323,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+39
-46
@@ -12,7 +12,7 @@
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.dark {
|
||||
html.dark {
|
||||
--bg-primary: #121212;
|
||||
--bg-surface: #1E1E1E;
|
||||
--text-primary: #E0E0E0;
|
||||
@@ -377,50 +377,6 @@ body {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--bg-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
color: var(--accent);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
@@ -436,10 +392,47 @@ body {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lottie-animation {
|
||||
/* Replaces a 305 KB Lottie player that existed to draw this one picture.
|
||||
Same 200px box, so the empty state keeps its original proportions. */
|
||||
.empty-art {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 24px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.empty-art-box {
|
||||
animation: empty-float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.empty-art-motes path {
|
||||
animation: empty-mote 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.empty-art-motes path:nth-child(2) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.empty-art-motes path:nth-child(3) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes empty-float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
|
||||
@keyframes empty-mote {
|
||||
0%, 100% { opacity: 0.15; transform: translateY(0); }
|
||||
50% { opacity: 0.6; transform: translateY(-6px); }
|
||||
}
|
||||
|
||||
/* Decorative only — respect a reduced-motion preference. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.empty-art-box,
|
||||
.empty-art-motes path {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
publicDir: 'assets',
|
||||
publicDir: 'public',
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
|
||||
Reference in New Issue
Block a user