cybermax / sigsprig /app.js
CyberMax-tools's picture
Deploy sigsprig to the hub
f2a1c24 verified
Raw History Blame Contribute Delete
17.2 kB
/* Sigsprig app: form -> live preview -> copy / download / save / team ZIP. All data stays in this browser.
* (c) 2026 CyberMax. All rights reserved. */
(function () {
'use strict';
var Sig = window.Sig, Pro = window.CMXPro;
var APP = 'https://cybermax-tools-cybermax.static.hf.space/sigsprig/';
var LOCAL = new URL('./', location.href).href;
var K_DRAFT = 'sigsprig:draft', K_SAVED = 'sigsprig:saved', K_TEAM = 'sigsprig:team';
var FREE_SAVES = 1;
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
var state = { template: 'classic', data: null };
var last = null;
function load(k, d) { try { var v = JSON.parse(localStorage.getItem(k) || 'null'); return v == null ? d : v; } catch (e) { return d; } }
function store(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) { /* private mode */ } }
function toast(m) { Pro.toast(m); }
function isPro() { return Pro.isPro(); }
function tplInfo(id) { return Sig.TEMPLATES.filter(function (t) { return t.id === id; })[0] || Sig.TEMPLATES[0]; }
function locked(id) { return !isPro() && !tplInfo(id).free; }
function local(html) { return html.split(APP).join(LOCAL); }
// ---------- build the form ----------
var fontSel = $('#f-font');
Object.keys(Sig.FONTS).forEach(function (f) { var o = document.createElement('option'); o.value = o.textContent = f; o.style.fontFamily = Sig.FONTS[f]; fontSel.appendChild(o); });
var sf = $('#social-fields');
Sig.SOCIALS.forEach(function (s) {
var l = document.createElement('label');
l.innerHTML = '<span class="soc"><img src="icons/dark/' + s[0] + '.png" width="16" height="16" alt=""> ' + s[1] + '</span><input data-s="' + s[0] + '" placeholder="' + (s[0] === 'calendar' ? 'https://cal.com/you' : s[0] === 'whatsapp' ? '+1 555 010 2290' : '@handle or link') + '">';
sf.appendChild(l);
});
var tl = $('#tpl-list');
Sig.TEMPLATES.forEach(function (t) {
var b = document.createElement('button');
b.type = 'button'; b.className = 'tpl'; b.setAttribute('role', 'radio'); b.dataset.tpl = t.id;
b.innerHTML = '<b>' + t.name + '</b><span>' + t.desc + '</span>' + (t.free ? '' : '<i class="pro-tag">PRO</i>');
b.onclick = function () { state.template = t.id; render(); save(); };
tl.appendChild(b);
});
function collect() {
var d = { socials: {} };
$$('[data-k]').forEach(function (el) { d[el.dataset.k] = el.value; });
$$('[data-s]').forEach(function (el) { d.socials[el.dataset.s] = el.value; });
d.fontSize = Number(d.fontSize); d.photoSize = Number(d.photoSize); d.logoWidth = Number(d.logoWidth);
return d;
}
function fill(d) {
d = d || {};
$$('[data-k]').forEach(function (el) {
var v = d[el.dataset.k];
if (el.type === 'color') el.value = /^#[0-9a-f]{6}$/i.test(v || '') ? v.toLowerCase() : el.defaultValue;
else if (el.tagName === 'SELECT') el.value = v != null && v !== '' ? String(v) : el.options[el.dataset.k === 'fontSize' ? 2 : 0].value;
else if (el.type === 'range') el.value = v || el.defaultValue;
else el.value = v == null ? '' : v;
});
$$('[data-s]').forEach(function (el) { el.value = (d.socials || {})[el.dataset.s] || ''; });
}
// ---------- render ----------
function render() {
var data = collect();
state.data = data;
var pro = isPro();
var b = Sig.build(data, { template: state.template, pro: pro });
last = b;
$('#preview').innerHTML = local(b.html);
// CTA preview for free users who filled it in
var cp = $('#cta-preview');
if (!pro && Sig.hasCta(Sig.normalize(data))) {
cp.hidden = false;
cp.innerHTML = '<div class="pro-preview"><span class="pro-tag">PRO</span> Button and banner are added with Pro:</div>' + local(Sig.ctaPreview(data));
} else { cp.hidden = true; cp.innerHTML = ''; }
var chk = Sig.check(b.html);
var line = $('#check-line');
line.textContent = (chk.ok ? 'Email-safe ✓ · ' : 'Check ✗ · ') + chk.size.toLocaleString('en-US') + ' / 10,000 characters';
line.className = 'chip ' + (chk.ok && !chk.warnings.length ? 'good' : chk.ok ? 'warn' : 'bad');
$('#issues').innerHTML = chk.errors.concat(chk.warnings).map(function (m) { return '<li>' + Sig.esc(m) + '</li>'; }).join('');
$$('.tpl').forEach(function (el) {
var on = el.dataset.tpl === state.template;
el.setAttribute('aria-checked', on ? 'true' : 'false');
el.classList.toggle('on', on);
el.classList.toggle('locked', locked(el.dataset.tpl));
});
$('#tpl-note').textContent = locked(state.template) ? '"' + tplInfo(state.template).name + '" is a Pro template: preview it freely; copying and downloading need Pro.' : (pro ? 'All 12 templates unlocked.' : '4 of 12 templates are free. PRO templates can be previewed.');
$('#photoSize-out').textContent = data.photoSize;
$('#logoWidth-out').textContent = data.logoWidth;
$('#embed-warn').hidden = !/^data:/.test(data.photo || '');
renderSaved();
renderTeam();
}
var timer;
function save() { clearTimeout(timer); timer = setTimeout(function () { store(K_DRAFT, { template: state.template, data: collect() }); }, 250); }
$('#editor').addEventListener('input', function () { render(); save(); });
$('#editor').addEventListener('change', function () { render(); save(); });
// ---------- outputs ----------
function gate(action) {
if (locked(state.template)) {
Pro.upsell('"' + tplInfo(state.template).name + '" is a Pro template. Pick a free one (Classic, Stacked, Compact, Bold bar) or unlock all 12.', 'template');
return false;
}
return true;
}
function note() { return last && last.ctaDropped ? ' (Without the button/banner: that is a Pro extra.)' : ''; }
function copyRich(html, text) {
if (navigator.clipboard && window.ClipboardItem && window.isSecureContext) {
return navigator.clipboard.write([new ClipboardItem({ 'text/html': new Blob([html], { type: 'text/html' }), 'text/plain': new Blob([text], { type: 'text/plain' }) })])
.then(function () { return true; }, function () { return legacyCopy(html, true); });
}
return Promise.resolve(legacyCopy(html, true));
}
function copyText(text) {
if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text).then(function () { return true; }, function () { return legacyCopy(text, false); });
return Promise.resolve(legacyCopy(text, false));
}
function legacyCopy(content, rich) {
var el = document.createElement(rich ? 'div' : 'textarea');
el.setAttribute('aria-hidden', 'true');
el.style.cssText = 'position:fixed;left:-9999px;top:0;background:#fff';
if (rich) { el.contentEditable = 'true'; el.innerHTML = content; } else el.value = content;
document.body.appendChild(el);
var ok = false;
try {
if (rich) { var r = document.createRange(); r.selectNodeContents(el); var s = getSelection(); s.removeAllRanges(); s.addRange(r); } else el.select();
ok = document.execCommand('copy');
} catch (e) { ok = false; }
el.remove();
if (getSelection) getSelection().removeAllRanges();
return ok;
}
$('#copy-rich').onclick = function () {
if (!gate()) return;
copyRich(last.html, last.text).then(function (ok) {
if (ok) { toast('Signature copied. Paste it into your email settings (guides below).' + note()); firstResult(); }
else toast('Copy was blocked by the browser: select the preview and press Ctrl+C.');
});
};
$('#copy-html').onclick = function () {
if (!gate()) return;
copyText(last.html).then(function (ok) { toast(ok ? 'HTML source copied.' + note() : 'Copy was blocked by the browser.'); if (ok) firstResult(); });
};
$('#dl-htm').onclick = function () {
if (!gate()) return;
var d = Sig.normalize(state.data);
Pro.download(Sig.fileName(d.name || 'signature', {}), Sig.doc(last.html, (d.name || 'Email') + ' signature'), 'text/html;charset=utf-8');
toast('Downloaded. Open it in a browser to copy, or use it with Outlook or Thunderbird.' + note()); firstResult();
};
function firstResult() { document.body.classList.add('has-result'); }
// ---------- saved signatures ----------
function saved() { return load(K_SAVED, []); }
function renderSaved() {
var list = saved(), pro = isPro();
$('#limit-line').textContent = pro ? list.length + ' saved · unlimited with Pro' : list.length + ' of ' + FREE_SAVES + ' free saved signature used' + (list.length >= FREE_SAVES ? '. Pro saves unlimited signatures.' : '.');
$('#saved-list').innerHTML = list.map(function (s, i) {
return '<li><span><b>' + Sig.esc(s.label) + '</b> <small>' + Sig.esc(tplInfo(s.template).name) + ' · ' + Sig.esc(s.date) + '</small></span><span><button type="button" class="btn ghost small" data-load="' + i + '">Load</button> <button type="button" class="btn ghost small" data-del="' + i + '" aria-label="Delete ' + Sig.esc(s.label) + '">Delete</button></span></li>';
}).join('') || '<li class="muted">Nothing saved yet.</li>';
}
$('#save-sig').onclick = function () {
var list = saved();
if (!isPro() && list.length >= FREE_SAVES) {
Pro.upsell('The free plan keeps 1 saved signature. Pro saves as many as you like.', 'saves');
return;
}
var d = collect();
list.push({ label: (d.name || 'Signature') + (d.company ? ' · ' + d.company : ''), template: state.template, data: d, date: new Date().toISOString().slice(0, 10) });
store(K_SAVED, list);
renderSaved();
toast('Saved in this browser.');
};
$('#saved-list').onclick = function (e) {
var t = e.target, list = saved();
if (t.dataset.load != null) { var s = list[+t.dataset.load]; if (s) { state.template = s.template; fill(s.data); render(); save(); toast('Loaded "' + s.label + '".'); } }
if (t.dataset.del != null) { list.splice(+t.dataset.del, 1); store(K_SAVED, list); renderSaved(); }
};
// ---------- photo embed ----------
$('#photo-file').onchange = function () {
var f = this.files && this.files[0];
if (!f) return;
var fr = new FileReader();
fr.onload = function () {
var im = new Image();
im.onload = function () {
var px = Math.min(200, Math.max(96, (Number(collect().photoSize) || 84) * 2));
var cv = document.createElement('canvas'); cv.width = cv.height = px;
var s = Math.min(im.width, im.height), cx = cv.getContext('2d');
cx.fillStyle = '#fff'; cx.fillRect(0, 0, px, px);
cx.drawImage(im, (im.width - s) / 2, (im.height - s) / 2, s, s, 0, 0, px, px);
$('#f-photo').value = cv.toDataURL('image/jpeg', 0.8);
render(); save();
toast('Photo embedded (cropped square, resized to ' + px + ' px). It stays in this browser.');
};
im.src = fr.result;
};
fr.readAsDataURL(f);
};
// ---------- team mode ----------
var SAMPLE_TEAM = 'name,title,email,phone,photo\nMaya Torres,Founder & Lead Designer,maya@northfield.example,+1 (555) 014-2290,' + APP + 'sample/photo.png\nAna Ruiz,Operations Lead,ana@northfield.example,+1 (555) 014-2291,\nBo Chen,Account Manager,bo@northfield.example,+1 (555) 014-2292,\n"Okafor, Dee",Studio Assistant,dee@northfield.example,,';
var teamCache = [];
function renderTeam() {
var txt = $('#team-csv').value;
var g = $('#team-gallery'), st = $('#team-status');
if (!txt.trim()) { g.innerHTML = ''; st.textContent = 'Paste a CSV or load the sample to see everyone\'s signature.'; teamCache = []; return; }
var p = Sig.people(txt);
var pro = isPro();
teamCache = Sig.team(state.data || collect(), p.people, { template: state.template, pro: pro });
var show = pro ? teamCache : teamCache.slice(0, 3);
st.textContent = p.people.length + ' ' + (p.people.length === 1 ? 'person' : 'people') + ' · template ' + tplInfo(state.template).name + (pro ? '' : ' · free preview shows the first 3; the ZIP download is Pro') + (p.errors.length ? ' · ' + p.errors.join(' ') : '');
g.innerHTML = show.map(function (s) { return '<figure class="g-item"><figcaption>' + Sig.esc(s.name) + ' <small>' + Sig.esc(s.file) + '</small></figcaption><div class="g-sig">' + local(s.html) + '</div></figure>'; }).join('');
}
$('#team-csv').addEventListener('input', function () { store(K_TEAM, this.value); renderTeam(); });
$('#team-sample').onclick = function () { $('#team-csv').value = SAMPLE_TEAM; store(K_TEAM, SAMPLE_TEAM); renderTeam(); };
$('#team-file').onchange = function () {
var f = this.files && this.files[0]; if (!f) return;
f.text().then(function (t) { $('#team-csv').value = t; store(K_TEAM, t); renderTeam(); });
};
$('#team-zip').onclick = function () {
if (!teamCache.length) { toast('Add your team first: paste a CSV or load the sample.'); return; }
if (!isPro()) { Pro.upsell('Team mode downloads are part of Pro and Team. The Team licence covers up to 50 people.', 'team'); return; }
if (locked(state.template)) return gate();
var files = teamCache.map(function (s) { return { name: s.file, data: Sig.doc(s.html, s.name + ' signature') }; });
files.unshift({ name: 'index.htm', data: Sig.gallery(teamCache, Sig.normalize(state.data).company) });
Pro.download('team-signatures.zip', new Blob([Sig.zip(files)], { type: 'application/zip' }));
toast('ZIP with ' + teamCache.length + ' signatures downloaded.'); firstResult();
};
// ---------- backup ----------
$('#export-json').onclick = function () {
var b = { app: 'sigsprig', version: 1, exported: new Date().toISOString(), draft: { template: state.template, data: collect() }, saved: saved(), team: $('#team-csv').value };
Pro.download('sigsprig-backup.json', JSON.stringify(b, null, 1), 'application/json');
};
$('#import-json').onchange = function () {
var f = this.files && this.files[0]; if (!f) return;
f.text().then(function (t) {
var b; try { b = JSON.parse(t); } catch (e) { b = null; }
if (!b || b.app !== 'sigsprig') { toast('That is not a Sigsprig backup file.'); return; }
var list = Array.isArray(b.saved) ? b.saved : [];
if (!isPro() && list.length > FREE_SAVES) { toast('Imported the first saved signature (free keeps 1; Pro keeps all).'); list = list.slice(0, FREE_SAVES); }
store(K_SAVED, list);
if (b.draft) { state.template = b.draft.template || 'classic'; fill(b.draft.data); }
if (typeof b.team === 'string') { $('#team-csv').value = b.team; store(K_TEAM, b.team); }
render(); save(); toast('Backup imported.');
});
};
$('#reset').onclick = function () {
if (!confirm('Clear the form? Saved signatures are kept.')) return;
state.template = 'classic'; fill({ photoShape: 'circle', iconStyle: 'dark', font: 'Arial', fontSize: 13, color: '#1F7A5A', textColor: '#1F2328' }); render(); save();
};
$('#load-example').onclick = function () {
state.template = 'classic'; fill(Sig.EXAMPLE); render(); save();
$('#app').scrollIntoView({ behavior: 'smooth', block: 'start' });
};
// ---------- install guide tabs ----------
var tabs = $$('#install-tabs [role=tab]');
function pick(t) {
tabs.forEach(function (x) { var on = x === t; x.setAttribute('aria-selected', on); x.tabIndex = on ? 0 : -1; $('#' + x.getAttribute('aria-controls')).hidden = !on; });
}
tabs.forEach(function (t, i) {
t.onclick = function () { pick(t); };
t.onkeydown = function (e) {
var n = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (n) { var x = tabs[(i + n + tabs.length) % tabs.length]; pick(x); x.focus(); e.preventDefault(); }
};
});
pick(tabs[/Mac|iPhone|iPad/.test(navigator.userAgent) && /iPhone|iPad/.test(navigator.userAgent) ? 4 : 0]);
// ---------- Pro state ----------
Pro.onChange(function (on) {
var pill = $('#pro-pill');
pill.textContent = on ? 'Pro ✓' : 'Pro';
pill.classList.toggle('on', on);
$('#midcta').hidden = on;
$('#free-line').textContent = on ? 'Pro is on: all templates, no credit link.' : 'Free: 4 templates · 1 saved signature · small credit link';
if (state.data) render();
});
// ---------- PWA ----------
var deferred = null;
window.addEventListener('beforeinstallprompt', function (e) { e.preventDefault(); deferred = e; $('#install-btn').hidden = false; });
$('#install-btn').onclick = function () { if (!deferred) return; deferred.prompt(); deferred.userChoice.then(function () { deferred = null; $('#install-btn').hidden = true; }); };
if ('serviceWorker' in navigator && (location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1')) {
navigator.serviceWorker.register('sw.js').catch(function () { /* offline cache is optional */ });
}
// ---------- start ----------
var draft = load(K_DRAFT, null);
if (draft && draft.data) { state.template = draft.template || 'classic'; fill(draft.data); }
else fill(Sig.EXAMPLE);
$('#team-csv').value = load(K_TEAM, '') || '';
render();
document.body.dataset.ready = '1';
})();