From 3ca9eb6202dfa05c4823f691f3df103965375793 Mon Sep 17 00:00:00 2001 From: krflol Date: Wed, 14 Oct 2026 20:29:45 -0500 Subject: [PATCH 1/2] Let browser starter users save edited HTML and CSS --- examples/browser/README.md | 7 +- examples/browser/index.html | 9 +- examples/browser/src/main.js | 10 ++ examples/browser/src/style.css | 1 + tools/check-browser-starter.py | 34 +++- verification/browser-template-downloads.json | 160 +++++++++++++++++++ 6 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 verification/browser-template-downloads.json diff --git a/examples/browser/README.md b/examples/browser/README.md index b9bebe4..be7f054 100644 --- a/examples/browser/README.md +++ b/examples/browser/README.md @@ -20,7 +20,12 @@ not upload document inputs to a PDF service. Nothing persists after a reload. ## Make your template Change the invoice fields, or open **Edit the HTML & CSS** below the preview. -Edits in that editor last until reload. To keep them in the project, edit: +Use **Save HTML** and **Save CSS** to download the selected template's current +source, including your edits and any placeholders. Saving does not require a +successful PDF render. The files are named `invoice.html` / `invoice.css` or +`report.html` / `report.css`; put them in the project's `src/` folder to use them +after a reload. The downloads contain template source, not the values filled +into the last PDF. You can also edit these files directly: - `src/invoice.html` and `src/invoice.css` - `src/report.html` and `src/report.css` diff --git a/examples/browser/index.html b/examples/browser/index.html index 93603a2..b19694d 100644 --- a/examples/browser/index.html +++ b/examples/browser/index.html @@ -50,13 +50,18 @@

A proper document.
Made in your browser.

Edit the HTML & CSS -

Make the template your own. Invoice fields replace {{customer}} and {{reference}}; {{ink}} applies the color in CSS. Edits stay in this tab until you reload. Fullbleed uses static print HTML and CSS; document scripts do not run.

+

Make the template your own. Invoice fields replace {{customer}} and {{reference}}; {{ink}} applies the color in CSS. Save your HTML and CSS before leaving to keep your edits, including these placeholders. Fullbleed uses static print HTML and CSS; document scripts do not run.

Template source
-
+
+
+

Use this in your app

+

Download the complete browser starter, then put your saved HTML and CSS in its src/ folder. It includes the templates, renderer setup, and dependency lockfile.

+
Download starter Setup guide
+
diff --git a/examples/browser/src/main.js b/examples/browser/src/main.js index 8e52c14..97c49e1 100644 --- a/examples/browser/src/main.js +++ b/examples/browser/src/main.js @@ -116,6 +116,16 @@ $('reset-source').addEventListener('click', () => { loadSource(); markChanged(); }); $('render-source').addEventListener('click', () => $('design-form').requestSubmit()); +function saveSource(extension) { + const design = $('kind').value; + const type = extension === 'html' ? 'text/html' : 'text/css'; + const url = URL.createObjectURL(new Blob([templates[design][extension]], { type: `${type};charset=utf-8` })); + const link = document.createElement('a'); + link.href = url; link.download = `${design}.${extension}`; link.click(); + setTimeout(() => URL.revokeObjectURL(url), 60_000); +} +$('save-html').addEventListener('click', () => saveSource('html')); +$('save-css').addEventListener('click', () => saveSource('css')); $('cancel').addEventListener('click', () => controller?.abort()); $('download').addEventListener('click', () => { if (!pdfUrl) return; diff --git a/examples/browser/src/style.css b/examples/browser/src/style.css index 94ddfa0..c28de8a 100644 --- a/examples/browser/src/style.css +++ b/examples/browser/src/style.css @@ -11,3 +11,4 @@ main{max-width:1440px;margin:auto;padding:0 5vw}.intro{padding:58px 0 38px}.eyeb .template-editor{border-top:1px solid #bdc7bd;margin-top:36px;padding-top:22px}.template-editor summary{font-size:17px;font-weight:650;cursor:pointer}.editor-note{color:#54645b;font-size:12px;max-width:760px}.editor-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}.editor-grid label{margin-top:0}.editor-grid textarea{width:100%;min-height:320px;resize:vertical;display:block;font:12px/1.6 monospace;padding:15px;background:#fffef9;color:#173b35;border:1px solid #bdc7bd;tab-size:2}.editor-actions{display:flex;gap:12px;align-items:center;margin-top:14px}.editor-actions p{font-size:11px;color:#657163;margin:0}code{font-size:.95em}@media(max-width:850px){.editor-grid{grid-template-columns:1fr}.editor-grid textarea{min-height:240px}} select{appearance:none;line-height:1.25;padding-right:36px;background-image:linear-gradient(45deg,transparent 50%,#173b35 50%),linear-gradient(135deg,#173b35 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 12px) 50%;background-size:4px 4px;background-repeat:no-repeat}.color-row input.color-text{width:90px;font:12px Inter,sans-serif;cursor:text} +.editor-actions{flex-wrap:wrap}.source-downloads{display:flex;gap:12px;margin-left:auto}.source-save{border-color:transparent;background:transparent;color:#173b35;text-decoration:underline;text-underline-offset:4px}.next-step{border-top:1px solid #bdc7bd;margin-top:32px;padding-top:22px}.next-step p{max-width:680px;font-size:13px;line-height:1.7;color:#54645b}.next-step>div{display:flex;flex-wrap:wrap;gap:12px 24px}.next-step a{display:inline-flex;align-items:center;gap:10px;min-height:44px;font-size:13px;font-weight:650;text-underline-offset:4px}@media(max-width:600px){.source-downloads{width:100%;margin-left:0}.source-save{flex:1;text-align:left;padding-left:0}.editor-actions{gap:8px}} diff --git a/tools/check-browser-starter.py b/tools/check-browser-starter.py index b75fdf0..d747d3b 100644 --- a/tools/check-browser-starter.py +++ b/tools/check-browser-starter.py @@ -45,7 +45,7 @@ def log_message(self,*a):pass server=ThreadingHTTPServer(('127.0.0.1',0),Handler) thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start() url=f'http://127.0.0.1:{server.server_port}{prefix}' -checks=[];network=[];errors=[];documents=[] +checks=[];network=[];errors=[];documents=[];sources=[] def check(name,passed): assert passed,name checks.append(name);print(name+': passed',flush=True) @@ -66,6 +66,13 @@ def download(name,pages,text): check(name+': download has the expected pages and text',len(pdf.pages)==pages and all(t in contents for t in text)) documents.append({'name':name,'filename':item.suggested_filename,'pages':len(pdf.pages),'sha256':hashlib.sha256(path.read_bytes()).hexdigest()}) return path + def save_source(design,extension,expected,name): + with page.expect_download() as event:page.locator('#save-'+extension).click() + item=event.value;path=out/(name+'.'+extension);item.save_as(path) + check(name+': source download preserves the edited UTF-8 bytes and template filename', + item.suggested_filename==design+'.'+extension and path.read_bytes()==expected.encode('utf-8')) + sources.append({'name':name,'filename':item.suggested_filename,'bytes':path.stat().st_size, + 'sha256':hashlib.sha256(path.read_bytes()).hexdigest()}) try: page.goto(url,wait_until='domcontentloaded');ready() check('runtime resolves beneath a nested deployment path',any('/nested/demo/fullbleed/worker.js' in r['url'] for r in network)) @@ -89,26 +96,43 @@ def download(name,pages,text): download('customized',1,['Birch & ','TEST-2042']) check('customized bytes differ from the default invoice',documents[-1]['sha256']!=documents[0]['sha256']) page.locator('#template-editor summary').click() - page.locator('#html-source').fill('

Edited template

{{customer}} / {{reference}}

') - page.locator('#css-source').fill('@page { size: A5; margin: 15mm } body { font-family: Helvetica } h1 { color: {{ink}}; font-size: 24pt }') + edited_html='

Edited template

\n

{{customer}} / {{reference}}

\n' + edited_css='/* Color: café & "ink" */\n@page { size: A5; margin: 15mm } body { font-family: Helvetica } h1 { color: {{ink}}; font-size: 24pt }\n' + page.locator('#html-source').fill(edited_html) + page.locator('#css-source').fill(edited_css) + check('source can be saved before rendering pending edits',page.locator('#download').is_disabled()) + save_source('invoice','html',edited_html,'edited-invoice') + save_source('invoice','css',edited_css,'edited-invoice') page.locator('#render-source').click();ready() path=download('edited',1,['Edited template','Birch & ','TEST-2042']) check('edited print CSS changes the PDF page size',abs(float(PdfReader(path).pages[0].mediabox.width)-419.52756)<1) check('document script does not run on the page',page.evaluate('window.documentScriptExecuted !== true')) verify_edited_preview(page, path, out, check) page.screenshot(path=str(out/'edited-template.png'),full_page=True) + for width,height in [(390,844),(320,760)]: + page.set_viewport_size({'width':width,'height':height}) + check(f'{width}px: expanded editor and source downloads fit the viewport',page.evaluate('document.documentElement.scrollWidth <= innerWidth')) + page.screenshot(path=str(out/f'editor-width-{width}.png'),full_page=True) + page.set_viewport_size({'width':1440,'height':1000}) verify_engine_fixes(page, out, check, ready, manual=True) - page.locator('#kind').select_option('report');page.locator('#generate').click();ready() + page.locator('#kind').select_option('report') + report_html=page.locator('#html-source').input_value()+'\n' + page.locator('#html-source').fill(report_html) + save_source('report','html',report_html,'edited-report') + save_source('report','css',page.locator('#css-source').input_value(),'edited-report') + page.locator('#generate').click();ready() download('report',3,['COMMON','1,240','420,000.00']) check('multi-page preview displays all report pages',page.locator('#preview img').count()==3) page.locator('#kind').select_option('invoice') check('each design preserves its template edits',page.locator('#html-source').input_value().startswith('

Edited template')) + save_source('invoice','html',edited_html,'invoice-after-switch') page.locator('#reset-source').click() check('reset restores only the selected template', 'Northstar' in page.locator('#html-source').input_value()) page.locator('#html-source').fill('') page.locator('#render-source').click() expect(page.locator('#status')).to_contain_text('nonempty') check('invalid input displays an error without a stale download',page.locator('#download').is_disabled()) + save_source('invoice','html','','empty-html-after-error') page.locator('#reset-source').click() pause_engine.set() page.locator('#generate').click() @@ -122,7 +146,7 @@ def download(name,pages,text): local_blob=f'blob:http://127.0.0.1:{server.server_port}/' check('no document uploads or cross-origin runtime requests',all(r['method']=='GET' and not r['body'] and r['url'].startswith((url,local_blob)) for r in network)) check('no uncaught page errors',not errors) - report={'ok':True,'checked_at':datetime.now(timezone.utc).isoformat(),'browser_engine':args.browser,'browser':browser.version,'playwright':version('playwright'),'pypdf':version('pypdf'),'checks':checks,'documents':documents,'network':network,'page_errors':errors,'csp':policy,'scope':'Owned browser; synthetic inputs; production build served at /nested/demo/ with no PDF server.'} + report={'ok':True,'checked_at':datetime.now(timezone.utc).isoformat(),'browser_engine':args.browser,'browser':browser.version,'playwright':version('playwright'),'pypdf':version('pypdf'),'checks':checks,'documents':documents,'source_downloads':sources,'network':network,'page_errors':errors,'csp':policy,'scope':'Owned browser; synthetic inputs; production build served at /nested/demo/ with no PDF server; exact source downloads including unsaved edits and failed renders.'} (out/'verification.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8') except Exception: page.screenshot(path=str(out/'failure.png'),full_page=True) diff --git a/verification/browser-template-downloads.json b/verification/browser-template-downloads.json new file mode 100644 index 0000000..16d0a12 --- /dev/null +++ b/verification/browser-template-downloads.json @@ -0,0 +1,160 @@ +{ + "checked_at": "2026-10-15T01:28:40.149449+00:00", + "scope": "Browser starter source downloads and existing PDF rendering/recovery; no SDK or engine change.", + "package_version": "0.4.1", + "engine_version": "2.5.22", + "browser": "155.0.8059.40", + "playwright": "1.63.0", + "checks": [ + "runtime resolves beneath a nested deployment path", + "bundled UI font loads", + "invoice: download has the expected pages and text", + "1440px: no horizontal overflow", + "390px: no horizontal overflow", + "320px: no horizontal overflow", + "color-only: download has the expected pages and text", + "changing only ink color changes the rendered PDF", + "changed inputs clear the previous preview", + "customized: download has the expected pages and text", + "customized bytes differ from the default invoice", + "source can be saved before rendering pending edits", + "edited-invoice: source download preserves the edited UTF-8 bytes and template filename", + "edited-invoice: source download preserves the edited UTF-8 bytes and template filename", + "edited: download has the expected pages and text", + "edited print CSS changes the PDF page size", + "document script does not run on the page", + "edited template uses unembedded standard Helvetica faces", + "edited standard-font preview contains visible text", + "390px: expanded editor and source downloads fit the viewport", + "320px: expanded editor and source downloads fit the viewport", + "engine-counter-sibling-reset: downloaded PDF has the expected page count", + "engine-counter-sibling-reset: sibling counter resets produce the required labels", + "engine-overflow-nested-clips: downloaded PDF has the expected page count", + "engine-overflow-nested-clips: downloaded PDF has the expected dimensions", + "engine-overflow-nested-clips: downloaded PDF matches independent color probes", + "engine-overflow-nested-clips: displayed preview has the expected dimensions", + "engine-overflow-nested-clips: displayed preview matches independent color probes", + "engine-border-center-zero-top-falls-back-bottom-svg: downloaded PDF has the expected page count", + "engine-border-center-zero-top-falls-back-bottom-svg: downloaded PDF has the expected dimensions", + "engine-border-center-zero-top-falls-back-bottom-svg: downloaded PDF matches independent color probes", + "engine-border-center-zero-top-falls-back-bottom-svg: displayed preview has the expected dimensions", + "engine-border-center-zero-top-falls-back-bottom-svg: displayed preview matches independent color probes", + "engine-first-letter-decorated-left: downloaded PDF has the expected page count", + "engine-first-letter-decorated-left: decorated initial preserves the paragraph text", + "engine-first-letter-decorated-left: downloaded PDF has the expected dimensions", + "engine-first-letter-decorated-left: downloaded PDF matches independent color probes", + "engine-first-letter-decorated-left: displayed preview has the expected dimensions", + "engine-first-letter-decorated-left: displayed preview matches independent color probes", + "edited-report: source download preserves the edited UTF-8 bytes and template filename", + "edited-report: source download preserves the edited UTF-8 bytes and template filename", + "report: download has the expected pages and text", + "multi-page preview displays all report pages", + "each design preserves its template edits", + "invoice-after-switch: source download preserves the edited UTF-8 bytes and template filename", + "reset restores only the selected template", + "invalid input displays an error without a stale download", + "empty-html-after-error: source download preserves the edited UTF-8 bytes and template filename", + "cancellation restores controls without a stale download", + "recovered: download has the expected pages and text", + "no document uploads or cross-origin runtime requests", + "no uncaught page errors" + ], + "source_downloads": [ + { + "name": "edited-invoice", + "filename": "invoice.html", + "bytes": 156, + "sha256": "d5b529705775dd9bd524d8aac1ccd3aac2d57456c93e72d4f419c6d8c3e977ae" + }, + { + "name": "edited-invoice", + "filename": "invoice.css", + "bytes": 131, + "sha256": "a757ddab08467b8b23a98158fef5eb7715286e2c42cbe8c843d41312697cd59b" + }, + { + "name": "edited-report", + "filename": "report.html", + "bytes": 7783, + "sha256": "904c7079213638d6166c1cdf1b6e7cb3820a3824a084af7634e843f4291304b3" + }, + { + "name": "edited-report", + "filename": "report.css", + "bytes": 4277, + "sha256": "519591ae75c239ec602324f008dd99108ebeae0b9306df7215fa5f0442905473" + }, + { + "name": "invoice-after-switch", + "filename": "invoice.html", + "bytes": 156, + "sha256": "d5b529705775dd9bd524d8aac1ccd3aac2d57456c93e72d4f419c6d8c3e977ae" + }, + { + "name": "empty-html-after-error", + "filename": "invoice.html", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ], + "documents": [ + { + "name": "invoice", + "filename": "invoice-NS-1042.pdf", + "pages": 1, + "sha256": "3078d155f30536f5ad721f7be3c241cd78661ae41249131fc0fd65f37f23e408" + }, + { + "name": "color-only", + "filename": "invoice-NS-1042.pdf", + "pages": 1, + "sha256": "a2b69ba41f7ad4c9bcbd38d803a46e70f0b9ccacd90ffbab5b0f7d87ffca5e4b" + }, + { + "name": "customized", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "31ff314b44a6f13534cc383e0cf24929f0695c1dd3d84769dedf8828c6486a93" + }, + { + "name": "edited", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "f5c6c1927fc6c636cee592a3ebc7bb7b3e91dd645ab34a9370590571506d278b" + }, + { + "name": "report", + "filename": "community-report.pdf", + "pages": 3, + "sha256": "139df89824e6a73c13ff62f1237bb8eb9f71e790dcbd051b1bb713ed2bc47802" + }, + { + "name": "recovered", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "31ff314b44a6f13534cc383e0cf24929f0695c1dd3d84769dedf8828c6486a93" + } + ], + "example_files": { + "examples/browser/.gitignore": "c023fec2bf12476cb47709e859f1fbb785aeb1e03281e52141e0bee1ee7dad72", + "examples/browser/LICENSE": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9", + "examples/browser/README.md": "5b2b8a4462e2fadf836a0f8022cae13b7824883f351c3297a6aea6d9c9643613", + "examples/browser/index.html": "08e7a0777827c94f1a3a1009f0e89a61dcd9bec2a5a76352a3b8d262650fddc1", + "examples/browser/package-lock.json": "cb74c22fe1b49cf8f31c979a56975dbd1e390dd899d8bc4cf2186a819482a2f6", + "examples/browser/package.json": "b67511c1509889ac90928909dc8a268ddc96c763828611409c779f1b73ef673a", + "examples/browser/public/favicon.svg": "3eed6dcf1013e48bc64886fed4cf815c609ed06b404742bdacdc44c1df8d3118", + "examples/browser/src/invoice.css": "0b310ce0f2964fd6b9b68e4afafa9307b4ff8c6b2f1fc753367dc755620ce427", + "examples/browser/src/invoice.html": "1f065ad6694021649e61b3f666cba6dc01aacdcbd8e80e97425b383e60d7d2fe", + "examples/browser/src/main.js": "3ffcbe09c7117e635c21c39ad9be1aa3e7af16be7fc325683a7baaecae04d275", + "examples/browser/src/report.css": "519591ae75c239ec602324f008dd99108ebeae0b9306df7215fa5f0442905473", + "examples/browser/src/report.html": "46b0e975d43914c8b4492218feae09ea0ad684091a9f9ee3b18f66a87202e521", + "examples/browser/src/style.css": "131426c760df6f64f5890d03f2b3301bf2003f63008b7f90aea311c400db6f17", + "examples/browser/vite.config.js": "ec725922705a3c5ba2df191a564c54c0cac6e8997ecd3d00b8a42c647bec8e74" + }, + "visual_review": { + "1440px": "Source actions and complete-project links readable beneath the editor.", + "390px_and_320px": "Source actions wrap without overlap or horizontal overflow; project links readable." + }, + "local_evidence": "Fullbleed maintainer workspace: target/growth/browser-template-downloads-20261015/chrome", + "limitations": "Chrome locally; CI supplies Firefox and Playwright WebKit coverage. This is not branded Safari certification." +} From b1d987d616bcb7cfa8b960508103904229f190b4 Mon Sep 17 00:00:00 2001 From: krflol Date: Wed, 14 Oct 2026 20:36:17 -0500 Subject: [PATCH 2/2] Name the template download controls as an accessible group --- examples/browser/index.html | 2 +- verification/browser-template-downloads.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/browser/index.html b/examples/browser/index.html index b19694d..7b41962 100644 --- a/examples/browser/index.html +++ b/examples/browser/index.html @@ -55,7 +55,7 @@

A proper document.
Made in your browser.

-
+

Use this in your app

diff --git a/verification/browser-template-downloads.json b/verification/browser-template-downloads.json index 16d0a12..0cc1a83 100644 --- a/verification/browser-template-downloads.json +++ b/verification/browser-template-downloads.json @@ -1,5 +1,5 @@ { - "checked_at": "2026-10-15T01:28:40.149449+00:00", + "checked_at": "2026-10-15T01:36:11.749432+00:00", "scope": "Browser starter source downloads and existing PDF rendering/recovery; no SDK or engine change.", "package_version": "0.4.1", "engine_version": "2.5.22", @@ -139,7 +139,7 @@ "examples/browser/.gitignore": "c023fec2bf12476cb47709e859f1fbb785aeb1e03281e52141e0bee1ee7dad72", "examples/browser/LICENSE": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9", "examples/browser/README.md": "5b2b8a4462e2fadf836a0f8022cae13b7824883f351c3297a6aea6d9c9643613", - "examples/browser/index.html": "08e7a0777827c94f1a3a1009f0e89a61dcd9bec2a5a76352a3b8d262650fddc1", + "examples/browser/index.html": "8714cd15f58623edf29fc4f725f280af35f5749e9fa1ebd9bb6f71985dbd1f3a", "examples/browser/package-lock.json": "cb74c22fe1b49cf8f31c979a56975dbd1e390dd899d8bc4cf2186a819482a2f6", "examples/browser/package.json": "b67511c1509889ac90928909dc8a268ddc96c763828611409c779f1b73ef673a", "examples/browser/public/favicon.svg": "3eed6dcf1013e48bc64886fed4cf815c609ed06b404742bdacdc44c1df8d3118", @@ -155,6 +155,6 @@ "1440px": "Source actions and complete-project links readable beneath the editor.", "390px_and_320px": "Source actions wrap without overlap or horizontal overflow; project links readable." }, - "local_evidence": "Fullbleed maintainer workspace: target/growth/browser-template-downloads-20261015/chrome", + "local_evidence": "Fullbleed maintainer workspace: target/growth/browser-template-downloads-20261015/chrome-final", "limitations": "Chrome locally; CI supplies Firefox and Playwright WebKit coverage. This is not branded Safari certification." }