Zwischenstand vom 29.05.2026

This commit is contained in:
khaccount
2026-05-29 11:14:02 +02:00
parent b6d2142214
commit dba154e572
36 changed files with 5113 additions and 649 deletions

View File

@@ -68,6 +68,28 @@
<source>Narrow sidebar + wide main column</source>
</trans-unit>
<trans-unit id="container.title" resname="container.title">
<source>VITEC · Container</source>
</trans-unit>
<trans-unit id="container.description" resname="container.description">
<source>Generic container for other content elements with a custom CSS class</source>
</trans-unit>
<trans-unit id="container.column" resname="container.column">
<source>Content</source>
</trans-unit>
<trans-unit id="container.flexform.sheet" resname="container.flexform.sheet">
<source>Container Settings</source>
</trans-unit>
<trans-unit id="container.flexform.label" resname="container.flexform.label">
<source>Container Settings</source>
</trans-unit>
<trans-unit id="container.cssclass.label" resname="container.cssclass.label">
<source>CSS Class</source>
</trans-unit>
<trans-unit id="container.cssclass.description" resname="container.cssclass.description">
<source>Custom CSS class that is rendered on the container in the frontend.</source>
</trans-unit>
<trans-unit id="column.1" resname="column.1">
<source>Column 1</source>
</trans-unit>

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_containers.xlf" product-name="vitec">
<header/>
<body>
<trans-unit id="group.header" resname="group.header">
<source>VITEC</source>
</trans-unit>
<trans-unit id="section.heading" resname="section.heading">
<source>Section Heading</source>
</trans-unit>
<trans-unit id="section.subline" resname="section.subline">
<source>Section Subline</source>
</trans-unit>
<trans-unit id="bg_variant.label" resname="bg_variant.label">
<source>Background Variant</source>
</trans-unit>
<trans-unit id="bg_variant.option.none" resname="bg_variant.option.none">
<source>None</source>
</trans-unit>
<trans-unit id="bg_variant.option.orange" resname="bg_variant.option.orange">
<source>Orange</source>
</trans-unit>
<trans-unit id="bg_variant.option.blue" resname="bg_variant.option.blue">
<source>Blue</source>
</trans-unit>
<trans-unit id="bg_variant.option.graphite" resname="bg_variant.option.graphite">
<source>Graphite</source>
</trans-unit>
<trans-unit id="bg_variant.option.midnight" resname="bg_variant.option.midnight">
<source>Midnight</source>
</trans-unit>
<trans-unit id="cols_50_50.title" resname="cols_50_50.title">
<source>VITEC · Two Columns (50 / 50)</source>
</trans-unit>
<trans-unit id="cols_50_50.description" resname="cols_50_50.description">
<source>Two equal columns</source>
</trans-unit>
<trans-unit id="cols_33_33_33.title" resname="cols_33_33_33.title">
<source>VITEC · Three Columns (33 / 33 / 33)</source>
</trans-unit>
<trans-unit id="cols_33_33_33.description" resname="cols_33_33_33.description">
<source>Three equal columns</source>
</trans-unit>
<trans-unit id="cols_25_25_25_25.title" resname="cols_25_25_25_25.title">
<source>VITEC · Four Columns (25 / 25 / 25 / 25)</source>
</trans-unit>
<trans-unit id="cols_25_25_25_25.description" resname="cols_25_25_25_25.description">
<source>Four equal columns</source>
</trans-unit>
<trans-unit id="cols_66_33.title" resname="cols_66_33.title">
<source>VITEC · Two Columns (66 / 33)</source>
</trans-unit>
<trans-unit id="cols_66_33.description" resname="cols_66_33.description">
<source>Wide main column + narrow sidebar</source>
</trans-unit>
<trans-unit id="cols_33_66.title" resname="cols_33_66.title">
<source>VITEC · Two Columns (33 / 66)</source>
</trans-unit>
<trans-unit id="cols_33_66.description" resname="cols_33_66.description">
<source>Narrow sidebar + wide main column</source>
</trans-unit>
<trans-unit id="column.1" resname="column.1">
<source>Column 1</source>
</trans-unit>
<trans-unit id="column.2" resname="column.2">
<source>Column 2</source>
</trans-unit>
<trans-unit id="column.3" resname="column.3">
<source>Column 3</source>
</trans-unit>
<trans-unit id="column.4" resname="column.4">
<source>Column 4</source>
</trans-unit>
<trans-unit id="column.main_66" resname="column.main_66">
<source>Main (66%)</source>
</trans-unit>
<trans-unit id="column.sidebar_33" resname="column.sidebar_33">
<source>Sidebar (33%)</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_containers.xlf" product-name="vitec">
<header/>
<body>
<trans-unit id="group.header" resname="group.header">
<source>VITEC</source>
</trans-unit>
<trans-unit id="section.heading" resname="section.heading">
<source>Section Heading</source>
</trans-unit>
<trans-unit id="section.subline" resname="section.subline">
<source>Section Subline</source>
</trans-unit>
<trans-unit id="bg_variant.label" resname="bg_variant.label">
<source>Background Variant</source>
</trans-unit>
<trans-unit id="bg_variant.option.none" resname="bg_variant.option.none">
<source>None</source>
</trans-unit>
<trans-unit id="bg_variant.option.orange" resname="bg_variant.option.orange">
<source>Orange</source>
</trans-unit>
<trans-unit id="bg_variant.option.blue" resname="bg_variant.option.blue">
<source>Blue</source>
</trans-unit>
<trans-unit id="bg_variant.option.graphite" resname="bg_variant.option.graphite">
<source>Graphite</source>
</trans-unit>
<trans-unit id="bg_variant.option.midnight" resname="bg_variant.option.midnight">
<source>Midnight</source>
</trans-unit>
<trans-unit id="cols_50_50.title" resname="cols_50_50.title">
<source>VITEC · Two Columns (50 / 50)</source>
</trans-unit>
<trans-unit id="cols_50_50.description" resname="cols_50_50.description">
<source>Two equal columns</source>
</trans-unit>
<trans-unit id="cols_33_33_33.title" resname="cols_33_33_33.title">
<source>VITEC · Three Columns (33 / 33 / 33)</source>
</trans-unit>
<trans-unit id="cols_33_33_33.description" resname="cols_33_33_33.description">
<source>Three equal columns</source>
</trans-unit>
<trans-unit id="cols_25_25_25_25.title" resname="cols_25_25_25_25.title">
<source>VITEC · Four Columns (25 / 25 / 25 / 25)</source>
</trans-unit>
<trans-unit id="cols_25_25_25_25.description" resname="cols_25_25_25_25.description">
<source>Four equal columns</source>
</trans-unit>
<trans-unit id="cols_66_33.title" resname="cols_66_33.title">
<source>VITEC · Two Columns (66 / 33)</source>
</trans-unit>
<trans-unit id="cols_66_33.description" resname="cols_66_33.description">
<source>Wide main column + narrow sidebar</source>
</trans-unit>
<trans-unit id="cols_33_66.title" resname="cols_33_66.title">
<source>VITEC · Two Columns (33 / 66)</source>
</trans-unit>
<trans-unit id="cols_33_66.description" resname="cols_33_66.description">
<source>Narrow sidebar + wide main column</source>
</trans-unit>
<trans-unit id="container.title" resname="container.title">
<source>VITEC · Container</source>
</trans-unit>
<trans-unit id="container.description" resname="container.description">
<source>Generic container for other content elements with a custom CSS class</source>
</trans-unit>
<trans-unit id="container.column" resname="container.column">
<source>Content</source>
</trans-unit>
<trans-unit id="container.flexform.sheet" resname="container.flexform.sheet">
<source>Container Settings</source>
</trans-unit>
<trans-unit id="container.cssclass.label" resname="container.cssclass.label">
<source>CSS Class</source>
</trans-unit>
<trans-unit id="container.cssclass.description" resname="container.cssclass.description">
<source>Custom CSS class that is rendered on the container in the frontend.</source>
</trans-unit>
<trans-unit id="column.1" resname="column.1">
<source>Column 1</source>
</trans-unit>
<trans-unit id="column.2" resname="column.2">
<source>Column 2</source>
</trans-unit>
<trans-unit id="column.3" resname="column.3">
<source>Column 3</source>
</trans-unit>
<trans-unit id="column.4" resname="column.4">
<source>Column 4</source>
</trans-unit>
<trans-unit id="column.main_66" resname="column.main_66">
<source>Main (66%)</source>
</trans-unit>
<trans-unit id="column.sidebar_33" resname="column.sidebar_33">
<source>Sidebar (33%)</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_ogimage.xlf">
<body>
<trans-unit id="mlang_tabs_tab">
<source>OG Image</source>
</trans-unit>
<trans-unit id="mlang_labels_tabdescr">
<source>Generate Open Graph images (1200×630) for social media sharing.</source>
</trans-unit>
<trans-unit id="mlang_labels_tablabel">
<source>OG Image Generator</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,7 +1,7 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:n="http://typo3.org/ns/GeorgRinger/News/ViewHelpers">
<div class="vitec-administration">
<f:flashMessages/>
<f:flashMessages queueIdentifier="core.template.flashMessages" />
<h2>Hi</h2>
<f:render section="main"/>
</div>

View File

@@ -0,0 +1,288 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Backend/Default" />
<f:section name="main">
<div class="module-docheader-bar module-docheader-bar-navigation">
<div class="module-docheader-bar-column-left">
<h2 class="t3js-title-inlineedit">OG Image Generator</h2>
</div>
</div>
<div class="row" id="vitec-ogimage-wrap">
<!-- ══════════════════════════════════════════════════════
LEFT: FORM
═══════════════════════════════════════════════════════ -->
<div class="col-md-7">
<f:flashMessages queueIdentifier="core.template.flashMessages" />
<form action="{f:be.uri(route: 'web_vitecogimage.generate')}" method="post" id="vitecOgForm" data-preview-url="{f:be.uri(route: 'web_vitecogimage.preview')}">
<!-- Preset quick-picks -->
<div class="card mb-3">
<div class="card-header"><strong>Quick Presets</strong></div>
<div class="card-body d-flex gap-2 flex-wrap">
<f:for each="{presets}" as="preset" key="key">
<button type="button" class="btn btn-outline-secondary btn-sm js-preset"
data-preset="{key}"
data-bg="{preset.bg_color}"
data-title-color="{preset.title_color}"
data-subtitle-color="{preset.subtitle_color}">
{preset.label}
</button>
</f:for>
</div>
</div>
<!-- Background -->
<div class="card mb-3">
<div class="card-header"><strong>Background</strong></div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Type</label>
<div class="d-flex gap-3">
<label class="form-check-label">
<input class="form-check-input me-1 js-bg-type" type="radio"
name="ogimage[bg_type]" value="color"
<f:if condition="{formData.bg_type} == 'color'">checked</f:if>/>
Solid colour
</label>
<label class="form-check-label">
<input class="form-check-input me-1 js-bg-type" type="radio"
name="ogimage[bg_type]" value="image"
<f:if condition="{formData.bg_type} == 'image'">checked</f:if>/>
Image
</label>
</div>
</div>
<div id="bg-color-wrap" class="mb-3">
<label class="form-label">Background Colour</label>
<div class="d-flex align-items-center gap-2">
<input type="color" class="form-control form-control-color js-live"
name="ogimage[bg_color]" id="bgColor"
value="{formData.bg_color}" />
<input type="text" class="form-control js-hex-sync" style="max-width:120px"
data-target="bgColor" value="{formData.bg_color}" />
</div>
</div>
<div id="bg-image-wrap" class="mb-3" style="display:none">
<label class="form-label">Background Image</label>
<f:if condition="{backgroundImages}">
<f:then>
<select class="form-select js-live" name="ogimage[bg_image]">
<option value="">— none —</option>
<f:for each="{backgroundImages}" as="img">
<option value="{img.path}"
<f:if condition="{formData.bg_image} == {img.path}">selected</f:if>>{img.label}</option>
</f:for>
</select>
<small class="text-muted">Upload images to <code>fileadmin/og-backgrounds/</code> to list them here.</small>
</f:then>
<f:else>
<input type="text" class="form-control js-live"
name="ogimage[bg_image]"
placeholder="/fileadmin/og-backgrounds/my-bg.jpg"
value="{formData.bg_image}" />
<small class="text-muted">No images found in <code>fileadmin/og-backgrounds/</code>. Enter a path manually, or upload files there.</small>
</f:else>
</f:if>
<div class="mt-2">
<label class="form-label">Dark overlay opacity: <span id="overlayLabel">{formData.overlay_opacity}</span>%</label>
<input type="range" class="form-range js-live" min="0" max="90" step="5"
name="ogimage[overlay_opacity]" value="{formData.overlay_opacity}"
oninput="document.getElementById('overlayLabel').textContent=this.value"/>
</div>
</div>
</div>
</div>
<!-- Text -->
<div class="card mb-3">
<div class="card-header"><strong>Text</strong></div>
<div class="card-body">
<div class="row mb-3">
<div class="col-8">
<label class="form-label">Title</label>
<input type="text" class="form-control js-live"
name="ogimage[title]" value="{formData.title}"
placeholder="Your headline" />
</div>
<div class="col-2">
<label class="form-label">Size</label>
<input type="number" class="form-control js-live"
name="ogimage[title_size]" value="{formData.title_size}"
min="20" max="120" />
</div>
<div class="col-2">
<label class="form-label">Colour</label>
<input type="color" class="form-control form-control-color js-live"
name="ogimage[title_color]" id="titleColor"
value="{formData.title_color}" />
</div>
</div>
<div class="row mb-2">
<div class="col-8">
<label class="form-label">Subtitle / Description</label>
<input type="text" class="form-control js-live"
name="ogimage[subtitle]" value="{formData.subtitle}"
placeholder="Short description" />
</div>
<div class="col-2">
<label class="form-label">Size</label>
<input type="number" class="form-control js-live"
name="ogimage[subtitle_size]" value="{formData.subtitle_size}"
min="14" max="80" />
</div>
<div class="col-2">
<label class="form-label">Colour</label>
<input type="color" class="form-control form-control-color js-live"
name="ogimage[subtitle_color]" id="subtitleColor"
value="{formData.subtitle_color}" />
</div>
</div>
</div>
</div>
<!-- Label / Badge -->
<div class="card mb-3">
<div class="card-header"><strong>Label / Badge</strong></div>
<div class="card-body">
<div class="row mb-3">
<div class="col-6">
<label class="form-label">Label text (leave blank to hide)</label>
<input type="text" class="form-control js-live"
name="ogimage[label_text]" value="{formData.label_text}"
placeholder="e.g. VITEC" />
</div>
<div class="col-3">
<label class="form-label">Background</label>
<input type="color" class="form-control form-control-color js-live"
name="ogimage[label_bg_color]" id="labelBg"
value="{formData.label_bg_color}" />
</div>
<div class="col-3">
<label class="form-label">Text colour</label>
<input type="color" class="form-control form-control-color js-live"
name="ogimage[label_text_color]" id="labelText"
value="{formData.label_text_color}" />
</div>
</div>
<div class="mb-2">
<label class="form-label">Position</label>
<select class="form-select js-live" name="ogimage[label_position]">
<option value="top-left" <f:if condition="{formData.label_position} == 'top-left'">selected</f:if>>Top Left</option>
<option value="top-right" <f:if condition="{formData.label_position} == 'top-right'">selected</f:if>>Top Right</option>
<option value="bottom-left" <f:if condition="{formData.label_position} == 'bottom-left'">selected</f:if>>Bottom Left</option>
<option value="bottom-right"<f:if condition="{formData.label_position} == 'bottom-right'">selected</f:if>>Bottom Right</option>
</select>
</div>
</div>
</div>
<!-- Output -->
<div class="card mb-3">
<div class="card-header"><strong>Output</strong></div>
<div class="card-body row">
<div class="col-4">
<label class="form-label">Format</label>
<select class="form-select" name="ogimage[output_format]">
<option value="jpg" <f:if condition="{formData.output_format} == 'jpg'">selected</f:if>>JPG</option>
<option value="png" <f:if condition="{formData.output_format} == 'png'">selected</f:if>>PNG</option>
</select>
</div>
<div class="col-4">
<label class="form-label">Quality (JPG): <span id="qualLabel">{formData.output_quality}</span></label>
<input type="range" class="form-range" min="50" max="100" step="5"
name="ogimage[output_quality]" value="{formData.output_quality}"
oninput="document.getElementById('qualLabel').textContent=this.value"/>
</div>
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="button" class="btn btn-secondary" id="btnPreview">
Preview
</button>
<button type="submit" class="btn btn-primary">
Save Image to fileadmin
</button>
</div>
</form>
</div><!-- /col form -->
<!-- ══════════════════════════════════════════════════════
RIGHT: PREVIEW + SAVED IMAGES
═══════════════════════════════════════════════════════ -->
<div class="col-md-5">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Preview <small class="text-muted">(1200 × 630)</small></strong>
<span id="previewSpinner" class="spinner-border spinner-border-sm text-secondary d-none" role="status"></span>
</div>
<div class="card-body p-2 bg-secondary">
<div style="position:relative; padding-top: 52.5%;"><!-- 630/1200 -->
<img id="ogPreviewImg" src="" alt="preview"
style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;display:none;" />
<div id="ogPreviewPlaceholder"
style="position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;">
Click "Preview" to render
</div>
</div>
</div>
<div class="card-footer text-end">
<a id="previewDownload" href="#" download="og-preview.png"
class="btn btn-sm btn-outline-secondary d-none">
Download preview
</a>
</div>
</div>
<f:if condition="{savedImages}">
<div class="card">
<div class="card-header"><strong>Recent saved images</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead><tr><th>File</th><th>Created</th><th></th></tr></thead>
<tbody>
<f:for each="{savedImages}" as="img">
<tr>
<td class="text-truncate" style="max-width:140px">{img.name}</td>
<td><small>{img.created}</small></td>
<td>
<a href="{img.path}" target="_blank" class="btn btn-xs btn-outline-primary p-1 lh-1">
View
</a>
</td>
</tr>
</f:for>
</tbody>
</table>
</div>
</div>
</f:if>
</div><!-- /col preview -->
</div><!-- /row -->
</f:section>
</html>

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<rect x="1.5" y="2.5" width="13" height="11" rx="1" fill="none" stroke="#000" stroke-width="1.2"/>
<rect x="3.5" y="4.5" width="9" height="2.2" rx="0.3" fill="#000" opacity="0.55"/>
<rect x="3.5" y="7.6" width="9" height="2.2" rx="0.3" fill="#000" opacity="0.35"/>
<rect x="3.5" y="10.7" width="9" height="1.6" rx="0.3" fill="#000" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
<rect x="1" y="3" width="14" height="10" rx="1.5" fill="#e63946"/>
<rect x="1" y="3" width="14" height="10" rx="1.5" stroke="#c1121f" stroke-width="0.5"/>
<text x="2" y="10.5" font-size="4.5" font-family="sans-serif" fill="#fff" font-weight="bold">OG</text>
<circle cx="11.5" cy="6.5" r="2" fill="#fff" opacity="0.9"/>
<polyline points="1,11 5,7 8,9.5 11,6 15,10" stroke="#fff" stroke-width="1" fill="none" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -0,0 +1,173 @@
/**
* OG Image Backend Module live preview via AJAX
* EXT:vitec/Resources/Public/Javascript/og-image-preview.js
*/
(function () {
'use strict';
/** Debounce helper */
function debounce(fn, ms) {
let t;
return function (...args) {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), ms);
};
}
/** Collect all form values into a FormData-compatible plain object */
function collectFormData() {
const form = document.getElementById('vitecOgForm');
if (!form) return {};
const fd = new FormData(form);
const out = {};
for (const [key, val] of fd.entries()) {
// key looks like "ogimage[something]"
const match = key.match(/^ogimage\[(.+)]$/);
if (match) {
out[key] = val;
}
}
return out;
}
/** POST to the preview endpoint, update the <img> */
async function requestPreview() {
const form = document.getElementById('vitecOgForm');
const spinner = document.getElementById('previewSpinner');
const img = document.getElementById('ogPreviewImg');
const placeholder = document.getElementById('ogPreviewPlaceholder');
const dl = document.getElementById('previewDownload');
if (!form) return;
if (spinner) spinner.classList.remove('d-none');
try {
const body = new URLSearchParams(collectFormData());
const previewUrl = form.dataset.previewUrl || window.location.href;
const resp = await fetch(previewUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
if (!resp.ok) throw new Error('Server error ' + resp.status);
const json = await resp.json();
if (json.success && json.dataUri) {
img.src = json.dataUri;
img.style.display = 'block';
if (placeholder) placeholder.style.display = 'none';
if (dl) {
dl.href = json.dataUri;
dl.classList.remove('d-none');
}
} else {
console.warn('[OG Preview]', json.message);
}
} catch (e) {
console.error('[OG Preview] fetch failed:', e);
} finally {
if (spinner) spinner.classList.add('d-none');
}
}
const debouncedPreview = debounce(requestPreview, 600);
// ── Wire up events once DOM is ready ──────────────────────────────
document.addEventListener('DOMContentLoaded', function () {
// Manual preview button
const btn = document.getElementById('btnPreview');
if (btn) {
btn.addEventListener('click', requestPreview);
}
// Auto-preview on any .js-live input change
document.querySelectorAll('.js-live').forEach(function (el) {
el.addEventListener('input', debouncedPreview);
el.addEventListener('change', debouncedPreview);
});
// Background type toggle (show colour vs image picker)
function syncBgType() {
const colorWrap = document.getElementById('bg-color-wrap');
const imageWrap = document.getElementById('bg-image-wrap');
const selected = document.querySelector('input.js-bg-type:checked');
if (!selected) return;
if (selected.value === 'image') {
colorWrap && (colorWrap.style.display = 'none');
imageWrap && (imageWrap.style.display = 'block');
} else {
colorWrap && (colorWrap.style.display = 'block');
imageWrap && (imageWrap.style.display = 'none');
}
}
document.querySelectorAll('input.js-bg-type').forEach(function (el) {
el.addEventListener('change', function () {
syncBgType();
debouncedPreview();
});
});
const bgImageSelect = document.querySelector('select[name="ogimage[bg_image]"]');
if (bgImageSelect) {
bgImageSelect.addEventListener('change', function () {
const imageTypeRadio = document.querySelector('input.js-bg-type[value="image"]');
if (imageTypeRadio) {
imageTypeRadio.checked = true;
}
syncBgType();
requestPreview();
});
}
syncBgType(); // initialise on load
// Hex text ↔ colour picker sync
document.querySelectorAll('.js-hex-sync').forEach(function (hexInput) {
const targetId = hexInput.dataset.target;
const picker = document.getElementById(targetId);
if (!picker) return;
picker.addEventListener('input', function () {
hexInput.value = picker.value;
});
hexInput.addEventListener('input', function () {
const val = hexInput.value.trim();
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
picker.value = val;
picker.dispatchEvent(new Event('input', { bubbles: true }));
}
});
});
// Preset buttons
document.querySelectorAll('.js-preset').forEach(function (btn) {
btn.addEventListener('click', function () {
const bg = btn.dataset.bg;
const tc = btn.dataset.titleColor;
const sc = btn.dataset.subtitleColor;
const bgInput = document.getElementById('bgColor');
if (bgInput && bg) { bgInput.value = bg; bgInput.dispatchEvent(new Event('input', {bubbles:true})); }
const tInput = document.getElementById('titleColor');
if (tInput && tc) { tInput.value = tc; tInput.dispatchEvent(new Event('input', {bubbles:true})); }
const sInput = document.getElementById('subtitleColor');
if (sInput && sc) { sInput.value = sc; sInput.dispatchEvent(new Event('input', {bubbles:true})); }
debouncedPreview();
});
});
});
})();