Spaces:
Running
Running
Download pactsprig/app.js from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 26.3 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/pactsprig/app.js
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/pactsprig/app.js
-
curl -L -o app.js https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/pactsprig/app.js
26.3 kB
| /* Pactsprig app: agreement picker, guided Cover Page form, live preview, PDF / DOCX / print, saved companies, | |
| * presets and backup. Everything stays in this browser. © 2026 CyberMax. All rights reserved. */ | |
| (function () { | |
| "use strict"; | |
| var C = window.PactCore, P = window.PactPDF, D = window.PactDOCX, X = window.CMXPro; | |
| var KEY = "pactsprig:v1", FREE_PDFS = 3; | |
| var AGREEMENTS = [ | |
| { id: "mnda", name: "Mutual NDA", desc: "Protect what both sides share", free: true }, | |
| { id: "psa", name: "Professional Services", desc: "Key Terms + Statement of Work", free: false }, | |
| { id: "design-partner", name: "Design Partner", desc: "Early access for feedback", free: false }, | |
| ]; | |
| var tpls = {}, state, tpl, model, renderTimer; | |
| function $(id) { return document.getElementById(id); } | |
| function pro() { return X.isPro(); } | |
| function toast(m) { X.toast(m); } | |
| function upsell(msg, from) { X.upsell(msg || "That is part of Pactsprig Pro.", from); } | |
| function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); } | |
| function agreementInfo(id) { return AGREEMENTS.filter(function (a) { return a.id === id; })[0] || AGREEMENTS[0]; } | |
| function partiesEmpty() { return state.parties.every(function (p) { return !((p.company || "") + (p.name || "")).trim(); }); } | |
| function allowed() { return agreementInfo(state.agreement).free || pro(); } | |
| /* ---------- storage ---------- */ | |
| function load() { | |
| var s = null; | |
| try { s = JSON.parse(localStorage.getItem(KEY) || "null"); } catch (e) { s = null; } | |
| if (!s || typeof s !== "object") s = { fresh: true }; | |
| s.agreement = agreementInfo(s.agreement).id; | |
| s.docs = s.docs || {}; | |
| s.parties = Array.isArray(s.parties) && s.parties.length === 2 ? s.parties : [C.emptyParty(), C.emptyParty()]; | |
| s.savedParties = Array.isArray(s.savedParties) ? s.savedParties : []; | |
| s.presets = Array.isArray(s.presets) ? s.presets : []; | |
| s.page = s.page === "A4" ? "A4" : "Letter"; | |
| return s; | |
| } | |
| function save() { | |
| try { var c = Object.assign({}, state); delete c.fresh; localStorage.setItem(KEY, JSON.stringify(c)); } catch (e) { /* private mode */ } | |
| } | |
| function getTpl(id) { | |
| if (tpls[id]) return Promise.resolve(tpls[id]); | |
| return fetch("templates/" + id + ".json").then(function (r) { if (!r.ok) throw new Error("template " + id); return r.json(); }).then(function (t) { tpls[id] = t; return t; }); | |
| } | |
| /* ---------- picker ---------- */ | |
| function renderPicker() { | |
| $("picker").innerHTML = AGREEMENTS.map(function (a) { | |
| return '<button type="button" role="radio" data-agreement="' + a.id + '" aria-checked="' + (a.id === state.agreement) + '"><b>' + esc(a.name) + "</b><span>" + esc(a.desc) + "</span>" + | |
| (a.free ? '<i class="tag free">Free</i>' : '<i class="tag pro">Pro</i>') + "</button>"; | |
| }).join(""); | |
| } | |
| $("picker").addEventListener("click", function (e) { | |
| var b = e.target.closest("button[data-agreement]"); | |
| if (!b) return; | |
| switchTo(b.getAttribute("data-agreement")); | |
| }); | |
| $("picker").addEventListener("keydown", function (e) { | |
| if (["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp"].indexOf(e.key) < 0) return; | |
| var i = AGREEMENTS.findIndex(function (a) { return a.id === state.agreement; }), d = e.key === "ArrowRight" || e.key === "ArrowDown" ? 1 : -1; | |
| var next = AGREEMENTS[(i + d + AGREEMENTS.length) % AGREEMENTS.length].id; | |
| e.preventDefault(); | |
| switchTo(next).then(function () { var b = document.querySelector('[data-agreement="' + next + '"]'); if (b) b.focus(); }); | |
| }); | |
| function switchTo(id, useExample) { | |
| return getTpl(id).then(function (t) { | |
| state.agreement = id; tpl = t; | |
| var doc = state.docs[id], ex = C.example(t, C.isoToday()); | |
| if (useExample === "blank") { state.docs[id] = { values: C.defaults(t, C.isoToday()) }; state.demo = false; } | |
| else if (useExample === true || (!doc && (state.demo || state.fresh || partiesEmpty()))) { | |
| // examples come with their own parties; keep the user's real parties if they typed some | |
| state.docs[id] = { values: ex.values }; | |
| if (useExample === true || state.demo || state.fresh || partiesEmpty()) { state.parties = ex.parties; state.demo = true; } | |
| } else if (!doc) state.docs[id] = { values: C.defaults(t, C.isoToday()) }; | |
| else { | |
| // add any keys a newer template introduced | |
| if (state.demo) state.parties = ex.parties; | |
| var dv = C.defaults(t, C.isoToday()); | |
| Object.keys(dv).forEach(function (k) { if (!(k in doc.values)) doc.values[k] = dv[k]; }); | |
| } | |
| if (!agreementInfo(id).free && !pro()) toast(t.name + " is part of Pro. You can fill it in and preview it; downloads need Pro."); | |
| renderPicker(); renderParties(); renderForm(); renderPreview(); save(); | |
| }).catch(function (err) { if (window.console) console.warn(err); toast("Could not load that agreement. Check your connection and try again."); }); | |
| } | |
| /* ---------- parties ---------- */ | |
| function renderParties() { | |
| var roles = tpl.partyRoles || tpl.parties || ["Party 1", "Party 2"]; | |
| [0, 1].forEach(function (i) { | |
| $("pl-" + i).textContent = roles[i]; | |
| ["company", "name", "title", "address"].forEach(function (f) { var el = $("p" + i + "-" + f); if (el !== document.activeElement) el.value = state.parties[i][f] || ""; }); | |
| }); | |
| var opts = '<option value="">Saved companies…</option>' + state.savedParties.map(function (p, i) { return '<option value="' + i + '">' + esc(p.company || p.name) + "</option>"; }).join(""); | |
| Array.prototype.forEach.call(document.querySelectorAll("select[data-load-party]"), function (s) { s.innerHTML = opts; s.hidden = !state.savedParties.length; }); | |
| } | |
| document.querySelector(".parties").addEventListener("input", function (e) { | |
| var el = e.target, i = el.getAttribute("data-party"), f = el.getAttribute("data-field"); | |
| if (i == null || !f) return; | |
| state.parties[+i][f] = el.value; | |
| state.demo = false; | |
| schedule(); | |
| }); | |
| document.querySelector(".parties").addEventListener("click", function (e) { | |
| var b = e.target.closest("[data-save-party]"); | |
| if (!b) return; | |
| if (!pro()) return upsell("Saved companies are part of Pro: save a company once and fill it into any agreement.", "parties"); | |
| var p = Object.assign({}, state.parties[+b.getAttribute("data-save-party")]); | |
| if (!(p.company || "").trim()) return toast("Type the company name first."); | |
| var at = state.savedParties.findIndex(function (q) { return (q.company || "").toLowerCase() === p.company.toLowerCase(); }); | |
| if (at > -1) state.savedParties[at] = p; else state.savedParties.push(p); | |
| save(); renderParties(); renderPresets(); | |
| toast("Saved " + p.company + "."); | |
| }); | |
| document.querySelector(".parties").addEventListener("change", function (e) { | |
| var s = e.target.closest("select[data-load-party]"); | |
| if (!s || s.value === "") return; | |
| if (!pro()) { s.value = ""; return upsell(null, "parties"); } | |
| var p = state.savedParties[+s.value]; | |
| if (p) { state.parties[+s.getAttribute("data-load-party")] = Object.assign(C.emptyParty(), p); renderParties(); renderPreview(); save(); } | |
| s.value = ""; | |
| }); | |
| /* ---------- cover page form ---------- */ | |
| function inputFor(key, spec, vals, label) { | |
| var v = vals[key] == null ? "" : vals[key], id = "v-" + key.replace(/[^A-Za-z0-9]+/g, "-"); | |
| var aria = ' aria-label="' + esc(label) + '"'; | |
| if (spec.k === "sel") return '<select id="' + id + '" data-key="' + esc(key) + '"' + aria + ">" + spec.o.map(function (o) { return "<option" + (o === v ? " selected" : "") + ">" + esc(o) + "</option>"; }).join("") + "</select>"; | |
| if (spec.k === "date") return '<input type="date" id="' + id + '" data-key="' + esc(key) + '" value="' + esc(v) + '"' + aria + ">"; | |
| if (spec.k === "area") return '<textarea id="' + id + '" class="wide" rows="4" data-key="' + esc(key) + '" placeholder="' + esc(spec.ph || "") + '"' + aria + ">" + esc(v) + "</textarea>"; | |
| var num = spec.k === "num"; | |
| return '<input id="' + id + '" class="inl' + (num ? " num" : "") + '" data-key="' + esc(key) + '" value="' + esc(v) + '"' + (num ? ' inputmode="decimal"' : "") + ' placeholder="' + esc(spec.ph || "") + '" style="--w:' + (spec.w || 12) + 'ch"' + aria + ">"; | |
| } | |
| /** Option text with its inline inputs: "Expires {years} year(s)" -> text + <input>. */ | |
| function inline(t, key, ins, vals, label) { | |
| return String(t).replace(/\{(@?\w+)\}|([^{]+)/g, function (m, name, txt) { | |
| if (txt) return '<span class="ot">' + esc(txt) + "</span>"; | |
| if (name.charAt(0) === "@") return '<span class="ot glob">' + esc(name === "@p1" ? "[Party 1 company]" : name === "@p2" ? "[Party 2 company]" : "[Key Terms Effective Date]") + "</span>"; | |
| return ins && ins[name] ? inputFor(key + "." + name, ins[name], vals, label + " " + name) : esc(m); | |
| }); | |
| } | |
| function optionsHtml(pgId, row, vals) { | |
| var k = pgId + ":" + row.id, name = "r-" + k.replace(/[^A-Za-z0-9]+/g, "-"); | |
| var html = row.lead ? '<p class="lead">' + esc(row.lead) + "</p>" : ""; | |
| function subs(o, ok) { | |
| if (!o.sub) return ""; | |
| return '<div class="subs">' + o.sub.map(function (s) { | |
| var sk = ok + "." + s.id; | |
| return '<div class="opt-row"><label class="optl"><input type="checkbox" data-key="' + esc(sk) + '"' + (vals[sk] ? " checked" : "") + "></label><div class=\"opt-body\">" + inline(s.t, sk, s.in, vals, row.label) + "</div></div>"; | |
| }).join("") + "</div>"; | |
| } | |
| (row.options || []).forEach(function (o) { | |
| var ok = k + "." + o.id, on = row.type === "choice" ? vals[k] === o.id : !!vals[ok]; | |
| var ctl = row.type === "choice" ? '<input type="radio" name="' + name + '" data-key="' + esc(k) + '" value="' + esc(o.id) + '"' + (on ? " checked" : "") + ">" : '<input type="checkbox" data-key="' + esc(ok) + '"' + (on ? " checked" : "") + ">"; | |
| html += '<div class="opt-row' + (on ? " on" : "") + '"><label class="optl">' + ctl + '</label><div class="opt-body">' + inline(o.t, ok, o.in, vals, row.label) + (on ? subs(o, ok) : "") + "</div></div>"; | |
| }); | |
| if (row.type === "choice" && row.optional) { | |
| html += '<div class="opt-row' + (!vals[k] ? " on" : "") + '"><label class="optl"><input type="radio" name="' + name + '" data-key="' + esc(k) + '" value=""' + (!vals[k] ? " checked" : "") + '></label><div class="opt-body"><span class="ot muted">None (leave this out)</span></div></div>'; | |
| } | |
| return html; | |
| } | |
| function rowHtml(pg, row, vals) { | |
| if (row.type === "heading") return '<h3 class="fh">' + esc(row.label) + "</h3>"; | |
| var k = pg.id + ":" + row.id, id = "v-" + k.replace(/[^A-Za-z0-9]+/g, "-"); | |
| var free = row.type === "text" || row.type === "textarea"; | |
| var head = row.omitLabel ? "" : '<div class="fl"><label' + (free ? ' for="' + id + '"' : "") + ">" + esc(row.label) + (row.optional ? ' <span class="opt">optional</span>' : "") + "</label>" + (row.sub ? '<span class="fsub">' + esc(row.sub) + "</span>" : "") + "</div>"; | |
| var body; | |
| if (row.type === "text" || row.type === "textarea") { | |
| body = row.type === "text" ? '<input id="' + id + '" data-key="' + esc(k) + '" value="' + esc(vals[k]) + '" placeholder="' + esc(row.ph || "") + '">' | |
| : '<textarea id="' + id + '" rows="3" data-key="' + esc(k) + '" placeholder="' + esc(row.ph || "") + '">' + esc(vals[k]) + "</textarea>"; | |
| } else if (row.type === "line") body = '<div class="line">' + inline(row.t, k, row.in, vals, row.label) + "</div>"; | |
| else if (row.type === "fixed") body = '<div class="line fixed">' + esc(row.t) + "</div>"; | |
| else body = '<div class="opts" role="group" aria-label="' + esc(row.label || row.lead || "") + '">' + optionsHtml(pg.id, row, vals) + "</div>"; | |
| return '<div class="frow' + (row.omitLabel ? " cont" : "") + '" data-row="' + esc(k) + '">' + head + body + (row.help ? '<p class="help">' + esc(row.help) + "</p>" : "") + "</div>"; | |
| } | |
| function renderForm() { | |
| var vals = state.docs[state.agreement].values; | |
| $("cover-form").innerHTML = tpl.cover.map(function (pg, i) { | |
| var title = pg.kicker ? (pg.kicker === "SOW" ? "Statement of Work (SOW)" : pg.kicker) : "Cover Page"; | |
| return '<details class="card cover-card" open data-page="' + esc(pg.id) + '"><summary><h2>' + esc(title) + '</h2><span class="step">' + esc(tpl.short || tpl.name) + "</span></summary>" + | |
| (i === 0 ? '<p class="hint">' + esc(tpl.blurb || "") + "</p>" : "") + pg.rows.map(function (r) { return rowHtml(pg, r, vals); }).join("") + "</details>"; | |
| }).join(""); | |
| } | |
| function onField(e) { | |
| var el = e.target, key = el.getAttribute && el.getAttribute("data-key"); | |
| if (!key) return; | |
| var vals = state.docs[state.agreement].values; | |
| if (el.type === "checkbox") vals[key] = el.checked; | |
| else vals[key] = el.value; | |
| if (el.type === "checkbox" || el.type === "radio") { | |
| // re-draw this row so nested options appear / disappear | |
| var rowEl = el.closest(".frow"), rk = rowEl && rowEl.getAttribute("data-row"); | |
| var pgId = rk.split(":")[0], rid = rk.split(":")[1]; | |
| var pg = tpl.cover.filter(function (p) { return p.id === pgId; })[0], row = pg.rows.filter(function (r) { return r.id === rid; })[0]; | |
| var tmp = document.createElement("div"); | |
| tmp.innerHTML = rowHtml(pg, row, vals); | |
| rowEl.replaceWith(tmp.firstChild); | |
| var again = document.querySelector('[data-row="' + rk + '"] [data-key="' + key + '"]' + (el.type === "radio" ? '[value="' + el.value + '"]' : "")); | |
| if (again) again.focus(); | |
| } | |
| schedule(); | |
| } | |
| $("cover-form").addEventListener("input", onField); | |
| $("cover-form").addEventListener("change", function (e) { if (e.target.type === "checkbox" || e.target.type === "radio" || e.target.tagName === "SELECT") onField(e); }); | |
| /* ---------- preview ---------- */ | |
| function schedule() { clearTimeout(renderTimer); renderTimer = setTimeout(function () { renderPreview(); save(); }, 120); } | |
| function itemHtml(it) { | |
| var t = esc(it.t).split(esc(C.BLANK)).join('<span class="miss">' + C.BLANK + "</span>"); | |
| return '<div class="d-it' + (it.bullet ? " b" : "") + (it.lead ? " lead" : "") + '" style="--lvl:' + it.lvl + '">' + t + "</div>"; | |
| } | |
| function runsHtml(b) { | |
| var h = ""; | |
| if (b.l === 1 && b.h && b.r) h += "<b>" + esc(b.n + " " + b.h) + ".</b> "; | |
| else if (b.n) h += b.l <= 2 ? "<b>" + esc(b.n) + "</b> " : esc(b.n) + " "; | |
| if (b.h3) h += "<b>" + esc(b.h3) + "</b> "; | |
| (b.r || []).forEach(function (r) { h += typeof r === "string" ? esc(r) : r[0] === "b" ? "<b>" + esc(r[1]) + "</b>" : '<span class="v">' + esc(r[1]) + "</span>"; }); | |
| return h; | |
| } | |
| function renderPreview() { | |
| var d = state.docs[state.agreement]; | |
| model = C.build(tpl, { values: d.values, parties: state.parties }); | |
| var h = ""; | |
| model.pages.forEach(function (pg) { | |
| h += '<section class="d-cover">' + (pg.kicker ? '<div class="d-kicker">' + esc(pg.kicker) + "</div>" : "") + '<h2 class="d-title">' + esc(pg.title) + "</h2>"; | |
| if (pg.intro) h += '<p class="d-intro">' + esc(pg.intro) + "</p>"; | |
| if (pg.lead) h += '<p class="d-lead">' + esc(pg.lead) + "</p>"; | |
| h += '<div class="d-rows">' + pg.rows.map(function (r) { | |
| if (r.heading) return '<div class="d-head">' + esc(r.heading) + "</div>"; | |
| return '<div class="d-row"><div class="d-l"><b>' + esc(r.label) + "</b>" + (r.sub ? "<small>" + esc(r.sub) + "</small>" : "") + '</div><div class="d-r">' + r.items.map(itemHtml).join("") + "</div></div>"; | |
| }).join("") + "</div>"; | |
| if (pg.closing) h += '<p class="d-closing">' + esc(pg.closing) + "</p>"; | |
| h += '<table class="d-sign"><thead><tr><th></th>' + pg.sign.heads.map(function (x) { return "<th>" + esc(x) + "</th>"; }).join("") + "</tr></thead><tbody>" + | |
| pg.sign.rows.map(function (r) { | |
| return '<tr><th scope="row">' + esc(r.label) + (r.note ? "<small>" + esc(r.note) + "</small>" : "") + "</th>" + r.values.map(function (v) { return r.line ? '<td class="sigline"><span></span></td>' : "<td>" + (esc(v) || '<span class="miss">' + C.BLANK + "</span>") + "</td>"; }).join("") + "</tr>"; | |
| }).join("") + "</tbody></table>"; | |
| h += '<p class="d-att">' + esc(model.attribution) + "</p></section>"; | |
| }); | |
| h += '<section class="d-terms"><h2 class="d-tt">' + esc(model.termsTitle) + '</h2><p class="d-tn">' + esc(model.termsNote) + "</p>"; | |
| h += model.terms.map(function (b) { | |
| if (b.l === 1 && b.h && !b.r) return '<h3 class="d-sec">' + esc(b.n + " " + b.h) + "</h3>"; | |
| return '<p class="d-p l' + b.l + '">' + runsHtml(b) + "</p>"; | |
| }).join(""); | |
| h += '<p class="d-att end"><a href="' + esc(model.termsUrl) + '" target="_blank" rel="noopener">' + esc(model.attribution.replace(/ CC BY 4\.0\.$/, "")) + '</a> <a href="' + esc(model.licenceUrl) + '" target="_blank" rel="noopener">CC BY 4.0</a>.</p>' + | |
| '<p class="d-src">Standard Terms: ' + esc(model.termsUrl) + " · Licence: " + esc(model.licenceUrl) + "</p></section>"; | |
| h += '<p class="d-credit">' + esc(C.CREDIT) + "</p>"; | |
| if (!allowed()) h += '<div class="d-lock"><p><b>' + esc(tpl.name) + "</b> is part of Pactsprig Pro. Keep filling it in; downloads and printing unlock with Pro ($29 once).</p><button type=\"button\" class=\"btn\" id=\"btn-unlock\">Unlock all agreements</button></div>"; | |
| var docEl = $("doc"); | |
| docEl.innerHTML = h; | |
| docEl.classList.toggle("locked", !allowed()); | |
| docEl.classList.toggle("a4", state.page === "A4"); | |
| var ul = $("btn-unlock"); if (ul) ul.onclick = function () { upsell(null, "agreement"); }; | |
| $("doc-meta").textContent = tpl.short + " · Version " + tpl.version; | |
| var w = $("warn"); | |
| w.textContent = model.missing.length ? "Still blank: " + model.missing.slice(0, 4).join(", ") + (model.missing.length > 4 ? " and " + (model.missing.length - 4) + " more" : "") + ". Blank lines print as ________ to fill by hand." : ""; | |
| quotaLine(); | |
| } | |
| /* ---------- quota + downloads ---------- */ | |
| function quotaLine() { | |
| var el = $("quota"); | |
| if (pro()) { el.textContent = "Pro: unlimited downloads, Word export, no Pactsprig footer line."; el.className = "quota"; return; } | |
| if (!agreementInfo(state.agreement).free) { el.textContent = "This agreement is part of Pro. The Mutual NDA is free."; el.className = "quota out"; return; } | |
| var q = X.quota("pdf", FREE_PDFS); | |
| el.textContent = q.left > 0 ? q.left + " of " + FREE_PDFS + " free PDFs left this month · printing is free" : "You have used this month's " + FREE_PDFS + " free PDFs. Printing still works, or go Pro for unlimited."; | |
| el.className = "quota" + (q.left > 0 ? "" : " out"); | |
| } | |
| function lockedUpsell() { upsell(tpl.name + " is part of Pactsprig Pro, with Word export and unlimited downloads.", "agreement"); } | |
| function downloadPdf() { | |
| if (!allowed()) return lockedUpsell(); | |
| var q = X.quota("pdf", FREE_PDFS); | |
| if (!pro() && q.left <= 0) { quotaLine(); return upsell("You have used this month's " + FREE_PDFS + " free PDFs. Go Pro for unlimited downloads with no footer line.", "pdf-limit"); } | |
| var bytes; | |
| try { bytes = P.build(model, { credit: !pro(), page: state.page }); } catch (e) { toast("Sorry, the PDF could not be made: " + e.message); return; } | |
| q.take(); | |
| X.download(C.fileName(tpl, state.parties, "pdf"), new Blob([bytes], { type: "application/pdf" })); | |
| quotaLine(); | |
| if (!pro()) { | |
| $("pro-cta").hidden = false; | |
| var left = X.quota("pdf", FREE_PDFS).left; | |
| toast("PDF downloaded. " + (left ? left + " free PDF" + (left === 1 ? "" : "s") + " left this month." : "That was your last free PDF this month.")); | |
| } else toast("PDF downloaded."); | |
| } | |
| function downloadDocx() { | |
| if (!pro()) return upsell("Word (DOCX) export is part of Pro, so you can edit the Cover Page in Word or upload it to your e-signature tool.", "docx"); | |
| var bytes = D.build(model, { page: state.page }); | |
| X.download(C.fileName(tpl, state.parties, "docx"), new Blob([bytes], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" })); | |
| toast("Word file downloaded."); | |
| } | |
| $("btn-pdf").addEventListener("click", downloadPdf); | |
| $("m-pdf").addEventListener("click", downloadPdf); | |
| $("btn-docx").addEventListener("click", downloadDocx); | |
| $("btn-print").addEventListener("click", function () { | |
| if (!allowed()) return lockedUpsell(); | |
| window.print(); | |
| }); | |
| $("f-page").addEventListener("change", function (e) { state.page = e.target.value === "A4" ? "A4" : "Letter"; save(); renderPreview(); }); | |
| /* ---------- presets, saved companies, backup ---------- */ | |
| function renderPresets() { | |
| var box = $("presets"); | |
| if (!pro()) { | |
| box.innerHTML = '<p class="hint">Pro remembers the companies you work with and your usual Cover Page answers (for example “Standard NDA: Delaware, 2 years”), so the next agreement takes seconds.</p><button type="button" class="btn sm" id="btn-presets-pro">Unlock with Pro</button>'; | |
| $("btn-presets-pro").onclick = function () { upsell(null, "presets"); }; | |
| return; | |
| } | |
| var mine = state.presets.filter(function (p) { return p.agreement === state.agreement; }); | |
| box.innerHTML = '<h3 class="mini">Cover Page presets for ' + esc(tpl.short) + '</h3><div class="preset-add"><label class="sr" for="f-preset">Preset name</label><input id="f-preset" placeholder="e.g. Standard NDA, Delaware, 2 years"><button type="button" class="btn sm" id="btn-preset-save">Save preset</button></div>' + | |
| '<div class="chips">' + (mine.length ? mine.map(function (p) { var i = state.presets.indexOf(p); return '<span class="chip"><button type="button" class="chip-load" data-preset="' + i + '">' + esc(p.name) + '</button><button type="button" data-del-preset="' + i + '" aria-label="Delete preset ' + esc(p.name) + '">×</button></span>'; }).join("") : '<span class="hint">No presets yet.</span>') + "</div>" + | |
| '<h3 class="mini">Saved companies</h3><div class="chips">' + (state.savedParties.length ? state.savedParties.map(function (p, i) { return '<span class="chip"><span>' + esc(p.company) + '</span><button type="button" data-del-party="' + i + '" aria-label="Delete ' + esc(p.company) + '">×</button></span>'; }).join("") : '<span class="hint">Use “Save company” on a party to keep it here.</span>') + "</div>"; | |
| } | |
| $("presets").addEventListener("click", function (e) { | |
| var t = e.target; | |
| if (t.id === "btn-preset-save") { | |
| var name = ($("f-preset").value || "").trim() || tpl.short + " preset " + (state.presets.length + 1); | |
| state.presets.push({ name: name, agreement: state.agreement, values: Object.assign({}, state.docs[state.agreement].values) }); | |
| save(); renderPresets(); toast("Preset saved: " + name); | |
| } else if (t.hasAttribute("data-preset")) { | |
| var p = state.presets[+t.getAttribute("data-preset")]; | |
| if (p) { state.docs[state.agreement].values = Object.assign(C.defaults(tpl, C.isoToday()), p.values); renderForm(); renderPreview(); save(); toast("Loaded preset: " + p.name); } | |
| } else if (t.hasAttribute("data-del-preset")) { | |
| state.presets.splice(+t.getAttribute("data-del-preset"), 1); save(); renderPresets(); | |
| } else if (t.hasAttribute("data-del-party")) { | |
| state.savedParties.splice(+t.getAttribute("data-del-party"), 1); save(); renderPresets(); renderParties(); | |
| } | |
| }); | |
| $("btn-backup").addEventListener("click", function () { | |
| if (!pro()) return upsell("JSON backup is part of Pro.", "backup"); | |
| var c = Object.assign({}, state); delete c.fresh; | |
| X.download("pactsprig-backup-" + C.isoToday() + ".json", JSON.stringify({ app: "pactsprig", version: 1, saved: new Date().toISOString(), data: c }, null, 1), "application/json"); | |
| }); | |
| $("f-restore").addEventListener("click", function (e) { if (!pro()) { e.preventDefault(); upsell("JSON backup and restore are part of Pro.", "backup"); } }); | |
| $("f-restore").addEventListener("change", function (e) { | |
| var f = e.target.files && e.target.files[0]; | |
| if (!f) return; | |
| f.text().then(function (txt) { | |
| var j = JSON.parse(txt); | |
| if (!j || j.app !== "pactsprig" || !j.data) throw new Error("not a Pactsprig backup"); | |
| localStorage.setItem(KEY, JSON.stringify(j.data)); | |
| state = load(); | |
| return switchTo(state.agreement).then(function () { renderPresets(); toast("Backup restored."); }); | |
| }).catch(function (err) { toast("Could not restore: " + err.message); }); | |
| e.target.value = ""; | |
| }); | |
| /* ---------- hero buttons ---------- */ | |
| $("btn-example").addEventListener("click", function () { switchTo(state.agreement, true).then(function () { toast("Example loaded. Change anything; the contract updates as you type."); }); }); | |
| $("btn-clear").addEventListener("click", function () { | |
| state.parties = [C.emptyParty(), C.emptyParty()]; | |
| switchTo(state.agreement, "blank").then(function () { toast("Blank Cover Page. Your saved companies and presets are still here."); }); | |
| }); | |
| $("btn-cta").addEventListener("click", function () { upsell(null, "mid-cta"); }); | |
| /* ---------- Pro on/off ---------- */ | |
| function onPro(on) { | |
| $("pro-pill").textContent = on ? "Pro ✓" : "Pro"; | |
| if (on) $("pro-cta").hidden = true; | |
| if (tpl) { renderPreview(); renderPresets(); } | |
| } | |
| /* ---------- PWA ---------- */ | |
| var deferred = null; | |
| window.addEventListener("beforeinstallprompt", function (e) { e.preventDefault(); deferred = e; $("btn-install").hidden = false; }); | |
| $("btn-install").addEventListener("click", function () { | |
| if (!deferred) return; | |
| deferred.prompt(); | |
| deferred.userChoice.finally(function () { deferred = null; $("btn-install").hidden = true; }); | |
| }); | |
| if ("serviceWorker" in navigator && (location.protocol === "https:" || /^(localhost|127\.0\.0\.1)$/.test(location.hostname))) { | |
| window.addEventListener("load", function () { navigator.serviceWorker.register("sw.js").catch(function () { /* offline cache is optional */ }); }); | |
| } | |
| /* ---------- start ---------- */ | |
| state = load(); | |
| $("f-page").value = state.page; | |
| renderPicker(); | |
| switchTo(state.agreement).then(function () { | |
| state.fresh = false; | |
| X.onChange(onPro); | |
| document.documentElement.setAttribute("data-ready", "1"); | |
| }); | |
| })(); | |