sheetra 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -13,15 +13,87 @@ Sheetra is a powerful, zero-dependency library for exporting data to Excel (XLSX
13
13
 
14
14
  ---
15
15
 
16
- ## Features
17
-
18
- - **Zero Dependencies** – Pure TypeScript/JavaScript implementation
19
- - **Multiple Formats** – Export to XLSX, CSV, JSON
20
- - **Styling Support** Bold, colors, borders, alignment
21
- - **Outlines & Groups** – Create collapsible sections
22
- - **Fluent API** Chain methods for easy construction
23
- - **TypeScript** Full type support
24
- - **Browser & Node** Works in both environments
16
+
17
+ ## Example Usage
18
+
19
+ ```ts
20
+ import { ExportBuilder, StyleBuilder } from 'sheetra';
21
+
22
+ // --- Comprehensive Example: All Features ---
23
+ const users = [
24
+ { name: 'John Doe', age: 30, email: 'john@example.com', department: 'Engineering' },
25
+ { name: 'Jane Smith', age: 25, email: 'jane@example.com', department: 'Marketing' },
26
+ ];
27
+ const parts = [
28
+ { part_number: 'P001', part_name: 'Widget', current_stock: 100, price: 29.99 },
29
+ { part_number: 'P002', part_name: 'Gadget', current_stock: 50, price: 49.99 },
30
+ ];
31
+ const salesData = [
32
+ { product: 'Widget', jan: 1500, feb: 1800, mar: 2100 },
33
+ { product: 'Gadget', jan: 900, feb: 1100, mar: 1300 },
34
+ ];
35
+
36
+ // Styled header
37
+ const headerStyle = StyleBuilder.create()
38
+ .bold()
39
+ .backgroundColor('#4F81BD')
40
+ .color('#FFFFFF')
41
+ .align('center')
42
+ .build();
43
+
44
+ ExportBuilder.create('All Features Demo')
45
+ // Set column widths
46
+ .setColumnWidths([150, 100, 200, 120])
47
+ // Add styled header row
48
+ .addHeaderRow(['Name', 'Age', 'Email', 'Department'], headerStyle)
49
+ // Add data rows
50
+ .addDataRows(users.map(u => [u.name, u.age, u.email, u.department]))
51
+ // Add section
52
+ .addSection({ name: 'Parts', title: 'Parts Inventory' })
53
+ .addHeaderRow(['Part Number', 'Part Name', 'Stock', 'Price'])
54
+ .addDataRows(parts.map(p => [p.part_number, p.part_name, p.current_stock, p.price]))
55
+ // Merge cells for a title
56
+ .addDataRows([['Monthly Sales Report', '', '', '']])
57
+ .mergeCells(7, 0, 7, 3)
58
+ .setAlignment(7, 0, 'center')
59
+ // Alignment demo
60
+ .addHeaderRow(['Left', 'Center', 'Right'])
61
+ .setAlignment(8, 0, 'left')
62
+ .setAlignment(8, 1, 'center')
63
+ .setAlignment(8, 2, 'right')
64
+ .addDataRows([
65
+ ['Left Text', 'Center Text', 'Right Text'],
66
+ ['Value 1', 'Value 2', 'Value 3'],
67
+ ])
68
+ .setRangeAlignment(9, 0, 10, 0, 'left')
69
+ .setRangeAlignment(9, 1, 10, 1, 'center')
70
+ .setRangeAlignment(9, 2, 10, 2, 'right')
71
+ // Auto-size columns
72
+ .autoSizeColumns()
73
+ // Download as XLSX
74
+ .download({ filename: 'all-features-demo.xlsx', format: 'xlsx' });
75
+ ```
76
+
77
+ This example demonstrates:
78
+ - Column widths and auto-sizing
79
+ - Merged cells for titles
80
+ - Alignment (left, center, right, range)
81
+ - Sections and headers
82
+ - Styled headers with StyleBuilder
83
+ - Data rows and multiple tables
84
+ - XLSX export
85
+
86
+ See the [sheetra-demo](./sheetra-demo/src/App.tsx) for a full-featured React demo with all features in action.
87
+ .addDataRows(users.map(u => [u.name, u.age, u.email, u.department]))
88
+ .download({ filename: 'styled-report.xlsx', format: 'xlsx' });
89
+ ```
90
+
91
+ ## Troubleshooting
92
+
93
+ **Upgrading from previous versions?**
94
+
95
+ If you previously encountered errors with styled exports (e.g., `SyntaxError: Expected ':' after property name in JSON`), upgrade to the latest version. Style and alignment handling is now robust and all edge cases are supported.
96
+
25
97
 
26
98
  ---
27
99
 
package/dist/index.esm.js CHANGED
@@ -524,22 +524,129 @@ class ExcelWriter {
524
524
  const zip = new ZipWriter();
525
525
  const sheets = workbook['sheets'];
526
526
  const sheetNames = sheets.map((sheet, index) => sheet.getName() || `Sheet${index + 1}`);
527
+ // Collect all styles from sheets
528
+ const styleRegistry = this.collectStyles(sheets);
527
529
  // Add required XLSX files
528
530
  zip.addFile('[Content_Types].xml', this.generateContentTypes(sheetNames));
529
531
  zip.addFile('_rels/.rels', this.generateRels());
530
532
  zip.addFile('xl/workbook.xml', this.generateWorkbook(sheetNames));
531
533
  zip.addFile('xl/_rels/workbook.xml.rels', this.generateWorkbookRels(sheetNames));
532
- zip.addFile('xl/styles.xml', this.generateStyles());
534
+ zip.addFile('xl/styles.xml', this.generateStyles(styleRegistry));
533
535
  zip.addFile('xl/sharedStrings.xml', this.generateSharedStrings(sheets));
534
536
  // Add each worksheet
535
537
  sheets.forEach((sheet, index) => {
536
- zip.addFile(`xl/worksheets/sheet${index + 1}.xml`, this.generateWorksheet(sheet));
538
+ zip.addFile(`xl/worksheets/sheet${index + 1}.xml`, this.generateWorksheet(sheet, styleRegistry));
537
539
  });
538
540
  const buffer = zip.generate();
539
541
  return new Blob([buffer.buffer], {
540
542
  type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
541
543
  });
542
544
  }
545
+ /**
546
+ * Collect all unique styles from sheets
547
+ */
548
+ static collectStyles(sheets) {
549
+ const registry = {
550
+ fonts: new Map(),
551
+ fills: new Map(),
552
+ borders: new Map(),
553
+ cellXfs: new Map()
554
+ };
555
+ // Add default font (Calibri 11)
556
+ const defaultFont = this.serializeFont({});
557
+ registry.fonts.set(defaultFont, 0);
558
+ // Add required fills (none and gray125)
559
+ registry.fills.set('none', 0);
560
+ registry.fills.set('gray125', 1);
561
+ // Add default border
562
+ registry.borders.set('none', 0);
563
+ // Add default cellXf (no style)
564
+ registry.cellXfs.set('default', 0);
565
+ // Collect styles from all cells
566
+ sheets.forEach((sheet) => {
567
+ const rows = sheet.getRows();
568
+ rows.forEach((row) => {
569
+ row.getCells().forEach((cell) => {
570
+ const cellData = cell.toData();
571
+ if (cellData.style) {
572
+ this.registerStyle(cellData.style, registry);
573
+ }
574
+ });
575
+ });
576
+ });
577
+ return registry;
578
+ }
579
+ /**
580
+ * Register a style and get its cellXf index
581
+ */
582
+ static registerStyle(style, registry) {
583
+ var _a, _b, _c, _d;
584
+ // Serialize and register font
585
+ const fontKey = this.serializeFont(style);
586
+ if (!registry.fonts.has(fontKey)) {
587
+ registry.fonts.set(fontKey, registry.fonts.size);
588
+ }
589
+ // Serialize and register fill
590
+ const fillKey = this.serializeFill(style);
591
+ if (fillKey !== 'none' && !registry.fills.has(fillKey)) {
592
+ registry.fills.set(fillKey, registry.fills.size);
593
+ }
594
+ // Serialize and register border
595
+ const borderKey = this.serializeBorder(style);
596
+ if (borderKey !== 'none' && !registry.borders.has(borderKey)) {
597
+ registry.borders.set(borderKey, registry.borders.size);
598
+ }
599
+ // Create cellXf key
600
+ const fontIndex = (_a = registry.fonts.get(fontKey)) !== null && _a !== void 0 ? _a : 0;
601
+ const fillIndex = (_b = registry.fills.get(fillKey)) !== null && _b !== void 0 ? _b : 0;
602
+ const borderIndex = (_c = registry.borders.get(borderKey)) !== null && _c !== void 0 ? _c : 0;
603
+ let alignment = this.serializeAlignment(style);
604
+ if (!alignment)
605
+ alignment = '{}';
606
+ const xfKey = `f${fontIndex}:l${fillIndex}:b${borderIndex}:a${alignment}`;
607
+ if (!registry.cellXfs.has(xfKey)) {
608
+ registry.cellXfs.set(xfKey, registry.cellXfs.size);
609
+ }
610
+ return (_d = registry.cellXfs.get(xfKey)) !== null && _d !== void 0 ? _d : 0;
611
+ }
612
+ /**
613
+ * Get style index for a cell style
614
+ */
615
+ static getStyleIndex(style, registry) {
616
+ if (!style)
617
+ return 0;
618
+ return this.registerStyle(style, registry);
619
+ }
620
+ static serializeFont(style) {
621
+ var _a, _b, _c, _d, _e, _f;
622
+ const bold = style.bold || ((_a = style.font) === null || _a === void 0 ? void 0 : _a.bold);
623
+ const italic = style.italic || ((_b = style.font) === null || _b === void 0 ? void 0 : _b.italic);
624
+ const underline = style.underline || ((_c = style.font) === null || _c === void 0 ? void 0 : _c.underline);
625
+ const color = style.color || ((_d = style.font) === null || _d === void 0 ? void 0 : _d.color);
626
+ const size = style.fontSize || ((_e = style.font) === null || _e === void 0 ? void 0 : _e.size) || 11;
627
+ const name = style.fontFamily || ((_f = style.font) === null || _f === void 0 ? void 0 : _f.name) || 'Calibri';
628
+ return JSON.stringify({ bold, italic, underline, color, size, name });
629
+ }
630
+ static serializeFill(style) {
631
+ var _a;
632
+ const bgColor = style.backgroundColor || ((_a = style.fill) === null || _a === void 0 ? void 0 : _a.fgColor);
633
+ if (!bgColor)
634
+ return 'none';
635
+ return JSON.stringify({ bgColor });
636
+ }
637
+ static serializeBorder(style) {
638
+ if (!style.border && !style.borderAll)
639
+ return 'none';
640
+ return JSON.stringify({ border: style.border, borderAll: style.borderAll });
641
+ }
642
+ static serializeAlignment(style) {
643
+ const h = style.alignment;
644
+ const v = style.verticalAlignment;
645
+ const wrap = style.wrapText;
646
+ if (!h && !v && !wrap)
647
+ return '';
648
+ return JSON.stringify({ h, v, wrap });
649
+ }
543
650
  static escapeXml(str) {
544
651
  if (typeof str !== 'string')
545
652
  return String(str !== null && str !== void 0 ? str : '');
@@ -593,17 +700,150 @@ class ExcelWriter {
593
700
  xml += '</Relationships>';
594
701
  return xml;
595
702
  }
596
- static generateStyles() {
703
+ static generateStyles(registry) {
597
704
  let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
598
705
  xml += '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
599
- xml += '<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>';
600
- xml += '<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>';
601
- xml += '<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>';
706
+ // Generate fonts
707
+ xml += `<fonts count="${registry.fonts.size}">`;
708
+ const fontEntries = Array.from(registry.fonts.entries()).sort((a, b) => a[1] - b[1]);
709
+ for (const [fontKey] of fontEntries) {
710
+ const font = JSON.parse(fontKey);
711
+ xml += '<font>';
712
+ if (font.bold)
713
+ xml += '<b/>';
714
+ if (font.italic)
715
+ xml += '<i/>';
716
+ if (font.underline)
717
+ xml += '<u/>';
718
+ xml += `<sz val="${font.size || 11}"/>`;
719
+ if (font.color) {
720
+ const colorHex = this.colorToARGB(font.color);
721
+ xml += `<color rgb="${colorHex}"/>`;
722
+ }
723
+ xml += `<name val="${font.name || 'Calibri'}"/>`;
724
+ xml += '</font>';
725
+ }
726
+ xml += '</fonts>';
727
+ // Generate fills
728
+ xml += `<fills count="${registry.fills.size}">`;
729
+ const fillEntries = Array.from(registry.fills.entries()).sort((a, b) => a[1] - b[1]);
730
+ for (const [fillKey] of fillEntries) {
731
+ if (fillKey === 'none') {
732
+ xml += '<fill><patternFill patternType="none"/></fill>';
733
+ }
734
+ else if (fillKey === 'gray125') {
735
+ xml += '<fill><patternFill patternType="gray125"/></fill>';
736
+ }
737
+ else {
738
+ const fill = JSON.parse(fillKey);
739
+ const colorHex = this.colorToARGB(fill.bgColor);
740
+ xml += `<fill><patternFill patternType="solid"><fgColor rgb="${colorHex}"/><bgColor indexed="64"/></patternFill></fill>`;
741
+ }
742
+ }
743
+ xml += '</fills>';
744
+ // Generate borders
745
+ xml += `<borders count="${registry.borders.size}">`;
746
+ const borderEntries = Array.from(registry.borders.entries()).sort((a, b) => a[1] - b[1]);
747
+ for (const [borderKey] of borderEntries) {
748
+ if (borderKey === 'none') {
749
+ xml += '<border><left/><right/><top/><bottom/><diagonal/></border>';
750
+ }
751
+ else {
752
+ const borderData = JSON.parse(borderKey);
753
+ xml += '<border>';
754
+ if (borderData.borderAll) {
755
+ const style = borderData.borderAll.style || 'thin';
756
+ const color = borderData.borderAll.color ? this.colorToARGB(borderData.borderAll.color) : 'FF000000';
757
+ xml += `<left style="${style}"><color rgb="${color}"/></left>`;
758
+ xml += `<right style="${style}"><color rgb="${color}"/></right>`;
759
+ xml += `<top style="${style}"><color rgb="${color}"/></top>`;
760
+ xml += `<bottom style="${style}"><color rgb="${color}"/></bottom>`;
761
+ }
762
+ else if (borderData.border) {
763
+ const b = borderData.border;
764
+ xml += this.generateBorderEdge('left', b.left);
765
+ xml += this.generateBorderEdge('right', b.right);
766
+ xml += this.generateBorderEdge('top', b.top);
767
+ xml += this.generateBorderEdge('bottom', b.bottom);
768
+ }
769
+ xml += '<diagonal/></border>';
770
+ }
771
+ }
772
+ xml += '</borders>';
773
+ // Generate cellStyleXfs (base styles)
602
774
  xml += '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>';
603
- xml += '<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>';
775
+ // Generate cellXfs (cell formats)
776
+ xml += `<cellXfs count="${registry.cellXfs.size}">`;
777
+ const xfEntries = Array.from(registry.cellXfs.entries()).sort((a, b) => a[1] - b[1]);
778
+ for (const [xfKey] of xfEntries) {
779
+ if (xfKey === 'default') {
780
+ xml += '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>';
781
+ }
782
+ else {
783
+ // Parse xf key: f{fontIndex}:l{fillIndex}:b{borderIndex}:a{alignment}
784
+ const parts = xfKey.split(':');
785
+ const fontId = parseInt(parts[0].substring(1)) || 0;
786
+ const fillId = parseInt(parts[1].substring(1)) || 0;
787
+ const borderId = parseInt(parts[2].substring(1)) || 0;
788
+ const alignmentJson = parts[3].substring(1);
789
+ let xf = `<xf numFmtId="0" fontId="${fontId}" fillId="${fillId}" borderId="${borderId}" xfId="0"`;
790
+ if (fontId > 0)
791
+ xf += ' applyFont="1"';
792
+ if (fillId > 0)
793
+ xf += ' applyFill="1"';
794
+ if (borderId > 0)
795
+ xf += ' applyBorder="1"';
796
+ let align = {};
797
+ if (alignmentJson && alignmentJson.trim().startsWith('{')) {
798
+ try {
799
+ align = JSON.parse(alignmentJson);
800
+ }
801
+ catch (e) {
802
+ align = {};
803
+ }
804
+ }
805
+ if (Object.keys(align).length > 0) {
806
+ xf += ' applyAlignment="1">';
807
+ xf += '<alignment';
808
+ if (align.h)
809
+ xf += ` horizontal="${align.h}"`;
810
+ if (align.v)
811
+ xf += ` vertical="${align.v === 'middle' ? 'center' : align.v}"`;
812
+ if (align.wrap)
813
+ xf += ' wrapText="1"';
814
+ xf += '/></xf>';
815
+ }
816
+ else {
817
+ xf += '/>';
818
+ }
819
+ xml += xf;
820
+ }
821
+ }
822
+ xml += '</cellXfs>';
604
823
  xml += '</styleSheet>';
605
824
  return xml;
606
825
  }
826
+ static generateBorderEdge(side, edge) {
827
+ if (!edge || !edge.style)
828
+ return `<${side}/>`;
829
+ const color = edge.color ? this.colorToARGB(edge.color) : 'FF000000';
830
+ return `<${side} style="${edge.style}"><color rgb="${color}"/></${side}>`;
831
+ }
832
+ static colorToARGB(color) {
833
+ if (!color)
834
+ return 'FF000000';
835
+ // Remove # if present
836
+ let hex = color.replace('#', '').toUpperCase();
837
+ // Add alpha if not present (6 chars -> 8 chars)
838
+ if (hex.length === 6) {
839
+ hex = 'FF' + hex;
840
+ }
841
+ // Handle 3 char hex
842
+ if (hex.length === 3) {
843
+ hex = 'FF' + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
844
+ }
845
+ return hex;
846
+ }
607
847
  static generateSharedStrings(sheets) {
608
848
  const strings = [];
609
849
  sheets.forEach((sheet) => {
@@ -627,14 +867,32 @@ class ExcelWriter {
627
867
  xml += '</sst>';
628
868
  return xml;
629
869
  }
630
- static generateWorksheet(sheet) {
870
+ static generateWorksheet(sheet, styleRegistry) {
631
871
  const rows = sheet.getRows();
872
+ const columns = sheet.getColumns();
632
873
  const allStrings = this.collectAllStrings(sheet);
633
874
  let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
634
875
  xml += '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
635
876
  // Add sheet dimension
636
877
  const dimension = this.getSheetDimension(rows);
637
878
  xml += `<dimension ref="${dimension}"/>`;
879
+ // Add column widths if defined
880
+ if (columns.length > 0) {
881
+ xml += '<cols>';
882
+ columns.forEach((col, index) => {
883
+ const colData = col.toData();
884
+ if (colData.width !== undefined && colData.width > 0) {
885
+ // Excel width is in character units, convert from pixels: width / 7
886
+ const excelWidth = colData.width / 7;
887
+ xml += `<col min="${index + 1}" max="${index + 1}" width="${excelWidth}" customWidth="1"`;
888
+ if (colData.hidden) {
889
+ xml += ' hidden="1"';
890
+ }
891
+ xml += '/>';
892
+ }
893
+ });
894
+ xml += '</cols>';
895
+ }
638
896
  xml += '<sheetData>';
639
897
  rows.forEach((row, rowIndex) => {
640
898
  const cells = row.getCells();
@@ -643,37 +901,54 @@ class ExcelWriter {
643
901
  cells.forEach((cell, colIndex) => {
644
902
  const cellData = cell.toData();
645
903
  const cellRef = this.columnToLetter(colIndex + 1) + (rowIndex + 1);
904
+ const styleIndex = this.getStyleIndex(cellData.style, styleRegistry);
905
+ const styleAttr = styleIndex > 0 ? ` s="${styleIndex}"` : '';
646
906
  if (cellData.formula) {
647
- xml += `<c r="${cellRef}"><f>${this.escapeXml(cellData.formula)}</f></c>`;
907
+ xml += `<c r="${cellRef}"${styleAttr}><f>${this.escapeXml(cellData.formula)}</f></c>`;
648
908
  }
649
909
  else if (cellData.value !== null && cellData.value !== undefined && cellData.value !== '') {
650
910
  if (typeof cellData.value === 'number') {
651
- xml += `<c r="${cellRef}"><v>${cellData.value}</v></c>`;
911
+ xml += `<c r="${cellRef}"${styleAttr}><v>${cellData.value}</v></c>`;
652
912
  }
653
913
  else if (typeof cellData.value === 'boolean') {
654
- xml += `<c r="${cellRef}" t="b"><v>${cellData.value ? 1 : 0}</v></c>`;
914
+ xml += `<c r="${cellRef}"${styleAttr} t="b"><v>${cellData.value ? 1 : 0}</v></c>`;
655
915
  }
656
916
  else if (cellData.type === 'date') {
657
917
  const excelDate = DateFormatter.toExcelDate(cellData.value);
658
- xml += `<c r="${cellRef}"><v>${excelDate}</v></c>`;
918
+ xml += `<c r="${cellRef}"${styleAttr}><v>${excelDate}</v></c>`;
659
919
  }
660
920
  else {
661
921
  // String value - use shared string index
662
922
  const stringIndex = allStrings.indexOf(String(cellData.value));
663
923
  if (stringIndex >= 0) {
664
- xml += `<c r="${cellRef}" t="s"><v>${stringIndex}</v></c>`;
924
+ xml += `<c r="${cellRef}"${styleAttr} t="s"><v>${stringIndex}</v></c>`;
665
925
  }
666
926
  else {
667
927
  // Inline string as fallback
668
- xml += `<c r="${cellRef}" t="inlineStr"><is><t>${this.escapeXml(String(cellData.value))}</t></is></c>`;
928
+ xml += `<c r="${cellRef}"${styleAttr} t="inlineStr"><is><t>${this.escapeXml(String(cellData.value))}</t></is></c>`;
669
929
  }
670
930
  }
671
931
  }
932
+ else if (styleIndex > 0) {
933
+ // Empty cell with style
934
+ xml += `<c r="${cellRef}"${styleAttr}/>`;
935
+ }
672
936
  });
673
937
  xml += '</row>';
674
938
  }
675
939
  });
676
940
  xml += '</sheetData>';
941
+ // Add merged cells if any
942
+ const sheetData = sheet.toData();
943
+ if (sheetData.mergeCells && sheetData.mergeCells.length > 0) {
944
+ xml += '<mergeCells>';
945
+ sheetData.mergeCells.forEach(merge => {
946
+ const startRef = this.columnToLetter(merge.startCol + 1) + (merge.startRow + 1);
947
+ const endRef = this.columnToLetter(merge.endCol + 1) + (merge.endRow + 1);
948
+ xml += `<mergeCell ref="${startRef}:${endRef}"/>`;
949
+ });
950
+ xml += '</mergeCells>';
951
+ }
677
952
  xml += '</worksheet>';
678
953
  return xml;
679
954
  }
@@ -967,23 +1242,46 @@ class ExportBuilder {
967
1242
  });
968
1243
  return this;
969
1244
  }
970
- addDataRows(data, fields) {
971
- data.forEach(item => {
1245
+ /**
1246
+ * Add multiple data rows to the sheet
1247
+ * @param data Array of row data
1248
+ * @param fields Optional array of field names (for object data)
1249
+ * @param styles Optional array of styles per row or per cell
1250
+ */
1251
+ addDataRows(data, fields, styles) {
1252
+ data.forEach((item, rowIdx) => {
972
1253
  const row = this.currentSheet.createRow();
1254
+ let rowStyle = undefined;
1255
+ let cellStyles = undefined;
1256
+ if (styles && styles[rowIdx]) {
1257
+ if (Array.isArray(styles[rowIdx])) {
1258
+ cellStyles = styles[rowIdx];
1259
+ }
1260
+ else {
1261
+ rowStyle = styles[rowIdx];
1262
+ }
1263
+ }
973
1264
  if (fields && fields.length > 0) {
974
- fields.forEach(field => {
1265
+ fields.forEach((field, colIdx) => {
975
1266
  const value = this.getNestedValue(item, field);
976
- row.createCell(value);
1267
+ const style = cellStyles ? cellStyles[colIdx] : rowStyle;
1268
+ row.createCell(value, style);
977
1269
  });
978
1270
  }
979
1271
  else if (Array.isArray(item)) {
980
- item.forEach(value => row.createCell(value));
1272
+ item.forEach((value, colIdx) => {
1273
+ const style = cellStyles ? cellStyles[colIdx] : rowStyle;
1274
+ row.createCell(value, style);
1275
+ });
981
1276
  }
982
1277
  else if (typeof item === 'object') {
983
- Object.values(item).forEach(value => row.createCell(value));
1278
+ Object.values(item).forEach((value, colIdx) => {
1279
+ const style = cellStyles ? cellStyles[colIdx] : rowStyle;
1280
+ row.createCell(value, style);
1281
+ });
984
1282
  }
985
1283
  else {
986
- row.createCell(item);
1284
+ row.createCell(item, rowStyle);
987
1285
  }
988
1286
  });
989
1287
  return this;
@@ -1042,6 +1340,55 @@ class ExportBuilder {
1042
1340
  });
1043
1341
  return this;
1044
1342
  }
1343
+ /**
1344
+ * Merge cells in the current worksheet
1345
+ * @param startRow Start row index (0-based)
1346
+ * @param startCol Start column index (0-based)
1347
+ * @param endRow End row index (0-based)
1348
+ * @param endCol End column index (0-based)
1349
+ */
1350
+ mergeCells(startRow, startCol, endRow, endCol) {
1351
+ this.currentSheet.mergeCells(startRow, startCol, endRow, endCol);
1352
+ return this;
1353
+ }
1354
+ /**
1355
+ * Set alignment for a specific cell
1356
+ * @param row Row index (0-based)
1357
+ * @param col Column index (0-based)
1358
+ * @param horizontal Horizontal alignment
1359
+ * @param vertical Vertical alignment (optional)
1360
+ */
1361
+ setAlignment(row, col, horizontal, vertical) {
1362
+ const rowObj = this.currentSheet.getRow(row);
1363
+ if (rowObj) {
1364
+ const cells = rowObj.getCells();
1365
+ if (cells[col]) {
1366
+ const style = StyleBuilder.create().align(horizontal);
1367
+ if (vertical) {
1368
+ style.verticalAlign(vertical);
1369
+ }
1370
+ cells[col].setStyle(style.build());
1371
+ }
1372
+ }
1373
+ return this;
1374
+ }
1375
+ /**
1376
+ * Set alignment for a range of cells
1377
+ * @param startRow Start row index (0-based)
1378
+ * @param startCol Start column index (0-based)
1379
+ * @param endRow End row index (0-based)
1380
+ * @param endCol End column index (0-based)
1381
+ * @param horizontal Horizontal alignment
1382
+ * @param vertical Vertical alignment (optional)
1383
+ */
1384
+ setRangeAlignment(startRow, startCol, endRow, endCol, horizontal, vertical) {
1385
+ for (let r = startRow; r <= endRow; r++) {
1386
+ for (let c = startCol; c <= endCol; c++) {
1387
+ this.setAlignment(r, c, horizontal, vertical);
1388
+ }
1389
+ }
1390
+ return this;
1391
+ }
1045
1392
  autoSizeColumns() {
1046
1393
  const rows = this.currentSheet.getRows();
1047
1394
  const maxLengths = [];
@@ -1166,6 +1513,28 @@ class SheetBuilder {
1166
1513
  });
1167
1514
  return this;
1168
1515
  }
1516
+ /**
1517
+ * Set the width of a specific column
1518
+ * @param colIndex Column index
1519
+ * @param width Width to set
1520
+ */
1521
+ setColumnWidth(colIndex, width) {
1522
+ const column = this.getOrCreateColumn(colIndex);
1523
+ column.setWidth(width);
1524
+ return this;
1525
+ }
1526
+ /**
1527
+ * Set the height of a specific row
1528
+ * @param rowIndex Row index
1529
+ * @param height Height to set
1530
+ */
1531
+ setRowHeight(rowIndex, height) {
1532
+ const row = this.worksheet.getRow(rowIndex);
1533
+ if (row) {
1534
+ row.setHeight(height);
1535
+ }
1536
+ return this;
1537
+ }
1169
1538
  /**
1170
1539
  * Add a title row
1171
1540
  * @param title Title text