Skill 24 · Composing Grid Canvases
Subchapter 24.1
references/component-example.mdMarkdown6 KBView on GitHub
This is a complete, buildable component project — the welcome checklist PostHog seeds onto new home canvases (products/canvas/backend/welcome.py, verified against the real canvas builder).
The envelope below shows the fields you author; its index.html and dependencies come from and are kept exactly as returned (see ) — the placeholders below stand in for them.
Start from it when building a checklist, settings, or any state-carrying widget, and keep its structure even when you replace the content: the project envelope, the placement contract, the capability declarations, and the defensive access are the parts that break when improvised.
canvas-source-retrieveph.state{
"schemaVersion": 1,
"entryHtml": "index.html",
"files": {
"index.html": "<synthetic shell from canvas-source-retrieve — loads /src/canvas.tsx as a module>",
"src/canvas.tsx": "<the component below>"
},
"dependencies": {
"react": "<pinned>",
"react-dom": "<pinned>",
"@posthog/quill": "<pinned>",
"lucide-react": "<pinned>"
},
"capabilities": {
"posthog": { "insights": [], "inlineQueries": false, "captureEvents": [], "state": ["user"], "actions": [] },
"network": { "origins": [] }
},
"component": { "size": { "defaultW": 3, "defaultH": 5, "minW": 2, "minH": 3 } }
}index.html must load /src/canvas.tsx as a module and every imported package must be in dependencies — the retrieved shell and dependency map already satisfy both. Improvising a comment-only entry fails the build with no_entry_module; dropping the @posthog/quill or lucide-react entries fails it with import_not_declared.capabilities.posthog shape (insights, inlineQueries, captureEvents, state, actions) even when a field is empty, as this example does — the build freezes the declared capabilities into the artifact manifest, which older clients read expecting every field. insights, inlineQueries, and captureEvents are also required by the API: a partial posthog object is rejected with a 400 before source validation runs. Populate only what the code calls and leave the rest empty.capabilities.posthog.state must name every scope the code passes to ph.state.* — validation rejects an undeclared scope.component.size is in grid units and advisory; the component still has to render at any size the user drags.import { useEffect, useState } from 'react'
import { Checkbox, SkeletonText, Text, Tooltip, TooltipContent, TooltipTrigger } from '@posthog/quill'
import { Info } from 'lucide-react'
const ITEMS = [
{ id: 'download-desktop', label: 'Download PostHog Desktop', hint: null },
{
id: 'add-widget',
label: 'Add a widget to this canvas',
hint: 'Select Edit in the top right, then click and drag anywhere on the dotted grid and describe what should go there.',
},
export default React component, no props, no createRoot — the host mounts it.react, @posthog/quill, lucide-react, recharts, dayjs).ph is a host-injected global — never import it, and feature-detect optional surfaces like ph.state so the widget degrades instead of crashing on an older runtime..catch that lands in a renderable state.min-h-screen and let the iframe document scroll.
h-screen caps the flex column, so tall children shrink and clip; h-full cannot resolve on the root because the published artifact shell has no explicit height.