Documentation menu
Documentation / Blocks & themes / blocks-portfolio-pack
Blocks Portfolio Pack
npm install @wabbit/tome-blocks-portfolio-packFirst install from our registry?
Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKEN
Portfolio blocks for Tome: a featured-project hero and a filterable work index (a ledger of job-numbered projects with a thumbnail column, type and status chips with live counts and a count line), rendered from the @wabbit/tome-portfolio layer's project records or from inline entries. Built for the Studio theme.
Overview
Portfolio blocks for Tome: a practice that works project by project shows one featured project as the hero and files every project under its permanent job number. The blocks read the project records of @wabbit/tome-portfolio, so nothing a block prints is typed twice, and they are written for the Studio theme (neutral by default, restyled through public data-* hooks).
Status: preview. Five blocks: work-index (four layouts: feature, index, selected and next), title-block, service-rows, and the case-study pair project-hero and plate-sequence. register() adds them as the portfolio-pack bundle, tier free.
Peer dependencies
| Peer | Range | Required |
|---|---|---|
@wabbit/tome-blocks-core | >=0.35.0 <1.0.0 | yes |
@wabbit/tome-core | >=1.22.0 <2.0.0 | yes |
@wabbit/tome-portfolio | >=0.0.0 <1.0.0 | yes (its first release is 0.1.0; the range starts at 0.0.0 so the workspace version, which is 0.0.0 until that release, satisfies it) |
@wabbit/tome-ui | >=0.18.0 <1.0.0 | no |
payload | >=3.67.0 | yes |
react | >=19.0.0 | yes |
react-dom | >=19.0.0 | yes |
Block styles read --tome-* tokens, so load the design tokens once in your global CSS, as with every Tome pack.
Register the block configs into your registries:
// payload.config.ts
import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { register } from '@wabbit/tome-blocks-portfolio-pack'
const blockRegistry = new BlockRegistry()
const bundleRegistry = new BundleRegistry()
register(blockRegistry, bundleRegistry)workIndexBlock.block({ projectsSlug }) takes the projects collection slug (default portfolio-projects); pass projectsSlug: null on a site without the portfolio layer and the block offers only its inline fields.
Register the renderers once at app startup:
import { registerRenderers } from '@wabbit/tome-blocks-portfolio-pack/render/register'
registerRenderers()Blocks
| Slug | Name | What it renders |
|---|---|---|
work-index | Work Index | Four layouts. feature: a split hero for one project, the plate on the right and on the left the job number, title, two sentences, the plate count and one button, with the nine-cell Title Block (project no., client, place, year, scope, lead, collaborators, photographer, status) under both; with no project chosen it shows the one flagged Featured. index: a ledger of every project filed by job number (number, title and place, type, status and year in one cell, an optional 4:3 thumbnail last), type and status chips with live conditional counts, a count line ("12 projects, 3 shown."), the featured project quieted as "Featured above", and a thumbnail credits line; filtering happens in place after mount (the URL keeps the state), a Plates / Ledger toggle is remembered in a cookie, and a phone filter sheet adds status, years and place. selected: up to three projects as large plates, each with its meta line, title, two sentences, a three-cell Title Block (scope, lead, result), a button and the photo credit; the step rhythm bleeds the first plate and insets the second, and straddle lets the last plate cross into the next band. next: exactly one next project at the foot of a case study, image first, with a link back to the index that restores the visitor's filter. |
project-hero | Project Hero | The top of a case study, read from the project record: a breadcrumb ("Work / Residential / No. 0412"), the lead image full bleed (21:9, a 4:5 phone crop) with nothing laid over it, then on the ground the plate line ("Plate 01 · photo: …"), the title, the summary, the nine-cell Title Block and the long description. The lead is preloaded and high priority, and it is the one image in the pack that carries a static view-transition-name (tome-project-<id>), so a listing image can grow into it. |
plate-sequence | Plate Sequence | One chapter of a case study as numbered plates: finished work, work in progress (progress) or drawings. Passages and plates in sizes bleed, column, offset, pair and trio (with an aspect), a count line from the layer's numberPlates ("Plate 04, 05 of 14", "Plate 10–12 of 14"), a caption after the images that pins beside the plate from 1024px, a credit composed per plate, before and after labels, and the figures with the sample label. A drawing prints the layer's sanitized inline SVG on the surface ground; on phones each gets an "Open the drawing" button that opens it at full size. |
The blocks are listed in the block catalog under the portfolio-pack bundle.
Where the data comes from
Each block carries a source field. collection (the default) reads the project records; inline uses the block's own fields, for a site with no Work collection.
The read is a virtual portfolioData field filled by an afterRead hook (makePortfolioReadHook), once per request and shared by every block on the page through req.context. The renderers stay pure: they receive plain data in a server component and in a client island alike. A failed read logs a warning and the block falls back to its inline fields; a page never fails because of the portfolio.
The hook reads @wabbit/tome-portfolio/server directly (listPortfolioProjects, featuredProject, getPortfolioProject, getPortfolioSettings), so there is nothing to wire. setPortfolioServer swaps that API for a wrapper or a test double, and setPortfolioServer(null) restores the layer's own.
A site can also pass entries it mapped itself (the studies precedent) and the route's parsed filter, as props on a wrapper around the renderer or through RenderBlocks({ context }):
| Extra | What it does |
|---|---|
entries | Mapped PortfolioIndexEntry rows that win over the read. |
filterState | { filter, view } from the route's query string: the server renders the filtered state, with no flash and with no script. |
currentProjectId | The project a route shows, for the layouts that need it. |
basePath | Overrides the settings' base path for the "more" link and the next layout's back link. |
Behavior worth knowing
- Numbers print as stored and are never renumbered. A gap (a project archived) stays a gap.
- Thresholds count visible projects. Under
minForFilters(8) no filter UI renders and the count line stays; underminToShow(3) the whole index renders nothing. - Filters work without script. Each chip is a
button[aria-pressed]in its ownform[method=get]carrying the query string the toggled state would produce. Counts are conditional: each facet ignores its own filter and applies the others. - Never a made-up fact. A fact with no data drops its cell; an empty client prints the settings' "Private client", and a client the data does not say anything about (inline or mapped rows) drops.
- Accent is a mark, never text: a dot beside "New" and a square beside "Featured above" and the current plate, both
aria-hidden. - Filters apply in place. The index is one client island: a chip, a year or a place recomputes the list, the counts and the count line with the layer's own
applyPortfolioFilterandfacetPortfolioEntries, writes the URL withhistory.replaceStateand restores from it onpopstate. The serialized query comes from the layer'sserializePortfolioFilter, so a shared link reproduces the state. A route that passes nofilterStategets the URL (and the remembered view) applied after mount. - The view is remembered. The Plates / Ledger toggle writes the cookie
tome-portfolio-view(one year,SameSite=Lax, path/) and puts?view=in the URL only when the choice differs from the block's default. A route reads it withreadViewCookie(cookieHeader)(exported from./render) intofilterState.view, so the remembered view renders on the server with no swap. - The phone sheet. Below 768px the band's chips give way to a Filter button that opens a
dialogwith every filter: type and status chips, two year selects and a place select, "Clear", and "Show N projects" (the live count). Focus moves in, Tab is trapped, Escape closes, and focus returns to the button. From 768px the button shows only when the sheet holds filters the band does not print (data-work-sheet-extra); a home slice that wants no sheet turns offfacets.status,facets.yearsandfacets.locality. - The last filter is kept for the way back. The island writes the serialized filter to
sessionStorage(tome-portfolio-last-filter); thenextlayout's "Back to the index" link reads the referrer, then that key, after mount. Every storage access is guarded: with storage or cookies blocked the page still renders and filters. - `next` picks with the layer's walk.
nextPortfolioEntry(from@wabbit/tome-portfolio/filters) is the same function the layer'snextPortfolioProjectuses; the route passescurrentProjectId, andwithinFilterkeeps the walk inside the filter the page was reached from. - The morph is data on listing surfaces. Project links are plain
<a>carryingdata-project-id; the image carriesdata-transition-name="tome-project-<id>"and noview-transition-name, so a page never has two. A small head script in the theme names the clicked image at navigation time.
Public API
| Entry | Exports | Runtime |
|---|---|---|
@wabbit/tome-blocks-portfolio-pack (.) | register(blockRegistry, bundleRegistry), portfolioPackBlocks, workIndexBlock, DEFAULT_PROJECTS_SLUG, the source trio (portfolioSourceFields(options), makePortfolioReadHook(kind)), toWorkEntry, toFeatureEntry, the layer seam (setPortfolioServer, getPortfolioServer, the PortfolioServerApi type), getDemoProps, portfolioPackDemoProps, and the WorkIndexData, WorkIndexInlineEntry, WorkIndexLayout, WorkIndexConfig, WorkEntry, WorkFeatureRead, WorkIndexRead, PortfolioFilter and PortfolioIndexEntry types | Payload config (block descriptors) |
./render | portfolioPackRenders (slug to renderer), WorkIndex, the shared TitleBlockCells, buildFactsCells, Mark, the morph helpers (morphName, claimMorphName, MORPH_CONTEXT_KEY), readPortfolioExtras, and the view memory (PORTFOLIO_VIEW_COOKIE, PORTFOLIO_LAST_FILTER_KEY, readViewCookie, toPortfolioView); importing it registers the renderers as a side effect | React server and client |
./render/register | renderers, registerRenderers(): the explicit, server-guaranteed registration path | React server |
./demo | getDemoProps(slug, variant?), portfolioPackDemoProps | Payload-free |
./meta | portfolioPackBlockMeta (each block's meta and variants) | Payload-free |
Server and client
Every renderer is a React server component except three client files: the index island (IndexIsland, filters, view toggle and the phone sheet), the next layout's BackLink, and the drawing viewer button and native <dialog> in plate-sequence (DrawingOpen.client.tsx, hidden until script runs; it opens a drawing at full size in a labelled, scrollable region, closes on Escape, the close button or the backdrop, and returns focus). None animates. Without JavaScript the ledger, the chips, the filtered state, the count line and the view toggle all work (the chips and the toggle submit GET forms); the phone sheet is the one piece that needs script, so its button appears only once the island runs.
Theming hooks
Public and stable (class names are scoped and are not): data-work-index with data-block-variant="feature|index|selected|next"; the index's data-work-view, data-work-view-toggle, data-work-view-option, data-work-sheet-open, data-work-sheet (and its -title, -close, -group, -label, -clear, -apply, data-work-year-from, data-work-year-to, data-work-locality), data-work-plates and data-work-tile (with -tile-image, -tile-caption, -tile-num, -tile-title, -tile-year, -tile-meta, -tile-credit); the selected rows' data-work-rhythm, data-work-straddle, data-work-selected-row (data-media-side, data-work-selected-scale), -text, -meta, -title, -teaser, -cta, -media, -credit and data-work-sample; the next teaser's data-work-next-media, -text, -label, -num, -title, -meta, -credit, -cta and data-work-back; the hero's data-work-feature-* and data-work-plate-count; the ledger's data-work-opener, data-work-tools, data-work-facet, data-work-chip, data-work-count, data-work-list, data-work-row (with data-project-id, data-status, data-new, data-featured-above), data-work-num, data-work-title, data-work-kind and data-work-thumb; the Title Block's data-title-block, data-tb-cell and data-tb-value; and the marks, data-mark="dot|square". The case-study blocks add project-hero's data-project-hero, -crumbs, -lead, -lead-image, -head, -plate-meta, -title, -summary, -about-label and -about; and plate-sequence's data-plate-sequence (with data-plate-mode and data-plate-surface), data-passage (-label, -number, -body, -figures, -sample), data-plate (with data-plate-size, data-plate-aspect, data-plate-pin, data-plate-caption-side, data-plate-kind), data-plate-images, data-plate-item, data-plate-image (with data-plate-number), data-plate-pair-label, data-plate-drawing, data-plate-caption, data-plate-count, data-plate-text, data-plate-credit, and the viewer's data-plate-open, data-plate-viewer (-bar, -title, -close, -scroll, -canvas). Drawings read --tome-drawing-ink, -muted, -surface and -font (with fallbacks), which a theme sets. The block's stylesheet places things on the page grid and sets a calm default look; a theme restyles through the hooks.
Block reference
work-index
The shop window and the filing cabinet of a practice that makes things project by project: one flagged project as the hero, and every project listed under its permanent job number. Everything it prints comes from the project record, so nothing is typed twice.
- When to use
- The top of a home page for a practice that leads with one project (layout feature)
- A home page slice of the latest projects, or a full Work page with filters (layout index)
- Two or three projects shown first, as large plates with their facts (layout selected)
- The foot of a case study: one next project and the way back to the index (layout next)
- Page types
- landing
- marketing
- portfolio
- How to use
Keep Data on Projects and add your projects in Work. Flag one as Featured and the hero follows it; leave Project empty to use the featured one. For the index, set a limit for a home slice and leave it empty on the Work page. Filters hide until the site has enough projects (8 by default) and the whole block hides under 3, so a young portfolio never shows an empty shell. Numbers are printed as stored: a gap means a project was archived, not renumbered. Turn on Thumbnails for the work column and Quiet the featured project to mark it "Featured above". Filtering happens in place and the URL keeps the state, so a filtered Work page can be shared; the Plates or Ledger view a visitor picks is remembered. For Selected work, pick up to three projects or leave it empty for the newest three that are not featured; Step makes the first plate bleed large and the second sit inset. Next project needs the route to say which project the page is showing.
- Pairs with
- project-hero
- plate-sequence
- title-block
- service-rows
- testimonial
- Precedes
- title-block
- service-rows
- Avoid when
- You have fewer than three finished projects to show: use a gallery or a single case study until the index has something to file
- Register in
marketing-landing
title-block
The facts of a practice or a project set as a line of ruled cells. Four layouts share one markup: facts, brief and credits read the project record, practice reads the whole portfolio.
- When to use
- The credits band at the foot of a case study (layout credits)
- A statement band on a home page, with facts that stay true because they are computed (layout practice)
- The facts or the brief of a project anywhere on its own page (layouts facts and brief)
- Page types
- landing
- marketing
- portfolio
- How to use
On a project document leave Project empty and the block reads that project. On any other page choose the project. For the practice layout write the statement and the paragraph; the facts compute from the Work collection and hide when the records cannot support them (no completed project, no place). Labels come from the portfolio settings, so the words stay in one place.
- Pairs with
- work-index
- project-hero
- plate-sequence
- service-rows
- Avoid when
- The site has no Work collection and no facts to state: use plain text instead of typing cells by hand
- Register in
marketing-landing
service-rows
The services of a practice that is known by its work: each numbered row names the service, says it in a sentence and points at one project that shows it.
- When to use
- The "What we do" band of a home page, under the statement
- A services overview where each service already has a finished project to point at
- Page types
- landing
- marketing
- portfolio
- How to use
Add a row per service (up to eight) and choose its representative project from the Work collection. A row with no project has no project line, and a project that is archived or unpublished drops its line, never the row. A site without the Work collection can type the line in Representative project (typed).
- Pairs with
- title-block
- work-index
- plate-sequence
- Follows
- title-block
- Avoid when
- There are no finished projects yet: a list of services alone is better as plain headings
- Register in
marketing-landing
project-hero
Opens a project page. The lead image runs edge to edge with nothing laid over it, and the title, summary and facts sit on the ground below, so the work is never covered by type.
- When to use
- The first block of a project (case study) page, on the project document itself or with a project chosen
- Anywhere a single project needs its lead image, title and facts together
- Page types
- portfolio
- How to use
Put the block first in the project's layout and leave Project empty: it reads the project it sits on. Choose a project to show another one. The lead image, its crop for phones, the credit, the title, summary, nine facts and description all come from the project record. Turn the breadcrumb, plate line, facts or description off if the page does not want them.
- Pairs with
- plate-sequence
- title-block
- work-index
- testimonial
- Precedes
- plate-sequence
- title-block
- Avoid when
- The page is not about one project in the Work collection: use a regular hero
- Register in
marketing-landing
plate-sequence
Tells a project in plates. Every image is numbered across the whole project ("Plate 04, 05 of 14"), captions follow their plate and pin beside it on wide screens, and a drawing shows as a line drawing you can open at full size.
- When to use
- The body of a project page: one block for the finished work, one for work in progress, one for drawings
- Any chapter that is a run of numbered images with short captions
- Page types
- portfolio
- How to use
Choose the chapter (Mode) and leave Project empty to read the project the page sits on. The block shows only the passages and plates of that mode from the project's story, so nothing is typed twice. Numbers run across the whole story, so "Plate 06 of 14" means the same in every chapter. Drawings sit on the surface colour and never mix with photographs. On a phone each drawing gets an "Open the drawing" button that opens it at full size, pannable.
- Pairs with
- project-hero
- title-block
- testimonial
- Follows
- project-hero
- Avoid when
- You need a free-form image gallery with no numbering or captions: use a gallery block
- Register in
marketing-landing
Exports
| Import path |
|---|
@wabbit/tome-blocks-portfolio-pack |
@wabbit/tome-blocks-portfolio-pack/render |
@wabbit/tome-blocks-portfolio-pack/render/register |
@wabbit/tome-blocks-portfolio-pack/demo |
@wabbit/tome-blocks-portfolio-pack/meta |
Changelog
0.1.xMinor
New package:
@wabbit/tome-blocks-portfolio-pack, portfolio blocks for a practice that works project by project, written for the Studio theme and reading the@wabbit/tome-portfolioproject records.- Block
work-indexwith four layouts (feature,index,selectedandnext).featureis a split hero for one project (the plate, the job number, title, two sentences, the plate count and one button, with the nine-cell Title Block under both); with no project chosen it shows the one flagged Featured.indexis a ledger of every project filed by job number, with a thumbnail column, type and status chips with live conditional counts, a count line, the featured project quieted as "Featured above" and a thumbnail credits line. Both render on the server and work without script. - The source trio (
source, the project relationship, the virtualportfolioDatafield) andmakePortfolioReadHook: one read per request, shared throughreq.context, falling back to the block's own fields when the read fails. - Shared
TitleBlockCells(the nine cells in a fixed order), the accent marks, and the per-request morph-name registry for the case page. Listing surfaces carrydata-project-idanddata-transition-nameas data only. - A route can pass
filterStateand mappedentries; the server renders the filtered state. - Entries:
register()at the root, plus./render,./render/register,./metaand./demo. Tieraddon. Requires@wabbit/tome-blocks-core0.35.0 or later and@wabbit/tome-portfolio0.1.0 or later (the pack reads its server API directly);@wabbit/tome-uiis an optional peer. - The
indexlayout is one client island: type and status chips (and year and place selects in a phone filter sheet) filter in place, with conditional counts and a live count line, and write the URL withhistory.replaceState(restored onpopstate). A Plates / Ledger toggle is remembered in the cookietome-portfolio-view, and the last filter is kept insessionStorage. Without script the chips and the toggle are GET forms that land on the same URLs. - Layout
selected: up to three projects as large plates with a three-cell Title Block (scope, lead, result), a button and the photo credit;rhythm: 'step'bleeds the first plate and insets the second, andstraddlelets the last plate cross into the next band from 1024px. Layoutnext: exactly one next project (the layer'snextPortfolioEntrywalk), image first, with a back link that restores the visitor's filter. - Every listing image (ledger thumbnail, Plates tile, selected plate) carries
data-transition-nameas data only and its linkdata-project-id; no listing sets aview-transition-name. - Blocks
project-heroandplate-sequence, the case-study pair.project-herois the top of a project page: breadcrumb, the lead image full bleed (21:9, a 4:5 phone crop) with the title, summary, nine-cell Title Block and description below it on the ground; the lead is preloaded and carries the page's one staticview-transition-name.plate-sequencerenders one chapter (finished work, work in progress or drawings) as numbered plates in sizesbleed,column,offset,pairandtrio, with count lines fromnumberPlates, captions that pin beside the plate from 1024px, composed credits, before and after labels and the figures with the sample label. A drawing prints the layer's sanitized inline SVG; on phones each gets an "Open the drawing" button that opens it at full size in a pannable dialog (the pack's one client island, hidden until script runs). title-blockcreditsprints its cells in the editor's authoredcredits[]order; the credit row that names the project's lead prints as the Lead cell where it sits, and a project with no such row keeps the lead first.includeLead("Show the lead") turns it off. The Photographer fallback cell is unchanged.- Fix: the read hooks call the layer without
withLayout, so reading a project never re-enters its own layout blocks (the hooks needstory, notlayout). Requires the@wabbit/tome-portfoliowithLayoutread option. title-blockpracticegains an optionalsamplenote: onep[data-tb-sample][data-compliance-label]after the facts (the pack's sample-note pattern), for a demo or sample site. Unset or blank prints nothing; the golden suite's markup is unchanged, and only its config section gains the new field.
- Block