feat(themes): Community Themes
This commit is contained in:
parent
77d99245c8
commit
29b4899089
6 changed files with 1086 additions and 0 deletions
77
THEME_GUIDE.md
Normal file
77
THEME_GUIDE.md
Normal file
|
|
@ -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!
|
||||
141
index.html
141
index.html
|
|
@ -1258,6 +1258,129 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="theme-store-modal" class="modal">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content wide" style="height: 80vh; display: flex; flex-direction: column; max-height: 80vh;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="margin: 0;">Theme Store</h3>
|
||||
<button class="close-modal-btn" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color:var(--muted-foreground);">×</button>
|
||||
</div>
|
||||
|
||||
<div class="search-tabs" style="margin-bottom: 1rem;">
|
||||
<button class="search-tab active" data-tab="browse">Browse</button>
|
||||
<button class="search-tab" data-tab="upload">Upload</button>
|
||||
</div>
|
||||
|
||||
<div id="theme-store-details" style="display: none; flex-direction: column; height: 100%; overflow: hidden;">
|
||||
<button class="btn-secondary" id="theme-details-back-btn" style="align-self: flex-start; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Back
|
||||
</button>
|
||||
<div style="flex: 1; overflow-y: auto; padding-right: 0.5rem;">
|
||||
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 2rem;">
|
||||
<div id="theme-details-preview-container" style="width: 300px; height: 220px; flex-shrink: 0; border: 1px solid var(--border); border-radius: var(--radius);"></div>
|
||||
<div style="flex: 1; min-width: 250px;">
|
||||
<h2 id="theme-details-name" style="font-size: 2rem; margin-bottom: 0.5rem;"></h2>
|
||||
<div class="meta" style="color: var(--muted-foreground); font-size: 0.9rem; margin-bottom: 1rem; line-height: 1.6;">
|
||||
<div id="theme-details-author"></div>
|
||||
<div>Created: <span id="theme-details-created"></span> - Updated: <span id="theme-details-updated"></span></div>
|
||||
<div>Installs: <span id="theme-details-installs">0</span></div>
|
||||
</div>
|
||||
<button id="theme-details-apply-btn" class="btn-primary" style="width: 100%; max-width: 200px;">Apply Theme</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h3 style="font-size: 1.2rem; margin-bottom: 0.5rem;">Description</h3>
|
||||
<p id="theme-details-desc" style="white-space: pre-wrap; color: var(--foreground); line-height: 1.6;"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="theme-store-browse" class="search-tab-content active" style="flex: 1; overflow-y: auto; min-height: 0;">
|
||||
<div class="track-list-search-container" style="margin: 0 0 1rem 0;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="search-icon"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<input type="search" id="theme-store-search" placeholder="Search themes..." class="track-list-search-input" autocomplete="off">
|
||||
</div>
|
||||
<div id="community-themes-grid" class="card-grid">
|
||||
</div>
|
||||
<div id="theme-store-loading" style="text-align: center; padding: 2rem; display: none;">
|
||||
<div class="animate-spin" style="display: inline-block;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="theme-store-upload" class="search-tab-content" style="flex: 1; overflow-y: auto; min-height: 0;">
|
||||
<div id="theme-upload-auth-message" style="display: none; text-align: center; padding: 2rem; flex-direction: column; align-items: center; gap: 1rem;">
|
||||
<p>You need to be logged in to upload themes.</p>
|
||||
<button class="btn-primary" id="theme-store-login-btn">Go to Login</button>
|
||||
</div>
|
||||
<form id="theme-upload-form">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Theme Name</label>
|
||||
<input type="text" id="theme-upload-name" class="template-input" placeholder="My Awesome Theme" required>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Description</label>
|
||||
<textarea id="theme-upload-desc" class="template-input" placeholder="Describe your theme..." style="min-height: 80px; resize: vertical;"></textarea>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Author Website (Optional)</label>
|
||||
<input type="url" id="theme-upload-website" class="template-input" placeholder="https://example.com">
|
||||
<p style="font-size: 0.8rem; color: var(--muted-foreground); margin-top: 0.25rem;">
|
||||
It is recommended to create a Monochrome profile instead.
|
||||
</p>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">CSS</label>
|
||||
<p style="font-size: 0.8rem; color: var(--muted-foreground); margin-bottom: 0.5rem;">
|
||||
Define your CSS variables or custom styles here. <a href="https://github.com/monochrome-music/monochrome/blob/main/THEME_GUIDE.md" target="_blank" style="text-decoration: underline; color: var(--primary);">Read the Theme Guide</a>.
|
||||
</p>
|
||||
<div class="theme-editor-toolbar">
|
||||
<div class="color-picker-group">
|
||||
<input type="color" id="te-bg-color" title="Background">
|
||||
<input type="color" id="te-fg-color" title="Foreground">
|
||||
<input type="color" id="te-primary-color" title="Primary">
|
||||
<input type="color" id="te-sec-color" title="Secondary">
|
||||
<input type="color" id="te-accent-color" title="Accent/Highlight">
|
||||
<input type="color" id="te-card-color" title="Card Background">
|
||||
<input type="color" id="te-border-color" title="Border Color">
|
||||
<input type="color" id="te-muted-color" title="Muted Text">
|
||||
</div>
|
||||
<div class="style-picker-group">
|
||||
<select id="te-font-family" title="Font Family">
|
||||
<option value="">Font...</option>
|
||||
<option value="'Inter', sans-serif">Inter</option>
|
||||
<option value="system-ui, -apple-system, sans-serif">System</option>
|
||||
<option value="'Courier New', monospace">Mono</option>
|
||||
<option value="'Times New Roman', serif">Serif</option>
|
||||
</select>
|
||||
<input type="text" id="te-font-custom" placeholder="Custom Font..." style="height: 24px; padding: 0 4px; font-size: 0.75rem; width: 100px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--input); color: var(--foreground);">
|
||||
<select id="te-radius" title="Border Radius">
|
||||
<option value="">Radius...</option>
|
||||
<option value="0px">Square</option>
|
||||
<option value="4px">Small</option>
|
||||
<option value="8px">Medium</option>
|
||||
<option value="16px">Large</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<button type="button" id="te-insert-template" class="btn-secondary" style="padding: 2px 8px; font-size: 0.8rem;">Template</button>
|
||||
<button type="button" id="te-toggle-preview" class="btn-secondary" style="padding: 2px 8px; font-size: 0.8rem;">Preview</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="theme-upload-css" class="template-input" style="min-height: 200px; font-family: monospace; white-space: pre;" placeholder=":root { --background: #000000; --foreground: #ffffff; /* ... */ }" required></textarea>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="submit" class="btn-primary">Upload Theme</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="theme-preview-window" style="display: none;"></div>
|
||||
|
||||
<div id="tracker-modal" class="modal tracker-modal">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content wide" style="max-height: 85vh; display: flex; flex-direction: column">
|
||||
|
|
@ -2974,6 +3097,17 @@
|
|||
<div class="theme-option" data-theme="latte">Latte</div>
|
||||
<div class="theme-option" data-theme="custom">Custom</div>
|
||||
</div>
|
||||
<div id="applied-community-theme-container" style="display: none; margin-top: 1rem;">
|
||||
<button id="applied-community-theme-btn" class="btn-secondary" style="width: 100%; display: flex; justify-content: space-between; align-items: center; padding: 1rem;">
|
||||
<span>Applied Community Theme</span>
|
||||
<span id="applied-theme-name" style="color: var(--muted-foreground); font-size: 0.9rem;"></span>
|
||||
</button>
|
||||
<div id="community-theme-details-panel" style="display: none; padding: 1rem; background: var(--secondary); border-radius: var(--radius); margin-top: 0.5rem; border: 1px solid var(--border);">
|
||||
<div style="margin-bottom: 0.5rem; font-weight: 600;" id="ct-details-title"></div>
|
||||
<div style="margin-bottom: 1rem; font-size: 0.9rem; color: var(--muted-foreground);" id="ct-details-author"></div>
|
||||
<button id="ct-unapply-btn" class="btn-secondary danger" style="width: 100%;">Unapply Theme</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-theme-editor" id="custom-theme-editor">
|
||||
<h4>Custom Theme</h4>
|
||||
<div class="theme-color-grid" id="theme-color-grid"></div>
|
||||
|
|
@ -2982,6 +3116,13 @@
|
|||
<button class="btn-secondary" id="reset-custom-theme">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Community Themes</span>
|
||||
<span class="description">Browse and apply themes created by the community</span>
|
||||
</div>
|
||||
<button id="open-theme-store-btn" class="btn-secondary">Open Theme Store</button>
|
||||
</div>
|
||||
<div class="setting-item font-settings-container">
|
||||
<div class="info">
|
||||
<span class="label">Font</span>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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() || {
|
||||
|
|
|
|||
713
js/themeStore.js
Normal file
713
js/themeStore.js
Normal file
|
|
@ -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 = '<div class="empty-state">No themes found.</div>';
|
||||
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 = '<div class="empty-state">Failed to load themes.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
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 = `<span class="author-link" style="cursor: pointer; text-decoration: underline;">${this.escapeHtml(authorName)}</span>`;
|
||||
} else if (theme.authorUrl) {
|
||||
authorHtml = `<a href="${this.escapeHtml(theme.authorUrl)}" target="_blank" style="color: inherit; text-decoration: underline;" onclick="event.stopPropagation();">${this.escapeHtml(authorName)}</a>`;
|
||||
}
|
||||
|
||||
let deleteBtnHtml = '';
|
||||
if (currentUserId && theme.author === currentUserId) {
|
||||
deleteBtnHtml = `
|
||||
<button class="btn-icon delete-theme-btn" title="Delete Theme" style="position: absolute; top: 0.5rem; right: 0.5rem; background: rgba(0,0,0,0.6); color: white; border-radius: 50%; padding: 0.25rem; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border: none; cursor: pointer; z-index: 10;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
const previewStyle = this.extractPreviewStyles(theme.css);
|
||||
const previewHtml = `
|
||||
<div class="theme-card-preview" style="${previewStyle}; height: 140px; position: relative;">
|
||||
<div class="theme-card-preview-header" style="background-color: var(--card); border-bottom: 1px solid var(--border);"></div>
|
||||
<div class="theme-card-preview-body" style="background-color: var(--background);">
|
||||
<div class="theme-card-preview-line" style="background-color: var(--foreground); width: 80%;"></div>
|
||||
<div class="theme-card-preview-line" style="background-color: var(--muted-foreground); width: 60%;"></div>
|
||||
<div class="theme-card-preview-line" style="background-color: var(--primary); width: 40%; margin-top: auto;"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
div.innerHTML = `
|
||||
<div style="position: relative;">
|
||||
${deleteBtnHtml}
|
||||
${previewHtml}
|
||||
</div>
|
||||
<div class="card-info" style="margin-top: 0.75rem;">
|
||||
<div class="card-title">${this.escapeHtml(theme.name)}</div>
|
||||
<div class="card-subtitle">by ${authorHtml}</div>
|
||||
<p style="font-size: 0.8rem; color: var(--muted-foreground); margin-top: 0.25rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">
|
||||
${this.escapeHtml(shortDesc)}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 <span style="cursor: pointer; text-decoration: underline; color: var(--primary);">${this.escapeHtml(authorName)}</span>`;
|
||||
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 = `
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<div style="height: 60px; background: var(--muted); border-radius: var(--radius); margin-bottom: 0.5rem;"></div>
|
||||
<div class="card-title">Preview</div>
|
||||
<div class="card-subtitle">Subtitle</div>
|
||||
</div>
|
||||
<button class="btn-primary" style="margin-bottom: 0.5rem; width: 100%;">Button</button>
|
||||
`;
|
||||
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 = `
|
||||
<h3 style="margin-top: 0;">Preview</h3>
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<div style="height: 100px; background: var(--muted); border-radius: var(--radius); margin-bottom: 0.5rem;"></div>
|
||||
<div class="card-title">Card Title</div>
|
||||
<div class="card-subtitle">Subtitle</div>
|
||||
</div>
|
||||
<button class="btn-primary" style="margin-bottom: 0.5rem;">Primary Button</button>
|
||||
<button class="btn-secondary">Secondary Button</button>
|
||||
<p style="color: var(--muted-foreground);">Muted text example.</p>
|
||||
`;
|
||||
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;
|
||||
}
|
||||
}
|
||||
97
styles.css
97
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue