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
| File | What it holds |
|---|---|
README.md | The page title and what each folder is for. |
index.html | The 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.json | A Vite and React 19 project named after the page address. |
page.config.json | The page settings as JSON: address, access, SEO, Open Graph, tracking and forms. The password and the image itself are left out. |
src/pages/<Page>.jsx | Imports each section and renders them in order inside <main className="lp">. |
src/sections/<Section>.jsx | One 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.css | The section stylesheet, src/pages/library/sections.css, as it is. |
src/styles/tokens.css | The design system as --lp-* custom properties. |
src/lib/tracking.js | Only when the pixel is on: the script that loads it. |
Folders are listed before files, and both are sorted by name.
States
| State | What shows |
|---|---|
| First open | The first file under src/pages/, or the first file. All folders open. |
| File picked | The row turns --color-primary-100 with --color-primary-500 text and icon, and has aria-current. |
| Folder folded | A right caret instead of a down caret. The fold is kept while the view is open. |
| Searching | Only paths that contain the text, case-insensitive, with every folder open. "No file matches." when nothing does. |
| Picked file gone | If the picked file is no longer in files, the page file opens instead. |
| No files | An empty .lib-code box. |
| Copied | A toast: "WebinarSignup.jsx copied.", or "Could not copy." |
| Downloaded | A toast: "13 files downloaded as a zip." |
Anatomy
| Part | Class | Style |
|---|---|---|
| Files column | .lib-files | 232px 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__search | 30px tall, 8px corners, white, a --color-grey-100 border that turns --color-primary-300 on focus. 12px text. |
| Row | .lib-files__row | 28px tall, 12px text, 14px icons in --color-grey-400, indented 14px per level. --color-grey-100 on hover. |
| Bar | .lib-project__bar | 40px 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-code | 12px 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, __word | Tags 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
| Prop | Type | Default | Notes |
|---|---|---|---|
files | { path: string, content: string }[] | Required. | |
name | string | "landing-page" | The zip's file name and the folder inside it. The page builder passes the page address. |
projectFor(root, options)
| Argument | Notes |
|---|---|
root | An element whose [data-key] children each hold one section. |
options.sections | The page's sections, in the same order, for their names. |
options.title, description, slug | Used in index.html, the README and the package name. |
options.vars | The design system, from designVars(design). |
options.settings | Written to page.config.json as it is. |
options.pixel, noindex, canonical, gtm, og | Optional 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 witharia-expanded; the open file hasaria-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-300focus 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.jsonand, when they change the page's head, intodocumentFor. - Don't use
PageCodeas 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
| File | What |
|---|---|
src/pages/library/PageBuilderPage.jsx | The 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."