Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,4 @@ jobs:
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
ignore-workspace-root-check=true
# vite-plus preview build registry bridge (auto-added by vp)
registry=https://registry-bridge.viteplus.dev/
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { createExcelSchema, createWorkbook } from "typed-xlsx";
- One schema API for buffered exports and streaming exports
- Report mode for custom layouts, summaries, and sub-row expansion
- Excel table mode for native tables, totals rows, autoFilter, and structured refs
- Renderer columns for badges, checkboxes, hyperlinks, sparklines, and embedded or URL-backed images
- Full comparison against SheetJS and ExcelJS: [Why typed-xlsx?](https://typed-xlsx.vercel.app/getting-started/comparison)

[![Executive Board Pack](./apps/docs/public/images/marketing/board-overview.png)](https://typed-xlsx.vercel.app/playground/executive-board-pack)
Expand All @@ -41,7 +42,7 @@ Most XLSX libraries give you a cell API. `typed-xlsx` gives you a schema API.
- Reference columns by ID in formulas instead of hard-coding fragile cell addresses
- Reuse one schema across buffered exports and streaming exports
- Choose between report mode and native Excel table mode without changing the authoring model
- Generate polished workbooks with summaries, grouped headers, sub-row expansion, validation, and conditional styles
- Generate polished workbooks with summaries, grouped headers, sub-row expansion, validation, conditional styles, badges, checkboxes, hyperlinks, sparklines, and product thumbnails

If you are currently evaluating `typed-xlsx` against lower-level spreadsheet libraries, read the full comparison with SheetJS and ExcelJS:

Expand Down Expand Up @@ -74,15 +75,15 @@ Read the full tradeoffs and feature matrix here:
Reach for a lower-level spreadsheet library when you need to:

- read or modify existing `.xlsx` files
- embed charts or worksheet images
- embed charts or arbitrary freeform worksheet drawings
- support spreadsheet formats beyond `.xlsx`
- do highly manual cell-by-cell spreadsheet editing

## The Core API

Three functions cover the main surface:

- `createExcelSchema()` describes columns, formulas, summaries, styles, groups, and validation
- `createExcelSchema()` describes columns, formulas, summaries, styles, groups, validation, badges, checkboxes, hyperlinks, sparklines, and image renderers
- `createWorkbook()` builds buffered workbooks for small and medium exports
- `createWorkbookStream()` commits row batches for large exports with much flatter memory usage

Expand Down Expand Up @@ -155,7 +156,13 @@ For the detailed tradeoffs, feature matrix, and cases where the alternatives sti

## Real Workbook Outputs

Each screenshot links to the full artifact in the playground.
Each example links to the full artifact in the playground.

### Kitchen sink feature tour

Open one workbook with a feature map and one named sheet per major library primitive.

Example: [Kitchen Sink Feature Tour](https://typed-xlsx.vercel.app/playground/kitchen-sink)

### Board-ready multi-sheet reporting

Expand All @@ -175,6 +182,12 @@ The schema stays the same while the builder switches to batch commits.

[![Streaming Fulfillment Export](./apps/docs/public/images/marketing/streaming-fulfillment.png)](https://typed-xlsx.vercel.app/playground/streaming-fulfillment-export)

### Product catalogs with media

Embed product thumbnails, use URL-backed image formulas, and combine badge, checkbox, hyperlink, and sparkline renderer columns.

Example: [Product Media Catalog](https://typed-xlsx.vercel.app/playground/product-media-catalog)

### Editable workflow-safe workbooks

Unlock inputs, validate user edits, and keep logic columns protected.
Expand Down
51 changes: 37 additions & 14 deletions apps/docs/app/components/content/LandingHomepage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,30 @@ const schema = createExcelSchema<Invoice>()
style: { numFmt: "$#,##0.00" },
summary: (s) => [s.formula("sum")],
})
// TypeScript fails if you reference a later column
// Badge renderer — value-to-style mapping declared on the column
.column("status", {
type: "badge",
accessor: "status",
style: (row) => ({
font: {
bold: row.status === "overdue",
color: {
rgb: row.status === "paid" ? "166534" : "B42318",
variants: {
paid: {
style: {
fill: { color: { rgb: "DCFCE7" } },
font: { color: { rgb: "166534" }, bold: true },
},
},
}),
overdue: {
style: {
fill: { color: { rgb: "FEE2E2" } },
font: { color: { rgb: "B42318" }, bold: true },
},
},
pending: {
style: {
fill: { color: { rgb: "FEF3C7" } },
font: { color: { rgb: "92400E" } },
},
},
},
})
.build();

Expand All @@ -67,10 +80,10 @@ const stats = [
sub: "Commit-based exports stay flat enough for real large datasets",
},
{
value: "13",
value: "14",
unit: "",
label: "Hard-case examples",
sub: "Real SheetJS-to-schema rewrites for formulas, summaries, theming, and more",
label: "Kitchen-sink tabs",
sub: "One playground workbook walks feature-by-feature through the library",
},
{ value: "4", unit: "", label: "Output targets", sub: "File, Buffer, Node stream & Web stream" },
] as const;
Expand Down Expand Up @@ -152,6 +165,12 @@ const valueProps = [
description:
"Generate columns from runtime data with fully inferred context. Missing or mistyped group context is a compile-time error, not a runtime surprise.",
},
{
icon: "i-lucide-image",
title: "Renderer columns",
description:
"Turn typed values into badges, checkboxes, hyperlinks, sparklines, and media columns without dropping into low-level worksheet object code.",
},
{
icon: "i-lucide-zap",
title: "Streaming pipeline",
Expand Down Expand Up @@ -259,14 +278,14 @@ const routeCards = [
title: "Explore the schema API",
description:
"Columns, formulas, groups, sub-rows, styling, and validation — the full schema surface explained.",
to: "/schema-builder/defining-columns",
to: "/columns/defining-columns",
icon: "i-lucide-layers",
cta: "Schema builder",
},
{
title: "Compare typed-xlsx / hucre / others",
title: "Compare typed-xlsx / others",
description:
"See when typed-xlsx wins on advanced typed reporting, and when hucre's broader spreadsheet engine is the better fit.",
"See when typed-xlsx wins on advanced typed reporting, and when lower-level spreadsheet libraries are the better fit.",
to: "/getting-started/comparison",
icon: "i-lucide-git-compare-arrows",
cta: "Library comparison",
Expand Down Expand Up @@ -576,8 +595,12 @@ useReveal(rootEl);
<h2
class="text-balance text-3xl font-bold tracking-tight text-highlighted sm:text-4xl lg:text-5xl"
>
Real outputs from<br /><em class="not-italic text-primary">real schemas.</em>
A feature tour plus<br /><em class="not-italic text-primary">real workbook outputs.</em>
</h2>
<p class="max-w-xl text-pretty text-base leading-7 text-toned">
Start with the kitchen sink to inspect each primitive one tab at a time, then jump into
real-world exports for product catalogs, board packs, renewals, quotes, and scale.
</p>
</div>
<div class="flex items-end">
<UButton
Expand Down
133 changes: 130 additions & 3 deletions apps/docs/app/components/content/LandingValueCarousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,129 @@ for (const order of orders) {

// Array accessors expand the logical row automatically.
// Single-value columns are merged for you.`,
},
{
id: "sparklines",
eyebrow: "Sparkline columns",
title: "Native Excel mini charts, one schema field",
body: "Declare a sparkline column against typed source columns and every row gets a live Excel chart — line, column, or win/loss. Sources are type-checked like formula refs.",
docsPath: "/columns/sparklines",
beforeCode:
"// SheetJS / ExcelJS: no sparkline API at all.\n" +
"// The only route is post-processing the OOXML by hand:\n" +
"// 1. unzip the generated .xlsx\n" +
"// 2. inject <x14:sparklineGroups> into sheet1.xml\n" +
"// 3. compute source/target refs per row yourself\n" +
"// 4. re-zip and hope Excel accepts it\n\n" +
'sheetXml = sheetXml.replace("</worksheet>", `\n' +
' <extLst><ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}">\n' +
" <x14:sparklineGroups>\n" +
" <x14:sparkline><xm:f>B2:D2</xm:f><xm:sqref>E2</xm:sqref></x14:sparkline>\n" +
" <!-- ...one entry per row... -->\n" +
" </x14:sparklineGroups></ext></extLst></worksheet>`);\n\n" +
"// In practice: the feature just gets dropped.",
afterCode: `.column("jan", { accessor: "jan" })
.column("feb", { accessor: "feb" })
.column("mar", { accessor: "mar" })
.column("trend", {
type: "sparkline",
header: "Trend",
source: { from: "jan", to: "mar" },
sparklineType: "line",
style: {
line: { color: "#2563EB", weight: 1.25 },
high: { color: "#16A34A" },
low: { color: "#DC2626" },
},
});

// Source columns are type-checked, refs resolve per row.
// Works in buffered and streaming exports.`,
},
{
id: "badge-checkbox",
eyebrow: "Badges & checkboxes",
title: "Status colors and boolean glyphs live in the schema",
body: "Map values to styled badge variants and render booleans as checkbox glyphs without losing formula-friendly truthy/falsey cells.",
docsPath: "/columns/badges-and-checkboxes",
beforeCode:
"// SheetJS: status styling is a lookup table + per-cell writes\n" +
"const statusStyles = {\n" +
' Live: { fill: "DCFCE7", font: "166534" },\n' +
' "Low stock": { fill: "FEF3C7", font: "92400E" },\n' +
"};\n\n" +
"for (let i = 0; i < rows.length; i++) {\n" +
" const s = statusStyles[rows[i].status];\n" +
" if (s) ws[`C${i + 2}`].s = {\n" +
" fill: { fgColor: { rgb: s.fill } },\n" +
" font: { color: { rgb: s.font }, bold: true },\n" +
" };\n" +
' ws[`D${i + 2}`] = { v: rows[i].done ? "☑" : "☐", t: "s" };\n' +
"}\n\n" +
"// Unknown status? Unstyled cell, no fallback.\n" +
"// Mapping logic copy-pasted into every export.",
afterCode: `.column("status", {
type: "badge",
accessor: "status",
variants: {
Live: {
style: {
fill: { color: { rgb: "DCFCE7" } },
font: { color: { rgb: "166534" }, bold: true },
},
},
"Low stock": {
style: {
fill: { color: { rgb: "FEF3C7" } },
font: { color: { rgb: "92400E" }, bold: true },
},
},
},
defaultVariant: {
style: { fill: { color: { rgb: "F1F5F9" } } },
},
})
.column("done", { type: "checkbox", accessor: "done" });

// Status mapping is declared once, typed against T.
// Checkbox cells display glyphs but store 1/0 for formulas.`,
},
{
id: "image-columns",
eyebrow: "Image columns",
title: "Embedded thumbnails or IMAGE() formulas per column",
body: "Point a column at image bytes or a URL. Drawing parts, anchors, and byte-level deduplication are handled — or skip embedding entirely with Excel's IMAGE() formula.",
docsPath: "/columns/images",
beforeCode:
"// ExcelJS: images are workbook-level objects + anchor math\n" +
"const imgId = workbook.addImage({\n" +
' buffer: pngBytes, extension: "png",\n' +
"});\n\n" +
"worksheet.addImage(imgId, {\n" +
" tl: { col: 0.15, row: rowIndex + 0.2 },\n" +
" ext: { width: 56, height: 56 },\n" +
"});\n" +
"worksheet.getRow(rowIndex + 1).height = 44;\n\n" +
"// One registration + anchor per cell, offsets tuned by hand.\n" +
"// URL-backed IMAGE() formulas? Not supported.",
afterCode: `// Embedded: bytes stored in the workbook, deduplicated
.column("thumbnail", {
type: "image",
accessor: "thumbnail",
alt: "name",
size: { width: 56, height: 56 },
padding: 3,
})

// Or URL-backed: Excel's IMAGE() formula, no bytes embedded
.column("preview", {
type: "image",
source: "url",
accessor: "thumbnailUrl",
});

// Anchors, drawing parts, and media dedup are generated.
// Identical bytes across rows are embedded once.`,
},
{
id: "excel-table-mode",
Expand Down Expand Up @@ -642,7 +765,7 @@ onBeforeUnmount(() => {
<div class="border-t border-default/40 bg-elevated/5 px-5 py-4 sm:px-6 sm:py-5">
<div class="mb-4 flex items-center justify-between gap-3">
<p class="font-mono text-[10px] uppercase tracking-[0.18em] text-toned/60">
13 SheetJS-to-schema examples
{{ stories.length }} before / after examples
</p>
<div class="flex items-center gap-2">
<UButton
Expand All @@ -662,7 +785,12 @@ onBeforeUnmount(() => {
</div>
</div>

<div class="dash-track" role="tablist" aria-label="Feature examples">
<div
class="dash-track"
role="tablist"
aria-label="Feature examples"
:style="{ gridTemplateColumns: `repeat(${stories.length}, minmax(0, 1fr))` }"
>
<button
v-for="(story, i) in stories"
:key="story.id"
Expand Down Expand Up @@ -769,7 +897,6 @@ onBeforeUnmount(() => {

.dash-track {
display: grid;
grid-template-columns: repeat(13, minmax(0, 1fr));
align-items: center;
gap: 0.5rem;
}
Expand Down
17 changes: 12 additions & 5 deletions apps/docs/app/data/artifactCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,18 @@ function inferKindFromArtifact(artifact: GeneratedExamplesArtifact): ArtifactKin
return "buffered";
}

export const artifactCatalog = getGeneratedExampleArtifacts().map((artifact) => ({
...artifact,
kind: inferKindFromArtifact(artifact),
inspectSummary: inspectSummaryById[artifact.id],
})) satisfies ArtifactCatalogEntry[];
export const artifactCatalog = getGeneratedExampleArtifacts()
.map((artifact) => ({
...artifact,
kind: inferKindFromArtifact(artifact),
inspectSummary: inspectSummaryById[artifact.id],
}))
.sort((left, right) => {
if (left.id === "kitchen-sink") return -1;
if (right.id === "kitchen-sink") return 1;

return left.title.localeCompare(right.title);
}) satisfies ArtifactCatalogEntry[];

export function getArtifactPreviewKind(artifact: ArtifactCatalogEntry) {
return artifact.preview.kind;
Expand Down
Loading
Loading