chore: migrate mail themes to shared framework and locked upstream inputs
This commit is contained in:
1 parent
5c0589f6f6
commit
fec3ace600
225 files changed
+4718
-15807
No files matched your search
+121
-50
@@ -239,6 +239,11 @@ html,body,#preview-area,#panel-body{
|
||||
<select id="sel-template" onchange="switchTemplate(this.value)"></select>
|
||||
</span>
|
||||
<span class="sep"></span>
|
||||
<span class="ctl-group">
|
||||
<label>Language</label>
|
||||
<select id="sel-locale" onchange="switchLocale(this.value)"></select>
|
||||
</span>
|
||||
<span class="sep"></span>
|
||||
<span class="ctl-group">
|
||||
<label>View</label>
|
||||
<select id="sel-client" onchange="switchClient(this.value)">
|
||||
@@ -323,10 +328,14 @@ html,body,#preview-area,#panel-body{
|
||||
//
|
||||
// State — REGISTRY and PARAMS are loaded from rendered.js (single source of truth)
|
||||
//
|
||||
const state = { theme:'', template:'', client:'modern', viewport:'desktop' };
|
||||
const state = { theme:'', template:'', client:'modern', viewport:'desktop', locale:'en-US' };
|
||||
let REGISTRY = {};
|
||||
let PARAMS = {};
|
||||
let rendered = null; // window.__RENDERED__
|
||||
let rendered = null;
|
||||
let manifest = null;
|
||||
let localeRequest = 0;
|
||||
let renderRequest = 0, renderTimer;
|
||||
const localeLoads = new Map();
|
||||
let devMode = false; // true once SSE connects
|
||||
|
||||
//
|
||||
@@ -338,20 +347,31 @@ function transform(html, mode) {
|
||||
const e = html.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
return '<!DOCTYPE html><html><head><style>body{background:#0d1117;color:#c9d1d9;font-family:Consolas,monospace;padding:16px;margin:0}pre{white-space:pre-wrap;word-break:break-all;font-size:11px;line-height:1.6;margin:0}</style></head><body><pre>'+e+'</pre></body></html>';
|
||||
}
|
||||
return html;
|
||||
// Gitea sends status icons as CID attachments. Preview has no MIME message,
|
||||
// so display equivalent local SVG icons without changing install templates.
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
// Static preview has no deployed instance; use the generated official asset.
|
||||
// Installable mail templates keep the instance's {{AppUrl}} image URL.
|
||||
doc.querySelectorAll('img.email-logo').forEach(img => { if(manifest?.logo) img.src=manifest.logo; });
|
||||
doc.querySelectorAll('img[src^="cid:"]').forEach(img => {
|
||||
const color = img.classList.contains('status-success') ? '#2da44e' : '#697077';
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><circle cx="9" cy="9" r="7" fill="'+color+'"/><path d="M5 9l3 3 5-6" fill="none" stroke="white" stroke-width="2"/></svg>';
|
||||
img.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
|
||||
});
|
||||
return '<!DOCTYPE html>' + doc.documentElement.outerHTML;
|
||||
}
|
||||
|
||||
//
|
||||
// Render preview
|
||||
//
|
||||
function render() {
|
||||
const request=++renderRequest;
|
||||
clearTimeout(renderTimer);
|
||||
const frame = document.getElementById('preview-frame');
|
||||
const loading = document.getElementById('loading');
|
||||
if(!rendered) return;
|
||||
|
||||
if (location.protocol !== 'file:') {
|
||||
frame.setAttribute('sandbox', '');
|
||||
}
|
||||
frame.setAttribute('sandbox', '');
|
||||
|
||||
frame.style.display = 'none';
|
||||
loading.style.display = 'flex';
|
||||
@@ -363,14 +383,15 @@ function render() {
|
||||
|
||||
let done = false;
|
||||
function finish() {
|
||||
if (done) return; done = true;
|
||||
if (done || request!==renderRequest) return; done = true;
|
||||
clearTimeout(renderTimer);
|
||||
loading.style.display = 'none';
|
||||
frame.style.display = 'block';
|
||||
frame.onload = null;
|
||||
}
|
||||
frame.onload = finish;
|
||||
frame.srcdoc = content;
|
||||
setTimeout(finish, 5000);
|
||||
renderTimer=setTimeout(finish, 5000);
|
||||
updatePanel();
|
||||
}
|
||||
|
||||
@@ -391,10 +412,11 @@ function toggleSection(id) {
|
||||
// Panel content updates
|
||||
//
|
||||
function updatePanel() {
|
||||
const escape = v => String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
const r = REGISTRY[state.template];
|
||||
document.getElementById('panel-name').textContent = r ? r.name : state.template;
|
||||
document.getElementById('panel-desc').textContent = r ? r.desc : '';
|
||||
document.getElementById('panel-path').textContent = r ? 'themes/'+state.theme+'/'+r.path : '-';
|
||||
document.getElementById('panel-path').textContent = r ? 'build/themes/'+state.theme+'/'+r.path+' (official snapshot + theme overrides)' : '-';
|
||||
|
||||
// Template stats
|
||||
const themes = rendered ? Object.keys(rendered).length : 0;
|
||||
@@ -407,13 +429,13 @@ function updatePanel() {
|
||||
}
|
||||
}
|
||||
document.getElementById('panel-stats').textContent =
|
||||
themes + ' themes loaded · ' + totalTemplates + ' templates (' + (totalTemplates - totalFailed) + ' ok, ' + totalFailed + ' failed)';
|
||||
manifest.upstream + ' · ' + state.locale + ' · ' + themes + ' themes · ' + totalTemplates + ' templates (' + (totalTemplates - totalFailed) + ' ok, ' + totalFailed + ' failed)';
|
||||
|
||||
const p = PARAMS[state.template] || {};
|
||||
let rows = '';
|
||||
for(const [k,v] of Object.entries(p)){
|
||||
const isReq = !k.includes('RenderedNote') && !k.includes('Attachments') && !k.includes('DisableDownload') && !k.includes('IsMention') && !k.includes('IsPull') && !k.includes('IsForcePush') && !k.includes('ReviewComments') && !k.includes('Commits') && !k.includes('BaseBranch');
|
||||
rows += '<tr><td>.'+k+'</td><td>'+String(v)+'</td><td><span class="badge '+(isReq?'badge-req':'badge-opt')+'">'+(isReq?'required':'optional')+'</span></td></tr>';
|
||||
rows += '<tr><td>.'+escape(k)+'</td><td>'+escape(v)+'</td><td><span class="badge '+(isReq?'badge-req':'badge-opt')+'">'+(isReq?'required':'optional')+'</span></td></tr>';
|
||||
}
|
||||
document.getElementById('panel-params').innerHTML = rows || '<tr><td colspan="3" style="color:var(--muted)">-</td></tr>';
|
||||
}
|
||||
@@ -422,7 +444,7 @@ function updatePanel() {
|
||||
// Events
|
||||
//
|
||||
function switchTheme(v) { state.theme=v; render(); }
|
||||
function switchTemplate(v) { state.template=v; render(); }
|
||||
function switchTemplate(v) { state.template=v; switchLocale(state.locale); }
|
||||
function switchClient(v) { state.client=v; render(); }
|
||||
function setViewport(v) {
|
||||
state.viewport=v;
|
||||
@@ -443,7 +465,7 @@ document.addEventListener('keydown',e=>{
|
||||
if(e.key==='p'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ togglePanel(); return; }
|
||||
|
||||
// ← → cycle focus between Theme / Template / Client selects
|
||||
const selects=['sel-theme','sel-template','sel-client'];
|
||||
const selects=['sel-theme','sel-template','sel-locale','sel-client'];
|
||||
const activeId=document.activeElement?.id;
|
||||
const cur=selects.indexOf(activeId);
|
||||
|
||||
@@ -460,38 +482,85 @@ document.addEventListener('keydown',e=>{
|
||||
});
|
||||
|
||||
//
|
||||
// Init — load everything from rendered.js (single source of truth)
|
||||
//
|
||||
(function(){
|
||||
if(window.__REGISTRY__) REGISTRY = window.__REGISTRY__;
|
||||
if(window.__PARAMS__) PARAMS = window.__PARAMS__;
|
||||
|
||||
if(window.__RENDERED__){
|
||||
rendered = window.__RENDERED__;
|
||||
|
||||
const themeKeys = Object.keys(rendered).sort();
|
||||
const selTheme = document.getElementById('sel-theme');
|
||||
themeKeys.forEach(t => {
|
||||
const o = document.createElement('option');
|
||||
o.value = t;
|
||||
o.textContent = t.charAt(0).toUpperCase() + t.slice(1);
|
||||
selTheme.appendChild(o);
|
||||
});
|
||||
state.theme = themeKeys[0] || '';
|
||||
|
||||
const selTpl = document.getElementById('sel-template');
|
||||
for(const [id, t] of Object.entries(REGISTRY)){
|
||||
const o = document.createElement('option');
|
||||
o.value = id;
|
||||
o.textContent = t.name;
|
||||
selTpl.appendChild(o);
|
||||
}
|
||||
state.template = Object.keys(REGISTRY)[0] || '';
|
||||
} else {
|
||||
document.getElementById('loading').style.display='none';
|
||||
showMessage('rendered.js not found — run <code>cd tools && go run . preview all</code> to generate', 'error');
|
||||
// Manifest and language bundles
|
||||
function fillSelect(id, entries, selected) {
|
||||
const select = document.getElementById(id);
|
||||
select.replaceChildren();
|
||||
for (const [value,label] of entries) {
|
||||
const option = document.createElement('option');
|
||||
option.value=value; option.textContent=label; select.appendChild(option);
|
||||
}
|
||||
select.value=selected;
|
||||
}
|
||||
|
||||
function applyManifest(data) {
|
||||
if (!data || data.schema !== 1 || !data.languages.length || !data.themes.length) {
|
||||
throw new Error('Invalid preview manifest; regenerate with go run . preview all');
|
||||
}
|
||||
manifest=data; REGISTRY=data.registry; PARAMS=data.params;
|
||||
if (!data.themes.includes(state.theme)) state.theme=data.themes[0];
|
||||
if (!REGISTRY[state.template]) state.template=Object.keys(REGISTRY)[0];
|
||||
if (!data.languages.includes(state.locale)) state.locale=data.languages.includes('en-US') ? 'en-US' : data.languages[0];
|
||||
fillSelect('sel-theme',data.themes.map(t=>[t,t.charAt(0).toUpperCase()+t.slice(1)]),state.theme);
|
||||
fillSelect('sel-template',Object.entries(REGISTRY).map(([id,t])=>[id,t.name]),state.template);
|
||||
let names; try { names=new Intl.DisplayNames([navigator.language],{type:'language'}); } catch (_) {}
|
||||
fillSelect('sel-locale',data.languages.map(l=>[l,names ? l+' — '+names.of(l) : l]),state.locale);
|
||||
}
|
||||
|
||||
function loadLocale(lang) {
|
||||
if (window.__MAIL_PREVIEW_DATA__?.[lang]) return Promise.resolve(window.__MAIL_PREVIEW_DATA__[lang]);
|
||||
if (localeLoads.has(lang)) return localeLoads.get(lang);
|
||||
const promise=new Promise((resolve,reject)=>{
|
||||
const script=document.createElement('script');
|
||||
script.src='rendered/'+encodeURIComponent(lang)+'.js'+(devMode ? '?v='+Date.now() : '');
|
||||
script.onload=()=>{
|
||||
script.remove();
|
||||
const data=window.__MAIL_PREVIEW_DATA__?.[lang];
|
||||
if (data) resolve(data); else reject(new Error('Invalid language bundle '+lang));
|
||||
};
|
||||
script.onerror=()=>{ script.remove(); reject(new Error('Language bundle missing: '+lang+'; regenerate preview data')); };
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
localeLoads.set(lang,promise);
|
||||
promise.catch(()=>localeLoads.delete(lang));
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function switchLocale(lang) {
|
||||
if (!manifest?.languages.includes(lang)) return;
|
||||
state.locale=lang; rendered=null;
|
||||
const request=++localeRequest;
|
||||
document.getElementById('preview-frame').style.display='none';
|
||||
document.getElementById('loading').style.display='flex';
|
||||
try {
|
||||
const data=await loadLocale(lang);
|
||||
if (request!==localeRequest) return;
|
||||
rendered=data;
|
||||
try { localStorage.setItem('mail-preview-locale',lang); } catch (_) {}
|
||||
document.getElementById('locale-message')?.remove();
|
||||
const warnings=manifest.warnings?.[lang]?.[state.template] || [];
|
||||
const fallback=manifest.fallback?.[lang] || [];
|
||||
if (fallback.length || warnings.length) {
|
||||
const el=showMessage('',warnings.length ? 'warn' : 'info'); el.id='locale-message';
|
||||
el.textContent=warnings.join(' ') || '[FALLBACK] '+lang+': '+fallback.length+' referenced translation keys use en-US';
|
||||
}
|
||||
render();
|
||||
} catch (error) {
|
||||
if (request!==localeRequest) return;
|
||||
document.getElementById('loading').style.display='none';
|
||||
showMessage('', 'error').textContent = String(error);
|
||||
}
|
||||
}
|
||||
|
||||
(function(){
|
||||
try {
|
||||
try { state.locale=localStorage.getItem('mail-preview-locale') || 'en-US'; } catch (_) {}
|
||||
applyManifest(window.__MAIL_PREVIEW__);
|
||||
switchLocale(state.locale);
|
||||
} catch (error) {
|
||||
document.getElementById('loading').style.display='none';
|
||||
showMessage('Preview data unavailable — run <code>cd tools && go run . preview all</code> to generate','error');
|
||||
}
|
||||
render();
|
||||
})();
|
||||
|
||||
// Message helpers
|
||||
@@ -512,7 +581,7 @@ function setDevMode(on) {
|
||||
if (on) {
|
||||
clearMessages();
|
||||
showMessage(
|
||||
'Live reload active — changes to .tmpl files will auto-refresh the preview.',
|
||||
'Live reload active — theme CSS, shared controls, lock/cache and fixtures auto-refresh the preview.',
|
||||
'info'
|
||||
).id = 'msg-dev-disclaimer';
|
||||
}
|
||||
@@ -520,6 +589,7 @@ function setDevMode(on) {
|
||||
|
||||
// Dev-server live-reload (SSE)
|
||||
(function(){
|
||||
if (location.protocol === 'file:') return;
|
||||
let es;
|
||||
try {
|
||||
es = new EventSource('/events');
|
||||
@@ -533,14 +603,15 @@ function setDevMode(on) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'rendered.js?v=' + Date.now();
|
||||
script.onload = function() {
|
||||
if (window.__RENDERED__) rendered = window.__RENDERED__;
|
||||
if (window.__REGISTRY__) REGISTRY = window.__REGISTRY__;
|
||||
if (window.__PARAMS__) PARAMS = window.__PARAMS__;
|
||||
render();
|
||||
script.remove();
|
||||
window.__MAIL_PREVIEW_DATA__ = {};
|
||||
localeLoads.clear();
|
||||
applyManifest(window.__MAIL_PREVIEW__);
|
||||
switchLocale(state.locale);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} else if (msg.type === 'error') {
|
||||
showMessage(msg.message, 'error');
|
||||
showMessage('', 'error').textContent = msg.message;
|
||||
}
|
||||
} catch(_) {}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user