feat(jsx): reflow dynamic box containers

This commit is contained in:
hand-dot
2026-05-11 15:44:12 +09:00
parent ca6b6dc8f3
commit cfd0c2fc30
19 changed files with 516 additions and 37 deletions

12
PLAN.md
View File

@@ -28,12 +28,12 @@ container、md2pdf pagination / block layout、workspace 運用の順で磨く
### 1. `@pdfme/jsx` layout / dynamic container フォローアップ
- Form 入力や dynamic layout reflow で `Text` / MVT が runtime に `overflow: "expand"` した場合、
JSX の親 `Box` / container は自動では広がらない。生成後の `Box` は rectangle schema であり、
子 schema の実描画高さと親 container を結び直す contract がまだないため。親子 container dynamic
layout は実装したい重要課題として扱う
- まずは JSX から生成された visual `Box` と子 text/MVT の関係を metadata として template に残す案と、
単一 text/MVT 子の decoration に畳む案を比較する。
- JSX から生成された auto-height visual `Box` は、子 schema 名を内部 metadata として持たせ、Form/Viewer
や generator の dynamic layout reflow 時に `overflow: "expand"` した子 text/MVT の高さへ追従する。
- 現在の対象は同一ページ内の parent/child decoration reflow。child schema が page break を跨いで split
する場合、child は分割できるが、親 `Box` decoration はまだ multi-page container としては分割しない
- 次に必要なら、multi-page container decoration、nested container の厳密な layout tree、単一 text/MVT 子の
decoration 畳み込みを検討する。
- CSS/Flexbox 互換を目指さず、flexbox の使いやすさだけを `Stack` / `Row` に取り込む。
- `flexWrap`, `flexShrink`, media query, full `style` prop, CSS parser は当面対象外。
- `%` width は将来検討でよい。まずは `flex` / `flexGrow` で比率指定を表現する。

View File

@@ -1,6 +1,10 @@
import { readFileSync } from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
getDynamicContainerMetadata,
setDynamicContainerMetadata,
} from '../src/dynamicContainer.js';
import { getDynamicTemplate } from '../src/dynamicTemplate.js';
import { Template, Schema, Font } from '../src/index.js';
@@ -68,6 +72,16 @@ describe('getDynamicTemplate', () => {
};
describe('Single page scenarios', () => {
test('should ignore non-finite dynamic container padding metadata', () => {
const schema = { ...template.schemas[0][0] };
setDynamicContainerMetadata(schema, {
childNames: ['a'],
paddingBottom: Number.NaN,
});
expect(getDynamicContainerMetadata(schema)).toEqual({ childNames: ['a'] });
});
test('should handle no page break', async () => {
const increaseHeights = [10, 10, 10, 10, 10];
const dynamicTemplate = await getDynamicTemplate(
@@ -83,6 +97,69 @@ describe('getDynamicTemplate', () => {
);
expect(dynamicTemplate.schemas[0][1].name).toEqual('b');
});
test('should resize non-flow decoration schemas without pushing their children twice', async () => {
const dynamicTemplate = await getDynamicTemplate({
template: {
schemas: [
[
{
name: 'box',
content: '',
type: 'rectangle',
position: { x: 10, y: 10 },
width: 80,
height: 20,
},
{
name: 'a',
content: 'a',
type: 'a',
position: { x: 14, y: 14 },
width: 20,
height: 5,
},
{
name: 'b',
content: 'b',
type: 'b',
position: { x: 10, y: 30 },
width: 20,
height: 5,
},
],
],
basePdf: { width: 100, height: 100, padding: [0, 0, 0, 0] },
},
input,
options,
_cache: new Map(),
getDynamicHeights: async (_value: string, args: { schema: Schema }) => {
if (args.schema.name === 'box') {
return { heights: [40], contributesToFlow: false };
}
if (args.schema.name === 'a') {
return [30];
}
return [args.schema.height];
},
});
expect(dynamicTemplate.schemas[0][0]).toMatchObject({
name: 'box',
position: { x: 10, y: 10 },
height: 40,
});
expect(dynamicTemplate.schemas[0][1]).toMatchObject({
name: 'a',
position: { x: 14, y: 14 },
height: 30,
});
expect(dynamicTemplate.schemas[0][2]).toMatchObject({
name: 'b',
position: { x: 10, y: 55 },
});
});
});
describe('Multiple page scenarios', () => {

View File

@@ -0,0 +1,44 @@
import type { Schema } from './types.js';
export const DYNAMIC_CONTAINER_METADATA_KEY = '__pdfmeDynamicContainer' as const;
export type DynamicContainerMetadata = {
childNames: string[];
paddingBottom?: number;
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;
export const getDynamicContainerMetadata = (
schema: Schema,
): DynamicContainerMetadata | undefined => {
const value = (schema as Record<string, unknown>)[DYNAMIC_CONTAINER_METADATA_KEY];
if (!isRecord(value) || !Array.isArray(value.childNames)) return undefined;
const childNames = value.childNames.filter(
(childName): childName is string => typeof childName === 'string' && childName.length > 0,
);
if (childNames.length === 0) return undefined;
const paddingBottom =
typeof value.paddingBottom === 'number' && Number.isFinite(value.paddingBottom)
? Math.max(0, value.paddingBottom)
: undefined;
return { childNames, paddingBottom };
};
export const setDynamicContainerMetadata = (schema: Schema, metadata: DynamicContainerMetadata) => {
const childNames = metadata.childNames.filter((childName) => childName.length > 0);
if (childNames.length === 0) return;
const paddingBottom =
typeof metadata.paddingBottom === 'number' && Number.isFinite(metadata.paddingBottom)
? Math.max(0, metadata.paddingBottom)
: undefined;
(schema as Record<string, unknown>)[DYNAMIC_CONTAINER_METADATA_KEY] = {
childNames,
...(paddingBottom != null ? { paddingBottom } : {}),
};
};

View File

@@ -1,10 +1,10 @@
import {
Schema,
Template,
BasePdf,
BlankPdf,
CommonOptions,
DynamicLayoutCallbackResult,
DynamicLayoutArgs,
DynamicLayoutResult,
} from './types.js';
import { cloneDeep, isBlankPdf } from './helper.js';
@@ -20,12 +20,7 @@ interface ModifyTemplateForDynamicTableArg {
options: CommonOptions;
getDynamicHeights: (
value: string,
args: {
schema: Schema;
basePdf: BasePdf;
options: CommonOptions;
_cache: Map<string | number, unknown>;
},
args: DynamicLayoutArgs,
) => Promise<DynamicLayoutCallbackResult>;
}
@@ -250,7 +245,9 @@ function processDynamicPage(
// Update offset: difference between actual and original end position
const originalGlobalEndY = item.baseY + item.height;
totalYOffset = actualGlobalEndY - originalGlobalEndY;
if (item.dynamicLayout.contributesToFlow !== false) {
totalYOffset = actualGlobalEndY - originalGlobalEndY;
}
}
sortPagesByOrder(pages, orderMap);
@@ -317,6 +314,9 @@ export const getDynamicTemplate = async (
basePdf,
options,
_cache,
input,
schemas: template.schemas,
pageSchemas,
}).then(normalizeDynamicLayoutResult);
}),
);

View File

@@ -83,6 +83,12 @@ import {
} from './helper.js';
import { PAGE_SIZE_PRESETS, detectPaperSize, resolvePageSize } from './pageSize.js';
import { getDynamicTemplate } from './dynamicTemplate.js';
import {
DYNAMIC_CONTAINER_METADATA_KEY,
getDynamicContainerMetadata,
setDynamicContainerMetadata,
} from './dynamicContainer.js';
import type { DynamicContainerMetadata } from './dynamicContainer.js';
import { createDynamicLayoutSplitRange, getDynamicLayoutSplitRange } from './splitRange.js';
import { replacePlaceholders } from './expression.js';
import { pluginRegistry } from './pluginRegistry.js';
@@ -110,6 +116,9 @@ export {
getInputFromTemplate,
isBlankPdf,
getDynamicTemplate,
DYNAMIC_CONTAINER_METADATA_KEY,
getDynamicContainerMetadata,
setDynamicContainerMetadata,
replacePlaceholders,
checkFont,
checkInputs,
@@ -178,4 +187,5 @@ export type {
PageOrientation,
PageSize,
PageSizePreset,
DynamicContainerMetadata,
};

View File

@@ -265,6 +265,12 @@ export type DynamicLayoutPatchArgs = {
export type DynamicLayoutResult = {
heights: number[];
avoidFirstUnitOnly?: boolean;
/**
* When false, the schema is emitted with its patched height but its height delta
* does not push later schemas. This is used for decoration schemas whose flow is
* driven by child schemas.
*/
contributesToFlow?: boolean;
patchSplitSchema?: (args: DynamicLayoutPatchArgs) => Partial<Schema>;
};
@@ -275,6 +281,9 @@ export type DynamicLayoutArgs = {
basePdf: BasePdf;
options: CommonOptions;
_cache: Map<string | number, unknown>;
input?: Record<string, string>;
schemas?: Schema[][];
pageSchemas?: Schema[];
};
export type GetDynamicLayout = (

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,5 +1,11 @@
import generate from '../src/generate.js';
import { Template, BLANK_PDF, Schema, type Plugin } from '@pdfme/common';
import {
Template,
BLANK_PDF,
Schema,
setDynamicContainerMetadata,
type Plugin,
} from '@pdfme/common';
import { PDFDocument } from '@pdfme/pdf-lib';
import { getFont, getImageSnapshotOptions, pdfToImages } from './utils.js';
@@ -163,6 +169,167 @@ describe('generate integrate test', () => {
expect(after?.position.y).toBeGreaterThan(20);
});
test('resizes JSX dynamic container decorations around expanded children', async () => {
const renderedSchemas: Schema[] = [];
const createProbePlugin = (defaultSchema: Schema): Plugin => ({
pdf: ({ schema }) => {
renderedSchemas.push({
...schema,
position: { ...schema.position },
});
},
ui: () => {},
propPanel: {
schema: {},
defaultSchema,
},
});
const boxSchema: Schema = {
name: 'box',
type: 'rectangle',
content: '',
position: { x: 10, y: 10 },
width: 50,
height: 24,
color: '#f8fafc',
};
setDynamicContainerMetadata(boxSchema, {
childNames: ['label', 'body'],
paddingBottom: 4,
});
await generate({
template: {
basePdf: { width: 100, height: 100, padding: [10, 10, 10, 10] },
schemas: [
[
boxSchema,
{
...textObject(14, 14, 'label'),
width: 42,
height: 5,
readOnly: true,
content: 'Message',
},
{
...textObject(14, 21, 'body'),
width: 32,
height: 5,
overflow: 'expand',
fontSize: 10,
lineHeight: 1,
characterSpacing: 0,
},
{
...textObject(10, 38, 'after'),
width: 50,
height: 5,
},
],
],
},
inputs: [{ body: 'long text '.repeat(10), after: 'after' }],
options: { font: getFont() },
plugins: {
rectangle: createProbePlugin(boxSchema),
text: createProbePlugin(textObject(0, 0)),
},
});
const box = renderedSchemas.find((schema) => schema.name === 'box');
const body = renderedSchemas.find((schema) => schema.name === 'body');
const after = renderedSchemas.find((schema) => schema.name === 'after');
expect(body?.height).toBeGreaterThan(5);
expect(box?.height).toBeCloseTo(21 - 10 + (body?.height ?? 0) + 4);
expect(after?.position.y).toBeCloseTo(38 + (body?.height ?? 0) - 5);
});
test('propagates expanded child height through nested dynamic container decorations', async () => {
const renderedSchemas: Schema[] = [];
const createProbePlugin = (defaultSchema: Schema): Plugin => ({
pdf: ({ schema }) => {
renderedSchemas.push({
...schema,
position: { ...schema.position },
});
},
ui: () => {},
propPanel: {
schema: {},
defaultSchema,
},
});
const outerBox: Schema = {
name: 'outer',
type: 'rectangle',
content: '',
position: { x: 10, y: 10 },
width: 60,
height: 24,
color: '#f8fafc',
};
const innerBox: Schema = {
name: 'inner',
type: 'rectangle',
content: '',
position: { x: 14, y: 14 },
width: 52,
height: 16,
color: '#ffffff',
};
setDynamicContainerMetadata(innerBox, {
childNames: ['body'],
paddingBottom: 3,
});
setDynamicContainerMetadata(outerBox, {
childNames: ['inner'],
paddingBottom: 4,
});
await generate({
template: {
basePdf: { width: 100, height: 100, padding: [10, 10, 10, 10] },
schemas: [
[
outerBox,
innerBox,
{
...textObject(18, 18, 'body'),
width: 28,
height: 5,
overflow: 'expand',
fontSize: 10,
lineHeight: 1,
characterSpacing: 0,
},
{
...textObject(10, 34, 'after'),
width: 50,
height: 5,
},
],
],
},
inputs: [{ body: 'long text '.repeat(10), after: 'after' }],
options: { font: getFont() },
plugins: {
rectangle: createProbePlugin(outerBox),
text: createProbePlugin(textObject(0, 0)),
},
});
const outer = renderedSchemas.find((schema) => schema.name === 'outer');
const inner = renderedSchemas.find((schema) => schema.name === 'inner');
const body = renderedSchemas.find((schema) => schema.name === 'body');
const after = renderedSchemas.find((schema) => schema.name === 'after');
expect(body?.height).toBeGreaterThan(5);
expect(inner?.height).toBeCloseTo(18 - 14 + (body?.height ?? 0) + 3);
expect(outer?.height).toBeCloseTo(14 - 10 + (inner?.height ?? 0) + 4);
expect(after?.position.y).toBeCloseTo(34 + (body?.height ?? 0) - 5);
});
test('splits expanded text schemas by line across blank PDF pages', async () => {
const renderedSchemas: Schema[] = [];
const textProbePlugin: Plugin = {
@@ -352,7 +519,7 @@ ERROR MESSAGE: Too small: expected array to have >=1 items
} catch (e: any) {
expect(e.message).toEqual(
`[@pdfme/common] fallback flag is not found in font. true fallback flag must be only one.
Check this document: https://pdfme.com/docs/custom-fonts#about-font-type`
Check this document: https://pdfme.com/docs/custom-fonts#about-font-type`,
);
}
});
@@ -383,7 +550,7 @@ Check this document: https://pdfme.com/docs/custom-fonts#about-font-type`
} catch (e: any) {
expect(e.message).toEqual(
`[@pdfme/common] 2 fallback flags found in font. true fallback flag must be only one.
Check this document: https://pdfme.com/docs/custom-fonts#about-font-type`
Check this document: https://pdfme.com/docs/custom-fonts#about-font-type`,
);
}
});
@@ -419,7 +586,7 @@ Check this document: https://pdfme.com/docs/custom-fonts#about-font-type`
} catch (e: any) {
expect(e.message).toEqual(
`[@pdfme/common] DUMMY_FONT of template.schemas is not found in font.
Check this document: https://pdfme.com/docs/custom-fonts`
Check this document: https://pdfme.com/docs/custom-fonts`,
);
}
});

View File

@@ -32,6 +32,9 @@ it provides its own `jsx-runtime` and `jsx-dev-runtime`.
- `Text` and `MultiVariableText` heights are measured with pdfme's text/rich text wrapping helpers
when `height` is omitted. Pass an explicit `height` when you need a fixed field box.
- Auto-height visual `Box` decorations track child `overflow="expand"` reflow for normal same-page
parent/child containers. Children can still split across pages, but the parent Box decoration is
not a multi-page container yet.
- `Text textFormat="inline-markdown"` is read-only only. Editable `Text` values use plain content.
- `MultiVariableText` uses `text` or children as the template string and stores `values` as the
JSON input. Variable names are inferred from `{name}` placeholders and can also be passed with

View File

@@ -1,6 +1,11 @@
/** @jsxImportSource @pdfme/jsx */
import { describe, expect, it } from 'vitest';
import { isBlankPdf, PAGE_SIZE_PRESETS } from '@pdfme/common';
import {
DYNAMIC_CONTAINER_METADATA_KEY,
getDynamicContainerMetadata,
isBlankPdf,
PAGE_SIZE_PRESETS,
} from '@pdfme/common';
import {
Absolute,
@@ -597,6 +602,27 @@ describe('@pdfme/jsx renderToTemplate', () => {
expect(after?.position.y).toBeCloseTo(box?.height ?? 0);
});
it('marks auto-height visual Box schemas as dynamic containers', async () => {
const result = await renderToTemplate(
<Page margin={0}>
<Box background="#eeeeee" padding={{ x: 3, y: 2 }}>
<Stack gap={1}>
<Text>Message</Text>
<MultiVariableText name="message" text="Hello {name}" overflow="expand" />
</Stack>
</Box>
</Page>,
);
const [box, label, message] = result.template.schemas[0] ?? [];
expect(box?.type).toBe('rectangle');
expect(getDynamicContainerMetadata(box!)).toEqual({
childNames: [label?.name, message?.name],
paddingBottom: 2,
});
expect(box).toHaveProperty(DYNAMIC_CONTAINER_METADATA_KEY);
});
it('does not render a rectangle schema for a Box without visual styles', async () => {
const result = await renderToTemplate(
<Page margin={0}>

View File

@@ -1,4 +1,11 @@
import { getDefaultFont, isBlankPdf, pt2mm, resolvePageSize } from '@pdfme/common';
import {
getDefaultFont,
getDynamicContainerMetadata,
isBlankPdf,
pt2mm,
resolvePageSize,
setDynamicContainerMetadata,
} from '@pdfme/common';
import type { Font, Schema, Template } from '@pdfme/common';
import type {
CellStyle as SchemaCellStyle,
@@ -791,7 +798,20 @@ const renderBox = async (
const height = props.height ?? childSize.height + padding.top + padding.bottom;
if (needsRect) {
ctx.schemas[beforeCount] = { ...ctx.schemas[beforeCount]!, height };
const boxSchema = { ...ctx.schemas[beforeCount]!, height };
if (props.height == null) {
const childSchemas = ctx.schemas.slice(beforeCount + 1);
const childNames = childSchemas
.map((schema) => schema.name)
.filter((name): name is string => typeof name === 'string' && name.length > 0);
if (childSchemas.some(isSchemaDynamicContainerSource)) {
setDynamicContainerMetadata(boxSchema, {
childNames,
paddingBottom: padding.bottom,
});
}
}
ctx.schemas[beforeCount] = boxSchema;
}
return { width, height };
@@ -802,6 +822,13 @@ const renderSpacer = (props: SpacerProps): { width: number; height: number } =>
height: props.height ?? 0,
});
const isSchemaDynamicContainerSource = (schema: Schema) =>
schema.type === 'table' ||
schema.type === 'list' ||
((schema.type === 'text' || schema.type === 'multiVariableText') &&
(schema as { overflow?: unknown }).overflow === 'expand') ||
getDynamicContainerMetadata(schema) != null;
const renderText = async (
props: TextProps,
frame: Rect,

View File

@@ -1,4 +1,11 @@
import type { DynamicLayoutArgs, DynamicLayoutCallbackResult, Schema } from '@pdfme/common';
import {
getDynamicContainerMetadata,
replacePlaceholders,
type DynamicLayoutArgs,
type DynamicLayoutCallbackResult,
type DynamicLayoutResult,
type Schema,
} from '@pdfme/common';
import { getDynamicLayoutForList } from './list/dynamicTemplate.js';
import { getDynamicLayoutForMultiVariableText } from './multiVariableText/dynamicTemplate.js';
import { getDynamicLayoutForTable } from './tables/dynamicTemplate.js';
@@ -24,12 +31,88 @@ const isExpandableTextSchema = (schema: Schema) =>
(schema as { overflow?: unknown }).overflow === TEXT_OVERFLOW_EXPAND;
export const isDynamicLayoutSchema = (schema: Schema) =>
schema.type === 'table' || schema.type === 'list' || isExpandableTextSchema(schema);
schema.type === 'table' ||
schema.type === 'list' ||
isExpandableTextSchema(schema) ||
getDynamicContainerMetadata(schema) != null;
const normalizeDynamicLayoutResult = (result: DynamicLayoutCallbackResult): DynamicLayoutResult => {
const dynamicLayout = Array.isArray(result) ? { heights: result } : result;
return {
...dynamicLayout,
heights: dynamicLayout.heights.length === 0 ? [0] : dynamicLayout.heights,
};
};
const getSchemaValue = (schema: Schema, args: DynamicLayoutArgs): string => {
if (!schema.readOnly) {
return args.input?.[schema.name] || '';
}
if (schema.type !== 'text' && schema.type !== 'multiVariableText') {
return schema.content || '';
}
return replacePlaceholders({
content: schema.content || '',
variables: args.input ?? {},
schemas: args.schemas ?? [args.pageSchemas ?? []],
});
};
const sumHeights = (layout: DynamicLayoutResult) =>
layout.heights.reduce((total, height) => total + height, 0);
const getDynamicLayoutForContainer = async (
args: DynamicLayoutArgs,
): Promise<DynamicLayoutResult | undefined> => {
const metadata = getDynamicContainerMetadata(args.schema);
if (!metadata || !args.pageSchemas) return undefined;
const pageOrder = new Map(args.pageSchemas.map((schema, index) => [schema.name, index]));
const pageSchemaMap = new Map(args.pageSchemas.map((schema) => [schema.name, schema]));
const children = metadata.childNames
.map((childName) => pageSchemaMap.get(childName))
.filter((schema): schema is Schema => schema != null)
.sort((a, b) => {
if (a.position.y !== b.position.y) return a.position.y - b.position.y;
if (a.position.x !== b.position.x) return a.position.x - b.position.x;
return (pageOrder.get(a.name) ?? 0) - (pageOrder.get(b.name) ?? 0);
});
if (children.length === 0) return undefined;
let localOffset = 0;
let contentBottom = 0;
for (const child of children) {
const value = getSchemaValue(child, args);
const childLayout = normalizeDynamicLayoutResult(
await getDynamicLayoutForSchema(value, { ...args, schema: child }),
);
const localY = Math.max(0, child.position.y - args.schema.position.y + localOffset);
const dynamicHeight = sumHeights(childLayout);
contentBottom = Math.max(contentBottom, localY + dynamicHeight);
const originalLocalEndY = Math.max(0, child.position.y - args.schema.position.y) + child.height;
if (childLayout.contributesToFlow !== false) {
localOffset = localY + dynamicHeight - originalLocalEndY;
}
}
const height = Math.max(args.schema.height, contentBottom + (metadata.paddingBottom ?? 0));
return {
heights: [height],
contributesToFlow: false,
};
};
export const getDynamicLayoutForSchema = (
value: string,
args: DynamicLayoutArgs,
): Promise<DynamicLayoutCallbackResult> => {
if (getDynamicContainerMetadata(args.schema) != null) {
return getDynamicLayoutForContainer(args).then((layout) => layout ?? [args.schema.height]);
}
switch (args.schema.type) {
case 'table':
return getDynamicLayoutForTable(value, args);

View File

@@ -74,7 +74,7 @@ return (
</Box>
<Box flex={1} padding={4} background="#ecfeff" borderColor="#06b6d4" borderWidth={0.4}>
<Text size={9} lineHeight={1.35}>
Editable fields usually keep explicit heights for predictable input boxes. Read-only text can usually omit height and let JSX measure it.
Auto-height Boxes follow expandable text fields in Form/Viewer and generated PDFs. Add explicit heights only when you want a fixed input area.
</Text>
</Box>
</Row>

View File

@@ -288,7 +288,14 @@
"color": "#f8fafc",
"borderColor": "#cbd5e1",
"borderWidth": 0.4,
"radius": 0
"radius": 0,
"__pdfmeDynamicContainer": {
"childNames": [
"text_7",
"message"
],
"paddingBottom": 4
}
},
{
"name": "text_7",
@@ -424,7 +431,7 @@
"y": 112.509184
},
"width": 112,
"height": 17.2588832,
"height": 21.5454032,
"rotate": 0,
"opacity": 1,
"readOnly": true,
@@ -436,13 +443,13 @@
{
"name": "text_9",
"type": "text",
"content": "Editable fields usually keep explicit heights for predictable input boxes. Read-only text can usually omit height and let JSX measure it.",
"content": "Auto-height Boxes follow expandable text fields in Form/Viewer and generated PDFs. Add explicit heights only when you want a fixed input area.",
"position": {
"x": 84,
"y": 116.509184
},
"width": 104,
"height": 9.2588832,
"height": 13.545403199999999,
"rotate": 0,
"opacity": 1,
"readOnly": true,

View File

@@ -321,7 +321,14 @@
"color": "",
"borderColor": "#e2e8f0",
"borderWidth": 0.4,
"radius": 0
"radius": 0,
"__pdfmeDynamicContainer": {
"childNames": [
"text_9",
"list_1"
],
"paddingBottom": 4
}
},
{
"name": "text_9",

View File

@@ -103,7 +103,13 @@
"color": "#f8fafc",
"borderColor": "#cbd5e1",
"borderWidth": 0.4,
"radius": 0
"radius": 0,
"__pdfmeDynamicContainer": {
"childNames": [
"text_4"
],
"paddingBottom": 5
}
},
{
"name": "text_4",

View File

@@ -354,7 +354,15 @@
"color": "",
"borderColor": "#cbd5e1",
"borderWidth": 0.4,
"radius": 0
"radius": 0,
"__pdfmeDynamicContainer": {
"childNames": [
"text_12",
"text_13",
"list_1"
],
"paddingBottom": 5
}
},
{
"name": "text_12",

View File

@@ -84,7 +84,9 @@ Use `gap`, `margin`, `alignItems`, `justifyContent`, and `flex` / `flexGrow` for
`Text`, `MultiVariableText`, `List`, and `Table` can usually omit `height`. JSX measures their initial
content while rendering and advances the surrounding `Stack`, `Row`, or `Box`. Use explicit `height`
when you want a fixed field, a fixed visual area, or predictable form input box. A `Box` without
`height` grows around its children during JSX rendering.
`height` grows around its children during JSX rendering. If that visual `Box` contains runtime
dynamic text/MVT content, pdfme also keeps the Box decoration height in sync when Form/Viewer or
generator reflows `overflow="expand"` content.
## Schema Components
@@ -166,5 +168,6 @@ page coordinate system and is intended for advanced overlays such as watermarks,
fonts, and generator/viewer options.
- `Header`, `Footer`, and `Static` are supported only as direct children of `Document`. They generate
blank `basePdf.staticSchema` and cannot be used with a custom PDF `basePdf`.
- If a form input later expands at runtime, a parent `Box` rectangle is not dynamically resized yet.
Dynamic parent/child container reflow is a future layout feature.
- Dynamic Box decoration reflow is intended for same-page parent/child visual containers. If a child
expands across a page break, the child schema can split across pages, but the parent Box decoration
is not a multi-page container yet.

View File

@@ -83,7 +83,8 @@ layout API は CSS/Flexbox 互換を目指していません。`gap`, `margin`,
`Text`, `MultiVariableText`, `List`, `Table` は通常 `height` を省略できます。JSX render 時に初期 content
を測定し、周囲の `Stack`, `Row`, `Box` を進めます。固定 field、固定 visual area、Form の入力 box を
明確にしたい場合だけ `height` を指定してください。`height` のない `Box` は、JSX render 時点では子要素の高さに
合わせて伸びます。
合わせて伸びます。その visual `Box` の中に runtime dynamic な text / MVT がある場合、Form/Viewer や
generator が `overflow="expand"` を reflow するときも Box decoration の高さを追従させます。
## schema component
@@ -163,5 +164,6 @@ watermark、crop mark、stamp などの advanced overlay 向けです。
- 出力は `Template + inputs` です。実際の描画は通常通り pdfme plugins, fonts, generator/viewer options に依存します。
- `Header`, `Footer`, `Static``Document` の direct child としてのみ使えます。これらは blank
`basePdf.staticSchema` を生成するため、custom PDF `basePdf` とは併用できません。
- Form 入力によって runtime に text / MVT が expand した場合、親 `Box` の rectangle はまだ自動では
再計算されません。親子 container の dynamic reflow は今後の layout 機能として扱います。
- Dynamic Box decoration reflow は同一ページ内の親子 visual container 向けです。child schema が page
break を跨いで split する場合、child は複数ページに分割できますが、親 Box decoration はまだ multi-page
container にはなりません。