i18n batch 2b: wire designer.js (~80 keys)

- All 12 element types (text, heading, image, video, clock, date,
  weather, ticker, shape, qr, countdown, webpage)
- Background swatches, properties panel, layers list
- Translated prompts for video/weather/RSS/QR/countdown/webpage URLs
- Toasts for publish, export, load, invalid file
- 612 keys total, parity 100% across en/es/fr/de/pt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-04-29 19:57:12 -05:00
parent 0743901e48
commit 103803fb92
6 changed files with 471 additions and 83 deletions

View file

@ -549,4 +549,80 @@ export default {
'widget.dir.remove_logo': 'Entfernen', 'widget.dir.remove_logo': 'Entfernen',
'widget.dir.no_bg_images': 'Keine Hintergrundbilder ausgewählt', 'widget.dir.no_bg_images': 'Keine Hintergrundbilder ausgewählt',
'widget.dir.remove_bg': 'Entfernen', 'widget.dir.remove_bg': 'Entfernen',
// Designer
'designer.title': 'Inhaltsdesigner',
'designer.subtitle': 'Erstellen Sie dynamischen Anzeigeinhalt',
'designer.help_tip': 'Erstellen Sie individuelle Anzeigen mit Live-Elementen: Uhren, Wetter, RSS-Ticker, Countdowns, QR-Codes. Als Widget veröffentlichen oder als PNG exportieren.',
'designer.load_design': 'Design laden',
'designer.export_png': 'PNG exportieren',
'designer.publish': 'In Bibliothek veröffentlichen',
'designer.preview_hint': 'Klicken Sie auf Elemente zur Auswahl. Ziehen zum Verschieben. Live-Vorschau aktualisiert in Echtzeit.',
'designer.add_element': 'Element hinzufügen',
'designer.background': 'Hintergrund',
'designer.bg_image': 'Bild',
'designer.properties': 'Eigenschaften',
'designer.layers': 'Ebenen',
'designer.no_elements': 'Noch keine Elemente',
'designer.save_design_file': 'Design-Datei speichern',
'designer.qr_label': 'QR-CODE',
'designer.loading_news': 'Nachrichten werden geladen...',
'designer.no_items': 'Keine Einträge',
'designer.feed_unavailable': 'Feed nicht verfügbar',
'designer.countdown_now': 'JETZT!',
'designer.widget_name': 'Design {date}',
'designer.el.text': 'Text',
'designer.el.heading': 'Überschrift',
'designer.el.image': 'Bild',
'designer.el.video': 'Video',
'designer.el.clock': 'Uhr',
'designer.el.date': 'Datum',
'designer.el.weather': 'Wetter',
'designer.el.ticker': 'Ticker',
'designer.el.shape': 'Form',
'designer.el.qr': 'QR-Code',
'designer.el.countdown': 'Countdown',
'designer.el.webpage': 'Webseite',
'designer.bg.black': 'Schwarz',
'designer.bg.dark_blue': 'Dunkelblau',
'designer.bg.dark_gradient': 'Dunkler Verlauf',
'designer.bg.blue_gradient': 'Blauer Verlauf',
'designer.bg.sunset': 'Sonnenuntergang',
'designer.bg.ocean': 'Ozean',
'designer.bg.forest': 'Wald',
'designer.bg.dark_red': 'Dunkelrot',
'designer.bg.white': 'Weiß',
'designer.default.text': 'Ihr Text hier',
'designer.default.heading': 'ÜBERSCHRIFT',
'designer.default.coming_soon': 'Demnächst',
'designer.prompt.video_url': 'Video-URL (MP4):',
'designer.prompt.weather_location': 'Stadt, Land:',
'designer.prompt.rss_url': 'RSS-Feed-URL:',
'designer.prompt.qr_url': 'QR-Code-URL:',
'designer.prompt.countdown_date': 'Zieldatum (JJJJ-MM-TT):',
'designer.prompt.webpage_url': 'Webseiten-URL:',
'designer.prop.text': 'Text',
'designer.prop.size': 'Größe',
'designer.prop.font': 'Schriftart',
'designer.prop.color': 'Farbe',
'designer.prop.bold': 'Fett',
'designer.prop.shadow': 'Schatten',
'designer.prop.format': 'Format',
'designer.prop.show_seconds': 'Sekunden anzeigen',
'designer.prop.muted': 'Stumm',
'designer.prop.loop': 'Schleife',
'designer.prop.opacity': 'Deckkraft',
'designer.prop.shape': 'Form',
'designer.prop.location': 'Standort',
'designer.prop.feed_url': 'Feed-URL',
'designer.prop.speed': 'Geschwindigkeit (Sekunden)',
'designer.prop.text_color': 'Textfarbe',
'designer.prop.bg_color': 'Hintergrundfarbe',
'designer.prop.target_date': 'Zieldatum',
'designer.prop.label': 'Beschriftung',
'designer.toast.published': 'Als Widget veröffentlicht! Weisen Sie es einer Layout-Zone zu.',
'designer.toast.publish_failed': 'Veröffentlichung fehlgeschlagen',
'designer.toast.export_failed': 'Export fehlgeschlagen: {error}',
'designer.toast.loaded': 'Design geladen',
'designer.toast.invalid_file': 'Ungültige Design-Datei',
}; };

View file

@ -580,4 +580,85 @@ export default {
'widget.dir.remove_logo': 'Remove', 'widget.dir.remove_logo': 'Remove',
'widget.dir.no_bg_images': 'No background images selected', 'widget.dir.no_bg_images': 'No background images selected',
'widget.dir.remove_bg': 'Remove', 'widget.dir.remove_bg': 'Remove',
// Designer
'designer.title': 'Content Designer',
'designer.subtitle': 'Create dynamic signage content',
'designer.help_tip': 'Create custom signage with live elements: clocks, weather, RSS tickers, countdowns, QR codes. Publish as a widget or export as PNG.',
'designer.load_design': 'Load Design',
'designer.export_png': 'Export PNG',
'designer.publish': 'Publish to Library',
'designer.preview_hint': 'Click elements to select. Drag to reposition. Live preview updates in real-time.',
'designer.add_element': 'Add Element',
'designer.background': 'Background',
'designer.bg_image': 'Image',
'designer.properties': 'Properties',
'designer.layers': 'Layers',
'designer.no_elements': 'No elements yet',
'designer.save_design_file': 'Save Design File',
'designer.qr_label': 'QR CODE',
'designer.loading_news': 'Loading news...',
'designer.no_items': 'No items',
'designer.feed_unavailable': 'Feed unavailable',
'designer.countdown_now': 'NOW!',
'designer.widget_name': 'Design {date}',
// Element buttons
'designer.el.text': 'Text',
'designer.el.heading': 'Heading',
'designer.el.image': 'Image',
'designer.el.video': 'Video',
'designer.el.clock': 'Clock',
'designer.el.date': 'Date',
'designer.el.weather': 'Weather',
'designer.el.ticker': 'Ticker',
'designer.el.shape': 'Shape',
'designer.el.qr': 'QR Code',
'designer.el.countdown': 'Countdown',
'designer.el.webpage': 'Webpage',
// Backgrounds
'designer.bg.black': 'Black',
'designer.bg.dark_blue': 'Dark Blue',
'designer.bg.dark_gradient': 'Dark Gradient',
'designer.bg.blue_gradient': 'Blue Gradient',
'designer.bg.sunset': 'Sunset',
'designer.bg.ocean': 'Ocean',
'designer.bg.forest': 'Forest',
'designer.bg.dark_red': 'Dark Red',
'designer.bg.white': 'White',
// Defaults / prompts
'designer.default.text': 'Your text here',
'designer.default.heading': 'HEADING',
'designer.default.coming_soon': 'Coming Soon',
'designer.prompt.video_url': 'Video URL (MP4):',
'designer.prompt.weather_location': 'City, State:',
'designer.prompt.rss_url': 'RSS Feed URL:',
'designer.prompt.qr_url': 'QR Code URL:',
'designer.prompt.countdown_date': 'Target date (YYYY-MM-DD):',
'designer.prompt.webpage_url': 'Webpage URL:',
// Properties
'designer.prop.text': 'Text',
'designer.prop.size': 'Size',
'designer.prop.font': 'Font',
'designer.prop.color': 'Color',
'designer.prop.bold': 'Bold',
'designer.prop.shadow': 'Shadow',
'designer.prop.format': 'Format',
'designer.prop.show_seconds': 'Show seconds',
'designer.prop.muted': 'Muted',
'designer.prop.loop': 'Loop',
'designer.prop.opacity': 'Opacity',
'designer.prop.shape': 'Shape',
'designer.prop.location': 'Location',
'designer.prop.feed_url': 'Feed URL',
'designer.prop.speed': 'Speed (seconds)',
'designer.prop.text_color': 'Text Color',
'designer.prop.bg_color': 'BG Color',
'designer.prop.target_date': 'Target Date',
'designer.prop.label': 'Label',
// Toasts
'designer.toast.published': 'Published as widget! Assign it to a layout zone.',
'designer.toast.publish_failed': 'Publish failed',
'designer.toast.export_failed': 'Export failed: {error}',
'designer.toast.loaded': 'Design loaded',
'designer.toast.invalid_file': 'Invalid design file',
}; };

View file

@ -548,4 +548,80 @@ export default {
'widget.dir.remove_logo': 'Quitar', 'widget.dir.remove_logo': 'Quitar',
'widget.dir.no_bg_images': 'No se han seleccionado imágenes de fondo', 'widget.dir.no_bg_images': 'No se han seleccionado imágenes de fondo',
'widget.dir.remove_bg': 'Quitar', 'widget.dir.remove_bg': 'Quitar',
// Designer
'designer.title': 'Diseñador de contenido',
'designer.subtitle': 'Crea contenido dinámico para señalización',
'designer.help_tip': 'Crea señalización personalizada con elementos en vivo: relojes, clima, tickers RSS, cuentas regresivas, códigos QR. Publica como widget o exporta como PNG.',
'designer.load_design': 'Cargar diseño',
'designer.export_png': 'Exportar PNG',
'designer.publish': 'Publicar en biblioteca',
'designer.preview_hint': 'Haz clic en los elementos para seleccionar. Arrastra para reposicionar. La vista previa se actualiza en tiempo real.',
'designer.add_element': 'Agregar elemento',
'designer.background': 'Fondo',
'designer.bg_image': 'Imagen',
'designer.properties': 'Propiedades',
'designer.layers': 'Capas',
'designer.no_elements': 'Aún no hay elementos',
'designer.save_design_file': 'Guardar archivo de diseño',
'designer.qr_label': 'CÓDIGO QR',
'designer.loading_news': 'Cargando noticias...',
'designer.no_items': 'Sin elementos',
'designer.feed_unavailable': 'Feed no disponible',
'designer.countdown_now': '¡AHORA!',
'designer.widget_name': 'Diseño {date}',
'designer.el.text': 'Texto',
'designer.el.heading': 'Título',
'designer.el.image': 'Imagen',
'designer.el.video': 'Video',
'designer.el.clock': 'Reloj',
'designer.el.date': 'Fecha',
'designer.el.weather': 'Clima',
'designer.el.ticker': 'Ticker',
'designer.el.shape': 'Forma',
'designer.el.qr': 'Código QR',
'designer.el.countdown': 'Cuenta regresiva',
'designer.el.webpage': 'Página web',
'designer.bg.black': 'Negro',
'designer.bg.dark_blue': 'Azul oscuro',
'designer.bg.dark_gradient': 'Gradiente oscuro',
'designer.bg.blue_gradient': 'Gradiente azul',
'designer.bg.sunset': 'Atardecer',
'designer.bg.ocean': 'Océano',
'designer.bg.forest': 'Bosque',
'designer.bg.dark_red': 'Rojo oscuro',
'designer.bg.white': 'Blanco',
'designer.default.text': 'Tu texto aquí',
'designer.default.heading': 'TÍTULO',
'designer.default.coming_soon': 'Próximamente',
'designer.prompt.video_url': 'URL del video (MP4):',
'designer.prompt.weather_location': 'Ciudad, Estado:',
'designer.prompt.rss_url': 'URL del feed RSS:',
'designer.prompt.qr_url': 'URL del código QR:',
'designer.prompt.countdown_date': 'Fecha objetivo (AAAA-MM-DD):',
'designer.prompt.webpage_url': 'URL de la página web:',
'designer.prop.text': 'Texto',
'designer.prop.size': 'Tamaño',
'designer.prop.font': 'Fuente',
'designer.prop.color': 'Color',
'designer.prop.bold': 'Negrita',
'designer.prop.shadow': 'Sombra',
'designer.prop.format': 'Formato',
'designer.prop.show_seconds': 'Mostrar segundos',
'designer.prop.muted': 'Silenciado',
'designer.prop.loop': 'Bucle',
'designer.prop.opacity': 'Opacidad',
'designer.prop.shape': 'Forma',
'designer.prop.location': 'Ubicación',
'designer.prop.feed_url': 'URL del feed',
'designer.prop.speed': 'Velocidad (segundos)',
'designer.prop.text_color': 'Color del texto',
'designer.prop.bg_color': 'Color de fondo',
'designer.prop.target_date': 'Fecha objetivo',
'designer.prop.label': 'Etiqueta',
'designer.toast.published': '¡Publicado como widget! Asígnalo a una zona de diseño.',
'designer.toast.publish_failed': 'Falló la publicación',
'designer.toast.export_failed': 'Falló la exportación: {error}',
'designer.toast.loaded': 'Diseño cargado',
'designer.toast.invalid_file': 'Archivo de diseño no válido',
}; };

View file

@ -549,4 +549,80 @@ export default {
'widget.dir.remove_logo': 'Retirer', 'widget.dir.remove_logo': 'Retirer',
'widget.dir.no_bg_images': 'Aucune image de fond sélectionnée', 'widget.dir.no_bg_images': 'Aucune image de fond sélectionnée',
'widget.dir.remove_bg': 'Retirer', 'widget.dir.remove_bg': 'Retirer',
// Designer
'designer.title': 'Concepteur de contenu',
'designer.subtitle': 'Créez du contenu d\'affichage dynamique',
'designer.help_tip': 'Créez de l\'affichage personnalisé avec des éléments en direct : horloges, météo, tickers RSS, comptes à rebours, codes QR. Publiez comme widget ou exportez en PNG.',
'designer.load_design': 'Charger un design',
'designer.export_png': 'Exporter PNG',
'designer.publish': 'Publier dans la bibliothèque',
'designer.preview_hint': 'Cliquez sur les éléments pour les sélectionner. Glissez pour les repositionner. L\'aperçu se met à jour en temps réel.',
'designer.add_element': 'Ajouter un élément',
'designer.background': 'Fond',
'designer.bg_image': 'Image',
'designer.properties': 'Propriétés',
'designer.layers': 'Calques',
'designer.no_elements': 'Pas encore d\'éléments',
'designer.save_design_file': 'Enregistrer le fichier de design',
'designer.qr_label': 'CODE QR',
'designer.loading_news': 'Chargement des actualités...',
'designer.no_items': 'Aucun élément',
'designer.feed_unavailable': 'Flux indisponible',
'designer.countdown_now': 'MAINTENANT !',
'designer.widget_name': 'Design {date}',
'designer.el.text': 'Texte',
'designer.el.heading': 'Titre',
'designer.el.image': 'Image',
'designer.el.video': 'Vidéo',
'designer.el.clock': 'Horloge',
'designer.el.date': 'Date',
'designer.el.weather': 'Météo',
'designer.el.ticker': 'Bandeau',
'designer.el.shape': 'Forme',
'designer.el.qr': 'Code QR',
'designer.el.countdown': 'Compte à rebours',
'designer.el.webpage': 'Page web',
'designer.bg.black': 'Noir',
'designer.bg.dark_blue': 'Bleu sombre',
'designer.bg.dark_gradient': 'Dégradé sombre',
'designer.bg.blue_gradient': 'Dégradé bleu',
'designer.bg.sunset': 'Coucher de soleil',
'designer.bg.ocean': 'Océan',
'designer.bg.forest': 'Forêt',
'designer.bg.dark_red': 'Rouge sombre',
'designer.bg.white': 'Blanc',
'designer.default.text': 'Votre texte ici',
'designer.default.heading': 'TITRE',
'designer.default.coming_soon': 'Bientôt',
'designer.prompt.video_url': 'URL de la vidéo (MP4) :',
'designer.prompt.weather_location': 'Ville, Région :',
'designer.prompt.rss_url': 'URL du flux RSS :',
'designer.prompt.qr_url': 'URL du code QR :',
'designer.prompt.countdown_date': 'Date cible (AAAA-MM-JJ) :',
'designer.prompt.webpage_url': 'URL de la page web :',
'designer.prop.text': 'Texte',
'designer.prop.size': 'Taille',
'designer.prop.font': 'Police',
'designer.prop.color': 'Couleur',
'designer.prop.bold': 'Gras',
'designer.prop.shadow': 'Ombre',
'designer.prop.format': 'Format',
'designer.prop.show_seconds': 'Afficher les secondes',
'designer.prop.muted': 'Muet',
'designer.prop.loop': 'Boucle',
'designer.prop.opacity': 'Opacité',
'designer.prop.shape': 'Forme',
'designer.prop.location': 'Emplacement',
'designer.prop.feed_url': 'URL du flux',
'designer.prop.speed': 'Vitesse (secondes)',
'designer.prop.text_color': 'Couleur du texte',
'designer.prop.bg_color': 'Couleur de fond',
'designer.prop.target_date': 'Date cible',
'designer.prop.label': 'Étiquette',
'designer.toast.published': 'Publié comme widget ! Attribuez-le à une zone de mise en page.',
'designer.toast.publish_failed': 'Échec de la publication',
'designer.toast.export_failed': 'Échec de l\'export : {error}',
'designer.toast.loaded': 'Design chargé',
'designer.toast.invalid_file': 'Fichier de design invalide',
}; };

View file

@ -549,4 +549,80 @@ export default {
'widget.dir.remove_logo': 'Remover', 'widget.dir.remove_logo': 'Remover',
'widget.dir.no_bg_images': 'Nenhuma imagem de fundo selecionada', 'widget.dir.no_bg_images': 'Nenhuma imagem de fundo selecionada',
'widget.dir.remove_bg': 'Remover', 'widget.dir.remove_bg': 'Remover',
// Designer
'designer.title': 'Designer de conteúdo',
'designer.subtitle': 'Crie conteúdo dinâmico de sinalização',
'designer.help_tip': 'Crie sinalização personalizada com elementos ao vivo: relógios, clima, tickers RSS, contagens regressivas, códigos QR. Publique como widget ou exporte como PNG.',
'designer.load_design': 'Carregar design',
'designer.export_png': 'Exportar PNG',
'designer.publish': 'Publicar na biblioteca',
'designer.preview_hint': 'Clique em elementos para selecionar. Arraste para reposicionar. Pré-visualização atualiza em tempo real.',
'designer.add_element': 'Adicionar elemento',
'designer.background': 'Fundo',
'designer.bg_image': 'Imagem',
'designer.properties': 'Propriedades',
'designer.layers': 'Camadas',
'designer.no_elements': 'Sem elementos ainda',
'designer.save_design_file': 'Salvar arquivo de design',
'designer.qr_label': 'CÓDIGO QR',
'designer.loading_news': 'Carregando notícias...',
'designer.no_items': 'Sem itens',
'designer.feed_unavailable': 'Feed indisponível',
'designer.countdown_now': 'AGORA!',
'designer.widget_name': 'Design {date}',
'designer.el.text': 'Texto',
'designer.el.heading': 'Título',
'designer.el.image': 'Imagem',
'designer.el.video': 'Vídeo',
'designer.el.clock': 'Relógio',
'designer.el.date': 'Data',
'designer.el.weather': 'Clima',
'designer.el.ticker': 'Ticker',
'designer.el.shape': 'Forma',
'designer.el.qr': 'Código QR',
'designer.el.countdown': 'Contagem regressiva',
'designer.el.webpage': 'Página web',
'designer.bg.black': 'Preto',
'designer.bg.dark_blue': 'Azul escuro',
'designer.bg.dark_gradient': 'Gradiente escuro',
'designer.bg.blue_gradient': 'Gradiente azul',
'designer.bg.sunset': 'Pôr do sol',
'designer.bg.ocean': 'Oceano',
'designer.bg.forest': 'Floresta',
'designer.bg.dark_red': 'Vermelho escuro',
'designer.bg.white': 'Branco',
'designer.default.text': 'Seu texto aqui',
'designer.default.heading': 'TÍTULO',
'designer.default.coming_soon': 'Em breve',
'designer.prompt.video_url': 'URL do vídeo (MP4):',
'designer.prompt.weather_location': 'Cidade, Estado:',
'designer.prompt.rss_url': 'URL do feed RSS:',
'designer.prompt.qr_url': 'URL do código QR:',
'designer.prompt.countdown_date': 'Data alvo (AAAA-MM-DD):',
'designer.prompt.webpage_url': 'URL da página web:',
'designer.prop.text': 'Texto',
'designer.prop.size': 'Tamanho',
'designer.prop.font': 'Fonte',
'designer.prop.color': 'Cor',
'designer.prop.bold': 'Negrito',
'designer.prop.shadow': 'Sombra',
'designer.prop.format': 'Formato',
'designer.prop.show_seconds': 'Mostrar segundos',
'designer.prop.muted': 'Mudo',
'designer.prop.loop': 'Loop',
'designer.prop.opacity': 'Opacidade',
'designer.prop.shape': 'Forma',
'designer.prop.location': 'Local',
'designer.prop.feed_url': 'URL do feed',
'designer.prop.speed': 'Velocidade (segundos)',
'designer.prop.text_color': 'Cor do texto',
'designer.prop.bg_color': 'Cor de fundo',
'designer.prop.target_date': 'Data alvo',
'designer.prop.label': 'Rótulo',
'designer.toast.published': 'Publicado como widget! Atribua a uma zona de layout.',
'designer.toast.publish_failed': 'Falha ao publicar',
'designer.toast.export_failed': 'Falha ao exportar: {error}',
'designer.toast.loaded': 'Design carregado',
'designer.toast.invalid_file': 'Arquivo de design inválido',
}; };

View file

@ -1,16 +1,19 @@
import { api } from '../api.js'; import { api } from '../api.js';
import { showToast } from '../components/toast.js'; import { showToast } from '../components/toast.js';
import { t } from '../i18n.js';
// Background swatches: ids resolve to translated names; values are the actual
// CSS to apply.
const BACKGROUNDS = [ const BACKGROUNDS = [
{ name: 'Black', value: '#000000' }, { id: 'black', value: '#000000' },
{ name: 'Dark Blue', value: '#0f172a' }, { id: 'dark_blue', value: '#0f172a' },
{ name: 'Dark Gradient', value: 'linear-gradient(135deg, #0c0c0c 0%, #1a1a2e 50%, #16213e 100%)' }, { id: 'dark_gradient', value: 'linear-gradient(135deg, #0c0c0c 0%, #1a1a2e 50%, #16213e 100%)' },
{ name: 'Blue Gradient', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, { id: 'blue_gradient', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
{ name: 'Sunset', value: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }, { id: 'sunset', value: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
{ name: 'Ocean', value: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, { id: 'ocean', value: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
{ name: 'Forest', value: 'linear-gradient(135deg, #134e5e 0%, #71b280 100%)' }, { id: 'forest', value: 'linear-gradient(135deg, #134e5e 0%, #71b280 100%)' },
{ name: 'Dark Red', value: 'linear-gradient(135deg, #200122 0%, #6f0000 100%)' }, { id: 'dark_red', value: 'linear-gradient(135deg, #200122 0%, #6f0000 100%)' },
{ name: 'White', value: '#FFFFFF' }, { id: 'white', value: '#FFFFFF' },
]; ];
const FONTS = ['Arial', 'Helvetica', 'Georgia', 'Impact', 'Verdana', 'Trebuchet MS', 'Courier New', 'Times New Roman']; const FONTS = ['Arial', 'Helvetica', 'Georgia', 'Impact', 'Verdana', 'Trebuchet MS', 'Courier New', 'Times New Roman'];
@ -30,11 +33,11 @@ export function render(container) {
container.innerHTML = ` container.innerHTML = `
<div class="page-header"> <div class="page-header">
<div><h1>Content Designer <span class="help-tip" data-tip="Create custom signage with live elements: clocks, weather, RSS tickers, countdowns, QR codes. Publish as a widget or export as PNG.">?</span></h1><div class="subtitle">Create dynamic signage content</div></div> <div><h1>${t('designer.title')} <span class="help-tip" data-tip="${t('designer.help_tip')}">?</span></h1><div class="subtitle">${t('designer.subtitle')}</div></div>
<div style="display:flex;gap:8px"> <div style="display:flex;gap:8px">
<button class="btn btn-secondary" id="loadDesignBtn">Load Design</button> <button class="btn btn-secondary" id="loadDesignBtn">${t('designer.load_design')}</button>
<button class="btn btn-secondary" id="exportPngBtn">Export PNG</button> <button class="btn btn-secondary" id="exportPngBtn">${t('designer.export_png')}</button>
<button class="btn btn-primary" id="publishBtn">Publish to Library</button> <button class="btn btn-primary" id="publishBtn">${t('designer.publish')}</button>
</div> </div>
</div> </div>
<div style="display:flex;gap:20px"> <div style="display:flex;gap:20px">
@ -43,51 +46,51 @@ export function render(container) {
<div id="previewWrap" style="position:relative;border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;background:#000;aspect-ratio:16/9"> <div id="previewWrap" style="position:relative;border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;background:#000;aspect-ratio:16/9">
<div id="designPreview" style="position:relative;width:100%;height:100%;overflow:hidden"></div> <div id="designPreview" style="position:relative;width:100%;height:100%;overflow:hidden"></div>
</div> </div>
<p style="font-size:11px;color:var(--text-muted);margin-top:8px">Click elements to select. Drag to reposition. Live preview updates in real-time.</p> <p style="font-size:11px;color:var(--text-muted);margin-top:8px">${t('designer.preview_hint')}</p>
</div> </div>
<!-- Sidebar --> <!-- Sidebar -->
<div style="width:300px;display:flex;flex-direction:column;gap:12px;max-height:calc(100vh - 120px);overflow-y:auto"> <div style="width:300px;display:flex;flex-direction:column;gap:12px;max-height:calc(100vh - 120px);overflow-y:auto">
<!-- Add Elements --> <!-- Add Elements -->
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px"> <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
<h4 style="font-size:13px;margin-bottom:10px">Add Element</h4> <h4 style="font-size:13px;margin-bottom:10px">${t('designer.add_element')}</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
<button class="btn btn-secondary btn-sm" id="addText" style="justify-content:center">&#128172; Text</button> <button class="btn btn-secondary btn-sm" id="addText" style="justify-content:center">&#128172; ${t('designer.el.text')}</button>
<button class="btn btn-secondary btn-sm" id="addHeading" style="justify-content:center">&#128220; Heading</button> <button class="btn btn-secondary btn-sm" id="addHeading" style="justify-content:center">&#128220; ${t('designer.el.heading')}</button>
<button class="btn btn-secondary btn-sm" id="addImage" style="justify-content:center">&#128247; Image</button> <button class="btn btn-secondary btn-sm" id="addImage" style="justify-content:center">&#128247; ${t('designer.el.image')}</button>
<button class="btn btn-secondary btn-sm" id="addVideo" style="justify-content:center">&#127916; Video</button> <button class="btn btn-secondary btn-sm" id="addVideo" style="justify-content:center">&#127916; ${t('designer.el.video')}</button>
<button class="btn btn-secondary btn-sm" id="addClock" style="justify-content:center">&#128339; Clock</button> <button class="btn btn-secondary btn-sm" id="addClock" style="justify-content:center">&#128339; ${t('designer.el.clock')}</button>
<button class="btn btn-secondary btn-sm" id="addDate" style="justify-content:center">&#128197; Date</button> <button class="btn btn-secondary btn-sm" id="addDate" style="justify-content:center">&#128197; ${t('designer.el.date')}</button>
<button class="btn btn-secondary btn-sm" id="addWeather" style="justify-content:center">&#9925; Weather</button> <button class="btn btn-secondary btn-sm" id="addWeather" style="justify-content:center">&#9925; ${t('designer.el.weather')}</button>
<button class="btn btn-secondary btn-sm" id="addTicker" style="justify-content:center">&#128240; Ticker</button> <button class="btn btn-secondary btn-sm" id="addTicker" style="justify-content:center">&#128240; ${t('designer.el.ticker')}</button>
<button class="btn btn-secondary btn-sm" id="addShape" style="justify-content:center">&#9632; Shape</button> <button class="btn btn-secondary btn-sm" id="addShape" style="justify-content:center">&#9632; ${t('designer.el.shape')}</button>
<button class="btn btn-secondary btn-sm" id="addQR" style="justify-content:center">&#9641; QR Code</button> <button class="btn btn-secondary btn-sm" id="addQR" style="justify-content:center">&#9641; ${t('designer.el.qr')}</button>
<button class="btn btn-secondary btn-sm" id="addCountdown" style="justify-content:center">&#9201; Countdown</button> <button class="btn btn-secondary btn-sm" id="addCountdown" style="justify-content:center">&#9201; ${t('designer.el.countdown')}</button>
<button class="btn btn-secondary btn-sm" id="addWebpage" style="justify-content:center">&#127760; Webpage</button> <button class="btn btn-secondary btn-sm" id="addWebpage" style="justify-content:center">&#127760; ${t('designer.el.webpage')}</button>
</div> </div>
</div> </div>
<!-- Background --> <!-- Background -->
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px"> <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
<h4 style="font-size:13px;margin-bottom:8px">Background</h4> <h4 style="font-size:13px;margin-bottom:8px">${t('designer.background')}</h4>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px"> <div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
${BACKGROUNDS.map(b => `<div style="width:30px;height:30px;border-radius:4px;cursor:pointer;border:2px solid var(--border);background:${b.value}" data-bg="${b.value}" title="${b.name}"></div>`).join('')} ${BACKGROUNDS.map(b => `<div style="width:30px;height:30px;border-radius:4px;cursor:pointer;border:2px solid var(--border);background:${b.value}" data-bg="${b.value}" title="${t('designer.bg.' + b.id)}"></div>`).join('')}
</div> </div>
<div style="display:flex;gap:6px"> <div style="display:flex;gap:6px">
<input type="color" id="bgColor" value="#000000" style="flex:1;height:32px;border:none;cursor:pointer;border-radius:4px"> <input type="color" id="bgColor" value="#000000" style="flex:1;height:32px;border:none;cursor:pointer;border-radius:4px">
<button class="btn btn-secondary btn-sm" id="bgImageBtn">Image</button> <button class="btn btn-secondary btn-sm" id="bgImageBtn">${t('designer.bg_image')}</button>
</div> </div>
<input type="file" id="bgImageInput" style="display:none" accept="image/*"> <input type="file" id="bgImageInput" style="display:none" accept="image/*">
</div> </div>
<!-- Properties --> <!-- Properties -->
<div id="propPanel" style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px;display:none"> <div id="propPanel" style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px;display:none">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<h4 style="font-size:13px">Properties</h4> <h4 style="font-size:13px">${t('designer.properties')}</h4>
<button class="btn btn-danger btn-sm" id="deleteEl">Delete</button> <button class="btn btn-danger btn-sm" id="deleteEl">${t('common.delete')}</button>
</div> </div>
<div id="propFields"></div> <div id="propFields"></div>
</div> </div>
<!-- Layers --> <!-- Layers -->
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px"> <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
<h4 style="font-size:13px;margin-bottom:8px">Layers</h4> <h4 style="font-size:13px;margin-bottom:8px">${t('designer.layers')}</h4>
<div id="layerList" style="font-size:12px"></div> <div id="layerList" style="font-size:12px"></div>
</div> </div>
</div> </div>
@ -108,8 +111,8 @@ export function render(container) {
}; };
// Add element handlers // Add element handlers
document.getElementById('addText').onclick = () => addElement({ type: 'text', x: 10, y: 60, text: 'Your text here', fontSize: 24, fontFamily: 'Arial', color: '#FFFFFF', bold: false, shadow: false }); document.getElementById('addText').onclick = () => addElement({ type: 'text', x: 10, y: 60, text: t('designer.default.text'), fontSize: 24, fontFamily: 'Arial', color: '#FFFFFF', bold: false, shadow: false });
document.getElementById('addHeading').onclick = () => addElement({ type: 'text', x: 5, y: 5, text: 'HEADING', fontSize: 64, fontFamily: 'Impact', color: '#FFFFFF', bold: true, shadow: true }); document.getElementById('addHeading').onclick = () => addElement({ type: 'text', x: 5, y: 5, text: t('designer.default.heading'), fontSize: 64, fontFamily: 'Impact', color: '#FFFFFF', bold: true, shadow: true });
document.getElementById('addImage').onclick = () => { document.getElementById('addImage').onclick = () => {
const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*';
input.onchange = () => { input.onchange = () => {
@ -120,30 +123,30 @@ export function render(container) {
input.click(); input.click();
}; };
document.getElementById('addVideo').onclick = () => { document.getElementById('addVideo').onclick = () => {
const url = prompt('Video URL (MP4):'); const url = prompt(t('designer.prompt.video_url'));
if (url) addElement({ type: 'video', x: 5, y: 5, width: 50, height: 50, src: url, muted: true, loop: true }); if (url) addElement({ type: 'video', x: 5, y: 5, width: 50, height: 50, src: url, muted: true, loop: true });
}; };
document.getElementById('addClock').onclick = () => addElement({ type: 'clock', x: 60, y: 5, fontSize: 48, fontFamily: 'Arial', color: '#FFFFFF', format: '12h', showSeconds: true, shadow: true }); document.getElementById('addClock').onclick = () => addElement({ type: 'clock', x: 60, y: 5, fontSize: 48, fontFamily: 'Arial', color: '#FFFFFF', format: '12h', showSeconds: true, shadow: true });
document.getElementById('addDate').onclick = () => addElement({ type: 'date', x: 60, y: 20, fontSize: 24, fontFamily: 'Arial', color: '#FFFFFF', shadow: false }); document.getElementById('addDate').onclick = () => addElement({ type: 'date', x: 60, y: 20, fontSize: 24, fontFamily: 'Arial', color: '#FFFFFF', shadow: false });
document.getElementById('addWeather').onclick = () => { document.getElementById('addWeather').onclick = () => {
const location = prompt('City, State:', 'Milwaukee, WI'); const location = prompt(t('designer.prompt.weather_location'), 'Milwaukee, WI');
if (location) addElement({ type: 'weather', x: 5, y: 70, fontSize: 36, color: '#FFFFFF', location, units: 'imperial' }); if (location) addElement({ type: 'weather', x: 5, y: 70, fontSize: 36, color: '#FFFFFF', location, units: 'imperial' });
}; };
document.getElementById('addTicker').onclick = () => { document.getElementById('addTicker').onclick = () => {
const url = prompt('RSS Feed URL:', 'https://feeds.bbci.co.uk/news/rss.xml'); const url = prompt(t('designer.prompt.rss_url'), 'https://feeds.bbci.co.uk/news/rss.xml');
if (url) addElement({ type: 'ticker', x: 0, y: 90, width: 100, height: 10, feedUrl: url, speed: 30, fontSize: 20, color: '#FFFFFF', bgColor: 'rgba(0,0,0,0.7)' }); if (url) addElement({ type: 'ticker', x: 0, y: 90, width: 100, height: 10, feedUrl: url, speed: 30, fontSize: 20, color: '#FFFFFF', bgColor: 'rgba(0,0,0,0.7)' });
}; };
document.getElementById('addShape').onclick = () => addElement({ type: 'shape', x: 20, y: 20, width: 30, height: 20, color: '#3b82f6', opacity: 0.7, radius: 8, shape: 'rect' }); document.getElementById('addShape').onclick = () => addElement({ type: 'shape', x: 20, y: 20, width: 30, height: 20, color: '#3b82f6', opacity: 0.7, radius: 8, shape: 'rect' });
document.getElementById('addQR').onclick = () => { document.getElementById('addQR').onclick = () => {
const data = prompt('QR Code URL:', 'https://example.com'); const data = prompt(t('designer.prompt.qr_url'), 'https://example.com');
if (data) addElement({ type: 'qr', x: 80, y: 70, size: 15, data, fgColor: '#FFFFFF', bgColor: '#000000' }); if (data) addElement({ type: 'qr', x: 80, y: 70, size: 15, data, fgColor: '#FFFFFF', bgColor: '#000000' });
}; };
document.getElementById('addCountdown').onclick = () => { document.getElementById('addCountdown').onclick = () => {
const target = prompt('Target date (YYYY-MM-DD):', '2026-04-01'); const target = prompt(t('designer.prompt.countdown_date'), '2026-04-01');
if (target) addElement({ type: 'countdown', x: 20, y: 40, fontSize: 48, color: '#FFFFFF', targetDate: target, label: 'Coming Soon' }); if (target) addElement({ type: 'countdown', x: 20, y: 40, fontSize: 48, color: '#FFFFFF', targetDate: target, label: t('designer.default.coming_soon') });
}; };
document.getElementById('addWebpage').onclick = () => { document.getElementById('addWebpage').onclick = () => {
const url = prompt('Webpage URL:'); const url = prompt(t('designer.prompt.webpage_url'));
if (url) addElement({ type: 'webpage', x: 5, y: 5, width: 40, height: 40, url }); if (url) addElement({ type: 'webpage', x: 5, y: 5, width: 40, height: 40, url });
}; };
@ -159,10 +162,10 @@ export function render(container) {
const res = await fetch('/api/widgets', { const res = await fetch('/api/widgets', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` },
body: JSON.stringify({ widget_type: 'text', name: `Design ${new Date().toLocaleDateString()}`, config: { html: generateInnerHTML(), css: '', background: bgValue } }) body: JSON.stringify({ widget_type: 'text', name: t('designer.widget_name', { date: new Date().toLocaleDateString() }), config: { html: generateInnerHTML(), css: '', background: bgValue } })
}); });
if (res.ok) showToast('Published as widget! Assign it to a layout zone.', 'success'); if (res.ok) showToast(t('designer.toast.published'), 'success');
else showToast('Publish failed', 'error'); else showToast(t('designer.toast.publish_failed'), 'error');
} catch (err) { showToast(err.message, 'error'); } } catch (err) { showToast(err.message, 'error'); }
}; };
@ -207,7 +210,7 @@ export function render(container) {
} }
const link = document.createElement('a'); const link = document.createElement('a');
link.download = 'signage-design.png'; link.href = canvas.toDataURL('image/png'); link.click(); link.download = 'signage-design.png'; link.href = canvas.toDataURL('image/png'); link.click();
} catch (err) { showToast('Export failed: ' + err.message, 'error'); } } catch (err) { showToast(t('designer.toast.export_failed', { error: err.message }), 'error'); }
}; };
// Load saved design // Load saved design
@ -222,8 +225,8 @@ export function render(container) {
bgValue = data.bgValue || '#000'; bgValue = data.bgValue || '#000';
bgImageDataUrl = data.bgImageDataUrl || null; bgImageDataUrl = data.bgImageDataUrl || null;
redraw(); redraw();
showToast('Design loaded', 'success'); showToast(t('designer.toast.loaded'), 'success');
} catch { showToast('Invalid design file', 'error'); } } catch { showToast(t('designer.toast.invalid_file'), 'error'); }
}; };
reader.readAsText(input.files[0]); reader.readAsText(input.files[0]);
}; };
@ -315,16 +318,16 @@ function redraw() {
html += `<div style="position:absolute;left:${el.x}%;top:${el.y}%;width:${el.width}%;height:${el.height}%;background:${el.color};opacity:${el.opacity};border-radius:${el.radius || 0}px;${el.shape === 'circle' ? 'border-radius:50%;' : ''}${border}${cursor}" data-idx="${i}"></div>`; html += `<div style="position:absolute;left:${el.x}%;top:${el.y}%;width:${el.width}%;height:${el.height}%;background:${el.color};opacity:${el.opacity};border-radius:${el.radius || 0}px;${el.shape === 'circle' ? 'border-radius:50%;' : ''}${border}${cursor}" data-idx="${i}"></div>`;
break; break;
case 'weather': case 'weather':
html += `<div style="position:absolute;left:${el.x}%;top:${el.y}%;font-size:${el.fontSize / 10}vw;color:${el.color};${border}${cursor}" data-idx="${i}" id="weather_${i}">&#9925; Loading...</div>`; html += `<div style="position:absolute;left:${el.x}%;top:${el.y}%;font-size:${el.fontSize / 10}vw;color:${el.color};${border}${cursor}" data-idx="${i}" id="weather_${i}">&#9925; ${t('common.loading')}</div>`;
break; break;
case 'ticker': case 'ticker':
html += `<div style="position:absolute;left:${el.x}%;top:${el.y}%;width:${el.width}%;height:${el.height}%;background:${el.bgColor};overflow:hidden;display:flex;align-items:center;${border}" data-idx="${i}"> html += `<div style="position:absolute;left:${el.x}%;top:${el.y}%;width:${el.width}%;height:${el.height}%;background:${el.bgColor};overflow:hidden;display:flex;align-items:center;${border}" data-idx="${i}">
<div style="white-space:nowrap;animation:ticker ${el.speed || 30}s linear infinite;font-size:${el.fontSize / 10}vw;color:${el.color}" id="ticker_${i}">Loading news...</div> <div style="white-space:nowrap;animation:ticker ${el.speed || 30}s linear infinite;font-size:${el.fontSize / 10}vw;color:${el.color}" id="ticker_${i}">${t('designer.loading_news')}</div>
</div>`; </div>`;
break; break;
case 'qr': case 'qr':
html += `<div style="position:absolute;left:${el.x}%;top:${el.y}%;width:${el.size}%;aspect-ratio:1;background:${el.bgColor};display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:8px;${border}${cursor}" data-idx="${i}"> html += `<div style="position:absolute;left:${el.x}%;top:${el.y}%;width:${el.size}%;aspect-ratio:1;background:${el.bgColor};display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:8px;${border}${cursor}" data-idx="${i}">
<div style="font-size:1.5vw;color:${el.fgColor};font-weight:bold">QR CODE</div> <div style="font-size:1.5vw;color:${el.fgColor};font-weight:bold">${t('designer.qr_label')}</div>
<div style="font-size:0.8vw;color:${el.fgColor};opacity:0.7;margin-top:4px">${el.data?.slice(0, 25)}</div> <div style="font-size:0.8vw;color:${el.fgColor};opacity:0.7;margin-top:4px">${el.data?.slice(0, 25)}</div>
</div>`; </div>`;
break; break;
@ -378,7 +381,7 @@ function updateDynamic() {
if (cdEl && el.targetDate) { if (cdEl && el.targetDate) {
const update = () => { const update = () => {
const diff = new Date(el.targetDate) - new Date(); const diff = new Date(el.targetDate) - new Date();
if (diff <= 0) { cdEl.textContent = 'NOW!'; return; } if (diff <= 0) { cdEl.textContent = t('designer.countdown_now'); return; }
const days = Math.floor(diff / 86400000); const days = Math.floor(diff / 86400000);
const hours = Math.floor((diff % 86400000) / 3600000); const hours = Math.floor((diff % 86400000) / 3600000);
const mins = Math.floor((diff % 3600000) / 60000); const mins = Math.floor((diff % 3600000) / 60000);
@ -397,15 +400,15 @@ function updateDynamic() {
const temp = el.units === 'metric' ? cur.temp_C + '°C' : cur.temp_F + '°F'; const temp = el.units === 'metric' ? cur.temp_C + '°C' : cur.temp_F + '°F';
wEl.textContent = `${temp} ${cur.weatherDesc?.[0]?.value || ''}`; wEl.textContent = `${temp} ${cur.weatherDesc?.[0]?.value || ''}`;
} }
}).catch(() => { wEl.textContent = '&#9925; ' + el.location; }); }).catch(() => { wEl.textContent = ' ' + el.location; });
} }
} }
if (el.type === 'ticker') { if (el.type === 'ticker') {
const tEl = document.getElementById(`ticker_${i}`); const tEl = document.getElementById(`ticker_${i}`);
if (tEl && el.feedUrl) { if (tEl && el.feedUrl) {
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(el.feedUrl)}`).then(r => r.json()).then(d => { fetch(`https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(el.feedUrl)}`).then(r => r.json()).then(d => {
tEl.textContent = (d.items || []).map(item => item.title).join(' • ') || 'No items'; tEl.textContent = (d.items || []).map(item => item.title).join(' • ') || t('designer.no_items');
}).catch(() => { tEl.textContent = 'Feed unavailable'; }); }).catch(() => { tEl.textContent = t('designer.feed_unavailable'); });
} }
} }
}); });
@ -426,42 +429,42 @@ function updateProps() {
</div>`; </div>`;
if (el.type === 'text') { if (el.type === 'text') {
html += `<div class="form-group"><label>Text</label><input type="text" class="input" value="${el.text}" data-prop="text"></div> html += `<div class="form-group"><label>${t('designer.prop.text')}</label><input type="text" class="input" value="${el.text}" data-prop="text"></div>
<div class="form-group"><label>Size</label><input type="range" min="8" max="120" value="${el.fontSize}" data-prop="fontSize" style="width:100%"><span style="font-size:11px;color:var(--text-muted)">${el.fontSize}px</span></div> <div class="form-group"><label>${t('designer.prop.size')}</label><input type="range" min="8" max="120" value="${el.fontSize}" data-prop="fontSize" style="width:100%"><span style="font-size:11px;color:var(--text-muted)">${el.fontSize}px</span></div>
<div class="form-group"><label>Font</label><select class="input" style="background:var(--bg-input)" data-prop="fontFamily">${FONTS.map(f => `<option ${f === el.fontFamily ? 'selected' : ''}>${f}</option>`).join('')}</select></div> <div class="form-group"><label>${t('designer.prop.font')}</label><select class="input" style="background:var(--bg-input)" data-prop="fontFamily">${FONTS.map(f => `<option ${f === el.fontFamily ? 'selected' : ''}>${f}</option>`).join('')}</select></div>
<div class="form-group"><label>Color</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none;cursor:pointer"></div> <div class="form-group"><label>${t('designer.prop.color')}</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none;cursor:pointer"></div>
<label style="font-size:12px;display:flex;gap:6px;margin:4px 0"><input type="checkbox" ${el.bold ? 'checked' : ''} data-prop="bold"> Bold</label> <label style="font-size:12px;display:flex;gap:6px;margin:4px 0"><input type="checkbox" ${el.bold ? 'checked' : ''} data-prop="bold"> ${t('designer.prop.bold')}</label>
<label style="font-size:12px;display:flex;gap:6px;margin:4px 0"><input type="checkbox" ${el.shadow ? 'checked' : ''} data-prop="shadow"> Shadow</label>`; <label style="font-size:12px;display:flex;gap:6px;margin:4px 0"><input type="checkbox" ${el.shadow ? 'checked' : ''} data-prop="shadow"> ${t('designer.prop.shadow')}</label>`;
} else if (el.type === 'clock') { } else if (el.type === 'clock') {
html += `<div class="form-group"><label>Size</label><input type="range" min="16" max="120" value="${el.fontSize}" data-prop="fontSize" style="width:100%"></div> html += `<div class="form-group"><label>${t('designer.prop.size')}</label><input type="range" min="16" max="120" value="${el.fontSize}" data-prop="fontSize" style="width:100%"></div>
<div class="form-group"><label>Color</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div> <div class="form-group"><label>${t('designer.prop.color')}</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div>
<div class="form-group"><label>Format</label><select class="input" style="background:var(--bg-input)" data-prop="format"><option ${el.format === '12h' ? 'selected' : ''} value="12h">12h</option><option ${el.format === '24h' ? 'selected' : ''} value="24h">24h</option></select></div> <div class="form-group"><label>${t('designer.prop.format')}</label><select class="input" style="background:var(--bg-input)" data-prop="format"><option ${el.format === '12h' ? 'selected' : ''} value="12h">12h</option><option ${el.format === '24h' ? 'selected' : ''} value="24h">24h</option></select></div>
<label style="font-size:12px;display:flex;gap:6px;margin:4px 0"><input type="checkbox" ${el.showSeconds ? 'checked' : ''} data-prop="showSeconds"> Show seconds</label>`; <label style="font-size:12px;display:flex;gap:6px;margin:4px 0"><input type="checkbox" ${el.showSeconds ? 'checked' : ''} data-prop="showSeconds"> ${t('designer.prop.show_seconds')}</label>`;
} else if (el.type === 'image' || el.type === 'video' || el.type === 'webpage') { } else if (el.type === 'image' || el.type === 'video' || el.type === 'webpage') {
html += `<div style="display:flex;gap:6px"><div class="form-group" style="flex:1;margin:0"><label>W%</label><input type="number" class="input" value="${Math.round(el.width)}" data-prop="width"></div> html += `<div style="display:flex;gap:6px"><div class="form-group" style="flex:1;margin:0"><label>W%</label><input type="number" class="input" value="${Math.round(el.width)}" data-prop="width"></div>
<div class="form-group" style="flex:1;margin:0"><label>H%</label><input type="number" class="input" value="${Math.round(el.height)}" data-prop="height"></div></div>`; <div class="form-group" style="flex:1;margin:0"><label>H%</label><input type="number" class="input" value="${Math.round(el.height)}" data-prop="height"></div></div>`;
if (el.type === 'video') html += `<label style="font-size:12px;display:flex;gap:6px;margin:8px 0"><input type="checkbox" ${el.muted ? 'checked' : ''} data-prop="muted"> Muted</label> if (el.type === 'video') html += `<label style="font-size:12px;display:flex;gap:6px;margin:8px 0"><input type="checkbox" ${el.muted ? 'checked' : ''} data-prop="muted"> ${t('designer.prop.muted')}</label>
<label style="font-size:12px;display:flex;gap:6px;margin:4px 0"><input type="checkbox" ${el.loop ? 'checked' : ''} data-prop="loop"> Loop</label>`; <label style="font-size:12px;display:flex;gap:6px;margin:4px 0"><input type="checkbox" ${el.loop ? 'checked' : ''} data-prop="loop"> ${t('designer.prop.loop')}</label>`;
} else if (el.type === 'shape') { } else if (el.type === 'shape') {
html += `<div style="display:flex;gap:6px"><div class="form-group" style="flex:1;margin:0"><label>W%</label><input type="number" class="input" value="${Math.round(el.width)}" data-prop="width"></div> html += `<div style="display:flex;gap:6px"><div class="form-group" style="flex:1;margin:0"><label>W%</label><input type="number" class="input" value="${Math.round(el.width)}" data-prop="width"></div>
<div class="form-group" style="flex:1;margin:0"><label>H%</label><input type="number" class="input" value="${Math.round(el.height)}" data-prop="height"></div></div> <div class="form-group" style="flex:1;margin:0"><label>H%</label><input type="number" class="input" value="${Math.round(el.height)}" data-prop="height"></div></div>
<div class="form-group"><label>Color</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div> <div class="form-group"><label>${t('designer.prop.color')}</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div>
<div class="form-group"><label>Opacity</label><input type="range" min="0" max="1" step="0.1" value="${el.opacity}" data-prop="opacity" style="width:100%"></div> <div class="form-group"><label>${t('designer.prop.opacity')}</label><input type="range" min="0" max="1" step="0.1" value="${el.opacity}" data-prop="opacity" style="width:100%"></div>
<div class="form-group"><label>Shape</label><select class="input" style="background:var(--bg-input)" data-prop="shape"><option ${el.shape === 'rect' ? 'selected' : ''}>rect</option><option ${el.shape === 'circle' ? 'selected' : ''}>circle</option></select></div>`; <div class="form-group"><label>${t('designer.prop.shape')}</label><select class="input" style="background:var(--bg-input)" data-prop="shape"><option ${el.shape === 'rect' ? 'selected' : ''}>rect</option><option ${el.shape === 'circle' ? 'selected' : ''}>circle</option></select></div>`;
} else if (el.type === 'weather') { } else if (el.type === 'weather') {
html += `<div class="form-group"><label>Location</label><input type="text" class="input" value="${el.location}" data-prop="location"></div> html += `<div class="form-group"><label>${t('designer.prop.location')}</label><input type="text" class="input" value="${el.location}" data-prop="location"></div>
<div class="form-group"><label>Size</label><input type="range" min="16" max="80" value="${el.fontSize}" data-prop="fontSize" style="width:100%"></div> <div class="form-group"><label>${t('designer.prop.size')}</label><input type="range" min="16" max="80" value="${el.fontSize}" data-prop="fontSize" style="width:100%"></div>
<div class="form-group"><label>Color</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div>`; <div class="form-group"><label>${t('designer.prop.color')}</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div>`;
} else if (el.type === 'ticker') { } else if (el.type === 'ticker') {
html += `<div class="form-group"><label>Feed URL</label><input type="text" class="input" value="${el.feedUrl}" data-prop="feedUrl"></div> html += `<div class="form-group"><label>${t('designer.prop.feed_url')}</label><input type="text" class="input" value="${el.feedUrl}" data-prop="feedUrl"></div>
<div class="form-group"><label>Speed (seconds)</label><input type="number" class="input" value="${el.speed}" data-prop="speed"></div> <div class="form-group"><label>${t('designer.prop.speed')}</label><input type="number" class="input" value="${el.speed}" data-prop="speed"></div>
<div class="form-group"><label>Text Color</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div> <div class="form-group"><label>${t('designer.prop.text_color')}</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div>
<div class="form-group"><label>BG Color</label><input type="text" class="input" value="${el.bgColor}" data-prop="bgColor"></div>`; <div class="form-group"><label>${t('designer.prop.bg_color')}</label><input type="text" class="input" value="${el.bgColor}" data-prop="bgColor"></div>`;
} else if (el.type === 'countdown') { } else if (el.type === 'countdown') {
html += `<div class="form-group"><label>Target Date</label><input type="date" class="input" value="${el.targetDate}" data-prop="targetDate"></div> html += `<div class="form-group"><label>${t('designer.prop.target_date')}</label><input type="date" class="input" value="${el.targetDate}" data-prop="targetDate"></div>
<div class="form-group"><label>Label</label><input type="text" class="input" value="${el.label}" data-prop="label"></div> <div class="form-group"><label>${t('designer.prop.label')}</label><input type="text" class="input" value="${el.label}" data-prop="label"></div>
<div class="form-group"><label>Size</label><input type="range" min="16" max="100" value="${el.fontSize}" data-prop="fontSize" style="width:100%"></div> <div class="form-group"><label>${t('designer.prop.size')}</label><input type="range" min="16" max="100" value="${el.fontSize}" data-prop="fontSize" style="width:100%"></div>
<div class="form-group"><label>Color</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div>`; <div class="form-group"><label>${t('designer.prop.color')}</label><input type="color" value="${el.color}" data-prop="color" style="width:100%;height:28px;border:none"></div>`;
} }
// Save design button // Save design button
@ -470,7 +473,7 @@ function updateProps() {
a.download = 'design.json'; a.download = 'design.json';
a.href = 'data:application/json,' + encodeURIComponent(JSON.stringify({elements: ${JSON.stringify(elements)}, bgValue: '${bgValue}'})); a.href = 'data:application/json,' + encodeURIComponent(JSON.stringify({elements: ${JSON.stringify(elements)}, bgValue: '${bgValue}'}));
a.click(); a.click();
})()">Save Design File</button>`; })()">${t('designer.save_design_file')}</button>`;
fields.innerHTML = html; fields.innerHTML = html;
@ -498,7 +501,7 @@ function updateLayers() {
<span>${typeIcons[el.type] || '?'}</span> <span>${typeIcons[el.type] || '?'}</span>
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${el.text || el.type}</span> <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${el.text || el.type}</span>
</div> </div>
`).join('') || '<p style="color:var(--text-muted)">No elements yet</p>'; `).join('') || `<p style="color:var(--text-muted)">${t('designer.no_elements')}</p>`;
list.querySelectorAll('[data-layer]').forEach(el => { list.querySelectorAll('[data-layer]').forEach(el => {
el.onclick = () => { selectedIdx = parseInt(el.dataset.layer); redraw(); }; el.onclick = () => { selectedIdx = parseInt(el.dataset.layer); redraw(); };