diff --git a/spotizerr-ui/dev-dist/sw.js b/spotizerr-ui/dev-dist/sw.js
index b2596b7..89aa4fe 100644
--- a/spotizerr-ui/dev-dist/sw.js
+++ b/spotizerr-ui/dev-dist/sw.js
@@ -82,7 +82,7 @@ define(['./workbox-f70c5944'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
- "revision": "0.ef94id05f1c"
+ "revision": "0.udbs5r8ifc8"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
diff --git a/spotizerr-ui/index.html b/spotizerr-ui/index.html
index f1da355..937799d 100644
--- a/spotizerr-ui/index.html
+++ b/spotizerr-ui/index.html
@@ -2,9 +2,19 @@
+
+
+
+
+
+
+
+
+
+
+
-
Spotizerr
diff --git a/spotizerr-ui/public/apple-touch-icon-180x180.png b/spotizerr-ui/public/apple-touch-icon-180x180.png
index f4c08db..ff73e15 100644
Binary files a/spotizerr-ui/public/apple-touch-icon-180x180.png and b/spotizerr-ui/public/apple-touch-icon-180x180.png differ
diff --git a/spotizerr-ui/public/favicon-128x128.png b/spotizerr-ui/public/favicon-128x128.png
index acf077f..9bdb0b1 100644
Binary files a/spotizerr-ui/public/favicon-128x128.png and b/spotizerr-ui/public/favicon-128x128.png differ
diff --git a/spotizerr-ui/public/favicon-16x16.png b/spotizerr-ui/public/favicon-16x16.png
index 3277063..1686296 100644
Binary files a/spotizerr-ui/public/favicon-16x16.png and b/spotizerr-ui/public/favicon-16x16.png differ
diff --git a/spotizerr-ui/public/favicon-256x256.png b/spotizerr-ui/public/favicon-256x256.png
index 1b3195e..7bf944a 100644
Binary files a/spotizerr-ui/public/favicon-256x256.png and b/spotizerr-ui/public/favicon-256x256.png differ
diff --git a/spotizerr-ui/public/favicon-32x32.png b/spotizerr-ui/public/favicon-32x32.png
index 02e9591..c3732a2 100644
Binary files a/spotizerr-ui/public/favicon-32x32.png and b/spotizerr-ui/public/favicon-32x32.png differ
diff --git a/spotizerr-ui/public/favicon-48x48.png b/spotizerr-ui/public/favicon-48x48.png
index 1ce92da..0ddde31 100644
Binary files a/spotizerr-ui/public/favicon-48x48.png and b/spotizerr-ui/public/favicon-48x48.png differ
diff --git a/spotizerr-ui/public/favicon-64x64.png b/spotizerr-ui/public/favicon-64x64.png
index 3c1762c..2501a45 100644
Binary files a/spotizerr-ui/public/favicon-64x64.png and b/spotizerr-ui/public/favicon-64x64.png differ
diff --git a/spotizerr-ui/public/favicon-96x96.png b/spotizerr-ui/public/favicon-96x96.png
index e8cac38..1b677bb 100644
Binary files a/spotizerr-ui/public/favicon-96x96.png and b/spotizerr-ui/public/favicon-96x96.png differ
diff --git a/spotizerr-ui/public/favicon.ico b/spotizerr-ui/public/favicon.ico
index f85f81a..a3bd07b 100644
Binary files a/spotizerr-ui/public/favicon.ico and b/spotizerr-ui/public/favicon.ico differ
diff --git a/spotizerr-ui/public/pwa-192x192.png b/spotizerr-ui/public/pwa-192x192.png
index f17481e..d9c77b0 100644
Binary files a/spotizerr-ui/public/pwa-192x192.png and b/spotizerr-ui/public/pwa-192x192.png differ
diff --git a/spotizerr-ui/public/pwa-512x512-maskable.png b/spotizerr-ui/public/pwa-512x512-maskable.png
index 0e81c1b..2055e18 100644
Binary files a/spotizerr-ui/public/pwa-512x512-maskable.png and b/spotizerr-ui/public/pwa-512x512-maskable.png differ
diff --git a/spotizerr-ui/public/pwa-512x512.png b/spotizerr-ui/public/pwa-512x512.png
index bf2d41d..8fa2c85 100644
Binary files a/spotizerr-ui/public/pwa-512x512.png and b/spotizerr-ui/public/pwa-512x512.png differ
diff --git a/spotizerr-ui/scripts/generate-icons.js b/spotizerr-ui/scripts/generate-icons.js
index 8ca901f..d809bbe 100644
--- a/spotizerr-ui/scripts/generate-icons.js
+++ b/spotizerr-ui/scripts/generate-icons.js
@@ -4,6 +4,16 @@ import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import toIco from 'to-ico';
+// Helper function to create a rounded square mask
+async function createRoundedSquareMask(size, radius) {
+ const svg = `
+
+ `;
+ return Buffer.from(svg);
+}
+
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -80,26 +90,36 @@ async function generateIcons() {
size: 16,
name: 'favicon-16x16.png',
padding: 0.1, // 10% padding for small icons
+ rounded: true,
+ cornerRadius: 3, // 3px radius for small icons
},
{
size: 32,
name: 'favicon-32x32.png',
padding: 0.1,
+ rounded: true,
+ cornerRadius: 6, // 6px radius
},
{
size: 180,
name: 'apple-touch-icon-180x180.png',
padding: 0.05, // 5% padding for Apple (they prefer less padding)
+ rounded: true,
+ cornerRadius: 32, // ~18% radius for Apple icons
},
{
size: 192,
name: 'pwa-192x192.png',
padding: 0.1,
+ rounded: true,
+ cornerRadius: 34, // ~18% radius
},
{
size: 512,
name: 'pwa-512x512.png',
padding: 0.1,
+ rounded: true,
+ cornerRadius: 92, // ~18% radius
}
];
@@ -107,7 +127,9 @@ async function generateIcons() {
const sourceImage = sharp(processedImage);
for (const config of iconConfigs) {
- const { size, name, padding } = config;
+ const { size, name, padding, rounded, cornerRadius } = config;
+
+ let finalIcon;
if (padding > 0) {
// Create icon with padding by compositing on a background
@@ -115,7 +137,7 @@ async function generateIcons() {
const offset = Math.round((size - paddedSize) / 2);
// Create a pure black background and composite the resized logo on top
- await sharp({
+ finalIcon = await sharp({
create: {
width: size,
height: size,
@@ -129,25 +151,42 @@ async function generateIcons() {
left: offset
}])
.png()
- .toFile(join(publicDir, name));
+ .toBuffer();
} else {
// Direct resize without padding
- await sourceImage
+ finalIcon = await sourceImage
.resize(size, size)
.png()
- .toFile(join(publicDir, name));
+ .toBuffer();
}
- console.log(`✅ Generated ${name} (${size}x${size}) - padding: ${padding * 100}%`);
+ // Apply rounded corners if specified
+ if (rounded && cornerRadius > 0) {
+ const mask = await createRoundedSquareMask(size, cornerRadius);
+ finalIcon = await sharp(finalIcon)
+ .composite([{
+ input: mask,
+ blend: 'dest-in'
+ }])
+ .png()
+ .toBuffer();
+ }
+
+ // Write the final icon to file
+ await sharp(finalIcon).toFile(join(publicDir, name));
+
+ const roundedText = rounded ? ` - rounded (${cornerRadius}px)` : '';
+ console.log(`✅ Generated ${name} (${size}x${size}) - padding: ${padding * 100}%${roundedText}`);
}
- // Create maskable icon (less padding, solid background)
+ // Create maskable icon (less padding, solid background, rounded)
const maskableSize = 512;
const maskablePadding = 0.05; // 5% padding for maskable icons
+ const maskableRadius = 92; // ~18% radius for consistency
const maskablePaddedSize = Math.round(maskableSize * (1 - maskablePadding * 2));
const maskableOffset = Math.round((maskableSize - maskablePaddedSize) / 2);
- await sharp({
+ let maskableIcon = await sharp({
create: {
width: maskableSize,
height: maskableSize,
@@ -161,47 +200,33 @@ async function generateIcons() {
left: maskableOffset
}])
.png()
- .toFile(join(publicDir, 'pwa-512x512-maskable.png'));
+ .toBuffer();
- console.log(`✅ Generated pwa-512x512-maskable.png (${maskableSize}x${maskableSize}) - maskable`);
+ // Apply rounded corners to maskable icon
+ const maskableMask = await createRoundedSquareMask(maskableSize, maskableRadius);
+ maskableIcon = await sharp(maskableIcon)
+ .composite([{
+ input: maskableMask,
+ blend: 'dest-in'
+ }])
+ .png()
+ .toBuffer();
- // Generate additional favicon sizes for ICO compatibility
+ await sharp(maskableIcon).toFile(join(publicDir, 'pwa-512x512-maskable.png'));
+
+ console.log(`✅ Generated pwa-512x512-maskable.png (${maskableSize}x${maskableSize}) - maskable, rounded (${maskableRadius}px)`);
+
+ // Generate additional favicon sizes for ICO compatibility (with rounded corners)
const additionalSizes = [48, 64, 96, 128, 256];
for (const size of additionalSizes) {
const padding = size <= 48 ? 0.05 : 0.1;
const paddedSize = Math.round(size * (1 - padding * 2));
const offset = Math.round((size - paddedSize) / 2);
+
+ // Calculate corner radius proportional to size (~18% for larger icons, smaller for tiny ones)
+ const cornerRadius = size <= 48 ? Math.round(size * 0.125) : Math.round(size * 0.18);
- await sharp({
- create: {
- width: size,
- height: size,
- channels: 4,
- background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background
- }
- })
- .composite([{
- input: await sourceImage.resize(paddedSize, paddedSize).png().toBuffer(),
- top: offset,
- left: offset
- }])
- .png()
- .toFile(join(publicDir, `favicon-${size}x${size}.png`));
-
- console.log(`✅ Generated favicon-${size}x${size}.png (${size}x${size}) - padding: ${padding * 100}%`);
- }
-
- // Generate favicon.ico with multiple sizes
- console.log('🎯 Generating favicon.ico...');
- const icoSizes = [16, 32, 48];
- const icoBuffers = [];
-
- for (const size of icoSizes) {
- const padding = 0.1; // 10% padding for ICO
- const paddedSize = Math.round(size * (1 - padding * 2));
- const offset = Math.round((size - paddedSize) / 2);
-
- const buffer = await sharp({
+ let additionalIcon = await sharp({
create: {
width: size,
height: size,
@@ -217,29 +242,82 @@ async function generateIcons() {
.png()
.toBuffer();
- icoBuffers.push(buffer);
+ // Apply rounded corners
+ const additionalMask = await createRoundedSquareMask(size, cornerRadius);
+ additionalIcon = await sharp(additionalIcon)
+ .composite([{
+ input: additionalMask,
+ blend: 'dest-in'
+ }])
+ .png()
+ .toBuffer();
+
+ await sharp(additionalIcon).toFile(join(publicDir, `favicon-${size}x${size}.png`));
+
+ console.log(`✅ Generated favicon-${size}x${size}.png (${size}x${size}) - padding: ${padding * 100}%, rounded (${cornerRadius}px)`);
+ }
+
+ // Generate favicon.ico with multiple sizes (rounded)
+ console.log('🎯 Generating favicon.ico with rounded corners...');
+ const icoSizes = [16, 32, 48];
+ const icoBuffers = [];
+
+ for (const size of icoSizes) {
+ const padding = 0.1; // 10% padding for ICO
+ const paddedSize = Math.round(size * (1 - padding * 2));
+ const offset = Math.round((size - paddedSize) / 2);
+ const cornerRadius = size <= 32 ? Math.round(size * 0.125) : Math.round(size * 0.18);
+
+ let icoIcon = await sharp({
+ create: {
+ width: size,
+ height: size,
+ channels: 4,
+ background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background
+ }
+ })
+ .composite([{
+ input: await sourceImage.resize(paddedSize, paddedSize).png().toBuffer(),
+ top: offset,
+ left: offset
+ }])
+ .png()
+ .toBuffer();
+
+ // Apply rounded corners to ICO icon
+ const icoMask = await createRoundedSquareMask(size, cornerRadius);
+ const roundedIcoIcon = await sharp(icoIcon)
+ .composite([{
+ input: icoMask,
+ blend: 'dest-in'
+ }])
+ .png()
+ .toBuffer();
+
+ icoBuffers.push(roundedIcoIcon);
}
// Create the ICO file
const icoBuffer = await toIco(icoBuffers);
writeFileSync(join(publicDir, 'favicon.ico'), icoBuffer);
- console.log(`✅ Generated favicon.ico (${icoSizes.join('x, ')}x sizes) - multi-size ICO`);
+ console.log(`✅ Generated favicon.ico (${icoSizes.join('x, ')}x sizes) - multi-size ICO, rounded corners`);
- console.log('🎉 All PWA icons generated successfully!');
+ console.log('🎉 All PWA icons generated successfully with rounded corners!');
console.log('');
console.log('📋 Generated files:');
iconConfigs.forEach(config => {
- console.log(` • ${config.name} (${config.size}x${config.size})`);
+ console.log(` • ${config.name} (${config.size}x${config.size}) - rounded`);
});
- console.log(' • pwa-512x512-maskable.png (512x512)');
+ console.log(' • pwa-512x512-maskable.png (512x512) - rounded');
additionalSizes.forEach(size => {
- console.log(` • favicon-${size}x${size}.png (${size}x${size})`);
+ console.log(` • favicon-${size}x${size}.png (${size}x${size}) - rounded`);
});
- console.log(' • favicon.ico (multi-size: 16x16, 32x32, 48x48)');
+ console.log(' • favicon.ico (multi-size: 16x16, 32x32, 48x48) - rounded');
console.log('');
console.log('💡 Icons generated from SVG source, scaled by 0.67, and centered on pure black backgrounds.');
- console.log('💡 The SVG logo is automatically processed and optimized for all icon formats.');
+ console.log('💡 All icons feature rounded corners for a modern, polished appearance.');
+ console.log('💡 Corner radius scales proportionally with icon size (~18% for larger icons, ~12.5% for smaller ones).');
console.log('💡 favicon.ico contains multiple sizes for optimal browser compatibility.');
} catch (error) {
diff --git a/spotizerr-ui/src/index.css b/spotizerr-ui/src/index.css
index 098f919..5b4fd95 100644
--- a/spotizerr-ui/src/index.css
+++ b/spotizerr-ui/src/index.css
@@ -1,103 +1,110 @@
@import "tailwindcss";
@theme {
- /* Background Colors */
+ /* Background Colors - Light theme: pure white to warm grays */
--color-surface: #ffffff;
- --color-surface-secondary: #f8fafc;
- --color-surface-muted: #f1f5f9;
- --color-surface-accent: #e2e8f0;
- --color-surface-overlay: rgba(255, 255, 255, 0.95);
+ --color-surface-secondary: #fafbfc;
+ --color-surface-muted: #f4f6f8;
+ --color-surface-accent: #e8ecf0;
+ --color-surface-overlay: rgba(255, 255, 255, 0.96);
- /* Dark mode backgrounds */
- --color-surface-dark: #0f172a;
- --color-surface-secondary-dark: #1e293b;
- --color-surface-muted-dark: #334155;
- --color-surface-accent-dark: #475569;
- --color-surface-overlay-dark: rgba(15, 23, 42, 0.95);
+ /* Dark mode backgrounds - Rich blacks with subtle warm undertones */
+ --color-surface-dark: #0a0a0a;
+ --color-surface-secondary-dark: #141414;
+ --color-surface-muted-dark: #1f1f1f;
+ --color-surface-accent-dark: #2d2d2d;
+ --color-surface-overlay-dark: rgba(10, 10, 10, 0.96);
- /* Text Colors */
- --color-content-primary: #0f172a;
- --color-content-secondary: #475569;
- --color-content-muted: #64748b;
- --color-content-accent: #94a3b8;
+ /* Text Colors - Light theme with enhanced contrast */
+ --color-content-primary: #0d1117;
+ --color-content-secondary: #57606a;
+ --color-content-muted: #768390;
+ --color-content-accent: #8c959f;
--color-content-inverse: #ffffff;
- /* Dark mode text */
- --color-content-primary-dark: #f8fafc;
- --color-content-secondary-dark: #e2e8f0;
- --color-content-muted-dark: #cbd5e1;
- --color-content-accent-dark: #94a3b8;
- --color-content-inverse-dark: #0f172a;
+ /* Dark mode text - Crisp whites and refined grays */
+ --color-content-primary-dark: #f0f6fc;
+ --color-content-secondary-dark: #c9d1d9;
+ --color-content-muted-dark: #8b949e;
+ --color-content-accent-dark: #6e7681;
+ --color-content-inverse-dark: #0d1117;
- /* Interactive Colors */
- --color-primary: #3b82f6;
- --color-primary-hover: #2563eb;
- --color-primary-active: #1d4ed8;
- --color-primary-muted: #dbeafe;
+ /* Interactive Colors - Enhanced Spotify green with sophistication */
+ --color-primary: #1ed760;
+ --color-primary-hover: #1fdf64;
+ --color-primary-active: #1db954;
+ --color-primary-muted: #d1f7e0;
- --color-secondary: #64748b;
- --color-secondary-hover: #475569;
- --color-secondary-active: #334155;
- --color-secondary-muted: #f1f5f9;
+ /* Secondary colors with purple accent for contrast */
+ --color-secondary: #8b5fbf;
+ --color-secondary-hover: #9d70d1;
+ --color-secondary-active: #7a4fa3;
+ --color-secondary-muted: #f0ebff;
+
+ /* Accent purple for complementary design */
+ --color-accent: #8b5fbf;
+ --color-accent-hover: #9d70d1;
+ --color-accent-active: #7a4fa3;
+ --color-accent-muted: #f0ebff;
- /* Status Colors */
- --color-success: #22c55e;
- --color-success-hover: #16a34a;
- --color-success-muted: #dcfce7;
- --color-success-text: #15803d;
+ /* Status Colors - Refined and harmonious */
+ --color-success: #28a745;
+ --color-success-hover: #1e7b34;
+ --color-success-muted: #d4edda;
+ --color-success-text: #155724;
- --color-error: #ef4444;
- --color-error-hover: #dc2626;
- --color-error-muted: #fef2f2;
- --color-error-text: #dc2626;
+ --color-error: #dc3545;
+ --color-error-hover: #c82333;
+ --color-error-muted: #f8d7da;
+ --color-error-text: #721c24;
- --color-warning: #f59e0b;
- --color-warning-hover: #d97706;
- --color-warning-muted: #fef3c7;
- --color-warning-text: #d97706;
+ --color-warning: #fd7e14;
+ --color-warning-hover: #e8590c;
+ --color-warning-muted: #fff3cd;
+ --color-warning-text: #856404;
- --color-info: #3b82f6;
- --color-info-hover: #2563eb;
- --color-info-muted: #dbeafe;
- --color-info-text: #2563eb;
+ --color-info: #17a2b8;
+ --color-info-hover: #138496;
+ --color-info-muted: #d1ecf1;
+ --color-info-text: #0c5460;
- --color-processing: #a855f7;
- --color-processing-hover: #9333ea;
- --color-processing-muted: #f3e8ff;
- --color-processing-text: #9333ea;
+ --color-processing: #8b5fbf;
+ --color-processing-hover: #7a4fa3;
+ --color-processing-muted: #f0ebff;
+ --color-processing-text: #5d3e7a;
- /* Border Colors */
- --color-border: #e2e8f0;
- --color-border-muted: #f1f5f9;
- --color-border-accent: #cbd5e1;
- --color-border-focus: #3b82f6;
- --color-border-dark: #475569;
- --color-border-muted-dark: #334155;
- --color-border-accent-dark: #64748b;
+ /* Border Colors - Subtle and refined */
+ --color-border: #e1e5e9;
+ --color-border-muted: #f4f6f8;
+ --color-border-accent: #d0d7de;
+ --color-border-focus: #1ed760;
+ --color-border-dark: #30363d;
+ --color-border-muted-dark: #21262d;
+ --color-border-accent-dark: #373e47;
/* Input Colors */
- --color-input-background: #f1f5f9;
- --color-input-border: #e2e8f0;
- --color-input-focus: #3b82f6;
- --color-input-background-dark: #1e293b;
- --color-input-border-dark: #475569;
+ --color-input-background: #f4f6f8;
+ --color-input-border: #e1e5e9;
+ --color-input-focus: #1ed760;
+ --color-input-background-dark: #21262d;
+ --color-input-border-dark: #30363d;
/* Button Colors */
- --color-button-primary: #3b82f6;
- --color-button-primary-hover: #2563eb;
+ --color-button-primary: #1ed760;
+ --color-button-primary-hover: #1fdf64;
--color-button-primary-text: #ffffff;
- --color-button-secondary: #f1f5f9;
- --color-button-secondary-hover: #64748b;
- --color-button-secondary-text: #64748b;
+ --color-button-secondary: #f4f6f8;
+ --color-button-secondary-hover: #8b5fbf;
+ --color-button-secondary-text: #57606a;
--color-button-secondary-text-hover: #ffffff;
- --color-button-success: #22c55e;
- --color-button-success-hover: #16a34a;
+ --color-button-success: #28a745;
+ --color-button-success-hover: #1e7b34;
--color-button-success-text: #ffffff;
- --color-icon-button-hover: #e5e7eb;
- --color-icon-button-hover-dark: #334155;
+ --color-icon-button-hover: #e8ecf0;
+ --color-icon-button-hover-dark: #30363d;
/* Icon Colors */
--color-icon-primary: #000000;
@@ -316,4 +323,20 @@
color: var(--color-icon-inverse-dark);
filter: brightness(0);
}
+
+ /* Logo Utility Classes - Automatic dark mode detection like Tailwind v4 */
+ .logo {
+ filter: brightness(0); /* Black in light mode */
+ }
+
+ @media (prefers-color-scheme: dark) {
+ .logo {
+ filter: brightness(0) invert(1); /* White in dark mode */
+ }
+ }
+
+ /* Class-based override for manual toggles */
+ .dark .logo {
+ filter: brightness(0) invert(1); /* White when dark class is present */
+ }
}
diff --git a/spotizerr-ui/src/routes/home.tsx b/spotizerr-ui/src/routes/home.tsx
index 41533a6..a075aac 100644
--- a/spotizerr-ui/src/routes/home.tsx
+++ b/spotizerr-ui/src/routes/home.tsx
@@ -130,7 +130,9 @@ export const Home = () => {
return (
-
Search Spotify
+
+
Spotizerr
+