diff --git a/docs/template-pack.md b/docs/template-pack.md index 798a242..3197ee3 100644 --- a/docs/template-pack.md +++ b/docs/template-pack.md @@ -31,6 +31,11 @@ my-pack.zip "accentColor": "#8b5e34", "borderColor": "#d8c7b3", "borderRadius": 14, + "pageMarginTop": 12, + "pageMarginRight": 12, + "pageMarginBottom": 12, + "pageMarginLeft": 12, + "pagePadding": 18, "pageBackgroundImage": "asset://paper-bg", "borderImage": "asset://page-border" }, @@ -107,6 +112,12 @@ Use `variant: "logo"` for compact brand marks and `variant: "hero"` for banners/ For report pagination, blocks avoid splitting across pages by default. Optional block style flags are `keepWithNext`, `pageBreakBefore`, `pageBreakAfter`, and `allowPageBreak`. +### Page setup + +Report themes can set A4 print margins independently in millimetres with `pageMarginTop`, `pageMarginRight`, `pageMarginBottom`, and `pageMarginLeft`. Each accepts 0–40 mm and defaults to 12 mm. + +`pagePadding` is separate: it controls the internal spacing inside the rendered document/frame in pixels and defaults to 18 px. + ### 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 79ae7f6..d01c37e 100644 --- a/server/templateEngine.js +++ b/server/templateEngine.js @@ -58,7 +58,11 @@ function themeOf(template) { buttonTextColor: safeHex(theme.buttonTextColor, '#ffffff'), borderColor: safeHex(theme.borderColor, '#dbe2ea'), borderRadius: safeNumber(theme.borderRadius, 12, 0, 32), - pagePadding: safeNumber(theme.pagePadding, 18, 0, 40), + pagePadding: safeNumber(theme.pagePadding, 18, 0, 80), + pageMarginTop: safeNumber(theme.pageMarginTop, 12, 0, 40), + pageMarginRight: safeNumber(theme.pageMarginRight, 12, 0, 40), + pageMarginBottom: safeNumber(theme.pageMarginBottom, 12, 0, 40), + pageMarginLeft: safeNumber(theme.pageMarginLeft, 12, 0, 40), fontFamily: safeFont(theme.fontFamily), pageBackgroundImage: safeUrl(theme.pageBackgroundImage, {}), borderImage: safeUrl(theme.borderImage, {}) @@ -178,7 +182,7 @@ export function renderReportHtml(template, data = {}) { return `
${blocks.map(block => blockHtml(block, data, theme)).join('\n')}
` } diff --git a/server/templateEngine.test.js b/server/templateEngine.test.js index c630980..95e43df 100644 --- a/server/templateEngine.test.js +++ b/server/templateEngine.test.js @@ -64,3 +64,19 @@ test('report blocks avoid page breaks by default and support explicit page contr assert.match(html, /break-after:avoid-page/) assert.match(html, /page-break-inside:avoid/) }) + +test('report renderer applies independent page margins in millimetres', () => { + const html = renderReportHtml({ + theme: { + pageMarginTop: 8, + pageMarginRight: 10, + pageMarginBottom: 14, + pageMarginLeft: 16, + pagePadding: 22 + }, + blocks: [{ type: 'text', props: { text: 'Margins' } }] + }, {}) + assert.match(html, /margin:8mm 10mm 14mm 16mm/) + assert.match(html, /min-height:calc\(297mm - 22mm\)/) + assert.match(html, /padding:22px/) +}) diff --git a/server/templatePack.js b/server/templatePack.js index b6fcda6..8d380b6 100644 --- a/server/templatePack.js +++ b/server/templatePack.js @@ -230,6 +230,11 @@ export const templatePackSpec = { accentColor: '#8b5e34', borderColor: '#d8c7b3', borderRadius: 14, + pageMarginTop: 12, + pageMarginRight: 12, + pageMarginBottom: 12, + pageMarginLeft: 12, + pagePadding: 18, pageBackgroundImage: 'asset://paper-bg', borderImage: 'asset://page-border' }, diff --git a/src/App.tsx b/src/App.tsx index c4bde48..7932577 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -203,6 +203,18 @@ export function App({ user }: Props) { })) } + function updateTheme(patch: Record) { + setTemplateDoc(current => ({ + ...current, + theme: { ...(current.theme || {}), ...patch } + })) + } + + function themeNumber(key: string, fallback: number) { + const value = Number(templateDoc.theme?.[key]) + return Number.isFinite(value) ? value : fallback + } + function moveBlock(id: string, direction: -1 | 1) { setTemplateDoc(current => { const blocks = [...current.blocks] @@ -513,6 +525,57 @@ export function App({ user }: Props) { + {active?.type === 'report' && ( +
+
+
+ PAGE SETUP +

A4 margins

+
+ mm +
+
+ {[ + ['Top', 'pageMarginTop', 12], + ['Right', 'pageMarginRight', 12], + ['Bottom', 'pageMarginBottom', 12], + ['Left', 'pageMarginLeft', 12] + ].map(([label, key, fallback]) => ( + + ))} +
+ + +
+ )}
diff --git a/src/refine.css b/src/refine.css index 5df913c..76996ce 100644 --- a/src/refine.css +++ b/src/refine.css @@ -709,3 +709,82 @@ body{ margin-left:auto; } } + +.page-setup-card{ + margin:0 0 14px; + padding:14px; + border:1px solid var(--line); + border-radius:16px; + background:white; + box-shadow:0 3px 12px rgba(15,23,42,.025); +} +.page-setup-heading{ + display:flex; + align-items:flex-end; + justify-content:space-between; + gap:12px; + margin-bottom:12px; +} +.page-setup-heading h3{ + margin:3px 0 0; + font-size:15px; +} +.page-setup-heading>span{ + color:#8994a4; + font-size:10px; + font-weight:800; +} +.page-margin-grid{ + display:grid; + grid-template-columns:repeat(4,minmax(0,1fr)); + gap:7px; +} +.page-margin-grid label{ + display:grid; + gap:5px; + min-width:0; + color:#7b8797; + font-size:9px; + font-weight:800; +} +.page-margin-grid input{ + width:100%; + min-width:0; + border:1px solid #d6dde6; + border-radius:9px; + padding:9px 6px; + text-align:center; + font-size:13px; + font-weight:800; + color:#263449; + background:#f9fafb; +} +.content-padding-control{ + display:grid; + grid-template-columns:auto auto 1fr auto; + align-items:center; + gap:7px; + margin-top:12px; + color:#59677a; + font-size:10px; + font-weight:800; +} +.content-padding-control>span{ + color:#9aa4b2; + font-size:9px; +} +.content-padding-control input{ + width:100%; + min-width:0; +} +.content-padding-control strong{ + min-width:38px; + text-align:right; + color:#263449; + font-size:10px; +} +.page-reset{ + margin-top:10px; + padding:0; + font-size:10px; +}