Skip to main content

Design and preview a Parable

Sources

Docs: Pages, Panes, Powers Web App: Open a Composed Page or Pane in Pieces, then use its source and preview surfaces. Ask Ponder to compose it while the Piece is selected.

Story

A Parable can use the same approved controls as the app. The shared library contains Parable’s adapted components, typography, colors, square corners, and icon treatments.

Structure

Import controls through the Parable-facing entry:
The app build includes the library, its compiled styles, embedded fonts, and editor types in the isolated preview automatically. No directive or installation inside a Page is needed. Only this public entry is available; internal package paths and unrelated component libraries are not exposed. Use the components’ props for their variants. For layout, use ordinary CSS or inline styles. Arbitrary Tailwind class strings written after the app build are not compiled automatically. Other application components are imported through Panes declared in the Perception. The supported platform entries are react, react/jsx-runtime, react/jsx-dev-runtime, react-dom, react-dom/client, motion, motion/react, kysely, d3, three, @psgen/scalar-lib, and parable/panes/placeholders. Package subpaths outside that list, such as three/addons/..., unrelated libraries, direct Plot modules, foreign-Parable imports, and dynamic imports are refused. The source editor marks an import outside that list. It does not mark a dynamic import() of a listed module, or a library that a Page may not import (see below); the server refuses both when the Piece is saved. Ordinary JSX and CSS remain available. @psgen/scalar-lib parses Parable scalars and mints new identities with newIdentityUUID(); see Preferences. A Page is composition only: it may import only its declared Panes and the runtime layout modules, so shared controls and chart libraries belong in the Panes it places. A Pane whose primitive is null holds no value but may still import them.

Sound

A Pane plays Parable’s sounds by naming what happened, never a file:
The events are activate, select, toggleOn, toggleOff, disabled, transitionUp, transitionDown, notification, caution, and celebration. The preview frame carries no audio: it sends the event to the app, which plays it under the person’s sound setting and the app’s throttle. With sound turned off, nothing plays. Play a sound after a committed click or key press, never on hover. A Page is composition only, so its sounds belong in the Panes it places.

Outlines

OutlineTree and OutlineNode draw a tree where every level is the same row: an icon, a name, a description, a count and a chevron. Opening a row shows its detail under it, and a line joins a row to the rows below it. Team Time Spend’s Perceptions page draws its named types with them, its Categories page its category sets, and a mapping Pane draws how one Provider’s rows map into a type.
The caller holds which rows are open, so a page can keep one open at a time. extra adds to the text, after sits before the count, and rowData sets the row’s data- attributes. The rows’ look comes with the components, so a Pane needs no CSS for them. A tree its reader edits opts into controls; a tree that passes none of them draws the same markup as before:
  • actions on a node shows its controls, such as add and remove, over the row’s end on hover or keyboard focus, beside the row’s button rather than inside it.
  • onMove on the tree gives each node with an id a drag grabber at its row’s start. Dropping a row on another calls onMove(id, targetId, position): before or after it on the top or bottom quarter, inside it in the middle. On the grabber, Alt with Arrow Up or Down moves the node among its siblings, Alt with Arrow Right moves it into the sibling above, and Alt with Arrow Left moves it out after its parent. canDrop(sourceId, position) on a node refuses a landing, such as a node into its own subtree. The caller changes its data; the tree redraws from it.

Logos

A Pane shows a company’s real logo by its domain:
Use a logo for vendors, tools, AI models and Providers, such as GitHub, Linear, Claude or Cursor. Never use one for a person or an internal team: a person has their name or an Avatar, and a team its name. Pass the company’s own domain (a URL works too) and its name, which labels the logo for screen readers and gives the initials drawn when there is no logo for the domain, or no domain. While a logo loads, its square is empty. size is the edge in pixels, 24 by default. theme is auto by default: it follows Ivory and Obsidian and draws the variant made for a dark background in Obsidian; light or dark fixes it. Some companies’ marks are a single white or black shape on a clear square in both variants, such as Dropbox’s and Okta’s white ones and Anaplan’s black one. When such a mark would vanish on the page, the logo draws a tile of the opposite ink behind it, in either theme; a Pane does nothing for it. The preview frame has no network. It asks the app for every logo a render draws in one request; the app fetches each from logo.dev once, keeps it, and hands it to the frame as image data. A Pane never sees a logo URL or the logo.dev key and needs neither. useBrandLogo returns that image data, null when there is no logo, or undefined while it loads. A Page is composition only, so its logos belong in the Panes it places.

Redacted values

A Pane shows where a person’s data was without showing it:
The categories are PERSON, EMAIL, PHONE, ADDRESS, DATE_OF_BIRTH, US_SSN, PAYMENT_CARD and IP_ADDRESS. A chip shows the category’s icon, its name in Title Case (Person, Email, Phone, Address, Date of Birth, US SSN, Payment Card, IP Address) and a fixed mask, and screen readers hear “Email, redacted”. PiiRedaction takes no value, so a chip can never show one. A Plot writes a token in place of each value: the category between ⟦ and ⟧ (U+27E6 and U+27E7), such as ⟦EMAIL⟧. RedactedText draws every token in its text as a chip. piiToken('EMAIL') writes a token, parsePiiText splits text into plain runs and tokens, and piiPlainText writes each token as “Email ••••••” for a place a chip cannot go, such as a title or a chart label. Redact in the Plot. Whatever a Plot returns reaches the reader’s browser, so redacting in the Plot’s SQL is the only way to keep a value off the page. These regexp_replace calls replace each kind the patterns can find, applied from the innermost out; pass 'g' so every match is replaced:
They find emails, payment card numbers, US SSNs written with dashes or spaces, IPv4 and IPv6 addresses, a date of birth after a label such as “DOB:” or “Born on”, also in Markdown bold (the label stays), and phone numbers with separators or a leading +. SQL cannot check a card number’s Luhn digit or an IPv6 address’s structure, so the card pattern also replaces other 13 to 19 digit numbers and the IPv6 pattern also replaces hex-only names in code such as db::add. A pattern cannot find a name or a street address: select '⟦PERSON⟧' or '⟦ADDRESS⟧' in place of a column that holds one. redactPii(text), and redact on RedactedText, run the same patterns in the browser for a Pane that only has raw text, and also check Luhn and IPv6 structure. The value then reaches the reader’s browser but never the page, because it is replaced before anything renders. Use it when the Plot cannot redact, not instead of the Plot.

Artifact bodies

A Pane shows the body of an artifact, such as a ticket’s description, an email or a calendar event’s notes, with its formatting:
format says how to read text:
  • markdown: paragraphs, headings, ordered, unordered and task lists, block quotes, fenced and inline code, bold, italic, strikethrough, links and images. A single newline is a line break, as in a GitHub issue. HTML inside Markdown shows as written text, and so do tables.
  • html: read as structured text. Paragraphs, line breaks, headings, lists, block quotes, preformatted text, bold, italic and links keep their structure; tables and other containers read as paragraphs. Scripts, styles, the head, embedded media, form controls and hidden text, such as an email’s preview text, are dropped.
  • text, the default: every line break, blank line and space stays.
Nothing in a body runs or loads. A link is its text, with the URL in its tooltip, and goes nowhere; an image is its alt text beside an image icon. The body keeps a readable line length and the app’s type and colors in Ivory and Obsidian. Every token such as ⟦EMAIL⟧ draws as a chip in any format, in code too. redact replaces the values redactPii finds, both in the text as written and after HTML’s character references are decoded, so jane@acme.com is found too. A Plot that redacts in its SQL still keeps the value off the page; redact keeps it out of what the page draws.

Dates and times

A Pane shows every time in the reader’s time zone and clock, with the same functions the app uses:
The Pane runs in the reader’s browser, and the functions read its time zone and locale; readerClock() returns both. A reader whose locale uses a 24-hour clock sees one, such as “17:31 BST”; everyone else sees a 12-hour clock with am or pm. The short zone name always follows the time, and the year joins the date when it is not this year. Each function takes a Date, an ISO string, or milliseconds, and an option object whose timeZone and locale replace the reader’s. A value that cannot be read formats as an empty string. A Plot returns instants and the Pane formats them. Plot SQL cannot take the reader’s time zone as a parameter, because AT TIME ZONE accepts only a quoted zone name. The helpers read the current time themselves, so a Pane never calls Date.now() while it renders. The same Pane, read in New York (en-US) and in London (en-GB):

Categories

A Pane that shows a set of categories, and how each one is decided, uses three parts:
  • CategorySet draws each category as a card (CategoryCard) with its color, its name, what it amounts to (value, such as hours), the first statement of its decision, and a line for counts (meta). A parent’s card holds its children’s cards, and its line counts them. While a pointer or the keyboard is on a card, a badge names how the category is decided: Decision by Ponder or Decision by Rules. DecisionMethodBadge draws that badge on its own.
  • DecisionStatements edits a decision as a list of statements. Enter starts the next statement, and pasting several lines adds one statement per line. It reads and writes one string, the text Ponder reads: one statement as written, or several as - lines. Text with any line that is not a - , * or • bullet is one statement, so a decision written as a paragraph keeps its exact text. splitStatements, joinStatements and firstStatement read the same string anywhere else.
  • RulesTable edits Rules. A rule has a priority, the category it decides, an On switch, and conditions, which must all hold. Rules run by priority, and the first that matches decides. With categories, the table is the whole set’s ordered list, and dragging a row renumbers it. Without them, it shows one category’s rules with their set-wide numbers; categoryId names the category a new rule decides, and allRules takes the whole set, so a new rule’s priority comes after every rule in it rather than tying a rule of another category. The table then hands back only that category’s rules, which the Pane puts back into the set.
A rule is { id, categoryId, priority, enabled, conditions }. Its conditions are Filters rules over columns: path is [columnId], and value follows the operator (one value; a list for is_any_of and is_none_of; [min, max] for between; none for empty and not_empty). RULE_OPERATORS lists each column type’s operators. Text is compared, searched, or tested with matches, a regular expression; numbers are compared or fall between two values; booleans are true or false; selects are one value or one of a list. negated: true means the condition must not hold. While a person adds a condition, it can lack an operator or a value: leave such a condition out when the rules run.

Icons

A Pane lets a reader choose an icon from Font Awesome’s whole catalog:
IconPickerButton is an outline icon button that draws the chosen icon and opens the picker beside it; a pick calls onChange and closes it. IconPicker is the picker alone, a search box over a grid, for a Pane that holds it in a popover or dialog of its own. IconGlyph draws a chosen icon anywhere else. The value is the icon string Pieces and a setting’s icon are stored as, such as fa:sharp:solid-duotone:mug-hot; null means no icon. A pick keeps the value’s style and color, and is Sharp solid two-tone when there is none. Search is Font Awesome’s own, the same one the app’s icon picker uses, so it finds icons by meaning: “coffee” finds a steaming mug and a coffee pot. With no query, the grid shows Font Awesome’s popular icons, and more load as the reader scrolls. The preview frame has no network: it asks the app, which searches Font Awesome and draws each glyph the frame shows.

Ponder

A reader who can use Ponder reaches it from any Pane that reads a Plot. Hovering the Pane shows a small sparkle in its top right corner; it opens a menu with Ponder this… and Plot the path to this…. Right-clicking anything in the Pane opens the same menu at the pointer. Shift-right-click, and right-click in a text field, keep the browser’s own menu. A Pane needs no code for this: the Page frame adds it to every Pane that reads a Plot, and to no Page.
  • Ponder this… puts a draft naming the thing in Ponder’s composer. Nothing is sent until the reader sends it.
  • Plot the path to this… opens a side sheet with the Plotted Path: how the answer was arrived at, from the Pane through the Plots its SQL reads to the Provider Pool tables the rows come from. The steps draw at once; then Ponder writes a sentence for each, in a conversation the reader can open with Continue in Ponder. The sheet’s header picks the model: the default, a profile (Simple, Standard, Strong) or any model the Workspace allows, searched by name or maker. Picking one plots the path again.
Mark a finer target, a bar, a row or a number, so the menu names it and a path follows its Plot:
label is what the reader pointed at, in their words. plot is the handle of a Plot the Pane reads, and row the values that identify the row. A target inside a Pane belongs to that Pane. Pane code asks Ponder with usePonder:
ask runs an ordinary Ponder turn for the reader, with Ponder’s tools and the reader’s own permissions, in a conversation named for the Pane that the reader can open in Ponder. answer is the Markdown so far. status is idle, asking, approval, done, stopped or error. A tool call that would change something waits for the reader in Ponder’s drawer (approval). One ask runs at a time; a new ask stops the one before it, and so does stop(). A stopped ask ends in Ponder too, so its reply runs no further. model is a model id from usePonderModels(), or simple, standard or strong for a profile of the default Persona; the Workspace’s AI processors policy refuses a model it does not allow. available is false where nothing answers: for a reader without Ponder, under View as, in the preview camera and in unsaved previews. PlottedPath draws a path from data a Pane already has, the same component the sheet uses:
A step’s kind is pane, plot, judgment, preference, refresh, policy or source, which picks its icon and label. A step can carry summary, facts, details (code, fields, facts or text, shown when it opens), branches, and a status of restricted or missing with a note. Walk me through it walks the path one step at a time.

Providers

Every Parable is built for every Provider that could feed it, however rare. A Provider the Workspace has not connected reads as an empty table, so a Parable whose Providers are partly connected still shows what it has. ImproveParable says what a visual is missing and offers to connect it. The Pane reads the Workspace’s connections with a Plot over workspace.providers and hands the component the hook that reads them:
Pass the hook, not its result. The component calls it inside its own error boundary and Suspense, so a read that fails, is refused, or is still loading leaves the Pane drawing without the chip or band. Read in the Pane’s own body, a refused read is thrown to the Pane’s boundary and “Could not read” replaces the whole Pane: on an older Refresh of AI Usage it replaced five chart Panes. There, a Plot the chosen older Refresh did not capture reads Not in this Refresh with Read Latest, and a stored Plot waiting for its Refresh on Latest reads Refreshing… over a skeleton, both without Retry. A read a filter Policy refuses (QL-PL-015) reads “Hidden by” and the filter’s name, also without Retry. In a preview that draws Panes without running them (the bridge’s PREVIEW_NO_DATA), a read is replaced by the kit’s empty state, “Live data isn’t drawn in previews”, with no Retry. connections still takes the rows directly, for a host that has them. The hook returns the Plot’s rows, { provider, connection } with connection connected or attention, or null while they load. Return null, never the empty rows of a read still running: no rows means nothing is connected, and PopulateParable would cover the Page. A Provider with no row is not connected. The Plot folds each connection’s connector_slug into the Provider’s name in Plot SQL and reads its health as the Providers directory does; while a sync runs, the status before it decides:
  • Wrapping a visual, it shows a chip at the visual’s bottom right on hover, with the logos of the Providers not yet connected. The chip opens their list: what each adds, and Connect.
  • With no children, it is the Improve this Parable band: how many of the Providers are connected, and the list, folded until opened.
  • PopulateParable wraps a Pane’s content and shows every listed Provider in its place until one is connected, with a description of what the Parable shows once it has data.
provider is the Provider’s name in Plot SQL, zoom in providers.zoom.meetings; name labels it, domain gives its logo, adds says in one sentence what connecting it adds here, and group is an optional heading. Each row ends in a colored status icon: green when the Provider is connected, amber when its connection needs attention, muted when it is not connected. A Provider whose connection needs attention counts as connected and offers Fix. Anyone who can read the Parable sees which Providers are connected. In the preview camera and under View as, where nothing can open a setup, the rows offer no Connect or Fix, and the icon says the rest. Both components draw nothing of their own when every listed Provider is connected, while the connections are null, or when their read fails. On a Page with several visuals, wrap each visual. Otherwise put one band at the top of the Page, in a Pane of its own, since a Page imports only its Panes. The Pane holding a Parable’s Home content wraps it in PopulateParable. Connect opens the Provider’s setup in the app, not in the frame: the Workspace’s own connection when it has one, else the Provider’s setup page. The setup pages show a back arrow to the Page, Back to the Parable, which the browser tab keeps through an OAuth sign-in.

Audiences

AudiencePicker chooses who a Policy grant admits: participation levels, Workspace Roles and people, in one multi-select. Primary is always admitted. Choosing a level admits it and every level above it; taking one away takes it and every level below it. Its value is the platform’s grant, so a control Pane saves it as a This Parable setting that a Policy reads (fromSetting, see Policies):
Each choice is { actor: { kind, id }, name, detail }, where kind is person or role and id is parsed with parseIdentityUUID from @psgen/scalar-lib. The choices come from the Pane’s own Plot over the Workspace’s people and Roles; the picker reads nothing itself. People show with their initials, since a Pane loads no remote images, and a name the value holds that the choices lack shows as unknown. audienceWidens(previous, next) says when a choice admits anyone the saved audience did not, so a Page can ask before it saves a wider one.

Services

A saved Page loads a library, and a heavy control such as Filters or Data Grid, only when one of its modules imports it. A Page built from light controls loads none of them. An unsaved preview works the same way: the live bundler receives only the shared controls, npm libraries and scalar parser that the Parable’s files import. d3 (7.9.0) and three (0.186.0) are available to Panes whole, as their namespaces: import * as d3 from 'd3' or named imports such as import { Scene, WebGLRenderer } from 'three'. A three scene draws into a canvas the Pane creates. Kysely is included by default at version 0.29.6, in both the live authoring preview and compiled Page/Pane runtime. Its real declaration files are also supplied to the source editor.
Use Kysely to compile query structure into SQL and bound parameters. The library’s presence does not provide a database connection or permission to execute a query. Send the declared Plot name, compiled SQL, and matching typed parameters to plotApi.preview(name, query); values remain bound, and current input Policies are checked on execution. See Execution context for how the requester, definition version, and selected Report reading relate.

Design Powers

The built-in Design Basics and Design Transitions Powers bundles and their provisioning hooks have been removed. Unedited copies are excluded from the catalog and new responses; edited Workspace copies and historical responses are preserved.

Storage

Publish compiles every Page and Pane in the candidate version, with its selected CSS. A syntax error or unavailable import stops publication; the published version and deployment pointers stay unchanged. Runtime errors and unavailable data can still occur after a successful build and are shown in the content area. Builds are cached by source and compiler version. Opening the same saved draft again reuses its build without requiring Publish. Changing source creates a new build. Switching between Pages from the same build keeps the renderer running and reuses already evaluated modules: the frame receives the new Page over its open connection and replaces only the Page’s content. Once a Page is on screen, the frame evaluates the build’s other modules while it is idle, so a later move finds its code ready. Every Page enters the same way. The frame draws it unseen until every Pane in it has drawn (at most 1.5 seconds), then it settles in: a fade, an 8px rise and a 3px blur clearing, the shared motion tokens’ page-slide values. On a move to another Page, the current Page stays on screen until then and fades out over --duration-quick. A Pane still loading its data shows skeletons inside the settled Page. A Plot result the session already read answers at once, so a return to a Page does not wait for its data; see Plots for what drops a kept result. With reduced motion the switch is immediate. The generating mosaic covers a Page only while it is built where you are looking: an edit is saved, or Ponder writes the Page. A Parable switch, another Release, View as, or a reload never shows it. Under the mosaic the Page draws at full speed: the mosaic is 0.999 opaque, because Chrome gives an iframe that an opaque layer covers one animation frame a second. Outside editing mode, a deployment displays its own published version or the upstream version it has received. Pending Changes are not the displayed content. Editing mode opens the selected working changes. Personal Parables continue to use their directly saved content. When populating takes longer than a brief transition, a screen reader hears Populating…. Fast navigation announces nothing.

If the preview fails

Read the visible compile or runtime error and repair the smallest failing boundary. Check the Page or Pane source, imported local components, and the attached data or configuration Pieces. The current published version remains unchanged while the repair stays in a Patch.

Security

Saved Pages and Panes use compiled code in an isolated frame. That frame has its own opaque origin and cannot read the app’s cookies or document. Data requests travel through the host’s existing authorization checks over a document-specific message channel. Authored React is compiled, never executed, on the server. SQL executes on the server through the query service. view.plots controls source inspection, while permitted Pane use can receive compiled React and CSS under view.preferences. Executable assets necessarily reach the browser; hiding the source editor is not a secrecy guarantee. props.policies contains resolved decisions, not the Policies settings. Ponder from a Pane acts as the reader, never as more. Each turn is an ordinary Ponder turn under the reader’s permissions, a conversation the reader can see, and a tool call that changes something waits for the reader’s approval in Ponder’s drawer, which the frame cannot reach. A Plotted Path shows a Plot’s SQL only where the reader’s view.plots allows it. Under View as a Pane gets no Ponder, because Ponder keeps working as the previewer. Unsaved edits retain the live sandbox bundler. Saving brings the Page back to the reusable compiled path. Both paths use the same shared controls and Pane SDK.

Stability

Check:
  • initial, loading, empty, error, and populated states;
  • Ivory and Obsidian themes;
  • keyboard order and visible focus;
  • reduced-motion behavior;
  • text wrapping and long synthetic values; and
  • a small and a large device, from the device buttons, where the Pane has layouts for more than one size.
The preview fits the selected logical viewport into the available canvas. A smaller on-screen image does not change the Page’s logical media or container queries. The preview offers Auto and a button for each kind of device. In Auto a Page takes the width it is given. A Pane is a component, so in Auto it shows as a card centred on the canvas: as wide as the canvas allows, up to the width where its M layout starts, and as tall as what it draws. The Pane chooses its layout by the card’s width. The frame still covers the whole canvas, so a menu or date picker the Pane opens can extend past the card. While an edit is unsaved, the editing preview fills the canvas instead. The device buttons preview on a real device. The first group is Auto, then one button per kind of device, each drawn as that device: watch, phone, tablet, laptop, monitor, television, projector, and wall. Choosing a kind opens its medium model and adds a second group with its small, medium, and large model, such as iPhone SE, iPhone, and iPhone Pro Max, followed by the frame’s pixels. The frame takes that device’s exact CSS pixels, and the Page or Pane shows the authored size those pixels fall into, the same layout a person holding the device gets. A turn button holds a phone, tablet, or monitor the other way, and moving between models keeps it turned. In a narrow panel the second group wraps under the first. The devices are the same ones Ponder’s screenshots use, and the choice stays in the link.