makersclaw 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/package.json +1 -1
- package/r/annotate.json +4 -4
- package/r/approval-request.json +2 -2
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +5 -5
- package/r/chat.json +3 -3
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/contact-section.json +2 -2
- package/r/core.json +7 -1
- package/r/csv-import.json +3 -3
- package/r/data-table.json +2 -2
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/flow-canvas.json +3 -3
- package/r/footer.json +3 -3
- package/r/gantt.json +3 -3
- package/r/job-panel.json +2 -2
- package/r/json-editor.json +3 -3
- package/r/map.json +2 -2
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +3 -3
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +3 -3
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-notes.json +4 -4
- package/r/registry.json +126 -121
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +3 -3
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +3 -3
- package/r/spreadsheet.json +2 -2
- package/r/stat-cards.json +3 -3
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +2 -2
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +14 -11
- package/registry/blocks/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +4 -1
- package/registry/blocks/chat/demo.jsx +2 -2
- package/registry/blocks/flow-canvas/README.md +15 -8
- package/registry/blocks/footer/README.md +3 -2
- package/registry/blocks/mind-map/README.md +4 -2
- package/registry/blocks/org-chart/README.md +4 -2
- package/registry/blocks/page-builder/README.md +1 -1
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/pdf-viewer/README.md +4 -1
- package/registry/blocks/sidebar/README.md +23 -13
- package/registry/blocks/sketch-canvas/README.md +15 -9
- package/registry/blocks/whiteboard/README.md +20 -14
- package/src/cli.mjs +2 -2
- package/src/install.mjs +23 -6
- package/src/registry.mjs +34 -3
- package/templates/next/files/README.md +291 -11
- package/templates/next/files/app/shell.jsx +54 -37
- package/templates/next/files/check/run.mjs +76 -4
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/layout.jsx +307 -0
- package/templates/next/files/lib/claw.js +4 -1
- package/templates/next/files/lib/hooks.js +71 -2
- package/templates/next/files/lib/platform.js +93 -4
- package/templates/next/template.json +10 -1
package/r/chat.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "Chat",
|
|
6
6
|
"description": "The parts of a chat: messages with avatars, times, grouping, sending/failed/streaming states, hover actions, thumbs up/down feedback, in-place editing and a folding reasoning section; a scroller that sticks to the bottom with day separators and older-message loading; a composer (a box or an \"ask anything\" pill) with attachments, a + menu, tools, suggestion chips, Enter to send and Stop; a header; and ChatThread, which composes them from messages + onSend.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"path": "registry/blocks/chat/README.md",
|
|
62
62
|
"type": "registry:file",
|
|
63
63
|
"target": "@components/makersclaw/blocks/chat/README.md",
|
|
64
|
-
"content": "# Chat\n\nThe parts of a chat, and a thread made of them. `Message` is one turn:\navatar, name, time, the words in a `Bubble`, its files, its state\n(sending, streaming, failed with Retry) and the actions that appear on\nhover or focus (copy, regenerate, a menu of yours). `MessageScroller` is\nthe list: it keeps to the bottom while a reply streams unless the reader\nscrolled up, then offers \"Jump to latest\"; it puts a day between days,\nruns consecutive messages from one author together, loads older messages\nfrom the top and says each finished reply to a screen reader.\n`Composer` is the input: it grows with the text, Enter sends,\nShift+Enter is a new line, files come by paperclip, paste or drop, and\nSend becomes Stop while a reply streams; it comes as a box or as the\nround \"ask anything\" pill, with a \"+\" menu, your tools in its bar and\nsuggestion chips. An assistant reply can fold its reasoning away\n(\"Thought for 4s\") and take a thumbs up or down; a user message can be\nedited in place. `ChatHeader` is the strip over a thread (name, status,\nactions). `Attachment` is one file. `ChatThread` puts them together from\n`messages` and `onSend`.\n\n```jsx\nimport { ChatThread } from \"../components/blocks/chat\"; // a Makersclaw app (\"./blocks/chat\" from components/)\nimport { ChatThread } from \"@/components/makersclaw/blocks/chat\"; // a shadcn project\n```\n\nEvery part is a client component: render them from a `\"use client\"`\ncomponent. The block imports nothing but React and the kit — it holds no\nconversation and calls no model. The messages come in as a prop and the\nsend goes out as `onSend`; where the reply comes from (your server action,\nthe AI SDK's `useChat`, a support inbox table) is the surface's.\n\nThis is not the app window's Agent tab — the OS draws that. It is for a\nconversation that lives on your own screen: a support reply pane, a\ncustomer-facing assistant, comments on a record.\n\n## The data\n\nA message:\n\n```js\n{\n id: \"m5\", // stable; the list is keyed on it\n role: \"user\", // user · assistant · system\n content: \"It's still showing…\", // a string — or `parts: [{ type: \"text\", text }]`\n createdAt: \"2026-09-23T10:04:00Z\",\n author: { name: \"Maya Chen\", src: \"https://…\" }, // optional\n status: \"failed\", // sending · streaming · failed · nothing (sent)\n error: \"you are offline\", // shown with a failed message\n attachments: [{ id, name, size, type, url }], // size in bytes; url shows an image's thumbnail\n reasoning: \"They asked about the double charge…\", // assistant only: folded over the reply\n reasoningDuration: 4, // seconds: \"Thought for 4s\"\n feedback: \"up\", // up · down — the thumbs already given (assistant only)\n}\n```\n\n`reasoning` is drawn in a kit `Collapsible`, closed, over the reply.\nWhile the message streams with no answer yet it reads \"Thinking…\"; give\n`reasoning: \"\"` from the start to show that line instead of the three\ndots.\n\nA message with `parts` (the AI SDK's shape) is drawn from its text parts;\nother parts are not drawn. A `system` message is a centred muted line.\nAn assistant message with `status: \"streaming\"` and no text yet shows\nthree dots; with text it shows a caret after it.\n\nThe words are drawn by a small safe renderer — never as HTML, and with no\nmarkdown library. It understands fenced code blocks (with a Copy button;\nan unclosed fence runs to the end, so a block still streaming already\nlooks like one), `code spans`, `**bold**` and line breaks. Everything else\n(links, lists, headings) is plain text. `<script>` in a message is shown,\nnot run.\n\n## Props\n\n### ChatThread\n\n- `messages` — the conversation, oldest first.\n- `onSend(text, attachments)` — send it. The composer clears at once;\n return `{ ok: false, error }` (or throw) and the text comes back to the\n field with the reason under it. The usual pattern is to add the message\n with `status: \"sending\"` here, then mark it sent or `failed`.\n- `streaming` — a reply is streaming: Send becomes Stop, Esc stops.\n Default: true while any message has `status: \"streaming\"`.\n- `onStop()` — stop the reply.\n- `onRetry(message)` — the Retry on a failed message. Without it, a\n failed message says so and offers nothing.\n- `onRegenerate(message)` — a regenerate button on assistant messages.\n- `actionsOf(message)` — more items for a message's menu, kit `Menu`\n shape: `[{ label, onSelect, icon?, tone?: \"danger\" }]`.\n- `onFeedback(message, \"up\" | \"down\" | null)` — thumbs up and down on\n assistant replies (pressing the chosen one again clears it: `null`).\n The choice shows at once; return `{ ok: false, error }` (or throw) and\n it goes back, with the reason under the message. Store it and pass it\n back as `message.feedback`.\n- `onEdit(message, text)` — an edit button on your own messages. The\n words come back in a field: Enter or Save saves, Esc or Cancel leaves\n them. Return `{ ok: false, error }` and the field stays open with the\n reason. Up arrow in an empty composer edits the last message you sent.\n What an edit does next (drop the replies after it, ask again) is yours.\n- `header` — a node over the list, or the props of a `ChatHeader`\n (`{ title, status, actions, menu }`).\n- `shape` — the composer: `\"box\"` (default) or `\"pill\"` (see Composer).\n- `tools` · `menu` — the composer's bar: nodes of yours, and kit `Menu`\n items behind a \"+\" (see Composer).\n- `suggestions` — prompt chips over the composer:\n `[{ label, prompt?, icon?, description? }]`. Shown while the thread is\n empty; `suggestionsWhen=\"always\"` keeps them. `suggestionMode`\n `\"send\"` (the default here: a press sends `prompt ?? label`) or\n `\"fill\"` (puts it in the field). `suggestionsLook` `\"chips\"` (default)\n or `\"cards\"`.\n- `onLoadMore()` · `hasMore` (false) · `loadingMore` (false) — older\n messages. Reaching the top (or pressing \"Load earlier messages\") calls\n `onLoadMore`; prepend them to `messages` and the reader keeps their place.\n- `attachments` · `onAttach(files)` · `onRemoveAttachment(att)` · `accept`\n — files waiting in the composer. `onAttach` gets the browser's `File`s:\n upload them and pass them back in `attachments` as\n `{ id, name, size, type, url?, progress?, error? }` (`progress` 0–100\n draws a bar; Send waits until every upload is at 100). Without\n `onAttach` there is no paperclip. They reach `onSend` as its second\n argument.\n- `disabled` (false) · `disabledPlaceholder` · `placeholder` — a closed\n or read-only thread.\n- `variant` — how assistant words sit: `\"plain\"` (default, on the page)\n or `\"bubble\"`. User messages are always filled bubbles at the end.\n- `assistantName` (\"Assistant\") · `assistantAvatar` · `userName` (\"You\")\n · `userAvatar` · `showUserAvatar` (false) — who is who, when a message\n has no `author`.\n- `emptyTitle` · `emptyBody` · `emptyState` — what an empty thread says;\n `emptyState` replaces the whole thing.\n- `height` — default `\"min(40rem, 80dvh)\"`; the list scrolls inside it.\n- `label` — what the thread is called to a screen reader.\n- `className` — layout around it (width, margin). Never colour.\n\n### Message\n\n`message` (the shape above), `first` / `last` (its place in a run from\none author: `first` shows the avatar, name and time), `latest` (the\nnewest reply: its actions stay in view instead of waiting for hover), and `variant`,\n`assistantName`, `assistantAvatar`, `userName`, `userAvatar`,\n`showUserAvatar`, `onRetry`, `onRegenerate`, `actionsOf`, `onFeedback`,\n`onEdit` as above. `editing` / `onEditingChange(next)` control the edit\nfield (leave them out and the message keeps its own).\n`onCopy(message, text)` replaces the clipboard write; `formatTime(date)`\nreplaces the time's format.\n\n### Bubble\n\n`from` (`\"user\"` · `\"assistant\"`), `variant` (`\"bubble\"` · `\"plain\"`),\n`first` / `last`, `streaming` (draws the caret), and `text` (drawn by the\nsafe renderer) or `children`.\n\n### ChatHeader\n\n`title`, then `status` (`{ tone: \"success\", label: \"Online\", pulse? }`,\na kit `StatusDot`) or `subtitle` (a muted line) under it, `avatar`\n(`{ name?, src?, status? }`), `actions` (nodes at the end: a \"New chat\"\nButton), `menu` (kit `Menu` items behind a \"⋯\") and `menuLabel`.\n\n### Attachment\n\n`name`, `size` (bytes), `type` (MIME; the extension is used when it is\nmissing), `url` (an image shows itself; anything else opens it),\n`href` (open this instead), `progress` (0–100, a bar until 100), `error`,\n`onRemove`, `removeLabel`. An image's thumbnail is a kit `Image`: a\nskeleton while it loads, the image glyph if it fails.\n\n### MessageScroller\n\n`messages`, `renderMessage(message, { first, last, latest })` (default: a plain\n`Message`), `idOf(message, i)`, `onLoadMore` · `hasMore` · `loadingMore`,\n`emptyState`, `formatDay(date)` (\"Today\", \"Yesterday\", \"Mon 21 Sep\"),\n`announce(message)` (what a screen reader hears when a reply is whole),\n`jumpLabel`, `loadMoreLabel`, `label`, `className` (give it a bounded\nheight, or put it in a flex column), `children` (drawn after the last\nmessage — a typing line, say).\n\nThe list is a kit `ScrollArea`. Once it overflows it joins the tab order\nas a region named `label`, so a keyboard can scroll it.\n\n### Composer\n\n`onSend`, `onStop`, `streaming`, `disabled`, `disabledPlaceholder`,\n`placeholder`, `attachments` · `onAttach` · `onRemoveAttachment` ·\n`accept` as in ChatThread, and `value` / `onValueChange` (control the\ntext — to quote a message into it, say), `maxRows` (8), `canSendEmpty`\n(false), `label` (\"Message\"), `hint` (true: the Enter / Shift+Enter keys\nunder the field, from md up), `autoFocus`, and:\n\n- `shape` — `\"box\"` (default: the text on top, the bar under it) or\n `\"pill\"`: the \"ask anything\" box, one round row with the \"+\" menu and\n the paperclip before the text and `tools` and Send after it. Past one\n line (or with files waiting) it opens into a rounded box, text on top;\n emptied, it is a pill again. The pill shows no key hint.\n- `tools` — nodes in the bar: a model `Select`, a kit `Menu` of\n settings with checkbox items, a `Toggle` for \"Auto\", a mic. The\n composer draws them; what they choose is yours (read it in `onSend`).\n- `menu` · `menuLabel` (\"Add to the message\") — kit `Menu` items behind\n a \"+\" at the start of the bar: \"Add files\", \"Search the web\".\n- `suggestions` — `[{ label, prompt?, icon?, description?, onSelect? }]`,\n drawn `suggestionsPlacement` `\"below\"` (default) or `\"above\"` the\n field. `suggestionMode` `\"fill\"` (default: the prompt goes in the\n field, caret at its end) or `\"send\"`; an item's own `onSelect(item)`\n replaces both. `suggestionsLook` `\"chips\"` (round, centred) or\n `\"cards\"` (a two-column grid with `description` under each label).\n `suggestionsLabel` names the list to a reader.\n- `onEditLast()` — Up arrow in the empty field (ChatThread opens the last\n message you sent for editing).\n\nThe field is the kit's multi-line `InputGroup`: the waiting files sit\nabove the text inside its one border, the paperclip (a kit `FileInput`)\nand the hint in its bar, Send at the end. The picker leaves out files\n`accept` rules out; a drop or a paste is passed on as it comes.\n\n## Example\n\n```jsx\n// surfaces/ticket.jsx — client: one ticket's conversation\n\"use client\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { loadRows, useRow, useRows, whenLanded } from \"../lib/data\";\nimport { insertRow } from \"../lib/actions\";\nimport { ChatThread } from \"../components/blocks/chat\";\n\nconst source = \"ticket_messages\";\nconst PAGE = 200;\nconst oldestFirst = (rows) => [...rows].reverse();\nconst asMessage = (r) => ({ id: r.id, role: r.from_agent ? \"assistant\" : \"user\", content: r.body, createdAt: r.created_at });\n\nexport default function SupportChat() {\n const id = useSearchParams().get(\"row\");\n const { row: ticket } = useRow({ source: \"tickets\", id });\n // The newest page, read again after every write.\n const { rows, loading } = useRows({ source, filter: { ticket_id: id }, orderBy: \"created_at desc\", limit: PAGE });\n const [earlier, setEarlier] = useState({ rows: [], more: true }); // what onLoadMore read, oldest first\n const [sending, setSending] = useState([]); // sent, not read back yet\n const seen = new Map([...earlier.rows, ...oldestFirst(rows)].map((r) => [r.id, r]));\n const shown = [...seen.values()];\n\n async function loadMore() {\n const oldest = shown[0]?.created_at;\n if (!oldest) return;\n const r = await loadRows({ source, filter: { ticket_id: id, created_at: { lt: oldest } }, orderBy: \"created_at desc\", limit: PAGE });\n // What is on screen is kept too: new messages push the oldest out of the page above.\n if (!r.error) setEarlier({ rows: [...oldestFirst(r.rows), ...shown], more: r.rows.length === PAGE });\n }\n\n async function send(text) {\n const mid = crypto.randomUUID();\n setSending((all) => [...all, { id: mid, role: \"user\", content: text, createdAt: new Date().toISOString(), status: \"sending\" }]);\n const r = await insertRow({ source, values: { id: mid, ticket_id: id, body: text }, detail: text });\n if (r.ok) await whenLanded(); // read back: it is in `rows` now\n setSending((all) => (r.ok ? all.filter((m) => m.id !== mid) : all.map((m) => (m.id === mid ? { ...m, status: \"failed\", error: r.error } : m))));\n }\n\n if (loading || !ticket) return null;\n return (\n <ChatThread\n messages={[...shown.map(asMessage), ...sending.filter((m) => !seen.has(m.id))]}\n onSend={send}\n onRetry={(m) => { setSending((all) => all.filter((x) => x.id !== m.id)); send(m.content); }}\n onLoadMore={loadMore}\n hasMore={rows.length === PAGE && earlier.more}\n assistantName={ticket.agent_name}\n userName=\"You\"\n label={`Ticket ${ticket.number}`}\n />\n );\n}\n```\n\n`onLoadMore` reads the page before the oldest message shown:\n`loadRows({ source, filter: { ticket_id, created_at: { lt: oldest } } })`,\nthe same filter in a server app's `loadRows` and the door.\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nWith the AI SDK in a shadcn project, `useChat()` already has the shape:\n`<ChatThread messages={messages} onSend={(text) => sendMessage({ text })}\nstreaming={status === \"streaming\" || status === \"submitted\"} onStop={stop}\nonRegenerate={() => regenerate()} />`.\n\n## What it will not do\n\n- Call a model, store a message or upload a file: those are the\n surface's, through `onSend`, `onAttach` and the rows it passes back.\n- Render HTML or full markdown: links, lists, tables and images inside\n the text stay plain text.\n- Keep the feedback or the edit: `onFeedback` and `onEdit` hand them to\n you, and the rows you pass back are what shows. Emoji reactions are\n yours, in `actionsOf`.\n- Record audio or pick a model: a mic or a model `Select` is a node you\n put in `tools`.\n- Draw the app window's Agent tab, or a job the founder commissioned —\n that is `job-panel`.\n\nA chat it cannot be is your own component in `components/`, built from\nthese parts and the kit — not an edit to this block. Its files are fixed:\n`npm run check` refuses a change, and `makersclaw add chat` restores them.\n\n## Credits\n\nThe pill composer, tools, suggestions, feedback, edit, reasoning and header are ported from blocks.so (https://blocks.so, ai-01–05 and chat-01–03, MIT) and Creative Tim UI (https://github.com/creativetimofficial/ui, ai-chat-streaming-01, MIT), rewritten on the Makersclaw kit.\n"
|
|
64
|
+
"content": "# Chat\n\nThe parts of a chat, and a thread made of them. `Message` is one turn:\navatar, name, time, the words in a `Bubble`, its files, its state\n(sending, streaming, failed with Retry) and the actions that appear on\nhover or focus (copy, regenerate, a menu of yours). `MessageScroller` is\nthe list: it keeps to the bottom while a reply streams unless the reader\nscrolled up, then offers \"Jump to latest\"; it puts a day between days,\nruns consecutive messages from one author together, loads older messages\nfrom the top and says each finished reply to a screen reader.\n`Composer` is the input: it grows with the text, Enter sends,\nShift+Enter is a new line, files come by paperclip, paste or drop, and\nSend becomes Stop while a reply streams; it comes as a box or as the\nround \"ask anything\" pill, with a \"+\" menu, your tools in its bar and\nsuggestion chips. An assistant reply can fold its reasoning away\n(\"Thought for 4s\") and take a thumbs up or down; a user message can be\nedited in place. `ChatHeader` is the strip over a thread (name, status,\nactions). `Attachment` is one file. `ChatThread` puts them together from\n`messages` and `onSend`.\n\n```jsx\nimport { ChatThread } from \"../components/blocks/chat\"; // a Makersclaw app (\"./blocks/chat\" from components/)\nimport { ChatThread } from \"@/components/makersclaw/blocks/chat\"; // a shadcn project\n```\n\nEvery part is a client component: render them from a `\"use client\"`\ncomponent. The block imports nothing but React and the kit — it holds no\nconversation and calls no model. The messages come in as a prop and the\nsend goes out as `onSend`; where the reply comes from (your server action,\nthe AI SDK's `useChat`, a support inbox table) is the surface's.\n\nThis is not the app window's Agent tab — the OS draws that. It is for a\nconversation that lives on your own screen: a support reply pane, a\ncustomer-facing assistant, comments on a record.\n\n## The data\n\nA message:\n\n```js\n{\n id: \"m5\", // stable; the list is keyed on it\n role: \"user\", // user · assistant · system\n content: \"It's still showing…\", // a string — or `parts: [{ type: \"text\", text }]`\n createdAt: \"2026-09-23T10:04:00Z\",\n author: { name: \"Maya Chen\", src: \"https://…\" }, // optional\n status: \"failed\", // sending · streaming · failed · nothing (sent)\n error: \"you are offline\", // shown with a failed message\n attachments: [{ id, name, size, type, url }], // size in bytes; url shows an image's thumbnail\n reasoning: \"They asked about the double charge…\", // assistant only: folded over the reply\n reasoningDuration: 4, // seconds: \"Thought for 4s\"\n feedback: \"up\", // up · down — the thumbs already given (assistant only)\n}\n```\n\n`reasoning` is drawn in a kit `Collapsible`, closed, over the reply.\nWhile the message streams with no answer yet it reads \"Thinking…\"; give\n`reasoning: \"\"` from the start to show that line instead of the three\ndots.\n\nA message with `parts` (the AI SDK's shape) is drawn from its text parts;\nother parts are not drawn. A `system` message is a centred muted line.\nAn assistant message with `status: \"streaming\"` and no text yet shows\nthree dots; with text it shows a caret after it.\n\nThe words are drawn by a small safe renderer — never as HTML, and with no\nmarkdown library. It understands fenced code blocks (with a Copy button;\nan unclosed fence runs to the end, so a block still streaming already\nlooks like one), `code spans`, `**bold**` and line breaks. Everything else\n(links, lists, headings) is plain text. `<script>` in a message is shown,\nnot run.\n\n## Props\n\n### ChatThread\n\n- `messages` — the conversation, oldest first.\n- `onSend(text, attachments)` — send it. The composer clears at once;\n return `{ ok: false, error }` (or throw) and the text comes back to the\n field with the reason under it. The usual pattern is to add the message\n with `status: \"sending\"` here, then mark it sent or `failed`.\n- `streaming` — a reply is streaming: Send becomes Stop, Esc stops.\n Default: true while any message has `status: \"streaming\"`.\n- `onStop()` — stop the reply.\n- `onRetry(message)` — the Retry on a failed message. Without it, a\n failed message says so and offers nothing.\n- `onRegenerate(message)` — a regenerate button on assistant messages.\n- `actionsOf(message)` — more items for a message's menu, kit `Menu`\n shape: `[{ label, onSelect, icon?, tone?: \"danger\" }]`.\n- `onFeedback(message, \"up\" | \"down\" | null)` — thumbs up and down on\n assistant replies (pressing the chosen one again clears it: `null`).\n The choice shows at once; return `{ ok: false, error }` (or throw) and\n it goes back, with the reason under the message. Store it and pass it\n back as `message.feedback`.\n- `onEdit(message, text)` — an edit button on your own messages. The\n words come back in a field: Enter or Save saves, Esc or Cancel leaves\n them. Return `{ ok: false, error }` and the field stays open with the\n reason. Up arrow in an empty composer edits the last message you sent.\n What an edit does next (drop the replies after it, ask again) is yours.\n- `header` — a node over the list, or the props of a `ChatHeader`\n (`{ title, status, actions, menu }`).\n- `shape` — the composer: `\"box\"` (default) or `\"pill\"` (see Composer).\n- `tools` · `menu` — the composer's bar: nodes of yours, and kit `Menu`\n items behind a \"+\" (see Composer).\n- `suggestions` — prompt chips over the composer:\n `[{ label, prompt?, icon?, description? }]`. Shown while the thread is\n empty; `suggestionsWhen=\"always\"` keeps them. `suggestionMode`\n `\"send\"` (the default here: a press sends `prompt ?? label`) or\n `\"fill\"` (puts it in the field). `suggestionsLook` `\"chips\"` (default)\n or `\"cards\"`.\n- `onLoadMore()` · `hasMore` (false) · `loadingMore` (false) — older\n messages. Reaching the top (or pressing \"Load earlier messages\") calls\n `onLoadMore`; prepend them to `messages` and the reader keeps their place.\n- `attachments` · `onAttach(files)` · `onRemoveAttachment(att)` · `accept`\n — files waiting in the composer. `onAttach` gets the browser's `File`s:\n upload them and pass them back in `attachments` as\n `{ id, name, size, type, url?, progress?, error? }` (`progress` 0–100\n draws a bar; Send waits until every upload is at 100). Without\n `onAttach` there is no paperclip. They reach `onSend` as its second\n argument.\n- `disabled` (false) · `disabledPlaceholder` · `placeholder` — a closed\n or read-only thread.\n- `variant` — how assistant words sit: `\"plain\"` (default, on the page)\n or `\"bubble\"`. User messages are always filled bubbles at the end.\n- `assistantName` (\"Assistant\") · `assistantAvatar` · `userName` (\"You\")\n · `userAvatar` · `showUserAvatar` (false) — who is who, when a message\n has no `author`.\n- `emptyTitle` · `emptyBody` · `emptyState` — what an empty thread says;\n `emptyState` replaces the whole thing.\n- `height` — default `\"min(40rem, 80dvh)\"` (the block's own); the list\n scrolls inside it. Yours is a length (`\"40rem\"`) or `\"100%\"` in a sized\n box, such as the kit's `AppLayout contentMode=\"fill\"`: a `vh` length in\n your code is refused by `npm run check`.\n- `label` — what the thread is called to a screen reader.\n- `className` — layout around it (width, margin). Never colour.\n\n### Message\n\n`message` (the shape above), `first` / `last` (its place in a run from\none author: `first` shows the avatar, name and time), `latest` (the\nnewest reply: its actions stay in view instead of waiting for hover), and `variant`,\n`assistantName`, `assistantAvatar`, `userName`, `userAvatar`,\n`showUserAvatar`, `onRetry`, `onRegenerate`, `actionsOf`, `onFeedback`,\n`onEdit` as above. `editing` / `onEditingChange(next)` control the edit\nfield (leave them out and the message keeps its own).\n`onCopy(message, text)` replaces the clipboard write; `formatTime(date)`\nreplaces the time's format.\n\n### Bubble\n\n`from` (`\"user\"` · `\"assistant\"`), `variant` (`\"bubble\"` · `\"plain\"`),\n`first` / `last`, `streaming` (draws the caret), and `text` (drawn by the\nsafe renderer) or `children`.\n\n### ChatHeader\n\n`title`, then `status` (`{ tone: \"success\", label: \"Online\", pulse? }`,\na kit `StatusDot`) or `subtitle` (a muted line) under it, `avatar`\n(`{ name?, src?, status? }`), `actions` (nodes at the end: a \"New chat\"\nButton), `menu` (kit `Menu` items behind a \"⋯\") and `menuLabel`.\n\n### Attachment\n\n`name`, `size` (bytes), `type` (MIME; the extension is used when it is\nmissing), `url` (an image shows itself; anything else opens it),\n`href` (open this instead), `progress` (0–100, a bar until 100), `error`,\n`onRemove`, `removeLabel`. An image's thumbnail is a kit `Image`: a\nskeleton while it loads, the image glyph if it fails.\n\n### MessageScroller\n\n`messages`, `renderMessage(message, { first, last, latest })` (default: a plain\n`Message`), `idOf(message, i)`, `onLoadMore` · `hasMore` · `loadingMore`,\n`emptyState`, `formatDay(date)` (\"Today\", \"Yesterday\", \"Mon 21 Sep\"),\n`announce(message)` (what a screen reader hears when a reply is whole),\n`jumpLabel`, `loadMoreLabel`, `label`, `className` (give it a bounded\nheight, or put it in a flex column), `children` (drawn after the last\nmessage — a typing line, say).\n\nThe list is a kit `ScrollArea`. Once it overflows it joins the tab order\nas a region named `label`, so a keyboard can scroll it.\n\n### Composer\n\n`onSend`, `onStop`, `streaming`, `disabled`, `disabledPlaceholder`,\n`placeholder`, `attachments` · `onAttach` · `onRemoveAttachment` ·\n`accept` as in ChatThread, and `value` / `onValueChange` (control the\ntext — to quote a message into it, say), `maxRows` (8), `canSendEmpty`\n(false), `label` (\"Message\"), `hint` (true: the Enter / Shift+Enter keys\nunder the field, from md up), `autoFocus`, and:\n\n- `shape` — `\"box\"` (default: the text on top, the bar under it) or\n `\"pill\"`: the \"ask anything\" box, one round row with the \"+\" menu and\n the paperclip before the text and `tools` and Send after it. Past one\n line (or with files waiting) it opens into a rounded box, text on top;\n emptied, it is a pill again. The pill shows no key hint.\n- `tools` — nodes in the bar: a model `Select`, a kit `Menu` of\n settings with checkbox items, a `Toggle` for \"Auto\", a mic. The\n composer draws them; what they choose is yours (read it in `onSend`).\n- `menu` · `menuLabel` (\"Add to the message\") — kit `Menu` items behind\n a \"+\" at the start of the bar: \"Add files\", \"Search the web\".\n- `suggestions` — `[{ label, prompt?, icon?, description?, onSelect? }]`,\n drawn `suggestionsPlacement` `\"below\"` (default) or `\"above\"` the\n field. `suggestionMode` `\"fill\"` (default: the prompt goes in the\n field, caret at its end) or `\"send\"`; an item's own `onSelect(item)`\n replaces both. `suggestionsLook` `\"chips\"` (round, centred) or\n `\"cards\"` (a two-column grid with `description` under each label).\n `suggestionsLabel` names the list to a reader.\n- `onEditLast()` — Up arrow in the empty field (ChatThread opens the last\n message you sent for editing).\n\nThe field is the kit's multi-line `InputGroup`: the waiting files sit\nabove the text inside its one border, the paperclip (a kit `FileInput`)\nand the hint in its bar, Send at the end. The picker leaves out files\n`accept` rules out; a drop or a paste is passed on as it comes.\n\n## Example\n\n```jsx\n// surfaces/ticket.jsx — client: one ticket's conversation\n\"use client\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { loadRows, useRow, useRows, whenLanded } from \"../lib/data\";\nimport { insertRow } from \"../lib/actions\";\nimport { ChatThread } from \"../components/blocks/chat\";\n\nconst source = \"ticket_messages\";\nconst PAGE = 200;\nconst oldestFirst = (rows) => [...rows].reverse();\nconst asMessage = (r) => ({ id: r.id, role: r.from_agent ? \"assistant\" : \"user\", content: r.body, createdAt: r.created_at });\n\nexport default function SupportChat() {\n const id = useSearchParams().get(\"row\");\n const { row: ticket } = useRow({ source: \"tickets\", id });\n // The newest page, read again after every write.\n const { rows, loading } = useRows({ source, filter: { ticket_id: id }, orderBy: \"created_at desc\", limit: PAGE });\n const [earlier, setEarlier] = useState({ rows: [], more: true }); // what onLoadMore read, oldest first\n const [sending, setSending] = useState([]); // sent, not read back yet\n const seen = new Map([...earlier.rows, ...oldestFirst(rows)].map((r) => [r.id, r]));\n const shown = [...seen.values()];\n\n async function loadMore() {\n const oldest = shown[0]?.created_at;\n if (!oldest) return;\n const r = await loadRows({ source, filter: { ticket_id: id, created_at: { lt: oldest } }, orderBy: \"created_at desc\", limit: PAGE });\n // What is on screen is kept too: new messages push the oldest out of the page above.\n if (!r.error) setEarlier({ rows: [...oldestFirst(r.rows), ...shown], more: r.rows.length === PAGE });\n }\n\n async function send(text) {\n const mid = crypto.randomUUID();\n setSending((all) => [...all, { id: mid, role: \"user\", content: text, createdAt: new Date().toISOString(), status: \"sending\" }]);\n const r = await insertRow({ source, values: { id: mid, ticket_id: id, body: text }, detail: text });\n if (r.ok) await whenLanded(); // read back: it is in `rows` now\n setSending((all) => (r.ok ? all.filter((m) => m.id !== mid) : all.map((m) => (m.id === mid ? { ...m, status: \"failed\", error: r.error } : m))));\n }\n\n if (loading || !ticket) return null;\n return (\n <ChatThread\n messages={[...shown.map(asMessage), ...sending.filter((m) => !seen.has(m.id))]}\n onSend={send}\n onRetry={(m) => { setSending((all) => all.filter((x) => x.id !== m.id)); send(m.content); }}\n onLoadMore={loadMore}\n hasMore={rows.length === PAGE && earlier.more}\n assistantName={ticket.agent_name}\n userName=\"You\"\n label={`Ticket ${ticket.number}`}\n />\n );\n}\n```\n\n`onLoadMore` reads the page before the oldest message shown:\n`loadRows({ source, filter: { ticket_id, created_at: { lt: oldest } } })`,\nthe same filter in a server app's `loadRows` and the door.\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nWith the AI SDK in a shadcn project, `useChat()` already has the shape:\n`<ChatThread messages={messages} onSend={(text) => sendMessage({ text })}\nstreaming={status === \"streaming\" || status === \"submitted\"} onStop={stop}\nonRegenerate={() => regenerate()} />`.\n\n## What it will not do\n\n- Call a model, store a message or upload a file: those are the\n surface's, through `onSend`, `onAttach` and the rows it passes back.\n- Render HTML or full markdown: links, lists, tables and images inside\n the text stay plain text.\n- Keep the feedback or the edit: `onFeedback` and `onEdit` hand them to\n you, and the rows you pass back are what shows. Emoji reactions are\n yours, in `actionsOf`.\n- Record audio or pick a model: a mic or a model `Select` is a node you\n put in `tools`.\n- Draw the app window's Agent tab, or a job the founder commissioned —\n that is `job-panel`.\n\nA chat it cannot be is your own component in `components/`, built from\nthese parts and the kit — not an edit to this block. Its files are fixed:\n`npm run check` refuses a change, and `makersclaw add chat` restores them.\n\n## Credits\n\nThe pill composer, tools, suggestions, feedback, edit, reasoning and header are ported from blocks.so (https://blocks.so, ai-01–05 and chat-01–03, MIT) and Creative Tim UI (https://github.com/creativetimofficial/ui, ai-chat-streaming-01, MIT), rewritten on the Makersclaw kit.\n"
|
|
65
65
|
}
|
|
66
66
|
],
|
|
67
67
|
"categories": [
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"docs": "Installed to components/makersclaw/blocks/chat. Import: import { ChatThread, ChatHeader, Message, Bubble, Attachment, MessageScroller, Composer } from \"@/components/makersclaw/blocks/chat\". Props and an example: components/makersclaw/blocks/chat/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
89
89
|
"meta": {
|
|
90
90
|
"makersclaw": {
|
|
91
|
-
"version": "0.
|
|
91
|
+
"version": "0.8.0",
|
|
92
92
|
"useWhen": "A conversation lives inside the app's own screen: a support inbox reply pane, a customer-facing assistant, comments between teammates on a record, a thread per object. Not the app window's Agent tab (the OS draws that) and not a one-off job the founder commissions (job-panel)."
|
|
93
93
|
}
|
|
94
94
|
}
|
package/r/code-editor.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "CodeEditor",
|
|
6
6
|
"description": "A code editor (CodeMirror 6) in the workspace theme: SQL with the database's tables and columns in autocomplete, JSON checked as you type with plain-words errors, or plain text; line numbers, problems underlined in the text with a dot in the gutter, find and replace, folding, and a run hook on Cmd/Ctrl+Enter that runs the selection or the statement under the cursor. A quiet status line shows problems, the cursor, the mode and a Run button. Helpers split SQL into statements and check or format JSON anywhere, the server included.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [
|
|
11
11
|
"@codemirror/state@6.7.6",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"docs": "Installed to components/makersclaw/blocks/code-editor. Import: import { CodeEditor, useCodeEditor, LANGUAGES, SQL_DIALECTS, splitStatements, statementAt, sqlNamespace, checkJson, formatJson, stringifyJson, lineCol, offsetOf, normalizeDiagnostics } from \"@/components/makersclaw/blocks/code-editor\". Props and an example: components/makersclaw/blocks/code-editor/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
88
88
|
"meta": {
|
|
89
89
|
"makersclaw": {
|
|
90
|
-
"version": "0.
|
|
90
|
+
"version": "0.8.0",
|
|
91
91
|
"useWhen": "A screen edits code or structured text: a SQL console, a query in a report, a JSON config or payload, a webhook body, a template, a script. json-editor and sql-console are built on it. Not for prose (doc-editor), one line of text (the kit's Input), or showing a diagram (diagram)."
|
|
92
92
|
}
|
|
93
93
|
}
|
package/r/comment-box.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "CommentBox",
|
|
6
6
|
"description": "A minimal rich comment field (DocEditor in minimal mode): bold, italic, links and lists from the selection toolbar or markdown shortcuts, @mentions as chips, Cmd/Ctrl+Enter to post. It posts through an async onPost(doc) that answers { ok }, keeps the draft and says why when it fails, and clears when it lands. With `comments` it draws the thread above it: avatar, name, relative time, the rich body. Also exports the composer alone, the read-only body renderer and the timestamp.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/doc-editor.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [],
|
|
12
12
|
"files": [
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"docs": "Installed to components/makersclaw/blocks/comment-box. Import: import { CommentBox, CommentComposer, CommentBody, CommentTime } from \"@/components/makersclaw/blocks/comment-box\". Props and an example: components/makersclaw/blocks/comment-box/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
58
58
|
"meta": {
|
|
59
59
|
"makersclaw": {
|
|
60
|
-
"version": "0.
|
|
60
|
+
"version": "0.8.0",
|
|
61
61
|
"useWhen": "People comment on something in the app: a task, a design, a document, an approval, a support ticket; a reply field with @mentions under a thread. Not dated notes kept on a record with edit and delete (record-notes), not a conversation with an assistant (chat), not a long document (doc-editor, doc-page)."
|
|
62
62
|
}
|
|
63
63
|
}
|
package/r/contact-section.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "Contact Section",
|
|
6
6
|
"description": "A public \"contact us\" section: a form (name, email, message; optional phone, company, subject, a topic as chips or radios, a consent box) and/or the ways to reach you (email, phone, address, hours, offices). The form alone, beside an info panel (plain or filled), or with info cards above or beside it; an optional picture beside or behind the form. Validates in place, submits through an async onSubmit that returns { ok }, and shows the failure or a sent state.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"docs": "Installed to components/makersclaw/blocks/contact-section. Import: import { ContactSection } from \"@/components/makersclaw/blocks/contact-section\". Props and an example: components/makersclaw/blocks/contact-section/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
47
47
|
"meta": {
|
|
48
48
|
"makersclaw": {
|
|
49
|
-
"version": "0.
|
|
49
|
+
"version": "0.8.0",
|
|
50
50
|
"useWhen": "The contact page or contact section of a public site: sales enquiries, support requests, a studio's \"start a project\" form. Not for a signed-in user's support inbox or ticket thread, or a general-purpose record form (use a form of your own from the kit)."
|
|
51
51
|
}
|
|
52
52
|
}
|
package/r/core.json
CHANGED
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
"path": "templates/next/files/components/ui/index.js",
|
|
154
154
|
"type": "registry:ui",
|
|
155
155
|
"target": "@components/makersclaw/ui/index.js",
|
|
156
|
-
"content": "// The kit. Import from \"../components/ui\" (a surface) or \"./ui\" (a\n// component). Every part is the workspace's look over the browser's own\n// behaviour; none of them takes a colour. README §3b.\nexport { cx } from \"./cx\";\nexport { Icon, ICON_NAMES } from \"./icon\";\nexport { Button, IconButton } from \"./button\";\nexport { Field, Input, TimeInput, Textarea, Select, Switch, Checkbox } from \"./field\";\nexport { Card, Notice, Badge, Kicker, Empty } from \"./notice\";\nexport { Sheet, Confirm } from \"./sheet\";\nexport { Tabs, TabPanel } from \"./tabs\";\nexport { Menu, MenuPanel } from \"./menu\";\nexport { ContextMenu } from \"./context-menu\";\nexport { Popover, usePopoverPosition, panelClass } from \"./popover\";\nexport {\n Calendar,\n parseDate,\n isDateString,\n toDateString,\n todayString,\n addDays,\n addMonths,\n startOfMonth,\n endOfMonth,\n dayOfWeek,\n formatDate,\n toLocalDate,\n firstDayOfWeek,\n} from \"./calendar\";\nexport { DatePicker, DateInput, DATE_PRESETS } from \"./date-picker\";\nexport { Tooltip, HoverCard } from \"./tooltip\";\nexport { Depiction } from \"./depiction\";\nexport { toast, Toaster } from \"./toast\";\nexport { Carousel } from \"./carousel\";\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle } from \"./resizable\";\nexport { Avatar, AvatarGroup } from \"./avatar\";\nexport { Breadcrumb } from \"./breadcrumb\";\nexport { Pagination } from \"./pagination\";\nexport { RadioGroup, Radio } from \"./radio\";\nexport { Toggle, ToggleGroup } from \"./toggle\";\nexport { ButtonGroup, ButtonGroupSeparator } from \"./button-group\";\nexport { Collapsible, Accordion } from \"./collapsible\";\nexport { Item, ItemGroup } from \"./item\";\nexport { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from \"./table\";\nexport { Spinner, Skeleton, Progress } from \"./feedback\";\nexport { Distribution } from \"./distribution\";\nexport { Swatch } from \"./swatch\";\nexport { Stat, StatGroup, Delta, StatusDot, Legend, Meter } from \"./stat\";\nexport { Steps } from \"./steps\";\nexport { Separator, Kbd, KbdGroup, AspectRatio } from \"./separator\";\nexport { ScrollArea } from \"./scroll-area\";\nexport { Slider } from \"./slider\";\nexport { InputGroup, PasswordInput } from \"./input-group\";\nexport { Image } from \"./image\";\nexport { FileInput, FileDrop, fileMatches } from \"./file-input\";\nexport { InputOTP } from \"./input-otp\";\nexport { Command, CommandDialog, commandScore, commandFilter } from \"./command\";\nexport { Combobox } from \"./combobox\";\nexport { Rating } from \"./rating\";\n"
|
|
156
|
+
"content": "// The kit. Import from \"../components/ui\" (a surface) or \"./ui\" (a\n// component). Every part is the workspace's look over the browser's own\n// behaviour; none of them takes a colour. README §3b.\nexport { cx } from \"./cx\";\nexport { Icon, ICON_NAMES } from \"./icon\";\nexport { Button, IconButton } from \"./button\";\nexport { Field, Input, TimeInput, Textarea, Select, Switch, Checkbox } from \"./field\";\nexport { Card, Notice, Badge, Kicker, Empty } from \"./notice\";\nexport { Sheet, Confirm } from \"./sheet\";\nexport { Tabs, TabPanel } from \"./tabs\";\nexport { Menu, MenuPanel } from \"./menu\";\nexport { ContextMenu } from \"./context-menu\";\nexport { Popover, usePopoverPosition, panelClass } from \"./popover\";\nexport {\n Calendar,\n parseDate,\n isDateString,\n toDateString,\n todayString,\n addDays,\n addMonths,\n startOfMonth,\n endOfMonth,\n dayOfWeek,\n formatDate,\n toLocalDate,\n firstDayOfWeek,\n} from \"./calendar\";\nexport { DatePicker, DateInput, DATE_PRESETS } from \"./date-picker\";\nexport { Tooltip, HoverCard } from \"./tooltip\";\nexport { Depiction } from \"./depiction\";\nexport { toast, Toaster } from \"./toast\";\nexport { Carousel } from \"./carousel\";\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle } from \"./resizable\";\nexport { Avatar, AvatarGroup } from \"./avatar\";\nexport { Breadcrumb } from \"./breadcrumb\";\nexport { Pagination } from \"./pagination\";\nexport { RadioGroup, Radio } from \"./radio\";\nexport { Toggle, ToggleGroup } from \"./toggle\";\nexport { ButtonGroup, ButtonGroupSeparator } from \"./button-group\";\nexport { Collapsible, Accordion } from \"./collapsible\";\nexport { Item, ItemGroup } from \"./item\";\nexport { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from \"./table\";\nexport { Spinner, Skeleton, Progress } from \"./feedback\";\nexport { Distribution } from \"./distribution\";\nexport { Swatch } from \"./swatch\";\nexport { Stat, StatGroup, Delta, StatusDot, Legend, Meter } from \"./stat\";\nexport { Steps } from \"./steps\";\nexport { Separator, Kbd, KbdGroup, AspectRatio } from \"./separator\";\nexport { ScrollArea } from \"./scroll-area\";\nexport { AppLayout, DocumentLayout } from \"./layout\";\nexport { Slider } from \"./slider\";\nexport { InputGroup, PasswordInput } from \"./input-group\";\nexport { Image } from \"./image\";\nexport { FileInput, FileDrop, fileMatches } from \"./file-input\";\nexport { InputOTP } from \"./input-otp\";\nexport { Command, CommandDialog, commandScore, commandFilter } from \"./command\";\nexport { Combobox } from \"./combobox\";\nexport { Rating } from \"./rating\";\n"
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"path": "templates/next/files/components/ui/input-group.jsx",
|
|
@@ -173,6 +173,12 @@
|
|
|
173
173
|
"target": "@components/makersclaw/ui/item.jsx",
|
|
174
174
|
"content": "import Link from \"next/link\";\nimport { Children } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Item — the list row: a contact, a file, a run, a setting. Media on the\n// left, title and description, meta on the right, then its own actions.\n// With `href` or `onClick` the WHOLE row is the target — the title is the\n// real <a> or <button> and stretches over the row, so the row's own\n// actions stay separate buttons (never a button inside a link) and a\n// screen reader hears the title, not the whole row read out.\n//\n// media a node on the left: an avatar, an <img>, a logo\n// icon or a glyph from ./icon, drawn in a muted square\n// title the one line that names the thing (truncates)\n// description up to two lines under it\n// meta a count, a date, a Badge — muted, on the right\n// actions buttons on the far right (IconButton, Menu); they sit above\n// the row's target, so they click on their own\n// href a next/link row · `onClick` a button row · neither: a plain row\n// size default (56px) · sm (44px, for dense lists and rails) ·\n// lg (64px, roomier: a pipeline's runs, a list read at leisure)\n// variant plain (no fill) · outline (a bordered card) · muted (a filled panel)\n// layout row (default) · stacked — a card for a grid: icon or media on\n// top, then the title (up to two lines), the description (up\n// to three), children, and meta at the foot; actions sit in the\n// top-right corner. It fills its grid cell's height, so a row\n// of cards lines up. Pair it with variant=\"outline\" or \"muted\".\n// The link or button target and what goes where are the row's\n//\n// Where the other props go: `aria-current`, `onClick`, `target` and `rel`\n// belong to the row's target, so they go on its <a> or <button> (with\n// `href`, `onClick` rides on the link: analytics, closing a sheet).\n// aria-current on the target fills the row, so the current page looks\n// like what a screen reader says it is. Every other prop (data-*, id,\n// aria-label…) goes on the row's <div>.\n//\n// ItemGroup stacks plain Items with a rule between each, as a list.\n\nconst VARIANTS = {\n plain: \"\",\n outline: \"border border-border bg-card text-card-foreground\",\n muted: \"bg-muted\",\n};\n\nconst SIZES = {\n default: { row: \"min-h-14 gap-3 px-3 py-2.5\", icon: \"size-10\", glyph: 18, desc: \"line-clamp-2 text-sm\" },\n sm: { row: \"min-h-11 gap-2.5 px-2.5 py-1.5\", icon: \"size-8\", glyph: 16, desc: \"truncate text-xs\" },\n lg: { row: \"min-h-16 gap-3.5 px-4 py-3\", icon: \"size-11\", glyph: 20, desc: \"line-clamp-2 text-sm\" },\n};\n\n// layout=\"stacked\": the card's padding and gaps, and its icon square.\nconst STACKED = {\n default: { box: \"gap-3 p-4\", icon: \"size-10\", glyph: 18 },\n sm: { box: \"gap-2 p-3\", icon: \"size-8\", glyph: 16 },\n lg: { box: \"gap-4 p-5\", icon: \"size-12\", glyph: 22 },\n};\n\nconst TARGET = \"outline-none after:absolute after:inset-0\";\n\n// The row filled while its target (or a plain row itself) is current.\nconst CURRENT =\n \"has-[[data-item-target][aria-current]:not([aria-current=false])]:bg-accent \" +\n \"[&[aria-current]:not([aria-current=false])]:bg-accent\";\n\nexport function Item({\n media,\n icon,\n title,\n description,\n meta,\n actions,\n href,\n onClick,\n size = \"default\",\n variant = \"plain\",\n layout = \"row\",\n disabled,\n className,\n children,\n \"aria-current\": ariaCurrent,\n target,\n rel,\n ...rest\n}) {\n const s = SIZES[size] ?? SIZES.default;\n const interactive = Boolean(href || onClick) && !disabled;\n\n let name = title;\n let targeted = false;\n if (href && !disabled) {\n targeted = true;\n name = (\n <Link href={href} onClick={onClick} target={target} rel={rel} aria-current={ariaCurrent} data-item-target className={TARGET}>\n {title}\n </Link>\n );\n } else if (onClick) {\n targeted = true;\n name = (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-current={ariaCurrent}\n data-item-target\n className={cx(TARGET, \"text-left disabled:cursor-not-allowed\")}\n >\n {title}\n </button>\n );\n }\n\n const ring =\n interactive &&\n \"hover:bg-accent has-[[data-item-target]:focus-visible]:ring-2 has-[[data-item-target]:focus-visible]:ring-inset has-[[data-item-target]:focus-visible]:ring-ring\";\n\n if (layout === \"stacked\") {\n const k = STACKED[size] ?? STACKED.default;\n return (\n <div\n className={cx(\"relative flex h-full flex-col items-start rounded-lg text-sm\", VARIANTS[variant] ?? VARIANTS.plain, k.box, ring, CURRENT, disabled && \"opacity-50\", className)}\n aria-current={targeted ? undefined : ariaCurrent}\n {...rest}\n >\n {icon ? (\n <span className={cx(\"flex shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\", variant === \"muted\" && \"bg-background\", k.icon)}>\n <Icon name={icon} size={k.glyph} />\n </span>\n ) : media ? (\n <span className=\"flex shrink-0 items-center\">{media}</span>\n ) : null}\n <div className={cx(\"w-full min-w-0 flex-1\", actions && !icon && !media && \"pr-10\")}>\n <p className=\"line-clamp-2 font-medium text-foreground\">{name}</p>\n {description ? <p className=\"mt-1 line-clamp-3 text-sm text-muted-foreground\">{description}</p> : null}\n {children ? <div className=\"mt-2\">{children}</div> : null}\n </div>\n {meta !== undefined && meta !== null ? (\n <div data-nums className=\"text-xs text-muted-foreground\">\n {meta}\n </div>\n ) : null}\n {actions ? <div className=\"absolute right-2 top-2 z-10 flex items-center gap-1\">{actions}</div> : null}\n </div>\n );\n }\n\n return (\n <div\n className={cx(\n \"relative flex items-center rounded-lg text-sm\",\n VARIANTS[variant] ?? VARIANTS.plain,\n s.row,\n ring,\n CURRENT,\n disabled && \"opacity-50\",\n className,\n )}\n aria-current={targeted ? undefined : ariaCurrent}\n {...rest}\n >\n {icon ? (\n <span className={cx(\"flex shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\", variant === \"muted\" && \"bg-background\", s.icon)}>\n <Icon name={icon} size={s.glyph} />\n </span>\n ) : media ? (\n <span className=\"flex shrink-0 items-center\">{media}</span>\n ) : null}\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium text-foreground\">{name}</p>\n {description ? <p className={cx(\"text-muted-foreground\", s.desc)}>{description}</p> : null}\n {children ? <div className=\"mt-1.5\">{children}</div> : null}\n </div>\n {meta !== undefined && meta !== null ? (\n <div data-nums className=\"shrink-0 text-xs text-muted-foreground\">\n {meta}\n </div>\n ) : null}\n {actions ? <div className=\"relative z-10 flex shrink-0 items-center gap-1\">{actions}</div> : null}\n </div>\n );\n}\n\n/** Items as a list, a rule between each. `variant=\"outline\"` boxes the\n * whole list; put plain Items in it. */\nexport function ItemGroup({ variant = \"plain\", className, children, \"aria-label\": ariaLabel }) {\n const rows = Children.toArray(children).filter(Boolean);\n if (rows.length === 0) return null;\n return (\n <ul\n role=\"list\"\n aria-label={ariaLabel}\n className={cx(\n \"flex flex-col\",\n variant === \"outline\" && \"overflow-hidden rounded-lg border border-border bg-card text-card-foreground [&>li>*]:rounded-none\",\n className,\n )}\n >\n {rows.map((row, i) => (\n <li key={row.key ?? i} className={cx(i > 0 && \"border-t border-border\")}>\n {row}\n </li>\n ))}\n </ul>\n );\n}\n"
|
|
175
175
|
},
|
|
176
|
+
{
|
|
177
|
+
"path": "templates/next/files/components/ui/layout.jsx",
|
|
178
|
+
"type": "registry:ui",
|
|
179
|
+
"target": "@components/makersclaw/ui/layout.jsx",
|
|
180
|
+
"content": "\"use client\";\n\nimport { createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport { cx } from \"./cx\";\nimport { Button, IconButton } from \"./button\";\nimport { Sheet } from \"./sheet\";\n\n// Layouts — who owns the window's height, and who scrolls.\n//\n// Every design-first build of 2026-10-07 shipped a shell of `min-h-dvh`\n// that grew with its content: the page itself scrolled, no pane scrolled\n// on its own, and a footer, an inspector or a button below a long\n// article sat past the end of the iframe, where nothing could reach it.\n// So the kit owns the window's height, once, and the app's code never\n// sizes anything by the window (`npm run check` refuses min-h-dvh,\n// h-screen, 100vh, h-[80vh]… outside the kit, rule 12).\n//\n// AppLayout — the app frame. Exactly one owns the viewport: the\n// outermost. A layout inside another fills the slot it is in instead,\n// whatever `height` says, so there is never a second viewport owner. The\n// shell (app/shell.jsx) is the outermost one in every app the template\n// lays; a surface that needs an inspector or a fill of its own returns\n// an AppLayout as its outermost element, and it fills the shell's\n// content edge to edge (the content's padding drops for it).\n//\n// header the top bar, always in view (title, the surface's\n// commands). The bar draws its own bottom border\n// navigation the surfaces, a page tree: a rail beside the content\n// from md (768px); below md a Sheet from the leading\n// edge, opened by the menu button in the top bar. A\n// press on a link or a button inside closes it\n// navigationLabel the Sheet's title and the menu button's words\n// (\"Navigation\")\n// navigationWidth the rail, in px (176; 160 to 320)\n// children the content. It scrolls inside its own region\n// contentMode scroll (the default): the content region scrolls ·\n// fill: the region is a bounded flex column and the\n// child owns its own scrolling (a canvas, a grid, a\n// spreadsheet, an editor that scrolls itself)\n// contentLabel the content region's name for a screen reader (\"Content\")\n// contentClassName spacing and token backgrounds for the content\n// region (p-4 sm:p-6, bg-card). Never a height or an\n// overflow: the layout owns those\n// scrollKey when it changes the content scrolls back to its top\n// and the navigation Sheet closes: pass the route\n// (usePathname()), since the frame stays mounted while\n// the surfaces change inside it\n// inspector the details, review or feedback panel beside the\n// content, scrolling on its own\n// inspectorLabel the inspector region's name (\"Details\")\n// inspectorWidth in px (360; 240 to 560). Builds have needed 300 and 440\n// inspectorAt md · lg (the default) · xl: the width from which the\n// inspector sits beside the content\n// footer under the content, always in view (the save state,\n// the composer, the commands for what is above)\n// height viewport (the default): the frame is the window's\n// height · parent: it fills a bounded parent. Nested,\n// it is parent whatever this says\n// className the frame's own tokens; never a height or an overflow\n//\n// Below `inspectorAt`, where there is no room for two columns:\n// - contentMode scroll: the content and the inspector share ONE scroll\n// region, the inspector after the content. Everything is reachable\n// by scrolling, and nothing is hidden behind a button.\n// - contentMode fill: the content keeps the room and the inspector is\n// a bounded pane under it (at most 45% of the height), scrolling on\n// its own. A fill child has no flow to stack into, and a pane with\n// no bound under it is cut off (workspace-layout's fault, 2026-10-07).\n// The footer stays under both, in view.\n//\n// In print the frame lets go: no bound, no overflow, no navigation, no\n// header, inspector or footer; the content flows onto pages. A page meant\n// for print or export is a DocumentLayout inside the content.\n\nconst Nested = createContext(false);\n\nconst px = (value, fallback, min, max) => {\n const n = Number(value);\n return `${Number.isFinite(n) && n > 0 ? Math.min(max, Math.max(min, Math.round(n))) : fallback}px`;\n};\n\n// Tailwind reads class names whole from the source, so every breakpoint\n// is written out.\nconst BESIDE = {\n md: {\n main: \"md:grid md:grid-cols-[minmax(0,1fr)_var(--app-inspector)] md:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"md:contents\",\n content: \"md:col-start-1 md:row-start-1\",\n scrolls: \"md:min-h-0 md:overflow-y-auto md:overscroll-contain\",\n inspector: \"md:col-start-2 md:row-span-2 md:row-start-1 md:max-h-none md:min-h-0 md:overflow-y-auto md:border-l md:border-t-0\",\n footer: \"md:col-start-1 md:row-start-2\",\n },\n lg: {\n main: \"lg:grid lg:grid-cols-[minmax(0,1fr)_var(--app-inspector)] lg:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"lg:contents\",\n content: \"lg:col-start-1 lg:row-start-1\",\n scrolls: \"lg:min-h-0 lg:overflow-y-auto lg:overscroll-contain\",\n inspector: \"lg:col-start-2 lg:row-span-2 lg:row-start-1 lg:max-h-none lg:min-h-0 lg:overflow-y-auto lg:border-l lg:border-t-0\",\n footer: \"lg:col-start-1 lg:row-start-2\",\n },\n xl: {\n main: \"xl:grid xl:grid-cols-[minmax(0,1fr)_var(--app-inspector)] xl:grid-rows-[minmax(0,1fr)_auto]\",\n flow: \"xl:contents\",\n content: \"xl:col-start-1 xl:row-start-1\",\n scrolls: \"xl:min-h-0 xl:overflow-y-auto xl:overscroll-contain\",\n inspector: \"xl:col-start-2 xl:row-span-2 xl:row-start-1 xl:max-h-none xl:min-h-0 xl:overflow-y-auto xl:border-l xl:border-t-0\",\n footer: \"xl:col-start-1 xl:row-start-2\",\n },\n};\n\nconst REGION = \"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\";\nconst LETS_GO = \"print:block print:h-auto print:min-h-0 print:overflow-visible\";\n\nexport function AppLayout({\n header,\n navigation,\n navigationLabel = \"Navigation\",\n navigationWidth,\n children,\n contentMode = \"scroll\",\n contentLabel = \"Content\",\n contentClassName,\n scrollKey,\n inspector,\n inspectorLabel = \"Details\",\n inspectorWidth,\n inspectorAt = \"lg\",\n footer,\n height = \"viewport\",\n className,\n}) {\n const nested = useContext(Nested);\n const bounded = nested || height === \"parent\";\n const fill = contentMode === \"fill\";\n const at = BESIDE[inspectorAt] ?? BESIDE.lg;\n const hasInspector = inspector != null && inspector !== false;\n const hasNavigation = navigation != null && navigation !== false;\n const shared = hasInspector && !fill; // below `at`: content and inspector in one scroll region\n const [open, setOpen] = useState(false);\n const flowRef = useRef(null);\n const contentRef = useRef(null);\n\n useEffect(() => {\n if (flowRef.current) flowRef.current.scrollTop = 0;\n if (contentRef.current) contentRef.current.scrollTop = 0;\n setOpen(false);\n }, [scrollKey]);\n\n return (\n <Nested.Provider value={true}>\n <div\n data-app-layout={bounded ? \"parent\" : \"viewport\"}\n style={{ \"--app-nav\": px(navigationWidth, 176, 160, 320), \"--app-inspector\": px(inspectorWidth, 360, 240, 560) }}\n className={cx(\n \"flex min-w-0 flex-col overflow-hidden bg-background text-foreground\",\n bounded ? \"h-full min-h-0 flex-1\" : \"h-dvh\",\n LETS_GO,\n className,\n )}\n >\n {header != null && header !== false ? (\n <div data-layout-header className=\"flex shrink-0 items-center gap-1 border-b border-border print:hidden\">\n {hasNavigation ? (\n <IconButton icon=\"menu\" label={`Open ${navigationLabel.toLowerCase()}`} onClick={() => setOpen(true)} className=\"ml-1 md:hidden\" />\n ) : null}\n <div className=\"min-w-0 flex-1\">{header}</div>\n </div>\n ) : hasNavigation ? (\n <div data-layout-header className=\"flex shrink-0 items-center border-b border-border px-1 py-1 md:hidden print:hidden\">\n <Button variant=\"quiet\" size=\"sm\" icon=\"menu\" onClick={() => setOpen(true)}>\n {navigationLabel}\n </Button>\n </div>\n ) : null}\n\n {hasNavigation ? (\n <Sheet open={open} onClose={() => setOpen(false)} title={navigationLabel} side=\"start\">\n {open ? (\n <div\n onClick={(e) => {\n if (e.target instanceof Element && e.target.closest(\"a,button\")) setOpen(false);\n }}\n >\n {navigation}\n </div>\n ) : null}\n </Sheet>\n ) : null}\n\n <div data-layout-body className={cx(\"flex min-h-0 min-w-0 flex-1\", LETS_GO)}>\n {hasNavigation ? (\n <div\n data-layout-navigation\n className=\"hidden shrink-0 flex-col overflow-y-auto overscroll-contain border-r border-border md:flex md:w-(--app-nav) print:hidden\"\n >\n {navigation}\n </div>\n ) : null}\n\n <div data-layout-main className={cx(\"flex min-h-0 min-w-0 flex-1 flex-col\", hasInspector && at.main, LETS_GO)}>\n <div\n ref={flowRef}\n data-layout-flow\n className={cx(\n \"flex min-h-0 min-w-0 flex-1 flex-col\",\n shared && \"overflow-y-auto overscroll-contain\",\n hasInspector && at.flow,\n LETS_GO,\n )}\n >\n <div\n ref={contentRef}\n data-layout-content\n role=\"region\"\n aria-label={contentLabel}\n tabIndex={0}\n className={cx(\n \"flex min-w-0 flex-col\",\n REGION,\n fill\n ? \"min-h-0 flex-1 overflow-hidden\"\n : shared\n ? cx(\"shrink-0\", at.scrolls)\n : \"min-h-0 flex-1 overflow-y-auto overscroll-contain\",\n hasInspector && at.content,\n LETS_GO,\n contentClassName,\n // A surface that is a frame of its own runs to the edges.\n \"has-[>[data-app-layout]]:p-0\",\n )}\n >\n {children}\n </div>\n {hasInspector ? (\n <div\n data-layout-inspector\n role=\"region\"\n aria-label={inspectorLabel}\n tabIndex={0}\n className={cx(\n \"min-w-0 shrink-0 border-t border-border print:hidden\",\n REGION,\n fill && \"max-h-[45%] overflow-y-auto overscroll-contain\",\n at.inspector,\n )}\n >\n {inspector}\n </div>\n ) : null}\n </div>\n {footer != null && footer !== false ? (\n <div data-layout-footer className={cx(\"shrink-0 border-t border-border print:hidden\", hasInspector && at.footer)}>\n {footer}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n </Nested.Provider>\n );\n}\n\n// DocumentLayout — a page that flows: a report, an invoice, an article\n// meant for print or export. It owns no height and scrolls nothing; it\n// sits in the AppLayout's content (contentMode scroll), which scrolls it,\n// and in print the frame lets go and it flows onto pages.\n//\n// children the document\n// toolbar a bar held at the top while the document scrolls under it\n// (Print, Export, the zoom); never printed\n// width prose (the default: a measure for reading) · page (A4,\n// 210mm) · letter (8.5in) · full. In print it is the paper's\n// paper true: drawn as a sheet on the desk on screen (bg-card, a\n// hairline, padding); plain in print\n// align start (the default) · center\n// label the document's name for a screen reader (\"Document\")\n// className the document's own spacing and tokens; never a height\nconst WIDTHS = {\n prose: \"max-w-prose\",\n page: \"max-w-[210mm]\",\n letter: \"max-w-[8.5in]\",\n full: \"max-w-none\",\n};\n\nexport function DocumentLayout({ children, toolbar, width = \"prose\", paper = false, align = \"start\", label = \"Document\", className }) {\n return (\n <div data-document-layout className=\"flex min-w-0 flex-col\">\n {toolbar != null && toolbar !== false ? (\n <div data-document-toolbar className=\"sticky top-0 z-10 border-b border-border bg-background print:hidden\">\n {toolbar}\n </div>\n ) : null}\n <article\n aria-label={label}\n className={cx(\n \"w-full min-w-0 print:max-w-none\",\n WIDTHS[width] ?? WIDTHS.prose,\n align === \"center\" && \"mx-auto\",\n paper && \"my-6 rounded-lg border border-border bg-card p-8 text-card-foreground shadow-sm print:my-0 print:rounded-none print:border-0 print:p-0 print:shadow-none\",\n className,\n )}\n >\n {children}\n </article>\n </div>\n );\n}\n"
|
|
181
|
+
},
|
|
176
182
|
{
|
|
177
183
|
"path": "templates/next/files/components/ui/menu.jsx",
|
|
178
184
|
"type": "registry:ui",
|
package/r/csv-import.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "CsvImport",
|
|
6
6
|
"description": "Import a CSV in three calm steps: drop a file (or paste text, or download a template), match each file column to a field with automatic matches and sample values (or, with no fields, name and type a new table's fields from guessed types), then review every row in an editable spreadsheet where cells that do not fit their type are marked red and fixed in place, delete rows or leave the bad ones out, and import through one async onImport that answers { ok }. Quoted commas and newlines, TSV and semicolon files, byte-order marks, 50,000 rows.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/spreadsheet.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [
|
|
12
12
|
"papaparse@5.7.0"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"docs": "Installed to components/makersclaw/blocks/csv-import. Import: import { CsvImport, parseCsvText, readCsvFile, headersOf, autoMatch, guessColumns, buildRows, cleanRow, templateCsv } from \"@/components/makersclaw/blocks/csv-import\". Props and an example: components/makersclaw/blocks/csv-import/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
57
57
|
"meta": {
|
|
58
58
|
"makersclaw": {
|
|
59
|
-
"version": "0.
|
|
59
|
+
"version": "0.8.0",
|
|
60
60
|
"useWhen": "People bring records in from a spreadsheet or another tool: contacts, leads, products, a price list, an old CRM export, a first table for a new app. Built on spreadsheet. Not for uploading files to keep (file-upload) or for editing records already in the app (spreadsheet, data-table)."
|
|
61
61
|
}
|
|
62
62
|
}
|
package/r/data-table.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "Data table",
|
|
6
6
|
"description": "Records as a table the founder can work: sort by a column (shift for several), search every column, faceted filters with counts, column visibility, row selection with a bulk-actions bar, a menu per row or its first actions as buttons in the row (async, with a spinner and a failure notice), row click, pagination with a page size, loading, empty and no-matches states, and cards on a phone. Opt in to expandable rows, row groups with subtotals, an aligned totals row, arrow-key navigation and a sticky first column. Client-side over rows, or driven by a server.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"docs": "Installed to components/makersclaw/blocks/data-table. Import: import { DataTable, useControllable, processRows, sortRows, searchRows, filterRows, facetOptions, nextSort, groupRows, groupValueOf } from \"@/components/makersclaw/blocks/data-table\". Props and an example: components/makersclaw/blocks/data-table/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
63
63
|
"meta": {
|
|
64
64
|
"makersclaw": {
|
|
65
|
-
"version": "0.
|
|
65
|
+
"version": "0.8.0",
|
|
66
66
|
"useWhen": "A list of records to scan, find, sort and act on: invoices, customers, orders, leads, users, tickets. Not for records that move through stages (a board), a few key numbers (stats), or a grid of cards whose picture matters more than its columns."
|
|
67
67
|
}
|
|
68
68
|
}
|
package/r/diagram.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "Diagram",
|
|
6
6
|
"description": "Draws Mermaid text (flowcharts, sequence, class, state, ER, Gantt, pie, mind maps, timelines, git graphs…) in the workspace theme: the tokens are read from the page at run time and handed to Mermaid, so nodes, lines, text and chart colours are the theme's and follow light/dark. Zoom and pan (buttons, Cmd/Ctrl+scroll, pinch, drag, keys), fit to view, a Source tab with line numbers, copy the source or the SVG, download SVG. A mistake shows its line and Mermaid's words, and keeps the last drawing that worked. On the server the frame shows the Mermaid text.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [
|
|
11
11
|
"mermaid@12.0.0"
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"docs": "Installed to components/makersclaw/blocks/diagram. Import: import { Diagram, diagramKind, diagramTitle, describeError, mermaidTheme } from \"@/components/makersclaw/blocks/diagram\". Props and an example: components/makersclaw/blocks/diagram/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
68
68
|
"meta": {
|
|
69
69
|
"makersclaw": {
|
|
70
|
-
"version": "0.
|
|
70
|
+
"version": "0.8.0",
|
|
71
71
|
"useWhen": "A screen shows a diagram written as text: a process or decision flow an agent wrote, a sequence of API calls, a data model, a project timeline, an org or system sketch in docs, a mind map. Not for a board people arrange by hand (flow-canvas), freehand drawing (sketch-canvas), or charts of numbers (chart)."
|
|
72
72
|
}
|
|
73
73
|
}
|
package/r/doc-editor.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "DocEditor",
|
|
6
6
|
"description": "A Notion-style rich-text editor (BlockNote) painted in the workspace theme: headings, lists, checklists, quotes, code, tables, images and dividers from a / menu or markdown shortcuts, a formatting toolbar, drag handles; @mentions and {merge fields} as inline chips; a minimal mode for comment boxes with Cmd/Ctrl+Enter to submit. The document is BlockNote JSON; helpers turn it into markdown or plain text and read its checklist, fields and mentions.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [
|
|
11
11
|
"@blocknote/core@0.55.0",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"docs": "Installed to components/makersclaw/blocks/doc-editor. Import: import { DocEditor, useDocEditor, docToMarkdown, docToText, emptyDoc, isDocEmpty, docChecklist, setChecklistItem, docFields, docMentions } from \"@/components/makersclaw/blocks/doc-editor\". Props and an example: components/makersclaw/blocks/doc-editor/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
76
76
|
"meta": {
|
|
77
77
|
"makersclaw": {
|
|
78
|
-
"version": "0.
|
|
78
|
+
"version": "0.8.0",
|
|
79
79
|
"useWhen": "A screen edits rich text: a wiki or doc page, notes on a record, a brief, meeting notes, an email body with merge fields, a comment field with mentions. The other text blocks (doc-page, record-notes, comment-box…) are built on it. Not for one line of plain text (the kit's Input) or a form of fields (record-form)."
|
|
80
80
|
}
|
|
81
81
|
}
|
package/r/doc-page.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "DocPage",
|
|
6
6
|
"description": "A Notion-style page: an emoji icon (picked from a grid), a big plain title, and the rich-text editor under it (doc-editor: / menu, markdown shortcuts, @mentions). Edits autosave through an async onSave a moment after typing stops, with Saved / Saving… / Couldn't save + Retry in the top bar, Cmd/Ctrl+S to save now and unsent edits sent when the page is left. A read-only mode shows the same page with nothing to edit; slots for a breadcrumb, actions and a meta line.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/doc-editor.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [],
|
|
12
12
|
"files": [
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"docs": "Installed to components/makersclaw/blocks/doc-page. Import: import { DocPage } from \"@/components/makersclaw/blocks/doc-page\". Props and an example: components/makersclaw/blocks/doc-page/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
55
55
|
"meta": {
|
|
56
56
|
"makersclaw": {
|
|
57
|
-
"version": "0.
|
|
57
|
+
"version": "0.8.0",
|
|
58
58
|
"useWhen": "A screen is one document someone reads and writes: a wiki or handbook page, a playbook, a policy, a project page, a one-pager. The knowledge-base block puts a page tree beside it. Not for a short field of rich text inside a form (doc-editor), notes on a record (record-notes) or a comment (comment-box)."
|
|
59
59
|
}
|
|
60
60
|
}
|
package/r/email-render.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "EmailRender",
|
|
6
6
|
"description": "Email HTML from React (react-email) in the workspace theme: a layout with the 600px column and the inbox preview line, a header brand line, a footer with the address and unsubscribe link, buttons, headings and text, and DocEmail, which draws a doc-editor document as email markup (lists, quotes, code, tables, images, merge fields). Email cannot read CSS variables, so resolveEmailTheme reads the tokens where the editor sits and hands the email plain hex values; renderEmail returns { html, text }, in the browser for a preview and on the server for a send.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [
|
|
11
11
|
"@react-email/components@1.0.12",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"docs": "Installed to components/makersclaw/blocks/email-render. Import: import { EmailLayout, EmailCard, EmailHeader, EmailFooter, EmailButton, EmailHeading, EmailText, EmailDivider, EMAIL_WIDTH, DocEmail, fieldText, renderEmail, emailPlainText, EMAIL_TOKENS, EMAIL_FALLBACK, resolveEmailTheme, emailColor, emailFontStack, emailThemeKey, luminance, toHex } from \"@/components/makersclaw/blocks/email-render\". Props and an example: components/makersclaw/blocks/email-render/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
67
67
|
"meta": {
|
|
68
68
|
"makersclaw": {
|
|
69
|
-
"version": "0.
|
|
69
|
+
"version": "0.8.0",
|
|
70
70
|
"useWhen": "Code that must produce an email's HTML: a newsletter, a transactional email (receipt, invite, reset), a digest, a notification, a test send from an editor. newsletter-builder is built on it. Not for writing the email (doc-editor or email-composer), sending it (your sender or the app's server) or a web page (page-builder)."
|
|
71
71
|
}
|
|
72
72
|
}
|
package/r/faq.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "FAQ",
|
|
6
6
|
"description": "A marketing section of questions and answers: an accordion (one open at a time, or several), a grid of cards with optional icons, or a plain list with every answer shown; the title above or beside them, and an optional \"need more help?\" call to action. Loading and empty states.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"docs": "Installed to components/makersclaw/blocks/faq. Import: import { Faq } from \"@/components/makersclaw/blocks/faq\". Props and an example: components/makersclaw/blocks/faq/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
44
44
|
"meta": {
|
|
45
45
|
"makersclaw": {
|
|
46
|
-
"version": "0.
|
|
46
|
+
"version": "0.8.0",
|
|
47
47
|
"useWhen": "The questions section of a public page: a landing page, pricing, a help centre's front page, a product page. Not for a searchable help centre with articles and categories, or collapsible settings inside an app (use the kit's Accordion)."
|
|
48
48
|
}
|
|
49
49
|
}
|
package/r/file-upload.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "FileUpload",
|
|
6
6
|
"description": "Files chosen or dropped, each uploaded through your async onUpload(file, { onProgress, signal }) with a progress bar, cancel while it runs, a retry when it fails and a remove once done. Variants: a list of file cards, one file at a time, Uploading/Finished sections, image thumbnails (one or two columns), and a profile picture.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"docs": "Installed to components/makersclaw/blocks/file-upload. Import: import { FileUpload, formatSize, fileIcon } from \"@/components/makersclaw/blocks/file-upload\". Props and an example: components/makersclaw/blocks/file-upload/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
48
48
|
"meta": {
|
|
49
49
|
"makersclaw": {
|
|
50
|
-
"version": "0.
|
|
50
|
+
"version": "0.8.0",
|
|
51
51
|
"useWhen": "A form or page takes files the app stores: attachments on a record, a CSV to import, product photos, a logo, a profile picture. Not for showing files already stored with no way to add (a table or list), and not for a chat message's attachments (the chat block's composer has those)."
|
|
52
52
|
}
|
|
53
53
|
}
|
package/r/flow-canvas.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "FlowCanvas",
|
|
6
6
|
"description": "A Miro-like board (React Flow) in the workspace theme: sticky notes, shapes, text, frames, cards and images on an infinite dotted canvas; a tool rail with shortcuts, double-click for a sticky, drag-out connectors that re-route as cards move, labels on connectors, a floating toolbar over the selection, right-click menu, undo/redo, copy/paste, snap to grid, minimap, list view and presentation mode. The board is plain JSON; helpers turn it into text or markdown and lay it out. Other blocks add their own node types and rail tools.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [
|
|
11
11
|
"@xyflow/react@12.11.6"
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"path": "registry/blocks/flow-canvas/README.md",
|
|
64
64
|
"type": "registry:file",
|
|
65
65
|
"target": "@components/makersclaw/blocks/flow-canvas/README.md",
|
|
66
|
-
"content": "# FlowCanvas\n\nA Miro-like board in the workspace theme. Sticky notes, shapes, text,\nframes, cards and images on an infinite dotted canvas. A tool rail on the\nleft (a bar along the bottom on a narrow canvas), double-click anywhere\nfor a sticky, drag from a card's dot to another card to connect them —\nor drop on empty canvas to pick what to add there. A floating toolbar\nover the selection, a right-click menu, undo/redo, copy/paste, snap to\ngrid, a minimap, a list view for keyboards and screen readers, and a\npresentation mode that steps through the frames.\n\nIt is [React Flow](https://reactflow.dev) (`@xyflow/react`, MIT)\nunderneath, with its colours mapped onto the workspace's tokens. The\ncanvas blocks — `account-map`, `process-map`, `journey-map`, `idea-board`\n— are built on this one: they import it, never `@xyflow/react`.\n\n```jsx\nimport { FlowCanvas, boardToText } from \"../components/blocks/flow-canvas\"; // a Makersclaw app (\"./blocks/flow-canvas\" from components/)\nimport { FlowCanvas, boardToText } from \"@/components/makersclaw/blocks/flow-canvas\"; // a shadcn project\n```\n\n`FlowCanvas` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`boardToText`, `autoLayout`, `createNode`…) are\nplain functions and work on the server too.\n\n`makersclaw add flow-canvas` adds `@xyflow/react` (pinned, exact) to\npackage.json: run `npm install` after. It loads with the page (no\n`import()`): the board mounts after the first render; until then — on the\nserver — the box holds the board as text for a screen reader.\n\n## The data\n\nA board is plain JSON. Store it as it comes out of `onChange` (one `jsonb`\ncolumn).\n\n```js\n{\n nodes: [\n { id: \"s1\", type: \"sticky\", position: { x: 40, y: 80 }, width: 200, height: 200,\n data: { text: \"Interview churned customers\", color: \"warning\", badge: 3, meta: \"Priya\" } },\n { id: \"f1\", type: \"frame\", position: { x: 0, y: 0 }, width: 720, height: 420, zIndex: -1,\n data: { title: \"Discover\", color: \"chart-2\" } },\n ],\n edges: [\n { id: \"e1\", source: \"s1\", target: \"s2\", label: \"feeds\",\n data: { line: \"curved\", arrow: \"end\", dashed: false } },\n ],\n viewport: { x: 0, y: 0, zoom: 1 }, // where the person last looked; optional\n}\n```\n\n- **Positions are absolute**, top-left corner, in board pixels. A card in a\n frame is not stored relative to it: a frame holds whatever lies inside\n its rectangle (`frameContents`, `frameOf`). Dragging a frame moves what\n is inside it.\n- **Colours are token names**, never colour values, so a board repaints\n with the theme: `\"warning\"` (the yellow sticky), `\"chart-1\"` … `\"chart-5\"`,\n `\"success\"`, `\"info\"`, `\"destructive\"`, `\"muted\"`. `COLORS` lists them;\n `STICKY_COLORS` is the sticky palette.\n- **Paint order is list order** (later is on top); frames always sit\n under everything (`zIndex: -1`).\n- `width`/`height` are set by resizing; `text` and `card` nodes leave\n `height` out and grow with their content.\n- A `sticky` or `shape` always has a box: one that arrives without a\n `width` or `height` gets its type's default for the missing side\n (sticky 200×200; a shape its outline's, e.g. pill 180×64). Their words\n are fitted to that box, so give a starter board's stickies the size you\n want them to have (`withFittedSize` is what the canvas applies).\n- A node may carry React Flow's own flags: `draggable: false`,\n `deletable: false`, `selectable: false`, `connectable: false`. Locking\n (the toolbar's lock) sets `data.locked: true` with `draggable` and\n `deletable` false.\n\nThe built-in node types and their `data`:\n\n| type | data |\n| --- | --- |\n| `sticky` | `text`, `color` (default `\"warning\"`), `badge?` (a count in the corner: votes), `meta?` (a small line at the foot: an author) |\n| `shape` | `text`, `shape`: `rect` · `ellipse` · `diamond` · `pill`, `color` (default `\"muted\"`), `textSize`: `sm` · `md` · `lg` · `xl` |\n| `text` | `text`, `textSize`, `align?`: `left` · `center` · `right` |\n| `frame` | `title`, `color?` (a faint wash and edge; none = the card surface) |\n| `card` | `title`, `body?`, `icon?` (a kit icon name), `badge?`, `color?` (a strip on top), `kind?`, anything else your `renderCard` reads |\n| `image` | `src`, `alt` |\n\nConnectors: `data.line` `curved` (default) · `straight` · `elbow`;\n`data.arrow` `end` (default) · `both` · `none`; `data.dashed`. They leave\nand enter by the sides of the two cards that face each other, so they\nre-route as cards move; handle ids are not stored.\n\n## Props\n\n- `value` — the board, controlled. A new `value` that differs from what\n the canvas last sent through `onChange` replaces it (loading another\n board, or a change made elsewhere — a vote count); that replacement can\n be undone. Pass back exactly what `onChange` gave you and nothing resets.\n- `defaultValue` — the starting board, uncontrolled. With neither, it\n starts empty (and shows the empty state).\n- `onChange(board)` — after each committed change (a drag ends, a card is\n placed, words stop being edited, a colour is picked), debounced by\n `changeDelay` (default 400 ms). The board is clean JSON (see above).\n Selection, typing in progress and panning never call it.\n- `onSave(board)` — `async`, return `{ ok: true }` or `{ ok: false, error }`\n (a throw counts as failed). Called after `onChange`, and on ⌘S. The\n canvas shows \"Saving…\", \"Saved\", or the error with a Retry beside\n undo/redo. Leave it out when `onChange` already saves.\n- `changeDelay` — ms, default 400.\n- `readOnly` — `false` (default). `true`: no rail, no editing, no\n connecting; pan, zoom, fit, the list view and `onOpenNode` still work.\n- `presentation` — `false` (default). `true`: read-only, and a bar steps\n through the frames in reading order (← → or the buttons; each fills the\n screen). `onExitPresentation()` — Esc and the bar's Exit call it; turn\n `presentation` off there.\n- `height` — the canvas's height, default `560` (px; any CSS length works:\n `\"70vh\"`, `\"100%\"` inside a sized box).\n- `bare` — `false` (default: rounded, bordered). `true`: no border or\n radius, for a full-page board.\n- `label` — the board's accessible name, default `\"Board\"`; also the list\n view's heading.\n- `className` — layout around it (margin, width). Never colour.\n- `palette` — the sticky colours offered, token names, default\n `STICKY_COLORS` (six). The toolbar adds a few more for shapes and frames.\n- `snap` — `true` (default): positions and sizes snap to `grid` (default\n `20` px — the dot spacing). The person can turn it off from the view menu.\n- `minimap` — `false` (default): whether the minimap starts open (a toggle\n in the zoom bar either way; hidden on a narrow canvas).\n- `hideAttribution` — `false` (default). React Flow asks that its small\n credit stays; its MIT licence allows hiding it.\n- `canvasRef` — a ref (`useFlowCanvas()`) that receives the handle (below).\n- `onSelectionChange(nodes, edges)` — the selection changed (a side panel\n for the selected person).\n- `onOpenNode(node)` — double-click or Enter on a node that has no words of\n its own to edit: your own node types, cards drawn by `renderCard`, and\n every node in read-only mode. Open your `Sheet` there. Without it,\n double-click edits the words.\n- `describeNode(node)` — a one-line description of one of YOUR node types,\n for the list view, `boardToText` and the server placeholder; return\n nothing for the default (`Sticky: \"…\"`).\n\nExtending it — how a block makes the canvas its own:\n\n- `nodeTypes` — `{ person: PersonNode, … }`: your own node components,\n added to the built-in ones (a key that matches a built-in replaces it).\n See \"Your own node types\".\n- `renderCard(node, ctx)` — draws the inside of every `card` node; return\n `undefined` for the default (title + body). `ctx`: `{ selected, editing,\n update(patch), api }`. Kit parts only (Avatar, Badge, Stat…); give an\n interactive part inside `className=\"nodrag\"` so pressing it does not drag\n the card. The simplest way to put your own card on the board.\n- `tools` — more rail tools: `[{ id, label, icon, shortcut?, create?,\n onSelect?, sizable?, connectable? }]`. See \"Rail tools\".\n- `rail` — the rail's contents, in order: built-in ids (`\"select\"`,\n `\"hand\"`, `\"sticky\"`, `\"shape\"`, `\"text\"`, `\"connector\"`, `\"frame\"`,\n `\"image\"`), your tools' ids, and `\"|\"` for a divider. Default: all the\n built-ins, then your tools (and `\"image\"` when `pickImage` is set). A\n process map drops the sticky: `rail={[\"select\", \"hand\", \"|\", \"shape\", \"connector\", \"step\"]}`.\n- `pickImage()` — `async () => ({ src, alt, width?, height? }) | null`: turns\n on the image tool (your upload, your picker). Without it there is no\n image tool; image nodes in the data still show.\n- `topBar` — a node shown floating at the top centre: a title, a save\n button, a share menu, a timer.\n- `toolbarExtra(selection, api)` — more controls at the end of the\n floating toolbar over a selection (`selection.nodes`): kit\n `IconButton size=\"sm\"` / `Menu` parts. \"Vote\", \"Assign owner\".\n- `contextMenuItems(selection, api)` — more right-click items, kit `Menu`\n shape (`[{ label, onSelect, icon?, shortcut?, tone? }]`); `selection.nodes`\n is empty for a right-click on the empty canvas.\n- `emptyState` — a node in place of the first-run hint; `false` for none.\n- `templates` — the starters on the empty state: `[{ id, label, icon?,\n description?, board }]` (`board` a board or a function returning one).\n Default `starterTemplates()` (Brainstorm, Flowchart, Retro); `[]` for none.\n- `constrain(node, { reason, board })` — runs on every node a change\n touched before it is committed (`reason`: `\"move\"` after a drag or a\n nudge, `\"create\"`, `\"paste\"`, `\"resize\"`, `\"align\"`, `\"layout\"`). Return\n the node adjusted (a new position, size or data), or nothing to keep it.\n This is how a journey map snaps a sticky into its cell and records it.\n- `beforeChange(next, prev, reason)` — runs on the whole board at every\n commit (`next` and `prev` are clean boards). Return `false` to refuse the\n change (the board goes back), a board to commit instead, or nothing to\n accept it.\n- `canConnect(connection, board)` — `false` refuses a connector\n (`connection`: `{ source, target }`). A card never connects to itself.\n\n## The handle\n\n`const board = useFlowCanvas()`, `<FlowCanvas canvasRef={board} />`, then\n`board.current?.…`. Tools' `create`/`onSelect`, `renderCard`,\n`toolbarExtra` and `contextMenuItems` get the same object as `api`, and a\nnode component reaches it with `useCanvas().api`. Every change made through\nit commits (undoable, then `onChange`) unless `{ commit: false }` says not.\n\n- Read: `getBoard()` (clean JSON, with the viewport) · `getSelection()` →\n `{ nodes, edges }` · `toText()` · `toMarkdown({ title })` · `canUndo()` ·\n `canRedo()` · `getTool()` · `readOnly`\n- Nodes: `addNodes(node | nodes, { select = true, edit = false, edges })` →\n ids · `updateNode(id, patch | (node) => patch, { commit })` ·\n `updateNodeData(id, patch | (data) => patch, { commit })` ·\n `updateNodesData(ids, patch)` · `removeNodes(ids)` · `duplicate(ids)` ·\n `lock(ids, on)` · `arrange(ids, \"front\" | \"back\")` · `align(ids, \"left\" |\n \"center\" | \"right\" | \"top\" | \"middle\" | \"bottom\")` · `distribute(ids,\n \"horizontal\" | \"vertical\")` · `frameSelection(ids)`\n- Connectors: `addEdge({ source, target, label?, data? })` → id ·\n `updateEdge(id, patch, { commit })` · `removeEdges(ids)`\n- The whole board: `setBoard(board, { fit = true })` (undoable) ·\n `autoLayout({ direction: \"LR\" | \"TB\", ids?, rankGap?, nodeGap? })` ·\n `undo()` · `redo()` · `save()` (send now, skipping the debounce)\n- Editing: `edit(id)` (start editing a node's words) · `editEdge(id)` ·\n `stopEditing()` · `commit(reason, ids?)` (after `{ commit: false }` changes)\n- View: `select(ids, edgeIds?)` · `focusNode(id)` (select, centre, focus) ·\n `fitView({ nodes?: [{ id }] })` · `zoomIn()` · `zoomOut()` · `zoomTo(z)` ·\n `centerPoint()` (the board point at the middle of the screen — where a\n picker's choice lands) · `screenToFlow({ x, y })` · `setTool(id)`\n\n## Your own node types\n\nA node component gets React Flow's props: `{ id, type, data, selected,\nwidth, height, dragging }`. Wrap its body in `NodeShell` — it draws the\nselection box, the four connection dots (shown on hover), the resize\nhandles, the lock mark and the whole-card drop target while a connector is\ndragged. Without it (or `NodeHandles`), connectors cannot reach the node.\nRead the canvas with `useCanvas()`; change the node through `api`.\n\n```jsx\nimport { FlowCanvas, NodeShell, EditableText, useCanvas, createNode } from \"./blocks/flow-canvas\";\nimport { Avatar, Badge } from \"./ui\";\n\nfunction PersonNode({ id, data, selected }) {\n const { api, editingNode, readOnly } = useCanvas();\n return (\n <NodeShell id={id} selected={selected} locked={data.locked} resizable={false}>\n <div className=\"flex w-64 items-center gap-3 rounded-lg border border-border bg-card p-3 shadow-sm\">\n <Avatar name={data.name} src={data.avatar} />\n <div className=\"min-w-0 flex-1\">\n <EditableText\n singleLine\n editing={!readOnly && editingNode === id}\n value={data.name}\n onChange={(v) => api.updateNodeData(id, { name: v }, { commit: false })}\n onDone={() => api.stopEditing()}\n label=\"Name\"\n className=\"truncate text-sm font-medium\"\n />\n <p className=\"truncate text-xs text-muted-foreground\">{data.title}</p>\n </div>\n <Badge tone={data.role === \"blocker\" ? \"danger\" : \"neutral\"}>{data.role}</Badge>\n </div>\n </NodeShell>\n );\n}\nconst NODE_TYPES = { person: PersonNode }; // module level: a new object each render remounts every node\n\n<FlowCanvas nodeTypes={NODE_TYPES} describeNode={(n) => n.type === \"person\" ? `Person: ${n.data.name} (${n.data.role})` : undefined} … />\n```\n\n- `NodeShell` props: `id`, `selected`, `locked`, `resizable` (default\n true), `minWidth`/`minHeight` (40/30), `keepAspectRatio`, `autoHeight`\n (height follows content), `handles` (false: nothing connects), `ring`\n (false: draw your own selection), `className` (layout only).\n- `EditableText` props: `value`, `editing`, `onChange(text)`, `onDone()`,\n `placeholder`, `singleLine` (Enter finishes), `label`, `className`,\n `style`. Shown and edited in the same box, so nothing jumps.\n- Double-click and Enter call `onOpenNode(node)` for your types when you\n pass it, else they set `editingNode` to the node's id (use it as above).\n- Colour your node from a token in its data with `colorClasses(token)` →\n `{ tint, solid, fill, stroke, wash, edge }` class strings\n (`colorClasses(\"chart-3\").tint` is `bg-chart-3/30`). `Palette`,\n `StickySwatch`, `ColorDot` and `ShapeGlyph` are the canvas's own pickers\n and swatches, for a legend or a side panel.\n- An interactive kit part inside a node takes `className=\"nodrag\"`.\n- Put a node's actions in `toolbarExtra` or `contextMenuItems`, not in a\n kit `Menu`/`Popover` inside the node: the node lives in the zoomed and\n panned viewport (a CSS transform), and the kit's floating panels anchor\n to their button without knowing about it. The canvas's own toolbars sit\n outside the viewport for this reason.\n\n## Rail tools\n\n```js\nconst TOOLS = [\n // A placing tool: pick it (or press P), click the canvas, a node appears there.\n { id: \"person\", label: \"Person\", icon: \"user\", shortcut: \"P\",\n create: (point, { rect, api }) => createNode(\"person\", point, { width: 256, data: { name: \"\", role: \"user\" } }) },\n // An action: pressing it runs onSelect — open your picker, then add.\n { id: \"pick\", label: \"Add from CRM\", icon: \"search\",\n onSelect: (api) => openPicker((p) => api.addNodes(createNode(\"person\", api.centerPoint(), { data: p }))) },\n];\n```\n\n- `create(point, { rect, api, grid })` returns a node or several (use\n `createNode`: it centres on `point` and snaps). With `sizable: true` the\n person can drag out a rectangle and you get it as `rect`.\n- A tool with `create` is also offered in the menu that opens when a\n connector is dropped on empty canvas (the new node arrives connected);\n `connectable: false` leaves it out.\n- The tool returns to Select after one placement. `shortcut` is one key.\n\n## Constraining behaviour — recipes\n\n- **Snap stickies into cells** (journey map): compute the cells from your\n lanes, then\n `constrain={(n) => { if (n.type !== \"sticky\") return; const r = nodeRect(n); const c = snapToCell(r, cells); return c && { position: { x: c.x, y: c.y }, data: { ...n.data, cell: c.cellId } }; }}`.\n- **Lock the lanes**: lanes are `frame` nodes with `draggable: false,\n deletable: false, selectable: false` (and `data.locked: true` to hide the\n resize handles). They still hold what is dropped inside them.\n- **Only steps connect** (process map): `canConnect={(c, b) => isStep(c.source) && isStep(c.target)}`.\n- **At most N votes** (idea board): keep votes in `data.badge` (the sticky\n shows it) and refuse extra ones in your own handler; or\n `beforeChange={(next) => votesOk(next) || false}`.\n- **Tidy a flow**: `api.autoLayout({ direction: \"LR\" })`, or in data\n `autoLayout(nodes, edges, { direction: \"TB\" })` before passing `value`.\n\n## The helpers\n\nImport them from the block; they take a board and never throw on a partial\none.\n\n- `emptyBoard()` — `{ nodes: [], edges: [] }`.\n- `createNode(type, point, { id?, data?, width?, height?, rect?, grid = 20, … })`\n — a node centred on `point` with its type's defaults, snapped. Extra keys\n (`draggable`, `zIndex`…) go on the node.\n- `createEdge(source, target, { id?, label?, data? })`.\n- `cleanBoard(board)` — what is stored (React Flow's working fields gone).\n- `boardToText(board, { describe? })` — plain text for an agent or a screen\n reader: `Board: 12 items, 5 connectors.`, then each frame with what it\n holds (reading order, nested), loose items, and `Connectors:` as\n `\"A\" → \"B\" (label)`.\n- `boardToMarkdown(board, { title?, describe? })` — a heading per frame, a\n bullet per item, a Connections list.\n- `autoLayout(nodes, edges, { direction = \"LR\", rankGap = 80, nodeGap = 40, ids?, grid = 20 })`\n — a layered layout written here (no dagre/elk): cycles broken, ranks by\n longest path, barycentre sweeps against crossings; unconnected nodes in\n a row after; the top-left corner kept. Returns every node, the laid-out\n ones moved. Frames are left alone unless named in `ids`.\n- `alignNodes(nodes, ids, how)`, `distributeNodes(nodes, ids, axis)`,\n `duplicateNodes(board, ids, { offset })` → `{ nodes, edges }`,\n `removeNodes(board, ids)`.\n- `frameContents(board, frameId)` → ids inside it; `frameOf(board)` →\n Map(nodeId → the smallest frame holding it).\n- `nodeRect(node)`, `boardBounds(nodes)`, `snapPoint(p, grid)`,\n `snapToCell(rect, cells, { pad })` → `{ x, y, cellId }`,\n `fitFontSize(text, width, height)` (the sticky's shrinking words).\n- `historyInit(board)`, `historyReducer(state, { type: \"push\" | \"undo\" |\n \"redo\" | \"reset\", present? })` — the undo stack the canvas uses, for a\n block that keeps its own.\n- `starterTemplates()`, `describeNode(node, describe?)`, `nodeText(node)`,\n `COLORS`, `STICKY_COLORS`, `COLOR_NAMES`, `SHAPES`, `GRID`.\n\n## Mouse and keyboard\n\n- Scroll or Space + drag pans; pinch or Ctrl/⌘ + scroll zooms; the middle\n button drags the canvas in any tool.\n- Drag on empty canvas draws a selection box (what it fully encloses);\n Shift-click adds to the selection; drag a frame by its title.\n- Tools: V select, H hand, N sticky, S shape, T text, L connector, F frame,\n and your tools' keys. Shapes, text and frames can be dragged out to a size.\n- Enter edits the selected card's words, Esc stops; Delete/Backspace\n deletes; ⌘D duplicates; ⌘C/⌘X/⌘V copy, cut, paste (paste lines of text\n from elsewhere: a sticky each); ⌘A selects all; ⌘Z / ⇧⌘Z undo, redo;\n arrows nudge 1 px (⇧ 10); ⌘] / ⌘[ front, back; ⇧⌘L lock; ⇧1 fit;\n ⇧2 zoom to selection; ⌘+ / ⌘− / ⌘0 zoom; ? the shortcuts sheet.\n- Tab walks the cards (each selected as it gets focus); Shift+F10 or the\n Menu key opens the actions menu; the List view (zoom bar) shows the board\n as a list, and choosing a row goes back to that card.\n\n## Example\n\n```jsx\n// surfaces/launch.jsx — client: one project's board\n\"use client\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow } from \"../lib/data\";\nimport { updateRow } from \"../lib/actions\";\nimport { FlowCanvas } from \"../components/blocks/flow-canvas\";\n\nexport default function LaunchSurface() {\n const id = useSearchParams().get(\"row\");\n const { row: project, loading } = useRow({ source: \"projects\", id });\n if (loading || !project) return null;\n return <LaunchBoard key={project.id} project={project} />;\n}\n\nfunction LaunchBoard({ project }) {\n const [board, setBoard] = useState(project.board); // jsonb, may be null; read once per project\n return (\n <FlowCanvas\n value={board ?? undefined}\n onChange={setBoard}\n onSave={(b) => updateRow({ source: \"projects\", id: project.id, patch: { board: b } })}\n label={`${project.name} board`}\n height=\"calc(100dvh - 8rem)\"\n />\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nA card drawn with kit parts, and an agent reading the board:\n\n```jsx\n<FlowCanvas\n value={board}\n onChange={setBoard}\n renderCard={(n) => n.data.kind === \"deal\" ? (\n <div className=\"grid gap-1 p-3\">\n <p className=\"text-sm font-medium\">{n.data.title}</p>\n <Stat size=\"sm\" label=\"Value\" value={n.data.amount} />\n </div>\n ) : undefined}\n/>\n\nconst context = boardToText(board); // hand this to the agent\n```\n\n## What it will not do\n\n- Collaborate live (several cursors at once). Saves are last-write-wins.\n- Draw freehand (that is `sketch-canvas`), or export an image.\n- Nest data under frames: membership is geometric, so a card dragged out\n of a frame simply leaves it.\n- Route connectors around cards; they take the shortest curve between the\n facing sides.\n- Upload images itself: pass `pickImage`.\n- Render on the server: the server shows the board as text until the\n canvas mounts. For a read-only page that must be rich in the HTML, render\n `boardToMarkdown` yourself.\n\nA board it cannot be is your own component in `components/` — not an edit\nto this block. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add flow-canvas` restores them.\n\n## Credits\n\nReact Flow (https://github.com/xyflow/xyflow), MIT, by webkid GmbH —\ninstalled from npm as a dependency, not copied here. Its attribution link\nstays in the corner unless `hideAttribution`. See THIRD-PARTY-NOTICES.md.\n"
|
|
66
|
+
"content": "# FlowCanvas\n\nA Miro-like board in the workspace theme. Sticky notes, shapes, text,\nframes, cards and images on an infinite dotted canvas. A tool rail on the\nleft (a bar along the bottom on a narrow canvas), double-click anywhere\nfor a sticky, drag from a card's dot to another card to connect them —\nor drop on empty canvas to pick what to add there. A floating toolbar\nover the selection, a right-click menu, undo/redo, copy/paste, snap to\ngrid, a minimap, a list view for keyboards and screen readers, and a\npresentation mode that steps through the frames.\n\nIt is [React Flow](https://reactflow.dev) (`@xyflow/react`, MIT)\nunderneath, with its colours mapped onto the workspace's tokens. The\ncanvas blocks — `account-map`, `process-map`, `journey-map`, `idea-board`\n— are built on this one: they import it, never `@xyflow/react`.\n\n```jsx\nimport { FlowCanvas, boardToText } from \"../components/blocks/flow-canvas\"; // a Makersclaw app (\"./blocks/flow-canvas\" from components/)\nimport { FlowCanvas, boardToText } from \"@/components/makersclaw/blocks/flow-canvas\"; // a shadcn project\n```\n\n`FlowCanvas` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`boardToText`, `autoLayout`, `createNode`…) are\nplain functions and work on the server too.\n\n`makersclaw add flow-canvas` adds `@xyflow/react` (pinned, exact) to\npackage.json: run `npm install` after. It loads with the page (no\n`import()`): the board mounts after the first render; until then — on the\nserver — the box holds the board as text for a screen reader.\n\n## The data\n\nA board is plain JSON. Store it as it comes out of `onChange` (one `jsonb`\ncolumn).\n\n```js\n{\n nodes: [\n { id: \"s1\", type: \"sticky\", position: { x: 40, y: 80 }, width: 200, height: 200,\n data: { text: \"Interview churned customers\", color: \"warning\", badge: 3, meta: \"Priya\" } },\n { id: \"f1\", type: \"frame\", position: { x: 0, y: 0 }, width: 720, height: 420, zIndex: -1,\n data: { title: \"Discover\", color: \"chart-2\" } },\n ],\n edges: [\n { id: \"e1\", source: \"s1\", target: \"s2\", label: \"feeds\",\n data: { line: \"curved\", arrow: \"end\", dashed: false } },\n ],\n viewport: { x: 0, y: 0, zoom: 1 }, // where the person last looked; optional\n}\n```\n\n- **Positions are absolute**, top-left corner, in board pixels. A card in a\n frame is not stored relative to it: a frame holds whatever lies inside\n its rectangle (`frameContents`, `frameOf`). Dragging a frame moves what\n is inside it.\n- **Colours are token names**, never colour values, so a board repaints\n with the theme: `\"warning\"` (the yellow sticky), `\"chart-1\"` … `\"chart-5\"`,\n `\"success\"`, `\"info\"`, `\"destructive\"`, `\"muted\"`. `COLORS` lists them;\n `STICKY_COLORS` is the sticky palette.\n- **Paint order is list order** (later is on top); frames always sit\n under everything (`zIndex: -1`).\n- `width`/`height` are set by resizing; `text` and `card` nodes leave\n `height` out and grow with their content.\n- A `sticky` or `shape` always has a box: one that arrives without a\n `width` or `height` gets its type's default for the missing side\n (sticky 200×200; a shape its outline's, e.g. pill 180×64). Their words\n are fitted to that box, so give a starter board's stickies the size you\n want them to have (`withFittedSize` is what the canvas applies).\n- A node may carry React Flow's own flags: `draggable: false`,\n `deletable: false`, `selectable: false`, `connectable: false`. Locking\n (the toolbar's lock) sets `data.locked: true` with `draggable` and\n `deletable` false.\n\nThe built-in node types and their `data`:\n\n| type | data |\n| --- | --- |\n| `sticky` | `text`, `color` (default `\"warning\"`), `badge?` (a count in the corner: votes), `meta?` (a small line at the foot: an author) |\n| `shape` | `text`, `shape`: `rect` · `ellipse` · `diamond` · `pill`, `color` (default `\"muted\"`), `textSize`: `sm` · `md` · `lg` · `xl` |\n| `text` | `text`, `textSize`, `align?`: `left` · `center` · `right` |\n| `frame` | `title`, `color?` (a faint wash and edge; none = the card surface) |\n| `card` | `title`, `body?`, `icon?` (a kit icon name), `badge?`, `color?` (a strip on top), `kind?`, anything else your `renderCard` reads |\n| `image` | `src`, `alt` |\n\nConnectors: `data.line` `curved` (default) · `straight` · `elbow`;\n`data.arrow` `end` (default) · `both` · `none`; `data.dashed`. They leave\nand enter by the sides of the two cards that face each other, so they\nre-route as cards move; handle ids are not stored.\n\n## Props\n\n- `value` — the board, controlled. A new `value` that differs from what\n the canvas last sent through `onChange` replaces it (loading another\n board, or a change made elsewhere — a vote count); that replacement can\n be undone. Pass back exactly what `onChange` gave you and nothing resets.\n- `defaultValue` — the starting board, uncontrolled. With neither, it\n starts empty (and shows the empty state).\n- `onChange(board)` — after each committed change (a drag ends, a card is\n placed, words stop being edited, a colour is picked), debounced by\n `changeDelay` (default 400 ms). The board is clean JSON (see above).\n Selection, typing in progress and panning never call it.\n- `onSave(board)` — `async`, return `{ ok: true }` or `{ ok: false, error }`\n (a throw counts as failed). Called after `onChange`, and on ⌘S. The\n canvas shows \"Saving…\", \"Saved\", or the error with a Retry beside\n undo/redo. Leave it out when `onChange` already saves.\n- `changeDelay` — ms, default 400.\n- `readOnly` — `false` (default). `true`: no rail, no editing, no\n connecting; pan, zoom, fit, the list view and `onOpenNode` still work.\n- `presentation` — `false` (default). `true`: read-only, and a bar steps\n through the frames in reading order (← → or the buttons; each fills the\n screen). `onExitPresentation()` — Esc and the bar's Exit call it; turn\n `presentation` off there.\n- `height` — the canvas's height, default `560` (px; any CSS length works:\n `\"100%\"` inside a sized box, such as the kit's `AppLayout\n contentMode=\"fill\"` for a board that is the whole surface, the\n Example). Never a `vh` length: `npm run check` refuses a size taken\n from the window.\n- `bare` — `false` (default: rounded, bordered). `true`: no border or\n radius, for a full-page board.\n- `label` — the board's accessible name, default `\"Board\"`; also the list\n view's heading.\n- `className` — layout around it (margin, width). Never colour.\n- `palette` — the sticky colours offered, token names, default\n `STICKY_COLORS` (six). The toolbar adds a few more for shapes and frames.\n- `snap` — `true` (default): positions and sizes snap to `grid` (default\n `20` px — the dot spacing). The person can turn it off from the view menu.\n- `minimap` — `false` (default): whether the minimap starts open (a toggle\n in the zoom bar either way; hidden on a narrow canvas).\n- `hideAttribution` — `false` (default). React Flow asks that its small\n credit stays; its MIT licence allows hiding it.\n- `canvasRef` — a ref (`useFlowCanvas()`) that receives the handle (below).\n- `onSelectionChange(nodes, edges)` — the selection changed (a side panel\n for the selected person).\n- `onOpenNode(node)` — double-click or Enter on a node that has no words of\n its own to edit: your own node types, cards drawn by `renderCard`, and\n every node in read-only mode. Open your `Sheet` there. Without it,\n double-click edits the words.\n- `describeNode(node)` — a one-line description of one of YOUR node types,\n for the list view, `boardToText` and the server placeholder; return\n nothing for the default (`Sticky: \"…\"`).\n\nExtending it — how a block makes the canvas its own:\n\n- `nodeTypes` — `{ person: PersonNode, … }`: your own node components,\n added to the built-in ones (a key that matches a built-in replaces it).\n See \"Your own node types\".\n- `renderCard(node, ctx)` — draws the inside of every `card` node; return\n `undefined` for the default (title + body). `ctx`: `{ selected, editing,\n update(patch), api }`. Kit parts only (Avatar, Badge, Stat…); give an\n interactive part inside `className=\"nodrag\"` so pressing it does not drag\n the card. The simplest way to put your own card on the board.\n- `tools` — more rail tools: `[{ id, label, icon, shortcut?, create?,\n onSelect?, sizable?, connectable? }]`. See \"Rail tools\".\n- `rail` — the rail's contents, in order: built-in ids (`\"select\"`,\n `\"hand\"`, `\"sticky\"`, `\"shape\"`, `\"text\"`, `\"connector\"`, `\"frame\"`,\n `\"image\"`), your tools' ids, and `\"|\"` for a divider. Default: all the\n built-ins, then your tools (and `\"image\"` when `pickImage` is set). A\n process map drops the sticky: `rail={[\"select\", \"hand\", \"|\", \"shape\", \"connector\", \"step\"]}`.\n- `pickImage()` — `async () => ({ src, alt, width?, height? }) | null`: turns\n on the image tool (your upload, your picker). Without it there is no\n image tool; image nodes in the data still show.\n- `topBar` — a node shown floating at the top centre: a title, a save\n button, a share menu, a timer.\n- `toolbarExtra(selection, api)` — more controls at the end of the\n floating toolbar over a selection (`selection.nodes`): kit\n `IconButton size=\"sm\"` / `Menu` parts. \"Vote\", \"Assign owner\".\n- `contextMenuItems(selection, api)` — more right-click items, kit `Menu`\n shape (`[{ label, onSelect, icon?, shortcut?, tone? }]`); `selection.nodes`\n is empty for a right-click on the empty canvas.\n- `emptyState` — a node in place of the first-run hint; `false` for none.\n- `templates` — the starters on the empty state: `[{ id, label, icon?,\n description?, board }]` (`board` a board or a function returning one).\n Default `starterTemplates()` (Brainstorm, Flowchart, Retro); `[]` for none.\n- `constrain(node, { reason, board })` — runs on every node a change\n touched before it is committed (`reason`: `\"move\"` after a drag or a\n nudge, `\"create\"`, `\"paste\"`, `\"resize\"`, `\"align\"`, `\"layout\"`). Return\n the node adjusted (a new position, size or data), or nothing to keep it.\n This is how a journey map snaps a sticky into its cell and records it.\n- `beforeChange(next, prev, reason)` — runs on the whole board at every\n commit (`next` and `prev` are clean boards). Return `false` to refuse the\n change (the board goes back), a board to commit instead, or nothing to\n accept it.\n- `canConnect(connection, board)` — `false` refuses a connector\n (`connection`: `{ source, target }`). A card never connects to itself.\n\n## The handle\n\n`const board = useFlowCanvas()`, `<FlowCanvas canvasRef={board} />`, then\n`board.current?.…`. Tools' `create`/`onSelect`, `renderCard`,\n`toolbarExtra` and `contextMenuItems` get the same object as `api`, and a\nnode component reaches it with `useCanvas().api`. Every change made through\nit commits (undoable, then `onChange`) unless `{ commit: false }` says not.\n\n- Read: `getBoard()` (clean JSON, with the viewport) · `getSelection()` →\n `{ nodes, edges }` · `toText()` · `toMarkdown({ title })` · `canUndo()` ·\n `canRedo()` · `getTool()` · `readOnly`\n- Nodes: `addNodes(node | nodes, { select = true, edit = false, edges })` →\n ids · `updateNode(id, patch | (node) => patch, { commit })` ·\n `updateNodeData(id, patch | (data) => patch, { commit })` ·\n `updateNodesData(ids, patch)` · `removeNodes(ids)` · `duplicate(ids)` ·\n `lock(ids, on)` · `arrange(ids, \"front\" | \"back\")` · `align(ids, \"left\" |\n \"center\" | \"right\" | \"top\" | \"middle\" | \"bottom\")` · `distribute(ids,\n \"horizontal\" | \"vertical\")` · `frameSelection(ids)`\n- Connectors: `addEdge({ source, target, label?, data? })` → id ·\n `updateEdge(id, patch, { commit })` · `removeEdges(ids)`\n- The whole board: `setBoard(board, { fit = true })` (undoable) ·\n `autoLayout({ direction: \"LR\" | \"TB\", ids?, rankGap?, nodeGap? })` ·\n `undo()` · `redo()` · `save()` (send now, skipping the debounce)\n- Editing: `edit(id)` (start editing a node's words) · `editEdge(id)` ·\n `stopEditing()` · `commit(reason, ids?)` (after `{ commit: false }` changes)\n- View: `select(ids, edgeIds?)` · `focusNode(id)` (select, centre, focus) ·\n `fitView({ nodes?: [{ id }] })` · `zoomIn()` · `zoomOut()` · `zoomTo(z)` ·\n `centerPoint()` (the board point at the middle of the screen — where a\n picker's choice lands) · `screenToFlow({ x, y })` · `setTool(id)`\n\n## Your own node types\n\nA node component gets React Flow's props: `{ id, type, data, selected,\nwidth, height, dragging }`. Wrap its body in `NodeShell` — it draws the\nselection box, the four connection dots (shown on hover), the resize\nhandles, the lock mark and the whole-card drop target while a connector is\ndragged. Without it (or `NodeHandles`), connectors cannot reach the node.\nRead the canvas with `useCanvas()`; change the node through `api`.\n\n```jsx\nimport { FlowCanvas, NodeShell, EditableText, useCanvas, createNode } from \"./blocks/flow-canvas\";\nimport { Avatar, Badge } from \"./ui\";\n\nfunction PersonNode({ id, data, selected }) {\n const { api, editingNode, readOnly } = useCanvas();\n return (\n <NodeShell id={id} selected={selected} locked={data.locked} resizable={false}>\n <div className=\"flex w-64 items-center gap-3 rounded-lg border border-border bg-card p-3 shadow-sm\">\n <Avatar name={data.name} src={data.avatar} />\n <div className=\"min-w-0 flex-1\">\n <EditableText\n singleLine\n editing={!readOnly && editingNode === id}\n value={data.name}\n onChange={(v) => api.updateNodeData(id, { name: v }, { commit: false })}\n onDone={() => api.stopEditing()}\n label=\"Name\"\n className=\"truncate text-sm font-medium\"\n />\n <p className=\"truncate text-xs text-muted-foreground\">{data.title}</p>\n </div>\n <Badge tone={data.role === \"blocker\" ? \"danger\" : \"neutral\"}>{data.role}</Badge>\n </div>\n </NodeShell>\n );\n}\nconst NODE_TYPES = { person: PersonNode }; // module level: a new object each render remounts every node\n\n<FlowCanvas nodeTypes={NODE_TYPES} describeNode={(n) => n.type === \"person\" ? `Person: ${n.data.name} (${n.data.role})` : undefined} … />\n```\n\n- `NodeShell` props: `id`, `selected`, `locked`, `resizable` (default\n true), `minWidth`/`minHeight` (40/30), `keepAspectRatio`, `autoHeight`\n (height follows content), `handles` (false: nothing connects), `ring`\n (false: draw your own selection), `className` (layout only).\n- `EditableText` props: `value`, `editing`, `onChange(text)`, `onDone()`,\n `placeholder`, `singleLine` (Enter finishes), `label`, `className`,\n `style`. Shown and edited in the same box, so nothing jumps.\n- Double-click and Enter call `onOpenNode(node)` for your types when you\n pass it, else they set `editingNode` to the node's id (use it as above).\n- Colour your node from a token in its data with `colorClasses(token)` →\n `{ tint, solid, fill, stroke, wash, edge }` class strings\n (`colorClasses(\"chart-3\").tint` is `bg-chart-3/30`). `Palette`,\n `StickySwatch`, `ColorDot` and `ShapeGlyph` are the canvas's own pickers\n and swatches, for a legend or a side panel.\n- An interactive kit part inside a node takes `className=\"nodrag\"`.\n- Put a node's actions in `toolbarExtra` or `contextMenuItems`, not in a\n kit `Menu`/`Popover` inside the node: the node lives in the zoomed and\n panned viewport (a CSS transform), and the kit's floating panels anchor\n to their button without knowing about it. The canvas's own toolbars sit\n outside the viewport for this reason.\n\n## Rail tools\n\n```js\nconst TOOLS = [\n // A placing tool: pick it (or press P), click the canvas, a node appears there.\n { id: \"person\", label: \"Person\", icon: \"user\", shortcut: \"P\",\n create: (point, { rect, api }) => createNode(\"person\", point, { width: 256, data: { name: \"\", role: \"user\" } }) },\n // An action: pressing it runs onSelect — open your picker, then add.\n { id: \"pick\", label: \"Add from CRM\", icon: \"search\",\n onSelect: (api) => openPicker((p) => api.addNodes(createNode(\"person\", api.centerPoint(), { data: p }))) },\n];\n```\n\n- `create(point, { rect, api, grid })` returns a node or several (use\n `createNode`: it centres on `point` and snaps). With `sizable: true` the\n person can drag out a rectangle and you get it as `rect`.\n- A tool with `create` is also offered in the menu that opens when a\n connector is dropped on empty canvas (the new node arrives connected);\n `connectable: false` leaves it out.\n- The tool returns to Select after one placement. `shortcut` is one key.\n\n## Constraining behaviour — recipes\n\n- **Snap stickies into cells** (journey map): compute the cells from your\n lanes, then\n `constrain={(n) => { if (n.type !== \"sticky\") return; const r = nodeRect(n); const c = snapToCell(r, cells); return c && { position: { x: c.x, y: c.y }, data: { ...n.data, cell: c.cellId } }; }}`.\n- **Lock the lanes**: lanes are `frame` nodes with `draggable: false,\n deletable: false, selectable: false` (and `data.locked: true` to hide the\n resize handles). They still hold what is dropped inside them.\n- **Only steps connect** (process map): `canConnect={(c, b) => isStep(c.source) && isStep(c.target)}`.\n- **At most N votes** (idea board): keep votes in `data.badge` (the sticky\n shows it) and refuse extra ones in your own handler; or\n `beforeChange={(next) => votesOk(next) || false}`.\n- **Tidy a flow**: `api.autoLayout({ direction: \"LR\" })`, or in data\n `autoLayout(nodes, edges, { direction: \"TB\" })` before passing `value`.\n\n## The helpers\n\nImport them from the block; they take a board and never throw on a partial\none.\n\n- `emptyBoard()` — `{ nodes: [], edges: [] }`.\n- `createNode(type, point, { id?, data?, width?, height?, rect?, grid = 20, … })`\n — a node centred on `point` with its type's defaults, snapped. Extra keys\n (`draggable`, `zIndex`…) go on the node.\n- `createEdge(source, target, { id?, label?, data? })`.\n- `cleanBoard(board)` — what is stored (React Flow's working fields gone).\n- `boardToText(board, { describe? })` — plain text for an agent or a screen\n reader: `Board: 12 items, 5 connectors.`, then each frame with what it\n holds (reading order, nested), loose items, and `Connectors:` as\n `\"A\" → \"B\" (label)`.\n- `boardToMarkdown(board, { title?, describe? })` — a heading per frame, a\n bullet per item, a Connections list.\n- `autoLayout(nodes, edges, { direction = \"LR\", rankGap = 80, nodeGap = 40, ids?, grid = 20 })`\n — a layered layout written here (no dagre/elk): cycles broken, ranks by\n longest path, barycentre sweeps against crossings; unconnected nodes in\n a row after; the top-left corner kept. Returns every node, the laid-out\n ones moved. Frames are left alone unless named in `ids`.\n- `alignNodes(nodes, ids, how)`, `distributeNodes(nodes, ids, axis)`,\n `duplicateNodes(board, ids, { offset })` → `{ nodes, edges }`,\n `removeNodes(board, ids)`.\n- `frameContents(board, frameId)` → ids inside it; `frameOf(board)` →\n Map(nodeId → the smallest frame holding it).\n- `nodeRect(node)`, `boardBounds(nodes)`, `snapPoint(p, grid)`,\n `snapToCell(rect, cells, { pad })` → `{ x, y, cellId }`,\n `fitFontSize(text, width, height)` (the sticky's shrinking words).\n- `historyInit(board)`, `historyReducer(state, { type: \"push\" | \"undo\" |\n \"redo\" | \"reset\", present? })` — the undo stack the canvas uses, for a\n block that keeps its own.\n- `starterTemplates()`, `describeNode(node, describe?)`, `nodeText(node)`,\n `COLORS`, `STICKY_COLORS`, `COLOR_NAMES`, `SHAPES`, `GRID`.\n\n## Mouse and keyboard\n\n- Scroll or Space + drag pans; pinch or Ctrl/⌘ + scroll zooms; the middle\n button drags the canvas in any tool.\n- Drag on empty canvas draws a selection box (what it fully encloses);\n Shift-click adds to the selection; drag a frame by its title.\n- Tools: V select, H hand, N sticky, S shape, T text, L connector, F frame,\n and your tools' keys. Shapes, text and frames can be dragged out to a size.\n- Enter edits the selected card's words, Esc stops; Delete/Backspace\n deletes; ⌘D duplicates; ⌘C/⌘X/⌘V copy, cut, paste (paste lines of text\n from elsewhere: a sticky each); ⌘A selects all; ⌘Z / ⇧⌘Z undo, redo;\n arrows nudge 1 px (⇧ 10); ⌘] / ⌘[ front, back; ⇧⌘L lock; ⇧1 fit;\n ⇧2 zoom to selection; ⌘+ / ⌘− / ⌘0 zoom; ? the shortcuts sheet.\n- Tab walks the cards (each selected as it gets focus); Shift+F10 or the\n Menu key opens the actions menu; the List view (zoom bar) shows the board\n as a list, and choosing a row goes back to that card.\n\n## Example\n\n```jsx\n// surfaces/launch.jsx — client: one project's board\n\"use client\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow } from \"../lib/data\";\nimport { updateRow } from \"../lib/actions\";\nimport { AppLayout } from \"../components/ui\";\nimport { FlowCanvas } from \"../components/blocks/flow-canvas\";\n\nexport default function LaunchSurface() {\n const id = useSearchParams().get(\"row\");\n const { row: project, loading } = useRow({ source: \"projects\", id });\n if (loading || !project) return null;\n return <LaunchBoard key={project.id} project={project} />;\n}\n\nfunction LaunchBoard({ project }) {\n const [board, setBoard] = useState(project.board); // jsonb, may be null; read once per project\n return (\n <AppLayout contentMode=\"fill\">\n <FlowCanvas\n value={board ?? undefined}\n onChange={setBoard}\n onSave={(b) => updateRow({ source: \"projects\", id: project.id, patch: { board: b } })}\n label={`${project.name} board`}\n height=\"100%\"\n bare\n />\n </AppLayout>\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nA card drawn with kit parts, and an agent reading the board:\n\n```jsx\n<FlowCanvas\n value={board}\n onChange={setBoard}\n renderCard={(n) => n.data.kind === \"deal\" ? (\n <div className=\"grid gap-1 p-3\">\n <p className=\"text-sm font-medium\">{n.data.title}</p>\n <Stat size=\"sm\" label=\"Value\" value={n.data.amount} />\n </div>\n ) : undefined}\n/>\n\nconst context = boardToText(board); // hand this to the agent\n```\n\n## What it will not do\n\n- Collaborate live (several cursors at once). Saves are last-write-wins.\n- Draw freehand (that is `sketch-canvas`), or export an image.\n- Nest data under frames: membership is geometric, so a card dragged out\n of a frame simply leaves it.\n- Route connectors around cards; they take the shortest curve between the\n facing sides.\n- Upload images itself: pass `pickImage`.\n- Render on the server: the server shows the board as text until the\n canvas mounts. For a read-only page that must be rich in the HTML, render\n `boardToMarkdown` yourself.\n\nA board it cannot be is your own component in `components/` — not an edit\nto this block. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add flow-canvas` restores them.\n\n## Credits\n\nReact Flow (https://github.com/xyflow/xyflow), MIT, by webkid GmbH —\ninstalled from npm as a dependency, not copied here. Its attribution link\nstays in the corner unless `hideAttribution`. See THIRD-PARTY-NOTICES.md.\n"
|
|
67
67
|
}
|
|
68
68
|
],
|
|
69
69
|
"categories": [
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"docs": "Installed to components/makersclaw/blocks/flow-canvas. Import: import { FlowCanvas, useFlowCanvas, useCanvas, NodeShell, NodeHandles, EditableText, Palette, StickySwatch, ColorDot, ShapeGlyph, colorClasses, emptyBoard, createNode, createEdge, cleanBoard, boardToText, boardToMarkdown, autoLayout, snapToCell, frameContents, historyReducer, starterTemplates } from \"@/components/makersclaw/blocks/flow-canvas\". Props and an example: components/makersclaw/blocks/flow-canvas/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
87
87
|
"meta": {
|
|
88
88
|
"makersclaw": {
|
|
89
|
-
"version": "0.
|
|
89
|
+
"version": "0.8.0",
|
|
90
90
|
"useWhen": "A screen where people arrange things spatially and connect them: a brainstorm, a flowchart or process map, an account or org map, a journey map, a retro, a planning board. The canvas blocks (account-map, process-map, journey-map, idea-board) are built on it. Not for freehand drawing (sketch-canvas), records in stage columns (board), or a chart of numbers (chart)."
|
|
91
91
|
}
|
|
92
92
|
}
|