@rarexlabs/gensheets
v0.3.0
Published
Printable / presentable resource-page primitives for React (Sheet, ResourceTopBar, PresentMode, Section, CheckBullet, Shot) plus a scaffolding + PDF export CLI.
Downloads
43
Readme
@rarexlabs/gensheets
Printable, presentable, PDF-exportable resource pages for React — build one-off docs and decks (pitch decks, one-pagers, workflow sheets, spec sheets, whatever) as ordinary React components, styled with your app's own Tailwind tokens, with print/PDF/present-mode handled for you.
Framework-agnostic: works in any React 18+ app (Next.js App Router, Vite, Remix, plain SPA). Zero dependency on Next.js.
Key features
- Two page shapes —
doc(portrait, US Letter) anddeck(landscape slides), both built from the sameSheetprimitive - Print & PDF out of the box — a print stylesheet keyed off the
Sheetmarkup, plus a Playwright-based generator/CLI that renders any URL to PDF - Fullscreen present mode —
PresentModeturns a deck into a keyboard-navigable, laser-pointer-enabled slideshow, no extra markup needed - A small shared component set —
Section,CheckBullet,Shotcover the recurring content patterns (titled sections, feature lists, screenshots) so pages don't reinvent them - Scaffolding CLI —
npx gensheets create doc|deckgenerates a new page from a template instead of hand-copying an existing one - Own a component or import it —
Section,CheckBullet,Shotcan be ejected into your own components directory for full editing control (npx gensheets add), instead of always imported from the package - Sensible default styling, fully overridable — namespaced
--gensheets-*tokens ship with real default values, so a consumer who does nothing gets a sane look, and reskinning is just redefining a CSS variable - Ships an
AGENTS.md— so an agent working on your pages knows which components exist and how a new page should be structured, without having to infer it from reading existing files
Install
npm i @rarexlabs/gensheets
npm i react react-dom lucide-react # required peersPDF generation needs two additional peers, only if you use it:
npm i -D playwright pdf-libCopy or link this package's AGENTS.md into your own app's AGENTS.md/
CLAUDE.md so an agent editing your pages inherits the component/token
conventions.
CSS custom-property contract
Components are styled entirely via Tailwind utility classes against
namespaced --gensheets-* tokens, shipped with real default values in
@rarexlabs/gensheets/tokens.css:
| Token | Default (oklch) | Used for |
| ----------------------- | --------------------------------------- | ------------------------------------------ |
| --gensheets-primary | oklch(0.205 0 0) — near-black | primary text color |
| --gensheets-secondary | oklch(0.371 0 0) — dark gray | secondary/body text color |
| --gensheets-muted | oklch(0.552 0 0) — mid gray | muted text, idle icon color |
| --gensheets-border | oklch(0.869 0 0) — light gray | borders/dividers |
| --gensheets-elevated | oklch(0.967 0 0) — near-white gray | elevated surface background |
| --gensheets-accent | oklch(0.546 0.215 262.9) — blue | accent/interactive color |
| --gensheets-paper | oklch(1 0 0) — white | the printable page's surface |
| --gensheets-canvas | oklch(0.967 0.001 286.4) — light gray | on-screen preview background behind sheets |
| --gensheets-success | oklch(0.527 0.154 150.1) — green | checkmark/success indicator |
A consuming app that imports tokens.css and defines nothing else gets this
neutral default look immediately. To reskin, redefine any --gensheets-*
variable in your own CSS — no component code is ever touched:
:root {
--gensheets-accent: oklch(
0.6 0.23 286
); /* swap the default blue for your brand purple */
}Multiple themes (multi-tenant / per-client styling)
An app that generates docs/decks for more than one brand or client (e.g. a
consultancy producing deliverables per client engagement) doesn't need a
different install per theme — scope the same override to an attribute
selector instead of :root, and tag each client's route/section root with
the matching attribute:
/* your app's globals.css / app.css */
[data-gensheets-theme="acme"] {
--gensheets-accent: oklch(0.6 0.23 286);
}
[data-gensheets-theme="globex"] {
--gensheets-accent: oklch(0.55 0.18 25);
}// e.g. the layout wrapping that client's printable routes
<div data-gensheets-theme="acme">{children}</div>--gensheets-* custom properties inherit down the DOM like any other CSS
variable, so every Sheet rendered under that wrapper picks up its client's
overrides while everything outside it keeps the shared default (or another
client's overrides) — no gensheets code or config is involved, and nothing
here is specific to this package. Attribute name and value are entirely up
to you; data-gensheets-theme="<client>" is just a suggested convention.
Import tokens.css from inside your own Tailwind entry stylesheet — the
file that already has @import "tailwindcss"; — not via a bare JS-level
import "@rarexlabs/gensheets/tokens.css" in a layout/component:
/* your app's globals.css / app.css */
@import "tailwindcss";
@import "@rarexlabs/gensheets/tokens.css";This matters: Tailwind v4's @theme inline merging (which is what turns
--gensheets-primary into a real text-gensheets-primary utility class)
only applies within the CSS import graph reachable from a Tailwind root. A
stylesheet pulled in purely via a JS-level import can end up bundled as an
isolated asset outside that graph — the variable would still exist, but the
utility classes components rely on (text-gensheets-primary, etc.) would
never get generated, and everything would silently render unstyled.
print.css, by contrast, is plain CSS with no @theme coupling and can be
imported either way.
Also add an explicit @source for this package under Tailwind v4.
Tailwind v4's automatic content detection skips node_modules by default, but
Sheet/ResourceTopBar/Section/etc. ship their class names as plain
strings inside this package's compiled dist/index.js — without an explicit
source, Tailwind never sees them and none of the utilities they use
(bg-gensheets-paper, shadow-xl, ...) get generated, so components render
with the right class names but no actual styling:
/* your app's globals.css / app.css */
@import "tailwindcss";
@import "@rarexlabs/gensheets/tokens.css";
@source "../node_modules/@rarexlabs/gensheets/dist";Adjust the relative path to match where your Tailwind entry stylesheet lives
relative to node_modules.
Docs vs. decks
- doc — one or more US Letter pages.
<Sheet>with novariant, portrait (8.5in × 11in) by default; passorientation="landscape"(11in × 8.5in) for a doc that needs the extra horizontal room (e.g. a wide table). Convention:docs/<slug>/page.tsx. - deck — one or more 16:9 landscape slides.
<Sheet variant="slide">. Always landscape —orientationis ignored. Convention:decks/<slug>/page.tsx.
Both are just stacks of <Sheet> — a doc that needs pagination and a deck
with several slides both render multiple Sheets in a row.
Components
import {
Sheet,
ResourceTopBar,
Section,
CheckBullet,
Shot,
} from "@rarexlabs/gensheets";
import "@rarexlabs/gensheets/print.css";
export default function Pricing() {
return (
<>
<ResourceTopBar />
<Sheet className="flex flex-col font-sans">
<Section title="Overview">
<CheckBullet>Ships with sensible defaults</CheckBullet>
<Shot label="Product screenshot" src="/pricing/screenshot.png" />
</Section>
</Sheet>
</>
);
}Sheet— a single printable page. Defaults to US Letter portrait (8.5in × 11in);orientation="landscape"flips a doc to 11in × 8.5in;variant="slide"switches to 16:9 landscape (10in × 5.625in) regardless oforientation. Renders adata-sheetattribute thatprint.cssand the PDF generator both key off, and manages its own@pagesize at print time (see "Landscape & the@pageoverride" below). Package-only — never ejectable, it carries the print/PDF contract.ResourceTopBar— back link, share, print, and PDF-download button (shown once a HEAD request to the matching.pdfpath succeeds). Renders<PresentMode />internally — you don't importPresentModedirectly. Package-only.PresentMode— fullscreen slide presenter for<Sheet variant="slide">decks. Renders nothing on pages with no slide sheets. Package-only.Section— a titled content block with a heading + hairline divider. Ejectable — see below.CheckBullet— a single checkmark bullet, for feature/benefit lists. Ejectable.Shot— a bordered, rounded frame around a screenshot/image (plain<img>, notnext/image). Ejectable.
Import print.css once, wherever your printable-page layout lives.
Import it, or own it
Section, CheckBullet, and Shot can be used either way:
// Default: import from the package
import { Section } from "@rarexlabs/gensheets";# Or eject it and own the source
npx gensheets add section// Then import your own copy
import { Section } from "@/components/gensheets/Section";Reach for the package import by default. Eject when a specific document
needs a reskinned or extended version of the component that isn't worth
threading through props. Sheet/ResourceTopBar/PresentMode have no eject
option — they encode the print/PDF/present pipeline and forking them per
consumer would risk silently breaking it. See "Ejecting a component to local
ownership" below for the full mechanics.
Adding a new shared component
If a content pattern repeats across more than one page (another kind of
callout, a table style, whatever), add it to this package rather than
duplicating it per-page — export it from src/index.ts and document it here
and in AGENTS.md so both humans and agents know it exists.
Landscape & the @page override
print.css sets a US Letter portrait @page as a static fallback, but
Sheet overrides it itself at print time to match its own variant/
orientation — mounting (and on every beforeprint event) it injects a
<style> tag with the matching @page { size: ...; margin: 0 } and
--sheet-width/--sheet-height, then moves that tag to the very end of
<head> right before printing (appendChild on an already-inserted node
moves it, so it wins the cascade regardless of when other stylesheets were
injected). This is why orientation="landscape" (or variant="slide") on a
Sheet is enough on its own — no per-page script needed.
The PDF generator dispatches a beforeprint event before calling page.pdf(),
so this same override applies during PDF export too. A page with more than
one Sheet should keep them all the same variant/orientation — the PDF
generator sizes the whole export off the first [data-sheet] element.
Scaffolding new pages
npx gensheets create doc --slug pricing --title "Pricing"
npx gensheets create deck --slug acme-pitch --title "Acme Pitch"Writes page.tsx from a bundled template into
src/app/resources/(printable)/<docs|decks>/<slug>/page.tsx by default
(override the base with --out-dir <path>, or set resourcesDir in
gensheets.config.json — see below). --title defaults to a title-cased
version of --slug if omitted. Fails rather than overwriting if the target
file already exists.
The generated page's imports automatically follow whatever you've ejected:
if you've run gensheets add section, a newly scaffolded doc imports
Section from your local components directory instead of the package —
nothing needs to be configured explicitly for this, see the next section.
Ejecting a component to local ownership
gensheets.config.json (optional — all fields fall back to the defaults
shown, create it with npx gensheets init) controls where ejected components
live and where the scaffolding CLI writes new pages:
{
"componentsDir": "src/components/gensheets",
"componentsAlias": "@/components/gensheets",
"resourcesDir": "src/app/resources/(printable)"
}npx gensheets init # write gensheets.config.json with defaults
npx gensheets add section # copy Section.tsx into componentsDir
npx gensheets add check-bullet
npx gensheets add shotFails rather than overwriting an existing file at the target path unless you
pass --force (useful for pulling in the package's latest version of a
component you'd previously ejected and haven't since edited).
Once ejected, gensheets create doc picks it up automatically — it checks
whether the file exists at componentsDir at generation time, no separate
flag or mode to set. For example, after npx gensheets add section only
(leaving CheckBullet un-ejected), a freshly generated doc page's imports
look like:
import { Sheet } from "@rarexlabs/gensheets";
import { CheckBullet } from "@rarexlabs/gensheets";
import { Section } from "@/components/gensheets/Section";With nothing ejected, the same generation produces a single grouped import line instead:
import { Sheet } from "@rarexlabs/gensheets";
import { Section, CheckBullet } from "@rarexlabs/gensheets";Pages already generated before you eject a component are not retroactively rewritten — this only changes what newly scaffolded pages do.
PDF generation
import { generatePdfs } from "@rarexlabs/gensheets/pdf";
await generatePdfs({
baseUrl: "http://localhost:4173",
pages: ["/resources/decks/acme-pitch/", "/resources/docs/pricing/"],
outDir: "./public/resources",
});Or via the CLI, against any URL that serves your rendered pages (a dev server, a preview server, or a deployed site):
npx gensheets pdf \
--base-url http://localhost:4173 \
--pages /resources/decks/acme-pitch/,/resources/docs/pricing/ \
--out-dir ./public/resources
# or, for a larger list:
npx gensheets pdf --base-url http://localhost:4173 --pages-file pages.json --out-dir ./public/resourcesEach target page must render at least one [data-sheet] element. The first
sheet's rendered size determines the PDF page dimensions.
Versioning
Standard semver. Patch = fixes, minor = additive API, major = breaking
changes. dist/ is committed to this repo (not just published output) so
that a git-installed tag is a complete, buildable-once artifact — package
managers have historically inconsistent prepare-script behavior for git
dependencies.
