├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.config.ts ├── bun.lockb ├── docs ├── .examples │ └── financial-report │ │ ├── data.ts │ │ ├── file.ts │ │ └── schema.ts ├── .vitepress │ ├── config.ts │ ├── global.d.ts │ └── theme │ │ ├── components │ │ ├── CodeSandbox.vue │ │ └── ExampleRenderer.vue │ │ ├── config │ │ └── themeVars.ts │ │ ├── data │ │ └── examples.data.ts │ │ ├── index.ts │ │ └── style.css ├── file-builder │ ├── build-excel-file.md │ ├── create-file-builder.md │ ├── define-sheets.md │ └── define-tables.md ├── getting-started │ ├── installation.md │ └── key-benefits-why.md ├── index.md ├── public │ ├── favicon.ico │ └── images │ │ ├── examples │ │ ├── col-dynamic-1.png │ │ ├── col-format-1.png │ │ ├── col-sub-rows.png │ │ ├── col-sum-1.png │ │ └── multi-tables-1.png │ │ ├── logo.png │ │ └── logo.svg └── schema-builder │ ├── build-schema.md │ ├── columns.md │ ├── create-schema.md │ ├── dynamic-columns.md │ ├── reusable-formatters.md │ └── reusable-transformers.md ├── eslint.config.js ├── examples ├── financial-report.xlsx ├── kitchen-sink.xlsx ├── playground.xlsx └── ~$playground.xlsx ├── image.png ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── const.ts ├── index.ts ├── types.ts └── utils.ts ├── test ├── financial-report.test.ts ├── kitchen-sink.test.ts └── play.test.ts ├── tsconfig.json ├── uno.config.ts └── vercel.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install bun 19 | uses: oven-sh/setup-bun@v1 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Install 27 | run: bun install 28 | 29 | - name: Lint 30 | run: bun run lint 31 | 32 | typecheck: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | 37 | - name: Install bun 38 | uses: oven-sh/setup-bun@v1 39 | 40 | - name: Set node 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: lts/* 44 | 45 | - name: Install 46 | run: bun install 47 | 48 | - name: Typecheck 49 | run: bun run typecheck 50 | 51 | test: 52 | runs-on: ${{ matrix.os }} 53 | 54 | strategy: 55 | matrix: 56 | node: [lts/*] 57 | os: [ubuntu-latest, windows-latest, macos-latest] 58 | fail-fast: false 59 | 60 | steps: 61 | - uses: actions/checkout@v3 62 | 63 | - name: Install bun 64 | uses: oven-sh/setup-bun@v1 65 | 66 | - name: Set node ${{ matrix.node }} 67 | uses: actions/setup-node@v3 68 | with: 69 | node-version: ${{ matrix.node }} 70 | 71 | - name: Install 72 | run: bun install 73 | 74 | - name: Build 75 | run: bun run build 76 | 77 | - name: Test 78 | run: bun run test 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install bun 20 | uses: oven-sh/setup-bun@v1 21 | 22 | - name: Set node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: lts/* 26 | 27 | - run: bunx changelogithub 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | docs/.vitepress/cache 13 | .vitepress/cache 14 | .vercel 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | // Disable the default formatter, use eslint instead 5 | "prettier.enable": false, 6 | "editor.formatOnSave": false, 7 | // Auto fix 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "explicit", 10 | "source.organizeImports": "never" 11 | }, 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { 15 | "rule": "style/*", 16 | "severity": "off" 17 | }, 18 | { 19 | "rule": "*-indent", 20 | "severity": "off" 21 | }, 22 | { 23 | "rule": "*-spacing", 24 | "severity": "off" 25 | }, 26 | { 27 | "rule": "*-spaces", 28 | "severity": "off" 29 | }, 30 | { 31 | "rule": "*-order", 32 | "severity": "off" 33 | }, 34 | { 35 | "rule": "*-dangle", 36 | "severity": "off" 37 | }, 38 | { 39 | "rule": "*-newline", 40 | "severity": "off" 41 | }, 42 | { 43 | "rule": "*quotes", 44 | "severity": "off" 45 | }, 46 | { 47 | "rule": "*semi", 48 | "severity": "off" 49 | } 50 | ], 51 | // Enable eslint for all supported languages 52 | "eslint.validate": [ 53 | "javascript", 54 | "javascriptreact", 55 | "typescript", 56 | "typescriptreact", 57 | "vue", 58 | "html", 59 | "markdown", 60 | "json", 61 | "jsonc", 62 | "yaml" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/antfu/contribute 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cyprien THAO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typed-xlsx 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | 10 | 11 | 12 | > ### **Export any data into xls/xlsx files effortlessly, while benefiting from great type-safety & developper experience.** 13 | 14 | ## Key Features : 15 | 16 | - 🛠 **Type-safe Schema Builder:** Design your spreadsheet schema with strong typing for enhanced reliability and developer experience. 17 | 18 | - 🔄 **Type-safe Data Serialization & Transformation:** Ensure data integrity through type-safe serialization and transformation functionalities. 19 | 20 | - 🔧 **Shared Type-safe Custom Value Pre-processors:** Utilize shared pre-processors for consistent value transformaiton 21 | 22 | - 🧮 **Column Summary :** Auto-insert computed column summaries for efficient data analysis and overview. 23 | 24 | - 🧩 **Complex Row Structures with Auto-Merging:** Implement advanced row layouts with sub-rows for automatic row merging / styling for seamless data organization and display. 25 | 26 | - 🎯 **Easy Default Values Management:** Manage default values effortlessly, ensuring your data is presented exactly as you intend. 27 | 28 | - 📊 **Dynamic Column Selection:** Selectively choose which columns of the schema to use when building a table 29 | 30 | - 🗺️ **Dynamic Column Mapping with Type-safe Context:** Map columns dynamically with a type-safe context, injected when building sheet 31 | 32 | - 🎨 **Dynamic Cell Styling/Formatting:** Customize cell styling and formatting dynamically per-row with ease 33 | 34 | - 📑 **Multi-sheet Support:** Create spreadsheets with multiple sheets 35 | 36 | - 🏗️ **Multiple Tables Per Sheet Support:** Include as many tables you need inside a same sheet 37 | 38 | - 🌐 **Linear or Grid-like Layout for Sheets with Multiple Tables:** Choose between linear or grid layouts for sheets 39 | 40 | ## INSTALLATION 41 | ```bash 42 | pnpm add @chronicstone/typed-xlsx 43 | ``` 44 | 45 | ## USAGE EXAMPLE 46 | 47 | #### 1. Define the type of exported data (Or infer it from a function / a db query, or wherever you want) : 48 | ```ts 49 | interface Organization { 50 | id: number 51 | name: string 52 | } 53 | 54 | interface User { 55 | id: number 56 | firstName: string 57 | lastName: string 58 | email: string 59 | roles: string[] 60 | organizations: Organization[] 61 | results: { 62 | general: { overall: number } 63 | technical: { overall: number } 64 | interview?: { overall: number } 65 | } 66 | } 67 | ``` 68 | 69 | #### 2. Build a sheet schema : 70 | ```ts 71 | import { ExcelSchemaBuilder } from '@chronicstone/typed-xlsx' 72 | 73 | // OPTIONAL : DEFINE SHARED TRANSFORMERS THAT CAN BE USE TO TRANSFORM VALUE INSERTED INTO A CELL 74 | const transformers = { 75 | boolean: (value: boolean) => value ? 'Yes' : 'No', 76 | list: (value: (string)[]) => value.join(', '), 77 | arrayLength: (value: any[]) => value.length, 78 | } satisfies TransformersMap 79 | 80 | // Use the schema builder to define your sheet schema 81 | const userExportSchema = ExcelSchemaBuilder 82 | .create() 83 | .withTransformers(transformers) 84 | .column('id', { 85 | key: 'id', 86 | summary: [{ value: () => 'TOTAL BEFORE VAT' }, { value: () => 'TOTAL' }], 87 | }) 88 | .column('firstName', { key: 'firstName' }) 89 | .column('lastName', { key: 'lastName' }) 90 | .column('email', { key: 'email' }) 91 | .column('roles', { 92 | key: 'roles', 93 | transform: 'list', 94 | cellStyle: data => ({ font: { color: { rgb: data.roles.includes('admin') ? 'd10808' : undefined } } }), 95 | }) 96 | .column('balance', { 97 | key: 'balance', 98 | format: '"$"#,##0.00_);\\("$"#,##0.00\\)', 99 | summary: [ 100 | { 101 | value: data => data.reduce((acc, user) => acc + user.balance, 0), 102 | format: '"$"#,##0.00_);\\("$"#,##0.00\\)', 103 | }, 104 | { 105 | value: data => data.reduce((acc, user) => acc + user.balance, 0) * 1.2, 106 | format: '"$"#,##0.00_);\\("$"#,##0.00\\)', 107 | }, 108 | ], 109 | }) 110 | .column('nbOrgs', { key: 'organizations', transform: 'arrayLength' }) 111 | .column('orgs', { key: 'organizations', transform: org => org.map(org => org.name).join(', ') }) 112 | .column('generalScore', { 113 | key: 'results.general.overall', 114 | format: '# / 10', 115 | summary: [{ 116 | value: data => data.reduce((acc, user) => acc + user.results.general.overall, 0) / data.length, 117 | format: '# / 10', 118 | }], 119 | }) 120 | .column('technicalScore', { 121 | key: 'results.technical.overall', 122 | summary: [{ 123 | value: data => data.reduce((acc, user) => acc + user.results.technical.overall, 0) / data.length, 124 | }], 125 | }) 126 | .column('interviewScore', { key: 'results.interview.overall', default: 'N/A' }) 127 | .column('createdAt', { key: 'createdAt', format: 'd mmm yyyy' }) 128 | .group('group:org', (builder, context: Organization[]) => { 129 | for (const org of context) { 130 | builder 131 | .column(`orga-${org.id}`, { 132 | label: `User in ${org.name}`, 133 | key: 'organizations', 134 | transform: orgs => orgs.some(o => o.id === org.id) ? 'YES' : 'NO', 135 | cellStyle: data => ({ 136 | font: { 137 | color: { rgb: data.organizations.some(o => o.id === org.id) ? '61eb34' : 'd10808' }, 138 | }, 139 | }), 140 | }) 141 | } 142 | }) 143 | .build() 144 | ``` 145 | 146 | #### 3. Safely compose excel file from schemas 147 | 148 | ```ts 149 | import { ExcelBuilder } from '@chronicstone/typed-xlsx' 150 | 151 | const buffer = ExcelBuilder 152 | .create() 153 | .sheet('Users - full') 154 | .addTable({ 155 | data: users, 156 | schema: assessmentExport, 157 | context: { 158 | 'group:org': organizations, 159 | }, 160 | }) 161 | .sheet('Users - partial') 162 | .addTable({ 163 | data: users, 164 | schema: assessmentExport, 165 | select: { 166 | firstName: true, 167 | lastName: true, 168 | email: true, 169 | }, 170 | }) 171 | .sheet('User - neg partial') 172 | .addTable({ 173 | data: users, 174 | schema: assessmentExport, 175 | select: { 176 | firstName: false, 177 | lastName: false, 178 | email: false, 179 | }, 180 | context: { 181 | 'group:org': organizations, 182 | }, 183 | }) 184 | .sheet('User - Multiple tables') 185 | .sheet('Multi-tables-grid', { tablesPerRow: 2 }) 186 | .addTable({ 187 | title: 'Table 1', 188 | data: users.filter((_, i) => i < 5), 189 | schema: assessmentExport, 190 | select: { firstName: true, lastName: true, email: true, createdAt: true }, 191 | }) 192 | .addTable({ 193 | title: 'Table 2', 194 | data: users.filter((_, i) => i < 5), 195 | schema: assessmentExport, 196 | select: { firstName: true, lastName: true, email: true, balance: true }, 197 | }) 198 | .addTable({ 199 | title: 'Table 3', 200 | data: users.filter((_, i) => i < 5), 201 | schema: assessmentExport, 202 | select: { firstName: true, lastName: true, email: true, balance: true }, 203 | }) 204 | .addTable({ 205 | title: 'Table 4', 206 | data: users.filter((_, i) => i < 5), 207 | schema: assessmentExport, 208 | select: { firstName: true, lastName: true, email: true, createdAt: true }, 209 | }) 210 | .build({ output: 'buffer' }) 211 | 212 | fs.writeFileSync('test.xlsx', arrayBuffer) 213 | ``` 214 | 215 | #### 4. Have fun 216 | 217 | Here's the generated file for the example from above 218 | 219 | [DOWNLOAD GENERATED EXAMPLE](https://github.com/ChronicStone/typed-xlsx/blob/main/example.xlsx) 220 | 221 | [OPEN EXAMPLE IN STACKBLITZ](https://stackblitz.com/edit/typescript-cvt29j?file=index.ts) 222 | 223 | 224 | ## License 225 | 226 | [MIT](./LICENSE) License © 2023-PRESENT [Cyprien THAO](https://github.com/ChronicStone) 227 | 228 | 229 | 230 | 231 | [npm-version-src]: https://img.shields.io/npm/v/@chronicstone/typed-xlsx?style=flat&colorA=080f12&colorB=1fa669 232 | [npm-version-href]: https://npmjs.com/package/@chronicstone/typed-xlsx 233 | [npm-downloads-src]: https://img.shields.io/npm/dm/@chronicstone/typed-xlsx?style=flat&colorA=080f12&colorB=1fa669 234 | [npm-downloads-href]: https://npmjs.com/package/@chronicstone/typed-xlsx 235 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@chronicstone/typed-xlsx?style=flat&colorA=080f12&colorB=1fa669&label=minzip 236 | [bundle-href]: https://bundlephobia.com/result?p=@chronicstone/typed-xlsx 237 | [license-src]: https://img.shields.io/github/license/ChronicStone/typed-xlsx.svg?style=flat&colorA=080f12&colorB=1fa669 238 | [license-href]: https://github.com/ChronicStone/typed-xlsx/blob/main/LICENSE 239 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 240 | [jsdocs-href]: https://www.jsdocs.io/package/@chronicstone/typed-xlsx 241 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | ], 7 | declaration: true, 8 | clean: true, 9 | rollup: { 10 | emitCJS: true, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChronicStone/typed-xlsx/bc387ebdc5c9cd0bb86f53b9cf6ed97be3a6419e/bun.lockb -------------------------------------------------------------------------------- /docs/.examples/financial-report/data.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker' 2 | 3 | export interface FinancialReport { 4 | month: string 5 | departments: DepartmentData[] 6 | totalRevenue: number 7 | totalExpenses: number 8 | totalProfit: number 9 | averageProfitMargin: number 10 | operatingCashFlow: number 11 | investingCashFlow: number 12 | financingCashFlow: number 13 | grossMargin: number 14 | EBIT: number 15 | debtToEquityRatio: number 16 | ROI: number 17 | YoYGrowth: number 18 | } 19 | 20 | export interface DepartmentData { 21 | name: string 22 | revenue: number 23 | targetRevenue: number 24 | expenses: number 25 | profit: number 26 | profitMargin: number 27 | revenueAchievement: number 28 | growth: number 29 | COGS: number // New field for Cost of Goods Sold 30 | } 31 | 32 | export function generateFinancialReportData(months: number, departmentsPerMonth: number): FinancialReport[] { 33 | const departments = ['Sales', 'Marketing', 'R&D', 'Customer Support', 'Human Resources'] 34 | 35 | return Array.from({ length: months }, () => { 36 | const month = faker.date.between({ from: '2023-01-01', to: '2023-12-31' }).toISOString().slice(0, 7) 37 | let totalRevenue = 0 38 | let totalExpenses = 0 39 | let totalProfit = 0 40 | let totalProfitMargin = 0 41 | let totalCOGS = 0 42 | 43 | const departmentData = Array.from({ length: departmentsPerMonth }, () => { 44 | const name = faker.helpers.arrayElement(departments) 45 | const targetRevenue = faker.number.int({ min: 20000, max: 120000 }) 46 | const revenue = faker.number.int({ min: 10000, max: 100000 }) 47 | const expenses = faker.number.int({ min: 5000, max: 50000 }) 48 | const profit = revenue - expenses 49 | const COGS = faker.number.int({ min: 2000, max: 40000 }) 50 | const profitMargin = Number.parseFloat(((profit / revenue) * 100).toFixed(2)) 51 | 52 | totalRevenue += revenue 53 | totalExpenses += expenses 54 | totalProfit += profit 55 | totalProfitMargin += profitMargin 56 | totalCOGS += COGS 57 | 58 | return { 59 | name, 60 | revenue, 61 | targetRevenue, 62 | expenses, 63 | profit, 64 | profitMargin, 65 | revenueAchievement: Number.parseFloat(((revenue / targetRevenue) * 100).toFixed(2)), 66 | growth: revenue - targetRevenue, 67 | COGS, 68 | } 69 | }) 70 | 71 | const grossMargin = totalRevenue > 0 ? Number.parseFloat(((totalRevenue - totalCOGS) / totalRevenue * 100).toFixed(2)) : 0 72 | const averageProfitMargin = departmentsPerMonth > 0 ? totalProfitMargin / departmentsPerMonth : 0 73 | 74 | return { 75 | month, 76 | departments: departmentData, 77 | totalRevenue, 78 | totalExpenses, 79 | totalProfit, 80 | averageProfitMargin, 81 | operatingCashFlow: faker.number.int({ min: 1000, max: 30000 }), 82 | investingCashFlow: faker.number.int({ min: -20000, max: 10000 }), 83 | financingCashFlow: faker.number.int({ min: -10000, max: 5000 }), 84 | grossMargin, 85 | EBIT: totalProfit - faker.number.int({ min: 1000, max: 5000 }), 86 | debtToEquityRatio: Number.parseFloat(faker.finance.amount({ min: 2000, max: 40000, dec: 2, symbol: '%', autoFormat: true })), 87 | ROI: Number.parseFloat(faker.finance.amount({ min: 2000, max: 40000, dec: 2, symbol: '%', autoFormat: true })), 88 | YoYGrowth: Number.parseFloat(faker.finance.amount({ min: -5000, max: 5000, dec: 2, symbol: '%', autoFormat: true })), 89 | } 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /docs/.examples/financial-report/file.ts: -------------------------------------------------------------------------------- 1 | import { ExcelBuilder } from '@chronicstone/typed-xlsx' 2 | import { generateFinancialReportData } from './data' 3 | import { financialReportSchema } from './schema' 4 | 5 | export const financialReportExcel = ExcelBuilder.create() 6 | .sheet('Financial Report | Full') 7 | .addTable({ 8 | data: generateFinancialReportData(10, 3), 9 | schema: financialReportSchema, 10 | }) 11 | .build({ output: 'buffer' }) 12 | -------------------------------------------------------------------------------- /docs/.examples/financial-report/schema.ts: -------------------------------------------------------------------------------- 1 | import { ExcelSchemaBuilder } from '@chronicstone/typed-xlsx' 2 | import type { FinancialReport } from './data' 3 | 4 | export const financialReportSchema = ExcelSchemaBuilder.create() 5 | .column('month', { key: 'month', label: 'Month', format: 'MMM YYYY' }) 6 | .column('Department Name', { 7 | key: 'departments', 8 | label: 'Department', 9 | transform: departments => departments.map(d => d.name), 10 | }) 11 | .column('Revenue', { 12 | key: 'departments', 13 | label: 'Revenue', 14 | transform: departments => departments.map(d => d.revenue), 15 | format: '$#,##0.00', 16 | }) 17 | .column('Expenses', { 18 | key: 'departments', 19 | label: 'Expenses', 20 | transform: departments => departments.map(d => d.expenses), 21 | format: '$#,##0.00', 22 | }) 23 | .column('Profit', { 24 | key: 'departments', 25 | label: 'Profit', 26 | transform: departments => departments.map(d => d.profit), 27 | format: '$#,##0.00', 28 | cellStyle: (data, _, valueIndex) => ({ 29 | font: { color: { rgb: data.departments[valueIndex].profit >= 0 ? '007500' : 'FF0000' } }, 30 | }), 31 | }) 32 | .column('Profit Margin', { 33 | key: 'departments', 34 | label: 'Profit Margin', 35 | transform: departments => departments.map(d => `${d.profitMargin}%`), 36 | cellStyle: (data, _, valueIndex) => ({ 37 | font: { color: { rgb: data.departments[valueIndex].profitMargin >= 0 ? '007500' : 'FF0000' } }, 38 | }), 39 | }) 40 | .column('totalRevenue', { 41 | key: 'totalRevenue', 42 | label: 'Total Revenue', 43 | format: '$#,##0.00', 44 | summary: [ 45 | { 46 | value: data => data.reduce((acc, item) => acc + item.totalRevenue, 0), 47 | format: () => '$#,##0.00', 48 | }, 49 | ], 50 | }) 51 | .column('totalExpenses', { 52 | key: 'totalExpenses', 53 | label: 'Total Expenses', 54 | format: '$#,##0.00', 55 | summary: [ 56 | { 57 | value: data => data.reduce((acc, item) => acc + item.totalExpenses, 0), 58 | format: () => '$#,##0.00', 59 | }, 60 | ], 61 | }) 62 | .column('totalProfit', { 63 | key: 'totalProfit', 64 | label: 'Total Profit', 65 | format: '$#,##0.00', 66 | summary: [ 67 | { 68 | value: data => data.reduce((acc, item) => acc + item.totalProfit, 0), 69 | format: () => '$#,##0.00', 70 | cellStyle: data => ({ 71 | font: { color: { rgb: data.reduce((acc, item) => acc + item.totalProfit, 0) >= 0 ? '007500' : 'FF0000' } }, 72 | }), 73 | }, 74 | ], 75 | cellStyle: data => ({ 76 | font: { color: { rgb: data.totalProfit >= 0 ? '007500' : 'FF0000' } }, 77 | }), 78 | }) 79 | .column('averageProfitMargin', { 80 | key: 'averageProfitMargin', 81 | label: 'Average Profit Margin', 82 | format: '0.00%', 83 | summary: [ 84 | { 85 | value: data => data.reduce((acc, item) => acc + item.averageProfitMargin, 0) / data.length, 86 | format: () => '0.00%', 87 | cellStyle: data => ({ 88 | font: { color: { rgb: data.reduce((acc, item) => acc + item.averageProfitMargin, 0) / data.length >= 0 ? '007500' : 'FF0000' } }, 89 | }), 90 | }, 91 | ], 92 | cellStyle: data => ({ 93 | font: { color: { rgb: data.averageProfitMargin >= 0 ? '007500' : 'FF0000' } }, 94 | }), 95 | }) 96 | .build() 97 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { transformerTwoslash } from '@shikijs/vitepress-twoslash' 3 | import container from 'markdown-it-container' 4 | import { renderSandbox } from 'vitepress-plugin-sandpack' 5 | import Unocss from 'unocss/vite' 6 | 7 | // https://vitepress.dev/reference/site-config 8 | export default defineConfig({ 9 | title: 'Typed-xlsx', 10 | sitemap: { 11 | hostname: 'https://typed-xlsx.vercel.app', 12 | }, 13 | description: 'Documentation of typed-xlsx library', 14 | markdown: { 15 | theme: { 16 | light: 'github-light', 17 | dark: 'github-dark', 18 | }, 19 | 20 | codeTransformers: [ 21 | transformerTwoslash(), 22 | ], 23 | config(md) { 24 | md 25 | // .use((md) => { 26 | // md 27 | // }) 28 | .use(container, 'sandbox', { 29 | render(tokens, idx) { 30 | return renderSandbox(tokens, idx, 'sandbox') 31 | }, 32 | }) 33 | .use(container, 'code-sandbox', { 34 | render(tokens, idx) { 35 | return renderSandbox(tokens, idx, 'code-sandbox') 36 | }, 37 | }) 38 | }, 39 | }, 40 | lastUpdated: true, 41 | ignoreDeadLinks: true, 42 | cleanUrls: true, 43 | router: { 44 | prefetchLinks: true, 45 | }, 46 | titleTemplate: 'Typed-xlsx | :title', 47 | head: [ 48 | ['meta', { name: 'google-site-verification', content: 'DPVOPrsgdIJ4_xJYhy6Azw6vGw51riJiJoaT7SBTARc' }], 49 | ['link', { rel: 'shortcut icon', href: '/favicon.ico' }], 50 | ['meta', { property: 'og:type', content: 'website' }], 51 | // ['script', { src: 'https://buttons.github.io/buttons.js', defer: 'true', async: 'true' }], 52 | ], 53 | themeConfig: { 54 | logo: '/images/logo.png', 55 | editLink: { 56 | pattern: 'https://github.com/ChronicStone/typed-xlsx/edit/main/docs/:path', 57 | text: 'Edit this page on GitHub', 58 | }, 59 | search: { provider: 'local' }, 60 | nav: [ 61 | { text: 'Home', link: '/' }, 62 | { text: 'Documentation', link: '/getting-started/key-benefits-why' }, 63 | ], 64 | socialLinks: [ 65 | { icon: 'github', link: 'https://github.com/ChronicStone/typed-xlsx' }, 66 | ], 67 | footer: { 68 | message: 'Released under the MIT License.', 69 | copyright: 'Copyright © 2023-present Cyprien THAO', 70 | }, 71 | sidebar: [ 72 | { 73 | text: 'Getting Started', 74 | items: [ 75 | { text: 'Key Benefits & Why', link: '/getting-started/key-benefits-why' }, 76 | { text: 'Installation', link: '/getting-started/installation' }, 77 | ], 78 | }, 79 | { 80 | text: 'Schema Builder', 81 | items: [ 82 | { text: 'Create schema', link: '/schema-builder/create-schema' }, 83 | { text: 'Define columns', link: '/schema-builder/columns' }, 84 | { text: 'Dynamic Columns', link: '/schema-builder/dynamic-columns' }, 85 | { text: 'Reusable Transformers', link: '/schema-builder/reusable-transformers' }, 86 | { text: 'Reusable Formatters', link: '/schema-builder/reusable-formatters' }, 87 | { text: 'Build Schema', link: '/schema-builder/build-schema' }, 88 | ], 89 | }, 90 | { 91 | text: 'File Builder', 92 | items: [ 93 | { text: 'Create file builder', link: '/file-builder/create-file-builder' }, 94 | { text: 'Define Sheets', link: '/file-builder/define-sheets' }, 95 | { text: 'Define Tables', link: '/file-builder/define-tables' }, 96 | { text: 'Build excel file', link: '/file-builder/build-excel-file' }, 97 | ], 98 | }, 99 | ], 100 | }, 101 | vite: { 102 | plugins: [ 103 | Unocss({}), 104 | ], 105 | ssr: { 106 | noExternal: ['xlsx-js-style'], 107 | }, 108 | }, 109 | }) 110 | -------------------------------------------------------------------------------- /docs/.vitepress/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/ban-types */ 2 | 3 | // [INFO] allow to import vue components - https://stackoverflow.com/questions/42002394/importing-vue-components-in-typescript-file 4 | declare module '*.vue' { 5 | import type { DefineComponent } from 'vue' 6 | 7 | const component: DefineComponent<{}, {}, any> 8 | export default component 9 | } 10 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/CodeSandbox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ExampleRenderer.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 |