Skip to content

datasette-paper embed provider + spreadsheet-faithful block preview#3

Draft
asg017 wants to merge 3 commits into
asg017/sheets-screenshotsfrom
paper-embed-0630
Draft

datasette-paper embed provider + spreadsheet-faithful block preview#3
asg017 wants to merge 3 commits into
asg017/sheets-screenshotsfrom
paper-embed-0630

Conversation

@asg017

@asg017 asg017 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Adds a paper_embed_provider so a sheets workbook can be referenced and embedded inside a datasette-paper document — an inline pill (workbook name + table icon) and a read-only block preview — and makes that block preview render like the real spreadsheet rather than a generic HTML table.

Stacked on #2 (asg017/sheets-screenshots). Review/merge that first; this PR is based on it so the diff stays focused on the embed work (3 commits).

Block preview — matches the live grid

The preview now mirrors docs/screenshots/editor.png cell-for-cell: monospace cells, A/B/C column-letter headers + row-number gutter on the grid's blue-grey header fill, right-aligned navy numerals, currency masks, bold header/total rows, and bottom folder-tabs with the per-sheet colour dot.

The real grid (target):

real grid

The embed block preview (this PR):

embed block preview

How it works

  • Provider (datasette_sheets/paper.py, registered in __init__.py): describes the sheets-workbook provider — ref_prefixes, /-menu source, vite bundle URLs — so paper lazy-loads the bundle on demand. Frontend entry frontend/src/pages/paper-embed/main.ts claims /-/sheets/workbook/{db}/{id} refs, resolves the pill, and mounts the block preview, fetching the sheets API with the viewer's cookie (leak discipline: 403 → denied, missing → not_found, no label/data leaked).
  • Structural fidelity is pure CSS mirroring the live grid's --sheet-* palette (paper-embed/style.css).
  • Content fidelity (currency, bold) needs real formatting: the /data endpoint grew an opt-in ?include_format=1 that returns a formats grid parallel to rows (each cell's parsed format_json, None where unformatted; arrays mode only — default response unchanged). The embed renders every cell through the shared formatValue / createDefaultFormat — the same single source of truth the live grid uses — so currency / bold / dates / booleans match exactly. No bundle bloat (formatter.ts pulls only type-only deps; the embed bundle stays ~5 kB).

Known scope limits

  • The preview always shows the first sheet; the tab bar is decorative/read-only.
  • Capped at the top-left 12×8 cells.

Testing

  • tests/test_workbooks.py covers the include_format contract (formats grid present + parallel only when requested, None for unformatted cells). Full Python suite + frontend gate (test/check/lint/format) green.
  • scripts/embed-preview-shot.mjs — dev-only harness (modelled on scripts/screenshots.mjs) that boots the seeded throwaway datasette and screenshots the embed web component standalone, for visual diffing against editor.png. It generated the screenshot above.

🤖 Generated with Claude Code

asg017 and others added 3 commits June 29, 2026 17:53
…ard)

Contribute a `paper_embed_provider` so a sheets workbook can be referenced
and embedded inside a paper document, mirroring datasette-places /
datasette-mendez.

Backend (`datasette_sheets/paper.py`, imported in `__init__.py`): describes
the `sheets-workbook` provider — its `ref_prefixes`, `/`-menu source, and
the vite paper-embed bundle URLs — so paper lazy-loads the bundle on demand.

Frontend (`src/pages/paper-embed/main.ts` + `style.css`, new vite entry):
the default-exported provider claims `/-/sheets/workbook/{db}/{id}` refs,
resolves the inline pill (workbook name + table icon) and mounts a read-only
block preview (sheet tabs + first sheet's top-left cells) fetched from the
sheets API with the viewer's cookie. Cross-database picker `search()`
enumerates `/.json` then each db's workbooks API. Leak discipline: 403 ->
denied, missing -> not_found, no label/data leaked.

Workbook pages are database-scoped, so the stored ref normalizes the db
behind the fixed prefix (`/-/sheets/workbook/{db}/{id}`) to keep paper's
prefix-based lazy-load working; `matchUrl` maps the real page URL onto it.

vite.config.ts: `preserveEntrySignatures: "strict"` so Rollup keeps the
entry's `export default` — paper `import()`s the bundle and reads the default
export to register the provider; without it the export is tree-shaken away
and every sheets ref renders as "not found".

pyproject.toml: `datasette-sheets[paper]` extra pins datasette-paper>=0.0.2a3
via a [tool.uv.sources] path to the built wheel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ikxoxihTC15haHeknRcgg
The datasette-paper block preview rendered a sheets workbook as a plain
sans-serif HTML table with raw values and top underline-tabs — it read as
a generic table, not a spreadsheet. Bring it to visual parity with the
live grid (docs/screenshots/editor.png).

Styling (paper-embed/style.css): mirror the grid's `--sheet-*` palette —
"Courier New" monospace cells, the #f0f3f5 blue-grey header fill on the
A/B/C column-letter row + row-number gutter, right-aligned accent (#276890)
numerals, and bottom folder-style sheet tabs with the per-sheet colour dot.

Content fidelity (currency masks + bold): the /data endpoint only returned
raw computed values, so the preview couldn't show $4,200 or bold header /
total rows. data.py grows an opt-in `?include_format=1` that adds a
`formats` grid parallel to `rows` (each cell's parsed format_json, None
where unformatted; arrays mode only, default response unchanged). The embed
requests it and renders every cell through the shared formatValue /
createDefaultFormat — the same single source of truth the live grid uses —
so currency / bold / dates / booleans match cell-for-cell. The formatter
import adds no bundle bloat (formatter.ts pulls only type-only deps).

tests/test_workbooks.py: cover the include_format contract (formats grid
present + parallel only when requested, None for unformatted cells).

scripts/embed-preview-shot.mjs: dev-only harness (modelled on
scripts/screenshots.mjs) that boots the seeded throwaway datasette and
screenshots the embed web component standalone, for visual diffing against
editor.png while iterating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
embed-preview-shot.mjs now clips to the block card (#embed-card) instead
of the full padded page, so docs/screenshots/paper-embed.png is a tight,
README/PR-ready asset mirroring docs/screenshots/editor.png.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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