@svgrid/enterprise 3.1.25 → 3.1.27

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 (61) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +18048 -17567
  2. package/dist/designer/assets/{GridFooter-DKMfZpL4.js → GridFooter-EyLg77jG.js} +1 -1
  3. package/dist/designer/assets/{GridMenus-DWDghydL.js → GridMenus-BAgfpDd-.js} +3 -3
  4. package/dist/designer/assets/{SvDateRangeInput-DbfmXqJj.js → SvDateRangeInput-y8PkioQk.js} +1 -1
  5. package/dist/designer/assets/SvGrid.helpers-DHkH3mHW.js +1 -0
  6. package/dist/designer/assets/SvGridCellEditor-BHxH4b5h.js +4 -0
  7. package/dist/designer/assets/{SvGridChart-DYtGHM3F.js → SvGridChart-DzCeEdlb.js} +3 -3
  8. package/dist/designer/assets/{SvGridChartBuilder-BjdQrnz5.js → SvGridChartBuilder-CsXVVKvA.js} +1 -1
  9. package/dist/designer/assets/SvGridChartPanel-Dj3IxTS2.js +4 -0
  10. package/dist/designer/assets/{SvGridChartView-Bfs0bvDb.js → SvGridChartView-Dwm4hxbW.js} +1 -1
  11. package/dist/designer/assets/{chart-export-B6bkQ3X8.js → chart-export-Dqq_eHRq.js} +4 -4
  12. package/dist/designer/assets/{chart-export-pdf-BbA28-gt.js → chart-export-pdf-Gw22jkxa.js} +1 -1
  13. package/dist/designer/assets/column-types-Bl5R-97G.js +1 -0
  14. package/dist/designer/assets/csv-injection-C4a0uulO.js +1 -0
  15. package/dist/designer/assets/export-format-NYxEY-0Y.js +6 -0
  16. package/dist/designer/assets/index-BU3ieI05.css +1 -0
  17. package/dist/designer/assets/{index-UhFdGGXv.js → index-Cim1_diz.js} +340 -330
  18. package/dist/designer/assets/src-D23PPsK1.js +1 -0
  19. package/dist/designer/assets/src-sFRH_up7.js +60 -0
  20. package/dist/designer/index.html +13 -11
  21. package/dist/export-conditional.d.ts +2 -0
  22. package/dist/export-conditional.js +2 -0
  23. package/dist/export-pdf.d.ts +142 -7
  24. package/dist/export-pdf.js +368 -44
  25. package/dist/export.d.ts +22 -4
  26. package/dist/export.js +150 -53
  27. package/dist/import.js +14 -3
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/node/studio.js +20 -12
  31. package/dist/pdf-worker.d.ts +92 -0
  32. package/dist/pdf-worker.js +147 -0
  33. package/dist/sheet/ods-document.js +3 -1
  34. package/dist/sheet/open-file.js +4 -2
  35. package/dist/sheet/xlsx-document.d.ts +2 -2
  36. package/dist/sheet/xlsx-document.js +18 -2
  37. package/dist/studio/emit-schema.js +18 -10
  38. package/dist/version.d.ts +2 -2
  39. package/dist/version.js +2 -2
  40. package/dist/wc/{GridMenus-h7RsvcmI.js → GridMenus-BIQXB6a7.js} +1 -1
  41. package/dist/wc/{SvDateRangeInput-CJZohgBe.js → SvDateRangeInput-DHYuAJeY.js} +1 -0
  42. package/dist/wc/SvGridCellEditor-CZwhQx0a.js +584 -0
  43. package/dist/wc/{SvGridChart-DWRqlhCV.js → SvGridChart-DcfmVvzB.js} +3 -3
  44. package/dist/wc/{SvGridChartBuilder-BXW3yUR0.js → SvGridChartBuilder-1Z69mxbM.js} +2 -2
  45. package/dist/wc/{SvGridChartPanel-28n4tomx.js → SvGridChartPanel-BaVsa-T-.js} +5 -5
  46. package/dist/wc/{SvGridChartView-CbG_WrU-.js → SvGridChartView-LEC8ZNeU.js} +1 -1
  47. package/dist/wc/{chart-export-CeSADGfi.js → chart-export-C1KSVdMU.js} +29 -28
  48. package/dist/wc/{chart-export-pdf-BGko7MER.js → chart-export-pdf-CmEd365D.js} +1 -1
  49. package/dist/wc/csv-injection-Ce26_TOb.js +8 -0
  50. package/dist/wc/sv-sheet-element.js +6275 -6215
  51. package/dist/xlsx-limits.d.ts +51 -0
  52. package/dist/xlsx-limits.js +89 -0
  53. package/package.json +7 -3
  54. package/dist/designer/assets/SvGrid.helpers-BF2QRcNt.js +0 -1
  55. package/dist/designer/assets/SvGridCellEditor-od4VbndV.js +0 -4
  56. package/dist/designer/assets/SvGridChartPanel-Cm9_K7BV.js +0 -4
  57. package/dist/designer/assets/export-format-BCuqcexA.js +0 -6
  58. package/dist/designer/assets/index-CHx3F4Ij.css +0 -1
  59. package/dist/designer/assets/src-CITpcm6b.js +0 -1
  60. package/dist/designer/assets/src-fddEt37f.js +0 -58
  61. package/dist/wc/SvGridCellEditor-C7ios4i_.js +0 -582
package/dist/export.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyGroupAggregate } from '@svgrid/grid/core';
1
2
  // Import the pure formatters from the grid's Svelte-free `/format` subpath so
2
3
  // this module (and the enterprise unit tests) don't drag in SvGrid.svelte.
3
4
  import { formatValueForExport, toExcelNumFmt, valueForExcel } from '@svgrid/grid/format';
@@ -6,7 +7,10 @@ import { installSmartShim } from './smart-shim.js';
6
7
  import { downloadBlobFile, downloadTextFile, serializeDelimited, serializeHtml, serializeJson, serializeMarkdown, serializeXml, } from './export-serialize.js';
7
8
  import { serializeSpreadsheetML } from './export-xls.js';
8
9
  import { buildXlsxParts, packageXlsx } from './export-ooxml.js';
9
- import { buildPdfDocDefinition, pdfBlob, readGridPdfTheme, registerPdfFonts, resolvePdfCharts } from './export-pdf.js';
10
+ import { buildPdfDocData, buildPdfDocDefinition, pdfBlob, PDF_ROBOTO_MISSING, readGridPdfTheme, registerPdfFont, registerPdfFonts, resolvePdfCharts } from './export-pdf.js';
11
+ import { renderPdfInWorker } from './pdf-worker.js';
12
+ /** Rows past which a PDF rendered on the page earns a warning: about a second of blocked page per thousand. */
13
+ const LARGE_PDF_ROWS = 2000;
10
14
  import { buildConditionalResolver } from './export-conditional.js';
11
15
  /** Formats served by the native, dependency-free, streaming serializers. */
12
16
  const NATIVE_TEXT_FORMATS = [
@@ -102,6 +106,7 @@ export function resolveColumns(api, opts, sourceRows) {
102
106
  header: c.header,
103
107
  format: c.format,
104
108
  align: c.align,
109
+ aggregate: c.aggregate,
105
110
  }));
106
111
  }
107
112
  if (sourceRows.length > 0) {
@@ -550,27 +555,58 @@ function isSummableColumn(col) {
550
555
  }
551
556
  /**
552
557
  * Build a grouped PDF body: a bold group header per cluster (nested for
553
- * multi-level grouping), the cluster's data rows, then a subtotal row summing
554
- * the numeric / currency columns. Values use the same formatting as the grid.
558
+ * multi-level grouping), the cluster's data rows, a subtotal row, and a grand
559
+ * total at the end. A column totals with its own `aggregate` (avg, min, max,
560
+ * count, ...), as the grid's group rows do; without one, number and currency
561
+ * columns sum. Values and group names use the same formatting as the grid.
562
+ * Each data row carries its index in `rows`, so conditional formatting and
563
+ * links can find their row.
555
564
  */
556
- export function buildGroupedPdfBody(cols, rows, groupBy) {
565
+ export function buildGroupedPdfBody(cols, rows, groupBy, opts = {}) {
557
566
  const out = [];
558
- const headerOf = (field) => cols.find((c) => c.field === field)?.header ?? field;
559
- // Put the "Subtotal" label in the first NON-numeric column; if every column
560
- // is numeric, -1 means "don't overwrite a sum with the label".
561
- const labelColIdx = cols.findIndex((c) => !isSummableColumn(c));
567
+ const colOf = (field) => cols.find((c) => c.field === field);
568
+ const aggOf = (c) => c.aggregate ?? (isSummableColumn(c) ? 'sum' : undefined);
569
+ const hasTotals = cols.some((c) => aggOf(c) !== undefined);
570
+ // The label goes in the first column that has no total of its own; if
571
+ // every column has one, -1 means "don't overwrite a total with the label".
572
+ const labelColIdx = cols.findIndex((c) => aggOf(c) === undefined);
573
+ const index = new Map();
574
+ rows.forEach((r, i) => index.set(r, i));
575
+ const field = (r, f) => r[f];
576
+ const total = (c, subset) => {
577
+ const agg = aggOf(c);
578
+ if (agg === undefined)
579
+ return '';
580
+ // The grid's own aggregator, over the same values the column shows.
581
+ const v = applyGroupAggregate(agg, c.field, subset.map((r) => ({ original: r, getCellValueByColumnId: () => field(r, c.field) })));
582
+ if (v == null || (typeof v === 'number' && !Number.isFinite(v)))
583
+ return '';
584
+ // A count is a number of rows, not an amount: no currency or percent.
585
+ if (agg === 'count' || agg === 'countDistinct')
586
+ return String(v);
587
+ if (Array.isArray(v))
588
+ return v.map((x) => formatValueForExport(x, c.format)).join(' - ');
589
+ return formatValueForExport(v, c.format);
590
+ };
591
+ const totalRow = (subset, label) => {
592
+ const cells = cols.map((c) => total(c, subset));
593
+ if (labelColIdx >= 0)
594
+ cells[labelColIdx] = label;
595
+ return { kind: 'subtotal', cells };
596
+ };
562
597
  const recurse = (subset, level) => {
563
598
  if (level >= groupBy.length) {
564
599
  for (const r of subset) {
565
- out.push({ kind: 'data', cells: cols.map((c) => String(cellValueFor(c, r, false) ?? '')) });
600
+ out.push({ kind: 'data', cells: cols.map((c) => String(cellValueFor(c, r, false) ?? '')), source: index.get(r) });
566
601
  }
567
602
  return;
568
603
  }
569
- const field = groupBy[level];
604
+ const groupField = groupBy[level];
605
+ const groupCol = colOf(groupField);
570
606
  // Preserve first-seen order of group keys.
571
607
  const buckets = new Map();
572
608
  for (const r of subset) {
573
- const key = String(r[field] ?? '');
609
+ const key = String(field(r, groupField) ?? '');
574
610
  const bucket = buckets.get(key);
575
611
  if (bucket)
576
612
  bucket.push(r);
@@ -578,26 +614,31 @@ export function buildGroupedPdfBody(cols, rows, groupBy) {
578
614
  buckets.set(key, [r]);
579
615
  }
580
616
  for (const [key, groupRows] of buckets) {
581
- out.push({ kind: 'group', label: `${headerOf(field)}: ${key} (${groupRows.length})`, level });
617
+ // The name as the grid shows it: a date group reads "Oct 3, 2026", not
618
+ // the timestamp it is keyed by.
619
+ const shown = groupCol ? String(cellValueFor(groupCol, groupRows[0], false) ?? key) : key;
620
+ out.push({ kind: 'group', label: `${groupCol?.header ?? groupField}: ${shown} (${groupRows.length})`, level });
582
621
  recurse(groupRows, level + 1);
583
- // Subtotal row for the numeric columns of this cluster.
584
- const hasSummable = cols.some(isSummableColumn);
585
- if (hasSummable) {
586
- const cells = cols.map((c) => {
587
- if (!isSummableColumn(c))
588
- return '';
589
- const sum = groupRows.reduce((a, r) => a + (Number(r[c.field]) || 0), 0);
590
- return formatValueForExport(sum, c.format);
591
- });
592
- if (labelColIdx >= 0)
593
- cells[labelColIdx] = `Subtotal (${key})`;
594
- out.push({ kind: 'subtotal', cells });
595
- }
622
+ if (hasTotals)
623
+ out.push(totalRow(groupRows, opts.labels?.subtotal?.(shown) ?? `Subtotal (${shown})`));
596
624
  }
597
625
  };
598
626
  recurse(rows, 0);
627
+ if (hasTotals && opts.grandTotal !== false && rows.length)
628
+ out.push(totalRow(rows, opts.labels?.total ?? 'Total'));
599
629
  return out;
600
630
  }
631
+ /**
632
+ * Say so when the PDF holds text Roboto cannot draw. Without a `pdf.font` it
633
+ * prints as empty boxes, and nothing else would tell the developer why.
634
+ */
635
+ function warnMissingGlyphs(texts) {
636
+ const hit = texts.find((t) => PDF_ROBOTO_MISSING.test(t));
637
+ if (hit === undefined)
638
+ return;
639
+ console.warn(`@svgrid/enterprise: the PDF has text its default font (Roboto) cannot draw, such as "${hit.slice(0, 40)}"; ` +
640
+ 'it prints as empty boxes. Pass pdf.font with a font that covers it (Noto Sans CJK, Noto Sans Arabic, ...).');
641
+ }
601
642
  /**
602
643
  * PDF export via our own pdfmake docDefinition (page setup, repeated header,
603
644
  * page numbers, per-column alignment/widths, title/logo, theme colors, and
@@ -605,18 +646,17 @@ export function buildGroupedPdfBody(cols, rows, groupBy) {
605
646
  * formatted display strings, so the PDF matches the grid.
606
647
  */
607
648
  async function exportPdf(api, opts) {
608
- await ensureGlobals('pdf');
609
- const pdfMake = globalThis.pdfMake;
610
- if (!pdfMake) {
611
- throw new Error('@svgrid/enterprise: pdf export requires the "pdfmake" peer dependency.');
612
- }
613
649
  const sourceRows = resolveRowSource(api, opts.rows);
614
650
  if (sourceRows.length === 0) {
615
651
  throw new Error('@svgrid/enterprise: nothing to export - the grid has no rows');
616
652
  }
617
653
  const cols = resolveColumns(api, opts, sourceRows);
618
654
  const fields = cols.map((c) => c.field);
619
- const columns = cols.map((c) => ({ header: c.header ?? c.field, align: c.align }));
655
+ // The grid's columns arrive with the alignment the screen uses; columns an
656
+ // app passes itself may not say, so a number or date format right-aligns,
657
+ // as it does on screen.
658
+ const alignOf = (c) => c.align ?? (c.format && ['number', 'currency', 'percent', 'date', 'datetime', 'time'].includes(String(c.format.type)) ? 'right' : undefined);
659
+ const columns = cols.map((c) => ({ header: c.header ?? c.field, align: alignOf(c) }));
620
660
  // Carry the grid's active grouping into group + subtotal rows (unless the
621
661
  // caller opted out or passed hierarchical), matching the xlsx outline path.
622
662
  const autoGrouping = opts.autoGroup !== false && !opts.groupBy?.length && !opts.hierarchical
@@ -648,33 +688,67 @@ async function exportPdf(api, opts) {
648
688
  return chartToPngBlob(el, { scale: 2 });
649
689
  });
650
690
  const now = new Date();
651
- let def;
691
+ let params;
692
+ // A boolean prints as the grid's checkbox does: a tick for true and nothing
693
+ // for false, rather than the words. Only plain fields: a column with a
694
+ // format or an `exportValue` prints what it asks for.
695
+ const boolAt = (rowIdx, colIdx) => {
696
+ const col = cols[colIdx];
697
+ if (!col || col.exportValue || col.format)
698
+ return undefined;
699
+ const v = sourceRows[rowIdx]?.[col.field];
700
+ return typeof v === 'boolean' ? v : undefined;
701
+ };
702
+ const withTick = (v, b) => {
703
+ const icon = v?.icon ?? (b ? '✓' : undefined);
704
+ return v || icon ? { fill: v?.fill, color: v?.color, bold: v?.bold, icon, iconOnly: v?.iconOnly } : undefined;
705
+ };
652
706
  if (effGroupBy?.length) {
653
- // Conditional formatting is skipped for grouped PDFs (group / subtotal
654
- // rows own the layout); the grid colors still show on screen.
655
- const body = buildGroupedPdfBody(cols, sourceRows, effGroupBy);
656
- def = buildPdfDocDefinition({ columns, body, opts: pdfOpts, styles: opts.styles, theme, ...lines, charts, now });
707
+ const grouped = buildGroupedPdfBody(cols, sourceRows, effGroupBy, { grandTotal: pdfOpts.grandTotal, labels: pdfOpts.labels });
708
+ const body = grouped.map((row) => row.kind === 'data' && row.source !== undefined
709
+ ? { ...row, cells: row.cells.map((text, c) => (boolAt(row.source, c) === undefined ? text : '')) }
710
+ : row);
711
+ if (!pdfOpts.font)
712
+ warnMissingGlyphs(body.flatMap((r) => (r.kind === 'group' ? [r.label] : r.cells)));
713
+ // Data rows come out in group order; conditional formatting and links are
714
+ // looked up by each row's place in the exported rows.
715
+ const sources = body.flatMap((r) => (r.kind === 'data' ? [r.source ?? -1] : []));
716
+ const visual = buildConditionalResolver(cols, sourceRows, opts.conditionalFormats);
717
+ const link = buildLinkResolver(cols, sourceRows);
718
+ params = {
719
+ columns,
720
+ body,
721
+ dataCellStyle: (r, c) => {
722
+ const at = sources[r] ?? -1;
723
+ return at >= 0 ? withTick(visual(at, c), boolAt(at, c)) : undefined;
724
+ },
725
+ dataCellLink: link
726
+ ? (r, c) => {
727
+ const at = sources[r] ?? -1;
728
+ return at >= 0 ? link(at, c) : undefined;
729
+ }
730
+ : undefined,
731
+ opts: pdfOpts,
732
+ styles: opts.styles,
733
+ theme,
734
+ ...lines,
735
+ charts,
736
+ now,
737
+ };
657
738
  }
658
739
  else {
659
740
  const projected = await projectRows(sourceRows, cols, opts);
660
- const dataRows = projected.slice(1).map((r) => fields.map((f) => String(r[f] ?? '')));
661
- // Conditional formatting -> per-cell fill / color / bold + icon prefix.
741
+ const dataRows = projected.slice(1).map((r, ri) => fields.map((f, c) => (boolAt(ri, c) === undefined ? String(r[f] ?? '') : '')));
742
+ // Conditional formatting -> per-cell fill / color / bold, and the icon,
743
+ // which the builder draws (the icon sets' glyphs are not in the font).
662
744
  const visual = buildConditionalResolver(cols, sourceRows, opts.conditionalFormats);
663
- for (let r = 0; r < dataRows.length; r++) {
664
- for (let c = 0; c < cols.length; c++) {
665
- const icon = visual(r, c)?.icon;
666
- if (icon)
667
- dataRows[r][c] = `${icon} ${dataRows[r][c] ?? ''}`;
668
- }
669
- }
670
745
  const link = buildLinkResolver(cols, sourceRows);
671
- def = buildPdfDocDefinition({
746
+ if (!pdfOpts.font)
747
+ warnMissingGlyphs([...columns.map((c) => c.header), ...dataRows.flat()]);
748
+ params = {
672
749
  columns,
673
750
  rows: dataRows,
674
- dataCellStyle: (r, c) => {
675
- const v = visual(r, c);
676
- return v ? { fill: v.fill, color: v.color, bold: v.bold } : undefined;
677
- },
751
+ dataCellStyle: (r, c) => withTick(visual(r, c), boolAt(r, c)),
678
752
  dataCellLink: link,
679
753
  opts: pdfOpts,
680
754
  styles: opts.styles,
@@ -683,11 +757,34 @@ async function exportPdf(api, opts) {
683
757
  merges: opts.merges,
684
758
  charts,
685
759
  now,
686
- });
760
+ };
687
761
  }
688
762
  const filename = (opts.filename ?? 'grid').trim() || 'grid';
689
- const blob = await pdfBlob(pdfMake.createPdf(def));
690
- opts.onProgress?.({ phase: 'write', ratio: 1, total: sourceRows.length });
763
+ const total = sourceRows.length;
764
+ let blob;
765
+ if (pdfOpts.worker) {
766
+ // The document crosses to the worker as data; pdfmake is loaded there.
767
+ blob = await renderPdfInWorker(pdfOpts.worker, buildPdfDocData(params), {
768
+ font: pdfOpts.font,
769
+ signal: opts.signal,
770
+ onProgress: (ratio) => opts.onProgress?.({ phase: 'write', ratio, total }),
771
+ });
772
+ }
773
+ else {
774
+ if (total > LARGE_PDF_ROWS) {
775
+ console.warn(`@svgrid/enterprise: rendering a ${total.toLocaleString('en-US')}-row PDF on the page; pdfmake blocks it until done ` +
776
+ '(about a second per thousand rows). Pass pdf.worker to render in a worker instead.');
777
+ }
778
+ await ensureGlobals('pdf');
779
+ const pdfMake = globalThis.pdfMake;
780
+ if (!pdfMake) {
781
+ throw new Error('@svgrid/enterprise: pdf export requires the "pdfmake" peer dependency.');
782
+ }
783
+ if (pdfOpts.font)
784
+ registerPdfFont(pdfMake, pdfOpts.font);
785
+ blob = await pdfBlob(pdfMake.createPdf(buildPdfDocDefinition(params)));
786
+ }
787
+ opts.onProgress?.({ phase: 'write', ratio: 1, total });
691
788
  return deliver(blob, ensureExt(filename, 'pdf'), 'application/pdf', sourceRows.length, opts.download);
692
789
  }
693
790
  /**
package/dist/import.js CHANGED
@@ -24,6 +24,7 @@
24
24
  * `print`, so unlicensed evaluation works but emits a one-time nudge.
25
25
  */
26
26
  import { assertEnterpriseLicensed } from './license.js';
27
+ import { createUnzipBudget, readZipEntry, XLSX_MAX_COLS, XLSX_MAX_FILL, xlsxFillError, xlsxRangeError } from './xlsx-limits.js';
27
28
  // ---------------------------------------------------------------------------
28
29
  // Lazy peer dependency: jszip (xlsx only)
29
30
  // ---------------------------------------------------------------------------
@@ -182,11 +183,12 @@ async function parseXlsx(file) {
182
183
  const J = await getJSZip();
183
184
  const buf = await file.arrayBuffer();
184
185
  const zip = await J.loadAsync(buf);
186
+ const budget = createUnzipBudget();
185
187
  // Read sharedStrings.xml if it exists.
186
188
  let sharedStrings = [];
187
189
  const ssEntry = zip.file('xl/sharedStrings.xml');
188
190
  if (ssEntry) {
189
- const ssText = await ssEntry.async('string');
191
+ const ssText = await readZipEntry(ssEntry, 'string', budget);
190
192
  sharedStrings = parseSharedStrings(ssText);
191
193
  }
192
194
  // Read the FIRST sheet. Workbook.xml -> rels mapping is involved; for the
@@ -196,7 +198,7 @@ async function parseXlsx(file) {
196
198
  if (!sheetEntry) {
197
199
  throw new Error('@svgrid/enterprise: could not locate sheet1.xml in the .xlsx archive');
198
200
  }
199
- const sheetText = await sheetEntry.async('string');
201
+ const sheetText = await readZipEntry(sheetEntry, 'string', budget);
200
202
  return parseSheetXml(sheetText, sharedStrings);
201
203
  }
202
204
  /** Pull every `<t>...</t>` out of sharedStrings.xml in document order. */
@@ -242,6 +244,7 @@ function parseSheetXml(xml, sharedStrings) {
242
244
  const tRe = /<t\b[^>]*>([\s\S]*?)<\/t>/g;
243
245
  const vRe = /<v>([\s\S]*?)<\/v>/;
244
246
  let rowMatch;
247
+ let filled = 0;
245
248
  while ((rowMatch = rowRe.exec(xml)) !== null) {
246
249
  const rowBody = rowMatch[1] ?? '';
247
250
  const cells = [];
@@ -253,7 +256,15 @@ function parseSheetXml(xml, sharedStrings) {
253
256
  const ref = /r="([A-Z]+)\d+"/.exec(attrs)?.[1] ?? '';
254
257
  const colIndex = colRefToIndex(ref);
255
258
  const type = /t="([^"]+)"/.exec(attrs)?.[1] ?? 'n';
256
- // Pad holes with empty strings so column alignment survives.
259
+ // Pad holes with empty strings so column alignment survives, within
260
+ // the sheet's real width and the blank budget (see xlsx-limits.ts).
261
+ if (colIndex >= XLSX_MAX_COLS)
262
+ throw xlsxRangeError(/r="([^"]*)"/.exec(attrs)?.[1] ?? ref);
263
+ if (colIndex > cells.length) {
264
+ filled += colIndex - cells.length;
265
+ if (filled > XLSX_MAX_FILL)
266
+ throw xlsxFillError();
267
+ }
257
268
  while (cells.length < colIndex)
258
269
  cells.push('');
259
270
  let value = '';
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { setLicenseKey, clearLicenseKey, getLicenseKey, isLicenseKeySet, hasVali
2
2
  export { parseLicenseExpiry } from './license-core.js';
3
3
  export { exportGrid, copyExportToClipboard, type ExportFormat, type ExportOptions, type ExportColumn, type ExportCellStyle, type ExportStyles, type ExportHeaderFooterLine, type ExportSheet, type ExportRowSource, type ExportProgress, type ExportResult, type ClipboardFormat, type ClipboardExportOptions, } from './export.js';
4
4
  export { default as SvExportMenu } from './SvExportMenu.svelte';
5
- export { buildPdfDocDefinition, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, type PdfExportOptions, type PdfPageSize, type PdfChart, type PdfKpi, type PdfChartImage, type PdfVirtualFileSystem, type PdfMakeLike } from './export-pdf.js';
5
+ export { buildPdfDocDefinition, buildPdfDocData, hydratePdfDoc, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, registerPdfFont, type PdfExportOptions, type PdfDocData, type PdfDocParams, type PdfWorkerLike, type PdfPageSize, type PdfChart, type PdfKpi, type PdfChartImage, type PdfVirtualFileSystem, type PdfMakeLike, type PdfFont } from './export-pdf.js';
6
6
  export { printGrid, type PrintOptions } from './print.js';
7
7
  export { importData, readImportMatrix, mapImportMatrix, mapImportMatrixAsync, autoMapColumns, inferImportColumnTypes, type ImportFormat, type ImportFieldType, type ImportOptions, type ImportResult, type ImportProgress, type ImportColumnMap, type ImportColumnTypes, type ImportGridColumn, type ImportRowError, type ImportValidator, } from './import.js';
8
8
  export { default as SvImportDialog } from './SvImportDialog.svelte';
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ licenseCovers, getLicenseEdition, nudgeEnterpriseFor, editionCovers, } from './l
8
8
  export { parseLicenseExpiry } from './license-core.js';
9
9
  export { exportGrid, copyExportToClipboard, } from './export.js';
10
10
  export { default as SvExportMenu } from './SvExportMenu.svelte';
11
- export { buildPdfDocDefinition, resolvePdfCharts, resolvePdfVfs, registerPdfFonts } from './export-pdf.js';
11
+ export { buildPdfDocDefinition, buildPdfDocData, hydratePdfDoc, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, registerPdfFont } from './export-pdf.js';
12
12
  export { printGrid } from './print.js';
13
13
  export { importData, readImportMatrix, mapImportMatrix, mapImportMatrixAsync, autoMapColumns, inferImportColumnTypes, } from './import.js';
14
14
  export { default as SvImportDialog } from './SvImportDialog.svelte';
@@ -16124,8 +16124,8 @@ function layoutFile(nav, opts = {}) {
16124
16124
  ${toolbarOn ? `<div class="sv-app__toolbar">
16125
16125
  <div class="sv-app__search">
16126
16126
  <svg class="sv-app__search-ic" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg>
16127
- <input class="sv-app__search-in" type="search" placeholder="Search {brand}…" bind:value={q} bind:this={searchEl} aria-label="Search screens" />
16128
- <kbd class="sv-app__kbd" aria-hidden="true">⌘K</kbd>
16127
+ <input class="sv-app__search-in" type="search" placeholder="Search {brand}…" bind:value={q} aria-label="Search screens" />
16128
+ <button type="button" class="sv-app__kbd" title="Command palette (⌘K / Ctrl+K)" aria-label="Open the command palette" onclick={() => (paletteOpen = true)}>⌘K</button>
16129
16129
  {#if results.length}
16130
16130
  <div class="sv-app__results">
16131
16131
  {#each results as r (r.href)}
@@ -16165,6 +16165,7 @@ function layoutFile(nav, opts = {}) {
16165
16165
  </div>
16166
16166
  </div>
16167
16167
  {#if bellOpen || menuOpen}<button type="button" class="sv-app__scrim" tabindex="-1" aria-label="Close menus" onclick={() => { bellOpen = false; menuOpen = false }}></button>{/if}
16168
+ <SvCommand bind:open={paletteOpen} {commands} placeholder="Go to a screen or run an action…" emptyText="No matching screen" />
16168
16169
  </div>\n ` : ""}<div class="sv-app__content">
16169
16170
  ${opts.supabaseAuth ? `<SvAuthGate client={supabaseClient as unknown as SupabaseAuthClientLike} title={\`Sign in to \${brand}\`}>
16170
16171
  {@render children()}
@@ -16254,7 +16255,7 @@ function layoutFile(nav, opts = {}) {
16254
16255
  import '../app.css'
16255
16256
  import '../custom.css'
16256
16257
  import { page } from '$app/state'
16257
- import { setLicenseKey } from '@svgrid/enterprise'${opts.access ? `\n import { currentRole, canScreen } from '$lib/access'` : ""}${opts.i18n ? `\n import { t, currentLocale, locales } from '$lib/i18n'` : ""}${opts.auth ? `\n import type { LayoutData } from './$types'` : ""}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '$lib/connections'` : ""}
16258
+ import { setLicenseKey } from '@svgrid/enterprise'${toolbarOn ? `\n import { goto } from '$app/navigation'\n import { SvCommand, type CommandItem } from '@svgrid/grid'` : ""}${opts.access ? `\n import { currentRole, canScreen } from '$lib/access'` : ""}${opts.i18n ? `\n import { t, currentLocale, locales } from '$lib/i18n'` : ""}${opts.auth ? `\n import type { LayoutData } from './$types'` : ""}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '$lib/connections'` : ""}
16258
16259
 
16259
16260
  // VITE_SVPRO_KEY in .env is your SvGrid license key; it removes the unlicensed watermark.
16260
16261
  if (import.meta.env.VITE_SVPRO_KEY) setLicenseKey(import.meta.env.VITE_SVPRO_KEY)
@@ -16280,24 +16281,30 @@ function layoutFile(nav, opts = {}) {
16280
16281
  try { const s = localStorage.getItem('svapp:theme'); if (s === 'light' || s === 'dark') t = s } catch (_) { /* storage blocked */ }
16281
16282
  applyTheme(t)
16282
16283
  })` : ""}${sideState}${toolbarOn ? `
16283
- // App-chrome quick-search: filter the screens by label as you type.
16284
+ // App-chrome quick-search: filter the screens by label as you type. Only the
16285
+ // screens the sidebar would show${opts.access ? " to this role" : ""}.
16284
16286
  const initials = ${JSON.stringify(initials)}
16285
16287
  const acctEmail = ${JSON.stringify(`admin@${brandSlug}.com`)}
16288
+ const visibleNav = ${opts.access ? "$derived(nav.filter((n) => !n.id || canScreen($currentRole, n.id)))" : "nav"}
16286
16289
  let q = $state('')
16287
- const results = $derived(q.trim() ? nav.filter((n) => n.label.toLowerCase().includes(q.trim().toLowerCase())).slice(0, 6) : [])
16290
+ const results = $derived(q.trim() ? visibleNav.filter((n) => n.label.toLowerCase().includes(q.trim().toLowerCase())).slice(0, 6) : [])
16291
+ // Cmd/Ctrl+K: a palette over the same screens. SvCommand binds the key itself.
16292
+ let paletteOpen = $state(false)
16293
+ const commands: CommandItem[] = $derived([
16294
+ ...visibleNav.map((n) => ({ id: 'go:' + n.href, label: ${opts.i18n ? "n.id ? $t('nav.' + n.id, n.label) : n.label" : "n.label"}, group: 'Go to', onRun: () => goto(n.href) })),${hasSwitch ? `
16295
+ { id: 'theme', label: theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode', group: 'Actions', onRun: toggleTheme },` : ""}
16296
+ ])
16288
16297
  let bellOpen = $state(false)
16289
16298
  let menuOpen = $state(false)
16290
- let searchEl: HTMLInputElement | null = $state(null)
16291
16299
  const notifications = [
16292
16300
  { title: 'Welcome to ' + brand, time: 'just now', color: 'var(--sg-accent, #6366f1)' },
16293
16301
  { title: 'Your workspace is ready', time: '2m ago', color: '#10b981' },
16294
16302
  { title: 'Sample data loaded', time: '5m ago', color: '#f59e0b' },
16295
16303
  ]
16296
- // Cmd/Ctrl+K focuses search; Escape closes the menus.
16304
+ // Escape closes the menus.
16297
16305
  $effect(() => {
16298
16306
  function onKey(e: KeyboardEvent) {
16299
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); searchEl?.focus() }
16300
- else if (e.key === 'Escape') { bellOpen = false; menuOpen = false }
16307
+ if (e.key === 'Escape') { bellOpen = false; menuOpen = false }
16301
16308
  }
16302
16309
  window.addEventListener('keydown', onKey)
16303
16310
  return () => window.removeEventListener('keydown', onKey)
@@ -16336,7 +16343,8 @@ ${styles}
16336
16343
  .sv-app__tool:hover { color: var(--sg-fg, #0f172a); border-color: color-mix(in srgb, var(--sg-fg, #0f172a) 20%, var(--sg-border, #e6e8ec)); }
16337
16344
  .sv-app__dot { position: absolute; top: 5px; right: 5px; width: 6px; height: 6px; border-radius: 50%; background: var(--sg-accent, #6366f1); box-shadow: 0 0 0 2px var(--sg-bg, #fff); }
16338
16345
  .sv-app__avatar { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%; font: inherit; font-size: 12px; font-weight: 700; color: #fff; background: var(--sg-accent, #6366f1); letter-spacing: 0.02em; cursor: pointer; }
16339
- .sv-app__kbd { position: absolute; right: 8px; font-size: 10.5px; font-weight: 600; color: var(--sg-muted, #94a3b8); background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 5px; padding: 1px 5px; pointer-events: none; }
16346
+ .sv-app__kbd { position: absolute; right: 8px; font-size: 10.5px; font-weight: 600; color: var(--sg-muted, #94a3b8); background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 5px; padding: 1px 5px; font-family: inherit; line-height: inherit; cursor: pointer; }
16347
+ .sv-app__kbd:hover { color: var(--sg-fg, #0f172a); }
16340
16348
  .sv-app__pop { position: relative; }
16341
16349
  .sv-app__scrim { position: fixed; inset: 0; z-index: 44; border: 0; padding: 0; background: transparent; cursor: default; }
16342
16350
  .sv-app__menu { position: absolute; top: calc(100% + 8px); right: 0; z-index: 46; min-width: 232px; padding: 6px; background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 11px; box-shadow: 0 16px 40px -10px rgba(15, 23, 42, 0.3); }
@@ -16506,7 +16514,7 @@ function hasFieldConditions(schema) {
16506
16514
  *
16507
16515
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
16508
16516
  */
16509
- var SVGRID_VERSION = "3.1.25";
16517
+ var SVGRID_VERSION = "3.1.27";
16510
16518
  /**
16511
16519
  * The version of `@svgrid/grid`, which releases on its own line: enterprise is
16512
16520
  * on 3.1.x while the grid is on 3.0.x.
@@ -16519,7 +16527,7 @@ var SVGRID_VERSION = "3.1.25";
16519
16527
  *
16520
16528
  * `version.test.ts` asserts this matches packages/grid/package.json.
16521
16529
  */
16522
- var GRID_VERSION = "3.0.22";
16530
+ var GRID_VERSION = "3.0.23";
16523
16531
  //#endregion
16524
16532
  //#region src/studio/emit-project.ts
16525
16533
  var has = (blocks, kind) => blocks.some((b) => b.config.kind === kind);
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Render a PDF export in a worker, so a large report does not freeze the
3
+ * page. pdfmake lays out and writes the whole document in one synchronous
4
+ * run, about a millisecond per row of eight columns on a desktop machine:
5
+ * a 20,000-row export held the main thread for over twenty seconds.
6
+ *
7
+ * The app owns the worker file, so any bundler builds it:
8
+ *
9
+ * ```ts
10
+ * // pdf.worker.ts
11
+ * import { servePdfWorker } from '@svgrid/enterprise/pdf-worker'
12
+ * servePdfWorker()
13
+ * ```
14
+ *
15
+ * ```ts
16
+ * await api.exportData({
17
+ * format: 'pdf',
18
+ * pdf: { worker: () => new Worker(new URL('./pdf.worker.ts', import.meta.url), { type: 'module' }) },
19
+ * })
20
+ * ```
21
+ *
22
+ * The main thread builds the document as plain data (`buildPdfDocData`) and
23
+ * posts it here with the font, if any; the worker loads pdfmake, attaches
24
+ * the layout and footer functions (`hydratePdfDoc`), renders, and posts the
25
+ * bytes back with progress along the way.
26
+ */
27
+ import { type PdfDocData, type PdfFont, type PdfMakeLike, type PdfWorkerLike } from './export-pdf.js';
28
+ export type { PdfWorkerLike };
29
+ /** Tags this channel's messages, so a worker shared with other traffic ignores the rest. */
30
+ export declare const PDF_WORKER_CHANNEL = "svgrid-pdf";
31
+ export type PdfWorkerRequest = {
32
+ channel: typeof PDF_WORKER_CHANNEL;
33
+ id: number;
34
+ doc: PdfDocData;
35
+ font?: PdfFont;
36
+ };
37
+ export type PdfWorkerReply = {
38
+ channel: typeof PDF_WORKER_CHANNEL;
39
+ id: number;
40
+ progress: number;
41
+ } | {
42
+ channel: typeof PDF_WORKER_CHANNEL;
43
+ id: number;
44
+ pdf: ArrayBuffer;
45
+ } | {
46
+ channel: typeof PDF_WORKER_CHANNEL;
47
+ id: number;
48
+ error: string;
49
+ };
50
+ /** The pdfmake calls rendering needs, in either the 0.2 or the 0.3 shape. */
51
+ export type PdfMakeRenderer = PdfMakeLike & {
52
+ fonts?: Record<string, unknown>;
53
+ addFonts?: (fonts: Record<string, unknown>) => void;
54
+ createPdf(def: unknown): {
55
+ getBuffer(cb?: (bytes: Uint8Array) => void, options?: {
56
+ progressCallback?: (progress: number) => void;
57
+ }): unknown;
58
+ };
59
+ };
60
+ /** The part of a worker's global scope (or a `MessagePort`) this needs. */
61
+ export type PdfWorkerScope = {
62
+ postMessage(message: unknown, transfer?: Transferable[]): void;
63
+ addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
64
+ removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
65
+ };
66
+ /**
67
+ * Render a document from `buildPdfDocData` with the pdfmake given. The
68
+ * worker's half of the export, exported so it can run anywhere pdfmake does.
69
+ */
70
+ export declare function renderPdfDoc(doc: PdfDocData, pdfMake: PdfMakeRenderer, opts?: {
71
+ font?: PdfFont;
72
+ onProgress?: (progress: number) => void;
73
+ }): Promise<Uint8Array>;
74
+ export type ServePdfWorkerOptions = {
75
+ /** Where to listen. Default: the worker's global scope. */
76
+ scope?: PdfWorkerScope;
77
+ /** How to get pdfmake. Default: import the installed `pdfmake` peer. */
78
+ loadPdfMake?: () => Promise<PdfMakeRenderer>;
79
+ };
80
+ /** Answer PDF export requests from the main thread. Call it once, in the worker. */
81
+ export declare function servePdfWorker(options?: ServePdfWorkerOptions): {
82
+ dispose(): void;
83
+ };
84
+ /**
85
+ * The main thread's half: post the document to a fresh worker, report its
86
+ * progress, and resolve with the PDF. An abort terminates the worker.
87
+ */
88
+ export declare function renderPdfInWorker(makeWorker: () => PdfWorkerLike, doc: PdfDocData, opts?: {
89
+ font?: PdfFont;
90
+ signal?: AbortSignal;
91
+ onProgress?: (progress: number) => void;
92
+ }): Promise<Blob>;