From 29b48990893d19f9ec3470205e31e0d7a69fb665 Mon Sep 17 00:00:00 2001 From: Samidy Date: Tue, 24 Feb 2026 01:57:17 +0300 Subject: [PATCH] feat(themes): Community Themes --- THEME_GUIDE.md | 77 +++++ index.html | 141 ++++++++++ js/app.js | 3 + js/settings.js | 55 ++++ js/themeStore.js | 713 +++++++++++++++++++++++++++++++++++++++++++++++ styles.css | 97 +++++++ 6 files changed, 1086 insertions(+) create mode 100644 THEME_GUIDE.md create mode 100644 js/themeStore.js diff --git a/THEME_GUIDE.md b/THEME_GUIDE.md new file mode 100644 index 0000000..11ed14f --- /dev/null +++ b/THEME_GUIDE.md @@ -0,0 +1,77 @@ +# Monochrome Theme Creation Guide + +Welcome to the Monochrome Theme Guide! This document explains how to create, style, and upload custom themes for Monochrome. + +## Getting Started + +Themes in Monochrome are essentially CSS snippets that override the default CSS variables (custom properties). You can create a theme by defining these variables inside a `:root` block. + +### Basic Structure + +```css +:root { + /* Base Colors */ + --background: #0a0a0a; + --foreground: #ededed; + + /* UI Elements */ + --card: #1a1a1a; + --card-foreground: #ededed; + --border: #2a2a2a; + + /* Accents */ + --primary: #3b82f6; + --primary-foreground: #ffffff; + --secondary: #2a2a2a; + --secondary-foreground: #ededed; + + /* Text */ + --muted: #2a2a2a; + --muted-foreground: #a0a0a0; + + /* Special */ + --highlight: #3b82f6; + --ring: #3b82f6; + --radius: 8px; + --font-family: 'Inter', sans-serif; +} +``` + +## CSS Variables Reference + +| Variable | Description | +| :--- | :--- | +| `--background` | The main background color for your theme. | +| `--foreground` | The main text color. | +| `--card` | Background color for cards, modals, and panels. | +| `--card-foreground` | Text color inside cards. | +| `--border` | Color for borders and separators. | +| `--primary` | Main accent color (buttons, active states). | +| `--primary-foreground` | Text color on top of primary elements. | +| `--secondary` | Secondary background (hover states, secondary buttons). | +| `--secondary-foreground` | Text color on secondary elements. | +| `--muted` | Muted background color (placeholders, skeletons). | +| `--muted-foreground` | Muted text color (subtitles, metadata). | +| `--highlight` | Color used for text highlighting and focus rings. | +| `--radius` | Border radius for cards and buttons (e.g., `8px`, `0px`). | +| `--font-family` | Font stack for the theme. | + +## Using the Theme Editor + +1. **Open the Theme Store**: Go to Settings > Appearance > Open Theme Store. +2. **Go to Upload Tab**: Click on the "Upload" tab. +3. **Use the Toolbar**: + * **Colors**: Use the color pickers to quickly set the main colors. + * **Styles**: Use the dropdowns to set font and border radius. + * **Template**: Click "Template" to insert a starter CSS block. + * **Preview**: Click "Preview" to see your changes in real-time on a sample card. +4. **Manual Editing**: You can manually edit the CSS in the text area for fine-grained control. + +## Uploading Your Theme + +1. **Name & Description**: Give your theme a unique name and a brief description. +2. **Author Website**: Optionally provide a link to your website. + * *Note:* If you have a Monochrome profile, your name will automatically link to it. +3. **Submit**: Click "Upload Theme". + +Once uploaded, your theme will be available for everyone to browse and apply! \ No newline at end of file diff --git a/index.html b/index.html index f5ed86f..4f1eca5 100644 --- a/index.html +++ b/index.html @@ -1258,6 +1258,129 @@ + + + + +
+
+ Community Themes + Browse and apply themes created by the community +
+ +
Font diff --git a/js/app.js b/js/app.js index adf8779..c0940bf 100644 --- a/js/app.js +++ b/js/app.js @@ -24,6 +24,7 @@ import { authManager } from './accounts/auth.js'; import { registerSW } from 'virtual:pwa-register'; import './smooth-scrolling.js'; import { openEditProfile } from './profile.js'; +import { ThemeStore } from './themeStore.js'; import { initTracker } from './tracker.js'; import { @@ -296,6 +297,8 @@ document.addEventListener('DOMContentLoaded', async () => { // Initialize analytics initAnalytics(); + new ThemeStore(); + const api = new MusicAPI(apiSettings); const audioPlayer = document.getElementById('audio-player'); diff --git a/js/settings.js b/js/settings.js index 8dbbdf2..c05f7c6 100644 --- a/js/settings.js +++ b/js/settings.js @@ -679,6 +679,61 @@ export function initializeSettings(scrobbler, player, api, ui) { }); }); + const communityThemeContainer = document.getElementById('applied-community-theme-container'); + const communityThemeBtn = document.getElementById('applied-community-theme-btn'); + const communityThemeDetails = document.getElementById('community-theme-details-panel'); + const communityThemeUnapplyBtn = document.getElementById('ct-unapply-btn'); + const appliedThemeName = document.getElementById('applied-theme-name'); + const ctDetailsTitle = document.getElementById('ct-details-title'); + const ctDetailsAuthor = document.getElementById('ct-details-author'); + + function updateCommunityThemeUI() { + const metadataStr = localStorage.getItem('community-theme'); + if (metadataStr) { + try { + const metadata = JSON.parse(metadataStr); + if (communityThemeContainer) communityThemeContainer.style.display = 'block'; + if (appliedThemeName) appliedThemeName.textContent = metadata.name; + if (ctDetailsTitle) ctDetailsTitle.textContent = metadata.name; + if (ctDetailsAuthor) ctDetailsAuthor.textContent = `by ${metadata.author}`; + } catch { + if (communityThemeContainer) communityThemeContainer.style.display = 'none'; + } + } else { + if (communityThemeContainer) communityThemeContainer.style.display = 'none'; + if (communityThemeDetails) communityThemeDetails.style.display = 'none'; + } + } + + updateCommunityThemeUI(); + window.addEventListener('theme-changed', updateCommunityThemeUI); + + if (communityThemeBtn) { + communityThemeBtn.addEventListener('click', () => { + const isVisible = communityThemeDetails.style.display === 'block'; + communityThemeDetails.style.display = isVisible ? 'none' : 'block'; + }); + } + + if (communityThemeUnapplyBtn) { + communityThemeUnapplyBtn.addEventListener('click', () => { + if (confirm('Unapply this community theme?')) { + localStorage.removeItem('custom_theme_css'); + localStorage.removeItem('community-theme'); + const styleEl = document.getElementById('custom-theme-style'); + if (styleEl) styleEl.remove(); + themeManager.setTheme('system'); + + const themePicker = document.getElementById('theme-picker'); + if (themePicker) { + themePicker.querySelectorAll('.theme-option').forEach(opt => opt.classList.remove('active')); + themePicker.querySelector('[data-theme="system"]')?.classList.add('active'); + } + document.getElementById('custom-theme-editor').classList.remove('show'); + } + }); + } + function renderCustomThemeEditor() { const grid = document.getElementById('theme-color-grid'); const customTheme = themeManager.getCustomTheme() || { diff --git a/js/themeStore.js b/js/themeStore.js new file mode 100644 index 0000000..5bd9294 --- /dev/null +++ b/js/themeStore.js @@ -0,0 +1,713 @@ +import { syncManager } from './accounts/pocketbase.js'; +import { authManager } from './accounts/auth.js'; +import { navigate } from './router.js'; + +export class ThemeStore { + constructor() { + this.pb = syncManager.pb; + this.modal = document.getElementById('theme-store-modal'); + this.grid = document.getElementById('community-themes-grid'); + this.uploadForm = document.getElementById('theme-upload-form'); + this.searchInput = document.getElementById('theme-store-search'); + this.loadingIndicator = document.getElementById('theme-store-loading'); + this._isCheckingAuth = false; + this.previewShadow = null; + this.init(); + } + + init() { + document.getElementById('open-theme-store-btn')?.addEventListener('click', () => { + this.modal.classList.add('active'); + this.loadThemes(); + }); + + this.modal?.querySelector('.close-modal-btn')?.addEventListener('click', () => { + this.modal.classList.remove('active'); + }); + + + const tabs = this.modal?.querySelectorAll('.search-tab'); + tabs?.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('active')); + this.modal.querySelectorAll('.search-tab-content').forEach(c => c.classList.remove('active')); + tab.classList.add('active'); + const contentId = tab.dataset.tab === 'browse' ? 'theme-store-browse' : 'theme-store-upload'; + document.getElementById(contentId)?.classList.add('active'); + if (tab.dataset.tab === 'upload') { + this.checkAuth(); + } + }); + }); + + let debounceTimer; + this.searchInput?.addEventListener('input', (e) => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => this.loadThemes(e.target.value), 300); + }); + + + this.uploadForm?.addEventListener('submit', (e) => this.handleUpload(e)); + + if (authManager) { + authManager.onAuthStateChanged(() => { + if (this.modal.classList.contains('active')) { + this.checkAuth(); + } + }); + } + + + document.getElementById('theme-store-login-btn')?.addEventListener('click', () => { + this.modal.classList.remove('active'); + document.getElementById('email-auth-modal')?.classList.add('active'); + }); + + + this.setupEditorTools(); + + document.getElementById('theme-details-back-btn')?.addEventListener('click', () => { + this.closeThemeDetails(); + }); + + this.applySavedTheme(); + } + + applySavedTheme() { + const theme = localStorage.getItem('monochrome-theme'); + const css = localStorage.getItem('custom_theme_css'); + if (theme === 'custom' && css) { + const metadataStr = localStorage.getItem('community-theme'); + let metadata = null; + if (metadataStr) { + try { metadata = JSON.parse(metadataStr); } catch (e) { console.warn(e); } + } + + if (metadata) { + this.applyTheme({ + css: css, + id: metadata.id, + name: metadata.name, + authorName: metadata.author + }); + } else { + this.applyTheme(css); + } + } + } + + async loadThemes(query = '') { + if (!this.grid) return; + this.grid.innerHTML = ''; + this.loadingIndicator.style.display = 'block'; + + let currentUserId = null; + if (authManager.user) { + try { + + const record = await syncManager._getUserRecord(authManager.user.uid); + currentUserId = record?.id; + } catch (e) { + console.warn('Failed to resolve user ID for theme ownership check', e); + } + } + + try { + const result = await this.pb.collection('themes').getList(1, 50, { + sort: '-created', + filter: query ? `name ~ "${query}" || description ~ "${query}"` : '', + expand: 'author' + }); + this.loadingIndicator.style.display = 'none'; + if (result.items.length === 0) { + this.grid.innerHTML = '
No themes found.
'; + return; + } + result.items.forEach(theme => { + this.grid.appendChild(this.createThemeCard(theme, currentUserId)); + }); + } catch (err) { + console.error('Failed to load themes:', err); + this.loadingIndicator.style.display = 'none'; + this.grid.innerHTML = '
Failed to load themes.
'; + } + } + + createThemeCard(theme, currentUserId) { + const div = document.createElement('div'); + div.className = 'card theme-card'; + const authorName = theme.expand?.author?.username + || theme.expand?.author?.display_name + || theme.authorName + || 'Unknown'; + + + const shortDesc = theme.description ? + (theme.description.length > 80 ? theme.description.substring(0, 80) + '...' : theme.description) + : ''; + + let authorHtml = this.escapeHtml(authorName); + let isInternalProfile = false; + + if (theme.expand?.author?.username) { + isInternalProfile = true; + authorHtml = `${this.escapeHtml(authorName)}`; + } else if (theme.authorUrl) { + authorHtml = `${this.escapeHtml(authorName)}`; + } + + let deleteBtnHtml = ''; + if (currentUserId && theme.author === currentUserId) { + deleteBtnHtml = ` + + `; + } + + const previewStyle = this.extractPreviewStyles(theme.css); + const previewHtml = ` +
+
+
+
+
+
+
+
`; + + div.innerHTML = ` +
+ ${deleteBtnHtml} + ${previewHtml} +
+
+
${this.escapeHtml(theme.name)}
+
by ${authorHtml}
+

+ ${this.escapeHtml(shortDesc)} +

+
+ `; + + div.addEventListener('click', (e) => { + if (e.target.closest('.delete-theme-btn')) { + e.stopPropagation(); + this.deleteTheme(theme.id); + return; + } + this.openThemeDetails(theme); + }); + + if (isInternalProfile) { + const link = div.querySelector('.author-link'); + link?.addEventListener('click', (e) => { + e.stopPropagation(); + this.modal.classList.remove('active'); + navigate(`/user/@${theme.expand.author.username}`); + }); + } + + return div; + } + + async deleteTheme(themeId) { + if (!confirm('Are you sure you want to delete this theme?')) return; + + try { + const fbUser = authManager.user; + if (!fbUser) throw new Error('Not authenticated'); + + await this.pb.collection('themes').delete(themeId, { f_id: fbUser.uid }); + alert('Theme deleted successfully.'); + this.loadThemes(); + } catch (err) { + console.error('Failed to delete theme:', err); + alert('Failed to delete theme. You might not have permission.'); + } + } + + openThemeDetails(theme) { + const detailsView = document.getElementById('theme-store-details'); + const browseView = document.getElementById('theme-store-browse'); + const tabs = this.modal.querySelector('.search-tabs'); + + + document.getElementById('theme-details-name').textContent = theme.name; + + const authorName = theme.expand?.author?.username || theme.expand?.author?.display_name || theme.authorName || 'Unknown'; + const authorEl = document.getElementById('theme-details-author'); + + if (theme.expand?.author?.username) { + authorEl.innerHTML = `by ${this.escapeHtml(authorName)}`; + authorEl.querySelector('span').onclick = () => { + this.modal.classList.remove('active'); + navigate(`/user/@${theme.expand.author.username}`); + }; + } else { + authorEl.textContent = `by ${authorName}`; + } + + document.getElementById('theme-details-created').textContent = new Date(theme.created).toLocaleDateString(); + document.getElementById('theme-details-updated').textContent = new Date(theme.updated).toLocaleDateString(); + document.getElementById('theme-details-installs').textContent = theme.installs || 0; + document.getElementById('theme-details-desc').textContent = theme.description || 'No description provided.'; + + + const applyBtn = document.getElementById('theme-details-apply-btn'); + applyBtn.onclick = async () => { + this.applyTheme(theme); + this.modal.classList.remove('active'); + + try { + const latest = await this.pb.collection('themes').getOne(theme.id); + await this.pb.collection('themes').update(theme.id, { + installs: (latest.installs || 0) + 1 + }); + } catch (e) { + console.warn('Failed to update theme installs:', e); + } + }; + + + const previewContainer = document.getElementById('theme-details-preview-container'); + previewContainer.innerHTML = ''; + this.detailsPreviewShadow = previewContainer.attachShadow({ mode: 'open' }); + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = '/styles.css'; + this.detailsPreviewShadow.appendChild(link); + + const styleTag = document.createElement('style'); + styleTag.textContent = theme.css.replace(/:root/g, ':host'); + this.detailsPreviewShadow.appendChild(styleTag); + + const wrapper = document.createElement('div'); + wrapper.className = 'preview-content'; + wrapper.style.padding = '1rem'; + wrapper.style.height = '100%'; + wrapper.style.background = 'var(--background)'; + wrapper.style.color = 'var(--foreground)'; + wrapper.style.overflow = 'hidden'; + wrapper.innerHTML = ` +
+
+
Preview
+
Subtitle
+
+ + `; + this.detailsPreviewShadow.appendChild(wrapper); + + + browseView.style.display = 'none'; + tabs.style.display = 'none'; + detailsView.style.display = 'flex'; + } + + closeThemeDetails() { + const detailsView = document.getElementById('theme-store-details'); + const browseView = document.getElementById('theme-store-browse'); + const tabs = this.modal.querySelector('.search-tabs'); + + detailsView.style.display = 'none'; + browseView.style.display = 'block'; + tabs.style.display = 'flex'; + + + document.getElementById('theme-details-preview-container').innerHTML = ''; + } + + extractPreviewStyles(css) { + const vars = ['--background', '--foreground', '--primary', '--card', '--border', '--muted-foreground']; + let style = ''; + vars.forEach(v => { + const regex = new RegExp(`${v}\\s*:\\s*([^;]+)`); + const match = css.match(regex); + if (match) { + style += `${v}: ${match[1]}; `; + } + }); + return style; + } + + applyTheme(theme) { + let css = theme.css; + if (!css && typeof theme === 'string') { + css = theme; + theme = { name: 'Custom Theme', authorName: 'Unknown' }; + } + + localStorage.setItem('custom_theme_css', css); + localStorage.setItem('monochrome-theme', 'custom'); + + const metadata = { + id: theme.id, + name: theme.name, + author: theme.authorName || theme.expand?.author?.username || theme.expand?.author?.display_name || 'Unknown', + }; + localStorage.setItem('community-theme', JSON.stringify(metadata)); + + let styleEl = document.getElementById('custom-theme-style'); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = 'custom-theme-style'; + document.head.appendChild(styleEl); + } + + const fontMatch = css.match(/--font-family:\s*([^;}]+)/); + const urlMatch = css.match(/--font-url:\s*([^;}]+)/); + + if (fontMatch && fontMatch[1]) { + const fontFamilyValue = fontMatch[1].trim(); + const mainFont = fontFamilyValue.split(',')[0].trim().replace(/['"]/g, ''); + + const genericFamilies = [ + 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', + 'system-ui', 'inter', 'ibm plex mono', 'roboto', 'open sans', + 'lato', 'montserrat', 'poppins', 'apple music', 'sf pro display', + 'courier new', 'times new roman', 'arial', 'helvetica', 'verdana', + 'tahoma', 'trebuchet ms', 'impact', 'gill sans' + ]; + const isPresetOrGeneric = genericFamilies.some(generic => mainFont.toLowerCase() === generic); + + if (!isPresetOrGeneric) { + const FONT_LINK_ID = 'monochrome-dynamic-font'; + let link = document.getElementById(FONT_LINK_ID); + + if (urlMatch && urlMatch[1]) { + const customUrl = urlMatch[1].trim().replace(/['"]/g, ''); + console.log(`Applying custom font URL: ${customUrl}`); + + if (customUrl.match(/\.(css)$/i) || customUrl.includes('fonts.googleapis.com')) { + if (!link) { + link = document.createElement('link'); + link.id = FONT_LINK_ID; + link.rel = 'stylesheet'; + document.head.appendChild(link); + } + link.href = customUrl; + } else { + if (link) link.remove(); + const fontFace = ` +@font-face { + font-family: '${mainFont}'; + src: url('${customUrl}'); + font-weight: 100 900; + font-display: swap; +} +`; + css = fontFace + css; + } + } else { + console.log(`Applying custom font from theme (Google Fonts): ${mainFont}`); + const encodedFamily = encodeURIComponent(mainFont); + const url = `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@100;200;300;400;500;600;700;800;900&display=swap`; + + if (!link) { + link = document.createElement('link'); + link.id = FONT_LINK_ID; + link.rel = 'stylesheet'; + document.head.appendChild(link); + } + link.href = url; + } + } + } + + styleEl.textContent = css; + + + const root = document.documentElement; + ['background', 'foreground', 'primary', 'secondary', 'muted', 'border', 'highlight', 'font-family'].forEach(key => { + root.style.removeProperty(`--${key}`); + }); + root.setAttribute('data-theme', 'custom'); + + document.querySelectorAll('.theme-option').forEach(el => el.classList.remove('active')); + document.querySelector('[data-theme="custom"]')?.classList.add('active'); + + document.documentElement.style.display = 'none'; + document.documentElement.offsetHeight; + document.documentElement.style.display = ''; + + window.dispatchEvent(new CustomEvent('theme-changed', { detail: { theme: 'custom' } })); + } + + async checkAuth() { + if (this._isCheckingAuth) return; + this._isCheckingAuth = true; + + + const isLoggedIn = !!authManager?.user; + + const authMessage = document.getElementById('theme-upload-auth-message'); + const form = document.getElementById('theme-upload-form'); + const websiteInput = document.getElementById('theme-upload-website'); + const websiteContainer = websiteInput?.parentElement; + + if (isLoggedIn) { + authMessage.style.display = 'none'; + form.style.display = 'block'; + + try { + const userData = await syncManager.getUserData(); + if (userData?.profile?.username && websiteContainer) { + websiteContainer.style.display = 'none'; + } else if (websiteContainer) { + websiteContainer.style.display = 'block'; + } + } catch (e) { + console.warn('Failed to check profile for website input visibility', e); + } + } else { + authMessage.style.display = 'flex'; + form.style.display = 'none'; + } + + this._isCheckingAuth = false; + } + + async handleUpload(e) { + e.preventDefault(); + + const name = document.getElementById('theme-upload-name').value; + const desc = document.getElementById('theme-upload-desc').value; + const css = document.getElementById('theme-upload-css').value; + const website = document.getElementById('theme-upload-website').value; + + const fbUser = authManager?.user; + if (!fbUser) { + alert('You must be logged in to upload themes.'); + return; + } + + let userId = null; + let userName = null; + + try { + + const dbUser = await syncManager._getUserRecord(fbUser.uid); + if (!dbUser) { + throw new Error('Could not find or create your user record. Please try again.'); + } + + userId = dbUser.id; + userName = dbUser.username || dbUser.display_name || fbUser.email; + + if (userId.length !== 15) { + throw new Error( + `Your user ID is corrupted (${userId.length} chars, expected 15). ` + + `Please go to Settings > System > Clear Cloud Data, then log out and back in.` + ); + } + + console.log('Uploading theme:', { name, author: userId, authorName: userName }); + + const formData = new FormData(); + formData.append('name', name); + formData.append('description', desc); + formData.append('css', css); + formData.append('author', userId); + formData.append('authorName', userName); + if (website) formData.append('authorUrl', website); + + + await this.pb.collection('themes').create(formData, { f_id: fbUser.uid }); + + alert('Theme uploaded successfully!'); + e.target.reset(); + + + const previewWindow = document.getElementById('theme-preview-window'); + const togglePreviewBtn = document.getElementById('te-toggle-preview'); + if (previewWindow) previewWindow.style.display = 'none'; + if (togglePreviewBtn) { + togglePreviewBtn.textContent = 'Preview'; + togglePreviewBtn.classList.remove('active'); + } + + this.modal.querySelector('[data-tab="browse"]').click(); + this.loadThemes(); + + } catch (err) { + console.error('Upload failed:', err); + console.error('Response data:', err.data); + + const responseData = err.data?.data || {}; + + if (Object.keys(responseData).length > 0) { + let msg = 'Failed to upload theme:\n'; + for (const [key, value] of Object.entries(responseData)) { + msg += `• ${key}: ${value.message}\n`; + } + alert(msg); + } else { + const message = err.message || err.data?.message || 'Unknown error'; + const debugInfo = `\n\nDebug: User ID: ${userId} (${userId?.length} chars) | Status: ${err.status}`; + alert(`Failed to upload theme: ${message}${debugInfo}`); + } + } + } + + escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + setupEditorTools() { + const cssInput = document.getElementById('theme-upload-css'); + const insertTemplateBtn = document.getElementById('te-insert-template'); + const togglePreviewBtn = document.getElementById('te-toggle-preview'); + const previewWindow = document.getElementById('theme-preview-window'); + + const colorMap = { + 'te-bg-color': '--background', + 'te-fg-color': '--foreground', + 'te-primary-color': '--primary', + 'te-sec-color': '--secondary', + 'te-accent-color': '--highlight', + 'te-card-color': '--card', + 'te-border-color': '--border', + 'te-muted-color': '--muted-foreground' + }; + + Object.entries(colorMap).forEach(([id, variable]) => { + document.getElementById(id)?.addEventListener('input', (e) => { + this.updateCssVariable(cssInput, variable, e.target.value); + this.updatePreview(); + }); + }); + + const styleMap = { + 'te-font-family': '--font-family', + 'te-radius': '--radius' + }; + + Object.entries(styleMap).forEach(([id, variable]) => { + document.getElementById(id)?.addEventListener('change', (e) => { + if (e.target.value) { + this.updateCssVariable(cssInput, variable, e.target.value); + this.updatePreview(); + e.target.value = ""; + } + }); + }); + + document.getElementById('te-font-custom')?.addEventListener('input', (e) => { + this.updateCssVariable(cssInput, '--font-family', e.target.value); + this.updatePreview(); + }); + + insertTemplateBtn?.addEventListener('click', () => { + if (cssInput.value.trim() && !confirm('Overwrite current CSS with template?')) return; + cssInput.value = `:root { + /* Base Colors */ + --background: #0a0a0a; + --foreground: #ededed; + + /* UI Elements */ + --card: #1a1a1a; + --card-foreground: #ededed; + --border: #2a2a2a; + + /* Accents */ + --primary: #3b82f6; + --primary-foreground: #ffffff; + --secondary: #2a2a2a; + --secondary-foreground: #ededed; + + /* Text */ + --muted: #2a2a2a; + --muted-foreground: #a0a0a0; + + /* Special */ + --highlight: #3b82f6; + --ring: #3b82f6; + --radius: 8px; + --font-family: 'Inter', sans-serif; + --font-size-scale: 100%; +}`; + this.updatePreview(); + }); + + togglePreviewBtn?.addEventListener('click', () => { + const isVisible = previewWindow.style.display !== 'none'; + if (isVisible) { + previewWindow.style.display = 'none'; + togglePreviewBtn.textContent = 'Preview'; + togglePreviewBtn.classList.remove('active'); + } else { + previewWindow.style.display = 'flex'; + togglePreviewBtn.textContent = 'Close Preview'; + togglePreviewBtn.classList.add('active'); + this.initPreviewWindow(); + this.updatePreview(); + } + }); + + cssInput?.addEventListener('input', () => this.updatePreview()); + } + + updateCssVariable(textarea, variable, value) { + let css = textarea.value; + const regex = new RegExp(`${variable}:\\s*[^;\\}]+(?:;|(?=\\}))`, 'g'); + const newLine = `${variable}: ${value};`; + + if (regex.test(css)) { + css = css.replace(regex, newLine); + } else { + if (css.includes(':root {')) { + css = css.replace(':root {', `:root {\n ${newLine}`); + } else { + css += `\n:root {\n ${newLine}\n}`; + } + } + textarea.value = css; + } + + initPreviewWindow() { + const container = document.getElementById('theme-preview-window'); + if (!this.previewShadow) { + this.previewShadow = container.attachShadow({ mode: 'open' }); + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = '/styles.css'; + this.previewShadow.appendChild(link); + + this.previewStyleTag = document.createElement('style'); + this.previewShadow.appendChild(this.previewStyleTag); + + const wrapper = document.createElement('div'); + wrapper.className = 'preview-content'; + wrapper.style.padding = '1rem'; + wrapper.style.height = '100%'; + wrapper.style.background = 'var(--background)'; + wrapper.style.color = 'var(--foreground)'; + wrapper.style.overflow = 'auto'; + + wrapper.innerHTML = ` +

Preview

+
+
+
Card Title
+
Subtitle
+
+ + +

Muted text example.

+ `; + this.previewShadow.appendChild(wrapper); + } + } + + updatePreview() { + if (!this.previewShadow || !this.previewStyleTag) return; + const css = document.getElementById('theme-upload-css').value; + const scopedCss = css.replace(/:root/g, ':host'); + this.previewStyleTag.textContent = scopedCss; + } +} \ No newline at end of file diff --git a/styles.css b/styles.css index aba77d4..46170e0 100644 --- a/styles.css +++ b/styles.css @@ -7558,3 +7558,100 @@ textarea:focus { text-align: left; justify-content: flex-start; } + +.theme-card-preview { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + border-radius: var(--radius); + overflow: hidden; + border: 1px solid var(--border); +} + +.theme-card-preview-header { + height: 30%; + border-bottom: 1px solid var(--border); +} + +.theme-card-preview-body { + flex: 1; + padding: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.theme-card-preview-line { + height: 4px; + border-radius: 2px; + width: 100%; + opacity: 0.5; +} + +.theme-editor-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--secondary); + padding: 0.5rem; + border: 1px solid var(--border); + border-bottom: none; + border-radius: var(--radius) var(--radius) 0 0; + flex-wrap: wrap; + gap: 0.5rem; +} + +.theme-editor-toolbar .color-picker-group { + display: flex; + gap: 0.25rem; +} + +.theme-editor-toolbar .style-picker-group { + display: flex; + gap: 0.25rem; +} + +.theme-editor-toolbar select { + height: 24px; + padding: 0 4px; + font-size: 0.75rem; + background-color: var(--input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--foreground); + cursor: pointer; +} + +.theme-editor-toolbar input[type="color"] { + width: 24px; + height: 24px; + padding: 0; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + background: none; +} + +#theme-upload-css { + border-top-left-radius: 0; + border-top-right-radius: 0; + max-width: 100%; +} + +#theme-preview-window { + position: fixed; + bottom: 20px; + right: 20px; + width: 300px; + height: 400px; + background: var(--background); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-xl); + z-index: 10000; + overflow: hidden; + resize: both; + display: flex; + flex-direction: column; +}