From 5bf1440b475f56ef559c39a0723f25ac6c2ce161 Mon Sep 17 00:00:00 2001 From: krflol Date: Mon, 5 Oct 2026 22:33:32 -0500 Subject: [PATCH 1/2] Add editable browser PDF starter using public npm package --- .github/workflows/browser-starter.yml | 45 ++ README.md | 4 + docs/browser.md | 3 + examples/browser/.gitignore | 4 + examples/browser/LICENSE | 21 + examples/browser/README.md | 69 +++ examples/browser/index.html | 64 ++ examples/browser/package-lock.json | 855 ++++++++++++++++++++++++++ examples/browser/package.json | 15 + examples/browser/public/favicon.svg | 1 + examples/browser/src/invoice.css | 48 ++ examples/browser/src/invoice.html | 12 + examples/browser/src/main.js | 120 ++++ examples/browser/src/report.css | 60 ++ examples/browser/src/report.html | 36 ++ examples/browser/src/style.css | 11 + examples/browser/vite.config.js | 3 + tools/check-browser-starter.py | 124 ++++ verification/browser-starter-030.json | 215 +++++++ verification/npm-030-public.json | 16 + 20 files changed, 1726 insertions(+) create mode 100644 .github/workflows/browser-starter.yml create mode 100644 examples/browser/.gitignore create mode 100644 examples/browser/LICENSE create mode 100644 examples/browser/README.md create mode 100644 examples/browser/index.html create mode 100644 examples/browser/package-lock.json create mode 100644 examples/browser/package.json create mode 100644 examples/browser/public/favicon.svg create mode 100644 examples/browser/src/invoice.css create mode 100644 examples/browser/src/invoice.html create mode 100644 examples/browser/src/main.js create mode 100644 examples/browser/src/report.css create mode 100644 examples/browser/src/report.html create mode 100644 examples/browser/src/style.css create mode 100644 examples/browser/vite.config.js create mode 100644 tools/check-browser-starter.py create mode 100644 verification/browser-starter-030.json create mode 100644 verification/npm-030-public.json diff --git a/.github/workflows/browser-starter.yml b/.github/workflows/browser-starter.yml new file mode 100644 index 0000000..79caf29 --- /dev/null +++ b/.github/workflows/browser-starter.yml @@ -0,0 +1,45 @@ +name: Browser starter + +on: + push: + branches: [main] + paths: ['examples/browser/**', 'tools/check-browser-starter.py', 'tools/browser-requirements.txt', '.github/workflows/browser-starter.yml'] + pull_request: + paths: ['examples/browser/**', 'tools/check-browser-starter.py', 'tools/browser-requirements.txt', '.github/workflows/browser-starter.yml'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + browser: + name: Browser starter (${{ matrix.browser }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + browser: [chrome, firefox, webkit] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: '22' + cache: npm + cache-dependency-path: examples/browser/package-lock.json + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - run: npm ci --ignore-scripts --no-audit --no-fund + working-directory: examples/browser + - run: npm run build + working-directory: examples/browser + - run: python -m pip install -r tools/browser-requirements.txt + - run: python -m playwright install --with-deps ${{ matrix.browser }} + - run: python tools/check-browser-starter.py --dist examples/browser/dist --out output/starter-${{ matrix.browser }} --browser ${{ matrix.browser }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: starter-${{ matrix.browser }} + path: output/starter-${{ matrix.browser }}/ + retention-days: 14 diff --git a/README.md b/README.md index 1e908d0..4a25bb1 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ data; connect your own authenticated record lookup when adapting it. ## Use it in a browser +Start with the [complete browser application](examples/browser): editable HTML/CSS, +invoice and report designs, previews, cancellation, and PDF downloads. + + Copy the browser runtime into your application's static directory: ```sh diff --git a/docs/browser.md b/docs/browser.md index db012d8..f09fe2b 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -5,6 +5,9 @@ dedicated Web Worker. It uses the same pinned 2.5.8 engine and bundled fonts as the Node entry. The result is a `Uint8Array`, suitable for a PDF `Blob`, plus optional PNG previews. No PDF server or account is required. +The [runnable browser starter](../examples/browser) includes editable templates, +previews, downloads, and production build settings. + ## Copy the runtime into your site Install the package in your web project, then copy its browser files into the diff --git a/examples/browser/.gitignore b/examples/browser/.gitignore new file mode 100644 index 0000000..df9ebe9 --- /dev/null +++ b/examples/browser/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +public/fullbleed/ +output/ diff --git a/examples/browser/LICENSE b/examples/browser/LICENSE new file mode 100644 index 0000000..4c99bbd --- /dev/null +++ b/examples/browser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Fullbleed Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/browser/README.md b/examples/browser/README.md new file mode 100644 index 0000000..aed0cc2 --- /dev/null +++ b/examples/browser/README.md @@ -0,0 +1,69 @@ +# Fullbleed browser starter + +A complete static web application: editable HTML/CSS templates, a designed invoice +and three-page report, local PDF/PNG rendering, cancellation, and downloads. +The sample records and amounts are fictional. MIT licensed. + +## Run + +Use Node.js 22.12 or newer for installation and builds: + +```sh +npm ci +npm run dev +``` + +Open the localhost URL printed by Vite. The first preview loads the engine and +fonts from that server. Each render runs in a fresh Web Worker; the starter does +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: + +- `src/invoice.html` and `src/invoice.css` +- `src/report.html` and `src/report.css` + +Invoice HTML uses `{{customer}}` and `{{reference}}`; print CSS uses `{{ink}}`. +`src/main.js` replaces these values, escapes text, bounds rendering, handles +errors, and releases old PDF/preview Blob URLs. The sample line items, totals, +addresses, and dates are fixed. Add your own structured data and calculations +when adapting it for real documents. + +Use [supported Fullbleed print CSS](https://docs.fullbleed.dev/css-coverage/). +Document scripts do not run. Supply custom image/font bytes through the SDK's +`assets` and `fonts` options; document URLs do not trigger browser fetches. + +## Build and deploy + +```sh +npm run build +npm run preview +``` + +Deploy the whole `dist/` directory to a static host over HTTPS. It needs no Node +server. The build uses relative URLs and supports a nested deployment path. +The asset-copy step verifies and copies the installed client, worker, engine, +fonts, build manifest, and notices. Keep those files together when deploying; +repeat the build after a package upgrade. + +Browsers need Web Workers, WebAssembly, and Web Crypto. Local development works +on localhost; production needs HTTPS. The starter renders one job at a time, +with a 30-second deadline and 20-page limit. It exposes ordinary PDF generation +and previews, not PDF/A, PDF/UA, PDF/X, or VDP options. + +The [browser API and hosting guide](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/browser.md) +explains CSP, custom assets, structured errors, and browser worker lifecycle limits. + +## Versions and verification + +The lockfile pins Fullbleed npm 0.3.0 / engine 2.5.8 and Vite 8.3.2. +The browser SDK is covered by the [versioned release evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.3.0). +The repository's starter check builds this app, serves it below `/nested/demo/`, +downloads actual PDFs, checks their text and page size, exercises editing and +cancellation, and captures desktop/mobile screenshots. Playwright WebKit is not +branded Safari certification. + +See the [public walkthrough](https://docs.fullbleed.dev/guides/browser-pdf/) or +the [source repository](https://github.com/fullbleed-engine/fullbleed-node/tree/main/examples/browser). diff --git a/examples/browser/index.html b/examples/browser/index.html new file mode 100644 index 0000000..93603a2 --- /dev/null +++ b/examples/browser/index.html @@ -0,0 +1,64 @@ + + + + + + + Browser PDF starter · Fullbleed + + + + +
+
+

HTML + CSS → PDF

+

A proper document.
Made in your browser.

+

Change the details. Choose a color. Download the real PDF.
Fullbleed handles the print layout locally, in a worker.

+
+
+
+
01

Make it yours

+
+
+ Document details + + +
+ + + + + +
#17382EYour print palette
+
+
+

Fictional invoice. Items and amounts are fixed for this example.

+
+
+

Preparing the first preview…

+

Rendered here. The app loads its engine and fonts from this site. Document inputs are not uploaded by this starter.

+

Your HTML. Your print styles.

Open the template editor below. Paste your own HTML and CSS, then generate a new PDF.

Explore supported CSS
+
+
+
02

The actual PDF

+

Your document will appear here.

+
+
+
+ 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.

+
Template source
+
+
+
+
+
+ +
+ + + diff --git a/examples/browser/package-lock.json b/examples/browser/package-lock.json new file mode 100644 index 0000000..e279b7f --- /dev/null +++ b/examples/browser/package-lock.json @@ -0,0 +1,855 @@ +{ + "name": "fullbleed-browser-starter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fullbleed-browser-starter", + "version": "1.0.0", + "dependencies": { + "fullbleed": "0.3.0" + }, + "devDependencies": { + "vite": "8.3.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@bjorn3/browser_wasi_shim": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.4.2.tgz", + "integrity": "sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA==", + "license": "MIT OR Apache-2.0" + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fullbleed": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.3.0.tgz", + "integrity": "sha512-9HimnoHTEpgtGhqi9gF+IMSEW8g3GRK11yUkmL6OYD40D/RhWoIF7SqKuDs6nrHF7loWnhtNDWz3Hp/DNaBbMQ==", + "license": "MIT", + "dependencies": { + "@bjorn3/browser_wasi_shim": "0.4.2" + }, + "bin": { + "fullbleed-browser-assets": "src/copy-browser-assets.cjs" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/examples/browser/package.json b/examples/browser/package.json new file mode 100644 index 0000000..421a529 --- /dev/null +++ b/examples/browser/package.json @@ -0,0 +1,15 @@ +{ + "name": "fullbleed-browser-starter", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { "node": ">=22.12.0" }, + "scripts": { + "assets": "fullbleed-browser-assets public/fullbleed", + "dev": "npm run assets && vite --host 127.0.0.1", + "build": "npm run assets && vite build", + "preview": "vite preview --host 127.0.0.1" + }, + "dependencies": { "fullbleed": "0.3.0" }, + "devDependencies": { "vite": "8.3.2" } +} diff --git a/examples/browser/public/favicon.svg b/examples/browser/public/favicon.svg new file mode 100644 index 0000000..842f2ec --- /dev/null +++ b/examples/browser/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/examples/browser/src/invoice.css b/examples/browser/src/invoice.css new file mode 100644 index 0000000..0c83bd8 --- /dev/null +++ b/examples/browser/src/invoice.css @@ -0,0 +1,48 @@ +@page { size: A4; margin: 0; } +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { font-family: Inter; font-size: 10pt; line-height: 1.45; color: {{ink}}; } +h1, h2, h3, p, figure { margin: 0; } +h1, h2, h3 { font-weight: 400; } +.page { width: 210mm; height: 296.9mm; padding: 15mm 16mm; position: relative; break-after: page; background: #f8f5ec; } +.page:last-child { break-after: auto; } +.kicker { font-size: 7.2pt; font-weight: 600; letter-spacing: 1.4pt; text-transform: uppercase; } +.serif { font-family: 'DM Serif Display'; font-weight: 400; } +.display { font-family: 'Bebas Neue'; font-weight: 400; } +.muted { color: #5b6c64; } +.small { font-size: 8pt; } +.rule { border-top: .6pt solid #9eafa4; } +.row { display: flex; justify-content: space-between; gap: 16pt; } +.grid-2 { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 24pt; } +.grid-2 > * { min-width: 0; } +.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16pt; } +.footer { position: absolute; left: 16mm; right: 16mm; bottom: 12mm; border-top: .6pt solid #9eafa4; padding-top: 8pt; display: flex; justify-content: space-between; gap: 12pt; font-size: 6.6pt; letter-spacing: .45pt; } +table { width: 100%; border-collapse: collapse; font-size: 9pt; } +th { font-size: 7.5pt; font-weight: 600; text-align: left; } +th, td { padding: 10pt 0; border-bottom: .55pt solid #bdc8bf; vertical-align: top; } +.number { text-align: right; white-space: nowrap; } +.money { font-variant-numeric: tabular-nums; } +strong { font-weight: 600; } +a { color: inherit; } + +.invoice .masthead { display: grid; grid-template-columns: 1.05fr .8fr 1fr; gap: 18pt; align-items: center; padding-bottom: 22pt; border-bottom: 1pt solid {{ink}}; } +.invoice .brand { font: 31pt/.88 'DM Serif Display'; letter-spacing: -1.3pt; } +.invoice .brand span { display: block; line-height: .98; } +.invoice .brand small { display: block; font: 6.5pt Inter; letter-spacing: 2.1pt; margin-top: 11pt; } +.invoice .brand-note { border-left: 1pt solid #c24929; padding: 4pt 0 4pt 16pt; line-height: 1.8; } +.invoice .address { font-size: 7.5pt; line-height: 1.7; border-left: 1pt solid #c24929; padding-left: 16pt; } +.invoice .title-row { display: grid; grid-template-columns: 3.3fr 1fr; gap: 20pt; align-items: center; padding: 22pt 0 16pt; } +.invoice h1 { font: 91pt/.9 'Bebas Neue'; letter-spacing: .3pt; } +.invoice .title-note { color: #b44123; font: 14pt/1.2 'Bebas Neue'; letter-spacing: .5pt; } +.invoice .details { border-top: .6pt solid #9eafa4; border-bottom: .6pt solid #9eafa4; padding: 15pt 0; margin-bottom: 10pt; } +.invoice .details p { margin-top: 5pt; font-size: 8.2pt; } +.invoice .details > div + div { border-left: .6pt solid #c7cdc4; padding-left: 14pt; } +.invoice th { color: #52665a; } +.invoice td { padding: 12pt 0; } +.invoice .item-note { color: #617069; font-size: 8pt; margin-top: 4pt; } +.invoice .amounts { display: grid; grid-template-columns: 1fr 1.25fr; gap: 27pt; margin-top: 23pt; } +.invoice .thanks { font: italic 22pt/1.2 'DM Serif Display'; color: #b44123; padding-top: 8pt; } +.invoice .summary-line { display: flex; justify-content: space-between; padding: 5pt 0; font-size: 8.5pt; } +.invoice .total { background: {{ink}}; color: #fff8e8; padding: 16pt; margin-top: 9pt; border-left: 4pt solid #d96238; } +.invoice .total .value { font: 38pt/1.1 'Bebas Neue'; margin-top: 7pt; } +.invoice .payment { margin-top: 20pt; padding-top: 12pt; border-top: .6pt solid #9eafa4; display: grid; grid-template-columns: 1fr 1fr; gap: 24pt; font-size: 8pt; } diff --git a/examples/browser/src/invoice.html b/examples/browser/src/invoice.html new file mode 100644 index 0000000..4d49034 --- /dev/null +++ b/examples/browser/src/invoice.html @@ -0,0 +1,12 @@ +Northstar Studio — Invoice {{reference}} +
+
NorthstarStudioDESIGN WITH DIRECTION
Brand
Digital
Experiences
That matter.
42 Cedar Avenue
Portland, OR 97205
hello@example.invalid
northstar.example.invalid
+

INVOICE

GOOD WORK.
CLEAR DETAILS.
LET'S MAKE
WHAT'S NEXT.

+

Prepared for

{{customer}}
Attn. Avery Morgan
Portland, Oregon

The project

Autumn launch
Brand workshop, digital
design & handoff

Invoice {{reference}}

Issued · 01 October 2026
Due · 31 October 2026
Currency · USD

+ + +
DESCRIPTIONHOURSRATEAMOUNT
Discovery & brand workshop

Positioning, priorities, and a clear creative direction

8$125.00$1,000.00
Digital design & implementation

A cohesive visual system, from layout to launch

6$95.00$570.00
Review & studio handoff

Final refinements, documentation, and delivery

2$150.00$300.00
+

A little clarity.
A lot of possibility.

Thank you for inviting us
to help shape your next chapter.

Subtotal$1,870.00
Tax · 0% in this sample$0.00
TOTAL DUE · USD
$1,870.00
+

Payment details

Reference {{reference}} with your payment.
Payment instructions are supplied separately.

A note from the studio

Questions about a line item?
Write to hello@example.invalid.

+
NORTHSTAR STUDIO / INVOICE {{reference}}FICTIONAL SAMPLE · 01 / 01
+
diff --git a/examples/browser/src/main.js b/examples/browser/src/main.js new file mode 100644 index 0000000..0e30b60 --- /dev/null +++ b/examples/browser/src/main.js @@ -0,0 +1,120 @@ +import { createRenderer, FullbleedError } from 'fullbleed/browser'; +import invoiceTemplate from './invoice.html?raw'; +import invoiceCss from './invoice.css?raw'; +import reportTemplate from './report.html?raw'; +import reportCss from './report.css?raw'; +import './style.css'; + +const $ = id => document.getElementById(id); +const assetBaseUrl = new URL('fullbleed/', new URL(import.meta.env.BASE_URL, location.href)); +let renderer; +const escapeHtml = value => value.replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]); +const placeholder = $('preview').firstElementChild.cloneNode(true); +let controller, pdfUrl, filename, previewUrls = []; +const defaults = { + invoice: { html: invoiceTemplate, css: invoiceCss }, + report: { html: reportTemplate, css: reportCss }, +}; +const templates = structuredClone(defaults); +function loadSource() { + const template = templates[$('kind').value]; + $('html-source').value = template.html; + $('css-source').value = template.css; +} +loadSource(); + +function clearOutput() { + if (pdfUrl) URL.revokeObjectURL(pdfUrl); + previewUrls.forEach(url => URL.revokeObjectURL(url)); + pdfUrl = null; previewUrls = []; + $('download').disabled = true; + $('page-count').textContent = ''; + $('preview').replaceChildren(placeholder.cloneNode(true)); +} +function status(text, error = false) { + $('status').textContent = text; + $('status').classList.toggle('error', error); +} +function markChanged() { + clearOutput(); + status('Generate a PDF to apply your changes.'); + const invoice = $('kind').value === 'invoice'; + $('invoice-fields').hidden = !invoice; + $('customer').required = $('reference').required = invoice; + $('color-value').textContent = $('accent').value.toUpperCase(); + $('sample-note').textContent = invoice ? 'Fictional invoice. Items and amounts are fixed for this example.' : 'A three-page report with tables, charts and print styling. All data is fictional.'; +} +function documentInput() { + const template = templates[$('kind').value]; + if ($('kind').value === 'report') return { ...template, filename: 'community-report.pdf' }; + const customer = $('customer').value.trim(); + const reference = $('reference').value.trim(); + if (!customer || !reference) throw new Error('Enter a customer name and invoice reference.'); + const accent = $('accent').value; + if (!/^#[\da-f]{6}$/i.test(accent)) throw new Error('Choose a valid ink color.'); + return { html: template.html.replace(/{{(customer|reference)}}/g, (_, key) => escapeHtml({ customer, reference }[key])), + css: template.css.replaceAll('{{ink}}', accent), filename: 'invoice-' + reference.replace(/[^\w-]+/g, '-').slice(0, 32) + '.pdf' }; +} +async function generate(event) { + event?.preventDefault(); + if (controller) return; + clearOutput(); + controller = new AbortController(); + $('fields').disabled = $('editor-fields').disabled = $('generate').disabled = $('render-source').disabled = $('reset-source').disabled = true; + $('cancel').hidden = false; + $('preview').setAttribute('aria-busy', 'true'); + status('Rendering locally… The first preview also loads the engine and fonts.'); + try { + const input = documentInput(); + renderer ??= createRenderer({ assetBaseUrl }); + const result = await renderer.renderPdf({ html: input.html, css: input.css, previewDpi: 96, maxPages: 20, timeoutMs: 30_000, signal: controller.signal }); + pdfUrl = URL.createObjectURL(new Blob([result.pdf], { type: 'application/pdf' })); + filename = input.filename; + const pages = result.previews.map((bytes, i) => { + const url = URL.createObjectURL(new Blob([bytes], { type: 'image/png' })); + previewUrls.push(url); + const image = document.createElement('img'); + image.src = url; image.alt = `Rendered PDF, page ${i + 1} of ${result.pages}`; + return image; + }); + $('preview').replaceChildren(...pages); + $('page-count').textContent = `${result.pages} ${result.pages === 1 ? 'page' : 'pages'}`; + $('download').disabled = false; + status(`Ready. ${result.pages} ${result.pages === 1 ? 'page' : 'pages'} · ${Math.ceil(result.pdf.byteLength / 1024)} KiB. Your PDF is ready to download.`); + } catch (error) { + const messages = { + ABORTED: 'Cancelled. You can generate another PDF.', + MISSING_GLYPHS: 'The bundled fonts do not include every character. Try another name or add a font to the project.', + TIMEOUT: 'This document took too long. Try again or reduce its size.', + ENGINE_LOAD_FAILED: 'The PDF runtime could not load. Check that the static files were deployed completely.', + VERSION_MISMATCH: 'The app and PDF runtime versions differ. Copy and deploy the current runtime files.', + WORKER_FAILED: 'The PDF worker could not start. Check the hosted files and the site worker policy.', + }; + status(error instanceof FullbleedError ? messages[error.code] ?? error.message : error.message, error.code !== 'ABORTED'); + } finally { + controller = null; + $('fields').disabled = $('editor-fields').disabled = $('generate').disabled = $('render-source').disabled = $('reset-source').disabled = false; + $('cancel').hidden = true; + $('preview').setAttribute('aria-busy', 'false'); + } +} +$('design-form').addEventListener('submit', generate); +$('fields').addEventListener('input', markChanged); +$('kind').addEventListener('change', loadSource); +$('editor-fields').addEventListener('input', () => { + templates[$('kind').value] = { html: $('html-source').value, css: $('css-source').value }; + markChanged(); +}); +$('reset-source').addEventListener('click', () => { + templates[$('kind').value] = { ...defaults[$('kind').value] }; + loadSource(); markChanged(); +}); +$('render-source').addEventListener('click', () => $('design-form').requestSubmit()); +$('cancel').addEventListener('click', () => controller?.abort()); +$('download').addEventListener('click', () => { + if (!pdfUrl) return; + const link = document.createElement('a'); link.href = pdfUrl; link.download = filename; link.click(); +}); +window.addEventListener('pagehide', () => { controller?.abort(); clearOutput(); }); +window.addEventListener('pageshow', event => { if (event.persisted) generate(); }); +generate(); diff --git a/examples/browser/src/report.css b/examples/browser/src/report.css new file mode 100644 index 0000000..585be7c --- /dev/null +++ b/examples/browser/src/report.css @@ -0,0 +1,60 @@ +@page { size: A4; margin: 0; } +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { font-family: Inter; font-size: 10pt; line-height: 1.45; color: #17382e; } +h1, h2, h3, p, figure { margin: 0; } +h1, h2, h3 { font-weight: 400; } +.page { width: 210mm; height: 296.9mm; padding: 15mm 16mm; position: relative; break-after: page; background: #f8f5ec; } +.page:last-child { break-after: auto; } +.kicker { font-size: 7.2pt; font-weight: 600; letter-spacing: 1.4pt; text-transform: uppercase; } +.serif { font-family: 'DM Serif Display'; font-weight: 400; } +.display { font-family: 'Bebas Neue'; font-weight: 400; } +.muted { color: #5b6c64; } +.small { font-size: 8pt; } +.rule { border-top: .6pt solid #9eafa4; } +.row { display: flex; justify-content: space-between; gap: 16pt; } +.grid-2 { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 24pt; } +.grid-2 > * { min-width: 0; } +.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16pt; } +.footer { position: absolute; left: 16mm; right: 16mm; bottom: 12mm; border-top: .6pt solid #9eafa4; padding-top: 8pt; display: flex; justify-content: space-between; gap: 12pt; font-size: 6.6pt; letter-spacing: .45pt; } +table { width: 100%; border-collapse: collapse; font-size: 9pt; } +th { font-size: 7.5pt; font-weight: 600; text-align: left; } +th, td { padding: 10pt 0; border-bottom: .55pt solid #bdc8bf; vertical-align: top; } +.number { text-align: right; white-space: nowrap; } +.money { font-variant-numeric: tabular-nums; } +strong { font-weight: 600; } +a { color: inherit; } + +.report .masthead { display: flex; justify-content: space-between; border-bottom: .8pt solid #183d2b; padding-bottom: 11pt; margin-bottom: 22pt; } +.report .brand { font: 15pt/1 'DM Serif Display'; } +.report .cover-title { font: 106pt/.83 'Bebas Neue'; letter-spacing: -.5pt; } +.report .cover-deck { display: grid; grid-template-columns: 1fr 1fr; align-items: start; margin-top: 23pt; } +.report .cover-deck p { font: 20pt/1.08 'DM Serif Display'; } +.report .year { font: 58pt/.9 'Bebas Neue'; color: #c65328; text-align: right; } +.report .landscape { display: block; width: 100%; height: 92mm; margin-top: 10pt; } +.report .contents { display: grid; grid-template-columns: .8fr 1fr 1fr; gap: 12pt; margin-top: 18pt; border-top: .7pt solid #8da28e; padding-top: 14pt; } +.report .contents > div + div { border-left: .7pt solid #c65328; padding-left: 13pt; } +.report .contents .serif { font-size: 12pt; margin-top: 4pt; line-height: 1.1; } +.report h1.section-title { font: 50pt/.96 'Bebas Neue'; margin: 12pt 0 14pt; } +.report h2 { font: 21pt/1.12 'DM Serif Display'; margin-bottom: 10pt; } +.report .deck { font-size: 11pt; line-height: 1.55; max-width: 450pt; } +.report .metrics { border-top: 1pt solid #244535; border-bottom: 1pt solid #244535; padding: 16pt 0 14pt; margin: 18pt 0; } +.report .metrics .value { font: 44pt/1 'Bebas Neue'; color: #b74624; margin-bottom: 5pt; } +.report .metric-caption { font-size: 8pt; } +.report .chart-panel { padding: 18pt; background: #1f4435; color: #f9f2dd; margin: 18pt 0; } +.report .chart-panel h2 { font-size: 21pt; } +.report .chart-bars { height: 127pt; display: flex; gap: 20pt; align-items: flex-end; margin: 12pt 0 8pt; border-bottom: .5pt solid #83a48b; } +.report .chart-col { width: 20%; text-align: center; } +.report .chart-value { font: 17pt 'Bebas Neue'; margin-bottom: 5pt; } +.report .bar { width: 100%; background: linear-gradient(180deg,#d9bc77,#a9b884); } +.report .chart-labels { display: flex; gap: 20pt; font-size: 7pt; } +.report .chart-labels span { display: block; flex: 1; text-align: center; } +.report .quote { font: 26pt/1.16 'DM Serif Display'; color: #a53e25; } +.report .note { font-size: 8.5pt; line-height: 1.55; } +.report .funding-table td { padding: 8pt 0; } +.report .funding-table .share { width: 88pt; } +.report .share-track { height: 5pt; background: #e3e5d7; width: 76pt; margin-top: 7pt; } +.report .share-fill { height: 5pt; background: #c65328; } +.report .allocation-total { background: #e4eadc; padding: 14pt; margin: 17pt 0 19pt; display: flex; align-items: baseline; justify-content: space-between; } +.report .allocation-total strong { font: 39pt 'Bebas Neue'; } +.report .callout { border-left: 3pt solid #c65328; padding-left: 14pt; } diff --git a/examples/browser/src/report.html b/examples/browser/src/report.html new file mode 100644 index 0000000..ec533e9 --- /dev/null +++ b/examples/browser/src/report.html @@ -0,0 +1,36 @@ +Common Ground — Community Review 2026 +
+
Common Ground
COMMUNITY TRUST
PEOPLE
PLACES
POSSIBILITIES
+

COMMON
GROUND

Stronger places.
More room
for everyone.

2026

COMMUNITY REVIEW

+ + + + + + + + + + + + + + + + +
A YEAR
IN COMMON
02 / OUR REACH

Small actions,
shared outcomes.

03 / OUR RESOURCES

Where the
support goes.

+
COMMON GROUND / COMMUNITY REVIEWFICTIONAL DATA · 01 / 03
+ +
Common Ground
02 / OUR REACH
+

A PLACE TO BEGIN

Small actions.
Shared outcomes.

An open door, a working kitchen, a garden plot. This year, our fictional neighborhood programs connected more people with practical spaces to learn, make, and belong.

+
1,240

households took part
across four neighborhoods

4,860

volunteer hours
shared across our programs

12

community spaces
kept open and active

+

More neighbors, each quarter.

PARTICIPATING HOUSEHOLDS
420
560
790
980
Q1Q2Q3Q4

Quarterly participation counts; a household can participate in more than one quarter.

+

“Belonging starts
with somewhere
to show up.”

OUR PROGRAM PRINCIPLE

Built around daily life.

Evening workshops, shared meals, and weekend garden sessions made participation easier for people with different schedules. The next step is a longer opening window at each neighborhood space.

+
ILLUSTRATIVE ANNUAL REVIEW / NOT AN ACTUAL ORGANIZATION02 / 03
+ +
Common Ground
03 / OUR RESOURCES

FOLLOW THE SUPPORT

Good intentions.
Clear allocations.

A practical view of this fictional operating budget. Every category supports the same aim: useful places, open to more people.

+
PROGRAM AREAALLOCATIONSHARE
Neighborhood spaces$168,000.00
Learning & workshops$126,000.00
Shared meals & gardens$84,000.00
Access & transport$42,000.00
+
TOTAL OPERATING ALLOCATION · USD$420,000.00
+

What we count.

Households are deduplicated within the annual program register. Volunteer hours include recorded event and maintenance time. Space counts refer to year-round locations, not individual events.

Quarterly counts show participation within each period and must not be added to estimate unique annual reach.

What comes next.

Extend evening access. Improve step-free entrances. Publish a simple monthly view of program activity and spending.

A shared tomorrow
is built in the everyday.

+
COMMON GROUND / METHODS & ALLOCATIONSFICTIONAL DATA · 03 / 03
+
diff --git a/examples/browser/src/style.css b/examples/browser/src/style.css new file mode 100644 index 0000000..bb3a511 --- /dev/null +++ b/examples/browser/src/style.css @@ -0,0 +1,11 @@ +@font-face{font-family:Inter;src:url('/fullbleed/fonts/Inter-Variable.ttf') format('truetype');font-weight:100 900;font-display:swap} +:root{font-family:Inter,sans-serif;color:#173b35;background:#f6f5ef;font-synthesis:none;font-size:15px;line-height:1.5} +*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button,a,input,select{touch-action:manipulation}a{color:inherit}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #d45d34;outline-offset:4px} +.site-header{max-width:1440px;margin:auto;padding:22px 5vw;display:flex;align-items:center;gap:28px;border-bottom:1px solid #d8ded5}.wordmark{font-weight:850;font-size:29px;letter-spacing:-1.6px;text-decoration:none}.wordmark span{color:#d85330}.header-note{font-size:10px;letter-spacing:2px;font-weight:700;margin-left:8px}.docs-link{margin-left:auto;text-decoration:none;font-size:13px;font-weight:650}.docs-link span{margin-left:10px} +main{max-width:1440px;margin:auto;padding:0 5vw}.intro{padding:58px 0 38px}.eyebrow{font-size:10px;letter-spacing:2px;font-weight:750;display:flex;align-items:center;gap:9px;margin:0 0 16px}.eyebrow span{width:7px;height:7px;border-radius:100%;background:#d85330}h1{font-size:clamp(34px,4.6vw,62px);font-weight:650;letter-spacing:-2.5px;line-height:1.05;margin:0 0 18px}.lede{font-size:16px;color:#54645b;line-height:1.65;margin:0}.workspace{display:grid;grid-template-columns:minmax(260px,340px) minmax(0,1fr);gap:32px;align-items:start}.controls{border-top:2px solid #173b35;padding-top:21px}.panel-heading,.preview-toolbar>div{display:flex;align-items:center;gap:12px}.step{color:#a64625;border:1px solid #d0c8ba;padding:3px 6px;font-size:10px;font-weight:700;letter-spacing:1px}h2{font-size:17px;letter-spacing:-.35px;margin:0;font-weight:650}.panel-heading{margin-bottom:22px}fieldset{border:0;margin:0;padding:0}label{display:block;font-size:12px;font-weight:650;margin:19px 0 7px}select,input:not([type=color]){width:100%;height:44px;border:1px solid #bdc7bd;border-radius:0;background:#fffef9;color:#173b35;padding:9px 11px}select{margin-bottom:3px}.color-row{display:flex;align-items:center;gap:10px;height:44px;border:1px solid #bdc7bd;background:#fffef9;padding:5px 8px}.color-row input{padding:0;border:0;width:30px;height:30px;background:transparent;cursor:pointer}.color-row input::-webkit-color-swatch-wrapper{padding:0}.color-row input::-webkit-color-swatch{border:0}.color-row span{font-size:11px;letter-spacing:.5px}.color-row .color-hint{color:#657163;margin-left:auto;letter-spacing:0}.sample-note{font-size:11px;line-height:1.6;color:#657163;margin:16px 0 20px}.actions{display:flex;gap:8px}button{border:1px solid #173b35;background:#173b35;color:#fffdf6;min-height:43px;padding:10px 15px;font-size:12px;font-weight:650;cursor:pointer}button:disabled{opacity:.45;cursor:default}.actions #generate{flex:1;text-align:left;display:flex;justify-content:space-between;gap:15px}.secondary{background:transparent;color:#173b35}button[hidden]{display:none}#status{font-size:12px;min-height:36px;margin:12px 0 24px;color:#54645b;overflow-wrap:anywhere}#status.error{color:#9c321c}.local-note{display:flex;align-items:flex-start;gap:9px;font-size:11px;color:#59695e;line-height:1.6;border-top:1px solid #d8ded5;padding-top:18px}.local-note p{margin:0}.local-note strong{color:#173b35}.local-dot{width:6px;height:6px;flex-shrink:0;background:#297254;border-radius:50%;margin-top:6px}.source-note{margin-top:29px;padding-top:19px;border-top:1px solid #d8ded5}.source-note h3{font-size:13px;font-weight:700;margin:0 0 7px}.source-note p{font-size:12px;color:#657163;line-height:1.6;margin:0 0 12px}.source-note a{font-size:11px;font-weight:650;text-underline-offset:4px} +.preview-panel{background:#e7e9e1;min-width:0;border:1px solid #d0d8cd}.preview-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:17px 20px;background:#fdfcf6;border-bottom:1px solid #d0d8cd}.preview-toolbar h2{font-size:14px}.preview-toolbar button{font-size:11px;white-space:nowrap;padding:8px 12px;min-height:35px}.preview-toolbar button span{margin-left:8px}#page-count{font-size:10px;color:#657163;white-space:nowrap}.preview-area{padding:28px;min-height:550px;max-height:1000px;overflow:auto;background-image:radial-gradient(#c9cec3 .6px,transparent .6px);background-size:8px 8px;display:flex;flex-direction:column;align-items:center;gap:24px}.preview-area img{display:block;width:100%;max-width:680px;height:auto;box-shadow:0 10px 34px #173b3522;background:white}.placeholder{min-height:480px;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#647166}.placeholder p{font-size:12px}.paper-icon{height:68px;width:49px;border:1px solid #91a08f;background:linear-gradient(#bfc9bb 0 0) 10px 17px/27px 1px no-repeat,linear-gradient(#bfc9bb 0 0) 10px 24px/27px 1px no-repeat,linear-gradient(#bfc9bb 0 0) 10px 31px/18px 1px no-repeat;background-color:#f5f5ee;transform:rotate(-6deg)}.preview-area[aria-busy=true] .paper-icon{animation:breathe 1.3s ease-in-out infinite}@keyframes breathe{50%{opacity:.35}}footer{display:flex;justify-content:space-between;gap:15px;border-top:1px solid #d8ded5;margin:50px 0 0;padding:20px 0 26px;font-size:11px;color:#657163}footer p{margin:0}footer a{text-underline-offset:4px}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}[hidden]{display:none!important} +@media(max-width:850px){.workspace{grid-template-columns:1fr;gap:28px}.controls{max-width:none}.intro{padding-top:36px}.header-note{display:none}h1{letter-spacing:-1.7px}.preview-area{max-height:none;min-height:400px;padding:16px}.source-note{margin-top:18px}.local-note{font-size:12px}.preview-toolbar{padding:14px;flex-wrap:wrap}.preview-toolbar>div{gap:8px}footer{margin-top:30px;flex-direction:column}.wide-only{display:none}} +@media(max-width:380px){.site-header{padding:16px 5vw}.wordmark{font-size:26px}.docs-link{font-size:11px}.intro{padding-top:29px}h1{font-size:33px;letter-spacing:-1.4px}.lede{font-size:14px}.preview-toolbar{gap:10px}.preview-toolbar button{width:100%}.preview-area{padding:10px}} +@media(prefers-reduced-motion:reduce){.preview-area[aria-busy=true] .paper-icon{animation:none}} + +.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}} diff --git a/examples/browser/vite.config.js b/examples/browser/vite.config.js new file mode 100644 index 0000000..fa22a17 --- /dev/null +++ b/examples/browser/vite.config.js @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ base: './', build: { assetsInlineLimit: 0 } }); diff --git a/tools/check-browser-starter.py b/tools/check-browser-starter.py new file mode 100644 index 0000000..1930e56 --- /dev/null +++ b/tools/check-browser-starter.py @@ -0,0 +1,124 @@ +"""Exercise the built browser starter under a URL prefix with synthetic inputs.""" +import argparse +from datetime import datetime, timezone +import hashlib +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import threading +from urllib.parse import unquote, urlsplit +from importlib.metadata import version + +from playwright.sync_api import sync_playwright, expect +from pypdf import PdfReader + +parser=argparse.ArgumentParser(description=__doc__) +parser.add_argument('--dist',type=Path,required=True) +parser.add_argument('--out',type=Path,required=True) +parser.add_argument('--browser',choices=['chrome','firefox','webkit'],default='chrome') +args=parser.parse_args() +root=args.dist.resolve();out=args.out.resolve();out.mkdir(parents=True,exist_ok=False) +prefix='/nested/demo/' +policy="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; img-src 'self' blob:; font-src 'self'; style-src 'self'; object-src 'none'; base-uri 'none'" +pause_engine=threading.Event();engine_requested=threading.Event();release_engine=threading.Event() +class Handler(SimpleHTTPRequestHandler): + def __init__(self,*a,**kw):super().__init__(*a,directory=str(root),**kw) + def translate_path(self,path): + path=unquote(urlsplit(path).path) + if not path.startswith(prefix):return str(root/'does-not-exist') + resolved=(root/path[len(prefix):]).resolve() + return str(resolved if resolved.is_relative_to(root) else root/'does-not-exist') + def end_headers(self): + self.send_header('Content-Security-Policy',policy) + self.send_header('Cache-Control','no-store') + super().end_headers() + def do_GET(self): + if pause_engine.is_set() and urlsplit(self.path).path.endswith('/fullbleed/engine.wasm'): + engine_requested.set();release_engine.wait(10) + super().do_GET() + def copyfile(self,source,outputfile): + try:super().copyfile(source,outputfile) + except (BrokenPipeError,ConnectionResetError,ConnectionAbortedError):pass + 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=[] +def check(name,passed): + assert passed,name + checks.append(name);print(name+': passed',flush=True) +try: + with sync_playwright() as pw: + browser=pw.chromium.launch(channel='chrome') if args.browser=='chrome' else getattr(pw,args.browser).launch() + context=browser.new_context(viewport={'width':1440,'height':1000},accept_downloads=True) + page=context.new_page() + page.on('pageerror',lambda error:errors.append(str(error))) + page.on('request',lambda r:network.append({'url':r.url,'method':r.method,'body':r.post_data})) + def ready(): + expect(page.locator('#download')).to_be_enabled(timeout=45000) + expect(page.locator('#status')).to_contain_text('Ready.') + def download(name,pages,text): + with page.expect_download() as event:page.locator('#download').click() + item=event.value;path=out/(name+'.pdf');item.save_as(path) + pdf=PdfReader(path);contents='\n'.join(p.extract_text() for p in pdf.pages) + 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 + 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)) + check('bundled UI font loads',page.evaluate("document.fonts.check('15px Inter')")) + download('invoice',1,['Maple & Finch','NS-1042','1,870.00']) + for width,height in [(1440,1000),(390,844),(320,760)]: + page.set_viewport_size({'width':width,'height':height}) + check(f'{width}px: no horizontal overflow',page.evaluate('document.documentElement.scrollWidth <= innerWidth')) + page.screenshot(path=str(out/f'width-{width}.png'),full_page=True) + page.set_viewport_size({'width':1440,'height':1000}) + page.locator('#customer').fill('Birch & ') + expect(page.locator('#download')).to_be_disabled() + check('changed inputs clear the previous preview',page.locator('#preview img').count()==0) + page.locator('#reference').fill('TEST-2042') + page.locator('#accent').fill('#233b79') + page.locator('#generate').click();ready() + 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 } h1 { color: {{ink}}; font-size: 24pt }') + 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')) + page.screenshot(path=str(out/'edited-template.png'),full_page=True) + page.locator('#kind').select_option('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')) + 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()) + page.locator('#reset-source').click() + pause_engine.set() + page.locator('#generate').click() + assert engine_requested.wait(5),'Worker did not request the held engine file' + page.locator('#cancel').click() + expect(page.locator('#status')).to_contain_text('Cancelled.') + check('cancellation restores controls without a stale download',page.locator('#generate').is_enabled() and page.locator('#download').is_disabled()) + pause_engine.clear();release_engine.set() + page.locator('#generate').click();ready() + download('recovered',1,['Birch & ','TEST-2042']) + 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.'} + (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) + (out/'failure.json').write_text(json.dumps({'checks':checks,'errors':errors,'network':network},indent=2)+'\n',encoding='utf-8') + raise + finally:context.close();browser.close() +finally:release_engine.set();server.shutdown();server.server_close();thread.join(timeout=5) diff --git a/verification/browser-starter-030.json b/verification/browser-starter-030.json new file mode 100644 index 0000000..c8e2140 --- /dev/null +++ b/verification/browser-starter-030.json @@ -0,0 +1,215 @@ +{ + "ok": true, + "checked_at": "2026-10-06T03:33:32.142655+00:00", + "package_version": "0.3.0", + "engine_version": "2.5.8", + "source_files": { + "examples/browser/.gitignore": "c023fec2bf12476cb47709e859f1fbb785aeb1e03281e52141e0bee1ee7dad72", + "examples/browser/index.html": "79baecb0b24716e78799893fc0076d88b2c026354f9f31acdf799c8f305c954b", + "examples/browser/LICENSE": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9", + "examples/browser/package-lock.json": "c24943d2d37f25768ce71581290e03aba5daac39beeae11e94bf2318d539ba17", + "examples/browser/package.json": "638a60e7b5aebe6f614f0808bfbe085866392b094e1b403b5dffd52141fe1c43", + "examples/browser/README.md": "1a1cc25ecb04b943b26703114135e42cfbb44baa4b939eb7534e6013e67563ca", + "examples/browser/vite.config.js": "ec725922705a3c5ba2df191a564c54c0cac6e8997ecd3d00b8a42c647bec8e74", + "examples/browser/public/favicon.svg": "3eed6dcf1013e48bc64886fed4cf815c609ed06b404742bdacdc44c1df8d3118", + "examples/browser/src/invoice.css": "0b310ce0f2964fd6b9b68e4afafa9307b4ff8c6b2f1fc753367dc755620ce427", + "examples/browser/src/invoice.html": "1f065ad6694021649e61b3f666cba6dc01aacdcbd8e80e97425b383e60d7d2fe", + "examples/browser/src/main.js": "4a320083d641b9660f113d959e707e90bf7900fcbce947f66948d17954c0c387", + "examples/browser/src/report.css": "519591ae75c239ec602324f008dd99108ebeae0b9306df7215fa5f0442905473", + "examples/browser/src/report.html": "46b0e975d43914c8b4492218feae09ea0ad684091a9f9ee3b18f66a87202e521", + "examples/browser/src/style.css": "0f66c68661a08ecbfdbbfb624b46ed6e1144d7d95de9dcc53d09180816ed0da5" + }, + "browsers": [ + { + "browser_engine": "chrome", + "browser": "154.0.8037.98", + "playwright": "1.63.0", + "pypdf": "6.19.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", + "changed inputs clear the previous preview", + "customized: download has the expected pages and text", + "customized bytes differ from the default invoice", + "edited: download has the expected pages and text", + "edited print CSS changes the PDF page size", + "document script does not run on the page", + "report: download has the expected pages and text", + "multi-page preview displays all report pages", + "each design preserves its template edits", + "reset restores only the selected template", + "invalid input displays an error without a stale download", + "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" + ], + "documents": [ + { + "name": "invoice", + "filename": "invoice-NS-1042.pdf", + "pages": 1, + "sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054" + }, + { + "name": "customized", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "ae64ae2bb3e47e564f59cd6111f200e224140ced8bfc3dff7e5e614011ab52ae" + }, + { + "name": "edited", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "9dd12f70c9b7ef78c8420d22658de164fda994796b4dbebf9c6579dd827499fc" + }, + { + "name": "report", + "filename": "community-report.pdf", + "pages": 3, + "sha256": "cbe463b5759957261dc41aadd8727f8d01485e405c2e2841c9b34c0289876c10" + }, + { + "name": "recovered", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "ae64ae2bb3e47e564f59cd6111f200e224140ced8bfc3dff7e5e614011ab52ae" + } + ], + "scope": "Owned browser; synthetic inputs; production build served at /nested/demo/ with no PDF server." + }, + { + "browser_engine": "firefox", + "browser": "155.0", + "playwright": "1.63.0", + "pypdf": "6.19.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", + "changed inputs clear the previous preview", + "customized: download has the expected pages and text", + "customized bytes differ from the default invoice", + "edited: download has the expected pages and text", + "edited print CSS changes the PDF page size", + "document script does not run on the page", + "report: download has the expected pages and text", + "multi-page preview displays all report pages", + "each design preserves its template edits", + "reset restores only the selected template", + "invalid input displays an error without a stale download", + "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" + ], + "documents": [ + { + "name": "invoice", + "filename": "invoice-NS-1042.pdf", + "pages": 1, + "sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054" + }, + { + "name": "customized", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "ae64ae2bb3e47e564f59cd6111f200e224140ced8bfc3dff7e5e614011ab52ae" + }, + { + "name": "edited", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "9dd12f70c9b7ef78c8420d22658de164fda994796b4dbebf9c6579dd827499fc" + }, + { + "name": "report", + "filename": "community-report.pdf", + "pages": 3, + "sha256": "cbe463b5759957261dc41aadd8727f8d01485e405c2e2841c9b34c0289876c10" + }, + { + "name": "recovered", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "ae64ae2bb3e47e564f59cd6111f200e224140ced8bfc3dff7e5e614011ab52ae" + } + ], + "scope": "Owned browser; synthetic inputs; production build served at /nested/demo/ with no PDF server." + }, + { + "browser_engine": "webkit", + "browser": "26.6", + "playwright": "1.63.0", + "pypdf": "6.19.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", + "changed inputs clear the previous preview", + "customized: download has the expected pages and text", + "customized bytes differ from the default invoice", + "edited: download has the expected pages and text", + "edited print CSS changes the PDF page size", + "document script does not run on the page", + "report: download has the expected pages and text", + "multi-page preview displays all report pages", + "each design preserves its template edits", + "reset restores only the selected template", + "invalid input displays an error without a stale download", + "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" + ], + "documents": [ + { + "name": "invoice", + "filename": "invoice-NS-1042.pdf", + "pages": 1, + "sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054" + }, + { + "name": "customized", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "ae64ae2bb3e47e564f59cd6111f200e224140ced8bfc3dff7e5e614011ab52ae" + }, + { + "name": "edited", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "9dd12f70c9b7ef78c8420d22658de164fda994796b4dbebf9c6579dd827499fc" + }, + { + "name": "report", + "filename": "community-report.pdf", + "pages": 3, + "sha256": "cbe463b5759957261dc41aadd8727f8d01485e405c2e2841c9b34c0289876c10" + }, + { + "name": "recovered", + "filename": "invoice-TEST-2042.pdf", + "pages": 1, + "sha256": "ae64ae2bb3e47e564f59cd6111f200e224140ced8bfc3dff7e5e614011ab52ae" + } + ], + "scope": "Owned browser; synthetic inputs; production build served at /nested/demo/ with no PDF server." + } + ], + "reviewed_screenshots": [ + "Chrome 1440px and 320px", + "WebKit 390px" + ], + "scope": "Public npm package, production Vite bundle under a URL prefix, synthetic inputs. Browser API certification and standards conformance are not claimed." +} diff --git a/verification/npm-030-public.json b/verification/npm-030-public.json new file mode 100644 index 0000000..2e4a788 --- /dev/null +++ b/verification/npm-030-public.json @@ -0,0 +1,16 @@ +{ + "ok": true, + "checked_at": "2026-10-06T03:32:03.294918+00:00", + "package_version": "0.3.0", + "engine_version": "2.5.8", + "registry": "https://registry.npmjs.org/fullbleed/0.3.0", + "tarball_sha256": "e02086f88d9472bba34435ba7229423218a03c98964cc5f0cbed8c16dcea68d4", + "package_integrity": "sha512-9HimnoHTEpgtGhqi9gF+IMSEW8g3GRK11yUkmL6OYD40D/RhWoIF7SqKuDs6nrHF7loWnhtNDWz3Hp/DNaBbMQ==", + "installed_files_matched": 28, + "fresh_cache": true, + "empty_user_config": true, + "release": "https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.3.0", + "source_commit": "b53615597fdc45b0dfb2adc2cfeea6dd9e6dafcd", + "publication": "https://github.com/fullbleed-engine/fullbleed-node/actions/runs/37409246969", + "scope": "Anonymous public-registry bytes match the CI/GitHub release. Every installed package file matches the tarball. Vite starter built from that package; browser UI checks are retained separately." +} From 55261db4edc915f546d0e55e578d613bae858c22 Mon Sep 17 00:00:00 2001 From: krflol Date: Mon, 5 Oct 2026 22:35:30 -0500 Subject: [PATCH 2/2] Handle WebKit select and color-input appearance --- examples/browser/src/main.js | 6 ++++++ examples/browser/src/style.css | 1 + tools/check-browser-starter.py | 4 ++++ verification/browser-starter-030.json | 30 ++++++++++++++++++++++++--- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/examples/browser/src/main.js b/examples/browser/src/main.js index 0e30b60..8e52c14 100644 --- a/examples/browser/src/main.js +++ b/examples/browser/src/main.js @@ -10,6 +10,12 @@ const assetBaseUrl = new URL('fullbleed/', new URL(import.meta.env.BASE_URL, loc let renderer; const escapeHtml = value => value.replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]); const placeholder = $('preview').firstElementChild.cloneNode(true); +if ($('accent').type !== 'color') { + $('accent').classList.add('color-text'); + $('accent').maxLength = 7; + $('accent').setAttribute('pattern', '#[0-9a-fA-F]{6}'); + $('color-value').hidden = true; +} let controller, pdfUrl, filename, previewUrls = []; const defaults = { invoice: { html: invoiceTemplate, css: invoiceCss }, diff --git a/examples/browser/src/style.css b/examples/browser/src/style.css index bb3a511..76e7e34 100644 --- a/examples/browser/src/style.css +++ b/examples/browser/src/style.css @@ -9,3 +9,4 @@ main{max-width:1440px;margin:auto;padding:0 5vw}.intro{padding:58px 0 38px}.eyeb @media(prefers-reduced-motion:reduce){.preview-area[aria-busy=true] .paper-icon{animation:none}} .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} diff --git a/tools/check-browser-starter.py b/tools/check-browser-starter.py index 1930e56..105f784 100644 --- a/tools/check-browser-starter.py +++ b/tools/check-browser-starter.py @@ -74,6 +74,10 @@ def download(name,pages,text): check(f'{width}px: no horizontal overflow',page.evaluate('document.documentElement.scrollWidth <= innerWidth')) page.screenshot(path=str(out/f'width-{width}.png'),full_page=True) page.set_viewport_size({'width':1440,'height':1000}) + page.locator('#accent').fill('#233b79') + page.locator('#generate').click();ready() + download('color-only',1,['Maple & Finch','NS-1042']) + check('changing only ink color changes the rendered PDF',documents[-1]['sha256']!=documents[0]['sha256']) page.locator('#customer').fill('Birch & ') expect(page.locator('#download')).to_be_disabled() check('changed inputs clear the previous preview',page.locator('#preview img').count()==0) diff --git a/verification/browser-starter-030.json b/verification/browser-starter-030.json index c8e2140..ff72376 100644 --- a/verification/browser-starter-030.json +++ b/verification/browser-starter-030.json @@ -1,6 +1,6 @@ { "ok": true, - "checked_at": "2026-10-06T03:33:32.142655+00:00", + "checked_at": "2026-10-06T03:35:30.320823+00:00", "package_version": "0.3.0", "engine_version": "2.5.8", "source_files": { @@ -14,10 +14,10 @@ "examples/browser/public/favicon.svg": "3eed6dcf1013e48bc64886fed4cf815c609ed06b404742bdacdc44c1df8d3118", "examples/browser/src/invoice.css": "0b310ce0f2964fd6b9b68e4afafa9307b4ff8c6b2f1fc753367dc755620ce427", "examples/browser/src/invoice.html": "1f065ad6694021649e61b3f666cba6dc01aacdcbd8e80e97425b383e60d7d2fe", - "examples/browser/src/main.js": "4a320083d641b9660f113d959e707e90bf7900fcbce947f66948d17954c0c387", + "examples/browser/src/main.js": "10312b7064b0b2c19d97194b3953f639cc1f6842c35acf85f5e9543fa9c1d27f", "examples/browser/src/report.css": "519591ae75c239ec602324f008dd99108ebeae0b9306df7215fa5f0442905473", "examples/browser/src/report.html": "46b0e975d43914c8b4492218feae09ea0ad684091a9f9ee3b18f66a87202e521", - "examples/browser/src/style.css": "0f66c68661a08ecbfdbbfb624b46ed6e1144d7d95de9dcc53d09180816ed0da5" + "examples/browser/src/style.css": "66ac49a5f55e8b855d4f9c1222b349fcca7948f0ea4f71411ecbae53b16f7211" }, "browsers": [ { @@ -32,6 +32,8 @@ "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", @@ -55,6 +57,12 @@ "pages": 1, "sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054" }, + { + "name": "color-only", + "filename": "invoice-NS-1042.pdf", + "pages": 1, + "sha256": "424848919562d03127bee320322615211ec2df84f3fbdb38685cceb718124c69" + }, { "name": "customized", "filename": "invoice-TEST-2042.pdf", @@ -94,6 +102,8 @@ "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", @@ -117,6 +127,12 @@ "pages": 1, "sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054" }, + { + "name": "color-only", + "filename": "invoice-NS-1042.pdf", + "pages": 1, + "sha256": "424848919562d03127bee320322615211ec2df84f3fbdb38685cceb718124c69" + }, { "name": "customized", "filename": "invoice-TEST-2042.pdf", @@ -156,6 +172,8 @@ "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", @@ -179,6 +197,12 @@ "pages": 1, "sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054" }, + { + "name": "color-only", + "filename": "invoice-NS-1042.pdf", + "pages": 1, + "sha256": "424848919562d03127bee320322615211ec2df84f3fbdb38685cceb718124c69" + }, { "name": "customized", "filename": "invoice-TEST-2042.pdf",