@skalfa/skalfa-app 1.0.0 → 1.0.1

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.
Files changed (87) hide show
  1. package/.env.example +43 -43
  2. package/.github/workflows/publish.yml +39 -0
  3. package/CONTRIBUTING.md +45 -0
  4. package/LICENSE +21 -0
  5. package/README.md +91 -28
  6. package/app/auth/edit/page.tsx +65 -65
  7. package/app/auth/login/page.tsx +63 -63
  8. package/app/auth/me/page.tsx +58 -58
  9. package/app/auth/register/page.tsx +69 -69
  10. package/app/auth/verify/page.tsx +53 -53
  11. package/app/dashboard/user/page.tsx +76 -76
  12. package/app/layout.tsx +37 -37
  13. package/app/manifest.ts +25 -0
  14. package/app/page.tsx +13 -13
  15. package/barrels.json +5 -5
  16. package/blueprints/starter.blueprint.json +102 -102
  17. package/bun.lock +916 -0
  18. package/components/base.components/chip/Chip.component.tsx +39 -39
  19. package/components/base.components/document/DocumentViewer.component.tsx +163 -163
  20. package/components/base.components/document/ExportExcel.component.tsx +340 -340
  21. package/components/base.components/document/ImportExcel.component.tsx +315 -315
  22. package/components/base.components/document/PrintTable.component.tsx +204 -204
  23. package/components/base.components/document/RenderPDF.component.tsx +415 -415
  24. package/components/base.components/input/Checkbox.component.tsx +109 -109
  25. package/components/base.components/input/Input.component.tsx +332 -332
  26. package/components/base.components/input/InputCheckbox.component.tsx +174 -174
  27. package/components/base.components/input/InputCurrency.component.tsx +163 -163
  28. package/components/base.components/input/InputDate.component.tsx +352 -352
  29. package/components/base.components/input/InputDatetime.component.tsx +260 -260
  30. package/components/base.components/input/InputDocument.component.tsx +351 -351
  31. package/components/base.components/input/InputImage.component.tsx +533 -533
  32. package/components/base.components/input/InputMap.component.tsx +317 -317
  33. package/components/base.components/input/InputNumber.component.tsx +192 -192
  34. package/components/base.components/input/InputOtp.component.tsx +169 -169
  35. package/components/base.components/input/InputPassword.component.tsx +236 -236
  36. package/components/base.components/input/InputRadio.component.tsx +175 -175
  37. package/components/base.components/input/InputTime.component.tsx +275 -275
  38. package/components/base.components/input/InputValues.component.tsx +68 -68
  39. package/components/base.components/input/Radio.component.tsx +102 -102
  40. package/components/base.components/input/Select.component.tsx +541 -541
  41. package/components/base.components/modal/BottomSheet.component.tsx +245 -245
  42. package/components/base.components/supervision/FormSupervision.component.tsx +433 -433
  43. package/components/base.components/supervision/TableSupervision.component.tsx +697 -697
  44. package/components/base.components/table/ControlBar.component.tsx +497 -497
  45. package/components/base.components/table/FilterComponent.tsx +518 -518
  46. package/components/base.components/table/Table.component.tsx +469 -469
  47. package/components/base.components/typography/TypographyArticle.component.tsx +26 -26
  48. package/components/base.components/typography/TypographyColumn.component.tsx +20 -20
  49. package/components/base.components/typography/TypographyContent.component.tsx +20 -20
  50. package/components/base.components/typography/TypographyTips.component.tsx +20 -20
  51. package/components/base.components/wrap/Draggable.component.tsx +303 -303
  52. package/components/base.components/wrap/IDBProvider.tsx +12 -12
  53. package/components/base.components/wrap/Image.component.tsx +9 -9
  54. package/components/base.components/wrap/ShortcutProvider.tsx +57 -57
  55. package/components/base.components/wrap/Swipe.component.tsx +93 -93
  56. package/components/index.ts +2 -2
  57. package/contexts/AppProvider.tsx +11 -11
  58. package/contexts/Auth.context.tsx +64 -64
  59. package/contexts/Toggle.context.tsx +44 -44
  60. package/next.config.ts +15 -1
  61. package/package.json +14 -13
  62. package/public/204.svg +19 -19
  63. package/public/500.svg +39 -39
  64. package/public/icon-192.png +0 -0
  65. package/public/icon-512.png +0 -0
  66. package/public/images/logo-fill.png +0 -0
  67. package/public/images/logo-full-fill.png +0 -0
  68. package/public/images/logo-full.png +0 -0
  69. package/public/images/logo.png +0 -0
  70. package/schema/idb/app.schema.ts +8 -8
  71. package/src-tauri/Cargo.toml +14 -0
  72. package/src-tauri/build.rs +3 -0
  73. package/src-tauri/capabilities/default.json +11 -0
  74. package/src-tauri/icons/128x128.png +0 -0
  75. package/src-tauri/icons/128x128@2x.png +0 -0
  76. package/src-tauri/icons/32x32.png +0 -0
  77. package/src-tauri/icons/icon.icns +0 -0
  78. package/src-tauri/icons/icon.ico +0 -0
  79. package/src-tauri/src/main.rs +7 -0
  80. package/src-tauri/tauri.conf.json +36 -0
  81. package/styles/globals.css +231 -231
  82. package/styles/tailwind.safelist +68 -68
  83. package/utils/commands/barrels.ts +27 -27
  84. package/utils/commands/light.ts +21 -21
  85. package/utils/commands/logger.ts +42 -42
  86. package/utils/commands/stubs/table-blueprint.stub +12 -12
  87. package/utils/commands/use-pdf.ts +29 -29
@@ -1,416 +1,416 @@
1
- "use client"
2
-
3
- import { PDFDocument, StandardFonts, PDFPage, rgb } from 'pdf-lib'
4
- import { useEffect, useRef } from 'react'
5
-
6
- export const PaperSize = {
7
- LETTER: { width: 612, height: 792 },
8
- A4: { width: 595, height: 842 },
9
- }
10
-
11
- export type RenderPDFProps = {
12
- content: PageSchema[]
13
- }
14
-
15
- export type PageSchema = {
16
- page: {
17
- size?: keyof typeof PaperSize | { width: number; height: number }
18
- margin?: number
19
- content: NodeSchema[]
20
- }
21
- }
22
-
23
- export type Style = {
24
- width?: number
25
- height?: number
26
-
27
- padding?: number
28
- paddingTop?: number
29
- paddingRight?: number
30
- paddingBottom?: number
31
- paddingLeft?: number
32
- paddingX?: number
33
- paddingY?: number
34
-
35
- marginTop?: number
36
- marginBottom?: number
37
-
38
- fontSize?: number
39
- fontWeight?: "normal" | "bold"
40
- lineHeight?: number
41
- letterSpacing?: number
42
- color?: string
43
- opacity?: number
44
- align?: "left" | "center" | "right"
45
- textTransform?: "uppercase" | "lowercase" | "capitalize"
46
-
47
- backgroundColor?: string
48
- borderColor?: string
49
- borderWidth?: number
50
-
51
- underline?: boolean
52
-
53
- textAlign?: "left" | "center" | "right"
54
- }
55
-
56
- export type NodeSchema =
57
- | { type: "view"; style?: Style; content: NodeSchema[] }
58
- | { type: "text"; content: string; style?: Style }
59
- | { type: "image"; src: string | Uint8Array | ArrayBuffer; style?: Style }
60
- | { type: "table"; content: NodeSchema[] }
61
- | { type: "tr"; content: NodeSchema[], style?: Style }
62
- | { type: 'td' | 'th'; content: NodeSchema[] | string; style?: Style }
63
-
64
- // ==================================================
65
- // Layout Context
66
- // ==================================================
67
-
68
- class LayoutContext {
69
- x: number
70
- y: number
71
- constructor(
72
- public width: number,
73
- public height: number,
74
- public margin: number
75
- ) {
76
- this.x = margin
77
- this.y = height - margin
78
- }
79
-
80
- needBreak(h: number) {
81
- return this.y - h < this.margin
82
- }
83
-
84
- reset() {
85
- this.x = this.margin
86
- this.y = this.height - this.margin
87
- }
88
- }
89
-
90
- // ==================================================
91
- // Helpers
92
- // ==================================================
93
-
94
- function resolvePadding(style?: Style) {
95
- const p = style?.padding ?? 0
96
- const px = style?.paddingX ?? p
97
- const py = style?.paddingY ?? p
98
-
99
- return {
100
- top: style?.paddingTop ?? py,
101
- bottom: style?.paddingBottom ?? py,
102
- left: style?.paddingLeft ?? px,
103
- right: style?.paddingRight ?? px,
104
- }
105
- }
106
-
107
- function resolveText(text: string, style?: Style) {
108
- if (!style?.textTransform) return text
109
- if (style.textTransform === "uppercase") return text.toUpperCase()
110
- if (style.textTransform === "lowercase") return text.toLowerCase()
111
- if (style.textTransform === "capitalize")
112
- return text.replace(/\b\w/g, c => c.toUpperCase())
113
- return text
114
- }
115
-
116
- function hexToRgb(hex?: string) {
117
- if (!hex) return undefined
118
- const h = hex.replace("#", "")
119
- return rgb(
120
- parseInt(h.slice(0, 2), 16) / 255,
121
- parseInt(h.slice(2, 4), 16) / 255,
122
- parseInt(h.slice(4, 6), 16) / 255
123
- )
124
- }
125
-
126
- async function embedImage(pdf: PDFDocument, bytes: Uint8Array | ArrayBuffer) {
127
- const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
128
- try {
129
- return await pdf.embedPng(data)
130
- } catch {
131
- return await pdf.embedJpg(data)
132
- }
133
- }
134
-
135
- async function resolveImageSource(
136
- src: string | Uint8Array | ArrayBuffer
137
- ): Promise<Uint8Array> {
138
- if (src instanceof Uint8Array) return src
139
- if (src instanceof ArrayBuffer) return new Uint8Array(src)
140
-
141
- const res = await fetch(src)
142
- if (!res.ok) {
143
- throw new Error(`Failed to load image: ${src}`)
144
- }
145
-
146
- const buffer = await res.arrayBuffer()
147
- return new Uint8Array(buffer)
148
- }
149
-
150
-
151
- function normalizeContent(
152
- content: string | NodeSchema[]
153
- ): NodeSchema[] {
154
- if (typeof content === "string") {
155
- return [
156
- {
157
- type: "text",
158
- content
159
- }
160
- ]
161
- }
162
- return content
163
- }
164
-
165
-
166
- // ==================================================
167
- // Render Engine
168
- // ==================================================
169
-
170
- export async function RenderPDF(
171
- { content }: RenderPDFProps
172
- ): Promise<Uint8Array> {
173
-
174
- const pdf = await PDFDocument.create()
175
-
176
- const fontRegular = await pdf.embedFont(StandardFonts.Courier)
177
- const fontBold = await pdf.embedFont(StandardFonts.CourierBold)
178
-
179
- for (const p of content) {
180
- const size =
181
- typeof p.page.size === "string"
182
- ? PaperSize[p.page.size]
183
- : p.page.size ?? PaperSize.A4
184
-
185
- const margin = p.page.margin ?? 40
186
-
187
- let page: PDFPage = pdf.addPage([size.width, size.height])
188
- const ctx = new LayoutContext(size.width, size.height, margin)
189
-
190
- const draw = async (node: NodeSchema) => {
191
-
192
- // ===================== VIEW =====================
193
- if (node.type === "view") {
194
- const pad = resolvePadding(node.style)
195
- const startY = ctx.y
196
-
197
- ctx.y -= pad.top
198
- ctx.x += pad.left
199
-
200
- for (const c of node.content) await draw(c)
201
-
202
- const endY = ctx.y
203
- const boxHeight = startY - endY
204
-
205
- if (node.style?.backgroundColor) {
206
- page.drawRectangle({
207
- x: ctx.margin,
208
- y: endY,
209
- width: size.width - ctx.margin * 2,
210
- height: boxHeight,
211
- color: hexToRgb(node.style.backgroundColor),
212
- })
213
- }
214
-
215
- if (node.style?.borderWidth && node.style?.borderColor) {
216
- page.drawRectangle({
217
- x: ctx.margin,
218
- y: endY,
219
- width: size.width - ctx.margin * 2,
220
- height: boxHeight,
221
- borderColor: hexToRgb(node.style.borderColor),
222
- borderWidth: node.style.borderWidth,
223
- })
224
- }
225
-
226
- ctx.x -= pad.left
227
- ctx.y -= pad.bottom
228
- return
229
- }
230
-
231
- // ===================== TEXT =====================
232
- if (node.type === "text") {
233
- const style = node.style
234
- const fs = style?.fontSize ?? 12
235
- const lh = style?.lineHeight ?? fs + 4
236
-
237
- if (ctx.needBreak(lh)) {
238
- page = pdf.addPage([size.width, size.height])
239
- ctx.reset()
240
- }
241
-
242
- const font =
243
- style?.fontWeight === "bold"
244
- ? fontBold
245
- : fontRegular
246
-
247
- const text = resolveText(node.content, style)
248
- const color = hexToRgb(style?.color)
249
-
250
- let x = ctx.x
251
- if (style?.align === "center") {
252
- const w = font.widthOfTextAtSize(text, fs)
253
- x = (size.width - w) / 2
254
- }
255
- if (style?.align === "right") {
256
- const w = font.widthOfTextAtSize(text, fs)
257
- x = size.width - ctx.margin - w
258
- }
259
-
260
- page.drawText(text, {
261
- x,
262
- y: ctx.y - fs,
263
- size: fs,
264
- font,
265
- color,
266
- opacity: style?.opacity,
267
- })
268
-
269
- if (style?.underline) {
270
- const w = font.widthOfTextAtSize(text, fs)
271
- page.drawLine({
272
- start: { x, y: ctx.y - fs - 2 },
273
- end: { x: x + w, y: ctx.y - fs - 2 },
274
- thickness: 1,
275
- })
276
- }
277
-
278
- ctx.y -= lh + (style?.marginBottom ?? 0)
279
- return
280
- }
281
-
282
- // ===================== IMAGE =====================
283
- if (node.type === "image") {
284
- const bytes = await resolveImageSource(node.src)
285
- const img = await embedImage(pdf, bytes)
286
- const base = img.scale(1)
287
-
288
- let w = node.style?.width ?? base.width
289
- let h = node.style?.height ?? base.height
290
-
291
- if (node.style?.width && !node.style?.height)
292
- h = (base.height / base.width) * w
293
-
294
- if (node.style?.height && !node.style?.width)
295
- w = (base.width / base.height) * h
296
-
297
- if (ctx.needBreak(h)) {
298
- page = pdf.addPage([size.width, size.height])
299
- ctx.reset()
300
- }
301
-
302
- page.drawImage(img, {
303
- x: ctx.x,
304
- y: ctx.y - h,
305
- width: w,
306
- height: h,
307
- })
308
-
309
- ctx.y -= h + (node.style?.marginBottom ?? 0)
310
- return
311
- }
312
-
313
-
314
- // ===================== TABLE =====================
315
- if (node.type === "table") {
316
- for (const r of node.content) await draw(r)
317
- ctx.y -= 8
318
- return
319
- }
320
-
321
- if (node.type === "tr") {
322
- const rowH = node.style?.height || 20;
323
-
324
- if (ctx.needBreak(rowH)) {
325
- page = pdf.addPage([size.width, size.height])
326
- ctx.reset()
327
- }
328
-
329
- const tableWidth = size.width - ctx.margin * 2
330
- const colCount = node.content.length
331
- const colWidth = tableWidth / colCount
332
-
333
- const originalX = ctx.x
334
- let x = ctx.margin
335
-
336
- for (const cell of node.content) {
337
- if (cell.type !== "td" && cell.type !== "th") continue
338
-
339
- const children = normalizeContent(cell.content)
340
- const pad = resolvePadding(cell.style)
341
- const startY = ctx.y
342
-
343
- ctx.x = x + pad.left
344
- ctx.y -= pad.top
345
-
346
- for (const child of children) {
347
- await draw(child)
348
- }
349
-
350
- ctx.y = startY
351
- x += colWidth
352
- }
353
-
354
- ctx.x = originalX
355
- ctx.y -= rowH
356
- return
357
- }
358
-
359
-
360
-
361
-
362
- }
363
-
364
- for (const n of p.page.content) await draw(n)
365
- }
366
-
367
- return await pdf.save()
368
- }
369
-
370
- export function RenderPDFPreview({ schema, className }: { schema: PageSchema[], className?: string }) {
371
- const canvasRef = useRef<HTMLCanvasElement>(null);
372
-
373
- useEffect(() => {
374
- let cancelled = false;
375
-
376
- (async () => {
377
- const bytes = await RenderPDF({ content: schema });
378
- const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
379
- pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";
380
-
381
- const pdf = await pdfjs.getDocument({ data: bytes }).promise;
382
- if (cancelled) return;
383
-
384
- const page = await pdf.getPage(1);
385
- const dpr = 1;
386
- const viewport = page.getViewport({ scale: 1 });
387
-
388
- const canvas = canvasRef.current!;
389
- const ctx = canvas.getContext("2d")!;
390
- canvas.style.width = `${viewport.width}px`;
391
- canvas.style.height = `${viewport.height}px`
392
- canvas.width = viewport.width;
393
- canvas.height = viewport.height;
394
-
395
- const scaledViewport = page.getViewport({ scale: dpr });
396
-
397
- const renderTask = page.render({
398
- canvas,
399
- canvasContext: ctx,
400
- viewport: scaledViewport,
401
- });
402
-
403
- await renderTask.promise;
404
- })();
405
-
406
- return () => {
407
- cancelled = true;
408
- };
409
- }, [schema]);
410
-
411
- return <>
412
- <div className={className}>
413
- <canvas ref={canvasRef} className="w-full border" />
414
- </div>
415
- </>
1
+ "use client"
2
+
3
+ import { PDFDocument, StandardFonts, PDFPage, rgb } from 'pdf-lib'
4
+ import { useEffect, useRef } from 'react'
5
+
6
+ export const PaperSize = {
7
+ LETTER: { width: 612, height: 792 },
8
+ A4: { width: 595, height: 842 },
9
+ }
10
+
11
+ export type RenderPDFProps = {
12
+ content: PageSchema[]
13
+ }
14
+
15
+ export type PageSchema = {
16
+ page: {
17
+ size?: keyof typeof PaperSize | { width: number; height: number }
18
+ margin?: number
19
+ content: NodeSchema[]
20
+ }
21
+ }
22
+
23
+ export type Style = {
24
+ width?: number
25
+ height?: number
26
+
27
+ padding?: number
28
+ paddingTop?: number
29
+ paddingRight?: number
30
+ paddingBottom?: number
31
+ paddingLeft?: number
32
+ paddingX?: number
33
+ paddingY?: number
34
+
35
+ marginTop?: number
36
+ marginBottom?: number
37
+
38
+ fontSize?: number
39
+ fontWeight?: "normal" | "bold"
40
+ lineHeight?: number
41
+ letterSpacing?: number
42
+ color?: string
43
+ opacity?: number
44
+ align?: "left" | "center" | "right"
45
+ textTransform?: "uppercase" | "lowercase" | "capitalize"
46
+
47
+ backgroundColor?: string
48
+ borderColor?: string
49
+ borderWidth?: number
50
+
51
+ underline?: boolean
52
+
53
+ textAlign?: "left" | "center" | "right"
54
+ }
55
+
56
+ export type NodeSchema =
57
+ | { type: "view"; style?: Style; content: NodeSchema[] }
58
+ | { type: "text"; content: string; style?: Style }
59
+ | { type: "image"; src: string | Uint8Array | ArrayBuffer; style?: Style }
60
+ | { type: "table"; content: NodeSchema[] }
61
+ | { type: "tr"; content: NodeSchema[], style?: Style }
62
+ | { type: 'td' | 'th'; content: NodeSchema[] | string; style?: Style }
63
+
64
+ // ==================================================
65
+ // Layout Context
66
+ // ==================================================
67
+
68
+ class LayoutContext {
69
+ x: number
70
+ y: number
71
+ constructor(
72
+ public width: number,
73
+ public height: number,
74
+ public margin: number
75
+ ) {
76
+ this.x = margin
77
+ this.y = height - margin
78
+ }
79
+
80
+ needBreak(h: number) {
81
+ return this.y - h < this.margin
82
+ }
83
+
84
+ reset() {
85
+ this.x = this.margin
86
+ this.y = this.height - this.margin
87
+ }
88
+ }
89
+
90
+ // ==================================================
91
+ // Helpers
92
+ // ==================================================
93
+
94
+ function resolvePadding(style?: Style) {
95
+ const p = style?.padding ?? 0
96
+ const px = style?.paddingX ?? p
97
+ const py = style?.paddingY ?? p
98
+
99
+ return {
100
+ top: style?.paddingTop ?? py,
101
+ bottom: style?.paddingBottom ?? py,
102
+ left: style?.paddingLeft ?? px,
103
+ right: style?.paddingRight ?? px,
104
+ }
105
+ }
106
+
107
+ function resolveText(text: string, style?: Style) {
108
+ if (!style?.textTransform) return text
109
+ if (style.textTransform === "uppercase") return text.toUpperCase()
110
+ if (style.textTransform === "lowercase") return text.toLowerCase()
111
+ if (style.textTransform === "capitalize")
112
+ return text.replace(/\b\w/g, c => c.toUpperCase())
113
+ return text
114
+ }
115
+
116
+ function hexToRgb(hex?: string) {
117
+ if (!hex) return undefined
118
+ const h = hex.replace("#", "")
119
+ return rgb(
120
+ parseInt(h.slice(0, 2), 16) / 255,
121
+ parseInt(h.slice(2, 4), 16) / 255,
122
+ parseInt(h.slice(4, 6), 16) / 255
123
+ )
124
+ }
125
+
126
+ async function embedImage(pdf: PDFDocument, bytes: Uint8Array | ArrayBuffer) {
127
+ const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
128
+ try {
129
+ return await pdf.embedPng(data)
130
+ } catch {
131
+ return await pdf.embedJpg(data)
132
+ }
133
+ }
134
+
135
+ async function resolveImageSource(
136
+ src: string | Uint8Array | ArrayBuffer
137
+ ): Promise<Uint8Array> {
138
+ if (src instanceof Uint8Array) return src
139
+ if (src instanceof ArrayBuffer) return new Uint8Array(src)
140
+
141
+ const res = await fetch(src)
142
+ if (!res.ok) {
143
+ throw new Error(`Failed to load image: ${src}`)
144
+ }
145
+
146
+ const buffer = await res.arrayBuffer()
147
+ return new Uint8Array(buffer)
148
+ }
149
+
150
+
151
+ function normalizeContent(
152
+ content: string | NodeSchema[]
153
+ ): NodeSchema[] {
154
+ if (typeof content === "string") {
155
+ return [
156
+ {
157
+ type: "text",
158
+ content
159
+ }
160
+ ]
161
+ }
162
+ return content
163
+ }
164
+
165
+
166
+ // ==================================================
167
+ // Render Engine
168
+ // ==================================================
169
+
170
+ export async function RenderPDF(
171
+ { content }: RenderPDFProps
172
+ ): Promise<Uint8Array> {
173
+
174
+ const pdf = await PDFDocument.create()
175
+
176
+ const fontRegular = await pdf.embedFont(StandardFonts.Courier)
177
+ const fontBold = await pdf.embedFont(StandardFonts.CourierBold)
178
+
179
+ for (const p of content) {
180
+ const size =
181
+ typeof p.page.size === "string"
182
+ ? PaperSize[p.page.size]
183
+ : p.page.size ?? PaperSize.A4
184
+
185
+ const margin = p.page.margin ?? 40
186
+
187
+ let page: PDFPage = pdf.addPage([size.width, size.height])
188
+ const ctx = new LayoutContext(size.width, size.height, margin)
189
+
190
+ const draw = async (node: NodeSchema) => {
191
+
192
+ // ===================== VIEW =====================
193
+ if (node.type === "view") {
194
+ const pad = resolvePadding(node.style)
195
+ const startY = ctx.y
196
+
197
+ ctx.y -= pad.top
198
+ ctx.x += pad.left
199
+
200
+ for (const c of node.content) await draw(c)
201
+
202
+ const endY = ctx.y
203
+ const boxHeight = startY - endY
204
+
205
+ if (node.style?.backgroundColor) {
206
+ page.drawRectangle({
207
+ x: ctx.margin,
208
+ y: endY,
209
+ width: size.width - ctx.margin * 2,
210
+ height: boxHeight,
211
+ color: hexToRgb(node.style.backgroundColor),
212
+ })
213
+ }
214
+
215
+ if (node.style?.borderWidth && node.style?.borderColor) {
216
+ page.drawRectangle({
217
+ x: ctx.margin,
218
+ y: endY,
219
+ width: size.width - ctx.margin * 2,
220
+ height: boxHeight,
221
+ borderColor: hexToRgb(node.style.borderColor),
222
+ borderWidth: node.style.borderWidth,
223
+ })
224
+ }
225
+
226
+ ctx.x -= pad.left
227
+ ctx.y -= pad.bottom
228
+ return
229
+ }
230
+
231
+ // ===================== TEXT =====================
232
+ if (node.type === "text") {
233
+ const style = node.style
234
+ const fs = style?.fontSize ?? 12
235
+ const lh = style?.lineHeight ?? fs + 4
236
+
237
+ if (ctx.needBreak(lh)) {
238
+ page = pdf.addPage([size.width, size.height])
239
+ ctx.reset()
240
+ }
241
+
242
+ const font =
243
+ style?.fontWeight === "bold"
244
+ ? fontBold
245
+ : fontRegular
246
+
247
+ const text = resolveText(node.content, style)
248
+ const color = hexToRgb(style?.color)
249
+
250
+ let x = ctx.x
251
+ if (style?.align === "center") {
252
+ const w = font.widthOfTextAtSize(text, fs)
253
+ x = (size.width - w) / 2
254
+ }
255
+ if (style?.align === "right") {
256
+ const w = font.widthOfTextAtSize(text, fs)
257
+ x = size.width - ctx.margin - w
258
+ }
259
+
260
+ page.drawText(text, {
261
+ x,
262
+ y: ctx.y - fs,
263
+ size: fs,
264
+ font,
265
+ color,
266
+ opacity: style?.opacity,
267
+ })
268
+
269
+ if (style?.underline) {
270
+ const w = font.widthOfTextAtSize(text, fs)
271
+ page.drawLine({
272
+ start: { x, y: ctx.y - fs - 2 },
273
+ end: { x: x + w, y: ctx.y - fs - 2 },
274
+ thickness: 1,
275
+ })
276
+ }
277
+
278
+ ctx.y -= lh + (style?.marginBottom ?? 0)
279
+ return
280
+ }
281
+
282
+ // ===================== IMAGE =====================
283
+ if (node.type === "image") {
284
+ const bytes = await resolveImageSource(node.src)
285
+ const img = await embedImage(pdf, bytes)
286
+ const base = img.scale(1)
287
+
288
+ let w = node.style?.width ?? base.width
289
+ let h = node.style?.height ?? base.height
290
+
291
+ if (node.style?.width && !node.style?.height)
292
+ h = (base.height / base.width) * w
293
+
294
+ if (node.style?.height && !node.style?.width)
295
+ w = (base.width / base.height) * h
296
+
297
+ if (ctx.needBreak(h)) {
298
+ page = pdf.addPage([size.width, size.height])
299
+ ctx.reset()
300
+ }
301
+
302
+ page.drawImage(img, {
303
+ x: ctx.x,
304
+ y: ctx.y - h,
305
+ width: w,
306
+ height: h,
307
+ })
308
+
309
+ ctx.y -= h + (node.style?.marginBottom ?? 0)
310
+ return
311
+ }
312
+
313
+
314
+ // ===================== TABLE =====================
315
+ if (node.type === "table") {
316
+ for (const r of node.content) await draw(r)
317
+ ctx.y -= 8
318
+ return
319
+ }
320
+
321
+ if (node.type === "tr") {
322
+ const rowH = node.style?.height || 20;
323
+
324
+ if (ctx.needBreak(rowH)) {
325
+ page = pdf.addPage([size.width, size.height])
326
+ ctx.reset()
327
+ }
328
+
329
+ const tableWidth = size.width - ctx.margin * 2
330
+ const colCount = node.content.length
331
+ const colWidth = tableWidth / colCount
332
+
333
+ const originalX = ctx.x
334
+ let x = ctx.margin
335
+
336
+ for (const cell of node.content) {
337
+ if (cell.type !== "td" && cell.type !== "th") continue
338
+
339
+ const children = normalizeContent(cell.content)
340
+ const pad = resolvePadding(cell.style)
341
+ const startY = ctx.y
342
+
343
+ ctx.x = x + pad.left
344
+ ctx.y -= pad.top
345
+
346
+ for (const child of children) {
347
+ await draw(child)
348
+ }
349
+
350
+ ctx.y = startY
351
+ x += colWidth
352
+ }
353
+
354
+ ctx.x = originalX
355
+ ctx.y -= rowH
356
+ return
357
+ }
358
+
359
+
360
+
361
+
362
+ }
363
+
364
+ for (const n of p.page.content) await draw(n)
365
+ }
366
+
367
+ return await pdf.save()
368
+ }
369
+
370
+ export function RenderPDFPreview({ schema, className }: { schema: PageSchema[], className?: string }) {
371
+ const canvasRef = useRef<HTMLCanvasElement>(null);
372
+
373
+ useEffect(() => {
374
+ let cancelled = false;
375
+
376
+ (async () => {
377
+ const bytes = await RenderPDF({ content: schema });
378
+ const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
379
+ pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";
380
+
381
+ const pdf = await pdfjs.getDocument({ data: bytes }).promise;
382
+ if (cancelled) return;
383
+
384
+ const page = await pdf.getPage(1);
385
+ const dpr = 1;
386
+ const viewport = page.getViewport({ scale: 1 });
387
+
388
+ const canvas = canvasRef.current!;
389
+ const ctx = canvas.getContext("2d")!;
390
+ canvas.style.width = `${viewport.width}px`;
391
+ canvas.style.height = `${viewport.height}px`
392
+ canvas.width = viewport.width;
393
+ canvas.height = viewport.height;
394
+
395
+ const scaledViewport = page.getViewport({ scale: dpr });
396
+
397
+ const renderTask = page.render({
398
+ canvas,
399
+ canvasContext: ctx,
400
+ viewport: scaledViewport,
401
+ });
402
+
403
+ await renderTask.promise;
404
+ })();
405
+
406
+ return () => {
407
+ cancelled = true;
408
+ };
409
+ }, [schema]);
410
+
411
+ return <>
412
+ <div className={className}>
413
+ <canvas ref={canvasRef} className="w-full border" />
414
+ </div>
415
+ </>
416
416
  }