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, parable/panes/placeholders, and parable/pages. 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. Prepare also refuses a name parable/panes/placeholders does not export, and names it: a typo, a made-up control, or one that was removed. A name the build had that the open page’s design system does not have asks the reader to reload, because the Parable was built for a newer design system than the tab loaded. 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 type is null holds no value but may still import them. parable/pages is the module of formatted Pages (see Pages). It provides the fixed layouts that Prose, Playbook, Proceedings and Participants Pages re-export as their code: ProsePage, PlaybookPage, ProceedingsPage and ParticipantsPage. For the layouts Ponder writes, it provides Region, which draws one named region of the Page’s content, the hooks a layout reads its content through (usePageFormat, usePageContent and useRegion), Callout, RichTextView, Slide and SlideMedia, and the Probe binding: ProbeForm, ProbeField, useProbe, ProbeWelcome, ProbeQuestions, ProbeSection, ProbeContentItem, ProbeProgress, ProbeNav and ProbeEnding. ProbeField draws one question with its Probe component from parable/panes/placeholders (ProbeText, ProbeChoice, ProbeRating, ProbeMatrix and the others), reads and saves its answer, and shows its error. A Probe layout draws every answer through it. A region is read-only, except on a Prose Page where the reader may edit the Page. Rich text takes its size from where the layout puts it: set a font size on a region’s holder (a slide uses container units), and its headings and body scale with it. The renderer loads the module only for a Page that imports it. The app is the only writer: a layout proposes changed content, and the app checks it and saves it with the Page. A Pane in rich text draws only when the Page’s Perception declares it. A Product Page and a Pane have no content, so they have no use for this module.

Layers

Every Pane a Page places draws in the element that scopes its Palette (Palettes, “Where a Palette applies”). The element adds no box, so it never moves the layout. It also decides how the Pane stacks with the rest of the Page:
  • A z-index inside a Pane orders things within that Pane only. One Pane’s tab labels or sticky headers never draw through another Pane’s tooltip.
  • The Pane the reader points at, or works in, lies over the other Panes. A tooltip, menu or sheet that the Pane draws itself is on top where it spills over a neighbour.
  • The Pane the reader points at lies over the Pane that holds the focus. A click focuses the button it lands on, so after a click in one Pane, the tooltip of the Pane the reader then points at still shows on top.
  • A Pane that draws an open menu itself lies over every other Pane, also the Pane the reader points at. The menu stays on top when the pointer moves onto a neighbour.
A menu is open while the control that opens it has aria-expanded="true" and aria-haspopup, or has the combobox role. Give the button of a menu that a Pane draws itself both attributes. If the button also names the menu with aria-controls, the Pane lifts only while that menu is in the Pane. A disclosure, such as an accordion, a tree row or a collapsible section, has no aria-haspopup. Its content flows in its own Pane, so an expanded disclosure does not lift the Pane. The shared menus, selects, popovers and comboboxes name their popup with aria-controls and draw it outside every Pane, over all of them, so they do not lift the Pane that opens them. The shared Popover and Combobox let the pointer move onto a neighbour while they are open, and the neighbour’s tooltip still shows over the Pane that opened them. A shared Command that a Pane draws in place does not lift the Pane either: its list flows in the Pane. A position, isolation or z-index that a Pane sets on its own root still applies. Page chrome that must stay above a Pane with an open menu, such as a sticky bar, needs a z-index of 4 or more.

Other people on the Page

When other people are on the same Page, Release and Refresh, the preview draws their cursors and the outlines of the Panes they work in. The app owns this layer. It draws over the Page content and under the shared controls’ menus, popovers and tooltips, so a person’s cursor never covers the menu the reader uses. It takes no pointer input, and a Page or Pane cannot read, restyle or remove it. A Page or Pane needs no code for this. Draw the Pane as usual, and keep its box steady: a cursor sits at the same place in the Pane on every screen, as a fraction of the Pane’s box. A Pane whose content another reader cannot see still has a box, and the cursor shows over it. A Pane that the Page draws once for each row has no single box, so a cursor over one of its copies takes the box of the Pane around the copies.

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 plain rows:
  • 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.
current on a node marks the chosen row (aria-current, with the open row’s border), and expandable={false} draws a row that opens nothing, with no chevron. fold, which CardTree sets, hides a row’s child rows while the row is closed and shows its detail, then its child rows, in one panel while it is open.

Card trees

CardTree is an expandable card tree built on the outline rows. It takes a flat list: each card names its parent (null at the top), and siblings keep their order in the list. Each row shows the card’s icon in its color, its name, the first sentence of its description, and metaOf’s muted line. Every card starts closed. Opening a card shows renderDetail’s page under its row, then the cards under it. A closed card shows neither, so a long tree stays short until the reader opens a branch. One path is open at a time, as in an accordion: opening a card closes its open siblings and everything under them. Only the deepest open card shows its page. A card above it keeps its open row and shows only the cards under it. Closing a card makes its parent the open card, so the parent’s page shows again. A card opens when it has a page or cards under it. A page of null or false counts as no page. A card with neither has no chevron. When the open card loses both, its parent shows its page and ends the open path. No prop turns the fold off. Give the tree every card, and do not fold it yourself.
  • Editing. With editable and onChange, hovering or focusing a card shows add and remove, and a grabber that moves it by drag or by Alt and the arrow keys. The tree hands back the whole list after each change. A card never moves into its own subtree, and removing a card removes the cards under it. A reader who cannot edit sees add and remove disabled, with reason on hover and focus, and no grabber. newItem makes a new card. Without it, nothing can be added. words renames what the controls say, such as { root: 'Estimate', addChild: name => ... }. A new card opens. Removing the open card, or a card above it, makes the removed card’s parent the open card. Moving a card inside a closed card opens that card, so the moved card stays in view. When the moved card holds the open card, the open card stays open.
  • Choosing. With onSelect, a click chooses a card instead of opening it, as a list of threads does. selected is the chosen card’s id. A tree that chooses folds nothing: every card stays on screen.
  • Search. reveal takes the ids of the cards a filter matches. Every card above each one stays open, so the matches show without a click. Those cards show only the cards under them, not their pages. The reader can still close them. Opening another card leaves them open, so every match stays in view. A new reveal forgets which cards the reader closed, also when an earlier reveal comes back.
  • Menus. renderMenu(item) returns ContextMenuItems. The menu opens on a right-click, or the Menu key or Shift+F10 on a card.
  • Controls. controlsOf(item) returns a card’s own controls, such as a Switch. They show at the card’s end at all times, beside its row, so a click on them does not open the card. With them, add and remove also show at all times.
  • open and onOpenChange hold the open card for a caller that needs it. open names the deepest open card, and every card above it is open too. Only the open card shows its page, or its parent when the open card cannot open (see the fold above). onOpenChange gets each new open card, from a click, an add, a remove or a move. renderIcon draws a card’s icon in place of icon, and describeOf its line under the name.

Logos

A Pane shows a company or product logo through the shared BrandLogo component:
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. Product marks are registered in the platform Provider catalog. Recognized domains, such as calendar.google.com, meet.google.com, docs.google.com and sheets.google.com, select their registered product through the host. Pass product when the host does not name one product. For example, use product="google-sheets" with docs.google.com, or product="jira" / product="confluence" with a Workspace’s *.atlassian.net host. That host has no automatic mapping and falls through to logo.dev without a product key. A registered product needs no domain prop. name overrides its registered label. The same label is available on hover and to screen readers. showName={false} suppresses the hover label beside an already visible name. Other brands show a hover label only with showName={true}. The application and each Pane frame share one tooltip provider. product accepts a registered key. Provider onboarding can add products without a frontend or Pane-runtime deployment. The local seed catalog includes Google Calendar, Meet, Gmail, Chat, Drive, Docs and Sheets; Microsoft Outlook, Teams, OneDrive, SharePoint and Copilot; and Jira, Confluence, Bitbucket and Trello. useBrandLogo also accepts product in its options. The catalog stores identities and verified logo.dev domains or names, not image files. The host uses the existing cached logo.dev path to fetch each product independently. Trello currently has a confirmed product lookup in the initial catalog. Existing deployed Providers receive empty catalogs during migration. Register their products through the Admin API after deploy. The remaining initial products show initials for explicit product requests. A successful name search can still return an unrelated brand, so onboarding must check the image. External artwork for missing products is deferred. Registered names supply hover labels and accessible names. An explicit product without a verified lookup draws initials and never selects the parent Provider mark. Domain-only requests retain their ordinary logo.dev fallback when a registered alias has no lookup or the catalog is unavailable or ambiguous. The catalog refreshes after one minute and retains its last successful result through an outage. Failed refreshes wait five seconds before retrying. Explicit product image responses have a five-second browser cache. Ordinary domain images retain a seven-day browser cache, with a one-hour cache for missing logos. A newly registered domain alias can wait for that browser cache to expire. Use an explicit product key to avoid that delay. Reload the app after registration and the catalog refresh interval to read new mappings. outline={false} removes the outline, the contrast backing tile, and the initials’ background. It preserves the artwork’s own background and padding. The default appearance and domain fallback remain unchanged. The preview frame has no network. The app hands each logo to the frame as image data. A Pane never sees a logo URL and needs none. 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. A row that stands for a record in a Provider, such as a pull request, an issue or a meeting, links to that record:
ExternalLinkButton is a small external-link icon. In a row it shows while the row is hovered or holds keyboard focus, as a row’s Preview does, and always on a screen nothing hovers, such as a phone. Outside a row it always shows. A row is a table row, a list item, an element with role="row", or any element marked data-row. Put it beside a row’s own button, never inside it: a link inside a button is not valid HTML. Pass the record’s address from the Plot’s rows. Only an https URL draws a button, so a record with no address shows none. label names what the link opens for screen readers, and reads “Open on github.com” when left out. A click opens the record in a new browser tab, never in the Page, and leaves the row’s own click alone; the browser shows the address on hover. The frame has no pop-ups, so the Parable page opens the tab, only just after the reader’s click or key press. To open a link from a control of your own, call openExternal(url) from the Pane’s module in its click handler (Panes).

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

Contextual Ponder is a menu a reader opens on something on a Page. It has two entries: Ponder this… and Plot the path to this…. The Page frame adds it to nothing. The author places it, case by case, on the things a reader asks about.

Where to place it

  • Place it on visuals, numbers, and values that need an explanation: a chart, a total, a rate, a value in a row.
  • Do not place it on navigation, filters, settings, forms, or a plain list of links. A reader does not ask how a button came to be.
  • Place one target per thing a reader points at. A card holds one target; a bar or a row inside it can be a target of its own.

How to place it

Wrap the thing in ContextualPonder, and place one ContextualPonderSparkle inside it, where the sparkle shows:
  • asChild puts the target on its one child, so the target adds no box to the layout. Use it whenever the target is one element.
  • Without children, ContextualPonderSparkle takes no room. Put it directly after the last word of a title, a value, or a label, where the line has empty space after the words. The sparkle shows in that space.
  • side="start" shows the sparkle before the words that follow the spot. Use it for a value at the end of a row, where the space is on the left.
  • With children, such as a card’s icon, the sparkle takes their place in the same box: they fade out as it fades in, and come back as it fades out.
The sparkle shows while the pointer is over its target or the keyboard is in it. It fades in and out, and it never covers the content under it, moves a box, or shifts the Page. Do not add padding, a gutter, or an empty column for it: it needs none. The keyboard reaches the sparkle with Tab, and it shows while it holds the focus. A target with no sparkle placed in it still opens its menu on a right-click. Right-clicking anything in a target opens the same menu at the pointer, naming the innermost target there. A long press opens it on a device that cannot hover. Shift-right-click, and right-click in a text field, keep the browser’s own menu. The menu’s entries are registered, not fixed:
  • Ponder this… opens a small composer where it was chosen: at the sparkle, or at the pointer for a right-click. What the reader pointed at sits in it as a chip. Enter sends what they type, after a quoted line naming the Pane, as one Message in the Parable’s current Ponder thread; when no thread is open, Ponder opens one. The Message goes through Ponder’s own composer, with the Pane as the Message’s current view, and the thread comes into view. Whatever keeps the composer from sending, such as a reader who may read the thread but not add to it, refuses the Message in the composer’s words, and the typed words stay. So does a thread that has not opened within 30 seconds, and a Message refused that way is never sent later. The app draws this composer itself, over the Pane, so no Pane code can write a Message as the reader: a Pane names what was pointed at, never the words. That name shows only in the chip; the Message’s quoted line uses the Pane’s own name from the Parable, never words the Pane chose. The app also holds these rules for any code in the frame:
    • It opens only after the reader’s press in the Page, by the rule every press-gated call follows: the Page has the focus, the window has a press, and the app saw no press of its own since the Page took the focus. That stops a Page that asks on its own, on load or on a timer, or while the reader types in Ponder’s own composer. It does not prove the press was the reader’s: code in the frame can move the focus into the Page itself.
    • It never takes the keyboard. It opens in view saying Click to type, and the reader clicks it, or tabs to it, to type. Keys typed while the focus is in the Page or in Ponder’s own composer never reach it.
    • It closes when the focus moves into the Page and when the reader presses on the Page. While it is open the Page takes no pointer input, so that press closes it and does not reach the Page.
    • It opens only wholly inside the part of the Page on screen, never over the navigation or Ponder’s column. A place outside that part is not used: the composer opens at its top right corner instead. A Page scrolled out of sight, or showing too little of itself to hold the composer, opens none. On screen means where the frame is drawn: a frame drawn transparent still counts. Its field grows with the words to at most 12rem, and less where the composer has less room there, then scrolls, so the composer never grows out of that part.
    These rules do not stop Pane code from choosing when, right after a press, and where the composer opens, or from asking again each time it closes. So Pane code can put the field under the second press of a double-click or under a press repeated because the first did nothing, or just after its own last field so that a Tab lands in it, and keep it on the Page through every press. The reader’s words still go only when they send them. Inside the frame, the place a Pane names is also kept inside that Pane: the composer opens at the Pane’s top right corner for a place outside it. That holds Panes that ask through the design system; only the app’s rules above hold any code in the frame. Escape or a send in the composer closes it and puts the focus on the Page’s edge, so Tab goes back into the Page. The app never moves the focus into the Page itself: a focus it moved there would count as the reader’s press in the Page, and the reader’s next press inside a Page that already holds the focus is one the app cannot see. A press anywhere else outside the composer closes it too, and the focus stays where the reader put it. The Page frame’s runtime also closes it on Escape typed in the Page. The sparkle it was chosen from stays while it is open.
  • Plot the path to this… opens a side sheet with the Plotted Path: how the answer came to be, from the tools its information comes from up to the Pane, in a few steps a CEO or an intern can follow. Each step is numbered and has a title and short bullets; the first is always where the information comes from, with each tool’s brand icon. The platform writes the steps in that order. It is on every Pane that reads a Plot, for every reader with or without Ponder, on the Workspace’s Parable page, in a Platform Parable’s tab and in Admin Ponder. Under View as it is not offered.
    • The steps are one walk: the step it is on is prominent and the others are faded, and still readable. Back and Next move it, and so do choosing a step and the Left and Right keys. They always sit outside the sheet, at its top left, beside it and its panels; where their words have no room, they stay there as two arrow buttons. On a phone, where the sheet fills the screen, they sit in its footer. There are no tabs and no other order.
    • A step’s … menu, shown while the step is hovered or holds focus (always, on a touch screen), lists under View Panes the Panes it links. Choosing one opens it in a side panel to the right of the sheet, titled “The name Pane”: what it is, its own steps, whether it has settings, and, where the Page on screen draws it, the Pane itself with its own controls. The panel slides in (at once for a reader who asks for reduced motion) with a sound, under the reader’s sound setting. A Pane opened from a panel opens to that panel’s right. A panel never covers the sheet, and Back and Next stay on the Page: where the Page is too narrow for all of them, the panels nearest the sheet close, and a lone panel narrows to what the sheet at its least width leaves it, down to a least width of its own. Escape closes the rightmost panel, and the focus goes back to the … it was opened from; the arrow keys in a panel are the Pane’s own. Closing the sheet hands the focus back to the control whose menu opened it: chosen from a Pane’s sparkle, the sparkle stays while the sheet is open and takes the focus back.
      • A Provider the Workspace has not connected is listed under the sources step with Connect.
    • The platform writes the explanation once per Pane and published Version, and keeps it, so it usually opens at once. A Page with filter Policies gets its own explanation of the Pane. A skeleton shows while one is written. Someone who publishes the Parable can choose Explain again. How it is written, and how to write Panes it explains well: Explaining Panes.
    Ponder this… is there for a reader who can use Ponder, and Plot the path to this… for every reader. View as gets neither.
  • A Pane’s own entries come after Ponder’s, and a Pane can turn any entry off on every target in it:
Mark a finer target inside, a bar, a row or a number, so the menu names it, a path follows its Plot, and it can carry entries of its own. Give it a ContextualPonderSparkle only where it has room for one; a right-click reaches it either way:
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. An entry has an id, a label, an optional Font Awesome icon, and disabled, true or why in a few words, to show it without letting it be picked. The why is drawn under the label. off takes entry ids, the built-in ponder and plotPath included. A Pane that reads no Plot offers no Plot the path to this…. The Improve panel is not in this menu. The Parable’s Providers button opens it (Providers). 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 column (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 Ponder at that effort level; 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 a Pane picture that Ponder takes, and in unsaved previews. PlottedPath draws a path from data a Pane already has, the same component the sheet uses:
It numbers the steps from 1 in the order given. A bullet’s icon is a Font Awesome class list: a brand (fa-brands) keeps its own family, and every other icon is drawn in Parable’s. With onWalk, the step at walk is prominent and the others faded; without it every step is drawn alike. A step’s open lists Panes as { key, name, description, hasSettings }; with onOpenPart, each step that links any gets a … menu with View Panes, and onOpenPart receives the one chosen. Write the steps as the platform does, from where the information comes from up to the thing on screen. A Platform Parable’s Page can read the platform’s Plotted Path of a Pane or a Plot of its subject, the Parable it reads beside it (PARABLE-8460). usePonderSource() gives the same source that the Pane menu uses. Its path takes the part’s key and subject: true, and streams the path as it is read or written. A Plot’s key explains the Pane that owns it:
The server reads the subject’s line at the revision on screen there, and it admits the person by their own seat on the subject and the Pane’s view.pane, as in the Pane menu. A Platform Parable’s tab offers Plotted Path too, except under View as.

Providers

Every Parable is built for every Provider that could feed it, however rare, and for every stream it reads, whichever ones a Workspace turned on. A Provider the Workspace has not connected, or a stream it has not turned on, reads as an empty table, so a Parable whose Providers are partly connected still shows what it has. Never assume a stream is on: each Pane draws its empty state when its Plot returns no rows. The Parable names its Providers once, as groups on its Home Page’s Perception (Providers a Parable reads): each group has a title, a description, a role (Required to Populate or Improves the Parable) and a choice rule (at least 1, at most 1, exactly 1). The Page frame installs them with the Workspace’s connections, read as the Providers directory reads them. A healthy connection is waiting while the directory says its first data has not arrived: a push Provider until a sender’s data lands, a pull Provider until a sync completes. A Pane shows the groups whose Providers its Plots read; nothing declares that per Pane. To find them, the frame follows each parables.<parable>.<plot> that a Plot’s SQL reads, when <parable> is this Parable’s catalog slug or its key. The catalog slug is the one the Parable got when it was made. A rename of its handle does not change it, so the frame and the editor’s Performance tab still follow the Plots of a renamed Parable. A Pane whose Plots read another Parable’s Plot, or a Plot whose SQL the reader may not view, shows every group. The Parable’s Providers button, a plug beside Pieces and Problems in the Parable’s sidebar, opens the Improve panel for the whole Parable. It shows for a Parable that declares Provider groups, to every reader. A Page needs no copy of it: the Parable declares its groups once. The chip on a visual, the band at the top of a Page and Populate this Parable open the same panel:
  • Its title is Populate this Parable while the Parable cannot populate (a Required group is not met, or no Provider has data yet), and Improve this Parable after that. No heading repeats it.
  • Where the Parable has groups of both roles, two labels split them: Required, then Optional.
  • Each group is a row of a card tree: its name, a mark that says whether its rule is met, and its rule (“At least 1”, “At most 1”, “Exactly 1”). The rule fades out once it is met, and back in while the row is hovered, focused or open. A group that is not met starts open.
  • Open, a group lists its Providers by logo and name, with Connect, or Fix on one that needs attention. Neither a group nor a Provider shows a description.
  • At the bottom of a group, each above a border: Connected folds the Providers the Workspace has, and, in an “exactly 1” or “at most 1” group that has its one, Other folds the Providers the reader cannot add.
ImproveParable wears the chip on a visual, or is the band, and PopulateParable covers a Page while it cannot populate. Where the Parable declares no groups, they read the Providers a Pane lists and the Pane’s own read of the Workspace’s connections, a Plot over workspace.providers, and the Pane 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. 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. A read the server refuses the reader (HTTP 403) reads “You don’t have access to this”, 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 takes the rows directly instead, for a Pane that already has them. The hook returns the Plot’s rows, { provider, connection } with connection connected, attention or waiting, 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. A connection whose setup was started and never saved a credential starts with no status past idle and no sync, so setup_finished is false and it counts as no connection; Connect continues that setup. Once its status is recomputed it reads config_invalid (setup not finished): setup_finished turns true, it reads as needing attention, as the Providers directory lists it, and Fix opens the connection so the customer can finish it (PARABLE-9399). A connection waiting on its OAuth consent has a pending credential, so it reads as needing attention, as it does in the directory. A Provider with a healthy connection is waiting until data from any of its connections has reached the Pool, which workspace.stream_data answers: one row per stream with committed data, whose provider_id joins workspace.providers.id:
A push Provider (Claude Telemetry, Slack Events, Parable Pulse) is waiting from setup until its senders’ first data reaches its Pool, and a pull Provider until its first sync lands. A stream gets its row when its first load reaches its Provider Pool. Push data that lands reads as arrived only after it reaches the Pool. Like every workspace.* table, stream_data is not narrowed by Provider Policies: it says when a stream last received data, not what the data holds, so a reader a Policy restricts reads the same connection state as everyone else. A Parable that reads a stream a Provider can leave off, such as Slack’s replies, also marks that Provider attention while none of its connections has the stream on, so its row offers Fix, which opens the connection where the stream is turned on. Read the streams from workspace.provider_connections: add a streams step before folded, join it in folded with LEFT JOIN streams s ON s.provider_id = w.id, and add one WHEN per such stream before ELSE 'connected'. A connection marked this way is not healthy, so its Provider reads attention rather than waiting, even before any of its data arrives: no data comes from a stream that is off.
Without declared groups, group is the kind of data a Provider holds, and the Providers of one group form a group that one connected Provider meets, such as a ticketing tool or an HRIS; a Provider without one is a group of its own. emphasis names the groups the visual needs most, such as People on adoption by team; they lead the panel and the chip’s logos. Once the Parable declares groups, they are used, and a Pane’s list only lends its Providers’ logos.
  • Wrapping a visual, it shows a chip at the visual’s bottom right on hover, with the logos of the unmet groups’ Providers. On a visual that ends in a <footer>, the chip centers on the footer’s row. The chip opens the Improve panel: in a side sheet for declared groups, else in a popover.
  • With no children, it is the Improve this Parable band: how many of the groups are met, and the panel, folded until opened.
  • PopulateParable wraps a Pane’s content and draws the panel in its place while a Required to Populate group has fewer connected than its rule asks, or while no Provider has data or needs attention, with a description of what the Parable shows once it has data. While a connected Provider waits for its first data, its heading is Waiting for first data from and the Provider’s name.
provider is the Provider’s name in Plot SQL, zoom in providers.zoom.meetings; name labels it, domain gives its logo, and group is its heading and its kind of data. A Provider without a group is a group of its own. adds, a sentence on what connecting it adds, is still accepted, and the panel does not draw it. A row shows a status icon only where the reader can act or wait: amber when its connection needs attention, and a muted hourglass while it waits for its first data. A Provider whose connection needs attention counts as connected, and is listed above the folds with Fix. A Provider waiting for its first data counts as connected, folds under Connected and offers nothing: there is nothing left for the reader to do. Anyone who can read the Parable sees which Providers are connected. In Pane pictures that Ponder takes and under View as, where nothing can open a setup, the rows offer no Connect or Fix, and the icon says the rest. ImproveParable draws nothing of its own when no unmet group has a Provider left to offer, and PopulateParable when each Required to Populate group has as many connected as its rule asks and a Provider has data or needs attention. A group with more connected than its rule allows, such as two in an Exactly 1 group, shows in the panel without its check, but it never hides the Page or adds a chip or a band. Neither draws anything 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 connector page in the app, not in the frame: with the row of the Workspace’s own connection open when it has one, else with the empty row that adds one. The connector page’s tab row starts with 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. A grant that lists a level always admits Primary. Choosing a level admits it and every level above it; taking one away takes it and every level below it. A grant that lists no level admits only the people and Roles it names, such as a setting for two staff Roles: the picker shows no level for it, and adding or removing a name keeps it so. Its value is the platform’s grant, so an Audiences Pane saves it as This Parable’s row of one item 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.

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 every Deployment stay unchanged. Runtime errors and unavailable data can still occur after a successful build and are shown in the content area. A move to another Page of the same build is quick, because the frame already holds its code. 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 browser already read answers at once, also after a reload, so a return to a Page does not wait for its data; see Plots for what drops a kept result. Newer rows of a live Plot replace the old ones in place, and the platform moves the chart’s marks to their new values. With reduced motion the switch is immediate and marks move at once. 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. 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. The frame tells the app where the reader’s pointer is, never what is under it: the Pane, or the Page away from every Pane, and fractions of its box. No row, label, value, element or text leaves the frame for presence. A Provider Policy can give two readers different rows under one point, so each person’s screen shows only its own content under another person’s cursor. 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 column, which the frame cannot reach. A Plotted Path is admitted like the Pane’s data: the reader’s seat and the Pane’s view.pane. It never shows SQL, never names a part fewer people may view than the Pane, and links only parts the reader may open. Under View as a Pane gets no Ponder, because Ponder keeps working as the previewer. Admin Ponder admits staff to a Plotted Path by the same rule (PARABLE-6962). Staff need a seat on the Parable in the selected Workspace, and no staff permission replaces it. Staff with no seat there cannot open the Parable in Admin Ponder, and the server refuses their Plotted Path too. An unsaved edit previews in a live bundler. A saved Page uses its compiled build. Both 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.