diff --git a/.gitattributes b/.gitattributes index 49fbf25..de3dc4e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -135,3 +135,20 @@ examples/accessible-showcase-alpha5/fonts/*.ttf binary docs/assets/accessible-showcase-alpha5/** -text whitespace=cr-at-eol docs/assets/accessible-showcase-alpha5/*.pdf binary docs/assets/accessible-showcase-alpha5/fonts/*.ttf binary + +# Alpha 6 download hashes cover exact source and evidence bytes on every OS. +examples/component-notebook-alpha6/** text eol=lf +docs/assets/component-notebook-alpha6/** -text +docs/assets/component-notebook-alpha6/*.pdf binary +examples/bootstrap-alpha6/** -text whitespace=cr-at-eol +docs/assets/bootstrap-alpha6/** -text +docs/assets/bootstrap-alpha6/*.pdf binary +examples/showcase-authoring-alpha6/** -text whitespace=cr-at-eol +examples/showcase-authoring-alpha6/**/fonts/*-OFL.txt -whitespace +docs/assets/showcase-authoring-alpha6/** -text whitespace=cr-at-eol +docs/assets/showcase-authoring-alpha6/*.pdf binary +examples/accessible-showcase-alpha6/** -text +examples/accessible-showcase-alpha6/fonts/*.ttf binary +docs/assets/accessible-showcase-alpha6/** -text whitespace=cr-at-eol +docs/assets/accessible-showcase-alpha6/*.pdf binary +docs/assets/accessible-showcase-alpha6/fonts/*.ttf binary diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9b8d406..74f3577 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,18 +21,18 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - channel: [alpha4, alpha5] + channel: [alpha4, alpha5, alpha6] steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: '3.12' - run: | - python -m pip install --only-binary=:all: -r examples/accessible-showcase${{ matrix.channel == 'alpha5' && '-alpha5' || '' }}/requirements-check.txt + python -m pip install --only-binary=:all: -r examples/accessible-showcase${{ matrix.channel != 'alpha4' && format('-{0}', matrix.channel) || '' }}/requirements-check.txt - name: Rebuild the source kit and reproduce the externally validated PDF run: | python tools/build_accessible_showcase.py --channel ${{ matrix.channel }} - git diff --exit-code -- docs/assets/accessible-showcase${{ matrix.channel == 'alpha5' && '-alpha5' || '' }}/project.zip docs/assets/accessible-showcase${{ matrix.channel == 'alpha5' && '-alpha5' || '' }}/source.json + git diff --exit-code -- docs/assets/accessible-showcase${{ matrix.channel != 'alpha4' && format('-{0}', matrix.channel) || '' }}/project.zip docs/assets/accessible-showcase${{ matrix.channel != 'alpha4' && format('-{0}', matrix.channel) || '' }}/source.json python tools/verify_accessible_showcase.py --channel ${{ matrix.channel }} --out target/accessible-showcase - uses: actions/upload-artifact@v4 if: always() @@ -48,19 +48,19 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - channel: [alpha4, alpha5] + channel: [alpha4, alpha5, alpha6] steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: '3.12' - run: | - python -m pip install --only-binary=:all: -r examples/showcase-authoring${{ matrix.channel == 'alpha5' && '-alpha5' || '' }}/requirements.txt pypdf==6.19.0 pypdfium2==5.14.0 Pillow==12.3.0 + python -m pip install --only-binary=:all: -r examples/showcase-authoring${{ matrix.channel != 'alpha4' && format('-{0}', matrix.channel) || '' }}/requirements.txt pypdf==6.19.0 pypdfium2==5.14.0 Pillow==12.3.0 - name: Rebuild the download and compare both authoring paths shell: bash run: | python tools/build_showcase_authoring.py --channel ${{ matrix.channel }} - git diff --exit-code -- docs/assets/showcase-authoring${{ matrix.channel == 'alpha5' && '-alpha5' || '' }}/project.zip docs/assets/showcase-authoring${{ matrix.channel == 'alpha5' && '-alpha5' || '' }}/source.json + git diff --exit-code -- docs/assets/showcase-authoring${{ matrix.channel != 'alpha4' && format('-{0}', matrix.channel) || '' }}/project.zip docs/assets/showcase-authoring${{ matrix.channel != 'alpha4' && format('-{0}', matrix.channel) || '' }}/source.json python tools/verify_showcase_authoring.py --channel ${{ matrix.channel }} --out target/showcase-authoring python target/showcase-authoring/project/tools/check_showcase_preview.py --project target/showcase-authoring/project/examples/design_showcase --out target/showcase-authoring/live-preview - uses: actions/upload-artifact@v4 @@ -77,7 +77,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - channel: [alpha3, alpha5] + channel: [alpha3, alpha5, alpha6] steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v6 @@ -151,7 +151,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - channel: [preview, alpha, alpha2, alpha3, alpha4, alpha5] + channel: [preview, alpha, alpha2, alpha3, alpha4, alpha5, alpha6] steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v6 @@ -934,8 +934,8 @@ jobs: - name: Verify live component previews and browser downloads run: | python -m venv output/showcase-live-env - output/showcase-live-env/bin/python -m pip install --only-binary=:all: -r examples/showcase-authoring/requirements.txt - python examples/showcase-authoring/tools/check_showcase_preview_browser.py --project examples/showcase-authoring/examples/design_showcase --python output/showcase-live-env/bin/python --out output/showcase-live-ui --channel chrome + output/showcase-live-env/bin/python -m pip install --only-binary=:all: -r examples/showcase-authoring-alpha6/requirements.txt + python examples/showcase-authoring-alpha6/tools/check_showcase_preview_browser.py --project examples/showcase-authoring-alpha6/examples/design_showcase --python output/showcase-live-env/bin/python --out output/showcase-live-ui --channel chrome - uses: actions/upload-artifact@v4 if: always() with: diff --git a/docs/assets/accessible-showcase-alpha6/a11y-source.json b/docs/assets/accessible-showcase-alpha6/a11y-source.json new file mode 100644 index 0000000..bd4b251 --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/a11y-source.json @@ -0,0 +1,9 @@ +{ + "diagnostics": [], + "error_count": 0, + "errors": [], + "mode": "raise", + "ok": true, + "warning_count": 0, + "warnings": [] +} diff --git a/docs/assets/accessible-showcase-alpha6/browser-verification.json b/docs/assets/accessible-showcase-alpha6/browser-verification.json new file mode 100644 index 0000000..c011485 --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/browser-verification.json @@ -0,0 +1,270 @@ +{ + "ok": true, + "browser": "155.0.8059.40", + "styled": [ + { + "width": 1440, + "ok": true, + "metrics": { + "viewport": 1440, + "scroll": 1440, + "images": [ + { + "complete": true, + "width": 880, + "alt": "Illustration of a shared garden: three raised planting beds beside a broad, level path, with a bench and a greenhouse." + } + ], + "headings": [ + { + "tag": "H1", + "text": "A place to grow.A place to belong." + }, + { + "tag": "H2", + "text": "Find your starting point" + }, + { + "tag": "H3", + "text": "Grow something" + }, + { + "tag": "H3", + "text": "Share a table" + }, + { + "tag": "H3", + "text": "Make it last" + }, + { + "tag": "H2", + "text": "Every visit starts with hello" + }, + { + "tag": "H2", + "text": "Make room for a good day." + }, + { + "tag": "H2", + "text": "Plan a comfortable visit" + }, + { + "tag": "H3", + "text": "An easy arrival" + }, + { + "tag": "H3", + "text": "Choose your pace" + }, + { + "tag": "H2", + "text": "A small first step is enough" + }, + { + "tag": "H2", + "text": "About this guide" + } + ], + "link": "https://docs.fullbleed.dev/", + "grids": [ + "209.281px 209.297px 209.297px", + "316.594px 316.594px" + ], + "sheets": [ + { + "bottom": 1142.140625, + "contentBottom": 866.578125 + }, + { + "bottom": 2284.28125, + "contentBottom": 2185.640625 + } + ] + } + }, + { + "width": 390, + "ok": true, + "metrics": { + "viewport": 390, + "scroll": 390, + "images": [ + { + "complete": true, + "width": 880, + "alt": "Illustration of a shared garden: three raised planting beds beside a broad, level path, with a bench and a greenhouse." + } + ], + "headings": [ + { + "tag": "H1", + "text": "A place to grow.A place to belong." + }, + { + "tag": "H2", + "text": "Find your starting point" + }, + { + "tag": "H3", + "text": "Grow something" + }, + { + "tag": "H3", + "text": "Share a table" + }, + { + "tag": "H3", + "text": "Make it last" + }, + { + "tag": "H2", + "text": "Every visit starts with hello" + }, + { + "tag": "H2", + "text": "Make room for a good day." + }, + { + "tag": "H2", + "text": "Plan a comfortable visit" + }, + { + "tag": "H3", + "text": "An easy arrival" + }, + { + "tag": "H3", + "text": "Choose your pace" + }, + { + "tag": "H2", + "text": "A small first step is enough" + }, + { + "tag": "H2", + "text": "About this guide" + } + ], + "link": "https://docs.fullbleed.dev/", + "grids": [ + "342px", + "342px" + ], + "sheets": [ + { + "bottom": 1019.640625, + "contentBottom": 995.640625 + }, + { + "bottom": 2374.828125, + "contentBottom": 2340.171875 + } + ] + } + }, + { + "width": 320, + "ok": true, + "metrics": { + "viewport": 320, + "scroll": 320, + "images": [ + { + "complete": true, + "width": 880, + "alt": "Illustration of a shared garden: three raised planting beds beside a broad, level path, with a bench and a greenhouse." + } + ], + "headings": [ + { + "tag": "H1", + "text": "A place to grow.A place to belong." + }, + { + "tag": "H2", + "text": "Find your starting point" + }, + { + "tag": "H3", + "text": "Grow something" + }, + { + "tag": "H3", + "text": "Share a table" + }, + { + "tag": "H3", + "text": "Make it last" + }, + { + "tag": "H2", + "text": "Every visit starts with hello" + }, + { + "tag": "H2", + "text": "Make room for a good day." + }, + { + "tag": "H2", + "text": "Plan a comfortable visit" + }, + { + "tag": "H3", + "text": "An easy arrival" + }, + { + "tag": "H3", + "text": "Choose your pace" + }, + { + "tag": "H2", + "text": "A small first step is enough" + }, + { + "tag": "H2", + "text": "About this guide" + } + ], + "link": "https://docs.fullbleed.dev/", + "grids": [ + "272px", + "272px" + ], + "sheets": [ + { + "bottom": 1166.515625, + "contentBottom": 1142.515625 + }, + { + "bottom": 2719.8125, + "contentBottom": 2685.15625 + } + ] + } + } + ], + "unstyled_semantics_and_no_css": true, + "keyboard_link_focus": true, + "authored_text_color_contrast": [ + { + "foreground": "#17382e", + "background": "#f8f5ec", + "ratio": 11.73 + }, + { + "foreground": "#17382e", + "background": "#e3eadc", + "ratio": 10.4 + }, + { + "foreground": "#17382e", + "background": "#e9eddf", + "ratio": 10.75 + }, + { + "foreground": "#3c5548", + "background": "#f8f5ec", + "ratio": 7.45 + } + ] +} diff --git a/docs/assets/accessible-showcase-alpha6/common-ground-guide.pdf b/docs/assets/accessible-showcase-alpha6/common-ground-guide.pdf new file mode 100644 index 0000000..b0c5123 Binary files /dev/null and b/docs/assets/accessible-showcase-alpha6/common-ground-guide.pdf differ diff --git a/docs/assets/accessible-showcase-alpha6/fonts/DMSerifDisplay-OFL.txt b/docs/assets/accessible-showcase-alpha6/fonts/DMSerifDisplay-OFL.txt new file mode 100644 index 0000000..aea163a --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/fonts/DMSerifDisplay-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. Copyright 2019 Google LLC. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/assets/accessible-showcase-alpha6/fonts/DMSerifDisplay-Regular.ttf b/docs/assets/accessible-showcase-alpha6/fonts/DMSerifDisplay-Regular.ttf new file mode 100644 index 0000000..bc5ed0c Binary files /dev/null and b/docs/assets/accessible-showcase-alpha6/fonts/DMSerifDisplay-Regular.ttf differ diff --git a/docs/assets/accessible-showcase-alpha6/fonts/Inter-OFL.txt b/docs/assets/accessible-showcase-alpha6/fonts/Inter-OFL.txt new file mode 100644 index 0000000..909a6bf --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/fonts/Inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/assets/accessible-showcase-alpha6/fonts/Inter-Variable.ttf b/docs/assets/accessible-showcase-alpha6/fonts/Inter-Variable.ttf new file mode 100644 index 0000000..047c92f Binary files /dev/null and b/docs/assets/accessible-showcase-alpha6/fonts/Inter-Variable.ttf differ diff --git a/docs/assets/accessible-showcase-alpha6/garden.svg b/docs/assets/accessible-showcase-alpha6/garden.svg new file mode 100644 index 0000000..31a0f3f --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/garden.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/accessible-showcase-alpha6/guide-unstyled.html b/docs/assets/accessible-showcase-alpha6/guide-unstyled.html new file mode 100644 index 0000000..67b3458 --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/guide-unstyled.html @@ -0,0 +1 @@ +Common Ground: A place to grow, a place to belong

Common Ground

Community field guide / Autumn 2026

A place to grow.
A place to belong.

Good things happen when we make a little room for one another. Come for a quiet hour, a shared meal or something new to learn.

Illustration of a shared garden: three raised planting beds beside a broad, level path, with a bench and a greenhouse.

Find your starting point

Grow something

A few seeds. A pair of hands. A garden we tend together.

Share a table

Pull up a chair for conversation and a warm bowl of soup.

Make it last

Swap a skill, mend a favorite thing or try a small project.

Every visit starts with hello

All sessions are free. No membership, equipment or experience is needed. Come on your own or bring someone with you.

Your next Saturday

Make room for a good day.

Stay for one session or linger a little longer. You decide how to take part.

Autumn 2026 · Weekly sessions at the Common Ground garden
ProgramWhenWhat to expect
Garden togetherSaturdays, 10–11:30 amPlant, water or harvest. Seated tasks are available.
The shared tableSaturdays, noon–1 pmBring your lunch or share the seasonal soup. Ingredients are listed.
Make & mendFirst Saturday, 2–3:30 pmBring a small repair or try a simple craft. Materials are provided.

Plan a comfortable visit

An easy arrival

  • Enter through the wide gate beside the greenhouse.
  • The main path is firm and level. Seating is available along the way.
  • A welcome volunteer can show you the garden and the accessible restroom.

Choose your pace

  • Take part seated or standing, or simply watch.
  • Use the quiet corner whenever you would like a break.
  • Ask for written instructions or a demonstration. Both are welcome.

A small first step is enough

You do not have to know anyone, know what to say or stay all day. There is a place for you here.

About this guide

Common Ground and its programs are fictional. This sample shows how a thoughtfully designed document can carry its structure into PDF and HTML.

Fullbleed documentation: docs.fullbleed.dev

\ No newline at end of file diff --git a/docs/assets/accessible-showcase-alpha6/guide.html b/docs/assets/accessible-showcase-alpha6/guide.html new file mode 100644 index 0000000..e813ca3 --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/guide.html @@ -0,0 +1,64 @@ +Common Ground: A place to grow, a place to belong

Common Ground

Community field guide / Autumn 2026

A place to grow.
A place to belong.

Good things happen when we make a little room for one another. Come for a quiet hour, a shared meal or something new to learn.

Illustration of a shared garden: three raised planting beds beside a broad, level path, with a bench and a greenhouse.

Find your starting point

Grow something

A few seeds. A pair of hands. A garden we tend together.

Share a table

Pull up a chair for conversation and a warm bowl of soup.

Make it last

Swap a skill, mend a favorite thing or try a small project.

Every visit starts with hello

All sessions are free. No membership, equipment or experience is needed. Come on your own or bring someone with you.

Your next Saturday

Make room for a good day.

Stay for one session or linger a little longer. You decide how to take part.

Autumn 2026 · Weekly sessions at the Common Ground garden
ProgramWhenWhat to expect
Garden togetherSaturdays, 10–11:30 amPlant, water or harvest. Seated tasks are available.
The shared tableSaturdays, noon–1 pmBring your lunch or share the seasonal soup. Ingredients are listed.
Make & mendFirst Saturday, 2–3:30 pmBring a small repair or try a simple craft. Materials are provided.

Plan a comfortable visit

An easy arrival

  • Enter through the wide gate beside the greenhouse.
  • The main path is firm and level. Seating is available along the way.
  • A welcome volunteer can show you the garden and the accessible restroom.

Choose your pace

  • Take part seated or standing, or simply watch.
  • Use the quiet corner whenever you would like a break.
  • Ask for written instructions or a demonstration. Both are welcome.

A small first step is enough

You do not have to know anyone, know what to say or stay all day. There is a place for you here.

About this guide

Common Ground and its programs are fictional. This sample shows how a thoughtfully designed document can carry its structure into PDF and HTML.

Fullbleed documentation: docs.fullbleed.dev

\ No newline at end of file diff --git a/docs/assets/accessible-showcase-alpha6/page-1.png b/docs/assets/accessible-showcase-alpha6/page-1.png new file mode 100644 index 0000000..180311b Binary files /dev/null and b/docs/assets/accessible-showcase-alpha6/page-1.png differ diff --git a/docs/assets/accessible-showcase-alpha6/page-2.png b/docs/assets/accessible-showcase-alpha6/page-2.png new file mode 100644 index 0000000..ef10978 Binary files /dev/null and b/docs/assets/accessible-showcase-alpha6/page-2.png differ diff --git a/docs/assets/accessible-showcase-alpha6/pdfua1.verapdf.json b/docs/assets/accessible-showcase-alpha6/pdfua1.verapdf.json new file mode 100644 index 0000000..ef75ac6 --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/pdfua1.verapdf.json @@ -0,0 +1,63 @@ +{"report":{"buildInformation":{"releaseDetails":[{ + "id" : "core", + "version" : "1.30.3", + "buildDate" : 1791385320000 +},{ + "id" : "validation-model", + "version" : "1.30.3", + "buildDate" : 1791398640000 +},{ + "id" : "apps", + "version" : "1.30.3", + "buildDate" : 1791399120000 +}]},"jobs":[{"itemDetails":{ + "name" : "C:\\dev\\workbench\\fullbleed-official\\target\\growth\\alpha-pagination-20261018\\accessible\\common-ground-guide.pdf", + "size" : 141684 +},"validationResult":[ { + "details" : { + "passedRules" : 106, + "failedRules" : 0, + "passedChecks" : 5512, + "failedChecks" : 0, + "ruleSummaries" : [ ] + }, + "jobEndStatus" : "normal", + "profileName" : "PDF/UA-1 validation profile", + "statement" : "PDF file is compliant with Validation Profile requirements.", + "compliant" : true +} ],"processingTime":{ + "start" : 1792328872357, + "finish" : 1792328876480, + "duration" : "00:00:04.123", + "difference" : 4123 +}}],"batchSummary":{ + "duration" : { + "start" : 1792328871640, + "finish" : 1792328876703, + "duration" : "00:00:05.063", + "difference" : 5063 + }, + "totalJobs" : 1, + "outOfMemory" : 0, + "veraExceptions" : 0, + "failedEncryptedJobs" : 0, + "validationSummary" : { + "nonCompliantPdfaCount" : 0, + "failedJobCount" : 0, + "totalJobCount" : 1, + "compliantPdfaCount" : 1, + "successfulJobCount" : 1 + }, + "failedParsingJobs" : 0, + "repairSummary" : { + "failedJobCount" : 0, + "totalJobCount" : 0, + "successfulJobCount" : 0 + }, + "featuresSummary" : { + "failedJobCount" : 0, + "totalJobCount" : 0, + "successfulJobCount" : 0 + }, + "multiJob" : false +}}} \ No newline at end of file diff --git a/docs/assets/accessible-showcase-alpha6/pdfua1.verapdf.stderr.txt b/docs/assets/accessible-showcase-alpha6/pdfua1.verapdf.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/assets/accessible-showcase-alpha6/project.zip b/docs/assets/accessible-showcase-alpha6/project.zip new file mode 100644 index 0000000..7284fca Binary files /dev/null and b/docs/assets/accessible-showcase-alpha6/project.zip differ diff --git a/docs/assets/accessible-showcase-alpha6/render-report.json b/docs/assets/accessible-showcase-alpha6/render-report.json new file mode 100644 index 0000000..39b181c --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/render-report.json @@ -0,0 +1,26 @@ +{ + "schema": "fullbleed.accessible_showcase.v1", + "ok": true, + "runtime": "2.6.0a6", + "pdf": "common-ground-guide.pdf", + "sha256": "e59754b06ebd3229b35033c74b7c08175543396b48d9c152cc3419c7e58ba17d", + "page_count": 2, + "deterministic_replay": true, + "html_css_pdf_byte_parity": true, + "native_diagnostic_gate": true, + "previews": [ + "preview/guide_page1.png", + "preview/guide_page2.png" + ], + "fonts": [ + { + "file": "Inter-Variable.ttf", + "sha256": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031" + }, + { + "file": "DMSerifDisplay-Regular.ttf", + "sha256": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616" + } + ], + "external_pdfua_validation": "Run veraPDF separately; see README.md." +} diff --git a/docs/assets/accessible-showcase-alpha6/source.json b/docs/assets/accessible-showcase-alpha6/source.json new file mode 100644 index 0000000..9659d3d --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/source.json @@ -0,0 +1,41 @@ +{ + "repository": "https://github.com/fullbleed-engine/fullbleed-official", + "commit": "9da6a672e81fabebb5733aadcdd932355188bac5", + "path": "examples/accessible_showcase", + "runtime": "2.6.0a6", + "source_files": { + ".gitignore": "7e2f65c80bddb76631f05a448b09378247e7f14af4ade18127925c07a6538dbf", + "LICENSE": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9", + "README.md": "52707f810bc076d0c58e76a9013d821b4cff00ba04fdf45d705b3c114ee9a6c6", + "browser.css": "59876dbb2ddce6f7fc94a435584bc430ce714fc1cc6095adaa4f9e58351369a1", + "fonts/DMSerifDisplay-OFL.txt": "aa49c876a28d8c0b0a3027d6c25b9c607eee8c0fb96165011d42a6ace1408fe1", + "fonts/DMSerifDisplay-Regular.ttf": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616", + "fonts/Inter-OFL.txt": "5dd548d31a85f756e01d63e00d7faf1e324103ed3e9102fcbbabf2cc2db6dd39", + "garden.svg": "b55568259e14cca65522b69dc18c4987b399a1862ec9d99ed1b647b97996043c", + "guide.css": "29cb17b5b0992607a22258a43964828ce1d6ae31c0402d648ff7479fdb5c4e07", + "guide.py": "55398c6d9d6e1614e1d0a17018bc0a73b2998bfdddce90d139beb974d5a6f689", + "render.py": "02ce7065ee7bf3fc319a11bd4f200ffd36c581d9b27b671919d8f074b6f0ad3f", + "requirements-check.txt": "4eadc7023aba8e289cb2e9b4f46eed8c350d9c55500f98c19d938010d46add8d", + "requirements.txt": "058f91f852902be181df6d21cf5e5484cecbc11b14f920337d28d86950cc3d17", + "verify.py": "33ed212e52bc9d3f1ba89e6b8ed835d067099926d08c2abb6b55125281ed11f3" + }, + "zip_sha256": "56a9336b74bea139ad49286f0f426a7cbb24aef79d36ff477dbebffc87a65d45", + "zip_bytes": 120614, + "package_files": { + ".gitignore": "7e2f65c80bddb76631f05a448b09378247e7f14af4ade18127925c07a6538dbf", + "LICENSE": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9", + "README.md": "52707f810bc076d0c58e76a9013d821b4cff00ba04fdf45d705b3c114ee9a6c6", + "SOURCE.json": "9afdac8d721d90d016baef628c62cae7acc4a8b9dca73e09ce68e0517c9b5033", + "browser.css": "59876dbb2ddce6f7fc94a435584bc430ce714fc1cc6095adaa4f9e58351369a1", + "fonts/DMSerifDisplay-OFL.txt": "aa49c876a28d8c0b0a3027d6c25b9c607eee8c0fb96165011d42a6ace1408fe1", + "fonts/DMSerifDisplay-Regular.ttf": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616", + "fonts/Inter-OFL.txt": "5dd548d31a85f756e01d63e00d7faf1e324103ed3e9102fcbbabf2cc2db6dd39", + "garden.svg": "b55568259e14cca65522b69dc18c4987b399a1862ec9d99ed1b647b97996043c", + "guide.css": "29cb17b5b0992607a22258a43964828ce1d6ae31c0402d648ff7479fdb5c4e07", + "guide.py": "55398c6d9d6e1614e1d0a17018bc0a73b2998bfdddce90d139beb974d5a6f689", + "render.py": "02ce7065ee7bf3fc319a11bd4f200ffd36c581d9b27b671919d8f074b6f0ad3f", + "requirements-check.txt": "4eadc7023aba8e289cb2e9b4f46eed8c350d9c55500f98c19d938010d46add8d", + "requirements.txt": "058f91f852902be181df6d21cf5e5484cecbc11b14f920337d28d86950cc3d17", + "verify.py": "33ed212e52bc9d3f1ba89e6b8ed835d067099926d08c2abb6b55125281ed11f3" + } +} diff --git a/docs/assets/accessible-showcase-alpha6/structure.json b/docs/assets/accessible-showcase-alpha6/structure.json new file mode 100644 index 0000000..330e151 --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/structure.json @@ -0,0 +1,817 @@ +{ + "ok": true, + "title": "Common Ground: A place to grow, a place to belong", + "language": "en-US", + "page_count": 2, + "roles": { + "/P": 18, + "/H1": 1, + "/Figure": 1, + "/H2": 6, + "/H3": 5, + "/Caption": 2, + "/Table": 1, + "/THead": 1, + "/TR": 4, + "/TH": 3, + "/TBody": 1, + "/TD": 9, + "/Span": 11, + "/L": 2, + "/LI": 6, + "/Lbl": 6, + "/LBody": 6 + }, + "headings": [ + { + "role": "/H1", + "page": 1, + "text": "A place to grow. A place to belong." + }, + { + "role": "/H2", + "page": 1, + "text": "Find your starting point" + }, + { + "role": "/H3", + "page": 1, + "text": "Grow something" + }, + { + "role": "/H3", + "page": 1, + "text": "Share a table" + }, + { + "role": "/H3", + "page": 1, + "text": "Make it last" + }, + { + "role": "/H2", + "page": 1, + "text": "Every visit starts with hello" + }, + { + "role": "/H2", + "page": 2, + "text": "Make room for a good day." + }, + { + "role": "/H2", + "page": 2, + "text": "Plan a comfortable visit" + }, + { + "role": "/H3", + "page": 2, + "text": "An easy arrival" + }, + { + "role": "/H3", + "page": 2, + "text": "Choose your pace" + }, + { + "role": "/H2", + "page": 2, + "text": "A small first step is enough" + }, + { + "role": "/H2", + "page": 2, + "text": "About this guide" + } + ], + "nodes": [ + { + "role": "/P", + "page": 1, + "text": "Common Ground" + }, + { + "role": "/P", + "page": 1, + "text": "COMMUNITY FIELD GUIDE / AUTUMN 2026" + }, + { + "role": "/H1", + "page": 1, + "text": "A place to grow. A place to belong." + }, + { + "role": "/P", + "page": 1, + "text": "Good things happen when we make a little room for one another. Come for a quiet hour, a shared meal or something new to learn." + }, + { + "role": "/Figure", + "page": 1, + "text": "", + "alt": "Illustration of a shared garden: three raised planting beds beside a broad, level path, with a bench and a greenhouse." + }, + { + "role": "/H2", + "page": 1, + "text": "Find your starting point" + }, + { + "role": "/H3", + "page": 1, + "text": "Grow something" + }, + { + "role": "/P", + "page": 1, + "text": "A few seeds. A pair of hands. A garden we tend together." + }, + { + "role": "/H3", + "page": 1, + "text": "Share a table" + }, + { + "role": "/P", + "page": 1, + "text": "Pull up a chair for conversation and a warm bowl of soup." + }, + { + "role": "/H3", + "page": 1, + "text": "Make it last" + }, + { + "role": "/P", + "page": 1, + "text": "Swap a skill, mend a favorite thing or try a small project." + }, + { + "role": "/H2", + "page": 1, + "text": "Every visit starts with hello" + }, + { + "role": "/P", + "page": 1, + "text": "All sessions are free. No membership, equipment or experience is needed. Come on your own or bring someone with you." + }, + { + "role": "/P", + "page": 2, + "text": "YOUR NEXT SATURDAY" + }, + { + "role": "/H2", + "page": 2, + "text": "Make room for a good day." + }, + { + "role": "/P", + "page": 2, + "text": "Stay for one session or linger a little longer. You decide how to take part." + }, + { + "role": "/Caption", + "page": 2, + "text": "Autumn 2026 \u00b7 Weekly sessions at the Common Ground garden" + }, + { + "role": "/Caption", + "page": 2, + "text": "Autumn 2026 \u00b7 Weekly sessions at the Common Ground garden" + }, + { + "role": "/Table", + "page": 2, + "text": "Program When What to expect Garden together Saturdays, 10\u201311:30 am Plant, water or harvest. Seated tasks are available. The shared table Saturdays, noon\u20131 pm Bring your lunch or share the seasonal soup. Ingredients are listed. Make & mend First Saturday, 2\u20133:30 pm Bring a small repair or try a simple craft. Materials are provided." + }, + { + "role": "/THead", + "page": 2, + "text": "Program When What to expect" + }, + { + "role": "/TR", + "page": 2, + "text": "Program When What to expect" + }, + { + "role": "/TH", + "page": 2, + "text": "Program", + "attributes": { + "/O": "/Table", + "/Scope": "/Column" + } + }, + { + "role": "/TH", + "page": 2, + "text": "When", + "attributes": { + "/O": "/Table", + "/Scope": "/Column" + } + }, + { + "role": "/TH", + "page": 2, + "text": "What to expect", + "attributes": { + "/O": "/Table", + "/Scope": "/Column" + } + }, + { + "role": "/TBody", + "page": 2, + "text": "Garden together Saturdays, 10\u201311:30 am Plant, water or harvest. Seated tasks are available. The shared table Saturdays, noon\u20131 pm Bring your lunch or share the seasonal soup. Ingredients are listed. Make & mend First Saturday, 2\u20133:30 pm Bring a small repair or try a simple craft. Materials are provided." + }, + { + "role": "/TR", + "page": 2, + "text": "Garden together Saturdays, 10\u201311:30 am Plant, water or harvest. Seated tasks are available." + }, + { + "role": "/TD", + "page": 2, + "text": "Garden together" + }, + { + "role": "/Span", + "page": 2, + "text": "Garden" + }, + { + "role": "/Span", + "page": 2, + "text": "together" + }, + { + "role": "/TD", + "page": 2, + "text": "Saturdays, 10\u201311:30 am" + }, + { + "role": "/TD", + "page": 2, + "text": "Plant, water or harvest. Seated tasks are available." + }, + { + "role": "/TR", + "page": 2, + "text": "The shared table Saturdays, noon\u20131 pm Bring your lunch or share the seasonal soup. Ingredients are listed." + }, + { + "role": "/TD", + "page": 2, + "text": "The shared table" + }, + { + "role": "/Span", + "page": 2, + "text": "The" + }, + { + "role": "/Span", + "page": 2, + "text": "shared" + }, + { + "role": "/Span", + "page": 2, + "text": "table" + }, + { + "role": "/TD", + "page": 2, + "text": "Saturdays, noon\u20131 pm" + }, + { + "role": "/TD", + "page": 2, + "text": "Bring your lunch or share the seasonal soup. Ingredients are listed." + }, + { + "role": "/TR", + "page": 2, + "text": "Make & mend First Saturday, 2\u20133:30 pm Bring a small repair or try a simple craft. Materials are provided." + }, + { + "role": "/TD", + "page": 2, + "text": "Make & mend" + }, + { + "role": "/Span", + "page": 2, + "text": "Make" + }, + { + "role": "/Span", + "page": 2, + "text": "&" + }, + { + "role": "/Span", + "page": 2, + "text": "mend" + }, + { + "role": "/TD", + "page": 2, + "text": "First Saturday, 2\u20133:30 pm" + }, + { + "role": "/TD", + "page": 2, + "text": "Bring a small repair or try a simple craft. Materials are provided." + }, + { + "role": "/H2", + "page": 2, + "text": "Plan a comfortable visit" + }, + { + "role": "/H3", + "page": 2, + "text": "An easy arrival" + }, + { + "role": "/L", + "page": 2, + "text": "Enter through the wide gate beside the greenhouse. The main path is firm and level. Seating is available along the way. A welcome volunteer can show you the garden and the accessible restroom.", + "attributes": { + "/O": "/List", + "/ListNumbering": "/Disc" + } + }, + { + "role": "/LI", + "page": 2, + "text": "Enter through the wide gate beside the greenhouse." + }, + { + "role": "/Lbl", + "page": 2, + "text": "" + }, + { + "role": "/LBody", + "page": 2, + "text": "Enter through the wide gate beside the greenhouse." + }, + { + "role": "/P", + "page": 2, + "text": "Enter through the wide gate beside the greenhouse." + }, + { + "role": "/LI", + "page": 2, + "text": "The main path is firm and level. Seating is available along the way." + }, + { + "role": "/Lbl", + "page": 2, + "text": "" + }, + { + "role": "/LBody", + "page": 2, + "text": "The main path is firm and level. Seating is available along the way." + }, + { + "role": "/P", + "page": 2, + "text": "The main path is firm and level. Seating is available along the way." + }, + { + "role": "/LI", + "page": 2, + "text": "A welcome volunteer can show you the garden and the accessible restroom." + }, + { + "role": "/Lbl", + "page": 2, + "text": "" + }, + { + "role": "/LBody", + "page": 2, + "text": "A welcome volunteer can show you the garden and the accessible restroom." + }, + { + "role": "/P", + "page": 2, + "text": "A welcome volunteer can show you the garden and the accessible restroom." + }, + { + "role": "/H3", + "page": 2, + "text": "Choose your pace" + }, + { + "role": "/L", + "page": 2, + "text": "Take part seated or standing, or simply watch. Use the quiet corner whenever you would like a break. Ask for written instructions or a demonstration. Both are welcome.", + "attributes": { + "/O": "/List", + "/ListNumbering": "/Disc" + } + }, + { + "role": "/LI", + "page": 2, + "text": "Take part seated or standing, or simply watch." + }, + { + "role": "/Lbl", + "page": 2, + "text": "" + }, + { + "role": "/LBody", + "page": 2, + "text": "Take part seated or standing, or simply watch." + }, + { + "role": "/P", + "page": 2, + "text": "Take part seated or standing, or simply watch." + }, + { + "role": "/LI", + "page": 2, + "text": "Use the quiet corner whenever you would like a break." + }, + { + "role": "/Lbl", + "page": 2, + "text": "" + }, + { + "role": "/LBody", + "page": 2, + "text": "Use the quiet corner whenever you would like a break." + }, + { + "role": "/P", + "page": 2, + "text": "Use the quiet corner whenever you would like a break." + }, + { + "role": "/LI", + "page": 2, + "text": "Ask for written instructions or a demonstration. Both are welcome." + }, + { + "role": "/Lbl", + "page": 2, + "text": "" + }, + { + "role": "/LBody", + "page": 2, + "text": "Ask for written instructions or a demonstration. Both are welcome." + }, + { + "role": "/P", + "page": 2, + "text": "Ask for written instructions or a demonstration. Both are welcome." + }, + { + "role": "/H2", + "page": 2, + "text": "A small first step is enough" + }, + { + "role": "/P", + "page": 2, + "text": "You do not have to know anyone, know what to say or stay all day. There is a place for you here." + }, + { + "role": "/H2", + "page": 2, + "text": "About this guide" + }, + { + "role": "/P", + "page": 2, + "text": "Common Ground and its programs are fictional. This sample shows how a thoughtfully designed document can carry its structure into PDF and HTML." + }, + { + "role": "/P", + "page": 2, + "text": "Fullbleed documentation: docs.fullbleed.dev" + }, + { + "role": "/Span", + "page": 2, + "text": "Fullbleed" + }, + { + "role": "/Span", + "page": 2, + "text": "documentation:" + }, + { + "role": "/Span", + "page": 2, + "text": "docs.fullbleed.dev" + } + ], + "reading_order": [ + { + "page": 1, + "mcid": 0, + "text": "Common Ground" + }, + { + "page": 1, + "mcid": 1, + "text": "COMMUNITY FIELD GUIDE / AUTUMN 2026" + }, + { + "page": 1, + "mcid": 2, + "text": "A place to grow. A place to belong." + }, + { + "page": 1, + "mcid": 3, + "text": "Good things happen when we make a little room for one another. Come for a quiet hour, a shared meal or something new to learn." + }, + { + "page": 1, + "mcid": 4, + "text": "" + }, + { + "page": 1, + "mcid": 5, + "text": "Find your starting point" + }, + { + "page": 1, + "mcid": 6, + "text": "Grow something" + }, + { + "page": 1, + "mcid": 7, + "text": "A few seeds. A pair of hands. A garden we tend together." + }, + { + "page": 1, + "mcid": 8, + "text": "Share a table" + }, + { + "page": 1, + "mcid": 9, + "text": "Pull up a chair for conversation and a warm bowl of soup." + }, + { + "page": 1, + "mcid": 10, + "text": "Make it last" + }, + { + "page": 1, + "mcid": 11, + "text": "Swap a skill, mend a favorite thing or try a small project." + }, + { + "page": 1, + "mcid": 12, + "text": "Every visit starts with hello" + }, + { + "page": 1, + "mcid": 13, + "text": "All sessions are free. No membership, equipment or experience is needed. Come on your own or bring someone with you." + }, + { + "page": 2, + "mcid": 0, + "text": "YOUR NEXT SATURDAY" + }, + { + "page": 2, + "mcid": 1, + "text": "Make room for a good day." + }, + { + "page": 2, + "mcid": 2, + "text": "Stay for one session or linger a little longer. You decide how to take part." + }, + { + "page": 2, + "mcid": 3, + "text": "Autumn 2026 \u00b7 Weekly sessions at the Common Ground garden" + }, + { + "page": 2, + "mcid": 4, + "text": "Program" + }, + { + "page": 2, + "mcid": 5, + "text": "When" + }, + { + "page": 2, + "mcid": 6, + "text": "What to expect" + }, + { + "page": 2, + "mcid": 7, + "text": "Garden" + }, + { + "page": 2, + "mcid": 8, + "text": "together" + }, + { + "page": 2, + "mcid": 9, + "text": "Saturdays, 10\u201311:30 am" + }, + { + "page": 2, + "mcid": 10, + "text": "Plant, water or harvest. Seated tasks are available." + }, + { + "page": 2, + "mcid": 11, + "text": "The" + }, + { + "page": 2, + "mcid": 12, + "text": "shared" + }, + { + "page": 2, + "mcid": 13, + "text": "table" + }, + { + "page": 2, + "mcid": 14, + "text": "Saturdays, noon\u20131 pm" + }, + { + "page": 2, + "mcid": 15, + "text": "Bring your lunch or share the seasonal soup. Ingredients are listed." + }, + { + "page": 2, + "mcid": 16, + "text": "Make" + }, + { + "page": 2, + "mcid": 17, + "text": "&" + }, + { + "page": 2, + "mcid": 18, + "text": "mend" + }, + { + "page": 2, + "mcid": 19, + "text": "First Saturday, 2\u20133:30 pm" + }, + { + "page": 2, + "mcid": 20, + "text": "Bring a small repair or try a simple craft. Materials are provided." + }, + { + "page": 2, + "mcid": 21, + "text": "Plan a comfortable visit" + }, + { + "page": 2, + "mcid": 22, + "text": "An easy arrival" + }, + { + "page": 2, + "mcid": 23, + "text": "" + }, + { + "page": 2, + "mcid": 24, + "text": "Enter through the wide gate beside the greenhouse." + }, + { + "page": 2, + "mcid": 25, + "text": "" + }, + { + "page": 2, + "mcid": 26, + "text": "The main path is firm and level. Seating is available along the way." + }, + { + "page": 2, + "mcid": 27, + "text": "" + }, + { + "page": 2, + "mcid": 28, + "text": "A welcome volunteer can show you the garden and the accessible restroom." + }, + { + "page": 2, + "mcid": 29, + "text": "Choose your pace" + }, + { + "page": 2, + "mcid": 30, + "text": "" + }, + { + "page": 2, + "mcid": 31, + "text": "Take part seated or standing, or simply watch." + }, + { + "page": 2, + "mcid": 32, + "text": "" + }, + { + "page": 2, + "mcid": 33, + "text": "Use the quiet corner whenever you would like a break." + }, + { + "page": 2, + "mcid": 34, + "text": "" + }, + { + "page": 2, + "mcid": 35, + "text": "Ask for written instructions or a demonstration. Both are welcome." + }, + { + "page": 2, + "mcid": 36, + "text": "A small first step is enough" + }, + { + "page": 2, + "mcid": 37, + "text": "You do not have to know anyone, know what to say or stay all day. There is a place for you here." + }, + { + "page": 2, + "mcid": 38, + "text": "About this guide" + }, + { + "page": 2, + "mcid": 39, + "text": "Common Ground and its programs are fictional. This sample shows how a thoughtfully designed document can carry its structure into PDF and HTML." + }, + { + "page": 2, + "mcid": 40, + "text": "Fullbleed" + }, + { + "page": 2, + "mcid": 41, + "text": "documentation:" + }, + { + "page": 2, + "mcid": 42, + "text": "docs.fullbleed.dev" + } + ], + "decorative_text_omitted": true, + "marked_content_nesting_valid": true, + "pdf_link_annotations": 0, + "scope": "Actual PDF structure, MCID text and semantic order; not an assistive-technology user test.", + "pdf_sha256": "e59754b06ebd3229b35033c74b7c08175543396b48d9c152cc3419c7e58ba17d" +} diff --git a/docs/assets/accessible-showcase-alpha6/validation.json b/docs/assets/accessible-showcase-alpha6/validation.json new file mode 100644 index 0000000..e3f1b1a --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/validation.json @@ -0,0 +1,36 @@ +{ + "ok": true, + "pdf_sha256": "e59754b06ebd3229b35033c74b7c08175543396b48d9c152cc3419c7e58ba17d", + "structure_passed": true, + "external_validation": { + "profile": "PDF/UA-1 validation profile", + "compliant": true, + "stderr_bytes": 0, + "details": { + "passedRules": 106, + "failedRules": 0, + "passedChecks": 5512, + "failedChecks": 0, + "ruleSummaries": [] + }, + "build": { + "releaseDetails": [ + { + "id": "core", + "version": "1.30.3", + "buildDate": 1791385320000 + }, + { + "id": "validation-model", + "version": "1.30.3", + "buildDate": 1791398640000 + }, + { + "id": "apps", + "version": "1.30.3", + "buildDate": 1791399120000 + } + ] + } + } +} diff --git a/docs/assets/accessible-showcase-alpha6/verification-guard.json b/docs/assets/accessible-showcase-alpha6/verification-guard.json new file mode 100644 index 0000000..a1a06e3 --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/verification-guard.json @@ -0,0 +1,9 @@ +{ + "ok": true, + "source_files": 14, + "pdf_sha256": "e59754b06ebd3229b35033c74b7c08175543396b48d9c152cc3419c7e58ba17d", + "matches_externally_validated_pdf": true, + "external_validator_rerun": false, + "marked_content_nesting_valid": true, + "negative_control_rejection": "Nested MCID on page 2: 7 -> 8" +} diff --git a/docs/assets/accessible-showcase-alpha6/visual-review.json b/docs/assets/accessible-showcase-alpha6/visual-review.json new file mode 100644 index 0000000..1fcb901 --- /dev/null +++ b/docs/assets/accessible-showcase-alpha6/visual-review.json @@ -0,0 +1,39 @@ +{ + "ok": true, + "method": "Visual inspection of every candidate page in independent PDFium rasterization at 144 DPI", + "reviewed": [ + { + "path": "independent-showcases/alpha6-invoice-components-1.png", + "sha256": "ce99eff5dfe2056ee59b065b32c692ff4d14f6c6f4294141659d1521c970b636" + }, + { + "path": "independent-showcases/alpha6-report-components-1.png", + "sha256": "44c1e3d96faa047eca1334d4efe65cae921c9791f00a534bb10614508387dc20" + }, + { + "path": "independent-showcases/alpha6-report-components-2.png", + "sha256": "640ca0ab644c131043d382e0ffebc31da310bbc1fad2d73e80644098742d15f4" + }, + { + "path": "independent-showcases/alpha6-report-components-3.png", + "sha256": "8093b7d33ab023e6c86da289e6ae98c2aa07b875fc6301fdc0374eef5c4316fb" + }, + { + "path": "independent-showcases/alpha6-accessible-1.png", + "sha256": "37dd5d5d49bd28e1d4099e2f55def5799b4d9ac534731f83825730f43d575b9e" + }, + { + "path": "independent-showcases/alpha6-accessible-2.png", + "sha256": "79d597f2f3c323aed9d379275356dbf88d04bd9461ca3df69f9def2ed097cf78" + } + ], + "findings": [ + "Northstar invoice retains its complete single-page composition, line items, totals and footer.", + "All three Common Ground report pages retain the artwork, chart labels, allocation table and complete text.", + "Both guide pages retain readable headings, illustration, table, list columns and documentation link.", + "No clipped content, missing glyphs, unexpected blank pages or text collisions were observed." + ], + "paired_pixels_and_text_equal": true, + "guide_pdf_sha256": "e59754b06ebd3229b35033c74b7c08175543396b48d9c152cc3419c7e58ba17d", + "scope": "Visual fixture review and retained machine validation; no human assistive-technology evaluation or general PDF/UA claim." +} diff --git a/docs/assets/bootstrap-alpha6/catalog.html b/docs/assets/bootstrap-alpha6/catalog.html new file mode 100644 index 0000000..70742f3 --- /dev/null +++ b/docs/assets/bootstrap-alpha6/catalog.html @@ -0,0 +1,30 @@ +Static Bootstrap 5.0.0 catalog

Accordion

Delivery

Dispatch within two working days.

Returns

Unused items can be returned.

Alerts

Ready for dispatch

The production batch has passed review.

Badge

Approved Priority

Breadcrumb

Buttons

View order

Button Group

Card

Product note

The Studio Collection

Made for everyday use

Printed on recycled stock and packed with care.

Close Button

Collapse

The authored expanded state is visible.

Dropdowns

List Group

  • Design approved
  • Production complete
  • Dispatch scheduled
  1. First checkpoint
  2. Final checkpoint

Modal

Navs Tabs

The current record is complete.

Navbar

Offcanvas

Details

Additional production notes.

Pagination

Popovers

Progress

75% complete

Scrollspy

Production

The batch is complete.

Spinners

Awaiting update Pending

Toasts

Production deskToday
The archive copy was created.

Tooltips

Layout

Eight columns
Four columns

Forms

@
Details confirmed

Content

Considered design, carefully delivered.
ItemQuantity
Notebook24

Utilities

A styled utility panel
\ No newline at end of file diff --git a/docs/assets/bootstrap-alpha6/catalog.unstyled.html b/docs/assets/bootstrap-alpha6/catalog.unstyled.html new file mode 100644 index 0000000..134f1f7 --- /dev/null +++ b/docs/assets/bootstrap-alpha6/catalog.unstyled.html @@ -0,0 +1 @@ +Static Bootstrap 5.0.0 catalog

Accordion

Delivery

Dispatch within two working days.

Returns

Unused items can be returned.

Alerts

Ready for dispatch

The production batch has passed review.

Badge

Approved Priority

Breadcrumb

Buttons

View order

Button Group

Card

Product note

The Studio Collection

Made for everyday use

Printed on recycled stock and packed with care.

Close Button

Collapse

The authored expanded state is visible.

Dropdowns

List Group

  • Design approved
  • Production complete
  • Dispatch scheduled
  1. First checkpoint
  2. Final checkpoint

Modal

Navs Tabs

The current record is complete.

Navbar

Offcanvas

Details

Additional production notes.

Pagination

Popovers

Progress

75% complete

Scrollspy

Production

The batch is complete.

Spinners

Awaiting update Pending

Toasts

Production deskToday
The archive copy was created.

Tooltips

Layout

Eight columns
Four columns

Forms

@
Details confirmed

Content

Considered design, carefully delivered.
ItemQuantity
Notebook24

Utilities

A styled utility panel
\ No newline at end of file diff --git a/docs/assets/bootstrap-alpha6/forms-page-1.png b/docs/assets/bootstrap-alpha6/forms-page-1.png new file mode 100644 index 0000000..07d1131 Binary files /dev/null and b/docs/assets/bootstrap-alpha6/forms-page-1.png differ diff --git a/docs/assets/bootstrap-alpha6/forms.pdf b/docs/assets/bootstrap-alpha6/forms.pdf new file mode 100644 index 0000000..9f0f44f Binary files /dev/null and b/docs/assets/bootstrap-alpha6/forms.pdf differ diff --git a/docs/assets/bootstrap-alpha6/project.zip b/docs/assets/bootstrap-alpha6/project.zip new file mode 100644 index 0000000..cc6f7ed Binary files /dev/null and b/docs/assets/bootstrap-alpha6/project.zip differ diff --git a/docs/assets/bootstrap-alpha6/report-page-1.png b/docs/assets/bootstrap-alpha6/report-page-1.png new file mode 100644 index 0000000..e2656fd Binary files /dev/null and b/docs/assets/bootstrap-alpha6/report-page-1.png differ diff --git a/docs/assets/bootstrap-alpha6/report.pdf b/docs/assets/bootstrap-alpha6/report.pdf new file mode 100644 index 0000000..25ad1d7 Binary files /dev/null and b/docs/assets/bootstrap-alpha6/report.pdf differ diff --git a/docs/assets/bootstrap-alpha6/report.styled.html b/docs/assets/bootstrap-alpha6/report.styled.html new file mode 100644 index 0000000..a0229cc --- /dev/null +++ b/docs/assets/bootstrap-alpha6/report.styled.html @@ -0,0 +1,40 @@ +Northstar / April production report
NORTHSTAR / STUDIOAPRIL 2026

Made with care. +Delivered with purpose.

A production report for people who care about the details.

408

Items delivered

$8,640

Collection revenue

98%

On-time dispatch

The April collection

Completed orders / April 1-30, 2026
CollectionUnitsRevenue
Studio notebooks240$3,600.00
Cotton stationery sets120$2,160.00
Archival print collection48$2,880.00

A lighter footprint

Every shipment used recycled packaging. The next collection enters production on May 6.

On the horizon

We are three quarters of the way to our spring production target.

75%
All figures are final. Prepared for the Northstar operations team.
\ No newline at end of file diff --git a/docs/assets/bootstrap-alpha6/report.unstyled.html b/docs/assets/bootstrap-alpha6/report.unstyled.html new file mode 100644 index 0000000..be4c49a --- /dev/null +++ b/docs/assets/bootstrap-alpha6/report.unstyled.html @@ -0,0 +1,2 @@ +Northstar / April production report
NORTHSTAR / STUDIOAPRIL 2026

Made with care. +Delivered with purpose.

A production report for people who care about the details.

408

Items delivered

$8,640

Collection revenue

98%

On-time dispatch

The April collection

Completed orders / April 1-30, 2026
CollectionUnitsRevenue
Studio notebooks240$3,600.00
Cotton stationery sets120$2,160.00
Archival print collection48$2,880.00

A lighter footprint

Every shipment used recycled packaging. The next collection enters production on May 6.

On the horizon

We are three quarters of the way to our spring production target.

75%
All figures are final. Prepared for the Northstar operations team.
\ No newline at end of file diff --git a/docs/assets/bootstrap-alpha6/source.json b/docs/assets/bootstrap-alpha6/source.json new file mode 100644 index 0000000..f96eaa5 --- /dev/null +++ b/docs/assets/bootstrap-alpha6/source.json @@ -0,0 +1,200 @@ +{ + "schema": "fullbleed.bootstrap-alpha6-source.v1", + "experimental": true, + "source_commit": "9da6a672e81fabebb5733aadcdd932355188bac5", + "contains_engine_binaries": false, + "engine_installation": "pypi", + "native_build_required": false, + "native_build_scope": "Compatible wheels need no Rust build; other platforms may build the PyPI sdist with Rust and native build tools.", + "source_engine_requirement": "fullbleed==2.6.0a6", + "engine_wheels": { + "package_version": "2.6.0a6", + "release_url": "https://github.com/fullbleed-engine/fullbleed-official/releases/tag/v2.6.0a6", + "index_url": "https://pypi.org/project/fullbleed/2.6.0a6/", + "files": [ + { + "file": "fullbleed-2.6.0a6-cp310-abi3-macosx_10_12_x86_64.whl", + "url": "https://files.pythonhosted.org/packages/d2/c6/3286f3baa5777ca5515b7ca6acaa840cca82775c0c09a406dd089512232c/fullbleed-2.6.0a6-cp310-abi3-macosx_10_12_x86_64.whl", + "bytes": 7648571, + "sha256": "28dc8ee1f634561dc8b170a4e83213dfa1668f0e2b715b1a31f9223bd9f9fd51", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-macosx_11_0_arm64.whl", + "url": "https://files.pythonhosted.org/packages/5f/f8/c27385503abce24d01eaee71a3c2888cd49eef91d679d3845431d126b83d/fullbleed-2.6.0a6-cp310-abi3-macosx_11_0_arm64.whl", + "bytes": 7247075, + "sha256": "47537d51432de97e51cf20ea99f908f4117966b21f8c2562b3a222451b874a49", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "url": "https://files.pythonhosted.org/packages/38/96/73f26c37a5ee80a1867d56656f26f8d5ea79f90f9ad49377a20c6723cff6/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "bytes": 7391630, + "sha256": "c9a7b6725d610847774937e3df6b27cb6933a299066a74ee8d2b5e065e959375", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", + "url": "https://files.pythonhosted.org/packages/0b/b5/65e81e3b982f4b7622cd23a93135a411673858cadcc91bfe25602a3d5b94/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", + "bytes": 9203264, + "sha256": "4c2f6344834caf5fb823ded48e13361d27f1b69d89526a66d8069fe8905f96a4", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", + "url": "https://files.pythonhosted.org/packages/77/5e/f18111ff6846608dd6317ffb43ee576597dfb03d635c9f097ba09cf50265/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", + "bytes": 9932252, + "sha256": "2f2796a905a17860c408c3d75370a867820ba2d6e801f76126b2aecb78530dd3", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "url": "https://files.pythonhosted.org/packages/dd/55/20f7adec6d0298ee238660c7ab15ac22ea23faaf28f889cc572f84d7b2c3/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "bytes": 8152134, + "sha256": "3ae86cd57d5899387d69540e3b74602d7a410f4d8af3e22a4ed8c18aaa170b8b", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "url": "https://files.pythonhosted.org/packages/02/15/151d678523a33790b0b309f914d03b474a2c772c77bd86a8ccf61696771e/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "bytes": 8218561, + "sha256": "335eaa63f6b9f688f2ab0d2ef0c96e746616cd279227b92c25ce5d2809a03694", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "url": "https://files.pythonhosted.org/packages/2f/34/cf185658535470f7f0361326de2add1dc30b1afd6fcb3b5fae5541f889b4/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "bytes": 7807566, + "sha256": "c081ea0f98f1fa1637d49c20de70a4876ccddffa7d376c4c58b6f0819a7c2817", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_aarch64.whl", + "url": "https://files.pythonhosted.org/packages/3c/0a/2802eacf894b188307cedc6b94eda7f6f3bf24524ae40c4d7f9a3a58f73b/fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_aarch64.whl", + "bytes": 7368110, + "sha256": "7c87201e0bfa95ce193893cb5d05852916883aeb7f304704a35bb7a3e37cb987", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_armv7l.whl", + "url": "https://files.pythonhosted.org/packages/64/b3/2044f1829b17349a49945695073a939b32a9eff0ce875876445507e73d23/fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_armv7l.whl", + "bytes": 9250127, + "sha256": "4bab4c563e8ac3aca6f6277dafecc70610f8a668a08f3b1d76a808f55e78a3b1", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_i686.whl", + "url": "https://files.pythonhosted.org/packages/63/c9/2fe1e0ba2b7268c74cbdd7ec084ab240ef83fd7f9d33c3863cc661cd8aa7/fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_i686.whl", + "bytes": 9901728, + "sha256": "da351fcac9916ed342033313d8dc229f0a3f33955d64dab9ce9ebdb948c3a23a", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_x86_64.whl", + "url": "https://files.pythonhosted.org/packages/fc/cb/43072820d6633a948ba85be48532015e50dc2d2b6c641deeb190fc486c44/fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_x86_64.whl", + "bytes": 7802074, + "sha256": "d31f8c82a7717daacb17d2e79fd33ab9a27a59e713b0be58a82ad6f21f43ef8c", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-win32.whl", + "url": "https://files.pythonhosted.org/packages/d4/6e/d56eb7b6cb3ce5422cc68bc6be572543e7a438233578e800a98e736aa53e/fullbleed-2.6.0a6-cp310-abi3-win32.whl", + "bytes": 9137566, + "sha256": "d675e696c1f2a848e4178e9d26b6c47212fd4a37107ec75fed659f20365f719a", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-win_amd64.whl", + "url": "https://files.pythonhosted.org/packages/1b/01/8451c57827252dd082c4c12fc9fcb28f44dd0419a8afda182784880abdcf/fullbleed-2.6.0a6-cp310-abi3-win_amd64.whl", + "bytes": 7971040, + "sha256": "d163fc15bdb1c3a950a94b7b72f1d7044263d29cc9804f2fce87f2a482164293", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-win_arm64.whl", + "url": "https://files.pythonhosted.org/packages/83/5d/33c5bbd45675c14e9075a14183329ca62f1440f5dc4b7c70eac137389a19/fullbleed-2.6.0a6-cp310-abi3-win_arm64.whl", + "bytes": 7490997, + "sha256": "541ca7f4f8148a72bd132f6c96ff79894e60aab409c96d44c96d03950e64dde4", + "kind": "wheel" + } + ] + }, + "engine_python_files": { + "fullbleed/ui/experimental/__init__.py": "d375915c58573c6769543dc33e2ec87084879412e63ea1bbcf8421458e0085fa", + "fullbleed/ui/experimental/_bridge.py": "d9d63910ad4e19bf63a3bd7c51507ebcc8aab14f86424259052be2f61ef6bf42", + "fullbleed/ui/experimental/_bridge.pyi": "372ce75241bfdadf41a7b38c4f301a35f7968c454f4d8c48fb565845f27754f9", + "fullbleed/ui/experimental/_document.py": "8201b0be994280baefe726100bf11c822ba2c57277050602059c9ca723889cea", + "fullbleed/ui/experimental/_document.pyi": "1112209e898c9bdbb16edb922a4df6c253fcbe3ded88e328b57bd4f26079d022", + "fullbleed/ui/experimental/_resources.py": "89561bebca0065b319f51133d6e9f5a747df32d7183c282aa036e11b751a395d", + "fullbleed/ui/experimental/_schema.py": "bf6463e42f5e8111e20975e949c7997a9da72b0a9996455df5d69d38d4543ca7", + "fullbleed/ui/experimental/_semantics.py": "dad8b9e7293eac421deb9ba9faf2bee24ccfdd69b5043413d053cb00b85883ab", + "fullbleed/ui/experimental/_style.py": "eadd516613985245691b0798361b7df6122c6906443ccefae77e154e3681e226", + "fullbleed/ui/experimental/_style.pyi": "5db65d4a5ff3ebba0cead751cb0bb1b5143d02dfa1981b3a82be3d8f4a4a03c6", + "fullbleed/ui/experimental/_table.py": "2dc2ab734d4f6a0bad6b9d90be29d2eacde50e9bd3ca5847f18392d18474eed4", + "fullbleed/ui/experimental/_table.pyi": "4304062d301270c8e80c454dec76704d84c7a9d5340be656ce035bf86d3d2898", + "fullbleed/ui/experimental/_tree.py": "035158f8115ae8f4da2d864a0a666128cf809b12f65fb4432f6cfd020df451e4", + "fullbleed/ui/experimental/_tree.pyi": "b4474fb9d51ee735c8a749765496d524a50e9dae405a1e5a2b239dfaa532e2d9", + "fullbleed/ui/experimental/bootstrap/__init__.py": "3d38275bdce34ed31cbb959dd8a7f8ce309b77dc3c482fff454718dd404f94d4", + "fullbleed/ui/experimental/bootstrap/__init__.pyi": "a2f96b9d41df8073231c914cef31f497c24974757ab625ae2cdb9a5cd43f7ee9", + "fullbleed/ui/experimental/bootstrap/_components.py": "5f402fe3b810bc5443cea9dd67cde5668923b3c98c20eb241141fdd33efefc7a", + "fullbleed/ui/experimental/bootstrap/_core.py": "d82ec29deec6d8a248b9f9a419bbcb915862393ba1de08d9f28d814249716c99", + "fullbleed/ui/experimental/bootstrap/_forms.py": "bd368525794d02f6ee4ececb1e42b0e16fe104da0579d4966ace0b6f9462963b", + "fullbleed/ui/experimental/bootstrap/_layout.py": "41ce430739224135c028c984a0e578fbbd0726c7dab059c685b0db844af2fe86", + "fullbleed/ui/experimental/bootstrap/_schema.py": "45ca43bcadc62e6b13338868e520f0ac91ce617aae090db9fb8fd2c5329d263e", + "fullbleed/ui/experimental/bootstrap/_utilities.py": "b7b5606b6113acf5de27d72a7ebf46e762a61132a19e954207708a300b47815a", + "fullbleed/ui/experimental/py.typed": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "adapted_example_files": { + "examples/component_bootstrap/catalog.py": { + "upstream_sha256": "2cfca9d51a892ac596b87f29bb44c8f84cf364d5933fb8b2b0e67f03bd781b36", + "sha256": "287faf4e607e0e3daba21b952e26dc0acc7033cfe0d07dd0639b0b43a4db7fd3", + "change": "Demo links target other existing catalog sections. The downloadable HTML has no unresolved placeholder anchors." + }, + "examples/component_bootstrap/main.py": { + "upstream_sha256": "1fbbb445e23a2096785b74aa663a5fe29a3da469c789911579d5d01e5dabd1aa", + "sha256": "bcd0eec912b0cd2ecff65d99c04afc907b2fd2ea95f11cabbd43ba0c4d051649", + "change": "Write textual artifacts with explicit LF line endings so downloadable HTML has identical bytes on Windows and Linux." + } + }, + "bootstrap_css_sha256": "04083fcecdd9e19b2acd1e0073437aa07c98d230f0198d7f1fd470ab220fef64", + "first_document_sha256": "33f8bcb22893ec7df5fe6562f0131790dd7006e71afa45abe98d120c20bf2024", + "files": [ + { + "path": "README.md", + "bytes": 6096, + "sha256": "5a291b3be52db88a4769576b037185dcfd12c6c7faa2f8ba8cc6d11e1c6cef07" + }, + { + "path": "LICENSE", + "bytes": 1079, + "sha256": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9" + }, + { + "path": "requirements.txt", + "bytes": 19, + "sha256": "058f91f852902be181df6d21cf5e5484cecbc11b14f920337d28d86950cc3d17" + }, + { + "path": "first_document.py", + "bytes": 1387, + "sha256": "33f8bcb22893ec7df5fe6562f0131790dd7006e71afa45abe98d120c20bf2024" + }, + { + "path": "upstream.json", + "bytes": 10963, + "sha256": "83072771517fe311b8e4e0e036e7e018268bd9a337536096aebdc9fde6a70b00" + }, + { + "path": "examples/component_bootstrap/main.py", + "bytes": 5875, + "sha256": "bcd0eec912b0cd2ecff65d99c04afc907b2fd2ea95f11cabbd43ba0c4d051649" + }, + { + "path": "examples/component_bootstrap/catalog.py", + "bytes": 5311, + "sha256": "287faf4e607e0e3daba21b952e26dc0acc7033cfe0d07dd0639b0b43a4db7fd3" + } + ], + "zip_sha256": "eeedc7bcfa711526c1e5d38fe340d27be27c5707711eeef5227fba3891bf3716", + "zip_bytes": 13074 +} diff --git a/docs/assets/bootstrap-alpha6/verification.json b/docs/assets/bootstrap-alpha6/verification.json new file mode 100644 index 0000000..a2875c0 --- /dev/null +++ b/docs/assets/bootstrap-alpha6/verification.json @@ -0,0 +1,78 @@ +{ + "schema": "fullbleed.bootstrap-alpha6-output.v1", + "experimental": true, + "source_commit": "9da6a672e81fabebb5733aadcdd932355188bac5", + "package_version": "2.6.0a6", + "publication_verified": true, + "core_ci": "https://github.com/fullbleed-engine/fullbleed-official/actions/runs/39133047267", + "core_ci_conclusion": "success", + "bootstrap": "5.0.0", + "bootstrap_css_sha256": "04083fcecdd9e19b2acd1e0073437aa07c98d230f0198d7f1fd470ab220fef64", + "preview_rendering": "PDFium 5.14.0 from each saved PDF at 108 DPI", + "files": [ + { + "file": "report.pdf", + "bytes": 78105, + "sha256": "8412ca9864f948695b2614cde22a722533a0dae6ce1e125d64ed84c915e75d15", + "pages": 1 + }, + { + "file": "forms.pdf", + "bytes": 11119, + "sha256": "3a624260f79afa97dc83e7071e4f7a31e7365ad626d095393969e37920107d65", + "pages": 1 + }, + { + "file": "report.styled.html", + "bytes": 162706, + "sha256": "62625bbcd9621690c75b6374554b18b664fb462d8a04fa61c4711f6e819bd1bb" + }, + { + "file": "report.unstyled.html", + "bytes": 3146, + "sha256": "633e2b318c1ed9263001b5b75e9f7d3d87dd90954714e4d4cabe9d48b5c66c68" + }, + { + "file": "catalog.html", + "bytes": 171383, + "sha256": "24cfb11194bf7bca1a71945e0565803017df3cc9bed6ab45062ff99a5c55e2e5" + }, + { + "file": "catalog.unstyled.html", + "bytes": 13601, + "sha256": "0aa42bbedb52e8c54b1a76af2b23866d4a1b70778e11166dab5508716beb13b7" + }, + { + "file": "report-page-1.png", + "bytes": 114451, + "sha256": "c1cb3b7b5251e5990d53768bd69b6dff5f01ff330fda4a0054d093fc7ed0bdd9" + }, + { + "file": "forms-page-1.png", + "bytes": 30230, + "sha256": "2e0c791a0cc88b5e09a616fc5012ed5dd94cb1578f836fc1e068b749b4bf6dd7" + } + ], + "preserved_previous_downloads": true, + "scope": "Verified public PyPI alpha 6 and the exact downloadable source ZIP: 29 one-page PDFs, printed and hidden states, three executable guide blocks, installed authoring-file hashes, and matching downloadable PDF/styled/unstyled HTML bytes. Native and independent previews were visually reviewed. These checks cover the provided examples, not exhaustive Bootstrap/CSS combinations, general accessibility or independent usability.", + "source_zip_sha256": "eeedc7bcfa711526c1e5d38fe340d27be27c5707711eeef5227fba3891bf3716", + "engine_wheel": { + "file": "fullbleed-2.6.0a6-cp310-abi3-win_amd64.whl", + "url": "https://files.pythonhosted.org/packages/1b/01/8451c57827252dd082c4c12fc9fcb28f44dd0419a8afda182784880abdcf/fullbleed-2.6.0a6-cp310-abi3-win_amd64.whl", + "bytes": 7971040, + "sha256": "d163fc15bdb1c3a950a94b7b72f1d7044263d29cc9804f2fce87f2a482164293", + "kind": "wheel" + }, + "release_conclusion": "success", + "build_run": "https://github.com/fullbleed-engine/fullbleed-official/actions/runs/39136534983", + "stable_default_version_after_publication": "2.5.32", + "starter_check": { + "ok": true, + "pdfs": 29, + "guide_blocks": 3, + "version": "2.6.0a6", + "source_zip_sha256": "eeedc7bcfa711526c1e5d38fe340d27be27c5707711eeef5227fba3891bf3716", + "installed_wheel_sha256": "d163fc15bdb1c3a950a94b7b72f1d7044263d29cc9804f2fce87f2a482164293" + }, + "changed_output_review": "The complete report and forms PDFs and independently rendered page pixels are identical to alpha 5. Both complete documents were visually reviewed; bootstrap-output-review.json retains that comparison." +} diff --git a/docs/assets/component-notebook-alpha6/bootstrap_components.ipynb b/docs/assets/component-notebook-alpha6/bootstrap_components.ipynb new file mode 100644 index 0000000..f899cd5 --- /dev/null +++ b/docs/assets/component-notebook-alpha6/bootstrap_components.ipynb @@ -0,0 +1,330 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "welcome", + "metadata": {}, + "source": [ + "# Make a PDF from Python components\n", + "\n", + "Edit an order, customize its cards and table, and export a PDF plus styled\n", + "and unstyled HTML. You write Python values and components; no HTML strings\n", + "are needed. All sample data is fictional.\n", + "\n", + "**In Colab:** choose **Runtime → Run all**, sign in if prompted, and use a\n", + "standard CPU runtime. No GPU or paid plan is needed for this example;\n", + "[Colab availability and limits](https://research.google.com/colaboratory/faq.html)\n", + "still apply. Runtime files are temporary, so download your results before\n", + "leaving. Local Jupyter also works.\n", + "\n", + "**Experimental:** this notebook pins Fullbleed **2.6.0a6**. The component API\n", + "may change and has documented CSS limits. Bootstrap components are static\n", + "in both PDF and HTML; no Bootstrap JavaScript is included." + ] + }, + { + "cell_type": "markdown", + "id": "install-help", + "metadata": {}, + "source": [ + "## 1. Install the tested alpha\n", + "\n", + "Run this before importing Fullbleed. If you already imported a different\n", + "version in this notebook session, restart its kernel/runtime first.\n", + "The wheel supplies the engine and font; Jupyter supplies display tools." + ] + }, + { + "cell_type": "code", + "id": "install-fullbleed", + "metadata": { + "tags": [ + "install-fullbleed" + ] + }, + "source": [ + "%pip install --quiet --only-binary=:all: \"fullbleed==2.6.0a6\"" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "data-help", + "metadata": {}, + "source": [ + "## 2. Make the order yours\n", + "\n", + "Change a name, quantity or price, then rerun this cell and the cells below it.\n", + "Prices are decimal strings; this example omits tax, shipping and discounts.\n", + "Try `items=[]` to see the empty state, or add rows to explore pagination." + ] + }, + { + "cell_type": "code", + "id": "order-data", + "metadata": {}, + "source": [ + "order = {\n", + " \"number\": \"1048\",\n", + " \"customer\": \"Moss & Fern Studio\",\n", + " \"collection\": \"The autumn collection\",\n", + " \"status\": \"Ready to ship\",\n", + " \"items\": [\n", + " {\"item\": \"Studio notebooks\", \"quantity\": 24, \"unit_price\": \"15.00\"},\n", + " {\"item\": \"Cotton stationery sets\", \"quantity\": 12, \"unit_price\": \"18.00\"},\n", + " {\"item\": \"Archival prints\", \"quantity\": 8, \"unit_price\": \"60.00\"},\n", + " ],\n", + "}" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "styles-help", + "metadata": {}, + "source": [ + "## 3. Choose the accent and type size\n", + "\n", + "These values feed ordinary Python styles. Later, `.with_parts(...)` styles\n", + "a card's own title and body. The custom `dispatch-note` class gets its rule\n", + "from `Document(styles=...)`; a class name alone does not create a style." + ] + }, + { + "cell_type": "code", + "id": "style-controls", + "metadata": {}, + "source": [ + "accent = \"#205b47\"\n", + "title_size = 30" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "components-help", + "metadata": {}, + "source": [ + "## 4. Compose reusable components\n", + "\n", + "`Panel` supplies reusable card defaults. `Metric` is an ordinary Python\n", + "function. `DataTable` builds its structure from rows and column functions.\n", + "Bootstrap classes such as `mb-4` come with the components; your custom\n", + "classes need their own rules. Text values are escaped automatically.\n", + "\n", + "A column callback can return a component: `Strong(row[\"item\"])` emphasizes\n", + "each item inside its table cell. Return text or compose other components\n", + "without constructing an HTML string.\n" + ] + }, + { + "cell_type": "code", + "id": "components", + "metadata": {}, + "source": [ + "from decimal import Decimal\n", + "from fullbleed.ui.experimental import Document, Page, Strong, Theme, mm, pt, style, styled\n", + "from fullbleed.ui.experimental import bootstrap as bs\n", + "\n", + "def money(value):\n", + " return f\"USD {value:,.2f}\"\n", + "\n", + "Panel = styled(bs.Card, border_color=\"#d7e1de\", border_radius=\"8px\")\n", + "\n", + "def Metric(value, label):\n", + " return Panel(\n", + " bs.Heading(value, level=2, class_name=\"mb-1\", style=style(color=accent)),\n", + " bs.Text(label, class_name=\"text-secondary\"),\n", + " )\n", + "\n", + "def OrderSummary(data):\n", + " rows = [dict(row, amount=Decimal(row[\"unit_price\"]) * row[\"quantity\"])\n", + " for row in data[\"items\"]]\n", + " total = sum((row[\"amount\"] for row in rows), Decimal(\"0.00\"))\n", + " units = sum(row[\"quantity\"] for row in rows)\n", + " details = bs.DataTable(\n", + " rows,\n", + " columns=[\n", + " bs.Column(\"Item\", lambda row: Strong(row[\"item\"])),\n", + " bs.Column(\"Quantity\", lambda row: row[\"quantity\"], align=\"right\"),\n", + " bs.Column(\"Amount\", lambda row: money(row[\"amount\"]), align=\"right\"),\n", + " ],\n", + " striped=True,\n", + " caption=\"Order contents · USD\",\n", + " ) if rows else bs.Alert(\"No items in this order.\", variant=\"light\")\n", + " return Document(\n", + " bs.Box(bs.Text(\"NORTHSTAR / STUDIO\", class_name=\"fw-bold\"),\n", + " bs.Badge(data[\"status\"], variant=\"light\", class_name=\"ms-3\"),\n", + " class_name=\"mb-4\"),\n", + " bs.Heading(data[\"collection\"], level=1,\n", + " style=style(font_size=pt(title_size), color=accent, line_height=1.1)),\n", + " bs.Paragraph(f'Prepared for {data[\"customer\"]}', class_name=\"lead mb-4\"),\n", + " bs.Row(\n", + " bs.Col(Metric(str(units), \"Items in this order\"), span=5),\n", + " bs.Col(Metric(money(total), \"Order total\"), span=7),\n", + " gutter=3, class_name=\"mb-4 mx-0\",\n", + " ),\n", + " Panel(details, title=f'Order {data[\"number\"]}',\n", + " footer=\"Made with care. Packed with purpose.\").with_parts(\n", + " title=style(color=accent, font_size=pt(16)),\n", + " body=style(padding=\"1.25rem\"),\n", + " ),\n", + " bs.Alert(\"Please retain this copy with your shipment.\",\n", + " class_name=\"dispatch-note mt-4\"),\n", + " title=f'Order {data[\"number\"]}',\n", + " page=Page(size=\"A4\", margin=mm(16)),\n", + " theme=Theme(text=style(font_family=\"Inter, Arial, sans-serif\", font_size=pt(10),\n", + " color=\"#18382f\", line_height=1.4)),\n", + " styles={\".dispatch-note\": style(color=accent, border_color=accent,\n", + " background_color=\"#f0f5f2\")},\n", + " )\n", + "\n", + "document = OrderSummary(order)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "render-help", + "metadata": {}, + "source": [ + "## 5. Render and export\n", + "\n", + "The PDF renderer receives the bundled Inter font file explicitly. A CSS\n", + "family name does not load a font. Styled HTML keeps the family declaration;\n", + "its browser host supplies Inter separately, or uses the Arial/sans-serif fallback.\n", + "\n", + "`include_styles=False` removes generated and inline styling but keeps classes\n", + "for a host application's CSS. Both export modes also work on an individual\n", + "component, such as `Metric(\"44\", \"Items\").to_html(include_styles=False)`.\n", + "Styled Bootstrap fragments include global Bootstrap rules; use unstyled\n", + "fragments when the host page already provides its stylesheet." + ] + }, + { + "cell_type": "code", + "id": "render-export", + "metadata": {}, + "source": [ + "from importlib.resources import files\n", + "from pathlib import Path\n", + "from fullbleed.ui.experimental import Renderer\n", + "\n", + "output = Path(\"output\")\n", + "output.mkdir(exist_ok=True)\n", + "font = files(\"fullbleed_assets\").joinpath(\"fonts/Inter-Variable.ttf\")\n", + "renderer = Renderer(font_files=[str(font)],\n", + " document_timestamp=\"2026-10-01T00:00:00Z\")\n", + "prepared = document.prepare(renderer)\n", + "pdf_path = output / \"order.pdf\"\n", + "pdf_path.write_bytes(prepared.render_pdf())\n", + "(output / \"order.styled.html\").write_text(\n", + " document.to_html(include_styles=True), encoding=\"utf-8\", newline=\"\\n\")\n", + "(output / \"order.unstyled.html\").write_text(\n", + " document.to_html(include_styles=False), encoding=\"utf-8\", newline=\"\\n\")\n", + "print(f\"Saved {pdf_path} and both HTML exports.\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "id": "preview", + "metadata": {}, + "source": [ + "from IPython.display import Image, display\n", + "\n", + "preview_paths = []\n", + "for number, png in enumerate(prepared.render_image_pages(dpi=110), start=1):\n", + " path = output / f\"order-page-{number}.png\"\n", + " path.write_bytes(png)\n", + " preview_paths.append(path)\n", + " display(Image(data=png, width=650))\n", + "print(f\"{len(preview_paths)} page(s). Review every page before using the document.\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "download-help", + "metadata": {}, + "source": [ + "## 6. Download your files\n", + "\n", + "This ZIP contains the current PDF, both HTML exports, and every current page\n", + "preview. In Colab the next cell starts the download. In local Jupyter it\n", + "displays a link. The runtime's `output/` folder also contains the individual\n", + "files. If you edit the order or styles, rerun from that edit through this cell." + ] + }, + { + "cell_type": "code", + "id": "download", + "metadata": {}, + "source": [ + "import sys\n", + "from zipfile import ZipFile, ZIP_DEFLATED\n", + "\n", + "bundle_path = output / \"component-order.zip\"\n", + "current_files = [pdf_path, output / \"order.styled.html\",\n", + " output / \"order.unstyled.html\", *preview_paths]\n", + "with ZipFile(bundle_path, \"w\", compression=ZIP_DEFLATED) as bundle:\n", + " for path in current_files:\n", + " bundle.write(path, arcname=path.name)\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import files\n", + " files.download(str(bundle_path))\n", + "else:\n", + " from IPython.display import FileLink\n", + " display(FileLink(str(bundle_path)))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "next-steps", + "metadata": {}, + "source": [ + "## Keep composing\n", + "\n", + "- [Component guide and catalog](https://docs.fullbleed.dev/guides/bootstrap-components-preview/)\n", + " explain class names, named parts, fonts, static states and current limits.\n", + "- [Northstar and Common Ground source kits](https://docs.fullbleed.dev/guides/showcase-authoring/)\n", + " show a complete invoice and illustrated report in components and HTML/CSS.\n", + "- [The authored accessible guide](https://docs.fullbleed.dev/guides/accessible-showcase/)\n", + " includes the exact PDF, semantic source and scoped validation receipts.\n", + "- [Share an authoring example](https://github.com/fullbleed-engine/fullbleed-official/discussions/86)\n", + " if a composition or styling task feels awkward. Include the small component,\n", + " expected result and Fullbleed version; use fictional data in public posts.\n", + "- [Stable Python guide](https://docs.fullbleed.dev/guides/python-components/)\n", + " documents the established API separately from this alpha.\n", + "\n", + "The notebook and Fullbleed are MIT licensed. This example does not establish\n", + "complete Bootstrap CSS, browser, accessibility or PDF conformance." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "colab": { + "name": "bootstrap_components.ipynb", + "provenance": [] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/assets/component-notebook-alpha6/order-page-1.png b/docs/assets/component-notebook-alpha6/order-page-1.png new file mode 100644 index 0000000..0303b90 Binary files /dev/null and b/docs/assets/component-notebook-alpha6/order-page-1.png differ diff --git a/docs/assets/component-notebook-alpha6/order.pdf b/docs/assets/component-notebook-alpha6/order.pdf new file mode 100644 index 0000000..2d2bb4b Binary files /dev/null and b/docs/assets/component-notebook-alpha6/order.pdf differ diff --git a/docs/assets/component-notebook-alpha6/order.styled.html b/docs/assets/component-notebook-alpha6/order.styled.html new file mode 100644 index 0000000..42fbeb4 --- /dev/null +++ b/docs/assets/component-notebook-alpha6/order.styled.html @@ -0,0 +1,39 @@ +Order 1048
NORTHSTAR / STUDIOReady to ship

The autumn collection

Prepared for Moss & Fern Studio

44

Items in this order

USD 1,056.00

Order total

Order 1048

Order contents · USD
ItemQuantityAmount
Studio notebooks24USD 360.00
Cotton stationery sets12USD 216.00
Archival prints8USD 480.00
Please retain this copy with your shipment.
\ No newline at end of file diff --git a/docs/assets/component-notebook-alpha6/order.unstyled.html b/docs/assets/component-notebook-alpha6/order.unstyled.html new file mode 100644 index 0000000..fe4de62 --- /dev/null +++ b/docs/assets/component-notebook-alpha6/order.unstyled.html @@ -0,0 +1 @@ +Order 1048
NORTHSTAR / STUDIOReady to ship

The autumn collection

Prepared for Moss & Fern Studio

44

Items in this order

USD 1,056.00

Order total

Order 1048

Order contents · USD
ItemQuantityAmount
Studio notebooks24USD 360.00
Cotton stationery sets12USD 216.00
Archival prints8USD 480.00
Please retain this copy with your shipment.
\ No newline at end of file diff --git a/docs/assets/component-notebook-alpha6/verification.json b/docs/assets/component-notebook-alpha6/verification.json new file mode 100644 index 0000000..c6173ca --- /dev/null +++ b/docs/assets/component-notebook-alpha6/verification.json @@ -0,0 +1,61 @@ +{ + "runtime_version": "2.6.0a6", + "notebook_sha256": "46dc83fb67764992a17b2382217ac52ccb054c2f30cfff38d36527dce8de9fe3", + "cases": [ + { + "name": "default", + "pages": 1, + "row_count": 3, + "total": "1056.00", + "pdf_sha256": "e273572ff4885ba8b139563cec45b590b385a2e7330d8728d2f71596e2fdbce6", + "accent": "#205b47", + "rich_cell_components_checked": true + }, + { + "name": "edited", + "pages": 1, + "row_count": 3, + "total": "1176.00", + "pdf_sha256": "5823731e0146ab74806acce6bbd0676af5b77bf04566827c9c50523004cdb4c4", + "accent": "#763c96", + "rich_cell_components_checked": true + }, + { + "name": "long", + "pages": 4, + "row_count": 72, + "total": "684.00", + "pdf_sha256": "c543c6cd5c6dadc7b4e77fd043a3fa5f2693dfbb0e20d9f844a5e6bb894a4735", + "accent": "#205b47", + "rich_cell_components_checked": true + }, + { + "name": "empty-after-long", + "pages": 1, + "row_count": 0, + "total": "0.00", + "pdf_sha256": "5994d1a6f3a7176c8f4f614221cd60d27028a1fd8d80a372f4510eeeb056b573", + "accent": "#205b47", + "rich_cell_components_checked": true + } + ], + "downloads": { + "bootstrap_components.ipynb": "46dc83fb67764992a17b2382217ac52ccb054c2f30cfff38d36527dce8de9fe3", + "order.pdf": "e273572ff4885ba8b139563cec45b590b385a2e7330d8728d2f71596e2fdbce6", + "order-page-1.png": "d84d30060d7593e28dbc9ae16cb0f7c4ef1a013f83ed82cff9df8e9519eae1fa", + "order.styled.html": "c53d95a831b1d2fda92da28e6dcc5bf6063a7b1090cf1053930c39ff7b54103f", + "order.unstyled.html": "3e072e3dd684f9e7c483859a41051619effa814225a64a1a0e500e4d6be936e6" + }, + "preview": "PDFium rendering of the default output PDF at 110 DPI", + "checks": [ + "Notebook installation and execution", + "Independent PDF text and page bounds", + "Requested PDF accent colours", + "Repeat/direct PDF byte equality", + "Static styled and unstyled HTML", + "Rich item cell emphasis, order and escaped text in both HTML exports", + "Exact ZIP contents", + "Long-to-empty rerun excludes old pages" + ], + "scope": "Executed Jupyter cells and independent PDF/HTML/ZIP checks. Hosted Colab runtime and download UI, exhaustive CSS/browser parity and standards conformance are not evaluated." +} diff --git a/docs/assets/showcase-authoring-alpha6/comparison.json b/docs/assets/showcase-authoring-alpha6/comparison.json new file mode 100644 index 0000000..ee26e15 --- /dev/null +++ b/docs/assets/showcase-authoring-alpha6/comparison.json @@ -0,0 +1,70 @@ +{ + "ok": true, + "engine_version": "2.6.0a6", + "dpi": 120, + "documents": [ + { + "name": "invoice", + "ok": true, + "failures": [], + "page_count": 1, + "html_pdf_sha256": "6c40e92c4800827efd0a407146bc196b38c266f4a181c3721446d90f221e14b1", + "component_pdf_sha256": "6c40e92c4800827efd0a407146bc196b38c266f4a181c3721446d90f221e14b1", + "pdf_bytes_identical": true, + "pages": [ + { + "page": 1, + "identical": true, + "html_sha256": "5ceda957db105b08796af42c5d8efa82e37424e320741770adcbbbdbac0e47a0", + "component_sha256": "5ceda957db105b08796af42c5d8efa82e37424e320741770adcbbbdbac0e47a0" + } + ], + "render_verify_replay": true, + "direct_component_api_pdf_identical": true, + "styled_html_pdf_identical": true, + "styled_and_unstyled_exports": true, + "authored_classes_preserved": true + }, + { + "name": "report", + "ok": true, + "failures": [], + "page_count": 3, + "html_pdf_sha256": "eb9d9e201b971c7d3a586df8f6aad0a58a6163c133463d767505deeea8f88ad0", + "component_pdf_sha256": "3821b11defee284b2df724bf4d4bfddb6d89aee288b61c08d1c9c51e615c43bd", + "pdf_bytes_identical": false, + "pages": [ + { + "page": 1, + "identical": true, + "html_sha256": "faec5cb1a8e58c6bad2a81c5ba902a66f26dd7fc638c1b543345256ed001b936", + "component_sha256": "faec5cb1a8e58c6bad2a81c5ba902a66f26dd7fc638c1b543345256ed001b936" + }, + { + "page": 2, + "identical": true, + "html_sha256": "7e7cff0f4b9911fea23a57d57f72b88b13c407c5fc736dd9cff05674261e76f3", + "component_sha256": "7e7cff0f4b9911fea23a57d57f72b88b13c407c5fc736dd9cff05674261e76f3" + }, + { + "page": 3, + "identical": true, + "html_sha256": "1ab62db939e6ef91ef9a107f759fcce11d896a17be4882cf4ee95ad26e74f12a", + "component_sha256": "1ab62db939e6ef91ef9a107f759fcce11d896a17be4882cf4ee95ad26e74f12a" + } + ], + "render_verify_replay": true, + "direct_component_api_pdf_identical": true, + "styled_html_pdf_identical": true, + "styled_and_unstyled_exports": true, + "authored_classes_preserved": true + } + ], + "fonts": { + "Inter-Variable.ttf": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031", + "BebasNeue-Regular.ttf": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73", + "DMSerifDisplay-Regular.ttf": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616", + "DMSerifDisplay-Italic.ttf": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45" + }, + "scope": "Same-runtime PDF page appearance for these fixed A4 compositions; not general HTML/CSS parity." +} diff --git a/docs/assets/showcase-authoring-alpha6/independent-reader.json b/docs/assets/showcase-authoring-alpha6/independent-reader.json new file mode 100644 index 0000000..f89f11b --- /dev/null +++ b/docs/assets/showcase-authoring-alpha6/independent-reader.json @@ -0,0 +1,166 @@ +{ + "ok": true, + "engine_version": "2.6.0a6", + "source_commit": "9da6a672e81fabebb5733aadcdd932355188bac5", + "zip_sha256": "778b31539e274db5f36fd07bbd1bd6de0d09c167d9f977c3bc17b5fccd40e526", + "native_comparison": { + "ok": true, + "engine_version": "2.6.0a6", + "dpi": 120, + "documents": [ + { + "name": "invoice", + "ok": true, + "failures": [], + "page_count": 1, + "html_pdf_sha256": "6c40e92c4800827efd0a407146bc196b38c266f4a181c3721446d90f221e14b1", + "component_pdf_sha256": "6c40e92c4800827efd0a407146bc196b38c266f4a181c3721446d90f221e14b1", + "pdf_bytes_identical": true, + "pages": [ + { + "page": 1, + "identical": true, + "html_sha256": "5ceda957db105b08796af42c5d8efa82e37424e320741770adcbbbdbac0e47a0", + "component_sha256": "5ceda957db105b08796af42c5d8efa82e37424e320741770adcbbbdbac0e47a0" + } + ], + "render_verify_replay": true, + "direct_component_api_pdf_identical": true, + "styled_html_pdf_identical": true, + "styled_and_unstyled_exports": true, + "authored_classes_preserved": true + }, + { + "name": "report", + "ok": true, + "failures": [], + "page_count": 3, + "html_pdf_sha256": "eb9d9e201b971c7d3a586df8f6aad0a58a6163c133463d767505deeea8f88ad0", + "component_pdf_sha256": "3821b11defee284b2df724bf4d4bfddb6d89aee288b61c08d1c9c51e615c43bd", + "pdf_bytes_identical": false, + "pages": [ + { + "page": 1, + "identical": true, + "html_sha256": "faec5cb1a8e58c6bad2a81c5ba902a66f26dd7fc638c1b543345256ed001b936", + "component_sha256": "faec5cb1a8e58c6bad2a81c5ba902a66f26dd7fc638c1b543345256ed001b936" + }, + { + "page": 2, + "identical": true, + "html_sha256": "7e7cff0f4b9911fea23a57d57f72b88b13c407c5fc736dd9cff05674261e76f3", + "component_sha256": "7e7cff0f4b9911fea23a57d57f72b88b13c407c5fc736dd9cff05674261e76f3" + }, + { + "page": 3, + "identical": true, + "html_sha256": "1ab62db939e6ef91ef9a107f759fcce11d896a17be4882cf4ee95ad26e74f12a", + "component_sha256": "1ab62db939e6ef91ef9a107f759fcce11d896a17be4882cf4ee95ad26e74f12a" + } + ], + "render_verify_replay": true, + "direct_component_api_pdf_identical": true, + "styled_html_pdf_identical": true, + "styled_and_unstyled_exports": true, + "authored_classes_preserved": true + } + ], + "fonts": { + "Inter-Variable.ttf": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031", + "BebasNeue-Regular.ttf": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73", + "DMSerifDisplay-Regular.ttf": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616", + "DMSerifDisplay-Italic.ttf": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45" + }, + "scope": "Same-runtime PDF page appearance for these fixed A4 compositions; not general HTML/CSS parity." + }, + "independent_pages": [ + { + "document": "invoice", + "page": 1, + "dpi": 144, + "pixels_sha256": "eed62fa682199704f450ffe208e17832ba43c4b95f48e82ce005ba45eb04341d", + "identical": true + }, + { + "document": "report", + "page": 1, + "dpi": 144, + "pixels_sha256": "5e50b32dc9766120ccba5baad60bc1f868d3427a0d1a535ec2ad4c93d351e318", + "identical": true + }, + { + "document": "report", + "page": 2, + "dpi": 144, + "pixels_sha256": "baa7fab1acdbee376757429a2d3f0a59358641fe6264d0a082038bf49470e0ba", + "identical": true + }, + { + "document": "report", + "page": 3, + "dpi": 144, + "pixels_sha256": "a66d50b48d29c0bad766a801e569e2af1ed60162f618ce70ca59f771384662fa", + "identical": true + } + ], + "pdfium_version": "5.14.0", + "guide_blocks": 3, + "edited_data": [ + { + "name": "invoice", + "ok": true, + "failures": [], + "page_count": 1, + "html_pdf_sha256": "d7495178e1d521540c596fa1030321f45c548796cca3da4888b61bd5b0aa61ed", + "component_pdf_sha256": "d7495178e1d521540c596fa1030321f45c548796cca3da4888b61bd5b0aa61ed", + "pdf_bytes_identical": true, + "pages": [ + { + "page": 1, + "identical": true, + "html_sha256": "d9cdc37b264838e9b5277d7d11fa114ab0271a4ec4332886d3c3e0763ee779e2", + "component_sha256": "d9cdc37b264838e9b5277d7d11fa114ab0271a4ec4332886d3c3e0763ee779e2" + } + ], + "render_verify_replay": true, + "direct_component_api_pdf_identical": true, + "styled_html_pdf_identical": true, + "styled_and_unstyled_exports": true, + "authored_classes_preserved": true + }, + { + "name": "report", + "ok": true, + "failures": [], + "page_count": 3, + "html_pdf_sha256": "69f520794b3b6ab26ff021d3ece358c16e9ecfbfc932c425aafc488dc7c36287", + "component_pdf_sha256": "05b94be8c731e69eae87076feab30e44f80a5db0f11a66550eb5346c88b7fac8", + "pdf_bytes_identical": false, + "pages": [ + { + "page": 1, + "identical": true, + "html_sha256": "faec5cb1a8e58c6bad2a81c5ba902a66f26dd7fc638c1b543345256ed001b936", + "component_sha256": "faec5cb1a8e58c6bad2a81c5ba902a66f26dd7fc638c1b543345256ed001b936" + }, + { + "page": 2, + "identical": true, + "html_sha256": "100bcdd7930a5752eafb67f092f392cf734e0e6fb452f9e840e2e90eb8fa1fe3", + "component_sha256": "100bcdd7930a5752eafb67f092f392cf734e0e6fb452f9e840e2e90eb8fa1fe3" + }, + { + "page": 3, + "identical": true, + "html_sha256": "05c9ce6a8d6d9f7993122a6fdf4fedc992b6518de6e52df884c543141bd686f5", + "component_sha256": "05c9ce6a8d6d9f7993122a6fdf4fedc992b6518de6e52df884c543141bd686f5" + } + ], + "render_verify_replay": true, + "direct_component_api_pdf_identical": true, + "styled_html_pdf_identical": true, + "styled_and_unstyled_exports": true, + "authored_classes_preserved": true + } + ] +} diff --git a/docs/assets/showcase-authoring-alpha6/invoice-1.png b/docs/assets/showcase-authoring-alpha6/invoice-1.png new file mode 100644 index 0000000..3528068 Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/invoice-1.png differ diff --git a/docs/assets/showcase-authoring-alpha6/invoice-components.pdf b/docs/assets/showcase-authoring-alpha6/invoice-components.pdf new file mode 100644 index 0000000..4ea3fa4 Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/invoice-components.pdf differ diff --git a/docs/assets/showcase-authoring-alpha6/invoice-html.pdf b/docs/assets/showcase-authoring-alpha6/invoice-html.pdf new file mode 100644 index 0000000..4ea3fa4 Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/invoice-html.pdf differ diff --git a/docs/assets/showcase-authoring-alpha6/preview-desktop.png b/docs/assets/showcase-authoring-alpha6/preview-desktop.png new file mode 100644 index 0000000..7a8dfc2 Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/preview-desktop.png differ diff --git a/docs/assets/showcase-authoring-alpha6/preview-error-mobile.png b/docs/assets/showcase-authoring-alpha6/preview-error-mobile.png new file mode 100644 index 0000000..7f5c688 Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/preview-error-mobile.png differ diff --git a/docs/assets/showcase-authoring-alpha6/project.zip b/docs/assets/showcase-authoring-alpha6/project.zip new file mode 100644 index 0000000..9080c3a Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/project.zip differ diff --git a/docs/assets/showcase-authoring-alpha6/report-1.png b/docs/assets/showcase-authoring-alpha6/report-1.png new file mode 100644 index 0000000..a588841 Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/report-1.png differ diff --git a/docs/assets/showcase-authoring-alpha6/report-2.png b/docs/assets/showcase-authoring-alpha6/report-2.png new file mode 100644 index 0000000..d1a4d9e Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/report-2.png differ diff --git a/docs/assets/showcase-authoring-alpha6/report-3.png b/docs/assets/showcase-authoring-alpha6/report-3.png new file mode 100644 index 0000000..6d92d7e Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/report-3.png differ diff --git a/docs/assets/showcase-authoring-alpha6/report-components.pdf b/docs/assets/showcase-authoring-alpha6/report-components.pdf new file mode 100644 index 0000000..2e9e0de Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/report-components.pdf differ diff --git a/docs/assets/showcase-authoring-alpha6/report-html.pdf b/docs/assets/showcase-authoring-alpha6/report-html.pdf new file mode 100644 index 0000000..589ab25 Binary files /dev/null and b/docs/assets/showcase-authoring-alpha6/report-html.pdf differ diff --git a/docs/assets/showcase-authoring-alpha6/source.json b/docs/assets/showcase-authoring-alpha6/source.json new file mode 100644 index 0000000..e716fcf --- /dev/null +++ b/docs/assets/showcase-authoring-alpha6/source.json @@ -0,0 +1,78 @@ +{ + "schema": "fullbleed.showcase-authoring.source.v1", + "source_commit": "9da6a672e81fabebb5733aadcdd932355188bac5", + "repository": "https://github.com/fullbleed-engine/fullbleed-official", + "engine_version": "2.6.0a6", + "source_files": { + "examples/design_showcase/.gitattributes": "3b2d2f910738670779137ecce4829c9d5e1a211d2b1174afb01ba87f06812564", + "examples/design_showcase/.gitignore": "7e2f65c80bddb76631f05a448b09378247e7f14af4ade18127925c07a6538dbf", + "examples/design_showcase/README.md": "a6463c5e9852adad8f69b23a8f986bbfe18eb3ef9676728b937c253cd3804f7a", + "examples/design_showcase/common.css": "59ed65ebed86dcebe554d6d2d48c53e0e75e67293e99f537fc767a45db1a84c7", + "examples/design_showcase/compare_authoring.py": "01bce371d15aac6821e3a6d00cf005d1c05927a51c5be5958100b1020c31c819", + "examples/design_showcase/component_primitives.py": "7cd0283d64abdb9d42076a5476eae83f2fdc6850fa9b5b88622542c365b16333", + "examples/design_showcase/data.json": "46cc3ad7cf6560349a41f8ba82bc9822c16176177f3c1236711fa28cb0baa13a", + "examples/design_showcase/fonts/BebasNeue-OFL.txt": "72082f6cb4d04be2ecf7cc7d9e1e7d73787f0af8a5a278a47cade70c16b78341", + "examples/design_showcase/fonts/BebasNeue-Regular.ttf": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73", + "examples/design_showcase/fonts/DMSerifDisplay-Italic.ttf": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45", + "examples/design_showcase/fonts/DMSerifDisplay-OFL.txt": "a3e5cdd67d4571dd0a24fcc968de0efde7ae97ef752daf0906e4767619dd7231", + "examples/design_showcase/fonts/DMSerifDisplay-Regular.ttf": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616", + "examples/design_showcase/fonts/sources.json": "01c23f8e23d039ef6a9945183ff98fd0022975227ae949de4f1669f06ad18db7", + "examples/design_showcase/invoice.css": "4d9d4708b861dd2c43600dc695552c88a445517994a52fc817ed242aac9eaa25", + "examples/design_showcase/invoice.html": "448051114ebd9f641b9232437a8bcd9f94f6460aa1656ccaed74912b0fe0d359", + "examples/design_showcase/invoice_components.py": "cfcf0124615c43d63620b550ea8a308ef364da4b18b96a7c48025345cd512ee4", + "examples/design_showcase/landscape.svg": "a0a997dc5f04b3842830e9ba6610365c4e62a0a32e13619a66c4ea25cca3e488", + "examples/design_showcase/notice.css": "391f879bf794d55b29748147a10526ee493032cea46dae15296858951d5c0afc", + "examples/design_showcase/notice.html": "9596597db1e2e6942f86b8422d646a68342af6a2bc9b82e87229f26936ccdaf7", + "examples/design_showcase/preview.html": "b79af54a21fc5dba7c344be9a7eeb359e0de7d514d8e239ec41a798872d58535", + "examples/design_showcase/preview.js": "089c951807be3c7f99a3a8bf186a7af250e2363d64aba9c63bca8f2d29999b61", + "examples/design_showcase/preview.py": "d4f072007687ff628eb8e932b3ab17c7faf3adbe334d1e561b872680f6ce4fd1", + "examples/design_showcase/render.py": "c66c04fa37c9fdc9cff733b214d5514f9f134c45ddfdda1e6d7a10c8f248d483", + "examples/design_showcase/report.css": "33de04b117f1b0c99298c22beeed184429de748c729f78d183ec9b570e7e8f38", + "examples/design_showcase/report.html": "352d7174f3c42b111a69686cf41bf18253635a1f925f96e0d7d967067fc9c753", + "examples/design_showcase/report_components.py": "6caa62f11d124bc504990df5972230523a1b962619195043309b3c98a2803cd1", + "examples/design_showcase/requirements-components.txt": "058f91f852902be181df6d21cf5e5484cecbc11b14f920337d28d86950cc3d17", + "examples/design_showcase/statement.css": "c8ad4a1606bcab299202f750263af7bedf567d7b34164bda898fa01814ffd4a3", + "examples/design_showcase/statement.html": "b3b597349e474204a72398052bff19486d8a551562568e051375e03871b4c096", + "tools/check_showcase_preview.py": "125ab4b4b3836bd5d707d138eaf6a14a713fcce54fe32052efada398d49cbc19", + "tools/check_showcase_preview_browser.py": "7d61236aecde53dd0cb11ed94f8ffb1ec7430b5e64db2251dc5daed860b51b58" + }, + "zip_sha256": "778b31539e274db5f36fd07bbd1bd6de0d09c167d9f977c3bc17b5fccd40e526", + "zip_bytes": 347575, + "package_files": { + "LICENSE": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9", + "README.md": "06551646e42a12bd3b0154a3021f191d66b8f889c5dfd7b6eea991cc2a7cadcd", + "SOURCE.json": "592ce4fdf8e7a78cdf37c6253e6f1834180d818b55309179d9c2b9631764ca6c", + "examples/design_showcase/.gitattributes": "3b2d2f910738670779137ecce4829c9d5e1a211d2b1174afb01ba87f06812564", + "examples/design_showcase/.gitignore": "7e2f65c80bddb76631f05a448b09378247e7f14af4ade18127925c07a6538dbf", + "examples/design_showcase/README.md": "a6463c5e9852adad8f69b23a8f986bbfe18eb3ef9676728b937c253cd3804f7a", + "examples/design_showcase/common.css": "59ed65ebed86dcebe554d6d2d48c53e0e75e67293e99f537fc767a45db1a84c7", + "examples/design_showcase/compare_authoring.py": "01bce371d15aac6821e3a6d00cf005d1c05927a51c5be5958100b1020c31c819", + "examples/design_showcase/component_primitives.py": "7cd0283d64abdb9d42076a5476eae83f2fdc6850fa9b5b88622542c365b16333", + "examples/design_showcase/data.json": "46cc3ad7cf6560349a41f8ba82bc9822c16176177f3c1236711fa28cb0baa13a", + "examples/design_showcase/fonts/BebasNeue-OFL.txt": "72082f6cb4d04be2ecf7cc7d9e1e7d73787f0af8a5a278a47cade70c16b78341", + "examples/design_showcase/fonts/BebasNeue-Regular.ttf": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73", + "examples/design_showcase/fonts/DMSerifDisplay-Italic.ttf": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45", + "examples/design_showcase/fonts/DMSerifDisplay-OFL.txt": "a3e5cdd67d4571dd0a24fcc968de0efde7ae97ef752daf0906e4767619dd7231", + "examples/design_showcase/fonts/DMSerifDisplay-Regular.ttf": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616", + "examples/design_showcase/fonts/sources.json": "01c23f8e23d039ef6a9945183ff98fd0022975227ae949de4f1669f06ad18db7", + "examples/design_showcase/invoice.css": "4d9d4708b861dd2c43600dc695552c88a445517994a52fc817ed242aac9eaa25", + "examples/design_showcase/invoice.html": "448051114ebd9f641b9232437a8bcd9f94f6460aa1656ccaed74912b0fe0d359", + "examples/design_showcase/invoice_components.py": "cfcf0124615c43d63620b550ea8a308ef364da4b18b96a7c48025345cd512ee4", + "examples/design_showcase/landscape.svg": "a0a997dc5f04b3842830e9ba6610365c4e62a0a32e13619a66c4ea25cca3e488", + "examples/design_showcase/notice.css": "391f879bf794d55b29748147a10526ee493032cea46dae15296858951d5c0afc", + "examples/design_showcase/notice.html": "9596597db1e2e6942f86b8422d646a68342af6a2bc9b82e87229f26936ccdaf7", + "examples/design_showcase/preview.html": "b79af54a21fc5dba7c344be9a7eeb359e0de7d514d8e239ec41a798872d58535", + "examples/design_showcase/preview.js": "089c951807be3c7f99a3a8bf186a7af250e2363d64aba9c63bca8f2d29999b61", + "examples/design_showcase/preview.py": "d4f072007687ff628eb8e932b3ab17c7faf3adbe334d1e561b872680f6ce4fd1", + "examples/design_showcase/render.py": "c66c04fa37c9fdc9cff733b214d5514f9f134c45ddfdda1e6d7a10c8f248d483", + "examples/design_showcase/report.css": "33de04b117f1b0c99298c22beeed184429de748c729f78d183ec9b570e7e8f38", + "examples/design_showcase/report.html": "352d7174f3c42b111a69686cf41bf18253635a1f925f96e0d7d967067fc9c753", + "examples/design_showcase/report_components.py": "6caa62f11d124bc504990df5972230523a1b962619195043309b3c98a2803cd1", + "examples/design_showcase/requirements-components.txt": "058f91f852902be181df6d21cf5e5484cecbc11b14f920337d28d86950cc3d17", + "examples/design_showcase/statement.css": "c8ad4a1606bcab299202f750263af7bedf567d7b34164bda898fa01814ffd4a3", + "examples/design_showcase/statement.html": "b3b597349e474204a72398052bff19486d8a551562568e051375e03871b4c096", + "requirements.txt": "058f91f852902be181df6d21cf5e5484cecbc11b14f920337d28d86950cc3d17", + "tools/check_showcase_preview.py": "125ab4b4b3836bd5d707d138eaf6a14a713fcce54fe32052efada398d49cbc19", + "tools/check_showcase_preview_browser.py": "7d61236aecde53dd0cb11ed94f8ffb1ec7430b5e64db2251dc5daed860b51b58" + } +} diff --git a/docs/getting-started/component-notebook.md b/docs/getting-started/component-notebook.md index 91e1346..3324387 100644 --- a/docs/getting-started/component-notebook.md +++ b/docs/getting-started/component-notebook.md @@ -11,12 +11,12 @@ The notebook contains the complete source, with no authored HTML strings. Item cells return `Strong(...)` components, so emphasis stays part of the component tree. -[Open in Colab](https://colab.research.google.com/github/fullbleed-engine/docs/blob/393748f31499e0d4ea9fc9a129810a85b98e4652/examples/component-notebook-alpha5/bootstrap_components.ipynb){ .md-button .md-button--primary } -[Download the notebook](../assets/component-notebook-alpha5/bootstrap_components.ipynb){ .md-button download="bootstrap_components.ipynb" } -[View the source](https://github.com/fullbleed-engine/docs/blob/393748f31499e0d4ea9fc9a129810a85b98e4652/examples/component-notebook-alpha5/bootstrap_components.ipynb){ .md-button } +[Open in Colab](https://colab.research.google.com/github/fullbleed-engine/docs/blob/ee8cfbef90116c4e1ea2db7c0f005dce946a11a8/examples/component-notebook-alpha6/bootstrap_components.ipynb){ .md-button .md-button--primary } +[Download the notebook](../assets/component-notebook-alpha6/bootstrap_components.ipynb){ .md-button download="bootstrap_components.ipynb" } +[View the source](https://github.com/fullbleed-engine/docs/blob/ee8cfbef90116c4e1ea2db7c0f005dce946a11a8/examples/component-notebook-alpha6/bootstrap_components.ipynb){ .md-button } !!! warning "Opt-in component alpha" - The notebook installs **Fullbleed 2.6.0a5**. Its APIs under + The notebook installs **Fullbleed 2.6.0a6**. Its APIs under `fullbleed.ui.experimental` may change and have documented CSS limits. Bootstrap components are static in PDF and HTML; no Bootstrap JavaScript is included. The [stable Python guide](../guides/python-components.md) @@ -37,11 +37,11 @@ a download link there. ## What you will make -[![A Northstar order summary with green headings, quantity and total cards, a three-row table and a shipment note.](../assets/component-notebook-alpha5/order-page-1.png){ width="480" }](../assets/component-notebook-alpha5/order.pdf) +[![A Northstar order summary with green headings, quantity and total cards, a three-row table and a shipment note.](../assets/component-notebook-alpha6/order-page-1.png){ width="480" }](../assets/component-notebook-alpha6/order.pdf) -[Open the sample PDF](../assets/component-notebook-alpha5/order.pdf){ .md-button } -[Open styled HTML](../assets/component-notebook-alpha5/order.styled.html){ .md-button } -[Open unstyled HTML](../assets/component-notebook-alpha5/order.unstyled.html){ .md-button } +[Open the sample PDF](../assets/component-notebook-alpha6/order.pdf){ .md-button } +[Open styled HTML](../assets/component-notebook-alpha6/order.styled.html){ .md-button } +[Open unstyled HTML](../assets/component-notebook-alpha6/order.unstyled.html){ .md-button } The fictional order contains 44 items totaling USD 1,056.00. Python `Decimal` calculates the total; the example omits tax, shipping and discounts. Try an empty @@ -74,7 +74,7 @@ compare repeat renders; and check both HTML exports and the ZIP's exact contents that rich item cells preserve their emphasis, order and escaped text. Old page previews are excluded from a shorter order's download. -The [verification record](../assets/component-notebook-alpha5/verification.json) includes +The [verification record](../assets/component-notebook-alpha6/verification.json) includes the sample and notebook hashes. These checks do not execute a hosted Colab runtime or its download UI, or establish complete CSS, browser or PDF-standard conformance. @@ -82,3 +82,5 @@ or its download UI, or establish complete CSS, browser or PDF-standard conforman if a composition or styling task feels awkward. Use fictional data in public examples. The [archived alpha 3 notebook](https://colab.research.google.com/github/fullbleed-engine/docs/blob/8cf4915321f2faf517ad7e5b0725e83754e227a6/examples/component-notebook/bootstrap_components.ipynb), [sample PDF](../assets/component-notebook/order.pdf) and [archived verification record](../assets/component-notebook/verification.json) retain their original runtime and bytes. + +The [alpha 5 notebook guide](https://github.com/fullbleed-engine/docs/blob/9aa0b2d9b26d398df58f55c8dd3b240a2457e96e/docs/getting-started/component-notebook.md) retains its original pinned notebook and sample files. diff --git a/docs/guides/accessible-showcase.md b/docs/guides/accessible-showcase.md index 6ddb551..b05cabd 100644 --- a/docs/guides/accessible-showcase.md +++ b/docs/guides/accessible-showcase.md @@ -10,18 +10,18 @@ reusable program cards, a captioned schedule and practical visitor information. Its Python source carries headings, table headers, lists and an image description through to the final PDF. -[Read the component source](https://github.com/fullbleed-engine/fullbleed-official/blob/25ef51e152744e36716054005b7a964e0cccc6e9/examples/accessible_showcase/guide.py). +[Read the component source](https://github.com/fullbleed-engine/fullbleed-official/blob/9da6a672e81fabebb5733aadcdd932355188bac5/examples/accessible_showcase/guide.py). -[Read the PDF](../assets/accessible-showcase-alpha5/common-ground-guide.pdf){ .md-button .md-button--primary } -[Download the source kit](../assets/accessible-showcase-alpha5/project.zip){ .md-button } -[Open the responsive HTML](../assets/accessible-showcase-alpha5/guide.html){ .md-button } +[Read the PDF](../assets/accessible-showcase-alpha6/common-ground-guide.pdf){ .md-button .md-button--primary } +[Download the source kit](../assets/accessible-showcase-alpha6/project.zip){ .md-button } +[Open the responsive HTML](../assets/accessible-showcase-alpha6/guide.html){ .md-button } -![Common Ground cover with forest-green serif typography, an illustrated shared garden, three program cards and a welcoming callout.](../assets/accessible-showcase-alpha5/page-1.png) +![Common Ground cover with forest-green serif typography, an illustrated shared garden, three program cards and a welcoming callout.](../assets/accessible-showcase-alpha6/page-1.png) -[View the schedule and visitor-information page](../assets/accessible-showcase-alpha5/page-2.png). +[View the schedule and visitor-information page](../assets/accessible-showcase-alpha6/page-2.png). !!! info "Pinned component alpha" - The kit uses `fullbleed.ui.experimental` from **2.6.0a5**. Its API may change + The kit uses `fullbleed.ui.experimental` from **2.6.0a6**. Its API may change before stabilization. The PDF and all verification receipts below refer to this specific specimen. @@ -68,7 +68,7 @@ styled = document.to_html(include_styles=True, a11y_mode="raise") unstyled = document.to_html(include_styles=False, a11y_mode="raise") ``` -[Open the unstyled HTML](../assets/accessible-showcase-alpha5/guide-unstyled.html) to see +[Open the unstyled HTML](../assets/accessible-showcase-alpha6/guide-unstyled.html) to see the semantic document without CSS. It retains authored class names, text, alt text and the link, so another application can supply its own presentation. The render command copies the local fonts and illustration beside both exports. @@ -93,12 +93,12 @@ the exact PDF by SHA-256. | Unstyled HTML | Semantic elements and authored classes retained; no CSS or inline style declarations | | Authored text colors | Minimum contrast ratio 7.45:1 across the design's text/background pairs | -[Validation receipt](../assets/accessible-showcase-alpha5/validation.json) · -[veraPDF report](../assets/accessible-showcase-alpha5/pdfua1.verapdf.json) · -[PDF tag and reading-order report](../assets/accessible-showcase-alpha5/structure.json) · -[Verifier failure control](../assets/accessible-showcase-alpha5/verification-guard.json) · -[Browser checks](../assets/accessible-showcase-alpha5/browser-verification.json) · -[Source manifest](../assets/accessible-showcase-alpha5/source.json) +[Validation receipt](../assets/accessible-showcase-alpha6/validation.json) · +[veraPDF report](../assets/accessible-showcase-alpha6/pdfua1.verapdf.json) · +[PDF tag and reading-order report](../assets/accessible-showcase-alpha6/structure.json) · +[Verifier failure control](../assets/accessible-showcase-alpha6/verification-guard.json) · +[Browser checks](../assets/accessible-showcase-alpha6/browser-verification.json) · +[Source manifest](../assets/accessible-showcase-alpha6/source.json) These checks are **not a screen-reader user test or blanket WCAG certification**. The kit includes `verify.py` to inspect the actual output again after edits. @@ -107,8 +107,9 @@ veraPDF. Alpha 5 corrects the [overlapping content tags in rich table cells](https://github.com/fullbleed-engine/fullbleed-official/issues/97). The program column now returns `Strong(row["program"])` directly, preserving that component's content inside its table cell. The kit still rejects the retained defective -alpha 4 PDF. Caption-containment issues in PDF/UA-2 and column-consistency issues -in the oversized-row stress fixture remain separate limitations. +alpha 4 PDF. Alpha 6 also corrects the oversized-cell fixture's column-consistency +failure: both direct and styled-export PDFs now pass the PDF/UA-1 checks. +Caption-containment findings in PDF/UA-2 remain a separate limitation. For an external check, run [veraPDF's UA-1 profile](https://docs.verapdf.org/cli/validation/) on the final PDF: @@ -126,3 +127,5 @@ For another way to author the same design, see the [paired Northstar and Common Ground examples](showcase-authoring.md). The [alpha 4 guide](https://github.com/fullbleed-engine/docs/blob/fff03de311627b15291a527996b94fd3626cd79d/docs/guides/accessible-showcase.md), [PDF](../assets/accessible-showcase/common-ground-guide.pdf) and [source kit](../assets/accessible-showcase/project.zip) retain their original bytes, validation receipt and plain-text table workaround. + +The [alpha 5 guide](https://github.com/fullbleed-engine/docs/blob/9aa0b2d9b26d398df58f55c8dd3b240a2457e96e/docs/guides/accessible-showcase.md) retains its original runtime pins and download links. diff --git a/docs/guides/bootstrap-components-preview.md b/docs/guides/bootstrap-components-preview.md index bdb856e..a178d93 100644 --- a/docs/guides/bootstrap-components-preview.md +++ b/docs/guides/bootstrap-components-preview.md @@ -9,15 +9,15 @@ Build an order summary with a card, a data table and a status badge. Export it as HTML or render it as a PDF. The component supplies its structure and default styling; your Python supplies the content. -!!! warning "Opt-in alpha 2.6.0a5" - This guide uses **Fullbleed 2.6.0a5**, with experimental APIs under +!!! warning "Opt-in alpha 2.6.0a6" + This guide uses **Fullbleed 2.6.0a6**, with experimental APIs under `fullbleed.ui.experimental`. Install the alpha explicitly; a normal stable installation does not select it. Names and behavior may change. Use the [released component guide](python-components.md) for the stable API. -[Download the Bootstrap kit](../assets/bootstrap-alpha5/project.zip){ .md-button .md-button--primary } -[Open the report PDF](../assets/bootstrap-alpha5/report.pdf){ .md-button } -[Browse the HTML catalog](../assets/bootstrap-alpha5/catalog.html){ .md-button } +[Download the Bootstrap kit](../assets/bootstrap-alpha6/project.zip){ .md-button .md-button--primary } +[Open the report PDF](../assets/bootstrap-alpha6/report.pdf){ .md-button } +[Browse the HTML catalog](../assets/bootstrap-alpha6/catalog.html){ .md-button } [Try the component notebook](../getting-started/component-notebook.md) to edit data and styles in Colab or Jupyter, then download a PDF and both HTML exports. @@ -34,7 +34,7 @@ their native browser behavior; PDF controls show printed values and selections.
-[![A Northstar production report with three metric cards, a striped revenue table, two explanatory cards and a progress bar.](../assets/bootstrap-alpha5/report-page-1.png)](../assets/bootstrap-alpha5/report.pdf) +[![A Northstar production report with three metric cards, a striped revenue table, two explanatory cards and a progress bar.](../assets/bootstrap-alpha6/report-page-1.png)](../assets/bootstrap-alpha6/report.pdf)
@@ -45,8 +45,8 @@ compose a single document. Its content is fictional. The kit includes the complete Python source, and this image is rendered from the downloadable PDF with PDFium. -[Open the styled HTML](../assets/bootstrap-alpha5/report.styled.html){ .md-button } -[Compare unstyled HTML](../assets/bootstrap-alpha5/report.unstyled.html) +[Open the styled HTML](../assets/bootstrap-alpha6/report.styled.html){ .md-button } +[Compare unstyled HTML](../assets/bootstrap-alpha6/report.unstyled.html)
@@ -63,8 +63,8 @@ Activate it with `.venv\Scripts\Activate.ps1` in Windows PowerShell, or `source .venv/bin/activate` on macOS/Linux. Then run: ```console -python -m pip install --pre "fullbleed==2.6.0a5" -fullbleed new remote python-components my-report --version 0.1.1 +python -m pip install --pre "fullbleed==2.6.0a6" +fullbleed new remote python-components my-report --version 0.1.2 cd my-report python first_document.py python examples/component_bootstrap/main.py --out output --catalog-pdfs @@ -74,15 +74,15 @@ The CLI downloads the pinned project and verifies its SHA-256 before extracting the files. Choose a new or empty target directory. You can discover the available component projects with `fullbleed new search components`. -If you downloaded the [kit ZIP](../assets/bootstrap-alpha5/project.zip) instead, -enter its `fullbleed-bootstrap-alpha5` folder and skip the `new remote` and +If you downloaded the [kit ZIP](../assets/bootstrap-alpha6/project.zip) instead, +enter its `fullbleed-bootstrap-alpha6` folder and skip the `new remote` and `cd my-report` steps. The same Python commands render the document and catalog. Open `order.pdf` and `order.html`, then `output/report.pdf` and `output/catalog.html`. The kit's `requirements.txt` pins the same alpha version. Compatible Windows, macOS and Linux wheels need no Rust build. On other platforms, pip may build the source distribution, which needs Rust/Cargo and native build tools. -See the [alpha release](https://github.com/fullbleed-engine/fullbleed-official/releases/tag/v2.6.0a5). +See the [alpha release](https://github.com/fullbleed-engine/fullbleed-official/releases/tag/v2.6.0a6). The [alpha 3 kit](../assets/bootstrap-alpha3/project.zip), [alpha 2 kit](../assets/bootstrap-alpha2/project.zip), @@ -185,7 +185,7 @@ Use `styled(bs.Card, ...)` to define reusable root defaults. `style(...)` checks a vocabulary of common properties; additional declarations use the explicit `css(...)` helper, such as `css("box-shadow: inset 4px 0 0 0 #205b47;")`. Compose it with `.with_style(...)` or a named-part style. The -[styling guide](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.6.0a5/docs/component-bootstrap.md#how-styling-works) +[styling guide](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.6.0a6/docs/component-bootstrap.md#how-styling-works) includes a complete reusable-card example. Cards expose `header`, `body`, `title`, `subtitle` and `footer` as style parts. @@ -198,7 +198,7 @@ Custom components declare required targets with `parts=(...)` and conditional targets with `optional_parts=(...)`. A parent exposes a selected child part with `.forward_parts(heading="label")`. Missing required targets produce a diagnostic when the component is created; child implementation details stay private. The -[part-styling guide](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.6.0a5/examples/component_recipes/README.md#customize-a-root-or-an-exposed-part) +[part-styling guide](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.6.0a6/examples/component_recipes/README.md#customize-a-root-or-an-exposed-part) shows reusable components and forwarding through containers. ## Export whole documents or fragments @@ -227,7 +227,7 @@ source, or `target="html"` for ordinary HTML controls. ## Explore the component catalog -The [HTML catalog](../assets/bootstrap-alpha5/catalog.html) shows all 23 +The [HTML catalog](../assets/bootstrap-alpha6/catalog.html) shows all 23 Bootstrap 5.0.0 component families, plus four groups for layout, forms, content and utilities. The source is in `examples/component_bootstrap/catalog.py`. @@ -246,8 +246,8 @@ state. Hidden panels remain hidden in unstyled HTML. PDF controls print values and selections, including masked passwords and selected options. They do not become fillable PDF fields. Compare the -[form PDF](../assets/bootstrap-alpha5/forms.pdf) and -[form preview](../assets/bootstrap-alpha5/forms-page-1.png) with the HTML catalog. +[form PDF](../assets/bootstrap-alpha6/forms.pdf) and +[form preview](../assets/bootstrap-alpha6/forms-page-1.png) with the HTML catalog. ## What has been checked @@ -255,18 +255,26 @@ The source implementation has focused tests for the component and export contracts. Its native catalog contains 27 one-page specimens; a separate 80-row table checks pagination and repeated headers. Browser checks exercise authored states and styled/unstyled HTML at desktop and mobile widths. -The [download manifest](../assets/bootstrap-alpha5/source.json) pins source -files and wheel hashes. The [output verification record](../assets/bootstrap-alpha5/verification.json) +The [download manifest](../assets/bootstrap-alpha6/source.json) pins source +files and wheel hashes. The [output verification record](../assets/bootstrap-alpha6/verification.json) records the published documents and checks. -Alpha 5 includes the stable inline-spacing and rich-table tagging fixes. +Alpha 6 includes the stable inline-spacing and rich-table tagging fixes. Components such as `Strong(...)` can own their content inside table cells without overlapping marked-content IDs. The [accessible showcase](accessible-showcase.md) uses this in its program schedule and retains validation of the actual PDF. +Long text cells aligned at the top or baseline, and top-aligned list cells, +can continue across pages. The retained checks cover visible page margins, +repeated headings, and content preserved through direct rendering, prepared +documents, and both HTML exports. Rich-cell continuation with middle or bottom +alignment remains unsupported; layout errors retain the component source ID. +PDF/UA-2 caption containment remains an open issue, so these checks do not +establish general table conformance. + Document class rules, inherited colors and named-part opacity reach supported inline SVG content. Isolated translucent groups apply opacity once in saved-PDF -previews. See the [SVG styling contract](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.6.0a5/docs/embedded-stylesheets.md) +previews. See the [SVG styling contract](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.6.0a6/docs/embedded-stylesheets.md) for the supported behavior. Card root styles, custom class rules and named body parts share the corrected @@ -286,8 +294,10 @@ reading the implementation. If a task requires generated HTML or an internal selector, that is useful feedback for the API design. [Share an editing task in the design discussion](https://github.com/fullbleed-engine/fullbleed-official/discussions/86){ .md-button } -[Read the complete API guide](https://github.com/fullbleed-engine/fullbleed-official/blob/25ef51e152744e36716054005b7a964e0cccc6e9/docs/component-bootstrap.md) -· [Read the implementation review](https://github.com/fullbleed-engine/fullbleed-official/blob/25ef51e152744e36716054005b7a964e0cccc6e9/docs/component-authoring-next-iteration.md) +[Read the complete API guide](https://github.com/fullbleed-engine/fullbleed-official/blob/9da6a672e81fabebb5733aadcdd932355188bac5/docs/component-bootstrap.md) +· [Read the implementation review](https://github.com/fullbleed-engine/fullbleed-official/blob/9da6a672e81fabebb5733aadcdd932355188bac5/docs/component-authoring-next-iteration.md) · [Review the draft PR](https://github.com/fullbleed-engine/fullbleed-official/pull/85) The [alpha 4 guide](https://github.com/fullbleed-engine/docs/blob/fff03de311627b15291a527996b94fd3626cd79d/docs/guides/bootstrap-components-preview.md) and [source kit](../assets/bootstrap-alpha4/project.zip) remain available with their original runtime pin and verification records. + +The [alpha 5 guide](https://github.com/fullbleed-engine/docs/blob/9aa0b2d9b26d398df58f55c8dd3b240a2457e96e/docs/guides/bootstrap-components-preview.md) retains its original runtime pins and download links. diff --git a/docs/guides/showcase-authoring.md b/docs/guides/showcase-authoring.md index ecd34ce..40f01f6 100644 --- a/docs/guides/showcase-authoring.md +++ b/docs/guides/showcase-authoring.md @@ -17,26 +17,26 @@ For a document with retained accessibility evidence, try the new [component-authored Common Ground guide](accessible-showcase.md): two designed pages, PDF/UA-1 machine validation, and styled or unstyled HTML exports. -[Download both source versions](../assets/showcase-authoring-alpha5/project.zip){ .md-button .md-button--primary } -[Northstar component PDF](../assets/showcase-authoring-alpha5/invoice-components.pdf){ .md-button } -[Common Ground component PDF](../assets/showcase-authoring-alpha5/report-components.pdf){ .md-button } +[Download both source versions](../assets/showcase-authoring-alpha6/project.zip){ .md-button .md-button--primary } +[Northstar component PDF](../assets/showcase-authoring-alpha6/invoice-components.pdf){ .md-button } +[Common Ground component PDF](../assets/showcase-authoring-alpha6/report-components.pdf){ .md-button } -!!! info "Component alpha 2.6.0a5" +!!! info "Component alpha 2.6.0a6" These examples use `fullbleed.ui.experimental` from the explicitly pinned - **2.6.0a5** prerelease. The API may change before stabilization. The original + **2.6.0a6** prerelease. The API may change before stabilization. The original [2.4.0 showcase downloads](../examples.md#styled-invoice) remain available; - this comparison renders both authoring paths on 2.6.0a5. + this comparison renders both authoring paths on 2.6.0a6. ## Keep the design, choose the source | Document | HTML/CSS output | Component output | Pages | | --- | --- | --- | --- | -| Northstar Studio | [HTML-authored PDF](../assets/showcase-authoring-alpha5/invoice-html.pdf) | [Component-authored PDF](../assets/showcase-authoring-alpha5/invoice-components.pdf) | 1 | -| Common Ground | [HTML-authored PDF](../assets/showcase-authoring-alpha5/report-html.pdf) | [Component-authored PDF](../assets/showcase-authoring-alpha5/report-components.pdf) | 3 | +| Northstar Studio | [HTML-authored PDF](../assets/showcase-authoring-alpha6/invoice-html.pdf) | [Component-authored PDF](../assets/showcase-authoring-alpha6/invoice-components.pdf) | 1 | +| Common Ground | [HTML-authored PDF](../assets/showcase-authoring-alpha6/report-html.pdf) | [Component-authored PDF](../assets/showcase-authoring-alpha6/report-components.pdf) | 3 |
-[![Northstar invoice with a condensed headline, serif studio identity and forest-green total panel.](../assets/showcase-authoring-alpha5/invoice-1.png)](../assets/showcase-authoring-alpha5/invoice-components.pdf) +[![Northstar invoice with a condensed headline, serif studio identity and forest-green total panel.](../assets/showcase-authoring-alpha6/invoice-1.png)](../assets/showcase-authoring-alpha6/invoice-components.pdf)
@@ -47,7 +47,7 @@ typed table columns. `AmountDue` owns the total panel and exposes its label and amount as named styling parts. The full document composes these pieces and calculates totals with `Decimal`. -[Read the component source](https://github.com/fullbleed-engine/fullbleed-official/blob/25ef51e152744e36716054005b7a964e0cccc6e9/examples/design_showcase/invoice_components.py) +[Read the component source](https://github.com/fullbleed-engine/fullbleed-official/blob/9da6a672e81fabebb5733aadcdd932355188bac5/examples/design_showcase/invoice_components.py)
@@ -83,7 +83,7 @@ targets. Call it again with different values to create another field.
-[![Common Ground cover with oversized green type, a warm orange sun and a vector neighborhood landscape.](../assets/showcase-authoring-alpha5/report-1.png)](../assets/showcase-authoring-alpha5/report-components.pdf) +[![Common Ground cover with oversized green type, a warm orange sun and a vector neighborhood landscape.](../assets/showcase-authoring-alpha6/report-1.png)](../assets/showcase-authoring-alpha6/report-components.pdf)
@@ -94,10 +94,10 @@ compose metrics, a participation chart and an allocation table. The illustration uses the original local SVG asset; chart heights and allocation shares come from the data. -[Read the component source](https://github.com/fullbleed-engine/fullbleed-official/blob/25ef51e152744e36716054005b7a964e0cccc6e9/examples/design_showcase/report_components.py) +[Read the component source](https://github.com/fullbleed-engine/fullbleed-official/blob/9da6a672e81fabebb5733aadcdd932355188bac5/examples/design_showcase/report_components.py) -[Reach page](../assets/showcase-authoring-alpha5/report-2.png) · -[Resources page](../assets/showcase-authoring-alpha5/report-3.png) +[Reach page](../assets/showcase-authoring-alpha6/report-2.png) · +[Resources page](../assets/showcase-authoring-alpha6/report-3.png)
@@ -140,8 +140,8 @@ columns' `white-space: nowrap` behavior. Create the project through the CLI in a fresh Python environment: ```sh -python -m pip install --pre "fullbleed==2.6.0a5" -fullbleed new remote component-showcase my-documents --version 0.1.1 +python -m pip install --pre "fullbleed==2.6.0a6" +fullbleed new remote component-showcase my-documents --version 0.1.2 cd my-documents ``` @@ -149,7 +149,7 @@ The CLI verifies the pinned source archive's SHA-256 before extracting it. Choose a new or empty target directory. `fullbleed new search components` also lists the smaller Bootstrap starter. -If you prefer the [source ZIP](../assets/showcase-authoring-alpha5/project.zip), extract +If you prefer the [source ZIP](../assets/showcase-authoring-alpha6/project.zip), extract it and run `python -m pip install -r requirements.txt` from its root instead. Both routes use the explicit component alpha. Then render and compare: @@ -175,7 +175,7 @@ Open the `http://127.0.0.1:...` URL printed in the terminal. Edit in your editor and save. The viewer refreshes the finalized PDF pages, with downloads for the PDF, styled HTML and unstyled HTML. -![The local Northstar preview with PDF and HTML downloads, an up-to-date status, and the generated invoice.](../assets/showcase-authoring-alpha5/preview-desktop.png) +![The local Northstar preview with PDF and HTML downloads, an up-to-date status, and the generated invoice.](../assets/showcase-authoring-alpha6/preview-desktop.png) Choose the report or the original HTML/CSS path: @@ -197,7 +197,7 @@ the result.
What an authoring error looks like -![A syntax error shown above the retained invoice, with last-version download labels and an expandable traceback.](../assets/showcase-authoring-alpha5/preview-error-mobile.png){ width="390" } +![A syntax error shown above the retained invoice, with last-version download labels and an expandable traceback.](../assets/showcase-authoring-alpha6/preview-error-mobile.png){ width="390" }
@@ -252,11 +252,13 @@ layout after substantial content growth. The quarterly chart keeps the original - Direct component rendering and embedded-style HTML produce the same component PDF bytes. Unstyled exports omit CSS and preserve authored classes. -[Native comparison](../assets/showcase-authoring-alpha5/comparison.json) · -[Independent-reader comparison](../assets/showcase-authoring-alpha5/independent-reader.json) · -[Source and download hashes](../assets/showcase-authoring-alpha5/source.json) +[Native comparison](../assets/showcase-authoring-alpha6/comparison.json) · +[Independent-reader comparison](../assets/showcase-authoring-alpha6/independent-reader.json) · +[Source and download hashes](../assets/showcase-authoring-alpha6/source.json) These checks cover these two designs on the pinned runtime. They do not imply that arbitrary HTML, CSS or browser layout has complete parity. The [alpha 4 guide](https://github.com/fullbleed-engine/docs/blob/fff03de311627b15291a527996b94fd3626cd79d/docs/guides/showcase-authoring.md) and [source kit](../assets/showcase-authoring/project.zip) remain available with their original pinned runtime and output. + +The [alpha 5 guide](https://github.com/fullbleed-engine/docs/blob/9aa0b2d9b26d398df58f55c8dd3b240a2457e96e/docs/guides/showcase-authoring.md) retains its original runtime pins and download links. diff --git a/examples/accessible-showcase-alpha6/.gitignore b/examples/accessible-showcase-alpha6/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/examples/accessible-showcase-alpha6/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/examples/accessible-showcase-alpha6/LICENSE b/examples/accessible-showcase-alpha6/LICENSE new file mode 100644 index 0000000..4c99bbd --- /dev/null +++ b/examples/accessible-showcase-alpha6/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/accessible-showcase-alpha6/README.md b/examples/accessible-showcase-alpha6/README.md new file mode 100644 index 0000000..b9fe1f0 --- /dev/null +++ b/examples/accessible-showcase-alpha6/README.md @@ -0,0 +1,103 @@ +# Common Ground: an accessible authored showcase + +A two-page community-program guide built with reusable Python components, a +local SVG illustration and embedded fonts. All program information is fictional. +The source contains no HTML templates or raw HTML strings. + +This source uses the experimental component API in **Fullbleed 2.6.0a6**. +Pin the version supplied with each downloaded source kit; the API may change +before stabilization. Historical downloads retain their original pins. + +## Render and edit + +```sh +python -m pip install -r requirements.txt +python render.py +``` + +Open `output/common-ground-guide.pdf`. The output directory also contains a +responsive `guide.html`, semantic `guide-unstyled.html`, the PDF's separate +`source.html` and `source.css`, PNG previews and diagnostic reports. Keep the +exported `fonts/` folder and `garden.svg` beside the HTML files. + +- `guide.py` composes `ProgramCard`, `ProgramSchedule`, headings, lists and an image. +- `SCHEDULE` supplies the table records. `Column` supplies labels and cell callbacks. +- `guide.css` provides the document design. For example, + `class_name="program-card"` emits a class selected by `.program-card`; it does + not discover or load a stylesheet. `Document(styles=...)` loads that CSS. +- `browser.css` adds the responsive browser presentation only. It is deliberately + excluded from the PDF source. `Page(...)` owns the PDF's A4 geometry. +- `render.py` registers local fonts and artwork and selects the `pdfua1` profile. + +```python +from guide import build_document, ProgramCard + +card = ProgramCard("Grow something", "A garden we tend together.") +document = build_document(browser=True) +styled_html = document.to_html(include_styles=True, a11y_mode="raise") +semantic_html = document.to_html(include_styles=False, a11y_mode="raise") +``` + +The unstyled export keeps semantic elements, text, alt text and authored class +names while removing CSS and inline style declarations. It can be styled by a +different host application. + +## Accessibility and verification + +The guide authors a document title and language, one H1 and a continuous H2/H3 +hierarchy, an informative image description, a captioned table with scoped column +headers, and two semantic lists. Column content remains in authored reading +order. Colors are not used as the only way to identify any information. + +`render.py` checks deterministic PDF replay, exact PDF byte parity with the +exported HTML/CSS path, two-page pagination, and native overflow, missing-glyph +and font-substitution gates. `verify.py` uses an independent PDF parser to inspect +the resulting structure tree and text associated with its marked-content IDs: + +```sh +python -m pip install -r requirements-check.txt +python verify.py +``` + +This check also rejects nested marked-content IDs and unbalanced tag boundaries +without requiring an external validator. The schedule uses a Strong component +inside its program cells. Alpha 5 carries the correction for the +[table-cell tagging issue](https://github.com/fullbleed-engine/fullbleed-official/issues/97) +that required plain text in the alpha 4 specimen. Edited documents must pass +the same saved-PDF check. + +For an external standards check, install [veraPDF](https://verapdf.org/software/) +and run its PDF/UA-1 profile against the **final** PDF: + +```sh +verapdf --format json -f ua1 output/common-ground-guide.pdf +``` + +Alternatively, `python verify.py --verapdf-cp PATH_TO_CLASSPATH` runs the +`org.verapdf.apps.GreenfieldCliWrapper` Java entry point and retains its JSON +report. Without this argument, the script explicitly records that external +validation did not run. Java and the independent readers are verification tools, +not dependencies of the document renderer. + +The alpha 5 specimen passed veraPDF 1.30.3's PDF/UA-1 machine checks: 106 rules, +5,512 checks, zero failures and zero warnings. Its validation receipt is tied to +the PDF's SHA-256. Both pages were visually reviewed; +the actual tag order, image description and table header scope were also checked. +These checks are **not a screen-reader user test or blanket WCAG certification**. +Changed content or styling needs fresh rendering, inspection and validation. + +Two current runtime boundaries are visible in this example: HTML links become +plain PDF text, so the destination is also written out; and decorative paragraph +text cannot be reliably excluded using HTML ARIA attributes in this pinned alpha, +so redundant folios and card numbers are omitted. The exported HTML retains its +descriptively named, keyboard-focusable link. + +The composition has a fixed two-page print layout. It is a showcase to adapt, +not an arbitrary-length program-directory template. + +## Assets and license + +Python, CSS and SVG source are MIT licensed. DM Serif Display is vendored under +the SIL Open Font License; Inter comes from the pinned Fullbleed wheel and is +copied beside the HTML for browser use. Both font licenses are in `fonts/`. +The renderer records the exact font hashes in `output/render-report.json`. diff --git a/examples/accessible-showcase-alpha6/SOURCE.json b/examples/accessible-showcase-alpha6/SOURCE.json new file mode 100644 index 0000000..f32232f --- /dev/null +++ b/examples/accessible-showcase-alpha6/SOURCE.json @@ -0,0 +1,22 @@ +{ + "repository": "https://github.com/fullbleed-engine/fullbleed-official", + "commit": "9da6a672e81fabebb5733aadcdd932355188bac5", + "path": "examples/accessible_showcase", + "runtime": "2.6.0a6", + "source_files": { + ".gitignore": "7e2f65c80bddb76631f05a448b09378247e7f14af4ade18127925c07a6538dbf", + "LICENSE": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9", + "README.md": "52707f810bc076d0c58e76a9013d821b4cff00ba04fdf45d705b3c114ee9a6c6", + "browser.css": "59876dbb2ddce6f7fc94a435584bc430ce714fc1cc6095adaa4f9e58351369a1", + "fonts/DMSerifDisplay-OFL.txt": "aa49c876a28d8c0b0a3027d6c25b9c607eee8c0fb96165011d42a6ace1408fe1", + "fonts/DMSerifDisplay-Regular.ttf": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616", + "fonts/Inter-OFL.txt": "5dd548d31a85f756e01d63e00d7faf1e324103ed3e9102fcbbabf2cc2db6dd39", + "garden.svg": "b55568259e14cca65522b69dc18c4987b399a1862ec9d99ed1b647b97996043c", + "guide.css": "29cb17b5b0992607a22258a43964828ce1d6ae31c0402d648ff7479fdb5c4e07", + "guide.py": "55398c6d9d6e1614e1d0a17018bc0a73b2998bfdddce90d139beb974d5a6f689", + "render.py": "02ce7065ee7bf3fc319a11bd4f200ffd36c581d9b27b671919d8f074b6f0ad3f", + "requirements-check.txt": "4eadc7023aba8e289cb2e9b4f46eed8c350d9c55500f98c19d938010d46add8d", + "requirements.txt": "058f91f852902be181df6d21cf5e5484cecbc11b14f920337d28d86950cc3d17", + "verify.py": "33ed212e52bc9d3f1ba89e6b8ed835d067099926d08c2abb6b55125281ed11f3" + } +} diff --git a/examples/accessible-showcase-alpha6/browser.css b/examples/accessible-showcase-alpha6/browser.css new file mode 100644 index 0000000..1516d1f --- /dev/null +++ b/examples/accessible-showcase-alpha6/browser.css @@ -0,0 +1,13 @@ +/* Only the styled browser export loads this responsive presentation layer. */ +@media screen { + body { background: #e1e5dd; } + .sheet { margin: 20px auto; } +} +@media screen and (max-width: 800px) { + .sheet { width: auto; height: auto; margin: 0; padding: 24px; } + .cover-title { font-size: 36pt; } + .garden { width: 100%; height: auto; } + .programs, .visit { grid-template-columns: 1fr !important; } + .schedule { table-layout: auto; } + .schedule th, .schedule td { width: auto !important; padding: 8px 5px; font-size: 9pt; } +} diff --git a/examples/accessible-showcase-alpha6/fonts/DMSerifDisplay-OFL.txt b/examples/accessible-showcase-alpha6/fonts/DMSerifDisplay-OFL.txt new file mode 100644 index 0000000..aea163a --- /dev/null +++ b/examples/accessible-showcase-alpha6/fonts/DMSerifDisplay-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. Copyright 2019 Google LLC. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/accessible-showcase-alpha6/fonts/DMSerifDisplay-Regular.ttf b/examples/accessible-showcase-alpha6/fonts/DMSerifDisplay-Regular.ttf new file mode 100644 index 0000000..bc5ed0c Binary files /dev/null and b/examples/accessible-showcase-alpha6/fonts/DMSerifDisplay-Regular.ttf differ diff --git a/examples/accessible-showcase-alpha6/fonts/Inter-OFL.txt b/examples/accessible-showcase-alpha6/fonts/Inter-OFL.txt new file mode 100644 index 0000000..909a6bf --- /dev/null +++ b/examples/accessible-showcase-alpha6/fonts/Inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/accessible-showcase-alpha6/garden.svg b/examples/accessible-showcase-alpha6/garden.svg new file mode 100644 index 0000000..31a0f3f --- /dev/null +++ b/examples/accessible-showcase-alpha6/garden.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/accessible-showcase-alpha6/guide.css b/examples/accessible-showcase-alpha6/guide.css new file mode 100644 index 0000000..fc4500d --- /dev/null +++ b/examples/accessible-showcase-alpha6/guide.css @@ -0,0 +1,38 @@ +/* The same authored class names work in PDF and static HTML exports. */ +@font-face { font-family: Inter; src: url('fonts/Inter-Variable.ttf'); } +@font-face { font-family: 'DM Serif Display'; src: url('fonts/DMSerifDisplay-Regular.ttf'); } +html, body { margin: 0; padding: 0; } +body { font-family: Inter; font-size: 10.5pt; line-height: 1.5; color: #17382e; } +h1, h2, h3, p, ul { margin: 0; } +h1, h2, h3 { font-weight: 400; } +h1, h2 { font-family: 'DM Serif Display'; } +h2 { font-size: 22pt; line-height: 1.15; margin-bottom: 11pt; } +h3 { font-size: 11pt; font-weight: 700; line-height: 1.35; margin-bottom: 7pt; } +strong { font-weight: 700; } +.sheet { position: relative; width: 210mm; height: 296.9mm; padding: 15mm 17mm 17mm; background: #f8f5ec; break-after: page; } +.sheet:last-child { break-after: auto; } +.brand { font-family: 'DM Serif Display'; font-size: 21pt; line-height: 1.1; border-bottom: 1pt solid #17382e; padding-bottom: 10pt; margin-bottom: 21pt; } +.eyebrow { font-size: 9pt; font-weight: 600; letter-spacing: .8pt; text-transform: uppercase; margin-bottom: 10pt; } +.cover-title { font-size: 46pt; line-height: 1.03; max-width: 460pt; margin-bottom: 14pt; } +.intro { font-size: 12pt; line-height: 1.55; max-width: 460pt; } +.garden { display: block; width: 176mm; height: 53mm; margin: 14pt 0 18pt; } +.programs { gap: 14pt; margin-bottom: 19pt; } +.program-card { border-top: 1.5pt solid #a23e24; padding-top: 8pt; } +.program-card p { font-size: 10pt; line-height: 1.5; } +.welcome { padding: 14pt 17pt; background: #e3eadc; } +.welcome h2 { font-size: 19pt; margin-bottom: 6pt; } +.details .eyebrow { padding-top: 2pt; border-bottom: 1pt solid #17382e; padding-bottom: 12pt; margin-bottom: 24pt; } +.page-title { font-size: 35pt; line-height: 1.08; margin-bottom: 13pt; } +.schedule { margin: 16pt 0 20pt; font-size: 10pt; } +.schedule caption { font-size: 9pt; color: #3c5548; padding-bottom: 10pt; } +.schedule th { padding: 10pt 8pt; font-size: 9pt; color: #f8f5ec; background: #17382e; border-bottom: 1pt solid #17382e; } +.schedule td { padding: 10pt 8pt; line-height: 1.5; border-bottom: .6pt solid #738577; } +.schedule tbody tr:nth-child(even) { background: #e9eddf; } +.visit { gap: 24pt; margin-bottom: 18pt; } +ul { padding-left: 13pt; } +li { padding-left: 2pt; margin-bottom: 6pt; } +.closing { padding: 14pt 17pt; background: #17382e; color: #f8f5ec; margin-bottom: 18pt; } +.closing h2 { font-size: 23pt; margin-bottom: 8pt; } +.about h2 { font-size: 17pt; margin-bottom: 7pt; } +.about p { font-size: 9pt; line-height: 1.5; margin-bottom: 8pt; } +a { color: #17382e; text-decoration: underline; text-underline-offset: 2pt; } diff --git a/examples/accessible-showcase-alpha6/guide.py b/examples/accessible-showcase-alpha6/guide.py new file mode 100644 index 0000000..765ef88 --- /dev/null +++ b/examples/accessible-showcase-alpha6/guide.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""A component-authored, tagged community guide. Requires fullbleed==2.6.0a6.""" + +from __future__ import annotations + +from pathlib import Path + +from fullbleed.ui.experimental import ( + Box, Column, DataTable, Document, Grid, Heading, Image, Node, + Page, Paragraph, Section, Strong, component, mm, +) + +ROOT = Path(__file__).resolve().parent +TITLE = "Common Ground: A place to grow, a place to belong" +IMAGE_ALT = ( + "Illustration of a shared garden: three raised planting beds beside a broad, " + "level path, with a bench and a greenhouse." +) +SCHEDULE = ( + {"program": "Garden together", "when": "Saturdays, 10–11:30 am", + "expect": "Plant, water or harvest. Seated tasks are available."}, + {"program": "The shared table", "when": "Saturdays, noon–1 pm", + "expect": "Bring your lunch or share the seasonal soup. Ingredients are listed."}, + {"program": "Make & mend", "when": "First Saturday, 2–3:30 pm", + "expect": "Bring a small repair or try a simple craft. Materials are provided."}, +) + + +@component +def ProgramCard(title, description): + """One reusable card, with its heading in the document's reading order.""" + return Section( + Heading(title, level=3), + Paragraph(description), + class_name="program-card", + ) + + +@component +def BulletList(*items): + # Node is the public escape hatch for semantic elements without a shortcut. + return Node("ul", tuple(Node("li", (item,)) for item in items)) + + +@component +def TextLink(label, href): + return Node("a", (label,), (("href", href),)) + + +@component +def ProgramSchedule(rows): + return DataTable( + rows, + columns=( + Column("Program", lambda row: Strong(row["program"]), width=mm(38)), + Column("When", lambda row: row["when"], width=mm(43)), + Column("What to expect", lambda row: row["expect"]), + ), + caption="Autumn 2026 · Weekly sessions at the Common Ground garden", + class_name="schedule", + ) + + +def build_document(*, browser=False): + styles = (ROOT / "guide.css").read_text(encoding="utf-8") + if browser: + styles += "\n" + (ROOT / "browser.css").read_text(encoding="utf-8") + return Document( + Section( + Paragraph("Common Ground", class_name="brand"), + Paragraph("Community field guide / Autumn 2026", class_name="eyebrow"), + Heading("A place to grow.", Node("br"), "A place to belong.", level=1, + class_name="cover-title"), + Paragraph( + "Good things happen when we make a little room for one another. " + "Come for a quiet hour, a shared meal or something new to learn.", + class_name="intro", + ), + Image(src="garden.svg", alt=IMAGE_ALT, class_name="garden"), + Heading("Find your starting point", level=2), + Grid( + ProgramCard("Grow something", + "A few seeds. A pair of hands. A garden we tend together."), + ProgramCard("Share a table", + "Pull up a chair for conversation and a warm bowl of soup."), + ProgramCard("Make it last", + "Swap a skill, mend a favorite thing or try a small project."), + columns=3, class_name="programs", + ), + Box( + Heading("Every visit starts with hello", level=2), + Paragraph("All sessions are free. No membership, equipment or experience is needed. " + "Come on your own or bring someone with you."), + class_name="welcome", + ), + class_name="sheet cover", + ), + Section( + Paragraph("Your next Saturday", class_name="eyebrow"), + Heading("Make room for a good day.", level=2, class_name="page-title"), + Paragraph("Stay for one session or linger a little longer. You decide how to take part.", + class_name="intro"), + ProgramSchedule(SCHEDULE), + Heading("Plan a comfortable visit", level=2), + Grid( + Section( + Heading("An easy arrival", level=3), + BulletList( + "Enter through the wide gate beside the greenhouse.", + "The main path is firm and level. Seating is available along the way.", + "A welcome volunteer can show you the garden and the accessible restroom.", + ), + ), + Section( + Heading("Choose your pace", level=3), + BulletList( + "Take part seated or standing, or simply watch.", + "Use the quiet corner whenever you would like a break.", + "Ask for written instructions or a demonstration. Both are welcome.", + ), + ), + columns=2, class_name="visit", + ), + Box( + Heading("A small first step is enough", level=2), + Paragraph("You do not have to know anyone, know what to say or stay all day. " + "There is a place for you here."), + class_name="closing", + ), + Section( + Heading("About this guide", level=2), + Paragraph("Common Ground and its programs are fictional. This sample shows how " + "a thoughtfully designed document can carry its structure into PDF and HTML."), + Paragraph(TextLink("Fullbleed documentation: docs.fullbleed.dev", + "https://docs.fullbleed.dev/")), + class_name="about", + ), + class_name="sheet details", + ), + title=TITLE, + lang="en-US", + page=Page(size="A4", margin=mm(0)), + styles=styles, + ) diff --git a/examples/accessible-showcase-alpha6/render.py b/examples/accessible-showcase-alpha6/render.py new file mode 100644 index 0000000..8b83ad4 --- /dev/null +++ b/examples/accessible-showcase-alpha6/render.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Render and export the accessible showcase using only Fullbleed and stdlib.""" + +from __future__ import annotations + +import argparse +import hashlib +from importlib import metadata, resources +import json +from pathlib import Path +import shutil +import subprocess +import sys + +import fullbleed +from fullbleed.ui.experimental import Renderer +from guide import ROOT, build_document + + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +class GuideRenderer(Renderer): + """Register the same named illustration for each isolated engine.""" + + def engine_for(self, document): + engine = super().engine_for(document) + assets = fullbleed.AssetBundle() + assets.add_file(str(ROOT / "garden.svg"), "svg", name="garden.svg") + engine.register_bundle(assets) + return engine + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=ROOT / "output") + args = parser.parse_args() + out = args.out.resolve() + out.mkdir(parents=True, exist_ok=True) + inter = Path(str(resources.files("fullbleed_assets").joinpath("fonts/Inter-Variable.ttf"))) + serif = ROOT / "fonts/DMSerifDisplay-Regular.ttf" + fonts = (inter, serif) + document = build_document() + source = document.compile(a11y_mode="raise", target="pdf") + renderer = GuideRenderer(font_files=[str(font) for font in fonts], pdf_profile="pdfua1") + prepared = document.prepare(renderer, a11y_mode="raise") + pdf = out / "common-ground-guide.pdf" + pdf.write_bytes(prepared.render_pdf()) + replay = document.render_pdf(renderer, a11y_mode="raise") + if replay != pdf.read_bytes(): + raise RuntimeError("PDF replay was not byte-for-byte deterministic") + (out / "source.html").write_text(source.html, encoding="utf-8", newline="\n") + (out / "source.css").write_text(source.css, encoding="utf-8", newline="\n") + browser_document = build_document(browser=True) + for styled, filename in ((True, "guide.html"), (False, "guide-unstyled.html")): + (out / filename).write_text(browser_document.to_html(include_styles=styled, a11y_mode="raise"), + encoding="utf-8", newline="\n") + shutil.copyfile(ROOT / "garden.svg", out / "garden.svg") + (out / "fonts").mkdir(exist_ok=True) + for font in fonts: + shutil.copyfile(font, out / "fonts" / font.name) + for license_file in (ROOT / "fonts").glob("*-OFL.txt"): + shutil.copyfile(license_file, out / "fonts" / license_file.name) + engine = renderer.engine_for(document) + previews = engine.render_finalized_pdf_image_pages_to_dir(str(pdf), str(out / "preview"), 144, "guide") + inspection = fullbleed.inspect_pdf(str(pdf)) + for name, value in (("a11y-source.json", source.a11y_report), + ("inspection.json", inspection), + ("pagination.json", prepared.export_render_time_pagination_trace())): + (out / name).write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + # CLI diagnostics independently exercise the exported HTML/CSS authoring path. + command = [sys.executable, "-I", "-m", "fullbleed", "--json-only", "verify", + "--html", str(out / "source.html"), "--css", str(out / "source.css"), + "--document-title", document.title, "--document-lang", document.lang, + "--pdf-profile", "pdfua1", "--emit-pdf", str(out / "verified.pdf"), + "--fail-on", "overflow", "--fail-on", "missing-glyphs", "--fail-on", "font-subst"] + for asset in (*fonts, ROOT / "garden.svg"): + command.extend(("--asset", str(asset))) + checked = subprocess.run(command, capture_output=True, text=True, encoding="utf-8") + (out / "verify.json").write_text(checked.stdout, encoding="utf-8") + (out / "verify.stderr.txt").write_text(checked.stderr, encoding="utf-8") + verification = json.loads(checked.stdout) + parity = sha(pdf) == sha(out / "verified.pdf") if (out / "verified.pdf").exists() else False + result = dict(schema="fullbleed.accessible_showcase.v1", + ok=checked.returncode == 0 and bool(verification.get("ok")) and parity + and inspection.get("page_count") == 2, + runtime=metadata.version("fullbleed"), pdf=pdf.name, sha256=sha(pdf), + page_count=inspection.get("page_count"), deterministic_replay=True, + html_css_pdf_byte_parity=parity, native_diagnostic_gate=verification.get("ok"), + previews=[Path(path).relative_to(out).as_posix() for path in previews], + fonts=[dict(file=font.name, sha256=sha(font)) for font in fonts], + external_pdfua_validation="Run veraPDF separately; see README.md.") + (out / "render-report.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + print(json.dumps(result), flush=True) + return 0 if result["ok"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/accessible-showcase-alpha6/requirements-check.txt b/examples/accessible-showcase-alpha6/requirements-check.txt new file mode 100644 index 0000000..16ec1fd --- /dev/null +++ b/examples/accessible-showcase-alpha6/requirements-check.txt @@ -0,0 +1,4 @@ +-r requirements.txt +pypdf==6.19.0 +pypdfium2==5.14.0 +Pillow==12.3.0 diff --git a/examples/accessible-showcase-alpha6/requirements.txt b/examples/accessible-showcase-alpha6/requirements.txt new file mode 100644 index 0000000..63061d3 --- /dev/null +++ b/examples/accessible-showcase-alpha6/requirements.txt @@ -0,0 +1 @@ +fullbleed==2.6.0a6 diff --git a/examples/accessible-showcase-alpha6/verify.py b/examples/accessible-showcase-alpha6/verify.py new file mode 100644 index 0000000..f84e040 --- /dev/null +++ b/examples/accessible-showcase-alpha6/verify.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Inspect the actual PDF's structure and optionally run veraPDF's UA-1 profile.""" + +from __future__ import annotations + +import argparse +from collections import Counter, defaultdict +import hashlib +import json +from pathlib import Path +import subprocess + +from pypdf import PdfReader +from guide import IMAGE_ALT, TITLE + + +def normalize(text): + return " ".join(text.split()) + + +def inspect_structure(pdf): + reader = PdfReader(pdf) + catalog = reader.trailer["/Root"] + assert len(reader.pages) == 2 + assert reader.metadata.title == TITLE + assert catalog["/Lang"] == "en-US" + assert catalog["/MarkInfo"]["/Marked"] + assert catalog["/ViewerPreferences"]["/DisplayDocTitle"] + chunks = defaultdict(list) + page_numbers = {} + artifact_text = [] + for page_number, page in enumerate(reader.pages, 1): + page_numbers[page.indirect_reference.idnum] = page_number + stack = [] + + def before(op, args, cm, tm): + if op == b"BDC": + props = args[1].get_object() + mcid = int(props["/MCID"]) if "/MCID" in props else None + if mcid is not None: + parent = next((value for _, value in reversed(stack) if value is not None), None) + assert parent is None, f"Nested MCID on page {page_number}: {parent} -> {mcid}" + stack.append((str(args[0]), mcid)) + elif op == b"BMC": + stack.append((str(args[0]), None)) + elif op == b"EMC": + assert stack, f"Unmatched marked-content end on page {page_number}" + stack.pop() + + def text(value, cm, tm, font, size): + if any(role == "/Artifact" for role, _ in stack): + artifact_text.append(value) + else: + for _, mcid in reversed(stack): + if mcid is not None: + chunks[(page_number, mcid)].append(value) + break + + page.extract_text(visitor_operand_before=before, visitor_text=text) + assert not stack, f"Unclosed marked content on page {page_number}" + roles = Counter() + nodes = [] + leaves = [] + + def walk(value, page_number=None): + value = value.get_object() if hasattr(value, "get_object") else value + if isinstance(value, list): + return " ".join(walk(item, page_number) for item in value) + if isinstance(value, int): + text = normalize("".join(chunks[(page_number, value)])) + leaves.append(dict(page=page_number, mcid=value, text=text)) + return text + if not isinstance(value, dict): + return "" + if "/Pg" in value: + page_number = page_numbers[value.raw_get("/Pg").idnum] + role = str(value.get("/S", "")) + roles[role] += 1 + node = dict(role=role, page=page_number) + nodes.append(node) + node["text"] = normalize(walk(value.get("/K", []), page_number)) + if "/Alt" in value: + node["alt"] = str(value["/Alt"]) + if "/A" in value: + node["attributes"] = {str(k): str(v) for k, v in value["/A"].items()} + return node["text"] + + walk(catalog["/StructTreeRoot"]["/K"]) + headings = [node for node in nodes if node["role"] in {"/H1", "/H2", "/H3"}] + expected = [ + ("/H1", "A place to grow. A place to belong."), + ("/H2", "Find your starting point"), + ("/H3", "Grow something"), ("/H3", "Share a table"), ("/H3", "Make it last"), + ("/H2", "Every visit starts with hello"), + ("/H2", "Make room for a good day."), ("/H2", "Plan a comfortable visit"), + ("/H3", "An easy arrival"), ("/H3", "Choose your pace"), + ("/H2", "A small first step is enough"), ("/H2", "About this guide"), + ] + assert [(node["role"], node["text"]) for node in headings] == expected, headings + assert roles["/Figure"] == 1 + assert next(node["alt"] for node in nodes if node["role"] == "/Figure") == IMAGE_ALT + assert roles["/Table"] == 1 and roles["/TH"] == 3 and roles["/TD"] == 9 + headers = [node for node in nodes if node["role"] == "/TH"] + assert [node["text"] for node in headers] == ["Program", "When", "What to expect"] + assert all(node["attributes"]["/Scope"] == "/Column" for node in headers) + assert roles["/L"] == 2 and roles["/LI"] == 6 + reading = normalize(" ".join(leaf["text"] for leaf in leaves)) + assert "Community field guide · 1" not in reading and "Community field guide · 2" not in reading + assert not any(leaf["text"] in {"01", "02", "03"} for leaf in leaves) + markers = ["Garden together", "The shared table", "Make & mend", "An easy arrival", + "Enter through the wide gate", "A welcome volunteer", "Choose your pace", + "Take part seated", "Ask for written instructions", "A small first step", + "About this guide", "Fullbleed documentation"] + positions = [reading.index(marker) for marker in markers] + assert positions == sorted(positions), "The logical reading order changed" + return dict(ok=True, title=reader.metadata.title, language=catalog["/Lang"], + page_count=len(reader.pages), roles=dict(roles), headings=headings, + nodes=nodes, reading_order=leaves, decorative_text_omitted=True, + marked_content_nesting_valid=True, + pdf_link_annotations=sum(len(page.get("/Annots", [])) for page in reader.pages), + scope="Actual PDF structure, MCID text and semantic order; not an assistive-technology user test.") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=Path(__file__).parent / "output") + parser.add_argument("--verapdf-cp", type=Path, + help="Classpath containing org.verapdf.apps.GreenfieldCliWrapper") + args = parser.parse_args() + out = args.out.resolve() + pdf = out / "common-ground-guide.pdf" + report = inspect_structure(pdf) + report["pdf_sha256"] = hashlib.sha256(pdf.read_bytes()).hexdigest() + (out / "structure.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + summary = dict(ok=True, pdf_sha256=report["pdf_sha256"], structure_passed=True, + external_validation="not run") + if args.verapdf_cp: + command = ["java", "-cp", str(args.verapdf_cp), "org.verapdf.apps.GreenfieldCliWrapper"] + result = subprocess.run(command + ["--format", "json", "--maxfailuresdisplayed", "-1", + "-f", "ua1", str(pdf)], capture_output=True) + (out / "pdfua1.verapdf.json").write_bytes(result.stdout) + (out / "pdfua1.verapdf.stderr.txt").write_bytes(result.stderr) + parsed = json.loads(result.stdout) + validated = parsed["report"]["jobs"][0]["validationResult"][0] + assert result.returncode == 0 and validated["compliant"], validated + assert b"WARNING" not in result.stderr and b"SEVERE" not in result.stderr, result.stderr.decode(errors="replace") + summary["external_validation"] = dict(profile=validated["profileName"], + compliant=validated["compliant"], + stderr_bytes=len(result.stderr), + details=validated["details"], + build=parsed["report"]["buildInformation"]) + (out / "validation.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(json.dumps(summary), flush=True) + + +if __name__ == "__main__": + main() diff --git a/examples/bootstrap-alpha6/LICENSE b/examples/bootstrap-alpha6/LICENSE new file mode 100644 index 0000000..4c99bbd --- /dev/null +++ b/examples/bootstrap-alpha6/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/bootstrap-alpha6/README.md b/examples/bootstrap-alpha6/README.md new file mode 100644 index 0000000..b659cfe --- /dev/null +++ b/examples/bootstrap-alpha6/README.md @@ -0,0 +1,107 @@ +# Static Bootstrap components for Python + +This kit uses the opt-in **Fullbleed 2.6.0a6** Python alpha, from source +`9da6a672e81fabebb5733aadcdd932355188bac5`. It includes the 23 Bootstrap 5.0.0 +component families, forms, layout, content, helpers and checked utility classes. +The API remains experimental under `fullbleed.ui.experimental`; names and +behavior may change. Normal stable installations do not select this alpha. + +All Bootstrap components are static, including HTML export. No Bootstrap +JavaScript is included. PDFs use explicit print representations for controls: +values and selections are printed, with no fillable fields. + +## Install and run + +Use CPython 3.10+ and an isolated virtual environment: + +```console +python -m venv .venv +``` + +Activate with `.venv\Scripts\Activate.ps1` on Windows PowerShell, or +`source .venv/bin/activate` on macOS/Linux. From this folder: + +```console +python -m pip install --pre -r requirements.txt +python first_document.py +python examples/component_bootstrap/main.py --out output --catalog-pdfs +``` + +The exact alpha version is in `requirements.txt`. pip downloads a compatible +wheel from PyPI on supported Windows, macOS and Linux platforms. Other platforms +may build the source distribution and require Rust/Cargo and native build tools. +You can also install it with `python -m pip install --pre "fullbleed==2.6.0a6"`. + +Open `order.pdf`, `order.html` and `order.unstyled.html` for the first example. +Then open `output/report.pdf`, `output/report.styled.html`, and +`output/catalog.html`. The catalog also writes 27 static specimen PDFs. +All data and organizations are fictional. + +## Edit content, structure and appearance + +| Task | Start here | +| --- | --- | +| Add a formatted column | `columns` in `first_document.py`; each `Column` owns its header and cell function | +| Change a card title's color | `.with_parts(title=style(...))`; no internal selector required | +| Reuse a component | Return a node from an ordinary Python function; use `@component` for named diagnostics | +| Apply Bootstrap styling | Named options such as `variant="success"` or literal `class_name="mb-3"` | +| Apply a custom CSS class | `class_name="order-note"` plus `Document(styles={".order-note": style(...)})` | +| Choose a static state | `active`, `open`, `checked`, `value`, and selected options in `catalog.py` | +| Export complete HTML with CSS | `document.to_html(include_styles=True)` | +| Export unstyled HTML | `document.to_html(include_styles=False)`; class names and content remain | +| Export a component fragment | The same `to_html` calls on a node | +| Change the report | `examples/component_bootstrap/main.py` | +| Explore all component families | `examples/component_bootstrap/catalog.py` | + +`class_name` is a literal CSS label. Bootstrap components carry Bootstrap CSS +automatically; your custom classes need explicit rules. Normal CSS specificity +applies, including Bootstrap utilities that use `!important`. An inline style +does not override an important utility declaration. + +Styled HTML embeds collected CSS, not fonts or images. HTML export does not +create a PDF renderer, but still requires the installed Fullbleed distribution. +The PDF examples explicitly select Fullbleed's bundled Inter font. + +Bootstrap CSS includes global reset and typography rules. When inserting a +fragment into an application that already loads Bootstrap, use +`include_styles=False` so the existing stylesheet styles its class names. + +Custom components can declare required `parts=(...)` and conditional +`optional_parts=(...)`. Use `.forward_parts(heading="label")` to expose a +selected child part deliberately. Missing required targets fail at construction; +private child parts remain private. See the +[authoring guide](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.6.0a6/examples/component_recipes/README.md#customize-a-root-or-an-exposed-part). + +Alpha 2 includes the stable 2.5.27 inline SVG styling fixes. Document selectors, +inherited color and named-part opacity apply to supported SVG descendants in +PDFs and styled HTML. Unstyled HTML keeps classes for a host stylesheet. + +Alpha 3 also includes the stable 2.5.28 inset-shadow corrections, including +negative spread, translated holes and clear edges. PDF and HTML remain static. + +Explicit theme and component font choices also replace Bootstrap's variable-based +font default. Register font files with Renderer for PDF output; browser hosts +provide their own fonts. Typography can reflow when the intended font is used. + +Alpha 4 also corrects saved-PDF previews of isolated translucent SVG groups, +applying group opacity once. + +## Scope + +The catalog demonstrates 23 component families plus layout, forms, content and +utilities. It is a bounded set of specimens, not exhaustive testing of every +Bootstrap combination. Tabs do not switch, carousels do not advance, and spinners +do not animate. Modals and other overlays appear in document flow. No browser +pixel parity, accessibility conformance, Sass compiler or standalone HTML-only +package is claimed. + +Read the [API guide](https://github.com/fullbleed-engine/fullbleed-official/blob/9da6a672e81fabebb5733aadcdd932355188bac5/docs/component-bootstrap.md) +and [implementation review](https://github.com/fullbleed-engine/fullbleed-official/blob/9da6a672e81fabebb5733aadcdd932355188bac5/docs/component-authoring-next-iteration.md). +If you try an editing task, share where the API helped or required generated +HTML/internal selectors in the existing +[design discussion](https://github.com/fullbleed-engine/fullbleed-official/discussions/86). +Use small synthetic examples without customer data. + +Alpha 5 incorporates the stable inline-spacing and rich table-cell tagging corrections. Styled components inside table cells retain separate content ownership; the published validation scope remains fixture-specific. + +Alpha 6 adds the stable table pagination fixes to the component preview. The checks cover top- and baseline-aligned long text cells and top-aligned list cells continuing across pages. Middle/bottom-aligned rich-cell continuation remains unsupported. diff --git a/examples/bootstrap-alpha6/examples/component_bootstrap/catalog.py b/examples/bootstrap-alpha6/examples/component_bootstrap/catalog.py new file mode 100644 index 0000000..ddbf8da --- /dev/null +++ b/examples/bootstrap-alpha6/examples/component_bootstrap/catalog.py @@ -0,0 +1,59 @@ +"""Every Bootstrap 5.0.0 component family, authored without HTML strings.""" +from fullbleed.ui.experimental import bootstrap as bs + + +def samples(): + return { + "accordion": bs.Accordion(bs.AccordionItem("Delivery", "Dispatch within two working days."), + bs.AccordionItem("Returns", "Unused items can be returned."), id="delivery"), + "alerts": bs.Alert("The production batch has passed review.", heading="Ready for dispatch", variant="success"), + "badge": bs.Box(bs.Badge("Approved", variant="success"), " ", bs.Badge("Priority", variant="warning", pill=True)), + "breadcrumb": bs.Breadcrumb(bs.BreadcrumbItem("Orders", href="#sample-card"), bs.BreadcrumbItem("April", active=True)), + "buttons": bs.Box(bs.Button("View order", href="#sample-card"), " ", bs.Button("Export copy", outline=True, disabled=True)), + "button-group": bs.ButtonGroup(bs.Button("Summary"), bs.Button("Details", outline=True), label="Document views"), + "card": bs.Card(bs.Paragraph("Printed on recycled stock and packed with care."), title="The Studio Collection", + subtitle="Made for everyday use", header="Product note", footer=bs.Badge("In stock", variant="success")), + "carousel": bs.Carousel(bs.CarouselItem(bs.Box("Selected collection", class_name="p-4 bg-light")), + bs.CarouselItem("Inactive collection")), + "close-button": bs.CloseButton(label="Close preview"), + "collapse": bs.Collapse(bs.Card("The authored expanded state is visible.")), + "dropdowns": bs.Dropdown("Document actions", bs.DropdownHeader("Available formats"), + bs.DropdownItem("Content sample", href="#sample-content"), bs.DropdownDivider(), + bs.DropdownItem("Archive copy", active=True)), + "list-group": bs.Box( + bs.ListGroup(bs.ListGroupItem("Design approved", active=True), + bs.ListGroupItem("Production complete"), bs.ListGroupItem("Dispatch scheduled", variant="success")), + bs.ListGroup(bs.ListGroupItem("First checkpoint"), bs.ListGroupItem("Final checkpoint"), + numbered=True, class_name="mt-3"), + ), + "modal": bs.Modal("Your document is ready for review.", title="Review summary", id="review", + footer=bs.Badge("Ready", variant="success")), + "navs-tabs": bs.Tabs(bs.Tab("Overview", bs.Paragraph("The current record is complete.")), + bs.Tab("History", "Inactive record history"), id="record"), + "navbar": bs.Navbar(bs.NavbarBrand("Northstar Studio"), bs.NavbarNav(bs.NavItem(bs.NavLink("Orders", active=True))), expand=True), + "offcanvas": bs.Offcanvas("Additional production notes.", title="Details", id="details"), + "pagination": bs.Pagination(bs.PageItem("Previous", disabled=True), bs.PageItem("1", active=True), bs.PageItem("2", href="#sample-forms")), + "popovers": bs.Popover("Includes all materials and finishing.", title="Price details"), + "progress": bs.Progress(bs.ProgressBar(75, label="75% complete", variant="success", striped=True)), + "scrollspy": bs.Scrollspy(bs.Heading("Production", level=3, id="production"), bs.Paragraph("The batch is complete."), + navigation=bs.Nav(bs.NavItem(bs.NavLink("Production", href="#production", active=True)), variant="pills")), + "spinners": bs.Box(bs.Spinner(label="Awaiting update"), " ", bs.Spinner(label="Pending", grow=True, variant="success")), + "toasts": bs.Toast("The archive copy was created.", title="Production desk", subtitle="Today"), + "tooltips": bs.Tooltip("Values are shown in US dollars."), + "layout": bs.Container(bs.Row(bs.Col(bs.Card("Eight columns"), span=8), bs.Col(bs.Card("Four columns"), span=4), gutter=3), fluid=True), + "forms": bs.Form( + bs.FloatingLabel(bs.Input(value="Northstar Studio"), "Company", id="company"), + bs.InputGroup(bs.InputGroupText("@"), bs.Input(placeholder="Account", attrs={"aria-label": "Account"})), + bs.Check("Include an archive copy", id="archive", checked=True), + bs.Check("Priority dispatch", id="priority", switch=True, checked=True), + bs.Check("Standard", id="standard", radio=True, name="delivery", checked=True), + bs.Select(bs.Option("Standard stock"), bs.Option("Premium stock"), attrs={"aria-label": "Paper stock"}), + bs.Textarea("Handle with care", rows=2, attrs={"aria-label": "Instructions"}), + bs.Range(value=50, attrs={"aria-label": "Coverage"}), + bs.Feedback("Details confirmed", valid=True, class_name="d-block"), + ), + "content": bs.Box(bs.Blockquote("Considered design, carefully delivered."), + bs.Table(bs.TableHead(bs.TableRow(bs.TableHeader("Item", attrs={"scope": "col"}), bs.TableHeader("Quantity", attrs={"scope": "col"}))), + bs.TableBody(bs.TableRow(bs.TableCell("Notebook"), bs.TableCell("24"))), striped=True, bordered=True)), + "utilities": bs.Box("A styled utility panel", class_name=bs.utilities(p=3, border=True, rounded=3, bg="light", text="primary")), + } diff --git a/examples/bootstrap-alpha6/examples/component_bootstrap/main.py b/examples/bootstrap-alpha6/examples/component_bootstrap/main.py new file mode 100644 index 0000000..5acc17e --- /dev/null +++ b/examples/bootstrap-alpha6/examples/component_bootstrap/main.py @@ -0,0 +1,94 @@ +"""Build a rich report, standalone HTML and a static Bootstrap catalog. + +Run with a wheel containing the experimental Bootstrap kit: + python examples/component_bootstrap/main.py --out target/bootstrap-demo +""" +from __future__ import annotations + +import argparse +from importlib.resources import files +import json +from pathlib import Path + +from fullbleed.ui.experimental import Document, Page, Renderer, Theme, mm, pt, style +from fullbleed.ui.experimental import bootstrap as bs +from catalog import samples + + +def report(): + rows = [ + {"name": "Studio notebooks", "units": 240, "amount": "$3,600.00"}, + {"name": "Cotton stationery sets", "units": 120, "amount": "$2,160.00"}, + {"name": "Archival print collection", "units": 48, "amount": "$2,880.00"}, + ] + metrics = [("408", "Items delivered"), ("$8,640", "Collection revenue"), ("98%", "On-time dispatch")] + return Document( + bs.Box(bs.Text("NORTHSTAR / STUDIO", class_name="fw-bold"), bs.Badge("APRIL 2026", variant="light", class_name="ms-3"), class_name="mb-4"), + bs.Heading("Made with care.\nDelivered with purpose.", level=1, style=style(font_size=pt(32), white_space="pre-line", line_height=1.1)), + bs.Paragraph("A production report for people who care about the details.", class_name="lead text-secondary mb-4"), + bs.Row((bs.Col(bs.Card(bs.Heading(value, level=2, class_name="mb-1"), bs.Text(label), variant="light"), span=4) + for value, label in metrics), gutter=3, class_name="mb-4"), + bs.Heading("The April collection", level=2, class_name="h4"), + bs.DataTable(rows, columns=[bs.Column("Collection", lambda row: row["name"]), + bs.Column("Units", lambda row: row["units"], align="right"), + bs.Column("Revenue", lambda row: row["amount"], align="right")], + striped=True, caption="Completed orders / April 1-30, 2026"), + bs.Row( + bs.Col(bs.Card(bs.Paragraph("Every shipment used recycled packaging. The next collection enters production on May 6."), + title="A lighter footprint", footer=bs.Badge("100% recycled packaging", variant="success")), span=7), + bs.Col(bs.Card(bs.Paragraph("We are three quarters of the way to our spring production target."), + bs.Progress(bs.ProgressBar(75, label="75%", variant="success")), title="On the horizon"), span=5), + gutter=3, class_name="mt-4", + ), + bs.Alert("All figures are final. Prepared for the Northstar operations team.", variant="light", class_name="mt-4 small"), + title="Northstar / April production report", page=Page(size="A4", margin=mm(16)), + theme=Theme(text=style(font_family="Inter, Arial, sans-serif", font_size=pt(10), color="#152c3a", line_height=1.4)), + styles={".card": {"border_color": "#d7e1de"}, ".card-title": {"font_size": "15pt"}}, + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=Path("target/bootstrap-demo")) + parser.add_argument("--catalog-pdfs", action="store_true") + args = parser.parse_args() + args.out.mkdir(parents=True, exist_ok=True) + font = files("fullbleed_assets").joinpath("fonts/Inter-Variable.ttf") + renderer = Renderer(font_files=[str(font)], document_timestamp="2026-04-30T00:00:00Z") + document = report() + document.emit_artifacts(html_path=args.out / "report.html", css_path=args.out / "report.css") + (args.out / "report.styled.html").write_text(document.to_html(include_styles=True), encoding="utf-8", newline="\n") + (args.out / "report.unstyled.html").write_text(document.to_html(include_styles=False), encoding="utf-8", newline="\n") + prepared = document.prepare(renderer) + (args.out / "report.pdf").write_bytes(prepared.render_pdf()) + for index, png in enumerate(prepared.render_image_pages(dpi=100), 1): + (args.out / f"report-{index}.png").write_bytes(png) + trace = prepared.export_render_time_pagination_trace() + (args.out / "report.pagination.json").write_text(json.dumps(trace, indent=2), encoding="utf-8", newline="\n") + catalog = samples() + theme = Theme(text=style(font_family="Inter, Arial, sans-serif", font_size=pt(10))) + gallery = Document((bs.Box(bs.Heading(name.replace("-", " ").title(), level=2), node, + class_name="mb-5", id="sample-" + name) for name, node in catalog.items()), + title="Static Bootstrap 5.0.0 catalog", theme=theme) + (args.out / "catalog.html").write_text(gallery.to_html(include_styles=True), encoding="utf-8", newline="\n") + (args.out / "catalog.unstyled.html").write_text(gallery.to_html(include_styles=False), encoding="utf-8", newline="\n") + if args.catalog_pdfs: + evidence = {} + for name, node in catalog.items(): + specimen = Document(bs.Heading(name.replace("-", " ").title(), level=1), node, + title=name, theme=theme, page=Page(size="A5", margin=mm(12))) + try: + compiled = specimen.prepare(renderer) + (args.out / f"catalog-{name}.pdf").write_bytes(compiled.render_pdf()) + pages = compiled.render_image_pages(dpi=100) + for i, png in enumerate(pages, 1): + (args.out / f"catalog-{name}-{i}.png").write_bytes(png) + evidence[name] = {"pages": len(pages), "trace": compiled.export_render_time_pagination_trace()} + except Exception as error: + evidence[name] = {"error": str(error), "type": type(error).__name__} + (args.out / "catalog.pagination.json").write_text(json.dumps(evidence, indent=2), encoding="utf-8", newline="\n") + print(args.out.resolve()) + + +if __name__ == "__main__": + main() diff --git a/examples/bootstrap-alpha6/first_document.py b/examples/bootstrap-alpha6/first_document.py new file mode 100644 index 0000000..79179a7 --- /dev/null +++ b/examples/bootstrap-alpha6/first_document.py @@ -0,0 +1,32 @@ +from pathlib import Path +from fullbleed.ui.experimental import Document, style +from fullbleed.ui.experimental import bootstrap as bs + +rows = [{"item": "Studio notebook", "quantity": 24, "amount": "$360.00"}] +card = bs.Card( + bs.DataTable( + rows, + columns=[ + bs.Column("Item", lambda row: row["item"]), + bs.Column("Quantity", lambda row: row["quantity"], align="right"), + bs.Column("Amount", lambda row: row["amount"], align="right"), + ], + striped=True, + ), + title="Ready to ship", + header="ORDER / 1048", + footer=bs.Badge("Approved", variant="success"), + class_name="mb-3", +).with_parts(title=style(color="#205b47")) + +document = Document(card, title="Order summary") +Path("order.html").write_text(document.to_html(include_styles=True), encoding="utf-8", newline="\n") +Path("order.unstyled.html").write_text(document.to_html(include_styles=False), encoding="utf-8", newline="\n") + +from importlib.resources import files +from fullbleed.ui.experimental import Renderer, Theme, pt + +document = Document(card, title="Order summary", theme=Theme(text=style(font_family="Inter", font_size=pt(11)))) +font = files("fullbleed_assets").joinpath("fonts/Inter-Variable.ttf") +renderer = Renderer(font_files=[str(font)], document_timestamp="2026-04-30T00:00:00Z") +Path("order.pdf").write_bytes(document.render_pdf(renderer)) diff --git a/examples/bootstrap-alpha6/requirements.txt b/examples/bootstrap-alpha6/requirements.txt new file mode 100644 index 0000000..63061d3 --- /dev/null +++ b/examples/bootstrap-alpha6/requirements.txt @@ -0,0 +1 @@ +fullbleed==2.6.0a6 diff --git a/examples/bootstrap-alpha6/upstream.json b/examples/bootstrap-alpha6/upstream.json new file mode 100644 index 0000000..ecd19d6 --- /dev/null +++ b/examples/bootstrap-alpha6/upstream.json @@ -0,0 +1,167 @@ +{ + "repository": "https://github.com/fullbleed-engine/fullbleed-official", + "commit": "9da6a672e81fabebb5733aadcdd932355188bac5", + "source_files": { + "LICENSE": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9" + }, + "engine_python_files": { + "fullbleed/ui/experimental/__init__.py": "d375915c58573c6769543dc33e2ec87084879412e63ea1bbcf8421458e0085fa", + "fullbleed/ui/experimental/_bridge.py": "d9d63910ad4e19bf63a3bd7c51507ebcc8aab14f86424259052be2f61ef6bf42", + "fullbleed/ui/experimental/_bridge.pyi": "372ce75241bfdadf41a7b38c4f301a35f7968c454f4d8c48fb565845f27754f9", + "fullbleed/ui/experimental/_document.py": "8201b0be994280baefe726100bf11c822ba2c57277050602059c9ca723889cea", + "fullbleed/ui/experimental/_document.pyi": "1112209e898c9bdbb16edb922a4df6c253fcbe3ded88e328b57bd4f26079d022", + "fullbleed/ui/experimental/_resources.py": "89561bebca0065b319f51133d6e9f5a747df32d7183c282aa036e11b751a395d", + "fullbleed/ui/experimental/_schema.py": "bf6463e42f5e8111e20975e949c7997a9da72b0a9996455df5d69d38d4543ca7", + "fullbleed/ui/experimental/_semantics.py": "dad8b9e7293eac421deb9ba9faf2bee24ccfdd69b5043413d053cb00b85883ab", + "fullbleed/ui/experimental/_style.py": "eadd516613985245691b0798361b7df6122c6906443ccefae77e154e3681e226", + "fullbleed/ui/experimental/_style.pyi": "5db65d4a5ff3ebba0cead751cb0bb1b5143d02dfa1981b3a82be3d8f4a4a03c6", + "fullbleed/ui/experimental/_table.py": "2dc2ab734d4f6a0bad6b9d90be29d2eacde50e9bd3ca5847f18392d18474eed4", + "fullbleed/ui/experimental/_table.pyi": "4304062d301270c8e80c454dec76704d84c7a9d5340be656ce035bf86d3d2898", + "fullbleed/ui/experimental/_tree.py": "035158f8115ae8f4da2d864a0a666128cf809b12f65fb4432f6cfd020df451e4", + "fullbleed/ui/experimental/_tree.pyi": "b4474fb9d51ee735c8a749765496d524a50e9dae405a1e5a2b239dfaa532e2d9", + "fullbleed/ui/experimental/bootstrap/__init__.py": "3d38275bdce34ed31cbb959dd8a7f8ce309b77dc3c482fff454718dd404f94d4", + "fullbleed/ui/experimental/bootstrap/__init__.pyi": "a2f96b9d41df8073231c914cef31f497c24974757ab625ae2cdb9a5cd43f7ee9", + "fullbleed/ui/experimental/bootstrap/_components.py": "5f402fe3b810bc5443cea9dd67cde5668923b3c98c20eb241141fdd33efefc7a", + "fullbleed/ui/experimental/bootstrap/_core.py": "d82ec29deec6d8a248b9f9a419bbcb915862393ba1de08d9f28d814249716c99", + "fullbleed/ui/experimental/bootstrap/_forms.py": "bd368525794d02f6ee4ececb1e42b0e16fe104da0579d4966ace0b6f9462963b", + "fullbleed/ui/experimental/bootstrap/_layout.py": "41ce430739224135c028c984a0e578fbbd0726c7dab059c685b0db844af2fe86", + "fullbleed/ui/experimental/bootstrap/_schema.py": "45ca43bcadc62e6b13338868e520f0ac91ce617aae090db9fb8fd2c5329d263e", + "fullbleed/ui/experimental/bootstrap/_utilities.py": "b7b5606b6113acf5de27d72a7ebf46e762a61132a19e954207708a300b47815a", + "fullbleed/ui/experimental/py.typed": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "bootstrap_css_sha256": "04083fcecdd9e19b2acd1e0073437aa07c98d230f0198d7f1fd470ab220fef64", + "first_document_sha256": "33f8bcb22893ec7df5fe6562f0131790dd7006e71afa45abe98d120c20bf2024", + "scope": "Example adaptations and runtime authoring files are identified by canonical source hashes. The alpha installs by explicit PyPI version; published distribution URLs and hashes are retained for verification.", + "adapted_files": { + "examples/component_bootstrap/catalog.py": { + "upstream_sha256": "2cfca9d51a892ac596b87f29bb44c8f84cf364d5933fb8b2b0e67f03bd781b36", + "sha256": "287faf4e607e0e3daba21b952e26dc0acc7033cfe0d07dd0639b0b43a4db7fd3", + "change": "Demo links target other existing catalog sections. The downloadable HTML has no unresolved placeholder anchors." + }, + "examples/component_bootstrap/main.py": { + "upstream_sha256": "1fbbb445e23a2096785b74aa663a5fe29a3da469c789911579d5d01e5dabd1aa", + "sha256": "bcd0eec912b0cd2ecff65d99c04afc907b2fd2ea95f11cabbd43ba0c4d051649", + "change": "Write textual artifacts with explicit LF line endings so downloadable HTML has identical bytes on Windows and Linux." + } + }, + "engine_wheels": { + "package_version": "2.6.0a6", + "release_url": "https://github.com/fullbleed-engine/fullbleed-official/releases/tag/v2.6.0a6", + "index_url": "https://pypi.org/project/fullbleed/2.6.0a6/", + "files": [ + { + "file": "fullbleed-2.6.0a6-cp310-abi3-macosx_10_12_x86_64.whl", + "url": "https://files.pythonhosted.org/packages/d2/c6/3286f3baa5777ca5515b7ca6acaa840cca82775c0c09a406dd089512232c/fullbleed-2.6.0a6-cp310-abi3-macosx_10_12_x86_64.whl", + "bytes": 7648571, + "sha256": "28dc8ee1f634561dc8b170a4e83213dfa1668f0e2b715b1a31f9223bd9f9fd51", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-macosx_11_0_arm64.whl", + "url": "https://files.pythonhosted.org/packages/5f/f8/c27385503abce24d01eaee71a3c2888cd49eef91d679d3845431d126b83d/fullbleed-2.6.0a6-cp310-abi3-macosx_11_0_arm64.whl", + "bytes": 7247075, + "sha256": "47537d51432de97e51cf20ea99f908f4117966b21f8c2562b3a222451b874a49", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "url": "https://files.pythonhosted.org/packages/38/96/73f26c37a5ee80a1867d56656f26f8d5ea79f90f9ad49377a20c6723cff6/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "bytes": 7391630, + "sha256": "c9a7b6725d610847774937e3df6b27cb6933a299066a74ee8d2b5e065e959375", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", + "url": "https://files.pythonhosted.org/packages/0b/b5/65e81e3b982f4b7622cd23a93135a411673858cadcc91bfe25602a3d5b94/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", + "bytes": 9203264, + "sha256": "4c2f6344834caf5fb823ded48e13361d27f1b69d89526a66d8069fe8905f96a4", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", + "url": "https://files.pythonhosted.org/packages/77/5e/f18111ff6846608dd6317ffb43ee576597dfb03d635c9f097ba09cf50265/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", + "bytes": 9932252, + "sha256": "2f2796a905a17860c408c3d75370a867820ba2d6e801f76126b2aecb78530dd3", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "url": "https://files.pythonhosted.org/packages/dd/55/20f7adec6d0298ee238660c7ab15ac22ea23faaf28f889cc572f84d7b2c3/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "bytes": 8152134, + "sha256": "3ae86cd57d5899387d69540e3b74602d7a410f4d8af3e22a4ed8c18aaa170b8b", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "url": "https://files.pythonhosted.org/packages/02/15/151d678523a33790b0b309f914d03b474a2c772c77bd86a8ccf61696771e/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "bytes": 8218561, + "sha256": "335eaa63f6b9f688f2ab0d2ef0c96e746616cd279227b92c25ce5d2809a03694", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "url": "https://files.pythonhosted.org/packages/2f/34/cf185658535470f7f0361326de2add1dc30b1afd6fcb3b5fae5541f889b4/fullbleed-2.6.0a6-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "bytes": 7807566, + "sha256": "c081ea0f98f1fa1637d49c20de70a4876ccddffa7d376c4c58b6f0819a7c2817", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_aarch64.whl", + "url": "https://files.pythonhosted.org/packages/3c/0a/2802eacf894b188307cedc6b94eda7f6f3bf24524ae40c4d7f9a3a58f73b/fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_aarch64.whl", + "bytes": 7368110, + "sha256": "7c87201e0bfa95ce193893cb5d05852916883aeb7f304704a35bb7a3e37cb987", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_armv7l.whl", + "url": "https://files.pythonhosted.org/packages/64/b3/2044f1829b17349a49945695073a939b32a9eff0ce875876445507e73d23/fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_armv7l.whl", + "bytes": 9250127, + "sha256": "4bab4c563e8ac3aca6f6277dafecc70610f8a668a08f3b1d76a808f55e78a3b1", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_i686.whl", + "url": "https://files.pythonhosted.org/packages/63/c9/2fe1e0ba2b7268c74cbdd7ec084ab240ef83fd7f9d33c3863cc661cd8aa7/fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_i686.whl", + "bytes": 9901728, + "sha256": "da351fcac9916ed342033313d8dc229f0a3f33955d64dab9ce9ebdb948c3a23a", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_x86_64.whl", + "url": "https://files.pythonhosted.org/packages/fc/cb/43072820d6633a948ba85be48532015e50dc2d2b6c641deeb190fc486c44/fullbleed-2.6.0a6-cp310-abi3-musllinux_1_2_x86_64.whl", + "bytes": 7802074, + "sha256": "d31f8c82a7717daacb17d2e79fd33ab9a27a59e713b0be58a82ad6f21f43ef8c", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-win32.whl", + "url": "https://files.pythonhosted.org/packages/d4/6e/d56eb7b6cb3ce5422cc68bc6be572543e7a438233578e800a98e736aa53e/fullbleed-2.6.0a6-cp310-abi3-win32.whl", + "bytes": 9137566, + "sha256": "d675e696c1f2a848e4178e9d26b6c47212fd4a37107ec75fed659f20365f719a", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-win_amd64.whl", + "url": "https://files.pythonhosted.org/packages/1b/01/8451c57827252dd082c4c12fc9fcb28f44dd0419a8afda182784880abdcf/fullbleed-2.6.0a6-cp310-abi3-win_amd64.whl", + "bytes": 7971040, + "sha256": "d163fc15bdb1c3a950a94b7b72f1d7044263d29cc9804f2fce87f2a482164293", + "kind": "wheel" + }, + { + "file": "fullbleed-2.6.0a6-cp310-abi3-win_arm64.whl", + "url": "https://files.pythonhosted.org/packages/83/5d/33c5bbd45675c14e9075a14183329ca62f1440f5dc4b7c70eac137389a19/fullbleed-2.6.0a6-cp310-abi3-win_arm64.whl", + "bytes": 7490997, + "sha256": "541ca7f4f8148a72bd132f6c96ff79894e60aab409c96d44c96d03950e64dde4", + "kind": "wheel" + } + ] + }, + "publication_verified": true, + "engine_sdist": { + "file": "fullbleed-2.6.0a6.tar.gz", + "url": "https://files.pythonhosted.org/packages/7e/99/49d755a15f27fa849efbd0f9b5b23da1ec667b04e32b1f95f1f7d7a70e9b/fullbleed-2.6.0a6.tar.gz", + "bytes": 4889270, + "sha256": "2083a363ca1b481121724ed63f9573b5bce523c5d573ff201f77b1795520ff9b", + "kind": "sdist" + } +} diff --git a/examples/component-notebook-alpha6/bootstrap_components.ipynb b/examples/component-notebook-alpha6/bootstrap_components.ipynb new file mode 100644 index 0000000..f899cd5 --- /dev/null +++ b/examples/component-notebook-alpha6/bootstrap_components.ipynb @@ -0,0 +1,330 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "welcome", + "metadata": {}, + "source": [ + "# Make a PDF from Python components\n", + "\n", + "Edit an order, customize its cards and table, and export a PDF plus styled\n", + "and unstyled HTML. You write Python values and components; no HTML strings\n", + "are needed. All sample data is fictional.\n", + "\n", + "**In Colab:** choose **Runtime → Run all**, sign in if prompted, and use a\n", + "standard CPU runtime. No GPU or paid plan is needed for this example;\n", + "[Colab availability and limits](https://research.google.com/colaboratory/faq.html)\n", + "still apply. Runtime files are temporary, so download your results before\n", + "leaving. Local Jupyter also works.\n", + "\n", + "**Experimental:** this notebook pins Fullbleed **2.6.0a6**. The component API\n", + "may change and has documented CSS limits. Bootstrap components are static\n", + "in both PDF and HTML; no Bootstrap JavaScript is included." + ] + }, + { + "cell_type": "markdown", + "id": "install-help", + "metadata": {}, + "source": [ + "## 1. Install the tested alpha\n", + "\n", + "Run this before importing Fullbleed. If you already imported a different\n", + "version in this notebook session, restart its kernel/runtime first.\n", + "The wheel supplies the engine and font; Jupyter supplies display tools." + ] + }, + { + "cell_type": "code", + "id": "install-fullbleed", + "metadata": { + "tags": [ + "install-fullbleed" + ] + }, + "source": [ + "%pip install --quiet --only-binary=:all: \"fullbleed==2.6.0a6\"" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "data-help", + "metadata": {}, + "source": [ + "## 2. Make the order yours\n", + "\n", + "Change a name, quantity or price, then rerun this cell and the cells below it.\n", + "Prices are decimal strings; this example omits tax, shipping and discounts.\n", + "Try `items=[]` to see the empty state, or add rows to explore pagination." + ] + }, + { + "cell_type": "code", + "id": "order-data", + "metadata": {}, + "source": [ + "order = {\n", + " \"number\": \"1048\",\n", + " \"customer\": \"Moss & Fern Studio\",\n", + " \"collection\": \"The autumn collection\",\n", + " \"status\": \"Ready to ship\",\n", + " \"items\": [\n", + " {\"item\": \"Studio notebooks\", \"quantity\": 24, \"unit_price\": \"15.00\"},\n", + " {\"item\": \"Cotton stationery sets\", \"quantity\": 12, \"unit_price\": \"18.00\"},\n", + " {\"item\": \"Archival prints\", \"quantity\": 8, \"unit_price\": \"60.00\"},\n", + " ],\n", + "}" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "styles-help", + "metadata": {}, + "source": [ + "## 3. Choose the accent and type size\n", + "\n", + "These values feed ordinary Python styles. Later, `.with_parts(...)` styles\n", + "a card's own title and body. The custom `dispatch-note` class gets its rule\n", + "from `Document(styles=...)`; a class name alone does not create a style." + ] + }, + { + "cell_type": "code", + "id": "style-controls", + "metadata": {}, + "source": [ + "accent = \"#205b47\"\n", + "title_size = 30" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "components-help", + "metadata": {}, + "source": [ + "## 4. Compose reusable components\n", + "\n", + "`Panel` supplies reusable card defaults. `Metric` is an ordinary Python\n", + "function. `DataTable` builds its structure from rows and column functions.\n", + "Bootstrap classes such as `mb-4` come with the components; your custom\n", + "classes need their own rules. Text values are escaped automatically.\n", + "\n", + "A column callback can return a component: `Strong(row[\"item\"])` emphasizes\n", + "each item inside its table cell. Return text or compose other components\n", + "without constructing an HTML string.\n" + ] + }, + { + "cell_type": "code", + "id": "components", + "metadata": {}, + "source": [ + "from decimal import Decimal\n", + "from fullbleed.ui.experimental import Document, Page, Strong, Theme, mm, pt, style, styled\n", + "from fullbleed.ui.experimental import bootstrap as bs\n", + "\n", + "def money(value):\n", + " return f\"USD {value:,.2f}\"\n", + "\n", + "Panel = styled(bs.Card, border_color=\"#d7e1de\", border_radius=\"8px\")\n", + "\n", + "def Metric(value, label):\n", + " return Panel(\n", + " bs.Heading(value, level=2, class_name=\"mb-1\", style=style(color=accent)),\n", + " bs.Text(label, class_name=\"text-secondary\"),\n", + " )\n", + "\n", + "def OrderSummary(data):\n", + " rows = [dict(row, amount=Decimal(row[\"unit_price\"]) * row[\"quantity\"])\n", + " for row in data[\"items\"]]\n", + " total = sum((row[\"amount\"] for row in rows), Decimal(\"0.00\"))\n", + " units = sum(row[\"quantity\"] for row in rows)\n", + " details = bs.DataTable(\n", + " rows,\n", + " columns=[\n", + " bs.Column(\"Item\", lambda row: Strong(row[\"item\"])),\n", + " bs.Column(\"Quantity\", lambda row: row[\"quantity\"], align=\"right\"),\n", + " bs.Column(\"Amount\", lambda row: money(row[\"amount\"]), align=\"right\"),\n", + " ],\n", + " striped=True,\n", + " caption=\"Order contents · USD\",\n", + " ) if rows else bs.Alert(\"No items in this order.\", variant=\"light\")\n", + " return Document(\n", + " bs.Box(bs.Text(\"NORTHSTAR / STUDIO\", class_name=\"fw-bold\"),\n", + " bs.Badge(data[\"status\"], variant=\"light\", class_name=\"ms-3\"),\n", + " class_name=\"mb-4\"),\n", + " bs.Heading(data[\"collection\"], level=1,\n", + " style=style(font_size=pt(title_size), color=accent, line_height=1.1)),\n", + " bs.Paragraph(f'Prepared for {data[\"customer\"]}', class_name=\"lead mb-4\"),\n", + " bs.Row(\n", + " bs.Col(Metric(str(units), \"Items in this order\"), span=5),\n", + " bs.Col(Metric(money(total), \"Order total\"), span=7),\n", + " gutter=3, class_name=\"mb-4 mx-0\",\n", + " ),\n", + " Panel(details, title=f'Order {data[\"number\"]}',\n", + " footer=\"Made with care. Packed with purpose.\").with_parts(\n", + " title=style(color=accent, font_size=pt(16)),\n", + " body=style(padding=\"1.25rem\"),\n", + " ),\n", + " bs.Alert(\"Please retain this copy with your shipment.\",\n", + " class_name=\"dispatch-note mt-4\"),\n", + " title=f'Order {data[\"number\"]}',\n", + " page=Page(size=\"A4\", margin=mm(16)),\n", + " theme=Theme(text=style(font_family=\"Inter, Arial, sans-serif\", font_size=pt(10),\n", + " color=\"#18382f\", line_height=1.4)),\n", + " styles={\".dispatch-note\": style(color=accent, border_color=accent,\n", + " background_color=\"#f0f5f2\")},\n", + " )\n", + "\n", + "document = OrderSummary(order)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "render-help", + "metadata": {}, + "source": [ + "## 5. Render and export\n", + "\n", + "The PDF renderer receives the bundled Inter font file explicitly. A CSS\n", + "family name does not load a font. Styled HTML keeps the family declaration;\n", + "its browser host supplies Inter separately, or uses the Arial/sans-serif fallback.\n", + "\n", + "`include_styles=False` removes generated and inline styling but keeps classes\n", + "for a host application's CSS. Both export modes also work on an individual\n", + "component, such as `Metric(\"44\", \"Items\").to_html(include_styles=False)`.\n", + "Styled Bootstrap fragments include global Bootstrap rules; use unstyled\n", + "fragments when the host page already provides its stylesheet." + ] + }, + { + "cell_type": "code", + "id": "render-export", + "metadata": {}, + "source": [ + "from importlib.resources import files\n", + "from pathlib import Path\n", + "from fullbleed.ui.experimental import Renderer\n", + "\n", + "output = Path(\"output\")\n", + "output.mkdir(exist_ok=True)\n", + "font = files(\"fullbleed_assets\").joinpath(\"fonts/Inter-Variable.ttf\")\n", + "renderer = Renderer(font_files=[str(font)],\n", + " document_timestamp=\"2026-10-01T00:00:00Z\")\n", + "prepared = document.prepare(renderer)\n", + "pdf_path = output / \"order.pdf\"\n", + "pdf_path.write_bytes(prepared.render_pdf())\n", + "(output / \"order.styled.html\").write_text(\n", + " document.to_html(include_styles=True), encoding=\"utf-8\", newline=\"\\n\")\n", + "(output / \"order.unstyled.html\").write_text(\n", + " document.to_html(include_styles=False), encoding=\"utf-8\", newline=\"\\n\")\n", + "print(f\"Saved {pdf_path} and both HTML exports.\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "id": "preview", + "metadata": {}, + "source": [ + "from IPython.display import Image, display\n", + "\n", + "preview_paths = []\n", + "for number, png in enumerate(prepared.render_image_pages(dpi=110), start=1):\n", + " path = output / f\"order-page-{number}.png\"\n", + " path.write_bytes(png)\n", + " preview_paths.append(path)\n", + " display(Image(data=png, width=650))\n", + "print(f\"{len(preview_paths)} page(s). Review every page before using the document.\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "download-help", + "metadata": {}, + "source": [ + "## 6. Download your files\n", + "\n", + "This ZIP contains the current PDF, both HTML exports, and every current page\n", + "preview. In Colab the next cell starts the download. In local Jupyter it\n", + "displays a link. The runtime's `output/` folder also contains the individual\n", + "files. If you edit the order or styles, rerun from that edit through this cell." + ] + }, + { + "cell_type": "code", + "id": "download", + "metadata": {}, + "source": [ + "import sys\n", + "from zipfile import ZipFile, ZIP_DEFLATED\n", + "\n", + "bundle_path = output / \"component-order.zip\"\n", + "current_files = [pdf_path, output / \"order.styled.html\",\n", + " output / \"order.unstyled.html\", *preview_paths]\n", + "with ZipFile(bundle_path, \"w\", compression=ZIP_DEFLATED) as bundle:\n", + " for path in current_files:\n", + " bundle.write(path, arcname=path.name)\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import files\n", + " files.download(str(bundle_path))\n", + "else:\n", + " from IPython.display import FileLink\n", + " display(FileLink(str(bundle_path)))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "next-steps", + "metadata": {}, + "source": [ + "## Keep composing\n", + "\n", + "- [Component guide and catalog](https://docs.fullbleed.dev/guides/bootstrap-components-preview/)\n", + " explain class names, named parts, fonts, static states and current limits.\n", + "- [Northstar and Common Ground source kits](https://docs.fullbleed.dev/guides/showcase-authoring/)\n", + " show a complete invoice and illustrated report in components and HTML/CSS.\n", + "- [The authored accessible guide](https://docs.fullbleed.dev/guides/accessible-showcase/)\n", + " includes the exact PDF, semantic source and scoped validation receipts.\n", + "- [Share an authoring example](https://github.com/fullbleed-engine/fullbleed-official/discussions/86)\n", + " if a composition or styling task feels awkward. Include the small component,\n", + " expected result and Fullbleed version; use fictional data in public posts.\n", + "- [Stable Python guide](https://docs.fullbleed.dev/guides/python-components/)\n", + " documents the established API separately from this alpha.\n", + "\n", + "The notebook and Fullbleed are MIT licensed. This example does not establish\n", + "complete Bootstrap CSS, browser, accessibility or PDF conformance." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "colab": { + "name": "bootstrap_components.ipynb", + "provenance": [] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/showcase-authoring-alpha6/LICENSE b/examples/showcase-authoring-alpha6/LICENSE new file mode 100644 index 0000000..4c99bbd --- /dev/null +++ b/examples/showcase-authoring-alpha6/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/showcase-authoring-alpha6/README.md b/examples/showcase-authoring-alpha6/README.md new file mode 100644 index 0000000..08933ba --- /dev/null +++ b/examples/showcase-authoring-alpha6/README.md @@ -0,0 +1,45 @@ +# One design, two authoring paths + +Recreate Northstar Studio and Common Ground with HTML/CSS or Python components. +The component API is an opt-in alpha. Python 3.10+ is required. + +From this extracted directory, preferably in a fresh virtual environment: + +```sh +python -m pip install -r requirements.txt +python examples/design_showcase/compare_authoring.py --out output +``` + +Open `output/html/invoice/invoice.pdf` and +`output/components/invoice/invoice.pdf`, then the corresponding `report` PDFs. +`output/comparison.json` records four identical page PNGs, page counts, PDF hashes, +replay checks, font diagnostics, and styled/unstyled HTML export checks. + +The invoice PDFs are byte-identical on the pinned runtime. Common Ground's SVG +nodes introduce extra graphics-state scopes, so its PDF bytes differ while all +three pages have identical pixels in the native preview and independent PDFium +checks. The historical website PDFs were made on 2.4.0; this comparison renders +both source paths on 2.6.0a6. + +Read `examples/design_showcase/README.md` for components, styling, direct PDF and +HTML export, fixed page limits, font licenses, and the four original HTML projects. +The direct component example is run from `examples/design_showcase/`. + +For automatic PDF previews while editing, run: + +```sh +python examples/design_showcase/preview.py +``` + +Open the local URL printed in the terminal, then save a Python component, CSS, +or JSON data edit. The page refreshes automatically and provides PDF plus HTML +downloads. Use `--document report` for Common Ground or `--authoring html` for +the original template path. Errors retain a clearly labeled last good preview; +saving a correction retries. Ctrl-C stops the server. Download anything you +want to keep before stopping it. + +This optional development helper uses only the pinned Fullbleed wheel and the +Python standard library. It executes the local source and binds to 127.0.0.1. +The comparison command above remains the explicit matching-output check. + +Guide: https://docs.fullbleed.dev/guides/showcase-authoring/ diff --git a/examples/showcase-authoring-alpha6/SOURCE.json b/examples/showcase-authoring-alpha6/SOURCE.json new file mode 100644 index 0000000..1640cf3 --- /dev/null +++ b/examples/showcase-authoring-alpha6/SOURCE.json @@ -0,0 +1,39 @@ +{ + "schema": "fullbleed.showcase-authoring.source.v1", + "source_commit": "9da6a672e81fabebb5733aadcdd932355188bac5", + "repository": "https://github.com/fullbleed-engine/fullbleed-official", + "engine_version": "2.6.0a6", + "source_files": { + "examples/design_showcase/.gitattributes": "3b2d2f910738670779137ecce4829c9d5e1a211d2b1174afb01ba87f06812564", + "examples/design_showcase/.gitignore": "7e2f65c80bddb76631f05a448b09378247e7f14af4ade18127925c07a6538dbf", + "examples/design_showcase/README.md": "a6463c5e9852adad8f69b23a8f986bbfe18eb3ef9676728b937c253cd3804f7a", + "examples/design_showcase/common.css": "59ed65ebed86dcebe554d6d2d48c53e0e75e67293e99f537fc767a45db1a84c7", + "examples/design_showcase/compare_authoring.py": "01bce371d15aac6821e3a6d00cf005d1c05927a51c5be5958100b1020c31c819", + "examples/design_showcase/component_primitives.py": "7cd0283d64abdb9d42076a5476eae83f2fdc6850fa9b5b88622542c365b16333", + "examples/design_showcase/data.json": "46cc3ad7cf6560349a41f8ba82bc9822c16176177f3c1236711fa28cb0baa13a", + "examples/design_showcase/fonts/BebasNeue-OFL.txt": "72082f6cb4d04be2ecf7cc7d9e1e7d73787f0af8a5a278a47cade70c16b78341", + "examples/design_showcase/fonts/BebasNeue-Regular.ttf": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73", + "examples/design_showcase/fonts/DMSerifDisplay-Italic.ttf": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45", + "examples/design_showcase/fonts/DMSerifDisplay-OFL.txt": "a3e5cdd67d4571dd0a24fcc968de0efde7ae97ef752daf0906e4767619dd7231", + "examples/design_showcase/fonts/DMSerifDisplay-Regular.ttf": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616", + "examples/design_showcase/fonts/sources.json": "01c23f8e23d039ef6a9945183ff98fd0022975227ae949de4f1669f06ad18db7", + "examples/design_showcase/invoice.css": "4d9d4708b861dd2c43600dc695552c88a445517994a52fc817ed242aac9eaa25", + "examples/design_showcase/invoice.html": "448051114ebd9f641b9232437a8bcd9f94f6460aa1656ccaed74912b0fe0d359", + "examples/design_showcase/invoice_components.py": "cfcf0124615c43d63620b550ea8a308ef364da4b18b96a7c48025345cd512ee4", + "examples/design_showcase/landscape.svg": "a0a997dc5f04b3842830e9ba6610365c4e62a0a32e13619a66c4ea25cca3e488", + "examples/design_showcase/notice.css": "391f879bf794d55b29748147a10526ee493032cea46dae15296858951d5c0afc", + "examples/design_showcase/notice.html": "9596597db1e2e6942f86b8422d646a68342af6a2bc9b82e87229f26936ccdaf7", + "examples/design_showcase/preview.html": "b79af54a21fc5dba7c344be9a7eeb359e0de7d514d8e239ec41a798872d58535", + "examples/design_showcase/preview.js": "089c951807be3c7f99a3a8bf186a7af250e2363d64aba9c63bca8f2d29999b61", + "examples/design_showcase/preview.py": "d4f072007687ff628eb8e932b3ab17c7faf3adbe334d1e561b872680f6ce4fd1", + "examples/design_showcase/render.py": "c66c04fa37c9fdc9cff733b214d5514f9f134c45ddfdda1e6d7a10c8f248d483", + "examples/design_showcase/report.css": "33de04b117f1b0c99298c22beeed184429de748c729f78d183ec9b570e7e8f38", + "examples/design_showcase/report.html": "352d7174f3c42b111a69686cf41bf18253635a1f925f96e0d7d967067fc9c753", + "examples/design_showcase/report_components.py": "6caa62f11d124bc504990df5972230523a1b962619195043309b3c98a2803cd1", + "examples/design_showcase/requirements-components.txt": "058f91f852902be181df6d21cf5e5484cecbc11b14f920337d28d86950cc3d17", + "examples/design_showcase/statement.css": "c8ad4a1606bcab299202f750263af7bedf567d7b34164bda898fa01814ffd4a3", + "examples/design_showcase/statement.html": "b3b597349e474204a72398052bff19486d8a551562568e051375e03871b4c096", + "tools/check_showcase_preview.py": "125ab4b4b3836bd5d707d138eaf6a14a713fcce54fe32052efada398d49cbc19", + "tools/check_showcase_preview_browser.py": "7d61236aecde53dd0cb11ed94f8ffb1ec7430b5e64db2251dc5daed860b51b58" + } +} diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/.gitattributes b/examples/showcase-authoring-alpha6/examples/design_showcase/.gitattributes new file mode 100644 index 0000000..2964500 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/.gitattributes @@ -0,0 +1,2 @@ +# Keep upstream font licenses byte-for-byte, including their original whitespace. +fonts/*-OFL.txt -text -whitespace diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/.gitignore b/examples/showcase-authoring-alpha6/examples/design_showcase/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/README.md b/examples/showcase-authoring-alpha6/examples/design_showcase/README.md new file mode 100644 index 0000000..a029e5b --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/README.md @@ -0,0 +1,181 @@ +# Designed document showcase + +Four fictional document families with complete Python, HTML/CSS, data, and font +sources. [Open the published PDFs and previews](https://docs.fullbleed.dev/examples/). + +| Example | Output | Design and rendering features | +| --- | --- | --- | +| Northstar Studio invoice | 1 page | Three embedded typefaces, grid, ruled line items, decimal totals, a high-contrast total panel | +| Common Ground review | 3 pages | Editorial cover, native SVG landscape, gradient chart bars, data-driven allocation shares, explicit page composition | +| Riverton service notice | 1 page | Cobalt palette, prominent schedule, semantic headings and lists, `pdfua1` profile | +| Hillside member statements | 3 records / 3 pages | Serif typography, a lilac balance panel, contribution charts, compiled reflow bindings | + +## Run + +From the repository root, with Python 3.10 or newer: + +```bash +python -m pip install fullbleed==2.4.0 +python examples/design_showcase/render.py --out output/design-showcase +``` + +The published examples were generated with the public 2.4.0 wheel. The script also +runs against the installed wheel in CI. No additional Python packages are needed. +Use `--only invoice`, `--only report`, `--only notice`, or `--only statements` to +render one family. PNG previews default to 120 DPI; change this with `--dpi`. + +Each output directory contains a PDF, expanded HTML/CSS, PNG previews, and JSON +inspection results. The statements directory also contains its binding data and +compiled-template statistics. `verification.json` records the engine version, +font hashes, PDF hashes, page counts, and the checks performed. + +## Make it yours + +Edit `data.json` for invoice items, report metrics and allocations, and member +records. Edit each HTML/CSS pair for the page composition. Shared page dimensions, +typographic helpers, grids, and table rules live in `common.css`. Invoice totals +use Python `Decimal`; report allocation shares and chart heights come from data. + +These are deliberately composed A4 pages with fixed page budgets. The report has +three explicit pages; it is not an arbitrary-length report template. Review every +page after adding content, changing fonts, or increasing the number of rows. + +The statements use `render_pdf_reflow_bindings_to_file`, with one record per page +for this dataset. They use solid panel and chart fills for the compiled reflow +path. The report demonstrates gradients through the ordinary HTML/CSS path. + +## Fonts and artwork + +Inter is explicitly loaded from the installed Fullbleed wheel. Bebas Neue and DM +Serif Display are included in `fonts/`, together with their SIL Open Font License +texts. `fonts/sources.json` records the pinned upstream URLs and SHA-256 hashes. +The landscape is an editable vector drawing in `landscape.svg`. + +The published previews are rendered from the finalized PDFs. Names, addresses, +organizations, financial figures, and service information are fictional. + +## Verification scope + +The script checks expected page counts, required text and record order, internal +PDF inspection, and matching PDF hashes on replay. The ordinary examples and a +first-record statement proof also pass the installed CLI's overflow, +missing-glyph, and font-substitution gates. The final compiled statement pages +are checked for content and replay consistency and must be visually reviewed. +CLI overflow checks do not establish that two elements never overlap. + +The notice uses `pdfua1` and includes tags, language, and profile metadata. These +checks do **not** establish complete accessibility or independent standards +conformance. Validate the exact delivered artifact with the applicable automated +and human checks before making such a claim. + +## One design, two authoring paths + +Northstar and Common Ground also have an experimental component version: + +- `invoice_components.py`: `InvoiceField`, `InvoiceItems`, `AmountDue`, and the complete invoice. +- `report_components.py`: reusable report pages, `Metric`, `ParticipationChart`, and `AllocationTable`. +- `component_primitives.py`: shared line breaks, folios, SVG asset loading, and document setup. + +The component functions author content in Python with `DataTable`/`Column`, child +composition and named styling parts. They share `common.css`, the family CSS, +`data.json`, vector artwork and explicitly registered fonts with the original HTML +path. `class_name="kicker"` attaches a class; its appearance comes from the shared +`.kicker` rule passed to `Document(styles=...)`. `style=` changes a root; +`.with_parts(...)` changes the declared internal targets. No Bootstrap stylesheet +is needed for this custom design. + +Install the opt-in alpha in a separate environment and compare both paths: + +```sh +python -m pip install -r examples/design_showcase/requirements-components.txt +python examples/design_showcase/compare_authoring.py --out target/showcase-comparison +``` + +The comparison renders and verifies both versions with the same installed engine. +It requires identical page PNG bytes at 120 DPI, checks one invoice page and three +report pages, retains overflow/font diagnostics, and checks deterministic replay. +`--only invoice` or `--only report` limits the comparison. Both versions remain fixed +A4 compositions; substantial content growth needs a layout adjustment. + +The checked 2.6.0a6 result has identical invoice PDF bytes. All four page images +match; Common Ground's PDF bytes differ because component-authored SVG nodes carry +extra graphics-state scopes. Its page appearance also matches under independent +PDFium rendering at 144 DPI. This compares the two paths on 2.6.0a6, not byte identity +with the historical 2.4.0 website downloads. + +Use the components directly from this directory: + +```python +from pathlib import Path +from fullbleed.ui.experimental import Renderer +from invoice_components import northstar_invoice +from render import DATA, FONTS + +invoice = northstar_invoice(DATA["invoice"]) +renderer = Renderer(font_files=[str(path) for path in FONTS]) +Path("invoice.pdf").write_bytes(invoice.render_pdf(renderer)) +Path("invoice.html").write_text(invoice.to_html(include_styles=True), encoding="utf-8") +Path("invoice.unstyled.html").write_text(invoice.to_html(include_styles=False), encoding="utf-8") +``` + +Styled export embeds CSS; unstyled export retains the authored classes and content +while omitting styles. Both are static HTML. Browser use also needs the named fonts +made available (for example through your application's `@font-face` rules); explicit +PDF font registration is separate. The comparison also verifies that direct component +rendering and the embedded-style HTML produce the same component PDF bytes. + +For a small customization without editing HTML: + +```python +from fullbleed.ui.experimental import pt, style +from report_components import Metric + +reach = Metric(1240, "households took part", "across four neighborhoods") +accented = reach.with_parts(value=style(color="#b74624", font_size=pt(44))) +``` + +The experimental API may change before stabilization. Keep the exact prerelease +pin while reproducing these examples. + +## Edit with a live PDF preview + +After installing `requirements-components.txt`, start the local preview: + +```sh +python examples/design_showcase/preview.py +``` + +Open the loopback URL printed in the terminal. Save `invoice_components.py`, +`component_primitives.py`, `invoice.css`, or `data.json` in your editor. The helper +reruns Python in a fresh process and refreshes images rendered from the finalized +PDF. Download the PDF, styled HTML, or unstyled HTML from the preview. + +Choose Common Ground or the HTML/CSS authoring path with CLI options: + +```sh +python examples/design_showcase/preview.py --document report +python examples/design_showcase/preview.py --document invoice --authoring html +``` + +The HTML path watches the same data and CSS, plus the original HTML template. +Its downloads contain the PDF and separate HTML/CSS sources. The preview uses +the same installed engine and fonts as the comparison command above. + +Syntax errors, missing files and failed renders appear in the viewer. The last +successful preview remains visible and its downloads are explicitly labeled as +the previous version. Saving a correction retries automatically. Edits made +during a render schedule another rebuild before the result is shown. When a +document becomes shorter, the old page images disappear from the current view. + +The helper watches this example directory, skipping `output`, `target`, virtual +environments and common caches. Use repeatable `--watch-path path` options for +imports or assets outside it. `--port 0` (the default) selects an available port; +`--dpi` defaults to 120, and `--timeout` allows 60 seconds per rebuild. Ctrl-C +stops the server and its render process. Download anything you need to keep: +preview artifacts live only for this session. + +This is an optional local development example that executes your Python source, +binds to `127.0.0.1`, and uses only Fullbleed plus the standard library. It is not +a hosted rendering service or a replacement for the comparison/verification +command. These compositions retain their fixed page budgets; review layout +after adding content. diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/common.css b/examples/showcase-authoring-alpha6/examples/design_showcase/common.css new file mode 100644 index 0000000..fba4731 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/common.css @@ -0,0 +1,26 @@ +@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; } diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/compare_authoring.py b/examples/showcase-authoring-alpha6/examples/design_showcase/compare_authoring.py new file mode 100644 index 0000000..f54fad4 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/compare_authoring.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Render the original HTML and component versions; require identical page PNGs. + +Run with the installed Fullbleed wheel and Python's standard library only. +The original templates, data, CSS and explicit font assets are the reference. +""" +from __future__ import annotations + +import argparse +from decimal import Decimal +from hashlib import sha256 +from html.parser import HTMLParser +from importlib.metadata import version +import json +from pathlib import Path +import sys + +from fullbleed.ui.experimental import Renderer + +import render as original +from invoice_components import northstar_invoice +from report_components import common_ground_report + + +class ExportCheck(HTMLParser): + def __init__(self): + super().__init__() + self.styles = self.inline_styles = self.scripts = 0 + self.classes = [] + + def handle_starttag(self, tag, attrs): + self.styles += tag == "style" + self.scripts += tag == "script" + self.inline_styles += "style" in dict(attrs) + self.classes.extend(name for name in dict(attrs).get("class", "").split() if not name.startswith("fbx-")) + + +def digest(path: Path) -> str: + return sha256(path.read_bytes()).hexdigest() + + +def compare(out: Path, name: str, *, dpi: int) -> dict: + if name == "invoice": + items = original.DATA["invoice"] + document = northstar_invoice(items) + total = sum((Decimal(item["rate"]) * item["hours"] for item in items), Decimal(0)) + expected_pages, markers = 1, ["NS-1042", "Maple & Finch", original.money(total)] + else: + report = original.DATA["report"] + document = common_ground_report(report) + total = sum(Decimal(item["amount"]) for item in report["allocations"]) + expected_pages, markers = 3, ["Common Ground", f"{report['households']:,}", original.money(total), "What we count."] + source = document.compile(target="pdf") + html, css = original.sources(name) + results = {} + for lane, text, rules in (("html", html, css), ("components", source.html, source.css)): + results[lane] = original.ordinary(out / lane, name, text, rules, expected_pages, markers, dpi=dpi) + failures = [f"{lane}: {failure}" for lane, result in results.items() for failure in result["failures"]] + left, right = (out / lane / name for lane in ("html", "components")) + pages = [] + for index in range(1, expected_pages + 1): + filename = f"{name}_page{index}.png" + a, b = left / "preview" / filename, right / "preview" / filename + equal = a.exists() and b.exists() and a.read_bytes() == b.read_bytes() + pages.append(dict(page=index, identical=equal, html_sha256=digest(a) if a.exists() else None, + component_sha256=digest(b) if b.exists() else None)) + if not equal: + failures.append(f"page {index}: authoring paths produce different PNGs") + exports = {} + for label, included in (("styled", True), ("unstyled", False)): + text = document.to_html(include_styles=included) + (right / f"{name}.{label}.html").write_text(text, encoding="utf-8", newline="\n") + check = ExportCheck() + check.feed(text) + exports[label] = check + if check.scripts or not included and (check.styles or check.inline_styles): + failures.append(f"{label}: unexpected script or styles") + classes_preserved = exports["styled"].classes == exports["unstyled"].classes + if not classes_preserved: + failures.append("unstyled export changed authored class names") + if not exports["styled"].styles: + failures.append("styled export omitted its CSS") + a, b = left / f"{name}.pdf", right / f"{name}.pdf" + renderer = Renderer(font_files=[str(path) for path in original.FONTS]) + api_pdf = document.render_pdf(renderer) + styled_pdf = renderer.engine_for(document).render_pdf(document.to_html(include_styles=True), "") + api_equal = b.exists() and api_pdf == b.read_bytes() + styled_equal = b.exists() and styled_pdf == b.read_bytes() + if not api_equal or not styled_equal: + failures.append("direct component API or embedded-style HTML changed PDF bytes") + return dict(name=name, ok=not failures, failures=failures, page_count=expected_pages, + html_pdf_sha256=digest(a) if a.exists() else None, + component_pdf_sha256=digest(b) if b.exists() else None, + pdf_bytes_identical=a.exists() and b.exists() and a.read_bytes() == b.read_bytes(), + pages=pages, render_verify_replay=all(result["ok"] for result in results.values()), + direct_component_api_pdf_identical=api_equal, styled_html_pdf_identical=styled_equal, + styled_and_unstyled_exports=True, authored_classes_preserved=classes_preserved) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=original.ROOT / "output" / "authoring-comparison") + parser.add_argument("--only", choices=("invoice", "report")) + parser.add_argument("--dpi", type=int, default=120) + args = parser.parse_args() + if args.dpi <= 0: + parser.error("--dpi must be positive") + out = args.out.resolve() + out.mkdir(parents=True, exist_ok=True) + records = [compare(out, name, dpi=args.dpi) for name in ([args.only] if args.only else ["invoice", "report"])] + result = dict(ok=all(record["ok"] for record in records), engine_version=version("fullbleed"), + dpi=args.dpi, documents=records, + fonts={path.name: digest(path) for path in original.FONTS}, + scope="Same-runtime PDF page appearance for these fixed A4 compositions; not general HTML/CSS parity.") + (out / "comparison.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8", newline="\n") + print(json.dumps(result, indent=2)) + return 0 if result["ok"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/component_primitives.py b/examples/showcase-authoring-alpha6/examples/design_showcase/component_primitives.py new file mode 100644 index 0000000..0736c64 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/component_primitives.py @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: MIT +"""Small shared components for the two designed showcase documents.""" +from __future__ import annotations + +from decimal import Decimal +from pathlib import Path +import xml.etree.ElementTree as ET + +from fullbleed.ui.experimental import ( + Document, Footer, Fragment, Node, Page, Text, component, mm, +) + +ROOT = Path(__file__).resolve().parent + + +def money(value) -> str: + return f"${Decimal(value):,.2f}" + + +@component +def Lines(*lines) -> Node: + """Compose deliberate print line breaks, including styled inline children.""" + return Fragment(*(child for i, line in enumerate(lines) + for child in ((Node("br"), line) if i else (line,)))) + + +@component +def Folio(label: str, page: str) -> Node: + return Footer(Text(label), Text(page), class_name="footer") + + +@component +def VectorArtwork(filename: str, *, class_name: str) -> Node: + """Reuse a trusted local SVG asset as vector nodes, without HTML injection.""" + root = ET.parse(ROOT / filename).getroot() + + def node(element): + children = [element.text] if element.text else [] + for child in element: + children.append(node(child)) + if child.tail: + children.append(child.tail) + attrs = dict(element.attrib) + if element is root: + attrs.update({"xmlns": "http://www.w3.org/2000/svg", "class": class_name}) + return Node(element.tag.removeprefix("{http://www.w3.org/2000/svg}"), + tuple(children), tuple(attrs.items())) + + return node(root) + + +def design_document(*children, kind: str, title: str) -> Document: + # Both authoring paths deliberately share the original art direction. + # Page owns paper geometry in the component API; selector CSS owns the rest. + common = (ROOT / "common.css").read_text(encoding="utf-8") + page_rule = "@page { size: A4; margin: 0; }" + if not common.startswith(page_rule): + raise ValueError("Update the explicit Page geometry when common.css changes") + styles = common[len(page_rule):] + "\n" + (ROOT / f"{kind}.css").read_text(encoding="utf-8") + # DataTable owns its cells. These two designs put numeric values after the + # first column; mirror the original HTML's .number wrapping rule there. + styles += "\ntable th:not(:first-child), table td:not(:first-child) { white-space: nowrap; }" + return Document(*children, title=title, lang="en-US", page=Page(size="A4", margin=mm(0)), + styles=styles) diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/data.json b/examples/showcase-authoring-alpha6/examples/design_showcase/data.json new file mode 100644 index 0000000..21365ba --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/data.json @@ -0,0 +1,27 @@ +{ + "invoice": [ + {"description": "Discovery & brand workshop", "detail": "Positioning, priorities, and a clear creative direction", "hours": 8, "rate": "125.00"}, + {"description": "Digital design & implementation", "detail": "A cohesive visual system, from layout to launch", "hours": 6, "rate": "95.00"}, + {"description": "Review & studio handoff", "detail": "Final refinements, documentation, and delivery", "hours": 2, "rate": "150.00"} + ], + "report": { + "households": 1240, + "volunteer_hours": 4860, + "spaces": 12, + "quarterly_households": [420, 560, 790, 980], + "allocations": [ + {"name": "Neighborhood spaces", "amount": 168000}, + {"name": "Learning & workshops", "amount": 126000}, + {"name": "Shared meals & gardens", "amount": 84000}, + {"name": "Access & transport", "amount": 42000} + ] + }, + "statements": { + "monthly_contributions": [120, 180, 140, 200, 180, 250], + "members": [ + {"member_name": "Jordan Ellis", "address": "742 Summit Lane, Riverton", "account_id": "HC-104829", "opening_balance": "320.00"}, + {"member_name": "Maya Chen", "address": "18 Willow Court, Riverton", "account_id": "HC-104830", "opening_balance": "650.00"}, + {"member_name": "Avery Morgan", "address": "96 Orchard Road, Riverton", "account_id": "HC-104831", "opening_balance": "910.00"} + ] + } +} diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/BebasNeue-OFL.txt b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/BebasNeue-OFL.txt new file mode 100644 index 0000000..da95714 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/BebasNeue-OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2010 by Dharma Type. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/BebasNeue-Regular.ttf b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/BebasNeue-Regular.ttf new file mode 100644 index 0000000..c328c6e Binary files /dev/null and b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/BebasNeue-Regular.ttf differ diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-Italic.ttf b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-Italic.ttf new file mode 100644 index 0000000..5369414 Binary files /dev/null and b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-Italic.ttf differ diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-OFL.txt b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-OFL.txt new file mode 100644 index 0000000..be384f0 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. Copyright 2019 Google LLC. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-Regular.ttf b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-Regular.ttf new file mode 100644 index 0000000..bc5ed0c Binary files /dev/null and b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/DMSerifDisplay-Regular.ttf differ diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/sources.json b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/sources.json new file mode 100644 index 0000000..0dc3f83 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/fonts/sources.json @@ -0,0 +1,36 @@ +{ + "license": "OFL-1.1", + "source_commit": "9710da1eacb3be272583c3224dcb70f9da6eadbb", + "files": [ + { + "file": "BebasNeue-Regular.ttf", + "url": "https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/bebasneue/BebasNeue-Regular.ttf", + "bytes": 61400, + "sha256": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73" + }, + { + "file": "BebasNeue-OFL.txt", + "url": "https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/bebasneue/OFL.txt", + "bytes": 4337, + "sha256": "72082f6cb4d04be2ecf7cc7d9e1e7d73787f0af8a5a278a47cade70c16b78341" + }, + { + "file": "DMSerifDisplay-Regular.ttf", + "url": "https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/dmserifdisplay/DMSerifDisplay-Regular.ttf", + "bytes": 76580, + "sha256": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616" + }, + { + "file": "DMSerifDisplay-Italic.ttf", + "url": "https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/dmserifdisplay/DMSerifDisplay-Italic.ttf", + "bytes": 71208, + "sha256": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45" + }, + { + "file": "DMSerifDisplay-OFL.txt", + "url": "https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/dmserifdisplay/OFL.txt", + "bytes": 4605, + "sha256": "a3e5cdd67d4571dd0a24fcc968de0efde7ae97ef752daf0906e4767619dd7231" + } + ] +} diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/invoice.css b/examples/showcase-authoring-alpha6/examples/design_showcase/invoice.css new file mode 100644 index 0000000..a056d9b --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/invoice.css @@ -0,0 +1,21 @@ +.invoice .masthead { display: grid; grid-template-columns: 1.05fr .8fr 1fr; gap: 18pt; align-items: center; padding-bottom: 22pt; border-bottom: 1pt solid #17382e; } +.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: #17382e; 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/showcase-authoring-alpha6/examples/design_showcase/invoice.html b/examples/showcase-authoring-alpha6/examples/design_showcase/invoice.html new file mode 100644 index 0000000..db3a60d --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/invoice.html @@ -0,0 +1,12 @@ +Northstar Studio — Invoice NS-1042 +
+
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

Maple & Finch
Attn. Avery Morgan
Portland, Oregon

The project

Autumn launch
Brand workshop, digital
design & handoff

Invoice NS-1042

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

+ +{{invoice_rows}} +
DESCRIPTIONHOURSRATEAMOUNT
+

A little clarity.
A lot of possibility.

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

Subtotal{{subtotal}}
Tax · 0% in this sample$0.00
TOTAL DUE · USD
{{total}}
+

Payment details

Reference NS-1042 with your payment.
Payment instructions are supplied separately.

A note from the studio

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

+ +
diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/invoice_components.py b/examples/showcase-authoring-alpha6/examples/design_showcase/invoice_components.py new file mode 100644 index 0000000..43c2815 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/invoice_components.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: MIT +"""Northstar Studio: reusable Python components, no authored HTML strings.""" +from __future__ import annotations + +from decimal import Decimal + +from fullbleed.ui.experimental import ( + Box, Column, DataTable, Document, Fragment, Header, Heading, Node, + Paragraph, Section, Strong, Text, component, pct, pt, style, +) +from component_primitives import Folio, Lines, design_document, money + + +@component +def StudioMasthead() -> Node: + return Header( + Box(Text("Northstar"), Text("Studio"), + Node("small", ("DESIGN WITH DIRECTION",)), class_name="brand"), + Box(Lines("Brand", "Digital", "Experiences", "That matter."), class_name="brand-note kicker"), + Box(Lines("42 Cedar Avenue", "Portland, OR 97205", "hello@example.invalid", + "northstar.example.invalid"), class_name="address"), + class_name="masthead", + ) + + +@component(parts=("label", "value")) +def InvoiceField(label: str, *lines) -> Node: + return Box(Heading(label, class_name="kicker", part="label"), + Paragraph(Lines(*lines), part="value")) + + +@component +def InvoiceItems(items) -> Node: + return DataTable( + items, + columns=( + Column("DESCRIPTION", lambda item: Fragment( + Strong(item["description"]), Paragraph(item["detail"], class_name="item-note")), width=pct(56)), + Column("HOURS", lambda item: item["hours"], align="right"), + Column("RATE", lambda item: money(item["rate"]), align="right"), + Column("AMOUNT", lambda item: money(Decimal(item["rate"]) * item["hours"]), align="right"), + ), + attrs={"aria-label": "Invoice line items"}, + style=style(table_layout="auto"), + ).with_parts( + header=style(padding=(pt(10), 0), border_bottom=".55pt solid #bdc8bf", font_weight=600), + cell=style(padding=(pt(12), 0), border_bottom=".55pt solid #bdc8bf"), + ) + + +@component +def SummaryLine(label: str, amount: str) -> Node: + return Box(Text(label), Text(amount), class_name="summary-line") + + +@component(parts=("label", "amount")) +def AmountDue(amount: Decimal) -> Node: + return Box(Box("TOTAL DUE · USD", class_name="kicker", part="label"), + Box(money(amount), class_name="value", part="amount"), class_name="total") + + +@component +def PaymentNote(title: str, *lines) -> Node: + return Box(Heading(title, class_name="kicker", style=style(margin_bottom=pt(6))), + Paragraph(Lines(*lines))) + + +def northstar_invoice(items) -> Document: + """Capture line items once; calculate all amounts with Decimal.""" + items = tuple(items) + subtotal = sum((Decimal(item["rate"]) * item["hours"] for item in items), Decimal("0")) + return design_document( + Section( + StudioMasthead(), + Box(Heading("INVOICE", level=1), + Paragraph(Lines("GOOD WORK.", "CLEAR DETAILS.", "LET'S MAKE", "WHAT'S NEXT."), + class_name="title-note"), class_name="title-row"), + Section( + InvoiceField("Prepared for", Strong("Maple & Finch"), "Attn. Avery Morgan", "Portland, Oregon"), + InvoiceField("The project", "Autumn launch", "Brand workshop, digital", "design & handoff"), + InvoiceField("Invoice NS-1042", "Issued · 01 October 2026", "Due · 31 October 2026", "Currency · USD"), + class_name="details grid-3", + ), + InvoiceItems(items), + Section( + Box(Paragraph(Lines("A little clarity.", "A lot of possibility."), class_name="thanks"), + Paragraph(Lines("Thank you for inviting us", "to help shape your next chapter."), + class_name="small muted", style=style(margin_top=pt(14)))), + Box(SummaryLine("Subtotal", money(subtotal)), SummaryLine("Tax · 0% in this sample", "$0.00"), + AmountDue(subtotal)), + class_name="amounts", + ), + Section( + PaymentNote("Payment details", "Reference NS-1042 with your payment.", + "Payment instructions are supplied separately."), + PaymentNote("A note from the studio", "Questions about a line item?", "Write to hello@example.invalid."), + class_name="payment", + ), + Folio("NORTHSTAR STUDIO / INVOICE NS-1042", "FICTIONAL SAMPLE · 01 / 01"), + class_name="page invoice", + ), + kind="invoice", title="Northstar Studio — Invoice NS-1042", + ) diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/landscape.svg b/examples/showcase-authoring-alpha6/examples/design_showcase/landscape.svg new file mode 100644 index 0000000..f4353f3 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/landscape.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/notice.css b/examples/showcase-authoring-alpha6/examples/design_showcase/notice.css new file mode 100644 index 0000000..c6946b8 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/notice.css @@ -0,0 +1,24 @@ +.notice { color: #123c79; background: #f6f5ef; } +.notice .masthead { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18pt; border-bottom: 1pt solid #426eaa; } +.notice .brand { font: 26pt/1.05 'DM Serif Display'; } +.notice .masthead .kicker { font-size: 6.6pt; line-height: 1.7; } +.notice .notice-title { display: grid; grid-template-columns: 2.3fr 1fr; gap: 22pt; padding: 23pt 0 18pt; } +.notice h1 { font: 47pt/.95 'Bebas Neue'; margin-top: 9pt; } +.notice .date { border-left: 1pt solid #6485b2; padding-left: 18pt; } +.notice .date strong { font: 34pt/1 'Bebas Neue'; display: block; } +.notice .date strong span { display: block; line-height: 1; } +.notice .date .time { font: 14pt/1.2 'Bebas Neue'; margin-top: 9pt; } +.notice .intro { font: 18pt/1.18 'DM Serif Display'; margin-bottom: 20pt; } +.notice .alert { background: #143e7b; color: white; padding: 20pt 22pt; border-left: 5pt solid #dcb845; margin-bottom: 23pt; } +.notice .alert p { font-size: 11pt; margin-top: 7pt; line-height: 1.5; } +.notice h2 { font-size: 8.5pt; font-weight: 600; letter-spacing: 1.2pt; text-transform: uppercase; margin-bottom: 10pt; } +.notice .details { font-size: 9.5pt; line-height: 1.55; } +.notice .details > div + div { border-left: .65pt solid #8ba2bd; padding-left: 22pt; } +.notice ul { padding-left: 13pt; margin: 0; } +.notice li { margin-bottom: 7pt; } +.notice .help { background: #e0e9f2; padding: 15pt; margin-top: 17pt; } +.notice .help p { font-size: 8.5pt; line-height: 1.6; } +.notice .timeline { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12pt; margin-top: 28pt; padding-top: 13pt; border-top: 2pt solid #1c4a86; } +.notice .timeline strong { display: block; font: 16pt 'Bebas Neue'; } +.notice .timeline p { font-size: 8pt; margin-top: 4pt; } +.notice .footer { border-color: #7d9bbf; } diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/notice.html b/examples/showcase-authoring-alpha6/examples/design_showcase/notice.html new file mode 100644 index 0000000..e49ec8f --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/notice.html @@ -0,0 +1,9 @@ +Riverton Services — Planned Water Maintenance +
Riverton
Neighborhood Services
CLEAR INFORMATION.
CONNECTED PLACES.
A BETTER EVERYDAY.
+

SERVICE NOTICE / RV-1020

WATER SUPPLY
MAINTENANCE

TUESDAY20 OCT2026

6:00 AM — 4:00 PM

+

A planned upgrade to the neighborhood network.
Here is what to expect and how to prepare.

+

Temporary interruption

Water service will be unavailable during the maintenance window in the areas listed below. Service is expected to return by 4:00 PM on Tuesday, 20 October.

+

Affected areas

  • Riverton Central
  • Maple Grove
  • East Harbor
  • Riverside Heights

This sample names fictional service areas. It is a document design demonstration, not a live service alert.

Plan ahead

  • Allow for the full maintenance window.
  • Check any equipment that needs a continuous water supply.
  • Contact the service team if you need this notice in another format.

Questions? We can help.

support@example.invalid
Monday–Friday · 8 AM–6 PM
Reference: RV-1020

+
12 OCTOBER

Advance notice
distributed

20 OCTOBER / 6 AM

Maintenance
window begins

20 OCTOBER / 4 PM

Expected service
restoration

+ +
diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/preview.html b/examples/showcase-authoring-alpha6/examples/design_showcase/preview.html new file mode 100644 index 0000000..6f00e85 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/preview.html @@ -0,0 +1,45 @@ + + + + + +Fullbleed · Local PDF preview + + + + +
+
fullbleedLocal authoring preview
+

Your document, as you edit.

+

Save your source files. Fullbleed rerenders the PDF and refreshes its page previews here.

+ +
+
+
Building preview…
+

The first render loads the engine and registered fonts.

+ +
Your PDF pages will appear here.
+
+ + + diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/preview.js b/examples/showcase-authoring-alpha6/examples/design_showcase/preview.js new file mode 100644 index 0000000..58240f8 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/preview.js @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +"use strict"; +const get = (id) => document.getElementById(id); +let shownRevision = null; +let stopped = false; + +function download(id, label, revision, name) { + const link = get(id); + link.textContent = label; + link.href = `revisions/${revision}/${name}`; + link.setAttribute("download", name); + link.removeAttribute("aria-disabled"); +} + +function show(value) { + const result = value.result; + const stale = value.phase !== "ready"; + get("state").className = `status ${value.phase}`; + get("title").textContent = value.document === "invoice" ? "Northstar Studio" : "Common Ground"; + get("intro").textContent = value.authoring === "components" + ? "Python components → PDF. Save a component, stylesheet or data file to see your changes." + : "HTML/CSS → PDF. Save a template, stylesheet or data file to see your changes."; + get("status").textContent = value.phase === "ready" ? "Preview up to date" + : value.phase === "error" ? "This edit could not be rendered" : "Rebuilding preview…"; + get("detail").textContent = stale && result + ? "Showing the last successful preview. Downloads contain that earlier version." + : value.phase === "error" ? "Fix the error and save again. The watcher will retry automatically." + : value.phase === "ready" ? "Save another edit to rebuild. Ctrl-C in the terminal stops this preview." + : "Loading the document and registered fonts."; + get("error-panel").hidden = value.phase !== "error"; + get("error-summary").textContent = value.error.split("\n").filter(Boolean).pop() || "Render failed"; + get("error").textContent = value.error; + if (!result) return; + download("pdf", stale ? "Download last PDF" : "Download PDF", value.revision, "document.pdf"); + if (value.authoring === "components") { + download("source", stale ? "Last styled HTML" : "Styled HTML", value.revision, "document.styled.html"); + download("extra", stale ? "Last unstyled HTML" : "Unstyled HTML", value.revision, "document.unstyled.html"); + } else { + download("source", stale ? "Last HTML source" : "HTML source", value.revision, "source.html"); + download("extra", stale ? "Last CSS source" : "CSS source", value.revision, "styles.css"); + } + get("meta").textContent = `${result.page_count} ${result.page_count === 1 ? "page" : "pages"} · ${result.dpi} DPI · Fullbleed ${result.engine_version}`; + if (shownRevision === value.revision) return; + shownRevision = value.revision; + const figures = []; + for (let page = 1; page <= result.page_count; page++) { + const figure = document.createElement("figure"); + const caption = document.createElement("figcaption"); + caption.textContent = `Page ${page} / ${result.page_count}`; + const image = document.createElement("img"); + image.src = `revisions/${value.revision}/page-${page}.png`; + image.alt = `${get("title").textContent}, PDF page ${page}`; + figure.append(caption, image); + figures.push(figure); + } + get("pages").replaceChildren(...figures); +} + +async function poll() { + if (stopped) return; + try { + const response = await fetch("status", { cache: "no-store" }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + show(await response.json()); + } catch (_) { + get("state").className = "status error"; + get("status").textContent = "Preview server disconnected"; + get("detail").textContent = "The displayed pages are from the last render. Restart the command and open its new URL."; + for (const id of ["pdf", "source", "extra"]) { + get(id).removeAttribute("href"); + get(id).setAttribute("aria-disabled", "true"); + } + } finally { + if (!stopped) window.setTimeout(poll, 500); + } +} +window.addEventListener("pagehide", () => { stopped = true; }); +poll(); diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/preview.py b/examples/showcase-authoring-alpha6/examples/design_showcase/preview.py new file mode 100644 index 0000000..9f79f34 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/preview.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Edit a showcase locally and see finalized PDF pages refresh after each save. + +This optional example uses Fullbleed and Python's standard library. It executes +your local Python source; it is a development tool, not a hosted rendering API. +""" +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from hashlib import sha256 +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +import math +import os +from pathlib import Path +import secrets +import subprocess +import sys +import tempfile +import threading +import time +from urllib.parse import urlsplit + +ROOT = Path(__file__).resolve().parent +IGNORED = {".git", ".venv", "venv", "__pycache__", "node_modules", "target", "output", ".pytest_cache"} +TITLES = {"invoice": "Northstar Studio — Invoice NS-1042", "report": "Common Ground — Community Review 2026"} + + +def render_worker(args): + """Every rebuild has fresh module state, fonts, data and native engine state.""" + from importlib.metadata import version + import fullbleed + import render as original + + out = args._render + if args.authoring == "components": + from fullbleed.ui.experimental import Renderer + if args.document == "invoice": + from invoice_components import northstar_invoice + document = northstar_invoice(original.DATA["invoice"]) + else: + from report_components import common_ground_report + document = common_ground_report(original.DATA["report"]) + renderer = Renderer(font_files=[str(path) for path in original.FONTS]) + engine = renderer.engine_for(document) + prepared = document.prepare(renderer) + exports = { + "document.styled.html": document.to_html(include_styles=True), + "document.unstyled.html": document.to_html(include_styles=False), + } + else: + html, css = original.sources(args.document) + engine = fullbleed.PdfEngine(font_files=[str(path) for path in original.FONTS], + document_title=TITLES[args.document], document_lang="en-US") + prepared = engine.compile_pdf(html, css) + exports = {"source.html": html, "styles.css": css} + pdf = prepared.render_pdf() + path = out / "document.pdf" + path.write_bytes(pdf) + inspection = fullbleed.inspect_pdf(str(path)) + if not 1 <= inspection["page_count"] <= 100: + raise ValueError("This local preview supports documents of 1–100 pages") + # Preview the saved PDF, including finalized transparency and paint order. + pages = engine.render_finalized_pdf_image_pages(str(path), dpi=args.dpi) + if len(pages) != inspection["page_count"]: + raise RuntimeError("Saved-PDF preview page count differs from PDF inspection") + files = ["document.pdf", *exports] + for index, data in enumerate(pages, 1): + name = f"page-{index}.png" + (out / name).write_bytes(data) + files.append(name) + for name, text in exports.items(): + (out / name).write_text(text, encoding="utf-8", newline="\n") + record = dict(title=TITLES[args.document], engine_version=version("fullbleed"), + page_count=len(pages), pdf_sha256=sha256(pdf).hexdigest(), + pdf_bytes=len(pdf), files=files, dpi=args.dpi) + (out / "result.json").write_text(json.dumps(record), encoding="utf-8") + + +def snapshot(roots): + """Notice edits, atomic saves and missing files without following directory links.""" + result = {} + + def remember(path): + try: + stat = path.stat() + result[str(path)] = (stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns, stat.st_ino) + except OSError as error: + result[str(path)] = ("unavailable", error.errno) + + for root in roots: + if not root.is_dir(): + remember(root) + continue + def failed(error): + result[str(error.filename)] = ("unavailable", error.errno) + for directory, folders, files in os.walk(root, onerror=failed, followlinks=False): + directory = Path(directory) + folders[:] = [name for name in folders if name not in IGNORED + and not (directory / name).is_symlink()] + for name in files: + remember(directory / name) + return result + + +@dataclass(frozen=True) +class Revision: + number: int + metadata: dict + files: dict[str, bytes] + + +class Preview: + def __init__(self, args): + self.args = args + self.roots = tuple(dict.fromkeys([ROOT, *(path.resolve() for path in args.watch_path)])) + self.lock = threading.Lock() + self.stop = threading.Event() + self.phase = "building" + self.error = "" + self.revision = 0 + self.attempt = 0 + self.history: list[Revision] = [] + + def status(self): + with self.lock: + latest = self.history[-1] if self.history else None + return dict(phase=self.phase, error=self.error, attempt=self.attempt, + document=self.args.document, authoring=self.args.authoring, + revision=latest.number if latest else None, + result=latest.metadata if latest else None) + + def rebuild(self): + with self.lock: + self.phase, self.error = "building", "" + self.attempt += 1 + with tempfile.TemporaryDirectory(prefix="fullbleed-preview-") as directory: + out = Path(directory) + environment = {**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONPYCACHEPREFIX": str(out / "bytecode")} + # A new cache prefix also prevents reading an old same-second .pyc. + command = [sys.executable, str(ROOT / "preview.py"), "--document", self.args.document, + "--authoring", self.args.authoring, "--dpi", str(self.args.dpi), "--_render", str(out)] + with (out / "worker.log").open("wb") as log: + process = subprocess.Popen(command, cwd=ROOT, env=environment, + stdin=subprocess.DEVNULL, stdout=log, stderr=log, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0, + start_new_session=os.name != "nt") + deadline = time.monotonic() + self.args.timeout + timed_out = False + try: + while process.poll() is None and not self.stop.wait(0.05): + if time.monotonic() >= deadline: + timed_out = True + break + finally: + if process.poll() is None: + process.kill() + process.wait() + if self.stop.is_set(): + return None + if timed_out: + raise RuntimeError(f"Rendering exceeded {self.args.timeout:g} seconds. Save an edit to retry, " + "or restart with a larger --timeout.") + if process.returncode: + with (out / "worker.log").open("rb") as log: + log.seek(max(0, log.seek(0, 2) - 12000)) + message = log.read().decode("utf-8", errors="replace").strip() + raise RuntimeError(message or f"Renderer exited with code {process.returncode}") + metadata = json.loads((out / "result.json").read_text(encoding="utf-8")) + names = metadata["files"] + if any(Path(name).name != name or name in {".", ".."} for name in names): + raise ValueError("Renderer returned an invalid artifact name") + files = {name: (out / name).read_bytes() for name in names} + return metadata, files + + def run(self): + previous, due = snapshot(self.roots), time.monotonic() + while not self.stop.is_set(): + current = snapshot(self.roots) + if current != previous: + previous, due = current, time.monotonic() + 0.25 + if due is not None and time.monotonic() >= due: + before = previous + try: + result = self.rebuild() + failure = "" + except Exception as error: + result, failure = None, str(error) + if self.stop.is_set(): + break + after = snapshot(self.roots) + if after != before: + # Never publish a build made from a mixture of saved inputs. + previous, due = after, time.monotonic() + 0.25 + continue + with self.lock: + if result is not None: + self.revision += 1 + self.history.append(Revision(self.revision, *result)) + self.history = self.history[-2:] + self.phase, self.error = "ready", "" + else: + self.phase, self.error = "error", failure + event = dict(event="preview_result", phase=self.phase, revision=self.revision, + attempt=self.attempt, error=failure.splitlines()[-1] if failure else "") + print(json.dumps(event), flush=True) + due = None + self.stop.wait(0.1) + + +def handler(preview, token): + class Handler(BaseHTTPRequestHandler): + def log_message(self, *_): + pass + + def reply(self, code, body=b"", content_type="text/plain; charset=utf-8", *, filename=None): + self.send_response(code) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("Referrer-Policy", "no-referrer") + self.send_header("Content-Security-Policy", "default-src 'none'; script-src 'self'; " + "style-src 'unsafe-inline'; img-src 'self'; connect-src 'self'; " + "base-uri 'none'; form-action 'none'; frame-ancestors 'none'") + if filename: + self.send_header("Content-Disposition", f'attachment; filename="{filename}"') + self.end_headers() + if self.command != "HEAD": + try: + self.wfile.write(body) + except (BrokenPipeError, ConnectionResetError): + pass + + def do_GET(self): + origin = f"http://127.0.0.1:{self.server.server_port}" + if self.headers.get("Host") != origin.removeprefix("http://") or ( + self.headers.get("Origin") not in {None, origin} + ) or self.headers.get("Sec-Fetch-Site") == "cross-site": + return self.reply(403, b"Local preview only") + path = urlsplit(self.path).path + prefix = f"/{token}/" + if not path.startswith(prefix): + return self.reply(404) + relative = path[len(prefix):] + if relative in {"", "preview.js"}: + name = "preview.html" if not relative else relative + kind = "text/html; charset=utf-8" if not relative else "text/javascript; charset=utf-8" + return self.reply(200, (ROOT / name).read_bytes(), kind) + if relative == "status": + return self.reply(200, json.dumps(preview.status()).encode(), "application/json") + parts = relative.split("/") + if len(parts) == 3 and parts[0] == "revisions" and parts[1].isdigit(): + with preview.lock: + revision = next((item for item in preview.history if item.number == int(parts[1])), None) + if revision and parts[2] in revision.files: + name, body = parts[2], revision.files[parts[2]] + mime = {".pdf": "application/pdf", ".png": "image/png", ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8"}[Path(name).suffix] + return self.reply(200, body, mime, filename=None if name.endswith(".png") else name) + self.reply(404) + + return Handler + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--document", choices=("invoice", "report"), default="invoice") + parser.add_argument("--authoring", choices=("components", "html"), default="components") + parser.add_argument("--port", type=int, default=0, help="Loopback port; default chooses a free port") + parser.add_argument("--dpi", type=int, default=120) + parser.add_argument("--timeout", type=float, default=60, help="Maximum seconds per rebuild") + parser.add_argument("--watch-path", type=Path, action="append", default=[], help="Additional file or directory to watch") + parser.add_argument("--_render", type=Path, help=argparse.SUPPRESS) + args = parser.parse_args() + if not 0 <= args.port <= 65535 or not 36 <= args.dpi <= 300 or not math.isfinite(args.timeout) or args.timeout <= 0: + parser.error("Use a port between 0 and 65535, DPI between 36 and 300, and a finite positive timeout") + if args._render is not None: + render_worker(args) + return 0 + preview = Preview(args) + token = secrets.token_urlsafe(24) + with ThreadingHTTPServer(("127.0.0.1", args.port), handler(preview, token)) as server: + server.daemon_threads = True + worker = threading.Thread(target=preview.run, name="showcase-render") + print(json.dumps(dict(event="preview_started", url=f"http://127.0.0.1:{server.server_port}/{token}/", + document=args.document, authoring=args.authoring)), flush=True) + print("Open the URL above. Save Python, CSS, HTML or data files to rebuild. Ctrl-C stops the preview.", + file=sys.stderr, flush=True) + worker.start() + try: + server.serve_forever(poll_interval=0.1) + except KeyboardInterrupt: + pass + finally: + preview.stop.set() + worker.join() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/render.py b/examples/showcase-authoring-alpha6/examples/design_showcase/render.py new file mode 100644 index 0000000..05b40e0 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/render.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Render four designed document families with installed Fullbleed and stdlib.""" + +from __future__ import annotations + +import argparse +from decimal import Decimal +import hashlib +from html import escape +from importlib import metadata, resources +import json +from pathlib import Path +import subprocess +import sys + +import fullbleed + +ROOT = Path(__file__).resolve().parent +DATA = json.loads((ROOT / 'data.json').read_text(encoding='utf-8')) +FONTS = [Path(str(resources.files('fullbleed_assets').joinpath('fonts/Inter-Variable.ttf'))), + ROOT / 'fonts/BebasNeue-Regular.ttf', ROOT / 'fonts/DMSerifDisplay-Regular.ttf', + ROOT / 'fonts/DMSerifDisplay-Italic.ttf'] + + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def money(value): + return f'${Decimal(value):,.2f}' + + +def substitute(template, values): + for key, value in values.items(): + template = template.replace('{{' + key + '}}', str(value)) + return template + + +def sources(name): + html = (ROOT / f'{name}.html').read_text(encoding='utf-8') + css = (ROOT / 'common.css').read_text(encoding='utf-8') + '\n' + (ROOT / f'{name}.css').read_text(encoding='utf-8') + if name == 'invoice': + total = Decimal('0') + rows = [] + for item in DATA['invoice']: + amount = Decimal(item['rate']) * item['hours'] + total += amount + rows.append(f"{escape(item['description'])}" + f"

{escape(item['detail'])}

" + f"{item['hours']}{money(item['rate'])}" + f"{money(amount)}") + html = substitute(html, dict(invoice_rows=''.join(rows), subtotal=money(total), total=money(total))) + elif name == 'report': + report = DATA['report'] + allocations = report['allocations'] + total = sum(item['amount'] for item in allocations) + funding = [] + for item in allocations: + share = Decimal(item['amount']) / total * 100 + funding.append(f"{escape(item['name'])}" + f"{money(item['amount'])}{share:.0f}%" + f"
") + bars = ''.join(f"
{value:,}
" + f"
" + for value in report['quarterly_households']) + landscape = (ROOT / 'landscape.svg').read_text(encoding='utf-8').replace('
${value}
" + f"
" + for value in DATA['statements']['monthly_contributions']) + html = substitute(html, dict(statement_bars=bars)) + return html, css + + +def member_values(member): + opening = Decimal(member['opening_balance']) + return {**{key: member[key] for key in ['member_name', 'address', 'account_id']}, + 'opening_balance': money(opening), 'first_balance': money(opening + 125), + 'second_balance': money(opening + 250), 'closing_balance': money(opening + 200), + 'bal': money(opening + 200)} + + +def invoke(command, log): + process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, encoding='utf-8') + log.write_text(process.stdout, encoding='utf-8') + log.with_suffix('.stderr.txt').write_text(process.stderr, encoding='utf-8') + try: + result = json.loads(process.stdout) + except json.JSONDecodeError: + result = dict(ok=False, error=dict(message=process.stderr or process.stdout)) + result['process_exit_code'] = process.returncode + return result + + +def ordinary(out, name, html, css, expected_pages, markers, *, profile='none', dpi=120): + folder = out / name + folder.mkdir(parents=True, exist_ok=True) + html_path, css_path, pdf = folder / f'{name}.html', folder / f'{name}.css', folder / f'{name}.pdf' + html_path.write_text(html, encoding='utf-8') + css_path.write_text(css, encoding='utf-8') + args = ['--html', str(html_path), '--css', str(css_path), '--out', str(pdf), + '--document-title', {'invoice':'Northstar Studio — Invoice NS-1042', + 'report':'Common Ground — Community Review 2026', + 'notice':'Riverton Services — Planned Water Maintenance', + 'statement-proof':'Hillside Collective — Member Statement'}[name], + '--document-lang', 'en-US', '--pdf-profile', profile, + '--fail-on', 'overflow', '--fail-on', 'missing-glyphs', '--fail-on', 'font-subst'] + for font in FONTS: + args += ['--asset', str(font)] + base = [sys.executable, '-I', '-m', 'fullbleed', '--json-only'] + rendered = invoke(base + ['render', *args, '--emit-image', str(folder / 'preview'), + '--image-dpi', str(dpi), '--emit-glyph-report', str(folder / 'glyphs.json'), + '--emit-page-data', str(folder / 'page-data.json'), + '--emit-jit', str(folder / 'render.jit')], folder / 'render.json') + verify_args = ['--emit-pdf' if arg == '--out' else arg for arg in args] + verified = invoke(base + ['verify', *verify_args], folder / 'verify.json') + first_hash = rendered.get('outputs', {}).get('sha256') + verified_hash = verified.get('outputs', {}).get('sha256') + replay_ok = bool(first_hash and first_hash == verified_hash and first_hash == sha(pdf)) + (folder / 'replay-verification.json').write_text(json.dumps(dict( + ok=replay_ok, method='separate CLI render and verify processes; identical PDF SHA-256', + render_sha256=first_hash, verify_sha256=verified_hash), indent=2)+'\n', encoding='utf-8') + inspection = fullbleed.inspect_pdf(str(pdf)) if pdf.exists() else {} + (folder / 'inspection.json').write_text(json.dumps(inspection, indent=2)+'\n', encoding='utf-8') + pages = fullbleed.extract_pdf_page_texts(str(pdf)).get('pages', []) if pdf.exists() else [] + # The internal extractor separates positioned paint chunks with newlines; + # a rich inline phrase can therefore contain one chunk per word. + text = ' '.join('\n'.join(page.get('text', '') for page in pages).split()) + failures = [f'missing text: {marker}' for marker in markers if marker not in text] + if '{{' in text or '}}' in text: + failures.append('unresolved template placeholder') + if not replay_ok: + failures.append('render/verify PDF hashes differ') + if inspection.get('page_count') != expected_pages: + failures.append(f"expected {expected_pages} pages, got {inspection.get('page_count')}") + for stage, result in [('render', rendered), ('verify', verified)]: + if not result.get('ok') or result['process_exit_code']: + failures.append(f"{stage}: {result.get('error') or result.get('message')}") + return dict(id=name, ok=not failures, failures=failures, page_count=inspection.get('page_count'), + pdf=str(pdf), sha256=sha(pdf) if pdf.exists() else None, + bytes=pdf.stat().st_size if pdf.exists() else 0, profile=inspection.get('profile'), + preview_paths=rendered.get('outputs', {}).get('image_paths', []), + fallbacks=rendered.get('outputs', {}).get('fallbacks'), + reproducibility='pass' if replay_ok else 'fail', + source_html=str(html_path), source_css=str(css_path)) + + +def statements(out, dpi): + template, css = sources('statement') + values = [member_values(member) for member in DATA['statements']['members']] + proof_html = substitute(template, {key:escape(value) for key,value in values[0].items()}) + proof = ordinary(out, 'statement-proof', proof_html, css, 1, ['Jordan Ellis', '$520.00'], dpi=dpi) + folder = out / 'statements' + folder.mkdir(parents=True, exist_ok=True) + (folder / 'statement-template.html').write_text(template, encoding='utf-8') + (folder / 'statement.css').write_text(css, encoding='utf-8') + bindings = {key:[value[key] for value in values] for key in values[0]} + (folder / 'bindings.json').write_text(json.dumps(bindings, indent=2)+'\n', encoding='utf-8') + engine = fullbleed.PdfEngine(font_files=[str(font) for font in FONTS], + document_title='Hillside Collective — Member Statements', document_lang='en-US') + compiled = engine.compile_pdf(template, css) + (folder / 'compiled-stats.json').write_text(json.dumps(compiled.stats(), indent=2)+'\n', encoding='utf-8') + pdf, replay = folder / 'statements.pdf', folder / 'replay.pdf' + compiled.render_pdf_reflow_bindings_to_file(bindings, str(pdf), compression='compact') + compiled.render_pdf_reflow_bindings_to_file(bindings, str(replay), compression='compact') + inspection = fullbleed.inspect_pdf(str(pdf)) + pages = fullbleed.extract_pdf_page_texts(str(pdf)).get('pages', []) + failures = list(proof['failures']) + if len(pages) != len(values): + failures.append(f'expected {len(values)} record pages, got {len(pages)}') + for page, value in zip(pages, values): + page_text = ' '.join(page.get('text', '').split()) + for key in ['member_name', 'account_id', 'closing_balance']: + if value[key] not in page_text: + failures.append(f"record {value['account_id']} missing {key}") + # Letter spacing may produce a separate extraction chunk per glyph. + compact_text = ''.join(page.get('text', '').split()) + if compact_text.count(value['closing_balance']) < 3: + failures.append(f"record {value['account_id']} missing a displayed closing balance") + if '{{' in page.get('text', '') or '}}' in page.get('text', ''): + failures.append(f"record {value['account_id']} has an unresolved placeholder") + if sha(pdf) != sha(replay): + failures.append('compiled replay hash differs') + previews = list(engine.render_finalized_pdf_image_pages_to_dir(str(pdf), str(folder / 'preview'), dpi, 'statements')) + (folder / 'inspection.json').write_text(json.dumps(inspection, indent=2)+'\n', encoding='utf-8') + return dict(id='statements', ok=not failures, failures=failures, pdf=str(pdf), sha256=sha(pdf), + bytes=pdf.stat().st_size, page_count=inspection.get('page_count'), record_count=len(values), + preview_paths=previews, reproducibility='pass' if sha(pdf)==sha(replay) else 'fail', + compiled_stats=compiled.stats(), proof=proof, source_html=str(folder / 'statement-template.html'), + source_css=str(folder / 'statement.css')) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--out', type=Path, default=ROOT / 'output') + parser.add_argument('--only', choices=['invoice', 'report', 'notice', 'statements']) + parser.add_argument('--dpi', type=int, default=120) + args = parser.parse_args() + out = args.out.resolve() + out.mkdir(parents=True, exist_ok=True) + results = [] + cases = {'invoice':(1, ['NS-1042', 'Maple & Finch', '$1,870.00']), + 'report':(3, ['Common Ground', '1,240', '$420,000.00', 'What we count.']), + 'notice':(1, ['WATER SUPPLY', '20 OCT', 'RV-1020'])} + for name, (pages, markers) in cases.items(): + if args.only and args.only != name: + continue + html, css = sources(name) + result = ordinary(out, name, html, css, pages, markers, profile='pdfua1' if name=='notice' else 'none', dpi=args.dpi) + results.append(result) + print(json.dumps({key:result[key] for key in ['id','ok','page_count','failures']}), flush=True) + if not args.only or args.only == 'statements': + result = statements(out, args.dpi) + results.append(result) + print(json.dumps({key:result[key] for key in ['id','ok','page_count','failures']}), flush=True) + report = dict(schema='fullbleed.design_showcase.v1', ok=all(case['ok'] for case in results), + fullbleed_version=metadata.version('fullbleed'), fictional_data=True, + checks=['expected page counts','required text and record order','resolved placeholders', + 'CLI overflow/glyph/font gates on ordinary cases and first-record statement proof', + 'deterministic replay','internal PDF inspection'], + external_standards_validation=False, + fonts=[dict(file=font.name, sha256=sha(font)) for font in FONTS], cases=results) + (out / 'verification.json').write_text(json.dumps(report, indent=2)+'\n', encoding='utf-8') + return 0 if report['ok'] else 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/report.css b/examples/showcase-authoring-alpha6/examples/design_showcase/report.css new file mode 100644 index 0000000..f604c8a --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/report.css @@ -0,0 +1,33 @@ +.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/showcase-authoring-alpha6/examples/design_showcase/report.html b/examples/showcase-authoring-alpha6/examples/design_showcase/report.html new file mode 100644 index 0000000..eeaa9b9 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/report.html @@ -0,0 +1,21 @@ +Common Ground — Community Review 2026 +
+
Common Ground
COMMUNITY TRUST
PEOPLE
PLACES
POSSIBILITIES
+

COMMON
GROUND

Stronger places.
More room
for everyone.

2026

COMMUNITY REVIEW

+{{landscape}} +
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.

+
{{households}}

households took part
across four neighborhoods

{{hours}}

volunteer hours
shared across our programs

{{spaces}}

community spaces
kept open and active

+

More neighbors, each quarter.

PARTICIPATING HOUSEHOLDS
{{report_bars}}
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.

+{{funding_rows}}
PROGRAM AREAALLOCATIONSHARE
+
TOTAL OPERATING ALLOCATION · USD{{funding_total}}
+

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.

+
+
diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/report_components.py b/examples/showcase-authoring-alpha6/examples/design_showcase/report_components.py new file mode 100644 index 0000000..bf4e58b --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/report_components.py @@ -0,0 +1,155 @@ +# SPDX-License-Identifier: MIT +"""Common Ground: a three-page editorial report composed in Python.""" +from __future__ import annotations + +from decimal import Decimal + +from fullbleed.ui.experimental import ( + Box, Column, DataTable, Document, Fragment, Header, Heading, Node, + Paragraph, Section, Strong, Text, component, pct, pt, style, +) +from component_primitives import Folio, Lines, VectorArtwork, design_document, money + + +@component +def ReportMasthead(section: str | Node, *, cover: bool = False) -> Node: + brand = Lines("Common Ground", Text("COMMUNITY TRUST", class_name="kicker")) if cover else "Common Ground" + return Header(Box(brand, class_name="brand"), Box(section, class_name="kicker"), class_name="masthead") + + +@component +def ReportPage(*content, section: str | Node, footer: str, number: str, cover: bool = False) -> Node: + return Section(ReportMasthead(section, cover=cover), *content, Folio(footer, number), class_name="page report") + + +@component +def ContentsEntry(label: str, *lines) -> Node: + return Box(Box(label, class_name="kicker"), Paragraph(Lines(*lines), class_name="serif")) + + +@component +def CoverPage() -> Node: + return ReportPage( + Heading(Lines("COMMON", "GROUND"), level=1, class_name="cover-title"), + Box(Paragraph(Lines("Stronger places.", "More room", "for everyone.")), + Box(Box("2026", class_name="year"), + Paragraph("COMMUNITY REVIEW", class_name="kicker", + style=style(font_family="Inter", font_size=pt(8), text_align="right", margin_top=pt(8)))), + class_name="cover-deck"), + VectorArtwork("landscape.svg", class_name="landscape"), + Box(Box(Lines("A YEAR", "IN COMMON"), class_name="kicker", style=style(color="#b44123")), + ContentsEntry("02 / OUR REACH", "Small actions,", "shared outcomes."), + ContentsEntry("03 / OUR RESOURCES", "Where the", "support goes."), class_name="contents"), + section=Lines("PEOPLE", "PLACES", "POSSIBILITIES"), cover=True, + footer="COMMON GROUND / COMMUNITY REVIEW", number="FICTIONAL DATA · 01 / 03", + ) + + +@component +def SectionIntro(kicker: str, title: tuple[str, ...], description: str) -> Node: + return Fragment( + Paragraph(kicker, class_name="kicker", style=style(color="#b44123")), + Heading(Lines(*title), level=1, class_name="section-title"), + Paragraph(description, class_name="deck"), + ) + + +@component(parts=("value", "caption")) +def Metric(value: int, *caption: str) -> Node: + return Box(Box(f"{value:,}", class_name="value", part="value"), + Paragraph(Lines(*caption), class_name="metric-caption", part="caption")) + + +@component +def ParticipationChart(values, *, maximum: int = 980) -> Node: + values = tuple(values) + # Use the same explicit scale as the original HTML authoring path. + if maximum <= 0: + raise ValueError("The chart maximum must be positive") + return Section( + Box(Heading("More neighbors, each quarter."), Text("PARTICIPATING HOUSEHOLDS", class_name="kicker"), + class_name="row"), + Box((Box(Box(f"{value:,}", class_name="chart-value"), + Box(class_name="bar", style=style(height=pt(round(value / maximum * 105, 2)))), + class_name="chart-col") for value in values), class_name="chart-bars"), + Box((Text(f"Q{index}") for index in range(1, len(values) + 1)), class_name="chart-labels"), + Paragraph("Quarterly participation counts; a household can participate in more than one quarter.", + class_name="small", style=style(margin_top=pt(12), color="#c4d4bd")), + class_name="chart-panel", + ) + + +@component +def ReachPage(report) -> Node: + return ReportPage( + SectionIntro("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."), + Box(Metric(report["households"], "households took part", "across four neighborhoods"), + Metric(report["volunteer_hours"], "volunteer hours", "shared across our programs"), + Metric(report["spaces"], "community spaces", "kept open and active"), class_name="metrics grid-3"), + ParticipationChart(report["quarterly_households"]), + Box(Box(Paragraph(Lines("“Belonging starts", "with somewhere", "to show up.”"), class_name="quote"), + Paragraph("OUR PROGRAM PRINCIPLE", class_name="kicker", style=style(margin_top=pt(12)))), + Box(Heading("Built around daily life."), + Paragraph("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.", class_name="note")), class_name="grid-2"), + section="02 / OUR REACH", footer="ILLUSTRATIVE ANNUAL REVIEW / NOT AN ACTUAL ORGANIZATION", number="02 / 03", + ) + + +@component +def AllocationShare(amount, total) -> Node: + share = Decimal(amount) / total * 100 if total else Decimal(0) + return Fragment(f"{share:.0f}%", + Box(Box(class_name="share-fill", style=style(width=pct(share))), + class_name="share-track", style=style(margin_left="auto"))) + + +@component +def AllocationTable(allocations, total) -> Node: + return DataTable( + allocations, + columns=( + Column("PROGRAM AREA", lambda item: Strong(item["name"]), width=pct(50)), + Column("ALLOCATION", lambda item: money(item["amount"]), align="right"), + Column("SHARE", lambda item: AllocationShare(item["amount"], total), align="right", width=pt(88)), + ), + class_name="funding-table", style=style(table_layout="auto", margin_top=pt(25)), + ).with_parts( + header=style(padding=(pt(10), 0), border_bottom=".55pt solid #bdc8bf", font_weight=600), + cell=style(padding=(pt(8), 0), border_bottom=".55pt solid #bdc8bf"), + ) + + +@component +def ResourcesPage(report) -> Node: + allocations = tuple(report["allocations"]) + total = sum((Decimal(item["amount"]) for item in allocations), Decimal(0)) + return ReportPage( + SectionIntro("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."), + AllocationTable(allocations, total), + Box(Text("TOTAL OPERATING ALLOCATION · USD", class_name="kicker"), Strong(money(total)), + class_name="allocation-total"), + Box(Section(Heading("What we count."), + Paragraph("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.", class_name="note"), + Paragraph("Quarterly counts show participation within each period and must not be added to " + "estimate unique annual reach.", class_name="note", style=style(margin_top=pt(11)))), + Section(Heading("What comes next."), + Paragraph("Extend evening access. Improve step-free entrances. Publish a simple monthly view " + "of program activity and spending.", class_name="note"), + Paragraph(Lines("A shared tomorrow", "is built in the everyday."), class_name="serif", + style=style(font_size=pt(21), line_height=1.15, margin_top=pt(19), color="#b44123")), + class_name="callout"), class_name="grid-2"), + section="03 / OUR RESOURCES", footer="COMMON GROUND / METHODS & ALLOCATIONS", number="FICTIONAL DATA · 03 / 03", + ) + + +def common_ground_report(report) -> Document: + return design_document(CoverPage(), ReachPage(report), ResourcesPage(report), + kind="report", title="Common Ground — Community Review 2026") diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/requirements-components.txt b/examples/showcase-authoring-alpha6/examples/design_showcase/requirements-components.txt new file mode 100644 index 0000000..63061d3 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/requirements-components.txt @@ -0,0 +1 @@ +fullbleed==2.6.0a6 diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/statement.css b/examples/showcase-authoring-alpha6/examples/design_showcase/statement.css new file mode 100644 index 0000000..c5c05c1 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/statement.css @@ -0,0 +1,25 @@ +.statement { background: #f8f4f6; color: #382047; } +.statement .masthead { display: flex; justify-content: space-between; align-items: center; border-bottom: .8pt solid #705876; padding-bottom: 17pt; } +.statement .brand { font: 24pt 'DM Serif Display'; } +.statement .title-row { display: grid; grid-template-columns: 2fr 1fr; gap: 21pt; margin: 23pt 0 18pt; } +.statement h1 { font: 40pt/.98 'DM Serif Display'; letter-spacing: -.6pt; } +.statement .account { border-left: .6pt solid #a18caa; padding-left: 17pt; font-size: 8pt; line-height: 1.8; } +.statement .recipient { font-size: 9pt; margin-top: 13pt; } +.statement .balance { background: #e8ddec; border-top: 1pt solid #67456e; padding: 20pt 22pt; margin: 21pt 0; display: grid; grid-template-columns: 2fr 1fr; align-items: center; } +.statement .balance .value { font: 62pt/1 'DM Serif Display'; margin-top: 7pt; letter-spacing: -1pt; } +.statement .balance .tagline { font: 16pt/1.2 'DM Serif Display'; border-left: .6pt solid #94779e; padding-left: 17pt; } +.statement h2 { font: 16pt 'DM Serif Display'; margin-bottom: 10pt; } +.statement .summary td { padding: 7pt 0; font-size: 8.2pt; border-color: #cdbed0; } +.statement .summary .closing { background: #e6d9e9; font-weight: 600; } +.statement .chart { display: flex; height: 92pt; align-items: flex-end; gap: 9pt; border-bottom: .6pt solid #94779e; padding-top: 9pt; } +.statement .chart-col { width: 26pt; flex: 1; min-width: 0; text-align: center; } +.statement .chart-value { font-size: 6.5pt; margin-bottom: 4pt; } +.statement .bar { background: #785487; width: 100%; } +.statement .chart-labels { display: flex; gap: 9pt; margin-top: 5pt; font-size: 6pt; } +.statement .chart-labels span { display: block; flex: 1; text-align: center; } +.statement .transactions { margin-top: 23pt; } +.statement .transactions td { padding: 8pt 0; font-size: 8pt; border-color: #cdbed0; } +.statement .transactions th { border-color: #85648f; } +.statement .closing-note { display: grid; grid-template-columns: 1fr 1fr; gap: 24pt; border-top: 1pt solid #705876; padding-top: 12pt; margin-top: 14pt; } +.statement .closing-note .serif { font-size: 21pt; font-style: italic; line-height: 1.15; } +.statement .footer { border-color: #a28aaa; } diff --git a/examples/showcase-authoring-alpha6/examples/design_showcase/statement.html b/examples/showcase-authoring-alpha6/examples/design_showcase/statement.html new file mode 100644 index 0000000..3bca0e7 --- /dev/null +++ b/examples/showcase-authoring-alpha6/examples/design_showcase/statement.html @@ -0,0 +1,9 @@ +Hillside Collective — Member Statement +
Hillside Collective
PEOPLE. PURPOSE.
POSSIBILITY.
+

MEMBER
STATEMENT

{{member_name}}
{{address}}

+

CURRENT BALANCE · USD

{{bal}}

A little
contribution.
A stronger
community.

+

Account summary

Previous balance{{opening_balance}}
Contributions$250.00
Payments−$50.00
Current balance{{closing_balance}}

Contributions / last 6 months

{{statement_bars}}
APRMAYJUNJULAUGSEP
+

Recent activity

DATEDESCRIPTIONAMOUNTBALANCE
01 Sep 2026Member contribution+$125.00{{first_balance}}
15 Sep 2026Member contribution+$125.00{{second_balance}}
22 Sep 2026Community workshop−$50.00{{closing_balance}}
+

People create
brighter communities.

Questions about this statement?
membership@example.invalid
Please quote {{account_id}}.

+ +
diff --git a/examples/showcase-authoring-alpha6/requirements.txt b/examples/showcase-authoring-alpha6/requirements.txt new file mode 100644 index 0000000..63061d3 --- /dev/null +++ b/examples/showcase-authoring-alpha6/requirements.txt @@ -0,0 +1 @@ +fullbleed==2.6.0a6 diff --git a/examples/showcase-authoring-alpha6/tools/check_showcase_preview.py b/examples/showcase-authoring-alpha6/tools/check_showcase_preview.py new file mode 100644 index 0000000..7395690 --- /dev/null +++ b/examples/showcase-authoring-alpha6/tools/check_showcase_preview.py @@ -0,0 +1,302 @@ +"""Exercise the actual local preview server against a disposable source copy. + +Requires the installed component alpha and the standard library. Source edits, +syntax errors, missing inputs, timeout recovery and both authoring paths cross +real process/HTTP boundaries. The original showcase source is never modified. +""" +from __future__ import annotations + +import argparse +from hashlib import sha256 +import json +import os +from pathlib import Path +import shutil +import signal +import subprocess +import sys +import time +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + + +class Server: + def __init__(self, project, output, document="invoice", authoring="components", timeout=15, python=None): + self.project, self.output = project, output + output.mkdir(parents=True) + self.log = (output / "server.stdout").open("wb") + self.errors = (output / "server.stderr").open("wb") + self.process = subprocess.Popen( + [str(python or sys.executable), str(project / "preview.py"), "--document", document, + "--authoring", authoring, "--timeout", str(timeout)], + cwd=project, stdin=subprocess.DEVNULL, stdout=self.log, stderr=self.errors, + env={**os.environ, "PYTHONIOENCODING": "utf-8"}, + ) + deadline = time.monotonic() + 20 + while time.monotonic() < deadline: + text = (output / "server.stdout").read_text(encoding="utf-8") + if "\n" in text: + self.url = json.loads(text.splitlines()[0])["url"] + break + if self.process.poll() is not None: + raise AssertionError((output / "server.stderr").read_text(encoding="utf-8")) + time.sleep(0.05) + else: + self.close() + raise AssertionError("Preview server did not start") + + def close(self): + if self.process.poll() is None: + if os.name == "nt": + subprocess.run(["taskkill", "/PID", str(self.process.pid), "/T", "/F"], + capture_output=True, check=True) + else: + self.process.send_signal(signal.SIGINT) + self.process.wait(timeout=15) + if os.name != "nt": + assert self.process.returncode == 0, "Ctrl-C must stop the watcher cleanly" + self.log.close() + self.errors.close() + + def fetch(self, path, *, headers=None, expected=200): + request = Request(self.url + path, headers=headers or {}) + try: + response = urlopen(request, timeout=10) + except HTTPError as error: + response = error + with response: + assert response.status == expected, (path, response.status, response.read()) + return response.read(), dict(response.headers) + + def status(self): + return json.loads(self.fetch("status")[0]) + + def wait(self, phase="ready", after=0, timeout=45): + deadline = time.monotonic() + timeout + latest = None + while time.monotonic() < deadline: + assert self.process.poll() is None, "Preview process stopped" + try: + latest = self.status() + if latest["phase"] == phase and latest["attempt"] > after: + return latest + except URLError: + pass + time.sleep(0.1) + raise AssertionError(f"Did not reach {phase!r} after attempt {after}: {latest}") + + def artifacts(self, label, state): + folder = self.output / label + folder.mkdir() + (folder / "status.json").write_text(json.dumps(state, indent=2), encoding="utf-8") + for name in state["result"]["files"]: + data, headers = self.fetch(f"revisions/{state['revision']}/{name}") + (folder / name).write_bytes(data) + assert headers["Cache-Control"] == "no-store" + assert headers["X-Content-Type-Options"] == "nosniff" + if not name.endswith(".png"): + assert headers["Content-Disposition"].startswith("attachment;") + assert sha256((folder / "document.pdf").read_bytes()).hexdigest() == state["result"]["pdf_sha256"] + return folder + + +def edit(path, old, new): + text = path.read_text(encoding="utf-8") + assert text.count(old) == 1, (path, old) + path.write_text(text.replace(old, new), encoding="utf-8", newline="\n") + + +def pdf_text(path): + import fullbleed + result = fullbleed.extract_pdf_page_texts(str(path)) + return " ".join(" ".join(page["text"] for page in result["pages"]).split()) + + +def check_components(project, out): + server = Server(project, out) + checks = [] + try: + initial = server.wait() + initial_files = server.artifacts("initial", initial) + assert initial["result"]["page_count"] == 1 + assert b"" in pdf_text(files / "document.pdf") + checks.append("JSON save and escaped content") + + good_source = component.read_text(encoding="utf-8") + component.write_text(good_source + "\nthis is a syntax error !!!\n", encoding="utf-8") + failed = server.wait("error", after=data_changed["attempt"]) + assert "SyntaxError" in failed["error"] + assert failed["revision"] == data_changed["revision"] + assert server.fetch(f"revisions/{failed['revision']}/document.pdf")[0] == (files / "document.pdf").read_bytes() + server.artifacts("syntax-error-retained-preview", failed) + component.write_text(good_source, encoding="utf-8") + recovered = server.wait(after=failed["attempt"]) + assert recovered["result"]["pdf_sha256"] == data_changed["result"]["pdf_sha256"] + checks.append("syntax error retains last good revision and recovers") + + # Rename emulates the temporary absence during an editor's atomic save. + moved = project / "data.json.saved" + assert project.resolve() in moved.resolve().parents + data_path.rename(moved) + missing = server.wait("error", after=recovered["attempt"]) + assert "FileNotFoundError" in missing["error"] + moved.rename(data_path) + restored = server.wait(after=missing["attempt"]) + assert restored["result"]["pdf_sha256"] == recovered["result"]["pdf_sha256"] + checks.append("missing input stays watched and recovers") + + # Give one in-progress render time to observe an old input, then save + # another input. The intermediate mixed build must not be published. + component.write_text(good_source + "\nimport time\ntime.sleep(1.5)\n", encoding="utf-8") + busy = server.wait("building", after=restored["attempt"]) + data["invoice"][0]["description"] = "Saved during rendering" + data_path.write_text(json.dumps(data), encoding="utf-8") + pending = server.wait(after=busy["attempt"]) + files = server.artifacts("edit-during-render", pending) + assert pending["revision"] == restored["revision"] + 1 + assert "Saved during rendering" in pdf_text(files / "document.pdf") + checks.append("save during a render queues a fresh rebuild without publishing mixed inputs") + component.write_text(good_source, encoding="utf-8") + settled = server.wait(after=pending["attempt"]) + attempt = settled["attempt"] + time.sleep(0.7) + assert server.status()["attempt"] == attempt + checks.append("render artifacts do not trigger a rebuild loop") + finally: + server.close() + return checks + + +def check_report(project, out, authoring): + server = Server(project, out, "report", authoring) + try: + initial = server.wait() + files = server.artifacts("initial", initial) + assert initial["result"]["page_count"] == 3 + assert "What we count." in pdf_text(files / "document.pdf") + if authoring == "components": + component = project / "report_components.py" + edit(component, "CoverPage(), ReachPage(report), ResourcesPage(report)", "CoverPage()") + else: + template = project / "report.html" + text = template.read_text(encoding="utf-8") + # Keep the complete first authored page and the document close. + end = text.index('') + len('') + template.write_text(text[:end] + '', encoding="utf-8") + reduced = server.wait(after=initial["attempt"]) + server.artifacts("one-page", reduced) + assert reduced["result"]["page_count"] == 1 + assert "page-2.png" not in reduced["result"]["files"] + server.fetch(f"revisions/{reduced['revision']}/page-2.png", expected=404) + return [f"{authoring}: three-page report and removal of obsolete page artifacts"] + finally: + server.close() + + +def check_timeout(project, out): + server = Server(project, out, timeout=5) + try: + initial = server.wait() + component = project / "invoice_components.py" + before = component.read_text(encoding="utf-8") + component.write_text(before + "\nimport time\ntime.sleep(30)\n", encoding="utf-8") + failed = server.wait("error", after=initial["attempt"]) + server.artifacts("timeout", failed) + assert "exceeded 5 seconds" in failed["error"], failed + assert failed["revision"] == initial["revision"] + component.write_text(before, encoding="utf-8") + recovered = server.wait(after=failed["attempt"]) + assert recovered["result"]["pdf_sha256"] == initial["result"]["pdf_sha256"] + return ["hung render is terminated, last good preview retained, next save recovers"] + finally: + server.close() + + +def check_html_invoice(project, out): + server = Server(project, out, authoring="html") + try: + initial = server.wait() + server.artifacts("initial", initial) + edit(project / "invoice.html", "Maple & Finch", "Holly & Pine") + updated = server.wait(after=initial["attempt"]) + files = server.artifacts("edited", updated) + assert "Holly & Pine" in pdf_text(files / "document.pdf") + return ["HTML invoice source edit refreshes its PDF and separate HTML/CSS downloads"] + finally: + server.close() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project", type=Path, default=Path(__file__).resolve().parents[1] / "examples/design_showcase") + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + out = args.out.resolve() + out.mkdir(parents=True, exist_ok=False) + checks = [] + for name, callback in [("component-edits", check_components), + ("invoice-html", check_html_invoice), + ("report-components", lambda project, output: check_report(project, output, "components")), + ("report-html", lambda project, output: check_report(project, output, "html")), + ("timeout", check_timeout)]: + project = out / name / "project" + shutil.copytree(args.project, project, ignore=shutil.ignore_patterns("output", "__pycache__", "*.pyc")) + checks.extend(callback(project, out / name / "evidence")) + print(json.dumps({"case": name, "ok": True}), flush=True) + matching_pages = [] + for document, left, right, count in [("invoice", "component-edits", "invoice-html", 1), + ("report", "report-components", "report-html", 3)]: + for page in range(1, count + 1): + name = f"page-{page}.png" + a = (out / left / "evidence/initial" / name).read_bytes() + b = (out / right / "evidence/initial" / name).read_bytes() + assert a == b, (document, page) + matching_pages.append(dict(document=document, page=page, sha256=sha256(a).hexdigest())) + result = dict(ok=True, checks=checks, python=sys.version, platform=sys.platform, + original_authoring_page_matches=matching_pages, + scope="Real local development server and edited synthetic showcase; not an engine conformance or delivery gate.") + (out / "verification.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + print(json.dumps(result), flush=True) + + +if __name__ == "__main__": + main() diff --git a/examples/showcase-authoring-alpha6/tools/check_showcase_preview_browser.py b/examples/showcase-authoring-alpha6/tools/check_showcase_preview_browser.py new file mode 100644 index 0000000..ef3a53b --- /dev/null +++ b/examples/showcase-authoring-alpha6/tools/check_showcase_preview_browser.py @@ -0,0 +1,117 @@ +"""Verify automatic refresh, errors and real downloads in a fresh browser.""" +from __future__ import annotations + +import argparse +from hashlib import sha256 +import json +from pathlib import Path +import shutil +import sys + +from playwright.sync_api import expect, sync_playwright +from check_showcase_preview import Server, edit + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project", type=Path, default=Path(__file__).resolve().parents[1] / "examples/design_showcase") + parser.add_argument("--python", type=Path, default=Path(sys.executable), help="Python with the installed component alpha") + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--channel", help="Optional Playwright browser channel, e.g. chrome") + args = parser.parse_args() + # Make the command independent of cwd without following a venv's interpreter symlink. + args.python = args.python.absolute() + out = args.out.resolve() + out.mkdir(parents=True, exist_ok=False) + project = out / "project" + shutil.copytree(args.project, project, ignore=shutil.ignore_patterns("output", "__pycache__", "*.pyc")) + server = Server(project, out / "server-invoice", python=args.python) + page_errors, screenshots, downloads = [], [], [] + + def capture(page, name): + page.locator("#pages img").evaluate_all("images => Promise.all(images.map(image => image.decode()))") + assert page.evaluate("document.documentElement.scrollWidth <= innerWidth + 1"), "Horizontal page overflow" + page.screenshot(path=str(out / name), animations="disabled") + screenshots.append(name) + + try: + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True, **({"channel": args.channel} if args.channel else {})) + context = browser.new_context(accept_downloads=True, viewport={"width": 1440, "height": 1000}) + page = context.new_page() + page.on("pageerror", lambda error: page_errors.append(str(error))) + initial = server.wait() + page.goto(server.url) + expect(page.get_by_role("status")).to_have_text("Preview up to date") + expect(page.locator("#pages img")).to_have_count(1) + for label, width, height in [("desktop", 1440, 1000), ("mobile", 390, 844), ("narrow", 320, 740)]: + page.set_viewport_size({"width": width, "height": height}) + capture(page, f"invoice-{label}.png") + page.set_viewport_size({"width": 1440, "height": 1000}) + for label, name in [("Download PDF", "document.pdf"), ("Styled HTML", "document.styled.html"), + ("Unstyled HTML", "document.unstyled.html")]: + with page.expect_download() as pending: + page.get_by_role("link", name=label, exact=True).click() + download = pending.value + assert download.suggested_filename == name + destination = out / name + download.save_as(destination) + expected = server.fetch(f"revisions/{initial['revision']}/{name}")[0] + assert destination.read_bytes() == expected + downloads.append({"file": name, "sha256": sha256(expected).hexdigest()}) + + component = project / "invoice_components.py" + edit(component, 'Strong("Maple & Finch")', 'Strong("Maple & Cedar")') + changed = server.wait(after=initial["attempt"]) + expect(page.locator("#pages img")).to_have_attribute("src", f"revisions/{changed['revision']}/page-1.png") + capture(page, "invoice-edited.png") + + good = component.read_text(encoding="utf-8") + component.write_text(good + "\nthis is a syntax error !!!\n", encoding="utf-8") + failure = server.wait("error", after=changed["attempt"]) + expect(page.get_by_role("status")).to_have_text("This edit could not be rendered") + expect(page.get_by_role("alert")).to_contain_text("SyntaxError") + expect(page.get_by_role("link", name="Download last PDF", exact=True)).to_be_visible() + expect(page.locator("#pages img")).to_have_attribute("src", f"revisions/{changed['revision']}/page-1.png") + page.set_viewport_size({"width": 390, "height": 844}) + capture(page, "error-mobile.png") + component.write_text(good, encoding="utf-8") + recovered = server.wait(after=failure["attempt"]) + expect(page.get_by_role("status")).to_have_text("Preview up to date") + expect(page.get_by_role("alert")).to_be_hidden() + expect(page.locator("#pages img")).to_have_attribute("src", f"revisions/{recovered['revision']}/page-1.png") + capture(page, "recovered-mobile.png") + + server.close() + server = Server(project, out / "server-report", document="report", python=args.python) + report = server.wait() + page.goto(server.url) + expect(page.locator("#pages img")).to_have_count(3) + page.set_viewport_size({"width": 1440, "height": 1000}) + capture(page, "report-three-pages.png") + edit(project / "report_components.py", "CoverPage(), ReachPage(report), ResourcesPage(report)", "CoverPage()") + reduced = server.wait(after=report["attempt"]) + expect(page.locator("#pages img")).to_have_count(1) + expect(page.locator("#meta")).to_contain_text("1 page") + expect(page.locator("#pages img")).to_have_attribute("src", f"revisions/{reduced['revision']}/page-1.png") + capture(page, "report-one-page.png") + + server.close() + expect(page.get_by_role("status")).to_have_text("Preview server disconnected", timeout=15000) + for identifier in ["pdf", "source", "extra"]: + expect(page.locator("#" + identifier)).to_have_attribute("aria-disabled", "true") + capture(page, "disconnected.png") + context.close() + browser.close() + assert not page_errors, page_errors + finally: + server.close() + result = dict(ok=True, screenshots=screenshots, downloads=downloads, page_errors=page_errors, + widths=[1440, 390, 320], automatic_refresh=True, error_recovery=True, + stale_preview_label=True, page_count_reduction=True, disconnected_downloads_disabled=True) + (out / "verification.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + print(json.dumps(result)) + + +if __name__ == "__main__": + main() diff --git a/tools/build_accessible_showcase.py b/tools/build_accessible_showcase.py index 5b1fa78..87b5edd 100644 --- a/tools/build_accessible_showcase.py +++ b/tools/build_accessible_showcase.py @@ -36,5 +36,5 @@ def main(channel='alpha4'): if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__) - parser.add_argument('--channel',choices=('alpha4','alpha5'),default='alpha4') + parser.add_argument('--channel',choices=('alpha4','alpha5','alpha6'),default='alpha4') main(parser.parse_args().channel) diff --git a/tools/build_bootstrap_preview_starter.py b/tools/build_bootstrap_preview_starter.py index 7942285..0784fc8 100644 --- a/tools/build_bootstrap_preview_starter.py +++ b/tools/build_bootstrap_preview_starter.py @@ -63,5 +63,5 @@ def build(channel="preview"): if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--channel", choices=("preview", "alpha", "alpha2", "alpha3", "alpha4", "alpha5"), default="preview") + parser.add_argument("--channel", choices=("preview", "alpha", "alpha2", "alpha3", "alpha4", "alpha5", "alpha6"), default="preview") build(parser.parse_args().channel) diff --git a/tools/build_component_notebook_samples.py b/tools/build_component_notebook_samples.py index 496f3b1..71648a1 100644 --- a/tools/build_component_notebook_samples.py +++ b/tools/build_component_notebook_samples.py @@ -9,15 +9,18 @@ ROOT = Path(__file__).resolve().parents[1] parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--verification-dir", required=True, type=Path) +parser.add_argument("--channel", choices=("alpha3", "alpha5", "alpha6"), default="alpha3") args = parser.parse_args() checked = args.verification_dir.resolve() report = json.loads((checked / "verification.json").read_text(encoding="utf-8")) -source = ROOT / "examples/component-notebook/bootstrap_components.ipynb" -assert report["ok"] and report["runtime_version"] == "2.6.0a3" +folder = "component-notebook" if args.channel == "alpha3" else f"component-notebook-{args.channel}" +runtime_version = {"alpha3": "2.6.0a3", "alpha5": "2.6.0a5", "alpha6": "2.6.0a6"}[args.channel] +source = ROOT / "examples" / folder / "bootstrap_components.ipynb" +assert report["ok"] and report["runtime_version"] == runtime_version assert report["notebook_sha256"] == sha256(source.read_bytes()).hexdigest() assert [r["name"] for r in report["cases"]] == ["default", "edited", "long", "empty-after-long"] assert report["stale_pages_excluded_from_download"] -assets = ROOT / "docs/assets/component-notebook" +assets = ROOT / "docs/assets" / folder assets.mkdir(parents=True, exist_ok=True) for name in ("order.pdf", "order.styled.html", "order.unstyled.html"): data = (checked / "default" / name).read_bytes() @@ -38,5 +41,9 @@ "Static styled and unstyled HTML", "Exact ZIP contents", "Long-to-empty rerun excludes old pages"], "scope": report["scope"], } +if all(row.get("rich_cell_components_checked") for row in report["cases"]): + for row in manifest["cases"]: + row["rich_cell_components_checked"] = True + manifest["checks"].insert(5, "Rich item cell emphasis, order and escaped text in both HTML exports") (assets / "verification.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8", newline="\n") print(json.dumps({"ok": True, "downloads": len(manifest["downloads"]), "cases": len(manifest["cases"])})) diff --git a/tools/build_showcase_authoring.py b/tools/build_showcase_authoring.py index 22eff8e..e563d67 100644 --- a/tools/build_showcase_authoring.py +++ b/tools/build_showcase_authoring.py @@ -38,5 +38,5 @@ def main(channel="alpha4"): if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--channel", choices=("alpha4", "alpha5"), default="alpha4") + parser.add_argument("--channel", choices=("alpha4", "alpha5", "alpha6"), default="alpha4") main(parser.parse_args().channel) diff --git a/tools/check_accessible_showcase_ui.py b/tools/check_accessible_showcase_ui.py index 12093cd..bdff5fa 100644 --- a/tools/check_accessible_showcase_ui.py +++ b/tools/check_accessible_showcase_ui.py @@ -26,7 +26,7 @@ def main(): server=ThreadingHTTPServer(('127.0.0.1',0),partial(Quiet,directory=str(args.site.resolve()))) threading.Thread(target=server.serve_forever,daemon=True).start() base=f'http://127.0.0.1:{server.server_port}' - assets=ROOT/'docs/assets/accessible-showcase-alpha5' + assets=ROOT/'docs/assets/accessible-showcase-alpha6' manifest=json.loads((assets/'source.json').read_text(encoding='utf-8')) validation=json.loads((assets/'validation.json').read_text(encoding='utf-8')) cases=[] @@ -49,7 +49,7 @@ def main(): target=args.out/f'project-{width}.zip' downloaded.value.save_as(str(target)) assert sha256(target.read_bytes()).hexdigest()==manifest['zip_sha256'] - pdf=context.request.get(base+'/assets/accessible-showcase-alpha5/common-ground-guide.pdf') + pdf=context.request.get(base+'/assets/accessible-showcase-alpha6/common-ground-guide.pdf') assert pdf.status==200 and sha256(pdf.body()).hexdigest()==validation['pdf_sha256'] page.get_by_role('link',name='Open the responsive HTML',exact=True).click() page.wait_for_load_state('networkidle');page.evaluate('document.fonts.ready') @@ -61,7 +61,7 @@ def main(): page.keyboard.press('Tab') assert page.evaluate('document.activeElement.tagName')=='A' page.screenshot(path=str(args.out/f'html-{width}.png'),full_page=True) - page.goto(base+'/assets/accessible-showcase-alpha5/guide-unstyled.html',wait_until='networkidle') + page.goto(base+'/assets/accessible-showcase-alpha6/guide-unstyled.html',wait_until='networkidle') assert page.locator('style,[style],link[rel=stylesheet]').count()==0 assert page.locator('.program-card').count()==3 and page.locator('th[scope=col]').count()==3 assert not errors,errors diff --git a/tools/check_bootstrap_preview_ui.py b/tools/check_bootstrap_preview_ui.py index 1b49a98..b6bfe22 100644 --- a/tools/check_bootstrap_preview_ui.py +++ b/tools/check_bootstrap_preview_ui.py @@ -19,7 +19,7 @@ def main(): args = parser.parse_args() root = args.site.resolve() args.out.mkdir(parents=True, exist_ok=True) - assets = root / "assets/bootstrap-alpha5" + assets = root / "assets/bootstrap-alpha6" source = json.loads((assets / "source.json").read_text(encoding="utf-8")) manifest = json.loads((assets / "verification.json").read_text(encoding="utf-8")) assert source["source_commit"] == manifest["source_commit"] @@ -56,10 +56,10 @@ def log_message(self, *a): assert response.status == 200 assert page.get_by_role("heading", name="Bootstrap components, authored in Python", exact=False).is_visible() article = page.locator("article").inner_text() - for value in ("Opt-in alpha 2.6.0a5", "fullbleed.ui.experimental", + for value in ("Opt-in alpha 2.6.0a6", "fullbleed.ui.experimental", "static, including HTML export", "class_name", "include_styles=True", "include_styles=False", "python first_document.py", - 'python -m pip install --pre "fullbleed==2.6.0a5"'): + 'python -m pip install --pre "fullbleed==2.6.0a6"'): assert value in article, value assert page.get_by_role("link", name="alpha release", exact=True).get_attribute("href") == source["engine_wheels"]["release_url"] legacy = page.get_by_role("link", name="pinned preview kit", exact=True) @@ -104,7 +104,7 @@ def log_message(self, *a): if width >= 390: assert page.evaluate("document.documentElement.scrollWidth <= innerWidth + 1") page.screenshot(path=str(args.out / f"{label}-catalog.png"), animations="disabled") - response = page.goto(base + "/assets/bootstrap-alpha5/catalog.unstyled.html", wait_until="networkidle") + response = page.goto(base + "/assets/bootstrap-alpha6/catalog.unstyled.html", wait_until="networkidle") assert response.status == 200 assert page.locator("style, [style], link[rel=stylesheet], script").count() == 0 assert page.locator('[id^="sample-"]').count() == 27 diff --git a/tools/check_component_notebook_ui.py b/tools/check_component_notebook_ui.py index b01f6d4..c9abba2 100644 --- a/tools/check_component_notebook_ui.py +++ b/tools/check_component_notebook_ui.py @@ -20,7 +20,7 @@ def main(): args = parser.parse_args() root = args.site.resolve() args.out.mkdir(parents=True, exist_ok=True) - folder = "component-notebook-alpha5" + folder = "component-notebook-alpha6" assets = root / "assets" / folder manifest = json.loads((assets / "verification.json").read_text(encoding="utf-8")) @@ -52,7 +52,7 @@ def log_message(self, *a): assert response.status == 200 assert page.get_by_role("heading", name="Try Python components in a notebook", exact=False).is_visible() article = page.locator("article").inner_text() - for value in ("Fullbleed 2.6.0a5", "fullbleed.ui.experimental", "no Bootstrap JavaScript", + for value in ("Fullbleed 2.6.0a6", "fullbleed.ui.experimental", "no Bootstrap JavaScript", "class_name", "Document(styles=...)", "Strong", "72-row", "four pages"): assert value in article, value colab = page.get_by_role("link", name="Open in Colab", exact=True).get_attribute("href") diff --git a/tools/check_showcase_authoring_ui.py b/tools/check_showcase_authoring_ui.py index 17aedf1..fbf98ba 100644 --- a/tools/check_showcase_authoring_ui.py +++ b/tools/check_showcase_authoring_ui.py @@ -33,7 +33,7 @@ def main(): server = ThreadingHTTPServer(("127.0.0.1", 0), partial(QuietHandler, directory=str(args.site.resolve()))) threading.Thread(target=server.serve_forever, daemon=True).start() base = f"http://127.0.0.1:{server.server_port}" - manifest = json.loads((ROOT / "docs/assets/showcase-authoring-alpha5/source.json").read_text(encoding="utf-8")) + manifest = json.loads((ROOT / "docs/assets/showcase-authoring-alpha6/source.json").read_text(encoding="utf-8")) cases = [] try: with sync_playwright() as playwright: @@ -64,7 +64,7 @@ def main(): download.save_as(str(destination)) assert sha256(destination.read_bytes()).hexdigest() == manifest["zip_sha256"] for filename in ("invoice-html.pdf", "invoice-components.pdf", "report-html.pdf", "report-components.pdf"): - href = f"/assets/showcase-authoring-alpha5/{filename}" + href = f"/assets/showcase-authoring-alpha6/{filename}" assert page.locator(f'a[href$="{filename}"]').count() >= 1 pdf = context.request.get(base + href) assert pdf.status == 200 and pdf.body() == (ROOT / "docs" / href.lstrip("/")).read_bytes() diff --git a/tools/verify_accessible_showcase.py b/tools/verify_accessible_showcase.py index b8899e3..6f3a979 100644 --- a/tools/verify_accessible_showcase.py +++ b/tools/verify_accessible_showcase.py @@ -14,7 +14,7 @@ def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--out',type=Path,default=ROOT/'target/accessible-showcase') - parser.add_argument('--channel',choices=('alpha4','alpha5'),default='alpha4') + parser.add_argument('--channel',choices=('alpha4','alpha5','alpha6'),default='alpha4') args=parser.parse_args() suffix='' if args.channel=='alpha4' else '-'+args.channel assets=ROOT/('docs/assets/accessible-showcase'+suffix) diff --git a/tools/verify_bootstrap_preview_starter.py b/tools/verify_bootstrap_preview_starter.py index 5ac9241..9ba77b5 100644 --- a/tools/verify_bootstrap_preview_starter.py +++ b/tools/verify_bootstrap_preview_starter.py @@ -19,7 +19,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--python", default=sys.executable) - parser.add_argument("--channel", choices=("preview", "alpha", "alpha2", "alpha3", "alpha4", "alpha5"), default="preview") + parser.add_argument("--channel", choices=("preview", "alpha", "alpha2", "alpha3", "alpha4", "alpha5", "alpha6"), default="preview") parser.add_argument("--install-report", type=Path) parser.add_argument("--out", type=Path, default=ROOT / "target/bootstrap-preview") args = parser.parse_args() diff --git a/tools/verify_component_notebook.py b/tools/verify_component_notebook.py index a899c51..e2ec547 100644 --- a/tools/verify_component_notebook.py +++ b/tools/verify_component_notebook.py @@ -27,6 +27,7 @@ CHANNELS = { "alpha3": ("2.6.0a3", "component-notebook"), "alpha5": ("2.6.0a5", "component-notebook-alpha5"), + "alpha6": ("2.6.0a6", "component-notebook-alpha6"), } @@ -234,7 +235,7 @@ def main(): edited.update(number="1051", customer="Moss & Fern ", status="Packed") edited["items"][0]["quantity"] = 32 edited["items"][1]["item"] = "Personalized note cards" - if args.channel == "alpha5": + if args.channel in {"alpha5", "alpha6"}: edited["items"][0]["item"] = "Studio & cards" long = deepcopy(default) long["items"] = [{"item": f"Edition {n:02d} / art print", "quantity": 1, "unit_price": "9.50"} @@ -265,7 +266,7 @@ def main(): destination = out / name destination.mkdir() execute(notebook, work, destination, kernel_dir) - report = verify(work, destination, name, order, accent, version, args.channel == "alpha5") + report = verify(work, destination, name, order, accent, version, args.channel in {"alpha5", "alpha6"}) reports.append(report) print(json.dumps({"case": name, "pages": report["pages"], "ok": True}), flush=True) assert len({r["pdf_sha256"] for r in reports}) == len(reports) diff --git a/tools/verify_showcase_authoring.py b/tools/verify_showcase_authoring.py index 64f807b..f4782fc 100644 --- a/tools/verify_showcase_authoring.py +++ b/tools/verify_showcase_authoring.py @@ -23,7 +23,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--out", type=Path, default=ROOT / "target/showcase-authoring") - parser.add_argument("--channel", choices=("alpha4", "alpha5"), default="alpha4") + parser.add_argument("--channel", choices=("alpha4", "alpha5", "alpha6"), default="alpha4") args = parser.parse_args() suffix = "" if args.channel == "alpha4" else "-" + args.channel assets = ROOT / ("docs/assets/showcase-authoring" + suffix) @@ -31,7 +31,7 @@ def main(): out = args.out.resolve() out.mkdir(parents=True, exist_ok=True) manifest = json.loads((assets / "source.json").read_text(encoding="utf-8")) - assert version("fullbleed") == manifest["engine_version"] == {"alpha4": "2.6.0a4", "alpha5": "2.6.0a5"}[args.channel] + assert version("fullbleed") == manifest["engine_version"] == {"alpha4": "2.6.0a4", "alpha5": "2.6.0a5", "alpha6": "2.6.0a6"}[args.channel] archive = assets / "project.zip" assert sha256(archive.read_bytes()).hexdigest() == manifest["zip_sha256"] project = out / "project"