Design system

Code view

Shows a landing page as the project it is published from: a searchable file tree, the open file with line numbers, copy and download as a zip.

import PageCode, { projectFor } from "@/pages/library/PageCode";
import "@/pages/library/library.css";

PageCode is what the page builder shows when someone switches the canvas from Preview to Code. It lays a landing page out as the files a developer would expect: one component per section, the page that orders them, the design system as tokens, the stylesheet the sections use, the page settings, and the index.html that is served.

The markup is read from the preview itself, by projectFor, so the code and the page can never disagree. PageCode only displays a list of files; projectFor makes that list.

A page as a project

The page opens on src/pages/…, the file that puts the sections in order. Click a file to open it, a folder to fold it, or search by path. Copy puts the open file on the clipboard. Download .zip saves every file in a folder named after the page.

// The page builder reads the project off the preview whenever the page or the design changes.
useEffect(() => {
  if (view !== "code" || !pageRef.current) return;
  setFiles(projectFor(pageRef.current, {
    title, description, vars: designVars(design), pixel, slug,
    sections: page.sections, settings, noindex, canonical, gtm, og,
  }));
}, [view, page, design]);

<div className="lib-browser">
  {view === "code" && <PageCode files={files} name={slug} />}
  <div ref={pageRef} className="lib-browser__page" hidden={view === "code"}>…</div>
</div>

The preview stays mounted while the code shows (hidden), because projectFor reads each [data-key] section's markup from it.

Any list of files

PageCode does not care where the files come from. Pass { path, content } objects; folders come from the slashes in path.

<PageCode
  name="webinar-signup"
  files={[
    { path: "README.md", content: "# Webinar sign-up\n\n…" },
    { path: "src/pages/WebinarSignup.jsx", content: "…" },
    { path: "src/styles/tokens.css", content: ":root { … }" },
  ]}
/>

The files projectFor makes

FileWhat it holds
README.mdThe page title and what each folder is for.
index.htmlThe page as served: meta tags from the page settings (description, noindex, canonical, Open Graph), Tag Manager and the tracking pixel when set, the design tokens in a <style> block, and each section's markup.
package.jsonA Vite and React 19 project named after the page address.
page.config.jsonThe page settings as JSON: address, access, SEO, Open Graph, tracking and forms. The password and the image itself are left out.
src/pages/<Page>.jsxImports each section and renders them in order inside <main className="lp">.
src/sections/<Section>.jsxOne component per section, named from the section name ("Hero with demo form" becomes HeroWithDemoForm, with a number added for repeats). The markup is turned into JSX: className, htmlFor, style objects, self-closed img and input.
src/styles/sections.cssThe section stylesheet, src/pages/library/sections.css, as it is.
src/styles/tokens.cssThe design system as --lp-* custom properties.
src/lib/tracking.jsOnly when the pixel is on: the script that loads it.

Folders are listed before files, and both are sorted by name.

States

StateWhat shows
First openThe first file under src/pages/, or the first file. All folders open.
File pickedThe row turns --color-primary-100 with --color-primary-500 text and icon, and has aria-current.
Folder foldedA right caret instead of a down caret. The fold is kept while the view is open.
SearchingOnly paths that contain the text, case-insensitive, with every folder open. "No file matches." when nothing does.
Picked file goneIf the picked file is no longer in files, the page file opens instead.
No filesAn empty .lib-code box.
CopiedA toast: "WebinarSignup.jsx copied.", or "Could not copy."
DownloadedA toast: "13 files downloaded as a zip."

Anatomy

PartClassStyle
Files column.lib-files232px wide, --color-grey-50, a --color-grey-100 line on the right.
Header.lib-files__head"Files" in 12px medium and the count in --color-grey-400.
Search.lib-files__search30px tall, 8px corners, white, a --color-grey-100 border that turns --color-primary-300 on focus. 12px text.
Row.lib-files__row28px tall, 12px text, 14px icons in --color-grey-400, indented 14px per level. --color-grey-100 on hover.
Bar.lib-project__bar40px tall: the path in 12px monospace, the line count in --color-grey-400, then Copy and Download .zip as small secondaryGhost Buttons (30px).
Code.lib-code12px monospace on a 20px line. A sticky gutter of line numbers in --color-grey-300 on --color-grey-50.
Colouring.lib-code__tag, __attr, __value, __wordTags and CSS properties in --color-primary-500, attributes and comments in --color-grey-500, strings in green (#0B7A3B), import and return in --color-primary-800.

The component fills its parent (flex: 1), so give the parent a height and display: flex.

Props

PageCode

PropTypeDefaultNotes
files{ path: string, content: string }[]Required.
namestring"landing-page"The zip's file name and the folder inside it. The page builder passes the page address.

projectFor(root, options)

ArgumentNotes
rootAn element whose [data-key] children each hold one section.
options.sectionsThe page's sections, in the same order, for their names.
options.title, description, slugUsed in index.html, the README and the package name.
options.varsThe design system, from designVars(design).
options.settingsWritten to page.config.json as it is.
options.pixel, noindex, canonical, gtm, ogOptional head tags.

documentFor(root, options) is also exported and returns only the index.html text.

Accessibility

  • The files column is an <aside> labelled "Project files". Folders are buttons with aria-expanded; the open file has aria-current.
  • The search field is labelled "Search files".
  • The code area is focusable (tabIndex={0}) and labelled with the file path, so it can be scrolled with the keyboard. It shows a 2px --color-primary-300 focus outline.
  • The line numbers are aria-hidden, so a screen reader reads only the code.
  • The colouring is decoration. Nothing depends on it.

Do and don't

  • Do read the files from the rendered preview with projectFor, so the code always matches the page.
  • Do keep the preview mounted while the code shows.
  • Do put new page settings into page.config.json and, when they change the page's head, into documentFor.
  • Don't use PageCode as a code editor. It only displays files.
  • Don't use it for short code samples in the product. It needs a full panel.

When to use it

Use it wherever a generated page or project needs to be shown as files. There is no code viewer in @/ui. For a short snippet, a <pre> with the .lib-code styles is enough.

Where it is used

FileWhat
src/pages/library/PageBuilderPage.jsxThe Code view of the page builder canvas, behind the Preview and Code switch in the header. projectFor runs whenever the view is Code and the page or the design system changes.

Copy uses navigator.clipboard?.writeText(…). Where the clipboard API is missing, such as a page served over plain HTTP, the call does nothing and no toast appears, not even "Could not copy."

On this page