h17-sspdf 0.4.1 → 0.5.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 CHANGED
@@ -30,9 +30,11 @@ Controls page geometry, background, and baseline rendering state.
30
30
  ```js
31
31
  page: {
32
32
  // Page geometry
33
- format: "a4", // only "a4" is supported
33
+ format: "a4", // named format: "a4", "letter", etc.
34
34
  orientation: "portrait", // "portrait" or "landscape"
35
35
  unit: "mm", // only "mm" is supported
36
+ pageWidthMm: 338, // custom width in mm (overrides format)
37
+ pageHeightMm: 190, // custom height in mm (overrides format)
36
38
  compress: true, // PDF compression (default true)
37
39
 
38
40
  // Margins - individual margins override the shared `margin` value
@@ -76,7 +78,9 @@ page: {
76
78
  }
77
79
  ```
78
80
 
79
- The content area runs from `marginTop` to `pageHeight - marginBottom` vertically, and `marginLeft` to `pageWidth - marginRight` horizontally. On A4 portrait, the page is 210 × 297 mm.
81
+ The content area runs from `marginTop` to `pageHeight - marginBottom` vertically, and `marginLeft` to `pageWidth - marginRight` horizontally. On A4 portrait, the page is 210 x 297 mm.
82
+
83
+ When `pageWidthMm` and `pageHeightMm` are both set, they override the `format` field and create a page with those exact dimensions. This is useful for non-standard formats like 16:9 presentations (e.g. 338 x 190 mm). All layout math -- margins, content area, pagination -- adapts automatically.
80
84
 
81
85
  ### `labels` (required)
82
86
 
@@ -235,6 +239,7 @@ Shared layout defaults read by operation handlers.
235
239
  ```js
236
240
  layout: {
237
241
  bulletIndentMm: 4.5, // text indent after bullet marker (default: 4)
242
+ chartAlign: "center", // chart horizontal alignment: "left" (default) or "center"
238
243
  }
239
244
  ```
240
245
 
@@ -553,6 +558,16 @@ Three forms (use exactly one):
553
558
  | `px` | number | Fixed space in CSS px |
554
559
  | `label` | string | Theme label with `spaceMm` or `spacePx` |
555
560
 
561
+ #### `pageBreak`
562
+
563
+ Forces a new page. The cursor resets to the top content area. Page templates (headers/footers) are applied to the new page automatically.
564
+
565
+ ```json
566
+ { "type": "pageBreak" }
567
+ ```
568
+
569
+ No fields required. Useful for presentation-style layouts where each section should start on its own page.
570
+
556
571
  #### `hiddenText`
557
572
 
558
573
  Renders text in the background color so it's invisible but present in the PDF text layer (for ATS keyword injection).
@@ -1714,11 +1729,12 @@ main();
1714
1729
  | `chartType` | yes | string | Chart.js type: `"bar"`, `"line"`, `"doughnut"`, etc. |
1715
1730
  | `data` | yes | object | Chart.js `data` config (`labels` + `datasets`) |
1716
1731
  | `widthMm` | no | number | Width in the PDF in mm (default: content area width) |
1717
- | `heightMm` | no | number | Height in the PDF in mm (default: 80) |
1732
+ | `heightMm` | no | number or `"fill"` | Height in mm (default: 80). `"fill"` uses remaining page space. |
1718
1733
  | `canvasWidth` | no | number | Canvas render width in pixels (default: 1600) |
1719
1734
  | `canvasHeight` | no | number | Canvas render height in pixels (default: 800) |
1720
1735
  | `options` | no | object | Chart.js `options` object. `responsive: false` and `animation: false` are injected automatically. |
1721
1736
  | `xMm` | no | number | Left edge in mm (default: content left margin) |
1737
+ | `align` | no | string | `"left"` (default) or `"center"`. Can also be set globally via `layout.chartAlign` in the theme. |
1722
1738
 
1723
1739
  ### Resolution
1724
1740
 
package/README.md CHANGED
@@ -263,6 +263,7 @@ labels: {
263
263
  | `divider` | Horizontal rule | `label`, `x1Mm`, `x2Mm` |
264
264
  | `image` | Embedded PNG/JPEG | `src`, `width` (percentage or mm), `caption` |
265
265
  | `spacer` | Vertical gap | `mm`, `px`, or `label` |
266
+ | `pageBreak` | Force new page | (none) |
266
267
  | `hiddenText` | Invisible text | `label`, `text` |
267
268
  | `quote` | Blockquote with attribution | `label`, `text`, `attribution` |
268
269
  | `block` | Group children, optional background + border | `children`, `keepTogether` |
@@ -466,7 +467,7 @@ Claude Code skills for generating PDFs and themes are available in the `skills/`
466
467
 
467
468
  ## Constraints
468
469
 
469
- - A4 only
470
+ - Page format defaults to A4; custom dimensions supported via `pageWidthMm`/`pageHeightMm` (e.g. 16:9 presentations)
470
471
  - Single-line `row` cells, no multi-line column pairs
471
472
  - `{{page}}` gives the current page number; `{{pages}}` (total page count) is not supported because keep-together rules make the final page count unpredictable until the last operation is laid out
472
473
  - Charts require the `canvas` npm package (native C++ addon) for server-side rendering; everything else is zero native dependencies
package/core/pdf-core.js CHANGED
@@ -77,6 +77,8 @@ class PDFCore {
77
77
  * @param {number} [theme.page.margin]
78
78
  * @param {string} [theme.page.format]
79
79
  * @param {string} [theme.page.orientation]
80
+ * @param {number} [theme.page.pageWidthMm]
81
+ * @param {number} [theme.page.pageHeightMm]
80
82
  * @param {string} [theme.page.unit]
81
83
  * @param {boolean} [theme.page.compress]
82
84
  * @param {number[]} [theme.page.backgroundColor]
@@ -102,11 +104,13 @@ class PDFCore {
102
104
  this.margin = this.marginLeftMm;
103
105
  this.headerHeightMm = Number(this.page.headerHeightMm) || 0;
104
106
  this.footerHeightMm = Number(this.page.footerHeightMm) || 0;
105
- this.format = String(this.page.format || "a4").toLowerCase();
107
+ const customW = Number(this.page.pageWidthMm) || 0;
108
+ const customH = Number(this.page.pageHeightMm) || 0;
109
+ const hasCustomDimensions = customW > 0 && customH > 0;
106
110
 
107
- if (this.format !== "a4") {
108
- throw new Error(`Unsupported page format "${this.page.format}". Only A4 is supported.`);
109
- }
111
+ this.format = hasCustomDimensions
112
+ ? [customW, customH]
113
+ : String(this.page.format || "a4").toLowerCase();
110
114
 
111
115
  this.doc = new jsPDF({
112
116
  orientation: this.page.orientation || "portrait",
@@ -132,14 +132,30 @@ module.exports = {
132
132
  );
133
133
  }
134
134
 
135
- const widthMm = operation.widthMm || (bounds.right - bounds.left);
136
- const heightMm = operation.heightMm || 80;
137
- const x = operation.xMm !== undefined ? operation.xMm : bounds.left;
135
+ const { theme } = ctx;
136
+ const contentWidth = bounds.right - bounds.left;
137
+ const widthMm = operation.widthMm || contentWidth;
138
+ const heightMm = operation.heightMm === "fill"
139
+ ? Math.max(0, core.contentBottomY - core.getCursorY())
140
+ : (operation.heightMm || 80);
141
+ const align = operation.align || (theme && theme.layout && theme.layout.chartAlign) || "left";
142
+
143
+ let x;
144
+ if (operation.xMm !== undefined) {
145
+ x = operation.xMm;
146
+ } else if (align === "center") {
147
+ x = bounds.left + (contentWidth - widthMm) / 2;
148
+ } else {
149
+ x = bounds.left;
150
+ }
138
151
 
139
152
  core.drawImage({ data: operation._buf, format: 'PNG', x, widthMm, heightMm });
140
153
  },
141
154
 
142
155
  estimateHeight(ctx) {
156
+ if (ctx.operation.heightMm === "fill") {
157
+ return ctx.core.contentBottomY - ctx.core.getCursorY();
158
+ }
143
159
  return (ctx.operation.heightMm || 80) + 4;
144
160
  },
145
161
 
@@ -221,17 +221,21 @@ function executeOperations(ctx) {
221
221
  }
222
222
  }
223
223
 
224
- core.withDocumentState(() => {
225
- executeOperation({
226
- core,
227
- theme,
228
- operation,
229
- index,
230
- templateMode,
231
- templateBypassMargins,
232
- insideContainer,
224
+ if (operation.type === "pageBreak") {
225
+ core.addPage();
226
+ } else {
227
+ core.withDocumentState(() => {
228
+ executeOperation({
229
+ core,
230
+ theme,
231
+ operation,
232
+ index,
233
+ templateMode,
234
+ templateBypassMargins,
235
+ insideContainer,
236
+ });
233
237
  });
234
- });
238
+ }
235
239
  }
236
240
  }
237
241
 
@@ -335,6 +339,7 @@ function isOperationType(type) {
335
339
  || type === "bullet"
336
340
  || type === "divider"
337
341
  || type === "spacer"
342
+ || type === "pageBreak"
338
343
  || type === "hiddenText"
339
344
  || type === "table"
340
345
  || type === "image"
@@ -1026,6 +1031,10 @@ function estimateOperationHeight(ctx) {
1026
1031
  throw new Error(`Spacer operation at index ${index} must provide mm, px, or label with spaceMm/spacePx`);
1027
1032
  }
1028
1033
 
1034
+ if (operation.type === "pageBreak") {
1035
+ return 0;
1036
+ }
1037
+
1029
1038
  if (operation.type === "hiddenText") {
1030
1039
  return 0;
1031
1040
  }
Binary file
@@ -0,0 +1,634 @@
1
+ {
2
+ "pageTemplates": {
3
+ "footer": [
4
+ {
5
+ "type": "divider",
6
+ "label": "slide.footer.rule",
7
+ "x1Mm": 24,
8
+ "x2Mm": 314
9
+ },
10
+ {
11
+ "type": "row",
12
+ "leftLabel": "slide.footer.left",
13
+ "rightLabel": "slide.footer.right",
14
+ "leftText": "h17.ai | sspdf",
15
+ "rightText": "{{page}}",
16
+ "xLeftMm": 24,
17
+ "xRightMm": 314
18
+ }
19
+ ],
20
+ "footerHeightMm": 8,
21
+ "footerStartMm": 178,
22
+ "footerBypassMargins": false
23
+ },
24
+ "sections": [
25
+ {
26
+ "type": "section",
27
+ "comment": "Title slide",
28
+ "content": [
29
+ {
30
+ "type": "image",
31
+ "src": "examples/sources/og-sspdf.png",
32
+ "width": "70%",
33
+ "label": "slide.image"
34
+ },
35
+ {
36
+ "type": "spacer",
37
+ "mm": 4
38
+ },
39
+ {
40
+ "type": "text",
41
+ "label": "slide.subtitle",
42
+ "text": "A math-first PDF engine. Send JSON, get a pixel-perfect document in under a second."
43
+ },
44
+ {
45
+ "type": "divider",
46
+ "label": "slide.divider.accent",
47
+ "widthMm": 60
48
+ },
49
+ {
50
+ "type": "text",
51
+ "label": "slide.author",
52
+ "text": "Hugo Palma | h17.ai | March 2026"
53
+ },
54
+ {
55
+ "type": "pageBreak"
56
+ }
57
+ ]
58
+ },
59
+ {
60
+ "type": "section",
61
+ "comment": "The problem",
62
+ "content": [
63
+ {
64
+ "type": "text",
65
+ "label": "slide.heading",
66
+ "text": "The problem"
67
+ },
68
+ {
69
+ "type": "divider",
70
+ "label": "slide.divider.accent",
71
+ "widthMm": 40
72
+ },
73
+ {
74
+ "type": "text",
75
+ "label": "slide.body",
76
+ "text": "Most PDF libraries force you to think in coordinates. You place text at (x, y), draw lines at absolute positions, and manually track where the cursor is. Change a font size and your entire layout breaks."
77
+ },
78
+ {
79
+ "type": "spacer",
80
+ "mm": 4
81
+ },
82
+ {
83
+ "type": "bullet",
84
+ "markerLabel": "slide.bullet.marker",
85
+ "label": "slide.bullet.text",
86
+ "text": "Layouts break when content length changes"
87
+ },
88
+ {
89
+ "type": "bullet",
90
+ "markerLabel": "slide.bullet.marker",
91
+ "label": "slide.bullet.text",
92
+ "text": "No automatic pagination or page-break logic"
93
+ },
94
+ {
95
+ "type": "bullet",
96
+ "markerLabel": "slide.bullet.marker",
97
+ "label": "slide.bullet.text",
98
+ "text": "Style changes require touching every draw call"
99
+ },
100
+ {
101
+ "type": "bullet",
102
+ "markerLabel": "slide.bullet.marker",
103
+ "label": "slide.bullet.text",
104
+ "text": "Themes, reuse, and consistency are an afterthought"
105
+ },
106
+ {
107
+ "type": "pageBreak"
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ "type": "section",
113
+ "comment": "Architecture",
114
+ "content": [
115
+ {
116
+ "type": "text",
117
+ "label": "slide.heading",
118
+ "text": "How sspdf works"
119
+ },
120
+ {
121
+ "type": "divider",
122
+ "label": "slide.divider.accent",
123
+ "widthMm": 40
124
+ },
125
+ {
126
+ "type": "text",
127
+ "label": "slide.body",
128
+ "text": "Two inputs, one output. The theme defines every visual rule. The source defines content as a flat list of operations. The engine handles layout, pagination, and rendering."
129
+ },
130
+ {
131
+ "type": "spacer",
132
+ "mm": 4
133
+ },
134
+ {
135
+ "type": "table",
136
+ "label": "slide.table",
137
+ "headerLabel": "slide.table.header",
138
+ "columns": [
139
+ {
140
+ "header": "Layer",
141
+ "widthMm": 80
142
+ },
143
+ {
144
+ "header": "Responsibility",
145
+ "widthMm": 210
146
+ }
147
+ ],
148
+ "rows": [
149
+ [
150
+ "Theme (.js)",
151
+ "Page geometry, fonts, colors, margins, label styles"
152
+ ],
153
+ [
154
+ "Source (.json)",
155
+ "Content: text, bullets, tables, charts, images, spacers"
156
+ ],
157
+ [
158
+ "PDFCore",
159
+ "Cursor math, page breaks, style reset, drawing primitives"
160
+ ],
161
+ [
162
+ "render-document",
163
+ "Reads source ops, resolves labels from theme, calls PDFCore"
164
+ ],
165
+ [
166
+ "Plugins",
167
+ "Chart rendering (Chart.js), image embedding (PNG/JPEG)"
168
+ ]
169
+ ]
170
+ },
171
+ {
172
+ "type": "pageBreak"
173
+ }
174
+ ]
175
+ },
176
+ {
177
+ "type": "section",
178
+ "comment": "Operation types",
179
+ "content": [
180
+ {
181
+ "type": "text",
182
+ "label": "slide.heading",
183
+ "text": "Operation types"
184
+ },
185
+ {
186
+ "type": "divider",
187
+ "label": "slide.divider.accent",
188
+ "widthMm": 40
189
+ },
190
+ {
191
+ "type": "text",
192
+ "label": "slide.body",
193
+ "text": "Every document is built from these primitives. Each one is self-contained and flows automatically."
194
+ },
195
+ {
196
+ "type": "spacer",
197
+ "mm": 4
198
+ },
199
+ {
200
+ "type": "table",
201
+ "label": "slide.table",
202
+ "headerLabel": "slide.table.header",
203
+ "columns": [
204
+ {
205
+ "header": "Operation",
206
+ "widthMm": 65
207
+ },
208
+ {
209
+ "header": "What it does",
210
+ "widthMm": 225
211
+ }
212
+ ],
213
+ "rows": [
214
+ [
215
+ "text",
216
+ "Single or multi-line text block with word wrapping and alignment"
217
+ ],
218
+ [
219
+ "row",
220
+ "Left/right pair on the same line (dates, labels, key-value)"
221
+ ],
222
+ [
223
+ "bullet",
224
+ "Bulleted item with text or vector shape markers"
225
+ ],
226
+ [
227
+ "divider",
228
+ "Horizontal rule with configurable color, width, dash pattern"
229
+ ],
230
+ [
231
+ "table",
232
+ "Multi-column table with headers, alt-row shading, page-break header repeat"
233
+ ],
234
+ [
235
+ "chart",
236
+ "Bar, line, doughnut, pie via Chart.js (server-side canvas)"
237
+ ],
238
+ [
239
+ "image",
240
+ "Embedded PNG/JPEG with percentage or explicit sizing"
241
+ ],
242
+ [
243
+ "spacer",
244
+ "Vertical gap in millimeters"
245
+ ],
246
+ [
247
+ "section",
248
+ "Grouping wrapper, transparent to layout"
249
+ ],
250
+ [
251
+ "hiddenText",
252
+ "Invisible metadata for PDF search indexing"
253
+ ]
254
+ ]
255
+ },
256
+ {
257
+ "type": "pageBreak"
258
+ }
259
+ ]
260
+ },
261
+ {
262
+ "type": "section",
263
+ "comment": "Benchmark chart",
264
+ "content": [
265
+ {
266
+ "type": "text",
267
+ "label": "slide.heading",
268
+ "text": "Render performance"
269
+ },
270
+ {
271
+ "type": "divider",
272
+ "label": "slide.divider.accent",
273
+ "widthMm": 40
274
+ },
275
+ {
276
+ "type": "text",
277
+ "label": "slide.body",
278
+ "text": "All documents render in under a second. Complex multi-page layouts with tables and charts stay well below 500ms."
279
+ },
280
+ {
281
+ "type": "spacer",
282
+ "mm": 4
283
+ },
284
+ {
285
+ "type": "chart",
286
+ "chartType": "bar",
287
+ "widthMm": 260,
288
+ "heightMm": "fill",
289
+ "canvasWidth": 2600,
290
+ "canvasHeight": 800,
291
+
292
+ "data": {
293
+ "labels": [
294
+ "Invoice",
295
+ "Article",
296
+ "Financial Report",
297
+ "Board Brief",
298
+ "Event Program",
299
+ "Certificate"
300
+ ],
301
+ "datasets": [
302
+ {
303
+ "label": "Render time (ms)",
304
+ "data": [
305
+ 42,
306
+ 68,
307
+ 187,
308
+ 95,
309
+ 112,
310
+ 31
311
+ ],
312
+ "backgroundColor": "rgba(0, 229, 255, 0.7)",
313
+ "borderColor": "rgba(0, 229, 255, 1)",
314
+ "borderWidth": 1
315
+ }
316
+ ]
317
+ },
318
+ "options": {
319
+ "indexAxis": "y",
320
+ "scales": {
321
+ "x": {
322
+ "beginAtZero": true,
323
+ "ticks": {
324
+ "color": "rgba(122, 122, 136, 1)",
325
+ "font": {
326
+ "size": 18
327
+ }
328
+ },
329
+ "grid": {
330
+ "color": "rgba(30, 30, 40, 1)"
331
+ },
332
+ "title": {
333
+ "display": true,
334
+ "text": "Milliseconds",
335
+ "color": "rgba(122, 122, 136, 1)",
336
+ "font": {
337
+ "size": 18
338
+ }
339
+ }
340
+ },
341
+ "y": {
342
+ "ticks": {
343
+ "color": "rgba(200, 200, 210, 1)",
344
+ "font": {
345
+ "size": 18
346
+ }
347
+ },
348
+ "grid": {
349
+ "display": false
350
+ }
351
+ }
352
+ },
353
+ "plugins": {
354
+ "legend": {
355
+ "display": false
356
+ }
357
+ },
358
+ "layout": {
359
+ "padding": {
360
+ "bottom": 5
361
+ }
362
+ }
363
+ }
364
+ },
365
+ {
366
+ "type": "pageBreak"
367
+ }
368
+ ]
369
+ },
370
+ {
371
+ "type": "section",
372
+ "comment": "Theme system",
373
+ "content": [
374
+ {
375
+ "type": "text",
376
+ "label": "slide.heading",
377
+ "text": "Theme system"
378
+ },
379
+ {
380
+ "type": "divider",
381
+ "label": "slide.divider.accent",
382
+ "widthMm": 40
383
+ },
384
+ {
385
+ "type": "text",
386
+ "label": "slide.body",
387
+ "text": "Themes are plain JS objects. Every visual decision lives in labels. Swap the theme file and the same source JSON renders in a completely different style."
388
+ },
389
+ {
390
+ "type": "spacer",
391
+ "mm": 4
392
+ },
393
+ {
394
+ "type": "row",
395
+ "leftLabel": "slide.row.left",
396
+ "rightLabel": "slide.row.right",
397
+ "leftText": "Built-in fonts",
398
+ "rightText": "20 Google Fonts shipped as base64 TTF (Inter, Roboto, Lora, Fira Code...)",
399
+ "xLeftMm": 24,
400
+ "xRightMm": 314
401
+ },
402
+ {
403
+ "type": "row",
404
+ "leftLabel": "slide.row.left",
405
+ "rightLabel": "slide.row.right",
406
+ "leftText": "Vector bullets",
407
+ "rightText": "20 shape markers (arrows, checkmarks, diamonds) -- no Unicode issues",
408
+ "xLeftMm": 24,
409
+ "xRightMm": 314
410
+ },
411
+ {
412
+ "type": "row",
413
+ "leftLabel": "slide.row.left",
414
+ "rightLabel": "slide.row.right",
415
+ "leftText": "Label properties",
416
+ "rightText": "Font, color, margin, padding, background, border, border-radius, left accent",
417
+ "xLeftMm": 24,
418
+ "xRightMm": 314
419
+ },
420
+ {
421
+ "type": "row",
422
+ "leftLabel": "slide.row.left",
423
+ "rightLabel": "slide.row.right",
424
+ "leftText": "Page templates",
425
+ "rightText": "Repeating headers and footers with page number tokens",
426
+ "xLeftMm": 24,
427
+ "xRightMm": 314
428
+ },
429
+ {
430
+ "type": "spacer",
431
+ "mm": 6
432
+ },
433
+ {
434
+ "type": "text",
435
+ "label": "slide.callout",
436
+ "text": "This presentation is itself rendered by sspdf using a 338x190mm (16:9) custom page format with the h17.ai color palette."
437
+ },
438
+ {
439
+ "type": "pageBreak"
440
+ }
441
+ ]
442
+ },
443
+ {
444
+ "type": "section",
445
+ "comment": "Adoption chart",
446
+ "content": [
447
+ {
448
+ "type": "text",
449
+ "label": "slide.heading",
450
+ "text": "Test coverage and growth"
451
+ },
452
+ {
453
+ "type": "divider",
454
+ "label": "slide.divider.accent",
455
+ "widthMm": 40
456
+ },
457
+ {
458
+ "type": "spacer",
459
+ "mm": 2
460
+ },
461
+ {
462
+ "type": "chart",
463
+ "chartType": "line",
464
+ "widthMm": 260,
465
+ "heightMm": "fill",
466
+ "canvasWidth": 2600,
467
+ "canvasHeight": 800,
468
+
469
+ "data": {
470
+ "labels": [
471
+ "v0.1.0",
472
+ "v0.1.2",
473
+ "v0.1.4",
474
+ "v0.1.6",
475
+ "v0.1.8",
476
+ "v0.2.0",
477
+ "v0.3.0",
478
+ "v0.4.0"
479
+ ],
480
+ "datasets": [
481
+ {
482
+ "label": "Test count",
483
+ "data": [
484
+ 8,
485
+ 14,
486
+ 22,
487
+ 30,
488
+ 38,
489
+ 40,
490
+ 42,
491
+ 48
492
+ ],
493
+ "borderColor": "rgba(0, 229, 255, 1)",
494
+ "backgroundColor": "rgba(0, 229, 255, 0.1)",
495
+ "fill": true,
496
+ "tension": 0.3,
497
+ "pointRadius": 6,
498
+ "pointBackgroundColor": "rgba(0, 229, 255, 1)",
499
+ "borderWidth": 3
500
+ },
501
+ {
502
+ "label": "Operation types",
503
+ "data": [
504
+ 3,
505
+ 4,
506
+ 5,
507
+ 7,
508
+ 8,
509
+ 9,
510
+ 9,
511
+ 10
512
+ ],
513
+ "borderColor": "rgba(0, 165, 184, 1)",
514
+ "backgroundColor": "rgba(0, 165, 184, 0.05)",
515
+ "fill": true,
516
+ "tension": 0.3,
517
+ "pointRadius": 6,
518
+ "pointBackgroundColor": "rgba(0, 165, 184, 1)",
519
+ "borderWidth": 3
520
+ }
521
+ ]
522
+ },
523
+ "options": {
524
+ "scales": {
525
+ "x": {
526
+ "ticks": {
527
+ "color": "rgba(122, 122, 136, 1)",
528
+ "font": {
529
+ "size": 18
530
+ }
531
+ },
532
+ "grid": {
533
+ "color": "rgba(30, 30, 40, 1)"
534
+ }
535
+ },
536
+ "y": {
537
+ "beginAtZero": true,
538
+ "ticks": {
539
+ "color": "rgba(122, 122, 136, 1)",
540
+ "font": {
541
+ "size": 18
542
+ }
543
+ },
544
+ "grid": {
545
+ "color": "rgba(30, 30, 40, 1)"
546
+ }
547
+ }
548
+ },
549
+ "plugins": {
550
+ "legend": {
551
+ "position": "bottom",
552
+ "labels": {
553
+ "color": "rgba(200, 200, 210, 1)",
554
+ "font": {
555
+ "size": 18
556
+ },
557
+ "padding": 20
558
+ }
559
+ }
560
+ },
561
+ "layout": {
562
+ "padding": {
563
+ "bottom": 5
564
+ }
565
+ }
566
+ }
567
+ },
568
+ {
569
+ "type": "pageBreak"
570
+ }
571
+ ]
572
+ },
573
+ {
574
+ "type": "section",
575
+ "comment": "Closing slide",
576
+ "content": [
577
+ {
578
+ "type": "spacer",
579
+ "mm": 24
580
+ },
581
+ {
582
+ "type": "text",
583
+ "label": "slide.kicker",
584
+ "text": "Get Started"
585
+ },
586
+ {
587
+ "type": "text",
588
+ "label": "slide.title",
589
+ "text": "npm install h17-sspdf"
590
+ },
591
+ {
592
+ "type": "spacer",
593
+ "mm": 8
594
+ },
595
+ {
596
+ "type": "divider",
597
+ "label": "slide.divider.accent",
598
+ "widthMm": 60
599
+ },
600
+ {
601
+ "type": "spacer",
602
+ "mm": 4
603
+ },
604
+ {
605
+ "type": "text",
606
+ "label": "slide.body",
607
+ "text": "Zero native dependencies. Works in Node.js. CLI and programmatic API included."
608
+ },
609
+ {
610
+ "type": "spacer",
611
+ "mm": 6
612
+ },
613
+ {
614
+ "type": "row",
615
+ "leftLabel": "slide.row.left",
616
+ "rightLabel": "slide.row.right",
617
+ "leftText": "GitHub",
618
+ "rightText": "github.com/hugopalma17/sspdf",
619
+ "xLeftMm": 24,
620
+ "xRightMm": 314
621
+ },
622
+ {
623
+ "type": "row",
624
+ "leftLabel": "slide.row.left",
625
+ "rightLabel": "slide.row.right",
626
+ "leftText": "Docs",
627
+ "rightText": "h17.ai/docs/sspdf",
628
+ "xLeftMm": 24,
629
+ "xRightMm": 314
630
+ }
631
+ ]
632
+ }
633
+ ]
634
+ }
@@ -0,0 +1,276 @@
1
+ const INTER = require("../../fonts/inter.js");
2
+
3
+ module.exports = {
4
+ name: "Presentation Theme",
5
+
6
+ customFonts: [
7
+ {
8
+ family: "Inter",
9
+ faces: [
10
+ { style: "normal", fileName: "Inter-Regular.ttf", data: INTER.Regular },
11
+ { style: "bold", fileName: "Inter-Bold.ttf", data: INTER.Bold },
12
+ ],
13
+ },
14
+ ],
15
+
16
+ page: {
17
+ pageWidthMm: 338,
18
+ pageHeightMm: 190,
19
+ orientation: "landscape",
20
+ unit: "mm",
21
+ compress: true,
22
+ marginTopMm: 16,
23
+ marginBottomMm: 14,
24
+ marginLeftMm: 24,
25
+ marginRightMm: 24,
26
+ backgroundColor: [6, 6, 8],
27
+ defaultText: {
28
+ fontFamily: "helvetica",
29
+ fontStyle: "normal",
30
+ fontSize: 14,
31
+ color: [232, 232, 236],
32
+ lineHeight: 1.4,
33
+ },
34
+ defaultStroke: {
35
+ color: [232, 232, 236],
36
+ lineWidth: 0.2,
37
+ lineCap: "butt",
38
+ lineJoin: "miter",
39
+ },
40
+ defaultFillColor: [6, 6, 8],
41
+ },
42
+
43
+ layout: {
44
+ chartAlign: "center",
45
+ },
46
+
47
+ labels: {
48
+ // ─── Slide footer ─────────────────────────────────────────
49
+ "slide.footer.rule": {
50
+ color: [74, 74, 85],
51
+ lineWidth: 0.25,
52
+ marginBottomPx: 2,
53
+ },
54
+ "slide.footer.left": {
55
+ fontFamily: "Inter",
56
+ fontStyle: "normal",
57
+ fontSize: 8,
58
+ color: [122, 122, 136],
59
+ marginBottomPx: 0,
60
+ },
61
+ "slide.footer.right": {
62
+ fontFamily: "Inter",
63
+ fontStyle: "normal",
64
+ fontSize: 8,
65
+ color: [122, 122, 136],
66
+ marginBottomPx: 0,
67
+ },
68
+
69
+ // ─── Title slide ──────────────────────────────────────────
70
+ "slide.kicker": {
71
+ fontFamily: "Inter",
72
+ fontStyle: "bold",
73
+ fontSize: 11,
74
+ textTransform: "upper",
75
+ align: "center",
76
+ color: [0, 229, 255],
77
+ marginBottomPx: 6,
78
+ },
79
+ "slide.title": {
80
+ fontFamily: "Inter",
81
+ fontStyle: "bold",
82
+ fontSize: 36,
83
+ align: "center",
84
+ color: [232, 232, 236],
85
+ lineHeight: 1.15,
86
+ marginBottomPx: 8,
87
+ },
88
+ "slide.subtitle": {
89
+ fontFamily: "Inter",
90
+ fontStyle: "normal",
91
+ fontSize: 16,
92
+ align: "center",
93
+ color: [122, 122, 136],
94
+ lineHeight: 1.4,
95
+ marginBottomPx: 6,
96
+ },
97
+ "slide.author": {
98
+ fontFamily: "Inter",
99
+ fontStyle: "normal",
100
+ fontSize: 12,
101
+ align: "center",
102
+ color: [74, 74, 85],
103
+ marginBottomPx: 0,
104
+ },
105
+
106
+ // ─── Content slides ───────────────────────────────────────
107
+ "slide.heading": {
108
+ fontFamily: "Inter",
109
+ fontStyle: "bold",
110
+ fontSize: 24,
111
+ align: "center",
112
+ color: [232, 232, 236],
113
+ lineHeight: 1.2,
114
+ marginBottomPx: 10,
115
+ },
116
+ "slide.body": {
117
+ fontFamily: "Inter",
118
+ fontStyle: "normal",
119
+ fontSize: 14,
120
+ align: "center",
121
+ color: [122, 122, 136],
122
+ lineHeight: 1.5,
123
+ marginBottomPx: 6,
124
+ },
125
+ "slide.body.bold": {
126
+ fontFamily: "Inter",
127
+ fontStyle: "bold",
128
+ fontSize: 14,
129
+ align: "center",
130
+ color: [232, 232, 236],
131
+ lineHeight: 1.5,
132
+ marginBottomPx: 6,
133
+ },
134
+ "slide.caption": {
135
+ fontFamily: "Inter",
136
+ fontStyle: "normal",
137
+ fontSize: 10,
138
+ color: [74, 74, 85],
139
+ lineHeight: 1.3,
140
+ marginBottomPx: 4,
141
+ },
142
+
143
+ // ─── Bullets ──────────────────────────────────────────────
144
+ "slide.bullet.marker": {
145
+ shape: "circle-filled",
146
+ shapeColor: [0, 229, 255],
147
+ shapeSize: 0.5,
148
+ textIndentMm: 3,
149
+ },
150
+ "slide.bullet.text": {
151
+ fontFamily: "Inter",
152
+ fontStyle: "normal",
153
+ fontSize: 14,
154
+ color: [122, 122, 136],
155
+ lineHeight: 1.5,
156
+ marginBottomPx: 5,
157
+ },
158
+
159
+ // ─── Metrics / KPI ────────────────────────────────────────
160
+ "slide.metric.value": {
161
+ fontFamily: "Inter",
162
+ fontStyle: "bold",
163
+ fontSize: 28,
164
+ color: [0, 229, 255],
165
+ align: "center",
166
+ marginBottomPx: 2,
167
+ },
168
+ "slide.metric.label": {
169
+ fontFamily: "Inter",
170
+ fontStyle: "normal",
171
+ fontSize: 11,
172
+ color: [122, 122, 136],
173
+ align: "center",
174
+ marginBottomPx: 4,
175
+ },
176
+
177
+ // ─── Dividers ─────────────────────────────────────────────
178
+ "slide.divider": {
179
+ color: [18, 18, 24],
180
+ lineWidth: 0.4,
181
+ marginTopPx: 6,
182
+ marginBottomPx: 6,
183
+ },
184
+ "slide.divider.accent": {
185
+ color: [0, 229, 255],
186
+ lineWidth: 1.2,
187
+ marginTopPx: 4,
188
+ marginBottomPx: 8,
189
+ },
190
+
191
+ // ─── Callout box ──────────────────────────────────────────
192
+ "slide.callout": {
193
+ fontFamily: "Inter",
194
+ fontStyle: "normal",
195
+ fontSize: 13,
196
+ align: "center",
197
+ color: [0, 229, 255],
198
+ lineHeight: 1.45,
199
+ backgroundColor: [12, 12, 16],
200
+ borderColor: [0, 165, 184],
201
+ borderWidthMm: 0.35,
202
+ borderRadiusMm: 2,
203
+ paddingTopMm: 4,
204
+ paddingBottomMm: 4,
205
+ paddingLeftMm: 5,
206
+ paddingRightMm: 5,
207
+ marginTopPx: 4,
208
+ marginBottomPx: 6,
209
+ },
210
+
211
+ // ─── Image ────────────────────────────────────────────────
212
+ "slide.image": {
213
+ paddingTopMm: 2,
214
+ paddingBottomMm: 2,
215
+ },
216
+ "slide.image.caption": {
217
+ fontFamily: "Inter",
218
+ fontStyle: "normal",
219
+ fontSize: 9,
220
+ color: [74, 74, 85],
221
+ align: "center",
222
+ lineHeight: 1.3,
223
+ },
224
+
225
+ // ─── Table ────────────────────────────────────────────────
226
+ "slide.table": {
227
+ fontFamily: "Inter",
228
+ fontStyle: "normal",
229
+ fontSize: 11,
230
+ color: [200, 200, 210],
231
+ lineHeight: 1.3,
232
+ cellPaddingMm: 2.5,
233
+ borderColor: [30, 30, 40],
234
+ borderTopMm: 0,
235
+ borderBottomMm: 0.15,
236
+ borderLeftMm: 0,
237
+ borderRightMm: 0,
238
+ altRowColor: [12, 12, 16],
239
+ },
240
+ "slide.table.header": {
241
+ fontFamily: "Inter",
242
+ fontStyle: "bold",
243
+ fontSize: 11,
244
+ color: [6, 6, 8],
245
+ lineHeight: 1.3,
246
+ cellPaddingMm: 2.5,
247
+ backgroundColor: [0, 229, 255],
248
+ borderColor: [0, 200, 220],
249
+ borderTopMm: 0,
250
+ borderBottomMm: 0.3,
251
+ borderLeftMm: 0,
252
+ borderRightMm: 0,
253
+ },
254
+
255
+ // ─── Row labels ───────────────────────────────────────────
256
+ "slide.row.left": {
257
+ fontFamily: "Inter",
258
+ fontStyle: "bold",
259
+ fontSize: 12,
260
+ color: [232, 232, 236],
261
+ marginBottomPx: 2,
262
+ },
263
+ "slide.row.right": {
264
+ fontFamily: "Inter",
265
+ fontStyle: "normal",
266
+ fontSize: 12,
267
+ color: [122, 122, 136],
268
+ marginBottomPx: 2,
269
+ },
270
+
271
+ // ─── Spacers ──────────────────────────────────────────────
272
+ "slide.spacer.sm": { spaceMm: 4 },
273
+ "slide.spacer.md": { spaceMm: 8 },
274
+ "slide.spacer.lg": { spaceMm: 16 },
275
+ },
276
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h17-sspdf",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Declarative PDF engine - define layout once, feed it JSON, get consistent PDFs",
5
5
  "main": "index.js",
6
6
  "author": "Hugo Palma",