h17-sspdf 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCUMENTATION.md +36 -0
- package/README.md +1 -0
- package/core/pdf-core.js +27 -0
- package/core/render-document.js +85 -0
- package/core/validate.js +15 -1
- package/examples/sources/source-two-column.json +68 -0
- package/examples/themes/theme-two-column.js +105 -0
- package/package.json +1 -1
package/DOCUMENTATION.md
CHANGED
|
@@ -240,6 +240,7 @@ Shared layout defaults read by operation handlers.
|
|
|
240
240
|
layout: {
|
|
241
241
|
bulletIndentMm: 4.5, // text indent after bullet marker (default: 4)
|
|
242
242
|
chartAlign: "center", // chart horizontal alignment: "left" (default) or "center"
|
|
243
|
+
columnGutterMm: 5, // default gutter between columns (required if columns ops omit gutterMm)
|
|
243
244
|
}
|
|
244
245
|
```
|
|
245
246
|
|
|
@@ -645,6 +646,39 @@ Identical to `block` but defaults to `keepTogether: false`. Use sections to grou
|
|
|
645
646
|
|
|
646
647
|
Alias for `block`.
|
|
647
648
|
|
|
649
|
+
#### `columns`
|
|
650
|
+
|
|
651
|
+
Renders two independent operation lists side by side. Each column gets half the content width minus half the gutter. The cursor advances to the bottom of the taller column, so subsequent full-width operations continue from the correct position.
|
|
652
|
+
|
|
653
|
+
```json
|
|
654
|
+
{
|
|
655
|
+
"type": "columns",
|
|
656
|
+
"gutterMm": 5,
|
|
657
|
+
"column1": [
|
|
658
|
+
{ "type": "text", "label": "doc.heading", "text": "Left Column" },
|
|
659
|
+
{ "type": "text", "label": "doc.body", "text": "Content on the left." }
|
|
660
|
+
],
|
|
661
|
+
"column2": [
|
|
662
|
+
{ "type": "text", "label": "doc.heading", "text": "Right Column" },
|
|
663
|
+
{ "type": "text", "label": "doc.body", "text": "Content on the right." }
|
|
664
|
+
]
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
| Field | Required | Type | Description |
|
|
669
|
+
|---|---|---|---|
|
|
670
|
+
| `column1` | yes | array | Operations for the left column |
|
|
671
|
+
| `column2` | yes | array | Operations for the right column |
|
|
672
|
+
| `gutterMm` | no* | number | Gap between columns in mm. *Required if `theme.layout.columnGutterMm` is not set |
|
|
673
|
+
|
|
674
|
+
**Gutter resolution:** `gutterMm` on the operation takes precedence. If omitted, `theme.layout.columnGutterMm` is used. If neither is defined, the engine throws.
|
|
675
|
+
|
|
676
|
+
**Column width:** `(contentWidth - gutterMm) / 2`. Both columns are always equal width.
|
|
677
|
+
|
|
678
|
+
**Cursor after:** `max(col1EndY, col2EndY)` — the bottom of the taller column.
|
|
679
|
+
|
|
680
|
+
Any operation type that works at the top level works inside columns. Tables, images, bullets, nested blocks — all work. Columns inside columns are supported (the margin stack is re-entrant).
|
|
681
|
+
|
|
648
682
|
### Inferred text operations
|
|
649
683
|
|
|
650
684
|
If a node has `label` and `text` (or `value`) but no `type`, it's treated as text:
|
|
@@ -675,6 +709,7 @@ The engine positions content using a cursor that starts at `marginTopMm` and mov
|
|
|
675
709
|
| `table` | marginTop + headerRowHeight + sum(dataRowHeights) + marginBottom |
|
|
676
710
|
| `image` | marginTop + paddingTop + imageHeightMm + captionHeight + paddingBottom + marginBottom |
|
|
677
711
|
| `block` | sum of children deltas (+ spaceAfter if defined) |
|
|
712
|
+
| `columns` | max(col1Height, col2Height) |
|
|
678
713
|
|
|
679
714
|
Where:
|
|
680
715
|
- `lineHeightMm = (fontSize × 25.4/72) × lineHeight` (font points → mm × multiplier)
|
|
@@ -1350,6 +1385,7 @@ This applies to all color properties: `color`, `backgroundColor`, `borderColor`,
|
|
|
1350
1385
|
| `section` | `content` | `label`, `keepTogether` |
|
|
1351
1386
|
| `quote` | `label`, `text` | `attribution`, `attributionLabel` |
|
|
1352
1387
|
| `hiddenText` | `label`, `text` | - |
|
|
1388
|
+
| `columns` | `column1`, `column2` | `gutterMm` (falls back to `theme.layout.columnGutterMm`) |
|
|
1353
1389
|
|
|
1354
1390
|
Every operation also accepts `xMm` and `maxWidthMm` to override the theme margins for that operation only.
|
|
1355
1391
|
|
package/README.md
CHANGED
|
@@ -270,6 +270,7 @@ labels: {
|
|
|
270
270
|
| `quote` | Blockquote with attribution | `label`, `text`, `attribution` |
|
|
271
271
|
| `block` | Group children, optional background + border | `children`, `keepTogether` |
|
|
272
272
|
| `section` | Logical group, allows breaks inside | `content` |
|
|
273
|
+
| `columns` | Two-column side-by-side layout | `column1`, `column2`, `gutterMm` |
|
|
273
274
|
|
|
274
275
|
### Position overrides
|
|
275
276
|
|
package/core/pdf-core.js
CHANGED
|
@@ -278,6 +278,33 @@ class PDFCore {
|
|
|
278
278
|
this.cursorY = Number(y) || this.cursorY;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Narrow the content area to a column band. Saves current margins and cursor.
|
|
283
|
+
* @param {number} left Absolute left x in mm
|
|
284
|
+
* @param {number} right Absolute right x in mm
|
|
285
|
+
*/
|
|
286
|
+
_enterColumnBounds(left, right) {
|
|
287
|
+
if (!this._colMarginStack) this._colMarginStack = [];
|
|
288
|
+
this._colMarginStack.push({ left: this.marginLeftMm, right: this.marginRightMm });
|
|
289
|
+
this.marginLeftMm = left;
|
|
290
|
+
this.marginRightMm = this.pageWidth - right;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Restore margins saved by _enterColumnBounds. Returns the cursor position
|
|
295
|
+
* reached at end of column so the caller can track the tallest column.
|
|
296
|
+
* @param {number} savedCursorY The cursor to restore after saving column end Y
|
|
297
|
+
* @returns {number} Cursor Y at end of column
|
|
298
|
+
*/
|
|
299
|
+
_exitColumnBounds(savedCursorY) {
|
|
300
|
+
const endY = this.cursorY;
|
|
301
|
+
const saved = this._colMarginStack.pop();
|
|
302
|
+
this.marginLeftMm = saved.left;
|
|
303
|
+
this.marginRightMm = saved.right;
|
|
304
|
+
this.cursorY = savedCursorY;
|
|
305
|
+
return endY;
|
|
306
|
+
}
|
|
307
|
+
|
|
281
308
|
/**
|
|
282
309
|
* Apply a text style to jsPDF.
|
|
283
310
|
* @param {object} style
|
package/core/render-document.js
CHANGED
|
@@ -284,6 +284,14 @@ function normalizeNode(node, path) {
|
|
|
284
284
|
}];
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
if (node.type === "columns") {
|
|
288
|
+
return [{
|
|
289
|
+
...node,
|
|
290
|
+
column1: Array.isArray(node.column1) ? normalizeNodes(node.column1, `${path}.column1`) : [],
|
|
291
|
+
column2: Array.isArray(node.column2) ? normalizeNodes(node.column2, `${path}.column2`) : [],
|
|
292
|
+
}];
|
|
293
|
+
}
|
|
294
|
+
|
|
287
295
|
if (isOperationType(node.type)) {
|
|
288
296
|
return expandOperationNode(node, path);
|
|
289
297
|
}
|
|
@@ -344,6 +352,7 @@ function isOperationType(type) {
|
|
|
344
352
|
|| type === "hiddenText"
|
|
345
353
|
|| type === "table"
|
|
346
354
|
|| type === "image"
|
|
355
|
+
|| type === "columns"
|
|
347
356
|
|| hasPlugin(type);
|
|
348
357
|
}
|
|
349
358
|
|
|
@@ -873,6 +882,39 @@ function executeOperation(ctx) {
|
|
|
873
882
|
return;
|
|
874
883
|
}
|
|
875
884
|
|
|
885
|
+
if (operation.type === "columns") {
|
|
886
|
+
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
887
|
+
const contentWidth = bounds.right - bounds.left;
|
|
888
|
+
const gutterMm = resolveColumnsGutter(operation, theme, index);
|
|
889
|
+
const colWidth = (contentWidth - gutterMm) / 2;
|
|
890
|
+
const col1Left = bounds.left;
|
|
891
|
+
const col1Right = bounds.left + colWidth;
|
|
892
|
+
const col2Left = col1Right + gutterMm;
|
|
893
|
+
const col2Right = bounds.right;
|
|
894
|
+
const startY = core.getCursorY();
|
|
895
|
+
|
|
896
|
+
core._enterColumnBounds(col1Left, col1Right);
|
|
897
|
+
executeOperations({
|
|
898
|
+
core, theme,
|
|
899
|
+
operations: Array.isArray(operation.column1) ? operation.column1 : [],
|
|
900
|
+
indexPrefix: `${index}.col1.`,
|
|
901
|
+
templateMode, templateBypassMargins,
|
|
902
|
+
});
|
|
903
|
+
const col1EndY = core._exitColumnBounds(startY);
|
|
904
|
+
|
|
905
|
+
core._enterColumnBounds(col2Left, col2Right);
|
|
906
|
+
executeOperations({
|
|
907
|
+
core, theme,
|
|
908
|
+
operations: Array.isArray(operation.column2) ? operation.column2 : [],
|
|
909
|
+
indexPrefix: `${index}.col2.`,
|
|
910
|
+
templateMode, templateBypassMargins,
|
|
911
|
+
});
|
|
912
|
+
const col2EndY = core._exitColumnBounds(startY);
|
|
913
|
+
|
|
914
|
+
core.setCursorY(Math.max(col1EndY, col2EndY));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
876
918
|
const plugin = getPlugin(operation.type);
|
|
877
919
|
if (plugin) {
|
|
878
920
|
const bounds = getHorizontalBounds(core, templateBypassMargins);
|
|
@@ -1120,6 +1162,35 @@ function estimateOperationHeight(ctx) {
|
|
|
1120
1162
|
return margins.top + padding.top + imgHeightMm + captionHeight + padding.bottom + margins.bottom;
|
|
1121
1163
|
}
|
|
1122
1164
|
|
|
1165
|
+
if (operation.type === "columns") {
|
|
1166
|
+
const contentWidth = core.pageWidth - core.marginLeftMm - core.marginRightMm;
|
|
1167
|
+
const gutterMm = resolveColumnsGutter(operation, theme, index);
|
|
1168
|
+
const colWidth = (contentWidth - gutterMm) / 2;
|
|
1169
|
+
|
|
1170
|
+
const savedMarginLeft = core.marginLeftMm;
|
|
1171
|
+
const savedMarginRight = core.marginRightMm;
|
|
1172
|
+
|
|
1173
|
+
core.marginLeftMm = savedMarginLeft;
|
|
1174
|
+
core.marginRightMm = core.pageWidth - (savedMarginLeft + colWidth);
|
|
1175
|
+
const col1H = estimateOperationsHeight({
|
|
1176
|
+
core, theme,
|
|
1177
|
+
operations: Array.isArray(operation.column1) ? operation.column1 : [],
|
|
1178
|
+
indexPrefix: `${index}.col1.`,
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
core.marginLeftMm = savedMarginLeft + colWidth + gutterMm;
|
|
1182
|
+
core.marginRightMm = core.pageWidth - (savedMarginLeft + colWidth + gutterMm + colWidth);
|
|
1183
|
+
const col2H = estimateOperationsHeight({
|
|
1184
|
+
core, theme,
|
|
1185
|
+
operations: Array.isArray(operation.column2) ? operation.column2 : [],
|
|
1186
|
+
indexPrefix: `${index}.col2.`,
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
core.marginLeftMm = savedMarginLeft;
|
|
1190
|
+
core.marginRightMm = savedMarginRight;
|
|
1191
|
+
return Math.max(col1H, col2H);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1123
1194
|
const plugin = getPlugin(operation.type);
|
|
1124
1195
|
if (plugin) {
|
|
1125
1196
|
if (typeof plugin.estimateHeight === "function") {
|
|
@@ -1172,6 +1243,20 @@ function applyPageTokens(value, core) {
|
|
|
1172
1243
|
return String(value).replace(/\{\{page\}\}/g, String(page));
|
|
1173
1244
|
}
|
|
1174
1245
|
|
|
1246
|
+
function resolveColumnsGutter(operation, theme, index) {
|
|
1247
|
+
if (operation.gutterMm !== undefined) {
|
|
1248
|
+
return Number(operation.gutterMm);
|
|
1249
|
+
}
|
|
1250
|
+
const themeGutter = (theme.layout || {}).columnGutterMm;
|
|
1251
|
+
if (themeGutter !== undefined) {
|
|
1252
|
+
return Number(themeGutter);
|
|
1253
|
+
}
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`columns operation at index ${index} has no gutterMm. ` +
|
|
1256
|
+
`Set gutterMm on the operation or columnGutterMm in theme.layout.`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1175
1260
|
function getHorizontalBounds(core, bypassMargins) {
|
|
1176
1261
|
if (bypassMargins) {
|
|
1177
1262
|
return { left: 0, right: core.pageWidth };
|
package/core/validate.js
CHANGED
|
@@ -2,7 +2,7 @@ const { hasPlugin } = require("./plugin-registry");
|
|
|
2
2
|
|
|
3
3
|
const OPERATION_TYPES = new Set([
|
|
4
4
|
"text", "row", "bullet", "divider", "spacer", "hiddenText",
|
|
5
|
-
"block", "group", "section", "quote", "table", "image",
|
|
5
|
+
"block", "group", "section", "quote", "table", "image", "columns",
|
|
6
6
|
]);
|
|
7
7
|
|
|
8
8
|
function validateSource(source) {
|
|
@@ -108,6 +108,18 @@ function validateOperation(op, path) {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
if (type === "columns") {
|
|
112
|
+
if (!Array.isArray(op.column1)) {
|
|
113
|
+
throw new Error(`${path}: columns requires column1 array`);
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(op.column2)) {
|
|
116
|
+
throw new Error(`${path}: columns requires column2 array`);
|
|
117
|
+
}
|
|
118
|
+
op.column1.forEach((child, i) => validateOperation(child, `${path}.column1[${i}]`));
|
|
119
|
+
op.column2.forEach((child, i) => validateOperation(child, `${path}.column2[${i}]`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
if (hasPlugin(type)) {
|
|
112
124
|
return;
|
|
113
125
|
}
|
|
@@ -147,6 +159,8 @@ function collectLabels(node, missing, themeLabels) {
|
|
|
147
159
|
if (Array.isArray(ops)) {
|
|
148
160
|
ops.forEach((n) => collectLabels(n, missing, themeLabels));
|
|
149
161
|
}
|
|
162
|
+
if (Array.isArray(node.column1)) node.column1.forEach((n) => collectLabels(n, missing, themeLabels));
|
|
163
|
+
if (Array.isArray(node.column2)) node.column2.forEach((n) => collectLabels(n, missing, themeLabels));
|
|
150
164
|
|
|
151
165
|
if (node.label && !themeLabels.has(node.label)) missing.push(node.label);
|
|
152
166
|
if (node.leftLabel && !themeLabels.has(node.leftLabel)) missing.push(node.leftLabel);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operations": [
|
|
3
|
+
{ "type": "text", "label": "demo.title", "text": "Service Plan Comparison" },
|
|
4
|
+
{ "type": "text", "label": "demo.subtitle", "text": "Choose the plan that fits your team. Both plans include SLA, onboarding, and 24/7 support." },
|
|
5
|
+
{ "type": "divider", "label": "demo.rule" },
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
"type": "columns",
|
|
9
|
+
"column1": [
|
|
10
|
+
{ "type": "text", "label": "demo.col.heading", "text": "Starter" },
|
|
11
|
+
{ "type": "text", "label": "demo.col.tagline", "text": "For small teams getting started with automation" },
|
|
12
|
+
{ "type": "text", "label": "demo.col.price", "text": "$49 / mo" },
|
|
13
|
+
{ "type": "text", "label": "demo.col.price.note", "text": "Per workspace, billed monthly" },
|
|
14
|
+
{ "type": "divider", "label": "demo.rule.soft" },
|
|
15
|
+
{
|
|
16
|
+
"type": "table",
|
|
17
|
+
"label": "demo.table.cell",
|
|
18
|
+
"headerLabel": "demo.table.header",
|
|
19
|
+
"columns": [
|
|
20
|
+
{ "header": "Feature", "width": "72%", "align": "left" },
|
|
21
|
+
{ "header": "Included", "width": "28%", "align": "center" }
|
|
22
|
+
],
|
|
23
|
+
"rows": [
|
|
24
|
+
["Users", "Up to 5"],
|
|
25
|
+
["Projects", "10"],
|
|
26
|
+
["Storage", "20 GB"],
|
|
27
|
+
["API calls / day", "5,000"],
|
|
28
|
+
["Custom integrations", "—"],
|
|
29
|
+
["SSO / SAML", "—"],
|
|
30
|
+
["Audit log", "—"],
|
|
31
|
+
["Priority support", "—"],
|
|
32
|
+
["SLA uptime", "99.5%"]
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"column2": [
|
|
37
|
+
{ "type": "text", "label": "demo.col.heading", "text": "Professional" },
|
|
38
|
+
{ "type": "text", "label": "demo.col.tagline", "text": "For growing teams that need scale and compliance" },
|
|
39
|
+
{ "type": "text", "label": "demo.col.price", "text": "$149 / mo" },
|
|
40
|
+
{ "type": "text", "label": "demo.col.price.note", "text": "Per workspace, billed monthly" },
|
|
41
|
+
{ "type": "divider", "label": "demo.rule.soft" },
|
|
42
|
+
{
|
|
43
|
+
"type": "table",
|
|
44
|
+
"label": "demo.table.cell",
|
|
45
|
+
"headerLabel": "demo.table.header",
|
|
46
|
+
"columns": [
|
|
47
|
+
{ "header": "Feature", "width": "72%", "align": "left" },
|
|
48
|
+
{ "header": "Included", "width": "28%", "align": "center" }
|
|
49
|
+
],
|
|
50
|
+
"rows": [
|
|
51
|
+
["Users", "Unlimited"],
|
|
52
|
+
["Projects", "Unlimited"],
|
|
53
|
+
["Storage", "500 GB"],
|
|
54
|
+
["API calls / day", "100,000"],
|
|
55
|
+
["Custom integrations", "Yes"],
|
|
56
|
+
["SSO / SAML", "Yes"],
|
|
57
|
+
["Audit log", "90 days"],
|
|
58
|
+
["Priority support", "Yes"],
|
|
59
|
+
["SLA uptime", "99.9%"]
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
{ "type": "text", "label": "demo.footer.note", "text": "Prices shown in USD. Annual billing saves 20%. All plans include a 14-day free trial with no credit card required." },
|
|
66
|
+
{ "type": "text", "label": "demo.footer.cta", "text": "Contact sales@example.com to discuss enterprise pricing or volume discounts." }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const table = require("./table");
|
|
2
|
+
|
|
3
|
+
const BLACK = [15, 15, 15];
|
|
4
|
+
const MUTED = [100, 100, 100];
|
|
5
|
+
const WHITE = [255, 255, 255];
|
|
6
|
+
const LIGHT = [245, 245, 245];
|
|
7
|
+
const RULE = [210, 210, 210];
|
|
8
|
+
const ACCENT = [30, 90, 180];
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
name: "Two-Column Demo",
|
|
12
|
+
|
|
13
|
+
page: {
|
|
14
|
+
format: "a4",
|
|
15
|
+
orientation: "portrait",
|
|
16
|
+
unit: "mm",
|
|
17
|
+
compress: true,
|
|
18
|
+
marginTopMm: 14,
|
|
19
|
+
marginBottomMm: 14,
|
|
20
|
+
marginLeftMm: 14,
|
|
21
|
+
marginRightMm: 14,
|
|
22
|
+
backgroundColor: WHITE,
|
|
23
|
+
defaultText: {
|
|
24
|
+
fontFamily: "helvetica",
|
|
25
|
+
fontStyle: "normal",
|
|
26
|
+
fontSize: 9,
|
|
27
|
+
color: BLACK,
|
|
28
|
+
lineHeight: 1.4,
|
|
29
|
+
},
|
|
30
|
+
defaultStroke: {
|
|
31
|
+
color: RULE,
|
|
32
|
+
lineWidth: 0.2,
|
|
33
|
+
lineCap: "butt",
|
|
34
|
+
lineJoin: "miter",
|
|
35
|
+
},
|
|
36
|
+
defaultFillColor: WHITE,
|
|
37
|
+
metadata: {
|
|
38
|
+
title: "Service Plan Comparison",
|
|
39
|
+
subject: "sspdf two-column layout demo",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
layout: {
|
|
44
|
+
columnGutterMm: 6,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
labels: {
|
|
48
|
+
"demo.title": {
|
|
49
|
+
fontFamily: "helvetica", fontStyle: "bold", fontSize: 18, color: BLACK,
|
|
50
|
+
lineHeight: 1.1, marginBottomMm: 1,
|
|
51
|
+
},
|
|
52
|
+
"demo.subtitle": {
|
|
53
|
+
fontFamily: "helvetica", fontStyle: "normal", fontSize: 10, color: MUTED,
|
|
54
|
+
lineHeight: 1.3, marginBottomMm: 5,
|
|
55
|
+
},
|
|
56
|
+
"demo.rule": {
|
|
57
|
+
color: ACCENT, lineWidth: 0.6, marginBottomMm: 5,
|
|
58
|
+
},
|
|
59
|
+
"demo.rule.soft": {
|
|
60
|
+
color: RULE, lineWidth: 0.2, marginTopMm: 3, marginBottomMm: 3,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"demo.col.heading": {
|
|
64
|
+
fontFamily: "helvetica", fontStyle: "bold", fontSize: 11, color: ACCENT,
|
|
65
|
+
lineHeight: 1.15, marginBottomMm: 2,
|
|
66
|
+
},
|
|
67
|
+
"demo.col.tagline": {
|
|
68
|
+
fontFamily: "helvetica", fontStyle: "normal", fontSize: 8, color: MUTED,
|
|
69
|
+
lineHeight: 1.3, marginBottomMm: 3,
|
|
70
|
+
},
|
|
71
|
+
"demo.col.price": {
|
|
72
|
+
fontFamily: "helvetica", fontStyle: "bold", fontSize: 20, color: BLACK,
|
|
73
|
+
lineHeight: 1.1, marginBottomMm: 1,
|
|
74
|
+
},
|
|
75
|
+
"demo.col.price.note": {
|
|
76
|
+
fontFamily: "helvetica", fontStyle: "normal", fontSize: 7, color: MUTED,
|
|
77
|
+
lineHeight: 1.2, marginBottomMm: 3,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"demo.table.cell": {
|
|
81
|
+
...table.cell,
|
|
82
|
+
fontFamily: "helvetica", fontStyle: "normal", fontSize: 8, color: BLACK,
|
|
83
|
+
lineHeight: 1.3, cellPaddingMm: 1.2,
|
|
84
|
+
backgroundColor: WHITE, altRowColor: LIGHT,
|
|
85
|
+
borderColor: RULE,
|
|
86
|
+
borderTopMm: 0, borderBottomMm: 0.15, borderLeftMm: 0, borderRightMm: 0,
|
|
87
|
+
},
|
|
88
|
+
"demo.table.header": {
|
|
89
|
+
...table.header,
|
|
90
|
+
fontFamily: "helvetica", fontStyle: "bold", fontSize: 8, color: WHITE,
|
|
91
|
+
lineHeight: 1.3, cellPaddingMm: 1.2,
|
|
92
|
+
backgroundColor: ACCENT, borderColor: ACCENT,
|
|
93
|
+
borderTopMm: 0, borderBottomMm: 0, borderLeftMm: 0, borderRightMm: 0,
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
"demo.footer.note": {
|
|
97
|
+
fontFamily: "helvetica", fontStyle: "normal", fontSize: 7.5, color: MUTED,
|
|
98
|
+
lineHeight: 1.4, marginTopMm: 4, align: "center",
|
|
99
|
+
},
|
|
100
|
+
"demo.footer.cta": {
|
|
101
|
+
fontFamily: "helvetica", fontStyle: "bold", fontSize: 9, color: ACCENT,
|
|
102
|
+
lineHeight: 1.3, align: "center",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|