Documentation menu

Documentation / Blocks & themes / blocks-portfolio-pack

Blocks Portfolio Pack

Package
@wabbit/tome-blocks-portfolio-pack
Version
v0.1.0
npm install @wabbit/tome-blocks-portfolio-pack
First install from our registry?
  1. Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.

  2. Add the registry and your token to the .npmrc at the root of your project, with your token in place of YOUR_TOKEN:

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
Blocks & themesPreview

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

PeerRangeRequired
@wabbit/tome-blocks-core>=0.35.0 <1.0.0yes
@wabbit/tome-core>=1.22.0 <2.0.0yes
@wabbit/tome-portfolio>=0.0.0 <1.0.0yes (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.0no
payload>=3.67.0yes
react>=19.0.0yes
react-dom>=19.0.0yes

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

SlugNameWhat it renders
work-indexWork IndexFour 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-heroProject HeroThe 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-sequencePlate SequenceOne 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 }):

ExtraWhat it does
entriesMapped 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.
currentProjectIdThe project a route shows, for the layouts that need it.
basePathOverrides 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; under minToShow (3) the whole index renders nothing.
  • Filters work without script. Each chip is a button[aria-pressed] in its own form[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 applyPortfolioFilter and facetPortfolioEntries, writes the URL with history.replaceState and restores from it on popstate. The serialized query comes from the layer's serializePortfolioFilter, so a shared link reproduces the state. A route that passes no filterState gets 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 with readViewCookie(cookieHeader) (exported from ./render) into filterState.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 dialog with 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 off facets.status, facets.years and facets.locality.
  • The last filter is kept for the way back. The island writes the serialized filter to sessionStorage (tome-portfolio-last-filter); the next layout'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's nextPortfolioProject uses; the route passes currentProjectId, and withinFilter keeps the walk inside the filter the page was reached from.
  • The morph is data on listing surfaces. Project links are plain <a> carrying data-project-id; the image carries data-transition-name="tome-project-<id>" and no view-transition-name, so a page never has two. A small head script in the theme names the clicked image at navigation time.

Public API

EntryExportsRuntime
@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 typesPayload config (block descriptors)
./renderportfolioPackRenders (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 effectReact server and client
./render/registerrenderers, registerRenderers(): the explicit, server-guaranteed registration pathReact server
./demogetDemoProps(slug, variant?), portfolioPackDemoPropsPayload-free
./metaportfolioPackBlockMeta (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-portfolio project records.
    • Block work-index with four layouts (feature, index, selected and next). feature is 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. index is 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 virtual portfolioData field) and makePortfolioReadHook: one read per request, shared through req.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 carry data-project-id and data-transition-name as data only.
    • A route can pass filterState and mapped entries; the server renders the filtered state.
    • Entries: register() at the root, plus ./render, ./render/register, ./meta and ./demo. Tier addon. Requires @wabbit/tome-blocks-core 0.35.0 or later and @wabbit/tome-portfolio 0.1.0 or later (the pack reads its server API directly); @wabbit/tome-ui is an optional peer.
    • The index layout 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 with history.replaceState (restored on popstate). A Plates / Ledger toggle is remembered in the cookie tome-portfolio-view, and the last filter is kept in sessionStorage. 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, and straddle lets the last plate cross into the next band from 1024px. Layout next: exactly one next project (the layer's nextPortfolioEntry walk), image first, with a back link that restores the visitor's filter.
    • Every listing image (ledger thumbnail, Plates tile, selected plate) carries data-transition-name as data only and its link data-project-id; no listing sets a view-transition-name.
    • Blocks project-hero and plate-sequence, the case-study pair. project-hero is 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 static view-transition-name. plate-sequence renders one chapter (finished work, work in progress or drawings) as numbered plates in sizes bleed, column, offset, pair and trio, with count lines from numberPlates, 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-block credits prints its cells in the editor's authored credits[] 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 need story, not layout). Requires the @wabbit/tome-portfolio withLayout read option.
    • title-block practice gains an optional sample note: one p[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.

Full changelog across all packages