Skip to content

feat(xlsx): @gridsheet/xlsx — xlsx ⇄ GridSheet converter - #147

Merged
righ merged 11 commits into
masterfrom
righ/xlsx-converter-plugin
Sep 28, 2026
Merged

righ merged 11 commits into
masterfrom
righ/xlsx-converter-plugin

Conversation

@righ

@righ righ commented Sep 28, 2026

Copy link
Copy Markdown
Member

What

New headless package @gridsheet/xlsx that converts between .xlsx and GridSheet — a TS-native OOXML reader/writer scoped to what CellType represents, using fflate for zip. It does not wrap exceljs/SheetJS (both effectively unmaintained); exceljs was used only as a reference for the OOXML shapes.

API

import { fromXlsx, toXlsx } from '@gridsheet/xlsx';
import { buildInitialCells } from '@gridsheet/react-core';

const parsed = fromXlsx(bytes);              // { [sheet]: { matrices, cells } }
<GridSheet initialCells={buildInitialCells(parsed.Sheet1)} />

const out = toXlsx({ Sheet1: sheetRef.current });  // Uint8Array

Supported (both directions)

  • Values (string / number / boolean) and formulas as their =... text, including cross-sheet references (=SUM(Sales!D3:D5), quoted names like 'Meta Data'!B3).
  • Multiple sheets, in workbook order.
  • Cell styles ↔ GridSheet style: background, text color, bold, italic, underline, and horizontal/vertical alignment (justifyContent/alignItems).
  • Column widths / row heights ↔ header cells (ch(col)/rh(row)), px↔xlsx units (sizes written when the input is a live sheet).
  • Dates import as their raw Excel serial (no number-format support yet); exported values write as ISO strings.

Unsupported features (merged cells, number formats, borders, charts/images) are ignored gracefully on import — a styled, merged, multi-sheet workbook reads fine.

Tests / verification

  • Jest (30): A1 helpers, a minimal XML parser, toXlsx↔fromXlsx round trips (types, formulas, escaping, shared-string dedup, sparse), a hand-built workbook exercising inlineStr / cached formulas, a complex openpyxl-written fixture (styles, merges, sizes, cross-sheet), and engine-integration specs (cross-sheet re-evaluation, style/size round trip).
  • Interop verified both ways against openpyxl (an independent OOXML implementation): our output opens cleanly (fill/font/alignment intact) and we read openpyxl's output correctly.
  • Storybook IO/Xlsx demo (ships a complex sample.xlsx; import a file / download) + Playwright e2e (e2e/xlsx.spec.ts) asserting imported styles render and cross-sheet formulas re-evaluate live.

Wiring

Root scripts (build:xlsx / jest:xlsx / typecheck:xlsx and the :all aggregates), a dependency-ordered upload-xlsx publish job in release.yaml, and AGENTS.md package table / testing policy.

Notes

  • npm publish prerequisite: @gridsheet/xlsx must be registered as an npm OIDC trusted publisher before the upload-xlsx job can publish it (the job is || true, so it won't fail the release until then).
  • This branch also carried the fixed-height sheet-resize fix, which already shipped in 3.4.4; it is therefore absent from this diff (already on master).

🤖 Generated with Claude Code

righ and others added 11 commits September 28, 2026 23:53
New headless package converting xlsx <-> GridSheet without exceljs/SheetJS:
a TS-native OOXML reader/writer scoped to what CellType uses, with fflate for
zip. v0 covers values (string/number/boolean), formulas round-tripping as their
"=..." text, multiple sheets, and dates as ISO strings.

- fromXlsx(bytes) -> { [sheet]: { matrices } } (buildInitialCells input shape)
- toXlsx(sheets) -> Uint8Array (accepts a live sheet, cell matrix, or values)
- Jest: a1 / xml units + toXlsx<->fromXlsx round trip + engine-sheet integration
- Verified interop both ways against openpyxl (independent OOXML reader)
- Storybook IO/Xlsx demo (ships sample.xlsx; import/download) + e2e spec
- Wire root scripts, release publish job, and AGENTS.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the sample with an openpyxl-written workbook exercising features v0 does
not support (merged cells, fonts/fills/borders, number formats, frozen panes,
multiple sheets) to prove graceful degradation: styling/merges are ignored,
values + formulas still import (merged range keeps its top-left value; a
formatted date reads as its raw serial number).

- src/__fixtures__/complex.xlsx + complex.spec.ts (Jest robustness)
- Storybook sample.xlsx swapped for the complex one; description + e2e updated
- README notes the graceful-degradation behavior

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the styled fixture with a `Summary` sheet whose formulas reference other
sheets — `=SUM(Sales!D3:D5)`, `=Sales!D3`, and `=Sales!D6*'Meta Data'!B3` (a
sheet name with a space, so quoting is exercised). Renames Meta -> "Meta Data".

- fromXlsx preserves cross-sheet formula text verbatim (complex.spec.ts)
- engine integration: importing the workbook into one shared registry and
  giving each sheet a unique id resolves the cross-sheet formulas
  (Summary B1=17, B2=4.5, B3~=2.601) (sheet-integration.spec.ts)
- Storybook demo now renders every imported sheet in a shared book, so
  cross-sheet references re-evaluate live; e2e asserts Summary!B2 = 17
- add @types/node so the fixture-reading specs typecheck

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-sheet formulas are easier to inspect when each sheet shows its formula bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse xl/styles.xml (fonts, solid fills, cellXfs + alignment) and map each
cell's style record to GridSheet's `style` (backgroundColor, color, fontWeight,
fontStyle, textDecoration) plus justifyContent/alignItems. Colors resolve from
rgb, the legacy indexed palette, and default theme colors with tint.

fromXlsx now returns { matrices, cells } per sheet; cells carries the imported
styles and buildInitialCells merges them with the values. Writing styles back
out is still TODO (toXlsx remains values + formulas).

- styles.ts + StyleTable; read.ts collects per-cell styles
- complex.spec.ts asserts title/header/zebra/alignment styles
- Storybook demo passes the imported cells so colors render; e2e asserts the
  dark fill + white bold title paint in the browser
- README documents style import and the export gap

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read explicit <cols customWidth> widths and <row customHeight> heights and map
them (converted to pixels) onto the column-/row-header cells (ch(col)/rh(row)),
so imported sheets keep their sizing. Sheet-wide col spans are skipped to avoid
creating thousands of header cells.

- read.ts parses cols/rows; complex fixture varies widths/heights
- complex.spec.ts asserts the imported px sizes; e2e checks a wide vs narrow column
- README updated

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A matrix-aligned, fixed-height GridSheet put the CSS `resize` handle on `.gs-main`
while also capping it with `max-height: sheetHeight`. A box can't be dragged past
its own max-height, so enlarging was silently blocked (shrinking still worked).
Switching to a controlled `height` doesn't help — React overwrites it on every
re-render, fighting the drag.

Fix: for a user-resizable fixed-height box, leave the height uncontrolled. Seed the
initial pixel height in a layout effect (capped at content height so short grids
still shrink to content), drop the max-height cap, and let the ResizeObserver sync
drags into sheetHeight without clamping a manual resize to the root height.

- e2e/sheet-resize.spec.ts guards: no max-height cap, and an enlarged height sticks

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
toXlsx now writes what fromXlsx reads: cell styles (background, text color,
bold/italic/underline, horizontal/vertical alignment) into a built styles.xml,
and — for a live GridSheet sheet — column widths (<cols>) and row heights
(<row ht>). Styles are interned so each distinct font/fill/alignment gets one xf;
fills reserve the Excel none/gray125 indices. Completes the style/size round trip.

- style_writer.ts: StyleSheetBuilder + CSS-color → ARGB
- write.ts: per-cell `s`, <cols>/<row ht>, dynamic styles.xml
- sheet-integration.spec.ts: styles + header sizes survive toXlsx → fromXlsx
- verified the styled output opens in openpyxl (bold/fill/color/align intact)
- README: styles and sizes are now both-directions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships under the npm 'next' dist-tag (the release workflow publishes any version
containing '-' with --tag next), keeping it off 'latest' while it stabilizes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@righ
righ merged commit f7f6b0f into master Sep 28, 2026
4 checks passed
@righ
righ deleted the righ/xlsx-converter-plugin branch September 28, 2026 19:39
righ added a commit that referenced this pull request Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant