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;
+}