@svgrid/enterprise 1.2.0 → 2.0.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/dist/cdn/svgrid-enterprise.svelte-external.js +26039 -805
- package/dist/designer/assets/index-Dp44bTid.js +939 -0
- package/dist/designer/assets/index-RJp6x8tw.css +1 -0
- package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
- package/dist/designer/index.html +13 -0
- package/dist/node/studio.js +22554 -0
- package/package.json +10 -3
- package/src/SvAuthGate.svelte +115 -0
- package/src/SvBoard.dom.test.ts +67 -0
- package/src/SvBoard.svelte +192 -0
- package/src/SvExportMenu.svelte +553 -0
- package/src/SvFileInput.svelte +113 -0
- package/src/SvGridEditPanel.dom.test.ts +146 -0
- package/src/SvGridEditPanel.svelte +793 -0
- package/src/SvGridMasterDetail.svelte +95 -0
- package/src/SvImportDialog.svelte +1020 -0
- package/src/SvLookupInput.svelte +180 -0
- package/src/SvRecordDetail.dom.test.ts +137 -0
- package/src/SvRecordDetail.svelte +288 -0
- package/src/SvSchedule.dom.test.ts +57 -0
- package/src/SvSchedule.svelte +156 -0
- package/src/SvSchemaChart.svelte +233 -0
- package/src/SvSchemaDashboard.svelte +130 -0
- package/src/ai-export-pdf.test.ts +74 -0
- package/src/ai-export-xlsx.test.ts +87 -0
- package/src/ai-export.test.ts +110 -0
- package/src/ai.ts +1188 -782
- package/src/edit-panel.test.ts +213 -0
- package/src/edit-panel.ts +228 -0
- package/src/export-conditional.test.ts +60 -0
- package/src/export-conditional.ts +94 -0
- package/src/export-ooxml.test.ts +152 -0
- package/src/export-ooxml.ts +485 -0
- package/src/export-pdf.test.ts +138 -0
- package/src/export-pdf.ts +245 -0
- package/src/export-print.test.ts +66 -0
- package/src/export-print.ts +132 -0
- package/src/export-serialize.test.ts +56 -0
- package/src/export-serialize.ts +24 -0
- package/src/export-xls.ts +196 -0
- package/src/export-xlsx-roundtrip.test.ts +120 -0
- package/src/export-xlsx.test.ts +150 -0
- package/src/export.test.ts +349 -0
- package/src/export.ts +1452 -549
- package/src/import-automap.test.ts +131 -0
- package/src/import-hardening.test.ts +158 -0
- package/src/import.ts +1042 -648
- package/src/index.ts +282 -0
- package/src/install.ts +134 -114
- package/src/license-core.test.ts +23 -0
- package/src/license-core.ts +35 -0
- package/src/license.ts +103 -90
- package/src/master-detail.test.ts +61 -0
- package/src/master-detail.ts +42 -0
- package/src/print.ts +107 -127
- package/src/schema-designer.test.ts +121 -0
- package/src/schema-designer.ts +142 -0
- package/src/schema.test.ts +268 -0
- package/src/schema.ts +499 -0
- package/src/smart-shim.ts +107 -105
- package/src/sources/aggregate.test.ts +79 -0
- package/src/sources/aggregate.ts +102 -0
- package/src/sources/auth-supabase.test.ts +80 -0
- package/src/sources/auth-supabase.ts +87 -0
- package/src/sources/dashboard.kpi.test.ts +50 -0
- package/src/sources/dashboard.test.ts +61 -0
- package/src/sources/dashboard.ts +136 -0
- package/src/sources/field-inference.test.ts +60 -0
- package/src/sources/field-inference.ts +63 -0
- package/src/sources/filters.test.ts +58 -0
- package/src/sources/filters.ts +64 -0
- package/src/sources/index.ts +67 -0
- package/src/sources/introspect-supabase.test.ts +100 -0
- package/src/sources/introspect-supabase.ts +119 -0
- package/src/sources/realtime-supabase.test.ts +93 -0
- package/src/sources/realtime-supabase.ts +99 -0
- package/src/sources/relation-lookup.test.ts +99 -0
- package/src/sources/relation-lookup.ts +128 -0
- package/src/sources/rest-adapters.test.ts +95 -0
- package/src/sources/rest-adapters.ts +120 -0
- package/src/sources/rest.test.ts +104 -0
- package/src/sources/rest.ts +129 -0
- package/src/sources/schema-from-columns.test.ts +106 -0
- package/src/sources/schema-from-columns.ts +88 -0
- package/src/sources/supabase.test.ts +110 -0
- package/src/sources/supabase.ts +99 -0
- package/src/sources/with-entity-rules.test.ts +104 -0
- package/src/sources/with-entity-rules.ts +78 -0
- package/src/sources/with-relation-labels.test.ts +75 -0
- package/src/sources/with-relation-labels.ts +73 -0
- package/src/studio/bug-report.test.ts +101 -0
- package/src/studio/bug-report.ts +165 -0
- package/src/studio/cli.test.ts +187 -0
- package/src/studio/cli.ts +112 -0
- package/src/studio/csv.test.ts +90 -0
- package/src/studio/csv.ts +165 -0
- package/src/studio/db-connect-string.test.ts +94 -0
- package/src/studio/db-connect-string.ts +128 -0
- package/src/studio/emit-project.test.ts +923 -0
- package/src/studio/emit-project.ts +1273 -0
- package/src/studio/emit-schema.test.ts +94 -0
- package/src/studio/emit-schema.ts +920 -0
- package/src/studio/index.ts +195 -0
- package/src/studio/introspect-db.test.ts +186 -0
- package/src/studio/introspect-db.ts +312 -0
- package/src/studio/introspect-prisma.test.ts +99 -0
- package/src/studio/introspect-prisma.ts +175 -0
- package/src/studio/introspect.test.ts +172 -0
- package/src/studio/introspect.ts +310 -0
- package/src/studio/pipeline.test.ts +42 -0
- package/src/studio/project-robust.test.ts +157 -0
- package/src/studio/project.test.ts +473 -0
- package/src/studio/project.ts +916 -0
- package/src/studio/sample-data.test.ts +58 -0
- package/src/studio/sample-data.ts +200 -0
- package/src/studio/samples/ats.ts +202 -0
- package/src/studio/samples/clinic.ts +177 -0
- package/src/studio/samples/crm.ts +250 -0
- package/src/studio/samples/ecommerce.ts +187 -0
- package/src/studio/samples/events.ts +199 -0
- package/src/studio/samples/fleet.ts +184 -0
- package/src/studio/samples/gym.ts +213 -0
- package/src/studio/samples/hr.ts +193 -0
- package/src/studio/samples/index.ts +55 -0
- package/src/studio/samples/insurance.ts +195 -0
- package/src/studio/samples/inventory.ts +182 -0
- package/src/studio/samples/invoicing.ts +162 -0
- package/src/studio/samples/library.ts +180 -0
- package/src/studio/samples/live-data.test.ts +98 -0
- package/src/studio/samples/live-data.ts +308 -0
- package/src/studio/samples/projects.ts +190 -0
- package/src/studio/samples/realestate.ts +196 -0
- package/src/studio/samples/restaurant.ts +187 -0
- package/src/studio/samples/samples.test.ts +208 -0
- package/src/studio/samples/school.ts +197 -0
- package/src/studio/samples/seed-floor.test.ts +25 -0
- package/src/studio/samples/shared.ts +305 -0
- package/src/studio/samples/subscriptions.ts +183 -0
- package/src/studio/samples/support.ts +182 -0
- package/src/studio/scaffold-app.test.ts +91 -0
- package/src/studio/scaffold-app.ts +161 -0
- package/src/studio/scaffold.test.ts +178 -0
- package/src/studio/scaffold.ts +374 -0
- package/src/studio/themes.ts +172 -0
- package/src/studio/verify.test.ts +47 -0
- package/src/studio/verify.ts +79 -0
- package/src/sveltekit/in-memory.test.ts +104 -0
- package/src/sveltekit/in-memory.ts +129 -0
- package/src/sveltekit/index.ts +23 -0
- package/src/sveltekit/query-plan.test.ts +109 -0
- package/src/sveltekit/query-plan.ts +125 -0
- package/src/sveltekit/sql-source.test.ts +185 -0
- package/src/sveltekit/sql-source.ts +166 -0
- package/src/sveltekit/sql.test.ts +80 -0
- package/src/sveltekit/sql.ts +131 -0
- package/src/sveltekit/transport.test.ts +175 -0
- package/src/sveltekit/transport.ts +254 -0
- package/src/sveltekit/types.ts +11 -0
- package/src/upgrade-prompt.ts +149 -148
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildPdfDocDefinition } from './export-pdf'
|
|
3
|
+
|
|
4
|
+
const columns = [
|
|
5
|
+
{ header: 'Company', align: 'left' as const },
|
|
6
|
+
{ header: 'Price', align: 'right' as const },
|
|
7
|
+
]
|
|
8
|
+
const rows = [
|
|
9
|
+
['ACME', '$19.95'],
|
|
10
|
+
['Globex', '$1,000.00'],
|
|
11
|
+
['Initech', '$42.00'],
|
|
12
|
+
]
|
|
13
|
+
const now = new Date('2026-07-10T12:00:00Z')
|
|
14
|
+
|
|
15
|
+
describe('buildPdfDocDefinition', () => {
|
|
16
|
+
it('builds a table with a bold, filled, repeated header row', () => {
|
|
17
|
+
const def = buildPdfDocDefinition({ columns, rows, now })
|
|
18
|
+
const table = (def.content.at(-1) as any).table
|
|
19
|
+
expect(table.headerRows).toBe(1)
|
|
20
|
+
expect(table.body).toHaveLength(4) // header + 3 rows
|
|
21
|
+
const header = table.body[0]
|
|
22
|
+
expect(header[0]).toMatchObject({ text: 'Company', bold: true, fillColor: '#334155' })
|
|
23
|
+
expect(header[1].alignment).toBe('right')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('carries per-column alignment into body cells', () => {
|
|
27
|
+
const def = buildPdfDocDefinition({ columns, rows, now })
|
|
28
|
+
const table = (def.content.at(-1) as any).table
|
|
29
|
+
expect(table.body[1][0]).toMatchObject({ text: 'ACME', alignment: 'left' })
|
|
30
|
+
expect(table.body[1][1]).toMatchObject({ text: '$19.95', alignment: 'right' })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('zebra-stripes even body rows but not the header', () => {
|
|
34
|
+
const def = buildPdfDocDefinition({ columns, rows, now })
|
|
35
|
+
const layout = (def.content.at(-1) as any).layout
|
|
36
|
+
expect(layout.fillColor(0)).toBeNull() // header
|
|
37
|
+
expect(layout.fillColor(1)).toBeNull() // odd body row
|
|
38
|
+
expect(layout.fillColor(2)).toBe('#f1f5f9') // even body row
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('defaults A4 portrait, auto-landscape for wide grids', () => {
|
|
42
|
+
const narrow = buildPdfDocDefinition({ columns, rows, now })
|
|
43
|
+
expect(narrow.pageSize).toBe('A4')
|
|
44
|
+
expect(narrow.pageOrientation).toBe('portrait')
|
|
45
|
+
|
|
46
|
+
const wideCols = Array.from({ length: 10 }, (_, i) => ({ header: `C${i}` }))
|
|
47
|
+
const wide = buildPdfDocDefinition({ columns: wideCols, rows: [], now })
|
|
48
|
+
expect(wide.pageOrientation).toBe('landscape')
|
|
49
|
+
|
|
50
|
+
const forced = buildPdfDocDefinition({ columns: wideCols, rows: [], opts: { pageOrientation: 'portrait' }, now })
|
|
51
|
+
expect(forced.pageOrientation).toBe('portrait')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('renders title / subtitle / logo before the table', () => {
|
|
55
|
+
const def = buildPdfDocDefinition({
|
|
56
|
+
columns,
|
|
57
|
+
rows,
|
|
58
|
+
opts: { title: 'Orders', subtitle: 'Q3', logo: 'data:image/png;base64,AAAA' },
|
|
59
|
+
now,
|
|
60
|
+
})
|
|
61
|
+
expect((def.content[0] as any).image).toBe('data:image/png;base64,AAAA')
|
|
62
|
+
expect((def.content[1] as any).text).toBe('Orders')
|
|
63
|
+
expect((def.content[2] as any).text).toBe('Q3')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('emits a "Page X of Y" + date footer by default, suppressible', () => {
|
|
67
|
+
const def = buildPdfDocDefinition({ columns, rows, now })
|
|
68
|
+
expect(def.footer).toBeTypeOf('function')
|
|
69
|
+
const footer = def.footer!(2, 5) as any
|
|
70
|
+
expect(footer.columns[0].text).toBe('2026-07-10')
|
|
71
|
+
expect(footer.columns[1].text).toBe('Page 2 of 5')
|
|
72
|
+
|
|
73
|
+
const noFooter = buildPdfDocDefinition({ columns, rows, opts: { showPageNumbers: false }, now })
|
|
74
|
+
expect(noFooter.footer).toBeUndefined()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('renders a structured body with group + subtotal rows', () => {
|
|
78
|
+
const def = buildPdfDocDefinition({
|
|
79
|
+
columns,
|
|
80
|
+
body: [
|
|
81
|
+
{ kind: 'group', label: 'Region: North (2)', level: 0 },
|
|
82
|
+
{ kind: 'data', cells: ['ACME', '$19.95'] },
|
|
83
|
+
{ kind: 'data', cells: ['Globex', '$1,000.00'] },
|
|
84
|
+
{ kind: 'subtotal', cells: ['Subtotal', '$1,019.95'] },
|
|
85
|
+
],
|
|
86
|
+
now,
|
|
87
|
+
})
|
|
88
|
+
const table = (def.content.at(-1) as any).table
|
|
89
|
+
expect(table.body).toHaveLength(5) // header + group + 2 data + subtotal
|
|
90
|
+
// Group header spans all columns, bold + filled.
|
|
91
|
+
expect(table.body[1][0]).toMatchObject({ text: 'Region: North (2)', colSpan: 2, bold: true })
|
|
92
|
+
// Subtotal row is bold + filled.
|
|
93
|
+
expect(table.body[4][1]).toMatchObject({ text: '$1,019.95', bold: true, fillColor: '#f8fafc' })
|
|
94
|
+
// Zebra only stripes plain data rows, never group / subtotal.
|
|
95
|
+
const layout = (def.content.at(-1) as any).layout
|
|
96
|
+
expect(layout.fillColor(1)).toBeNull() // group row
|
|
97
|
+
expect(layout.fillColor(4)).toBeNull() // subtotal row
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('applies dataCellStyle (conditional formatting) to data cells only', () => {
|
|
101
|
+
const def = buildPdfDocDefinition({
|
|
102
|
+
columns,
|
|
103
|
+
rows,
|
|
104
|
+
dataCellStyle: (r, c) =>
|
|
105
|
+
r === 0 && c === 1 ? { fill: '#fee2e2', color: '#991b1b', bold: true } : undefined,
|
|
106
|
+
now,
|
|
107
|
+
})
|
|
108
|
+
const table = (def.content.at(-1) as any).table
|
|
109
|
+
expect(table.body[1][1]).toMatchObject({ fillColor: '#fee2e2', color: '#991b1b', bold: true })
|
|
110
|
+
expect(table.body[2][1].fillColor).toBeUndefined() // other cells untouched
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('adds a hyperlink to a data cell', () => {
|
|
114
|
+
const def = buildPdfDocDefinition({
|
|
115
|
+
columns,
|
|
116
|
+
rows,
|
|
117
|
+
dataCellLink: (r, c) => (r === 0 && c === 0 ? 'https://x.test' : undefined),
|
|
118
|
+
now,
|
|
119
|
+
})
|
|
120
|
+
const table = (def.content.at(-1) as any).table
|
|
121
|
+
expect(table.body[1][0]).toMatchObject({ link: 'https://x.test', decoration: 'underline' })
|
|
122
|
+
expect(table.body[2][0].link).toBeUndefined()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('honors explicit column widths + margins + theme colors', () => {
|
|
126
|
+
const def = buildPdfDocDefinition({
|
|
127
|
+
columns,
|
|
128
|
+
rows,
|
|
129
|
+
opts: { columnWidths: [80, '*'], margins: [10, 10, 10, 10], headerColor: '#111827', zebra: false },
|
|
130
|
+
now,
|
|
131
|
+
})
|
|
132
|
+
const table = (def.content[0] as any).table
|
|
133
|
+
expect(table.widths).toEqual([80, '*'])
|
|
134
|
+
expect(def.pageMargins).toEqual([10, 10, 10, 10])
|
|
135
|
+
expect(table.body[0][0].fillColor).toBe('#111827')
|
|
136
|
+
expect((def.content[0] as any).layout.fillColor(2)).toBeNull() // zebra off
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF export via our own pdfmake document definition, instead of the vendored
|
|
3
|
+
* Smart exporter's basic table dump. This gives real control over the page:
|
|
4
|
+
* size / orientation / margins, a repeated header row on every page, automatic
|
|
5
|
+
* "Page X of Y" + generated-date footer, per-column alignment and widths, an
|
|
6
|
+
* optional title / subtitle / logo, and theme colors (header fill + zebra).
|
|
7
|
+
*
|
|
8
|
+
* `buildPdfDocDefinition` is a PURE function (no pdfmake, no DOM) returning the
|
|
9
|
+
* docDefinition object, so it's fully unit-testable; `exportGrid` feeds the
|
|
10
|
+
* result to `pdfMake.createPdf(def).download(...)`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type PdfPageSize = 'A4' | 'A3' | 'A5' | 'LETTER' | 'LEGAL'
|
|
14
|
+
|
|
15
|
+
export type PdfExportOptions = {
|
|
16
|
+
pageSize?: PdfPageSize
|
|
17
|
+
pageOrientation?: 'portrait' | 'landscape'
|
|
18
|
+
/** [left, top, right, bottom] in pt. Default [28, 34, 28, 34]. */
|
|
19
|
+
margins?: [number, number, number, number]
|
|
20
|
+
/** Document title rendered above the table. */
|
|
21
|
+
title?: string
|
|
22
|
+
/** Smaller line under the title. */
|
|
23
|
+
subtitle?: string
|
|
24
|
+
/** Logo as a data URL, rendered top-left above the title. */
|
|
25
|
+
logo?: string
|
|
26
|
+
/** Logo width in pt (height auto). Default 90. */
|
|
27
|
+
logoWidth?: number
|
|
28
|
+
/** Base font size in pt. Default 8. */
|
|
29
|
+
fontSize?: number
|
|
30
|
+
/** Header row fill color. Default '#334155'. */
|
|
31
|
+
headerColor?: string
|
|
32
|
+
/** Header row text color. Default '#ffffff'. */
|
|
33
|
+
headerTextColor?: string
|
|
34
|
+
/** Zebra-stripe alternate rows. Default true. */
|
|
35
|
+
zebra?: boolean
|
|
36
|
+
/** Zebra fill color. Default '#f1f5f9'. */
|
|
37
|
+
zebraColor?: string
|
|
38
|
+
/** Repeat the header row on every page. Default true. */
|
|
39
|
+
repeatHeader?: boolean
|
|
40
|
+
/** Show the "Page X of Y" + date footer. Default true. */
|
|
41
|
+
showPageNumbers?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* Column widths: '*' (fill, default), 'auto', or an explicit array of
|
|
44
|
+
* pt numbers / 'auto' / '*' per column.
|
|
45
|
+
*/
|
|
46
|
+
columnWidths?: '*' | 'auto' | Array<number | 'auto' | '*'>
|
|
47
|
+
/** Switch to landscape automatically when column count exceeds this (and no
|
|
48
|
+
* explicit orientation was set). Default 8. */
|
|
49
|
+
autoLandscapeThreshold?: number
|
|
50
|
+
/** Group header row fill color. Default '#e2e8f0'. */
|
|
51
|
+
groupColor?: string
|
|
52
|
+
/** Group header row text color. Default '#0f172a'. */
|
|
53
|
+
groupTextColor?: string
|
|
54
|
+
/** Subtotal row fill color. Default '#f8fafc'. */
|
|
55
|
+
subtotalColor?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A body row for the PDF table. `data` is a normal row; `group` is a bold,
|
|
60
|
+
* full-width group header (from grid grouping); `subtotal` is a per-group
|
|
61
|
+
* summary row (bold, aligned like the data).
|
|
62
|
+
*/
|
|
63
|
+
export type PdfBodyRow =
|
|
64
|
+
| { kind: 'data'; cells: ReadonlyArray<string> }
|
|
65
|
+
| { kind: 'group'; label: string; level?: number }
|
|
66
|
+
| { kind: 'subtotal'; cells: ReadonlyArray<string> }
|
|
67
|
+
|
|
68
|
+
type PdfCell = {
|
|
69
|
+
text: string
|
|
70
|
+
bold?: boolean
|
|
71
|
+
alignment?: 'left' | 'center' | 'right'
|
|
72
|
+
color?: string
|
|
73
|
+
fillColor?: string
|
|
74
|
+
colSpan?: number
|
|
75
|
+
margin?: [number, number, number, number]
|
|
76
|
+
link?: string
|
|
77
|
+
decoration?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type PdfDocDefinition = {
|
|
81
|
+
pageSize: PdfPageSize
|
|
82
|
+
pageOrientation: 'portrait' | 'landscape'
|
|
83
|
+
pageMargins: [number, number, number, number]
|
|
84
|
+
defaultStyle: { fontSize: number }
|
|
85
|
+
content: unknown[]
|
|
86
|
+
footer?: (currentPage: number, pageCount: number) => unknown
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a pdfmake docDefinition from resolved columns + formatted string rows.
|
|
91
|
+
* `now` is injected for a deterministic footer date in tests.
|
|
92
|
+
*/
|
|
93
|
+
export function buildPdfDocDefinition(params: {
|
|
94
|
+
columns: ReadonlyArray<{ header: string; align?: 'left' | 'center' | 'right' }>
|
|
95
|
+
/** Data rows as pre-formatted strings, in column order (no header row).
|
|
96
|
+
* Convenience for the flat (ungrouped) case. */
|
|
97
|
+
rows?: ReadonlyArray<ReadonlyArray<string>>
|
|
98
|
+
/** Structured body with group headers + subtotals. Takes precedence over
|
|
99
|
+
* `rows` when provided (used for grouped exports). */
|
|
100
|
+
body?: ReadonlyArray<PdfBodyRow>
|
|
101
|
+
/** Per-data-cell visual override (conditional formatting). `dataRowIdx` is
|
|
102
|
+
* the 0-based index among data rows. */
|
|
103
|
+
dataCellStyle?: (dataRowIdx: number, colIdx: number) => { fill?: string; color?: string; bold?: boolean } | undefined
|
|
104
|
+
/** Per-data-cell hyperlink URL. */
|
|
105
|
+
dataCellLink?: (dataRowIdx: number, colIdx: number) => string | undefined
|
|
106
|
+
opts?: PdfExportOptions
|
|
107
|
+
now?: Date
|
|
108
|
+
}): PdfDocDefinition {
|
|
109
|
+
const { columns } = params
|
|
110
|
+
const o = params.opts ?? {}
|
|
111
|
+
const now = params.now ?? new Date(0)
|
|
112
|
+
|
|
113
|
+
const fontSize = o.fontSize ?? 8
|
|
114
|
+
const headerColor = o.headerColor ?? '#334155'
|
|
115
|
+
const headerTextColor = o.headerTextColor ?? '#ffffff'
|
|
116
|
+
const zebra = o.zebra ?? true
|
|
117
|
+
const zebraColor = o.zebraColor ?? '#f1f5f9'
|
|
118
|
+
const groupColor = o.groupColor ?? '#e2e8f0'
|
|
119
|
+
const groupTextColor = o.groupTextColor ?? '#0f172a'
|
|
120
|
+
const subtotalColor = o.subtotalColor ?? '#f8fafc'
|
|
121
|
+
const repeatHeader = o.repeatHeader ?? true
|
|
122
|
+
|
|
123
|
+
const orientation: 'portrait' | 'landscape' =
|
|
124
|
+
o.pageOrientation ??
|
|
125
|
+
(columns.length > (o.autoLandscapeThreshold ?? 8) ? 'landscape' : 'portrait')
|
|
126
|
+
|
|
127
|
+
const widths: Array<number | 'auto' | '*'> = Array.isArray(o.columnWidths)
|
|
128
|
+
? o.columnWidths
|
|
129
|
+
: columns.map(() => (o.columnWidths === 'auto' ? 'auto' : '*'))
|
|
130
|
+
|
|
131
|
+
// Header row.
|
|
132
|
+
const headerRow: PdfCell[] = columns.map((c) => ({
|
|
133
|
+
text: c.header,
|
|
134
|
+
bold: true,
|
|
135
|
+
alignment: c.align ?? 'left',
|
|
136
|
+
color: headerTextColor,
|
|
137
|
+
fillColor: headerColor,
|
|
138
|
+
}))
|
|
139
|
+
|
|
140
|
+
// Normalize to a structured body: `body` wins, else wrap flat `rows`.
|
|
141
|
+
const model: ReadonlyArray<PdfBodyRow> =
|
|
142
|
+
params.body ?? (params.rows ?? []).map((cells) => ({ kind: 'data', cells }))
|
|
143
|
+
|
|
144
|
+
// Track which body-row indices are plain data rows, so zebra striping only
|
|
145
|
+
// hits those (group / subtotal rows carry their own fill).
|
|
146
|
+
const dataCellStyle = params.dataCellStyle
|
|
147
|
+
const dataCellLink = params.dataCellLink
|
|
148
|
+
let dataOrdinal = 0
|
|
149
|
+
const dataRowIdx = new Set<number>()
|
|
150
|
+
const bodyCells: PdfCell[][] = [headerRow]
|
|
151
|
+
model.forEach((row, i) => {
|
|
152
|
+
const bi = i + 1 // +1 for the header row
|
|
153
|
+
if (row.kind === 'group') {
|
|
154
|
+
const first: PdfCell = {
|
|
155
|
+
text: row.label,
|
|
156
|
+
bold: true,
|
|
157
|
+
color: groupTextColor,
|
|
158
|
+
fillColor: groupColor,
|
|
159
|
+
colSpan: columns.length,
|
|
160
|
+
margin: [(row.level ?? 0) * 12, 2, 0, 2],
|
|
161
|
+
}
|
|
162
|
+
const rest: PdfCell[] = Array.from({ length: Math.max(0, columns.length - 1) }, () => ({ text: '' }))
|
|
163
|
+
bodyCells.push([first, ...rest])
|
|
164
|
+
} else if (row.kind === 'subtotal') {
|
|
165
|
+
bodyCells.push(
|
|
166
|
+
columns.map((c, ci) => ({
|
|
167
|
+
text: row.cells[ci] ?? '',
|
|
168
|
+
bold: true,
|
|
169
|
+
alignment: c.align ?? 'left',
|
|
170
|
+
fillColor: subtotalColor,
|
|
171
|
+
})),
|
|
172
|
+
)
|
|
173
|
+
} else {
|
|
174
|
+
dataRowIdx.add(bi)
|
|
175
|
+
const ord = dataOrdinal++
|
|
176
|
+
bodyCells.push(
|
|
177
|
+
columns.map((c, ci) => {
|
|
178
|
+
const cell: PdfCell = { text: row.cells[ci] ?? '', alignment: c.align ?? 'left' }
|
|
179
|
+
const s = dataCellStyle?.(ord, ci)
|
|
180
|
+
if (s) {
|
|
181
|
+
if (s.fill) cell.fillColor = s.fill
|
|
182
|
+
if (s.color) cell.color = s.color
|
|
183
|
+
if (s.bold) cell.bold = true
|
|
184
|
+
}
|
|
185
|
+
const href = dataCellLink?.(ord, ci)
|
|
186
|
+
if (href) {
|
|
187
|
+
cell.link = href
|
|
188
|
+
cell.decoration = 'underline'
|
|
189
|
+
if (!s?.color) cell.color = '#2563eb'
|
|
190
|
+
}
|
|
191
|
+
return cell
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const table = {
|
|
198
|
+
table: {
|
|
199
|
+
headerRows: repeatHeader ? 1 : 0,
|
|
200
|
+
dontBreakRows: true,
|
|
201
|
+
widths,
|
|
202
|
+
body: bodyCells,
|
|
203
|
+
},
|
|
204
|
+
layout: {
|
|
205
|
+
// Zebra fill: only plain data rows (group / subtotal have their own).
|
|
206
|
+
fillColor: (rowIndex: number) =>
|
|
207
|
+
zebra && rowIndex > 0 && rowIndex % 2 === 0 && dataRowIdx.has(rowIndex) ? zebraColor : null,
|
|
208
|
+
hLineWidth: () => 0.5,
|
|
209
|
+
vLineWidth: () => 0.5,
|
|
210
|
+
hLineColor: () => '#e2e8f0',
|
|
211
|
+
vLineColor: () => '#e2e8f0',
|
|
212
|
+
paddingTop: () => 3,
|
|
213
|
+
paddingBottom: () => 3,
|
|
214
|
+
paddingLeft: () => 5,
|
|
215
|
+
paddingRight: () => 5,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const content: unknown[] = []
|
|
220
|
+
if (o.logo) content.push({ image: o.logo, width: o.logoWidth ?? 90, margin: [0, 0, 0, 8] })
|
|
221
|
+
if (o.title) content.push({ text: o.title, fontSize: fontSize + 8, bold: true, margin: [0, 0, 0, 2] })
|
|
222
|
+
if (o.subtitle) content.push({ text: o.subtitle, fontSize: fontSize + 1, color: '#64748b', margin: [0, 0, 0, 8] })
|
|
223
|
+
content.push(table)
|
|
224
|
+
|
|
225
|
+
const def: PdfDocDefinition = {
|
|
226
|
+
pageSize: o.pageSize ?? 'A4',
|
|
227
|
+
pageOrientation: orientation,
|
|
228
|
+
pageMargins: o.margins ?? [28, 34, 28, 34],
|
|
229
|
+
defaultStyle: { fontSize },
|
|
230
|
+
content,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (o.showPageNumbers ?? true) {
|
|
234
|
+
const dateStr = now.toISOString().slice(0, 10)
|
|
235
|
+
def.footer = (currentPage: number, pageCount: number) => ({
|
|
236
|
+
margin: [28, 6, 28, 0],
|
|
237
|
+
columns: [
|
|
238
|
+
{ text: dateStr, alignment: 'left', fontSize: 7, color: '#94a3b8' },
|
|
239
|
+
{ text: `Page ${currentPage} of ${pageCount}`, alignment: 'right', fontSize: 7, color: '#94a3b8' },
|
|
240
|
+
],
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return def
|
|
245
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildPrintDocument } from './export-print'
|
|
3
|
+
|
|
4
|
+
const columns = [
|
|
5
|
+
{ header: 'Company', align: 'left' as const },
|
|
6
|
+
{ header: 'Price', align: 'right' as const },
|
|
7
|
+
]
|
|
8
|
+
const rows = [
|
|
9
|
+
['ACME', '$19.95'],
|
|
10
|
+
['Globex', '$1,000.00'],
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
describe('buildPrintDocument', () => {
|
|
14
|
+
it('repeats the header, aligns columns, sets page size, and zebra-stripes', () => {
|
|
15
|
+
const html = buildPrintDocument({
|
|
16
|
+
columns,
|
|
17
|
+
rows,
|
|
18
|
+
opts: { title: 'Orders', orientation: 'landscape', pageSize: 'A4' },
|
|
19
|
+
})
|
|
20
|
+
expect(html).toContain('display: table-header-group') // repeated header
|
|
21
|
+
expect(html).toContain('@page { size: A4 landscape;')
|
|
22
|
+
expect(html).toContain('<th style="text-align:right">Price</th>')
|
|
23
|
+
expect(html).toContain('<td style="text-align:right">$19.95</td>')
|
|
24
|
+
expect(html).toContain('<h1>Orders</h1>')
|
|
25
|
+
expect(html).toContain('nth-child(even)')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('escapes HTML and supports subtitle / logo / no-zebra', () => {
|
|
29
|
+
const html = buildPrintDocument({
|
|
30
|
+
columns,
|
|
31
|
+
rows: [['<b>x</b>', '1']],
|
|
32
|
+
opts: { zebra: false, subtitle: 'Q3', logo: 'data:image/png;base64,AA' },
|
|
33
|
+
})
|
|
34
|
+
expect(html).toContain('<b>x</b>')
|
|
35
|
+
expect(html).not.toContain('nth-child(even)')
|
|
36
|
+
expect(html).toContain('<p class="subtitle">Q3</p>')
|
|
37
|
+
expect(html).toContain('src="data:image/png;base64,AA"')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('defaults to portrait with a 14mm margin', () => {
|
|
41
|
+
const html = buildPrintDocument({ columns, rows })
|
|
42
|
+
expect(html).toContain('@page { size: portrait; margin: 14mm;')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('renders hyperlinks from a cellLink hook', () => {
|
|
46
|
+
const html = buildPrintDocument({
|
|
47
|
+
columns,
|
|
48
|
+
rows,
|
|
49
|
+
cellLink: (r, c) => (r === 0 && c === 0 ? 'https://x.test' : undefined),
|
|
50
|
+
})
|
|
51
|
+
expect(html).toContain('<a href="https://x.test">ACME</a>')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('applies conditional-format cell styles + icon prefix', () => {
|
|
55
|
+
const html = buildPrintDocument({
|
|
56
|
+
columns,
|
|
57
|
+
rows,
|
|
58
|
+
cellStyle: (r, c) =>
|
|
59
|
+
r === 0 && c === 1 ? { fill: '#dcfce7', color: '#166534', bold: true, icon: '↑' } : undefined,
|
|
60
|
+
})
|
|
61
|
+
expect(html).toContain('background:#dcfce7')
|
|
62
|
+
expect(html).toContain('color:#166534')
|
|
63
|
+
expect(html).toContain('font-weight:700')
|
|
64
|
+
expect(html).toContain('↑ $19.95')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure HTML builder for the print view. `printGrid` opens this document in a
|
|
3
|
+
* new window and calls `window.print()`, so the user can print or "Save as
|
|
4
|
+
* PDF" using the browser's own engine (great font + CSS fidelity, CJK / RTL,
|
|
5
|
+
* zero dependency). Kept pure (no DOM) so it's unit-testable.
|
|
6
|
+
*
|
|
7
|
+
* The upgrades over a plain table dump: page size / orientation / margins via
|
|
8
|
+
* `@page`, a header row that repeats on every printed page
|
|
9
|
+
* (`display: table-header-group`), per-column alignment, zebra striping, and an
|
|
10
|
+
* optional title / subtitle / logo band. Browsers add their own page-number
|
|
11
|
+
* header/footer through the print dialog.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type PrintDocOptions = {
|
|
15
|
+
title?: string
|
|
16
|
+
subtitle?: string
|
|
17
|
+
/** Logo data URL, shown above the title. */
|
|
18
|
+
logo?: string
|
|
19
|
+
/** Logo width in px. Default 120. */
|
|
20
|
+
logoWidth?: number
|
|
21
|
+
orientation?: 'portrait' | 'landscape'
|
|
22
|
+
/** `@page size` keyword, e.g. 'A4' | 'Letter'. Combined with orientation. */
|
|
23
|
+
pageSize?: string
|
|
24
|
+
/** `@page margin`, e.g. '14mm'. Default '14mm'. */
|
|
25
|
+
margin?: string
|
|
26
|
+
/** Zebra-stripe rows. Default true. */
|
|
27
|
+
zebra?: boolean
|
|
28
|
+
zebraColor?: string
|
|
29
|
+
/** Header row background. Default '#f1f5f9'. */
|
|
30
|
+
headerColor?: string
|
|
31
|
+
headerTextColor?: string
|
|
32
|
+
/** Body font size in pt. Default 9. */
|
|
33
|
+
fontSize?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const HTML_ESCAPE: Record<string, string> = {
|
|
37
|
+
'&': '&',
|
|
38
|
+
'<': '<',
|
|
39
|
+
'>': '>',
|
|
40
|
+
'"': '"',
|
|
41
|
+
"'": ''',
|
|
42
|
+
}
|
|
43
|
+
function esc(value: unknown): string {
|
|
44
|
+
const s = value == null ? '' : String(value)
|
|
45
|
+
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPE[c]!)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a standalone, print-ready HTML document from resolved columns +
|
|
50
|
+
* pre-formatted string rows.
|
|
51
|
+
*/
|
|
52
|
+
export type PrintCellVisual = { fill?: string; color?: string; bold?: boolean; icon?: string }
|
|
53
|
+
|
|
54
|
+
export function buildPrintDocument(params: {
|
|
55
|
+
columns: ReadonlyArray<{ header: string; align?: 'left' | 'center' | 'right' }>
|
|
56
|
+
rows: ReadonlyArray<ReadonlyArray<string>>
|
|
57
|
+
/** Per-data-cell conditional-format visual. */
|
|
58
|
+
cellStyle?: (rowIdx: number, colIdx: number) => PrintCellVisual | undefined
|
|
59
|
+
/** Per-data-cell hyperlink URL. */
|
|
60
|
+
cellLink?: (rowIdx: number, colIdx: number) => string | undefined
|
|
61
|
+
opts?: PrintDocOptions
|
|
62
|
+
}): string {
|
|
63
|
+
const { columns, rows } = params
|
|
64
|
+
const cellStyle = params.cellStyle
|
|
65
|
+
const cellLink = params.cellLink
|
|
66
|
+
const o = params.opts ?? {}
|
|
67
|
+
const orientation = o.orientation ?? 'portrait'
|
|
68
|
+
const size = o.pageSize ? `${o.pageSize} ${orientation}` : orientation
|
|
69
|
+
const margin = o.margin ?? '14mm'
|
|
70
|
+
const zebra = o.zebra ?? true
|
|
71
|
+
const zebraColor = o.zebraColor ?? '#f6f8fa'
|
|
72
|
+
const headerColor = o.headerColor ?? '#f1f5f9'
|
|
73
|
+
const headerTextColor = o.headerTextColor ?? '#0f172a'
|
|
74
|
+
const fontSize = o.fontSize ?? 9
|
|
75
|
+
|
|
76
|
+
const alignStyle = (a?: 'left' | 'center' | 'right') =>
|
|
77
|
+
a && a !== 'left' ? ` style="text-align:${a}"` : ''
|
|
78
|
+
|
|
79
|
+
const thead =
|
|
80
|
+
'<thead><tr>' +
|
|
81
|
+
columns.map((c) => `<th${alignStyle(c.align)}>${esc(c.header)}</th>`).join('') +
|
|
82
|
+
'</tr></thead>'
|
|
83
|
+
|
|
84
|
+
const cellHtml = (c: { align?: 'left' | 'center' | 'right' }, value: string, ri: number, ci: number) => {
|
|
85
|
+
const styles: string[] = []
|
|
86
|
+
if (c.align && c.align !== 'left') styles.push(`text-align:${c.align}`)
|
|
87
|
+
const cf = cellStyle?.(ri, ci)
|
|
88
|
+
if (cf?.fill) styles.push(`background:${cf.fill}`)
|
|
89
|
+
if (cf?.color) styles.push(`color:${cf.color}`)
|
|
90
|
+
if (cf?.bold) styles.push('font-weight:700')
|
|
91
|
+
const style = styles.length ? ` style="${styles.join(';')}"` : ''
|
|
92
|
+
let text = cf?.icon ? `${esc(cf.icon)} ${esc(value)}` : esc(value)
|
|
93
|
+
const href = cellLink?.(ri, ci)
|
|
94
|
+
if (href) text = `<a href="${esc(href)}">${text}</a>`
|
|
95
|
+
return `<td${style}>${text}</td>`
|
|
96
|
+
}
|
|
97
|
+
const tbody =
|
|
98
|
+
'<tbody>' +
|
|
99
|
+
rows
|
|
100
|
+
.map(
|
|
101
|
+
(r, ri) =>
|
|
102
|
+
'<tr>' + columns.map((c, ci) => cellHtml(c, r[ci] ?? '', ri, ci)).join('') + '</tr>',
|
|
103
|
+
)
|
|
104
|
+
.join('') +
|
|
105
|
+
'</tbody>'
|
|
106
|
+
|
|
107
|
+
const band: string[] = []
|
|
108
|
+
if (o.logo) band.push(`<img class="logo" src="${esc(o.logo)}" alt="" />`)
|
|
109
|
+
if (o.title) band.push(`<h1>${esc(o.title)}</h1>`)
|
|
110
|
+
if (o.subtitle) band.push(`<p class="subtitle">${esc(o.subtitle)}</p>`)
|
|
111
|
+
|
|
112
|
+
return `<!DOCTYPE html>
|
|
113
|
+
<html><head><meta charset="utf-8"><title>${esc(o.title ?? 'Grid')}</title>
|
|
114
|
+
<style>
|
|
115
|
+
@page { size: ${size}; margin: ${margin}; }
|
|
116
|
+
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
|
117
|
+
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; color: #111; margin: 0; }
|
|
118
|
+
.logo { max-height: ${o.logoWidth ? '' : '48px'}; width: ${o.logoWidth ? `${o.logoWidth}px` : 'auto'}; display: block; margin-bottom: 8px; }
|
|
119
|
+
h1 { font-size: 16pt; margin: 0 0 2px 0; }
|
|
120
|
+
.subtitle { font-size: 10pt; color: #64748b; margin: 0 0 10px 0; }
|
|
121
|
+
table { border-collapse: collapse; width: 100%; font-size: ${fontSize}pt; }
|
|
122
|
+
thead { display: table-header-group; }
|
|
123
|
+
tr { break-inside: avoid; }
|
|
124
|
+
th, td { border: 1px solid #cbd5e1; padding: 4px 7px; text-align: left; }
|
|
125
|
+
th { background: ${headerColor}; color: ${headerTextColor}; font-weight: 700; }
|
|
126
|
+
${zebra ? `tbody tr:nth-child(even) { background: ${zebraColor}; }` : ''}
|
|
127
|
+
</style></head>
|
|
128
|
+
<body>
|
|
129
|
+
${band.join('\n ')}
|
|
130
|
+
<table>${thead}${tbody}</table>
|
|
131
|
+
</body></html>`
|
|
132
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { serializeDelimited, serializeHtml } from './export-serialize'
|
|
3
|
+
|
|
4
|
+
const header = { name: 'Name', note: 'Note', amount: 'Amount' }
|
|
5
|
+
const fields = ['name', 'note', 'amount']
|
|
6
|
+
|
|
7
|
+
describe('serializeDelimited (CSV)', () => {
|
|
8
|
+
it('writes a BOM + CRLF rows by default', async () => {
|
|
9
|
+
const out = await serializeDelimited([header, { name: 'A', note: 'x', amount: '1' }], fields)
|
|
10
|
+
expect(out.charCodeAt(0)).toBe(0xfeff) // BOM
|
|
11
|
+
expect(out).toContain('Name,Note,Amount\r\nA,x,1')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('RFC-4180 quotes values with commas, quotes, and newlines', async () => {
|
|
15
|
+
const rows = [
|
|
16
|
+
header,
|
|
17
|
+
{ name: 'Doe, John', note: 'say "hi"', amount: 'line1\nline2' },
|
|
18
|
+
]
|
|
19
|
+
const out = await serializeDelimited(rows, fields, { csv: { bom: false } })
|
|
20
|
+
expect(out).toContain('"Doe, John"')
|
|
21
|
+
expect(out).toContain('"say ""hi"""')
|
|
22
|
+
expect(out).toContain('"line1\nline2"')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('honors a custom delimiter and no-BOM (TSV-style)', async () => {
|
|
26
|
+
const out = await serializeDelimited([header, { name: 'A', note: 'b', amount: 'c' }], fields, {
|
|
27
|
+
csv: { delimiter: '\t', bom: false },
|
|
28
|
+
})
|
|
29
|
+
expect(out.startsWith('Name\tNote\tAmount')).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('reports progress and can be aborted', async () => {
|
|
33
|
+
const many = [header, ...Array.from({ length: 20 }, (_, i) => ({ name: `n${i}`, note: '', amount: `${i}` }))]
|
|
34
|
+
const onProgress = vi.fn()
|
|
35
|
+
await serializeDelimited(many, fields, { onProgress, chunkRows: 5 })
|
|
36
|
+
expect(onProgress).toHaveBeenCalled()
|
|
37
|
+
expect(onProgress.mock.calls.at(-1)![0].ratio).toBe(1)
|
|
38
|
+
|
|
39
|
+
const ctrl = new AbortController()
|
|
40
|
+
ctrl.abort()
|
|
41
|
+
await expect(serializeDelimited(many, fields, { signal: ctrl.signal })).rejects.toMatchObject({
|
|
42
|
+
name: 'AbortError',
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('serializeHtml', () => {
|
|
48
|
+
it('escapes HTML and applies column alignment', async () => {
|
|
49
|
+
const rows = [header, { name: '<b>A</b>', note: 'x & y', amount: '10' }]
|
|
50
|
+
const out = await serializeHtml(rows, fields, { align: { amount: 'right' } })
|
|
51
|
+
expect(out).toContain('<b>A</b>')
|
|
52
|
+
expect(out).toContain('x & y')
|
|
53
|
+
expect(out).toContain('<td style="text-align:right">10</td>')
|
|
54
|
+
expect(out).toContain('<th>Name</th>')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text serializers + delivery helpers moved into the FREE grid package
|
|
3
|
+
* (CSV / TSV / JSON export + clipboard now ship in @svgrid/grid). Enterprise
|
|
4
|
+
* re-uses that one implementation for its richer paths (styled HTML, xls, the
|
|
5
|
+
* xlsx/pdf blob download) instead of maintaining a second copy.
|
|
6
|
+
*
|
|
7
|
+
* Kept as a thin re-export from the Svelte-free `@svgrid/grid/format` subpath
|
|
8
|
+
* so existing imports (and the test mocks that target this module path) keep
|
|
9
|
+
* working unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
serializeDelimited,
|
|
13
|
+
serializeJson,
|
|
14
|
+
serializeHtml,
|
|
15
|
+
serializeMarkdown,
|
|
16
|
+
serializeXml,
|
|
17
|
+
downloadBlobFile,
|
|
18
|
+
downloadTextFile,
|
|
19
|
+
copyTextToClipboard,
|
|
20
|
+
type CsvOptions,
|
|
21
|
+
type SerializeOptions,
|
|
22
|
+
type SerializeProgress,
|
|
23
|
+
type ExportCellVisual,
|
|
24
|
+
} from '@svgrid/grid/format'
|