From 59d1269366e9a8b2dcf943d8f8d18a3629dd6fb7 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 23 Sep 2026 04:53:49 +0000 Subject: [PATCH] fix: make report page artwork opt-in --- docs/template-pack.md | 9 +++++ server/templateEngine.js | 16 +++++++-- server/templateEngine.test.js | 28 ++++++++++++++++ server/templatePack.js | 3 ++ src/App.tsx | 62 ++++++++++++++++++++++++++++++++++- src/refine.css | 45 +++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 4 deletions(-) diff --git a/docs/template-pack.md b/docs/template-pack.md index 3197ee3..11a2899 100644 --- a/docs/template-pack.md +++ b/docs/template-pack.md @@ -37,6 +37,9 @@ my-pack.zip "pageMarginLeft": 12, "pagePadding": 18, "pageBackgroundImage": "asset://paper-bg", + "pageBackgroundMode": "none", + "pageBackgroundOpacity": 0.08, + "decorativeBorder": false, "borderImage": "asset://page-border" }, "blocks": [ @@ -118,6 +121,12 @@ Report themes can set A4 print margins independently in millimetres with `pageMa `pagePadding` is separate: it controls the internal spacing inside the rendered document/frame in pixels and defaults to 18 px. +### Page artwork + +Reports default to a clean page even if the ZIP includes background or border assets. Set `pageBackgroundMode` to `"watermark"` or `"full"` to opt in to the background image. `pageBackgroundOpacity` controls artwork strength from 0 to 0.35. + +Decorative border artwork is also opt-in with `decorativeBorder: true`. This prevents stationery-style page chrome from appearing automatically on operational reports. + ### Styling The constrained theme supports colors, border radius, background imagery and report border imagery. Blocks may also include a `style` object with background/text/border colors, border width/radius, padding and alignment. Arbitrary CSS/HTML/JavaScript is intentionally not supported. diff --git a/server/templateEngine.js b/server/templateEngine.js index d01c37e..033f63c 100644 --- a/server/templateEngine.js +++ b/server/templateEngine.js @@ -47,6 +47,10 @@ function safeFont(value) { return allowed.includes(font) ? font : 'Arial' } +function safeBackgroundMode(value) { + return ['none', 'watermark', 'full'].includes(value) ? value : 'none' +} + function themeOf(template) { const theme = template?.theme || {} return { @@ -65,6 +69,9 @@ function themeOf(template) { pageMarginLeft: safeNumber(theme.pageMarginLeft, 12, 0, 40), fontFamily: safeFont(theme.fontFamily), pageBackgroundImage: safeUrl(theme.pageBackgroundImage, {}), + pageBackgroundMode: safeBackgroundMode(theme.pageBackgroundMode), + pageBackgroundOpacity: safeNumber(theme.pageBackgroundOpacity, 0.08, 0, 0.35), + decorativeBorder: theme.decorativeBorder === true, borderImage: safeUrl(theme.borderImage, {}) } } @@ -176,13 +183,16 @@ function blockHtml(block, data, theme) { export function renderReportHtml(template, data = {}) { const blocks = template?.blocks || [] const theme = themeOf(template) - const backgroundImage = theme.pageBackgroundImage === '#' ? 'none' : `url("${escapeHtml(theme.pageBackgroundImage)}")` - const borderImage = theme.borderImage === '#' ? 'none' : `url("${escapeHtml(theme.borderImage)}") 30 stretch` + const hasBackground = theme.pageBackgroundImage !== '#' && theme.pageBackgroundMode !== 'none' + const backgroundImage = hasBackground ? `url("${escapeHtml(theme.pageBackgroundImage)}")` : 'none' + const borderEnabled = theme.decorativeBorder && theme.borderImage !== '#' + const borderImage = borderEnabled ? `url("${escapeHtml(theme.borderImage)}") 30 stretch` : 'none' + const backgroundSize = theme.pageBackgroundMode === 'full' ? 'cover' : '100% auto' return `
${blocks.map(block => blockHtml(block, data, theme)).join('\n')}
` } diff --git a/server/templateEngine.test.js b/server/templateEngine.test.js index 95e43df..1b5acd0 100644 --- a/server/templateEngine.test.js +++ b/server/templateEngine.test.js @@ -80,3 +80,31 @@ test('report renderer applies independent page margins in millimetres', () => { assert.match(html, /min-height:calc\(297mm - 22mm\)/) assert.match(html, /padding:22px/) }) + +test('report page chrome defaults to clean without stacked artwork', () => { + const html = renderReportHtml({ + theme: { + pageBackgroundImage: 'https://example.com/paper.webp', + borderImage: 'https://example.com/border.png' + }, + blocks: [{ type: 'text', props: { text: 'Clean report' } }] + }, {}) + assert.match(html, /background-image:none/) + assert.match(html, /border:0px solid transparent/) +}) + +test('report page chrome can opt into subtle background and decorative border', () => { + const html = renderReportHtml({ + theme: { + pageBackgroundImage: 'https://example.com/paper.webp', + pageBackgroundMode: 'watermark', + pageBackgroundOpacity: 0.1, + borderImage: 'https://example.com/border.png', + decorativeBorder: true + }, + blocks: [{ type: 'text', props: { text: 'Styled report' } }] + }, {}) + assert.match(html, /paper\.webp/) + assert.match(html, /opacity:0\.1/) + assert.match(html, /border:10px solid transparent/) +}) diff --git a/server/templatePack.js b/server/templatePack.js index 8d380b6..a9c4de8 100644 --- a/server/templatePack.js +++ b/server/templatePack.js @@ -236,6 +236,9 @@ export const templatePackSpec = { pageMarginLeft: 12, pagePadding: 18, pageBackgroundImage: 'asset://paper-bg', + pageBackgroundMode: 'none', + pageBackgroundOpacity: 0.08, + decorativeBorder: false, borderImage: 'asset://page-border' }, blocks: [ diff --git a/src/App.tsx b/src/App.tsx index 7932577..278f428 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -215,6 +215,16 @@ export function App({ user }: Props) { return Number.isFinite(value) ? value : fallback } + function themeString(key: string, fallback: string) { + const value = templateDoc.theme?.[key] + return typeof value === 'string' ? value : fallback + } + + function themeBoolean(key: string, fallback = false) { + const value = templateDoc.theme?.[key] + return typeof value === 'boolean' ? value : fallback + } + function moveBlock(id: string, direction: -1 | 1) { setTemplateDoc(current => { const blocks = [...current.blocks] @@ -567,12 +577,62 @@ export function App({ user }: Props) { /> {themeNumber('pagePadding', 18)} px + +
+ Page artwork +
+ {[ + ['none', 'Clean'], + ['watermark', 'Subtle'], + ['full', 'Full'] + ].map(([value, label]) => ( + + ))} +
+
+ + {themeString('pageBackgroundMode', 'none') !== 'none' && ( + + )} + + + )} diff --git a/src/refine.css b/src/refine.css index 76996ce..5c457ce 100644 --- a/src/refine.css +++ b/src/refine.css @@ -788,3 +788,48 @@ body{ padding:0; font-size:10px; } + +.page-decoration-control{ + display:grid; + gap:7px; + margin-top:12px; +} +.control-label{ + color:#59677a; + font-size:10px; + font-weight:800; +} +.segmented.compact{ + margin:0; +} +.segmented.compact button{ + padding:8px 9px; + font-size:10px; +} +.toggle-row{ + display:flex!important; + align-items:center; + justify-content:space-between; + gap:14px; + margin-top:12px; + padding:10px 0 0; + border-top:1px solid #edf1f5; +} +.toggle-row>span{ + display:grid; + gap:2px; +} +.toggle-row strong{ + color:#263449; + font-size:11px; +} +.toggle-row small{ + color:#8994a4; + font-size:9px; + font-weight:500; +} +.toggle-row input{ + width:18px; + height:18px; + flex:0 0 auto; +}