makersclaw 0.6.0 → 0.7.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.
Files changed (141) hide show
  1. package/README.md +48 -8
  2. package/package.json +1 -1
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +3 -3
  5. package/r/audio-player.json +3 -3
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +4 -4
  8. package/r/calendar-view.json +4 -4
  9. package/r/cart.json +3 -3
  10. package/r/chart.json +3 -3
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +4 -4
  13. package/r/comment-box.json +4 -4
  14. package/r/contact-section.json +3 -3
  15. package/r/core.json +3 -3
  16. package/r/csv-import.json +4 -4
  17. package/r/data-table.json +3 -3
  18. package/r/diagram.json +3 -3
  19. package/r/doc-editor.json +4 -4
  20. package/r/doc-page.json +4 -4
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +3 -3
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +4 -4
  27. package/r/job-panel.json +3 -3
  28. package/r/json-editor.json +4 -4
  29. package/r/map.json +5 -5
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +6 -6
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +6 -6
  39. package/r/product-detail.json +3 -3
  40. package/r/product-grid.json +4 -4
  41. package/r/record-form.json +3 -3
  42. package/r/record-notes.json +5 -5
  43. package/r/registry.json +121 -121
  44. package/r/search-dialog.json +3 -3
  45. package/r/settings-list.json +3 -3
  46. package/r/setup-checklist.json +3 -3
  47. package/r/share-dialog.json +3 -3
  48. package/r/sidebar.json +2 -2
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +5 -5
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +4 -4
  53. package/r/task-list.json +4 -4
  54. package/r/task-model.json +3 -3
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +3 -3
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +14 -5
  59. package/registry/blocks/approval-request/README.md +15 -8
  60. package/registry/blocks/audio-player/README.md +17 -10
  61. package/registry/blocks/board/README.md +13 -9
  62. package/registry/blocks/board/board.jsx +1 -1
  63. package/registry/blocks/calendar-view/README.md +28 -4
  64. package/registry/blocks/cart/README.md +14 -10
  65. package/registry/blocks/chart/README.md +14 -7
  66. package/registry/blocks/chat/README.md +43 -13
  67. package/registry/blocks/code-editor/README.md +4 -0
  68. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  69. package/registry/blocks/comment-box/README.md +20 -13
  70. package/registry/blocks/contact-section/README.md +7 -6
  71. package/registry/blocks/csv-import/README.md +34 -12
  72. package/registry/blocks/data-table/README.md +41 -8
  73. package/registry/blocks/diagram/diagram.jsx +1 -1
  74. package/registry/blocks/doc-editor/README.md +18 -6
  75. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  76. package/registry/blocks/doc-page/README.md +16 -6
  77. package/registry/blocks/file-upload/README.md +7 -1
  78. package/registry/blocks/flow-canvas/README.md +16 -5
  79. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  80. package/registry/blocks/footer/README.md +4 -2
  81. package/registry/blocks/gantt/README.md +18 -12
  82. package/registry/blocks/job-panel/README.md +1 -1
  83. package/registry/blocks/json-editor/README.md +16 -4
  84. package/registry/blocks/map/README.md +19 -3
  85. package/registry/blocks/map/map-view.jsx +1 -1
  86. package/registry/blocks/map/maplibre.js +1 -1
  87. package/registry/blocks/mind-map/README.md +16 -5
  88. package/registry/blocks/org-chart/README.md +10 -7
  89. package/registry/blocks/page-builder/README.md +13 -6
  90. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  91. package/registry/blocks/page-builder/page-view.jsx +1 -1
  92. package/registry/blocks/page-builder/page.js +1 -1
  93. package/registry/blocks/pdf-viewer/README.md +22 -20
  94. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  95. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  96. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  97. package/registry/blocks/product-detail/README.md +13 -7
  98. package/registry/blocks/product-detail/demo.jsx +1 -1
  99. package/registry/blocks/product-grid/README.md +12 -10
  100. package/registry/blocks/record-form/README.md +26 -9
  101. package/registry/blocks/record-notes/README.md +42 -30
  102. package/registry/blocks/search-dialog/README.md +38 -7
  103. package/registry/blocks/settings-list/README.md +29 -17
  104. package/registry/blocks/setup-checklist/README.md +12 -14
  105. package/registry/blocks/share-dialog/README.md +1 -1
  106. package/registry/blocks/signature-pad/README.md +15 -4
  107. package/registry/blocks/sketch-canvas/README.md +13 -4
  108. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  109. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  110. package/registry/blocks/spreadsheet/README.md +17 -8
  111. package/registry/blocks/stat-cards/README.md +17 -8
  112. package/registry/blocks/task-list/README.md +14 -11
  113. package/registry/blocks/task-model/README.md +3 -3
  114. package/registry/blocks/transaction-list/README.md +15 -7
  115. package/registry/blocks/whiteboard/README.md +19 -13
  116. package/src/cli.mjs +31 -4
  117. package/src/install.mjs +263 -21
  118. package/src/registry.mjs +78 -1
  119. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  120. package/templates/next/browser/app/layout.jsx +94 -0
  121. package/templates/next/browser/app/page.jsx +60 -0
  122. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  123. package/templates/next/browser/lib/actions.js +16 -0
  124. package/templates/next/browser/lib/data.js +43 -0
  125. package/templates/next/files/README.md +810 -79
  126. package/templates/next/files/app/frame.js +128 -4
  127. package/templates/next/files/check/run.mjs +1438 -41
  128. package/templates/next/files/components/ui/item.jsx +2 -2
  129. package/templates/next/files/components/ui/notice.jsx +1 -1
  130. package/templates/next/files/components/ui/separator.jsx +1 -1
  131. package/templates/next/files/lib/actions.js +170 -51
  132. package/templates/next/files/lib/brain-server.js +96 -0
  133. package/templates/next/files/lib/brain.js +413 -0
  134. package/templates/next/files/lib/claw.js +11 -0
  135. package/templates/next/files/lib/data.js +119 -29
  136. package/templates/next/files/lib/db.js +97 -17
  137. package/templates/next/files/lib/door.js +680 -0
  138. package/templates/next/files/next.config.mjs +36 -2
  139. package/templates/next/files/surfaces.js +3 -2
  140. package/templates/next/template.json +9 -3
  141. package/templates/next/files/schema.sql +0 -22
package/r/cart.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "title": "Cart",
6
6
  "description": "A shopping cart: line items with picture, option, quantity and remove, a summary (subtotal, discount code, shipping, tax, total) and Checkout. step=\"checkout\" is the checkout page: contact and delivery details beside the order summary, ending at onPay, which hands off to Paddle. No card fields. Empty and loading states built in.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json"
9
9
  ],
10
10
  "dependencies": [],
11
11
  "files": [
@@ -31,7 +31,7 @@
31
31
  "path": "registry/blocks/cart/README.md",
32
32
  "type": "registry:file",
33
33
  "target": "@components/makersclaw/blocks/cart/README.md",
34
- "content": "# Cart\n\nThe basket and the checkout page. The cart lists line items (picture,\nname, option, quantity, remove) beside a summary: subtotal, a discount\ncode, shipping, tax, the total and Checkout. `step=\"checkout\"` is the next\npage: the buyer's email and delivery address beside the same summary,\nread-only. It ends at `onPay`. **There are no card fields**: the app hands\noff to Paddle there, which takes the payment.\n\nEvery change goes out through a callback that returns `{ ok }`; a failed\none is shown on the row (or the summary) that asked for it.\n\n```jsx\nimport { Cart } from \"../components/blocks/cart\"; // a Makersclaw app (\"./blocks/cart\" from components/)\nimport { Cart } from \"@/components/makersclaw/blocks/cart\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the cart in the server surface (README §4).\n\n## The data\n\nOne line item per row of the cart:\n\n```js\n{ id: \"li_1\", name: \"Merino crew\", image: \"https://…\", price: 89, quantity: 2, variant: \"Charcoal · M\" }\n```\n\n- `price` is one unit's price, a number in the currency's major unit\n (89.5 is $89.50). The row shows `price × quantity`.\n- `variant` is the chosen option in words, or empty.\n- `maxQuantity` (optional) caps the quantity picker; default 10.\n- Optional: `key` (a line's own key when the same product sits on two\n lines, one per size; default `id`), `href` (the name links to the\n product), `compareAt` (the old unit price, struck through beside the\n line total), `currency` (over the `currency` prop).\n\nThe same shape is what `product-detail` and `product-grid` hand to their\nadd-to-cart callbacks, so a cart is those rows.\n\n## Props\n\n- `items` — the line items. An empty list shows the empty state.\n- `step` — `\"cart\"` (default) or `\"checkout\"`.\n- `currency` — an ISO code, default `\"USD\"`. `locale` — for the money\n format, default the reader's.\n- `shipping` · `tax` — a number is a row that adds to the total (0 reads\n \"Free\" for shipping); a string is shown as written and adds nothing\n (`tax=\"Calculated at payment\"` — Paddle works tax out). Leave one out\n and its row is not drawn.\n- `discount` — the code in force: `{ code, amount, label? }`; `amount` is\n taken off the total.\n- `note` — a line at the foot of the summary, beside a truck (\"Free\n delivery on orders over $100\").\n- `loading` — skeleton rows. `error` — a line above everything (the cart\n did not load).\n- `title` — the heading; default \"Cart\" or \"Checkout\".\n\nThe cart step:\n\n- `onQuantityChange(item, quantity)` — save the new quantity; return\n `{ ok: true }` or `{ ok: false, error }`. Without it the quantity is\n shown, not picked.\n- `onRemove(item)` — remove the row, same return. Without it no ×.\n- `onApplyCode(code)` — a discount code field appears; return\n `{ ok: false, error: \"That code has expired.\" }` to say why not. On\n success pass the new `discount` back in.\n- `onRemoveCode()` — an × beside the discount.\n- `onCheckout()` — the Checkout button; move to the checkout step (route\n or state). May return `{ ok: false, error }`.\n- `checkoutLabel` — default \"Checkout\".\n- `onContinueShopping()` or `continueHref` — \"Continue shopping\", in the\n summary and in the empty state. `continueLabel` renames it.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n\nThe checkout step:\n\n- `onPay(details)` — required for checkout. `details` is `{ email }`, plus\n `{ name, address, city, postal, country }` when the address is asked\n for. Save the order, open Paddle's checkout with it, and return\n `{ ok: true }`; `{ ok: false, error }` shows the reason above the button.\n- `requireAddress` — default `true`; `false` for things that are not\n shipped (downloads, tickets): only the email is asked for.\n- `terms` — a checkbox label that must be ticked before paying (\"I agree\n to the terms of sale\"); a string or a node with links. Omit for none.\n- `defaultDetails` — prefill: `{ email, name, address, city, postal, country }`.\n- `payLabel` — the button; default \"Continue to payment · $123.00\".\n- `payNote` — the line under it; default says the card is entered on the\n next screen. `null` for none.\n- `onBack()` — \"Back to cart\". `backLabel` renames it.\n\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/Basket.jsx — client\n\"use client\";\nimport { useState } from \"react\";\nimport { deleteRow, updateRow } from \"../lib/actions\";\nimport { Cart } from \"./blocks/cart\";\n\nexport default function Basket({ items, error }) {\n const [step, setStep] = useState(\"cart\");\n return (\n <Cart\n items={items}\n error={error}\n step={step}\n shipping={0}\n tax=\"Calculated at payment\"\n note=\"Free delivery on every order this month.\"\n onQuantityChange={(item, quantity) => updateRow({ source: \"public.cart_items\", id: item.id, patch: { quantity } })}\n onRemove={(item) => deleteRow({ source: \"public.cart_items\", id: item.id })}\n onCheckout={() => setStep(\"checkout\")}\n onBack={() => setStep(\"cart\")}\n continueHref=\"/shop\"\n terms=\"I agree to the terms of sale\"\n onPay={async (details) => {\n const res = await fetch(\"/api/checkout\", { method: \"POST\", body: JSON.stringify(details) });\n if (!res.ok) return { ok: false, error: \"We could not start the payment. Try again.\" };\n const { transactionId } = await res.json();\n window.Paddle?.Checkout.open({ transactionId });\n return { ok: true };\n }}\n />\n );\n}\n```\n\n## What it will not do\n\n- Take a card number, expiry or CVC. Payment is Paddle's overlay or page,\n opened from `onPay`; the block never sees a card.\n- Work out tax or shipping: pass the numbers (or the words) in.\n- Keep the cart: `items` is the app's; every change is a callback.\n- Validate an address against a country's format: it checks that each\n field is filled.\n\nA cart it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add cart` restores them.\n\n## Credits\n\nShape and behaviour from Creative Tim UI (https://github.com/creativetimofficial/ui), MIT; this is a rewrite on the Makersclaw kit.\n"
34
+ "content": "# Cart\n\nThe basket and the checkout page. The cart lists line items (picture,\nname, option, quantity, remove) beside a summary: subtotal, a discount\ncode, shipping, tax, the total and Checkout. `step=\"checkout\"` is the next\npage: the buyer's email and delivery address beside the same summary,\nread-only. It ends at `onPay`. **There are no card fields**: the app hands\noff to Paddle there, which takes the payment.\n\nEvery change goes out through a callback that returns `{ ok }`; a failed\none is shown on the row (or the summary) that asked for it.\n\n```jsx\nimport { Cart } from \"../components/blocks/cart\"; // a Makersclaw app (\"./blocks/cart\" from components/)\nimport { Cart } from \"@/components/makersclaw/blocks/cart\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the cart in the server surface (README §4).\n\n## The data\n\nOne line item per row of the cart:\n\n```js\n{ id: \"li_1\", name: \"Merino crew\", image: \"https://…\", price: 89, quantity: 2, variant: \"Charcoal · M\" }\n```\n\n- `price` is one unit's price, a number in the currency's major unit\n (89.5 is $89.50). The row shows `price × quantity`.\n- `variant` is the chosen option in words, or empty.\n- `maxQuantity` (optional) caps the quantity picker; default 10.\n- Optional: `key` (a line's own key when the same product sits on two\n lines, one per size; default `id`), `href` (the name links to the\n product), `compareAt` (the old unit price, struck through beside the\n line total), `currency` (over the `currency` prop).\n\nThe same shape is what `product-detail` and `product-grid` hand to their\nadd-to-cart callbacks, so a cart is those rows.\n\n## Props\n\n- `items` — the line items. An empty list shows the empty state.\n- `step` — `\"cart\"` (default) or `\"checkout\"`.\n- `currency` — an ISO code, default `\"USD\"`. `locale` — for the money\n format, default the reader's.\n- `shipping` · `tax` — a number is a row that adds to the total (0 reads\n \"Free\" for shipping); a string is shown as written and adds nothing\n (`tax=\"Calculated at payment\"` — Paddle works tax out). Leave one out\n and its row is not drawn.\n- `discount` — the code in force: `{ code, amount, label? }`; `amount` is\n taken off the total.\n- `note` — a line at the foot of the summary, beside a truck (\"Free\n delivery on orders over $100\").\n- `loading` — skeleton rows. `error` — a line above everything (the cart\n did not load).\n- `title` — the heading; default \"Cart\" or \"Checkout\".\n\nThe cart step:\n\n- `onQuantityChange(item, quantity)` — save the new quantity; return\n `{ ok: true }` or `{ ok: false, error }`. Without it the quantity is\n shown, not picked.\n- `onRemove(item)` — remove the row, same return. Without it no ×.\n- `onApplyCode(code)` — a discount code field appears; return\n `{ ok: false, error: \"That code has expired.\" }` to say why not. On\n success pass the new `discount` back in.\n- `onRemoveCode()` — an × beside the discount.\n- `onCheckout()` — the Checkout button; move to the checkout step (route\n or state). May return `{ ok: false, error }`.\n- `checkoutLabel` — default \"Checkout\".\n- `onContinueShopping()` or `continueHref` — \"Continue shopping\", in the\n summary and in the empty state. `continueLabel` renames it.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n\nThe checkout step:\n\n- `onPay(details)` — required for checkout. `details` is `{ email }`, plus\n `{ name, address, city, postal, country }` when the address is asked\n for. Save the order, open Paddle's checkout with it, and return\n `{ ok: true }`; `{ ok: false, error }` shows the reason above the button.\n- `requireAddress` — default `true`; `false` for things that are not\n shipped (downloads, tickets): only the email is asked for.\n- `terms` — a checkbox label that must be ticked before paying (\"I agree\n to the terms of sale\"); a string or a node with links. Omit for none.\n- `defaultDetails` — prefill: `{ email, name, address, city, postal, country }`.\n- `payLabel` — the button; default \"Continue to payment · $123.00\".\n- `payNote` — the line under it; default says the card is entered on the\n next screen. `null` for none.\n- `onBack()` — \"Back to cart\". `backLabel` renames it.\n\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// surfaces/basket.jsx — client: the cart, read and changed\n\"use client\";\nimport { useState } from \"react\";\nimport { useRows } from \"../lib/data\";\nimport { deleteRow, updateRow } from \"../lib/actions\";\nimport { Cart } from \"../components/blocks/cart\";\n\nexport default function Basket() {\n const { rows: items, error, loading } = useRows({ source: \"cart_items\", orderBy: \"created_at asc\" });\n const [step, setStep] = useState(\"cart\");\n return (\n <Cart\n items={items}\n loading={loading}\n error={error}\n step={step}\n shipping={0}\n tax=\"Calculated at payment\"\n note=\"Free delivery on every order this month.\"\n onQuantityChange={(item, quantity) => updateRow({ source: \"cart_items\", id: item.id, patch: { quantity } })}\n onRemove={(item) => deleteRow({ source: \"cart_items\", id: item.id })}\n onCheckout={() => setStep(\"checkout\")}\n onBack={() => setStep(\"cart\")}\n continueHref=\"/shop\"\n terms=\"I agree to the terms of sale\"\n onPay={async () => {\n // Paddle.js opens its checkout in the browser from price ids: no route of the app's own.\n if (!window.Paddle) return { ok: false, error: \"Payments have not loaded. Try again.\" };\n window.Paddle.Checkout.open({ items: items.map((i) => ({ priceId: i.price_id, quantity: i.quantity })) });\n return { ok: true };\n }}\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\n## What it will not do\n\n- Take a card number, expiry or CVC. Payment is Paddle's overlay or page,\n opened from `onPay`; the block never sees a card.\n- Work out tax or shipping: pass the numbers (or the words) in.\n- Keep the cart: `items` is the app's; every change is a callback.\n- Validate an address against a country's format: it checks that each\n field is filled.\n\nA cart it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add cart` restores them.\n\n## Credits\n\nShape and behaviour from Creative Tim UI (https://github.com/creativetimofficial/ui), MIT; this is a rewrite on the Makersclaw kit.\n"
35
35
  }
36
36
  ],
37
37
  "categories": [
@@ -55,7 +55,7 @@
55
55
  "docs": "Installed to components/makersclaw/blocks/cart. Import: import { Cart } from \"@/components/makersclaw/blocks/cart\". Props and an example: components/makersclaw/blocks/cart/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
56
56
  "meta": {
57
57
  "makersclaw": {
58
- "version": "0.6.0",
58
+ "version": "0.7.0",
59
59
  "useWhen": "An app that sells things: the basket before paying, and the page that collects the buyer's email and address before the payment provider takes over. Not for a subscription plan picker (a pricing table) or for an order already placed (order-detail)."
60
60
  }
61
61
  }
package/r/chart.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "title": "Chart",
6
6
  "description": "Line, area (stacked), bar (grouped or stacked, columns or bars, value labels at the ends, a track behind), a muted or dashed series for a previous period to compare against, pie and donut charts, sparklines and heatmaps (a grid shaded by value, with a scale legend), drawn in SVG (the heatmap as an HTML table) with the workspace's chart colours: round ticks, gridlines, a tooltip and crosshair under the pointer or the arrow keys, a legend that hides a series, and a data table for screen readers.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json"
9
9
  ],
10
10
  "dependencies": [],
11
11
  "files": [
@@ -49,7 +49,7 @@
49
49
  "path": "registry/blocks/chart/README.md",
50
50
  "type": "registry:file",
51
51
  "target": "@components/makersclaw/blocks/chart/README.md",
52
- "content": "# Chart\n\nNumbers as a shape: `LineChart`, `AreaChart` (stacked or not), `BarChart`\n(grouped or stacked, columns or bars), `PieChart` (a pie or a donut with\nthe total in its middle), `Sparkline` (a trend beside a KPI) and\n`HeatmapChart` (a grid of two dimensions, each cell shaded by its value,\nwith `ScaleLegend` as its key). Drawn in SVG (the heatmap as an HTML\ntable), no chart library, in the workspace's chart colours. Each one is as\nwide as its container and redraws when that changes. Hover, touch or the\narrow keys show a tooltip with a crosshair; the legend hides and shows a\nseries; a screen reader gets a summary and the numbers as a table; with\nreduced motion nothing animates.\n\n```jsx\nimport { AreaChart, BarChart, HeatmapChart, PieChart, Sparkline } from \"../components/blocks/chart\"; // a Makersclaw app (\"./blocks/chart\" from components/)\nimport { AreaChart, BarChart, HeatmapChart, PieChart, Sparkline } from \"@/components/makersclaw/blocks/chart\"; // a shadcn project\n```\n\nThey are client components: render them from a `\"use client\"` component,\nand load the rows in the server surface (README §4). Put a chart in a\n`Card` — the card gives the title; the chart is only the picture.\n\n## The data\n\nRows, as `loadRows` returns them. One key holds the x (a month, a stage,\na week) and each series is another key holding a number, in the order the\nx should read — sort in `loadRows`. A value that is missing (`null`,\n`undefined`, `\"\"`) or not a number is a gap, never a zero: a line, an area\nand a sparkline break there (a value alone between two gaps is a dot), a\nbar or a slice is not drawn, a heatmap cell is blank, and the tooltip and\nthe table say —. A real zero is `0`.\n\n```js\n[{ month: \"Jan\", newBiz: 24600, expansion: 8100 }, { month: \"Feb\", newBiz: 26900, expansion: 8800 }]\n```\n\nA pie takes one row per slice: a key for the name, a key for the value.\n\nA heatmap takes one row per grid row: a key for the row's name and one key\nper column holding its number — a pivot, shaped in the query:\n\n```js\n[{ slot: \"9–12\", mon: 42, tue: 51, wed: 38 }, { slot: \"12–15\", mon: 18, tue: 24, wed: 65 }]\n```\n\n## Colours\n\n`series[].tone` picks a colour: `\"chart-1\"` … `\"chart-5\"` (or `1` … `5`).\nLeft out, the series take chart-1, chart-2, … in order. The chart colours\ncome from the workspace theme (`app/globals.css`, `--color-chart-N`): the\nfirst is the theme's primary, the others turn its hue so neighbours stay\napart in every theme. A series that means a status may take a theme tone\ninstead: `\"success\"`, `\"warning\"`, `\"danger\"`, `\"info\"`, `\"primary\"`,\n`\"muted\"`. In your own markup around a chart use the same classes:\n`bg-chart-1`, `text-chart-2`, `fill-chart-3`, `stroke-chart-4`.\n\n## Props\n\n### LineChart · AreaChart · BarChart\n\n- `data` — the rows. Empty (or missing) shows the empty state.\n- `x` — the key that holds each row's x, e.g. `\"month\"`.\n- `series` — what to draw, in order: `[{ key, label?, tone?, muted?,\n dashed? }]`. `label` defaults to the key, humanised.\n - `dashed` — LineChart and AreaChart: a dashed line with no fill under\n it. The key and the tooltip show a dashed swatch.\n - `muted` — drawn quieter and underneath the rest: the grey (unless it\n has its own `tone`), a thinner line, a fainter fill. On a BarChart it\n is only the grey.\n - Together they are a \"previous period\" to compare against:\n\n ```jsx\n <LineChart\n data={days} // [{ day: \"Mon\", thisWeek: 42, lastWeek: 35 }, …]\n x=\"day\"\n series={[{ key: \"thisWeek\", label: \"This week\" }, { key: \"lastWeek\", label: \"Last week\", muted: true, dashed: true }]}\n label=\"Signups, this week against last\"\n />\n ```\n- `format(value)` — how a value reads in the tooltip, the summary and the\n table. Default: `1,250` · `48.2K`.\n- `formatAxis(value)` — how a tick reads (default `format`); give a\n shorter one when `format` is long (`$48,200` → `$48K`).\n- `formatX(xValue, row)` — how an x reads (default: as written).\n- `label` — what the chart is, for a screen reader: `\"Revenue by month\"`.\n Default `\"Chart\"`; always set it.\n- `description` — the sentence a screen reader hears after `label`.\n Default: made from the data (the range, each series' first, last and\n highest value).\n- `xLabel` — the x column's heading in the hidden table (default: the key,\n humanised).\n- `height` — pixels, default `240`. The width is the container's.\n- `stacked` — AreaChart and BarChart: pile the series (negatives stack\n down). The tooltip adds a total. Default `false`.\n- `orientation` — BarChart: `\"vertical\"` (columns, the default) or\n `\"horizontal\"` (bars, categories down the left — for long names).\n- `valueLabels` — BarChart: each bar's value at its end (\"10%\"), in\n `format`; stacked, the stack's total. `true`, or `(value, row, key)` →\n the words (`key` is null for a total). With it the value axis is often\n noise: `xAxis={false}` (horizontal) or `yAxis={false}`. Default `false`.\n- `track` — BarChart: a quiet full-length track behind every bar, for a\n share or a distribution (a status's share of tickets). Pair it with\n `grid={false}`. Default `false`.\n- `curve` — LineChart and AreaChart: `\"monotone\"` (default, smooth and\n never overshooting), `\"linear\"` or `\"step\"`.\n- `dots` — LineChart and AreaChart: a dot on every point. Default `false`.\n- `grid` · `xAxis` · `yAxis` — gridlines and the two axes' labels. All\n default `true`.\n- `legend` — the legend under the chart; its entries are toggles that hide\n a series. Default: shown when there is more than one series.\n- `tooltip` — default `true`.\n- `onSelect(row, index)` — a click, a tap or Enter on a point or a bar\n calls it: open the month, filter by the stage.\n- `emptyLabel` · `emptyBody` — what the empty state says. Default\n `\"No data yet\"`.\n- `className` — layout around it (width, margin). Never colour.\n\n### PieChart\n\n- `data` — one row per slice.\n- `name` — the key holding a slice's name, default `\"name\"`.\n- `value` — the key holding its number, default `\"value\"`. Zero and\n negative values draw nothing; a missing one draws nothing and reads —\n in the legend and the table.\n- `donut` — open the middle. It shows the total, and the picked slice's\n value and share while one is picked. Default `false`.\n- `centerLabel` — a donut's middle instead of the total: a string, or\n `{ value, label }`.\n- `totalLabel` — the words under the total, default `\"Total\"`.\n- `format(value)` · `formatName(name, row)` · `label` · `description` ·\n `height` · `tooltip` · `onSelect(row, index)` · `emptyLabel` ·\n `emptyBody` · `className` — as above.\n- `legend` — default `true`; hiding a slice re-shares the rest.\n- `legendValues` — what follows each name in the legend: `\"percent\"`\n (default), `\"value\"` or `\"none\"`.\n- A row's own `tone` colours its slice; otherwise chart-1 … chart-5 in\n order (a sixth slice repeats chart-1 — five is the most a pie reads).\n\n### Sparkline\n\nA trend with no axes, gridlines, legend or tooltip, as wide as its box\nand `height` tall — small enough to sit under a kit `Stat` in a tile:\n\n```jsx\n<Card>\n <Stat label=\"MRR\" value=\"$84.2k\" delta={6.1} />\n <Sparkline data={mrr} label=\"MRR, last 12 months\" height={28} className=\"mt-2\" />\n</Card>\n```\n\n- `data` — numbers (`[12, 15, 14]`) or rows with `value`.\n- `value` — the key when `data` is rows, default `\"value\"`.\n- `label` — what it shows, e.g. `\"MRR, last 12 months\"`: a screen reader\n hears it with the first, last, low and high values. Always set it.\n- `format(value)` — for those words.\n- `tone` — default `\"chart-1\"`.\n- `area` — the faded fill under the line, default `true`.\n- `dot` — a dot on the latest value, default `true`.\n- `height` — default `36`. The width is the container's.\n- `className` — layout. Never colour.\n\n### HeatmapChart\n\n- `data` — one row per grid row. Empty (or no `columns`) shows the empty\n state.\n- `y` — the key holding each row's name, default `\"name\"`.\n- `columns` — the cells of a row, in order: `[\"mon\", \"tue\"]` (labels made\n from the keys) or `[{ key: \"mon\", label: \"Mon\" }, …]`.\n- `formatY(yValue, row)` — how a row's name reads (default: as written).\n- `tone` — the ramp's colour: `\"chart-1\"` (default) … `\"chart-5\"` (or\n `1` … `5`). One colour, light to full: a heatmap is one measure.\n- `min` · `max` — the ends of the ramp. Default: 0 (or the lowest value,\n when negative) and the highest value. Values past them take the end\n shades.\n- `steps` — how many shades, default `5`. Each cell takes the shade its\n value falls in (`scaleStep`), so equal shades mean equal bands.\n- `showValues` — the number on each cell, in `format`; on dark shades it\n is drawn in the background's colour so it stays readable. Default\n `false` (a compact grid of colour).\n- `format(value)` — how a value reads on the cell, in the tooltip, the\n totals and the legend. Default: `1,250` · `48.2K`.\n- `cellShape` — `\"square\"` (default, rounded) or `\"pill\"` (short and\n fully round — the contribution grid).\n- `rowTotals` · `colTotals` — a bold Total column at the right, a bold\n Total row at the foot (and the grand total where they meet). Missing\n values count as nothing. Default `false`.\n- `totalLabel` — default `\"Total\"`.\n- `tooltip` — the kit `Tooltip` on hover or keyboard focus. Default `true`.\n- `formatTooltip(value, row, column)` — its words (a string); `column` is\n `{ key, label }` and `value` is null when the cell has none. Default:\n `\"Tue · 9–12: 51\"`. The same words name the cell to a screen reader.\n- `legend` — a `ScaleLegend` under the grid, at the right. Default `true`.\n Set it `false` to place your own (in the card's header, say).\n- `legendLabels` — its end words, e.g. `[\"Less\", \"More\"]`; default the\n numbers at the two ends.\n- `onSelect(row, columnKey, value)` — a click, or Enter or Space on a\n focused cell.\n- `label` · `description` — as above; the description defaults to the\n size, the range and the highest cell.\n- `yLabel` — the row names' heading for a screen reader (default: the key,\n humanised).\n- `emptyLabel` · `emptyBody` · `className` — as above.\n\nThe grid is a table: a screen reader reads each cell with its row and\ncolumn. Tab lands on one cell; the arrow keys move, Home and End go to the\nrow's ends (with Ctrl or ⌘, the grid's). A grid wider than its box\nscrolls sideways inside it, and the row names stay at the left — on the\ncard's background, so put the heatmap in a `Card`.\n\n### ScaleLegend\n\nThe heatmap's key, for a legend placed yourself: `Less ▪▪▪▪▪ More`.\n\n- `min` · `max` — the ends, default `0` and `1`.\n- `steps` — the shades, default `5` (match the chart's).\n- `labels` — `[\"Less\", \"More\"]`, or leave it out for the numbers at the\n ends, in `format`.\n- `tone` · `format` — as the chart's.\n- `shape` — `\"square\"` (default) or `\"pill\"`, as the chart's cells.\n- `label` — what a screen reader hears it called, default `\"Scale\"`.\n\n### scaleStep · scaleShade\n\nThe ramp itself, for cells a block draws on its own.\n\n- `scaleStep(value, min, max, steps = 5)` → which shade a value falls in:\n `0` … `steps − 1`, or `null` for a missing value.\n- `scaleShade(value, { min, max, steps, tone })` → `{ step, style,\n className }`: put `style` (the fill) and `className` (the text colour\n that reads on it) on your element.\n\n### ChartLegend\n\nThe key the charts draw, for a legend of your own (several sparklines\nsharing one key, say).\n\n- `items` — `[{ key, label, tone?, value? }]`; `value` is a muted note\n after the label.\n- `hidden` — a `Set` (or array) of keys shown as off.\n- `onToggle(key)` — makes each entry a toggle; without it the legend is a\n plain list.\n- `label` — what it is called to a screen reader, default `\"Legend\"`.\n\n## Example\n\n```jsx\n// surfaces/sales.jsx — server: load, hand over\nimport { loadRows } from \"../lib/data\";\nimport SalesCharts from \"../components/SalesCharts\";\n\nexport default async function SalesSurface() {\n const { rows: months, error } = await loadRows({ source: \"public.revenue_by_month\", orderBy: \"month asc\" });\n const { rows: stages } = await loadRows({ source: \"public.deals_by_stage\", orderBy: \"position asc\" });\n const { rows: visits } = await loadRows({ source: \"public.visits_by_slot\", orderBy: \"position asc\" });\n return <SalesCharts months={months} stages={stages} visits={visits} error={error} />;\n}\n```\n\n```jsx\n// components/SalesCharts.jsx — client: the cards and the charts\n\"use client\";\nimport { Card, Notice } from \"./ui\";\nimport { AreaChart, BarChart, HeatmapChart, Sparkline } from \"./blocks/chart\";\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 });\nconst usdShort = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", notation: \"compact\" });\n\nexport default function SalesCharts({ months, stages, visits, error }) {\n return (\n <div className=\"grid gap-4\">\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n <Card title=\"MRR\">\n <Sparkline data={months} value=\"mrr\" label=\"MRR, last 12 months\" format={(v) => usd.format(v)} />\n </Card>\n <Card title=\"Revenue by month\">\n <AreaChart\n data={months}\n x=\"month\"\n series={[{ key: \"new_biz\", label: \"New business\" }, { key: \"expansion\", label: \"Expansion\" }]}\n stacked\n format={(v) => usd.format(v)}\n formatAxis={(v) => usdShort.format(v)}\n label=\"Revenue by month\"\n />\n </Card>\n <Card title=\"Deals by stage\">\n <BarChart data={stages} x=\"stage\" series={[{ key: \"deals\", label: \"Deals\" }]} orientation=\"horizontal\" label=\"Deals by stage\" />\n </Card>\n <Card title=\"Site visits by time of day\">\n <HeatmapChart\n data={visits}\n y=\"slot\"\n columns={[{ key: \"mon\", label: \"Mon\" }, { key: \"tue\", label: \"Tue\" }, { key: \"wed\", label: \"Wed\" }]}\n showValues\n rowTotals\n colTotals\n formatTooltip={(v, row, col) => `${col.label} ${row.slot}: ${v ?? 0} visits`}\n label=\"Site visits by weekday and time slot\"\n />\n </Card>\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Sort, group or sum: the rows come shaped. Totals by month or by stage\n are a query (a view, or `loadRows` over one).\n- A time scale: x is a list of labels, evenly spaced, in row order. Give\n every period a row (a month with no sales is a row with 0; a day with\n no data yet, a row with null).\n- Two value axes, scatter, radar, maps, zoom or brushing.\n- A colour outside the theme: series take the chart tones and the theme's\n status tones only. Colour by value is the heatmap's alone, on one chart\n tone.\n- Draw its own title: put it in a `Card`.\n\nA chart it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add chart` restores them.\n"
52
+ "content": "# Chart\n\nNumbers as a shape: `LineChart`, `AreaChart` (stacked or not), `BarChart`\n(grouped or stacked, columns or bars), `PieChart` (a pie or a donut with\nthe total in its middle), `Sparkline` (a trend beside a KPI) and\n`HeatmapChart` (a grid of two dimensions, each cell shaded by its value,\nwith `ScaleLegend` as its key). Drawn in SVG (the heatmap as an HTML\ntable), no chart library, in the workspace's chart colours. Each one is as\nwide as its container and redraws when that changes. Hover, touch or the\narrow keys show a tooltip with a crosshair; the legend hides and shows a\nseries; a screen reader gets a summary and the numbers as a table; with\nreduced motion nothing animates.\n\n```jsx\nimport { AreaChart, BarChart, HeatmapChart, PieChart, Sparkline } from \"../components/blocks/chart\"; // a Makersclaw app (\"./blocks/chart\" from components/)\nimport { AreaChart, BarChart, HeatmapChart, PieChart, Sparkline } from \"@/components/makersclaw/blocks/chart\"; // a shadcn project\n```\n\nThey are client components: render them from a `\"use client\"` component,\nand load the rows in the server surface (README §4). Put a chart in a\n`Card` — the card gives the title; the chart is only the picture.\n\n## The data\n\nRows, as `loadRows` returns them. One key holds the x (a month, a stage,\na week) and each series is another key holding a number, in the order the\nx should read — sort in `loadRows`. A value that is missing (`null`,\n`undefined`, `\"\"`) or not a number is a gap, never a zero: a line, an area\nand a sparkline break there (a value alone between two gaps is a dot), a\nbar or a slice is not drawn, a heatmap cell is blank, and the tooltip and\nthe table say —. A real zero is `0`.\n\n```js\n[{ month: \"Jan\", newBiz: 24600, expansion: 8100 }, { month: \"Feb\", newBiz: 26900, expansion: 8800 }]\n```\n\nA pie takes one row per slice: a key for the name, a key for the value.\n\nA heatmap takes one row per grid row: a key for the row's name and one key\nper column holding its number — a pivot, shaped in the query:\n\n```js\n[{ slot: \"9–12\", mon: 42, tue: 51, wed: 38 }, { slot: \"12–15\", mon: 18, tue: 24, wed: 65 }]\n```\n\n## Colours\n\n`series[].tone` picks a colour: `\"chart-1\"` … `\"chart-5\"` (or `1` … `5`).\nLeft out, the series take chart-1, chart-2, … in order. The chart colours\ncome from the workspace theme (`app/globals.css`, `--color-chart-N`): the\nfirst is the theme's primary, the others turn its hue so neighbours stay\napart in every theme. A series that means a status may take a theme tone\ninstead: `\"success\"`, `\"warning\"`, `\"danger\"`, `\"info\"`, `\"primary\"`,\n`\"muted\"`. In your own markup around a chart use the same classes:\n`bg-chart-1`, `text-chart-2`, `fill-chart-3`, `stroke-chart-4`.\n\n## Props\n\n### LineChart · AreaChart · BarChart\n\n- `data` — the rows. Empty (or missing) shows the empty state.\n- `x` — the key that holds each row's x, e.g. `\"month\"`.\n- `series` — what to draw, in order: `[{ key, label?, tone?, muted?,\n dashed? }]`. `label` defaults to the key, humanised.\n - `dashed` — LineChart and AreaChart: a dashed line with no fill under\n it. The key and the tooltip show a dashed swatch.\n - `muted` — drawn quieter and underneath the rest: the grey (unless it\n has its own `tone`), a thinner line, a fainter fill. On a BarChart it\n is only the grey.\n - Together they are a \"previous period\" to compare against:\n\n ```jsx\n <LineChart\n data={days} // [{ day: \"Mon\", thisWeek: 42, lastWeek: 35 }, …]\n x=\"day\"\n series={[{ key: \"thisWeek\", label: \"This week\" }, { key: \"lastWeek\", label: \"Last week\", muted: true, dashed: true }]}\n label=\"Signups, this week against last\"\n />\n ```\n- `format(value)` — how a value reads in the tooltip, the summary and the\n table. Default: `1,250` · `48.2K`.\n- `formatAxis(value)` — how a tick reads (default `format`); give a\n shorter one when `format` is long (`$48,200` → `$48K`).\n- `formatX(xValue, row)` — how an x reads (default: as written).\n- `label` — what the chart is, for a screen reader: `\"Revenue by month\"`.\n Default `\"Chart\"`; always set it.\n- `description` — the sentence a screen reader hears after `label`.\n Default: made from the data (the range, each series' first, last and\n highest value).\n- `xLabel` — the x column's heading in the hidden table (default: the key,\n humanised).\n- `height` — pixels, default `240`. The width is the container's.\n- `stacked` — AreaChart and BarChart: pile the series (negatives stack\n down). The tooltip adds a total. Default `false`.\n- `orientation` — BarChart: `\"vertical\"` (columns, the default) or\n `\"horizontal\"` (bars, categories down the left — for long names).\n- `valueLabels` — BarChart: each bar's value at its end (\"10%\"), in\n `format`; stacked, the stack's total. `true`, or `(value, row, key)` →\n the words (`key` is null for a total). With it the value axis is often\n noise: `xAxis={false}` (horizontal) or `yAxis={false}`. Default `false`.\n- `track` — BarChart: a quiet full-length track behind every bar, for a\n share or a distribution (a status's share of tickets). Pair it with\n `grid={false}`. Default `false`.\n- `curve` — LineChart and AreaChart: `\"monotone\"` (default, smooth and\n never overshooting), `\"linear\"` or `\"step\"`.\n- `dots` — LineChart and AreaChart: a dot on every point. Default `false`.\n- `grid` · `xAxis` · `yAxis` — gridlines and the two axes' labels. All\n default `true`.\n- `legend` — the legend under the chart; its entries are toggles that hide\n a series. Default: shown when there is more than one series.\n- `tooltip` — default `true`.\n- `onSelect(row, index)` — a click, a tap or Enter on a point or a bar\n calls it: open the month, filter by the stage.\n- `emptyLabel` · `emptyBody` — what the empty state says. Default\n `\"No data yet\"`.\n- `className` — layout around it (width, margin). Never colour.\n\n### PieChart\n\n- `data` — one row per slice.\n- `name` — the key holding a slice's name, default `\"name\"`.\n- `value` — the key holding its number, default `\"value\"`. Zero and\n negative values draw nothing; a missing one draws nothing and reads —\n in the legend and the table.\n- `donut` — open the middle. It shows the total, and the picked slice's\n value and share while one is picked. Default `false`.\n- `centerLabel` — a donut's middle instead of the total: a string, or\n `{ value, label }`.\n- `totalLabel` — the words under the total, default `\"Total\"`.\n- `format(value)` · `formatName(name, row)` · `label` · `description` ·\n `height` · `tooltip` · `onSelect(row, index)` · `emptyLabel` ·\n `emptyBody` · `className` — as above.\n- `legend` — default `true`; hiding a slice re-shares the rest.\n- `legendValues` — what follows each name in the legend: `\"percent\"`\n (default), `\"value\"` or `\"none\"`.\n- A row's own `tone` colours its slice; otherwise chart-1 … chart-5 in\n order (a sixth slice repeats chart-1 — five is the most a pie reads).\n\n### Sparkline\n\nA trend with no axes, gridlines, legend or tooltip, as wide as its box\nand `height` tall — small enough to sit under a kit `Stat` in a tile:\n\n```jsx\n<Card>\n <Stat label=\"MRR\" value=\"$84.2k\" delta={6.1} />\n <Sparkline data={mrr} label=\"MRR, last 12 months\" height={28} className=\"mt-2\" />\n</Card>\n```\n\n- `data` — numbers (`[12, 15, 14]`) or rows with `value`.\n- `value` — the key when `data` is rows, default `\"value\"`.\n- `label` — what it shows, e.g. `\"MRR, last 12 months\"`: a screen reader\n hears it with the first, last, low and high values. Always set it.\n- `format(value)` — for those words.\n- `tone` — default `\"chart-1\"`.\n- `area` — the faded fill under the line, default `true`.\n- `dot` — a dot on the latest value, default `true`.\n- `height` — default `36`. The width is the container's.\n- `className` — layout. Never colour.\n\n### HeatmapChart\n\n- `data` — one row per grid row. Empty (or no `columns`) shows the empty\n state.\n- `y` — the key holding each row's name, default `\"name\"`.\n- `columns` — the cells of a row, in order: `[\"mon\", \"tue\"]` (labels made\n from the keys) or `[{ key: \"mon\", label: \"Mon\" }, …]`.\n- `formatY(yValue, row)` — how a row's name reads (default: as written).\n- `tone` — the ramp's colour: `\"chart-1\"` (default) … `\"chart-5\"` (or\n `1` … `5`). One colour, light to full: a heatmap is one measure.\n- `min` · `max` — the ends of the ramp. Default: 0 (or the lowest value,\n when negative) and the highest value. Values past them take the end\n shades.\n- `steps` — how many shades, default `5`. Each cell takes the shade its\n value falls in (`scaleStep`), so equal shades mean equal bands.\n- `showValues` — the number on each cell, in `format`; on dark shades it\n is drawn in the background's colour so it stays readable. Default\n `false` (a compact grid of colour).\n- `format(value)` — how a value reads on the cell, in the tooltip, the\n totals and the legend. Default: `1,250` · `48.2K`.\n- `cellShape` — `\"square\"` (default, rounded) or `\"pill\"` (short and\n fully round — the contribution grid).\n- `rowTotals` · `colTotals` — a bold Total column at the right, a bold\n Total row at the foot (and the grand total where they meet). Missing\n values count as nothing. Default `false`.\n- `totalLabel` — default `\"Total\"`.\n- `tooltip` — the kit `Tooltip` on hover or keyboard focus. Default `true`.\n- `formatTooltip(value, row, column)` — its words (a string); `column` is\n `{ key, label }` and `value` is null when the cell has none. Default:\n `\"Tue · 9–12: 51\"`. The same words name the cell to a screen reader.\n- `legend` — a `ScaleLegend` under the grid, at the right. Default `true`.\n Set it `false` to place your own (in the card's header, say).\n- `legendLabels` — its end words, e.g. `[\"Less\", \"More\"]`; default the\n numbers at the two ends.\n- `onSelect(row, columnKey, value)` — a click, or Enter or Space on a\n focused cell.\n- `label` · `description` — as above; the description defaults to the\n size, the range and the highest cell.\n- `yLabel` — the row names' heading for a screen reader (default: the key,\n humanised).\n- `emptyLabel` · `emptyBody` · `className` — as above.\n\nThe grid is a table: a screen reader reads each cell with its row and\ncolumn. Tab lands on one cell; the arrow keys move, Home and End go to the\nrow's ends (with Ctrl or ⌘, the grid's). A grid wider than its box\nscrolls sideways inside it, and the row names stay at the left — on the\ncard's background, so put the heatmap in a `Card`.\n\n### ScaleLegend\n\nThe heatmap's key, for a legend placed yourself: `Less ▪▪▪▪▪ More`.\n\n- `min` · `max` — the ends, default `0` and `1`.\n- `steps` — the shades, default `5` (match the chart's).\n- `labels` — `[\"Less\", \"More\"]`, or leave it out for the numbers at the\n ends, in `format`.\n- `tone` · `format` — as the chart's.\n- `shape` — `\"square\"` (default) or `\"pill\"`, as the chart's cells.\n- `label` — what a screen reader hears it called, default `\"Scale\"`.\n\n### scaleStep · scaleShade\n\nThe ramp itself, for cells a block draws on its own.\n\n- `scaleStep(value, min, max, steps = 5)` → which shade a value falls in:\n `0` … `steps − 1`, or `null` for a missing value.\n- `scaleShade(value, { min, max, steps, tone })` → `{ step, style,\n className }`: put `style` (the fill) and `className` (the text colour\n that reads on it) on your element.\n\n### ChartLegend\n\nThe key the charts draw, for a legend of your own (several sparklines\nsharing one key, say).\n\n- `items` — `[{ key, label, tone?, value? }]`; `value` is a muted note\n after the label.\n- `hidden` — a `Set` (or array) of keys shown as off.\n- `onToggle(key)` — makes each entry a toggle; without it the legend is a\n plain list.\n- `label` — what it is called to a screen reader, default `\"Legend\"`.\n\n## Example\n\n```jsx\n// surfaces/sales.jsx — client: read, hand over\n\"use client\";\nimport { useRows } from \"../lib/data\";\nimport SalesCharts from \"../components/SalesCharts\";\n\nexport default function SalesSurface() {\n const months = useRows({ source: \"revenue_by_month\", orderBy: \"month asc\" });\n const stages = useRows({ source: \"deals_by_stage\", orderBy: \"position asc\" });\n const visits = useRows({ source: \"visits_by_slot\", orderBy: \"position asc\" });\n if (months.loading || stages.loading || visits.loading) return null;\n return (\n <SalesCharts months={months.rows} stages={stages.rows} visits={visits.rows}\n error={months.error ?? stages.error ?? visits.error} />\n );\n}\n```\n\n```jsx\n// components/SalesCharts.jsx — client: the cards and the charts\n\"use client\";\nimport { Card, Notice } from \"./ui\";\nimport { AreaChart, BarChart, HeatmapChart, Sparkline } from \"./blocks/chart\";\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 });\nconst usdShort = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", notation: \"compact\" });\n\nexport default function SalesCharts({ months, stages, visits, error }) {\n return (\n <div className=\"grid gap-4\">\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n <Card title=\"MRR\">\n <Sparkline data={months} value=\"mrr\" label=\"MRR, last 12 months\" format={(v) => usd.format(v)} />\n </Card>\n <Card title=\"Revenue by month\">\n <AreaChart\n data={months}\n x=\"month\"\n series={[{ key: \"new_biz\", label: \"New business\" }, { key: \"expansion\", label: \"Expansion\" }]}\n stacked\n format={(v) => usd.format(v)}\n formatAxis={(v) => usdShort.format(v)}\n label=\"Revenue by month\"\n />\n </Card>\n <Card title=\"Deals by stage\">\n <BarChart data={stages} x=\"stage\" series={[{ key: \"deals\", label: \"Deals\" }]} orientation=\"horizontal\" label=\"Deals by stage\" />\n </Card>\n <Card title=\"Site visits by time of day\">\n <HeatmapChart\n data={visits}\n y=\"slot\"\n columns={[{ key: \"mon\", label: \"Mon\" }, { key: \"tue\", label: \"Tue\" }, { key: \"wed\", label: \"Wed\" }]}\n showValues\n rowTotals\n colTotals\n formatTooltip={(v, row, col) => `${col.label} ${row.slot}: ${v ?? 0} visits`}\n label=\"Site visits by weekday and time slot\"\n />\n </Card>\n </div>\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\n## What it will not do\n\n- Sort, group or sum: the rows come shaped. Totals by month or by stage\n are a query (a view, or `loadRows` over one).\n- A time scale: x is a list of labels, evenly spaced, in row order. Give\n every period a row (a month with no sales is a row with 0; a day with\n no data yet, a row with null).\n- Two value axes, scatter, radar, maps, zoom or brushing.\n- A colour outside the theme: series take the chart tones and the theme's\n status tones only. Colour by value is the heatmap's alone, on one chart\n tone.\n- Draw its own title: put it in a `Card`.\n\nA chart it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add chart` restores them.\n"
53
53
  }
54
54
  ],
55
55
  "categories": [
@@ -76,7 +76,7 @@
76
76
  "docs": "Installed to components/makersclaw/blocks/chart. Import: import { LineChart, AreaChart, BarChart, PieChart, Sparkline, ChartLegend, HeatmapChart, ScaleLegend, scaleStep, scaleShade } from \"@/components/makersclaw/blocks/chart\". Props and an example: components/makersclaw/blocks/chart/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
77
77
  "meta": {
78
78
  "makersclaw": {
79
- "version": "0.6.0",
79
+ "version": "0.7.0",
80
80
  "useWhen": "Numbers the founder should see as a shape: revenue by month, deals by stage, pipeline by source, a KPI's trend. Not for a single figure with no history (a stat), exact lookups (a table), or records on a timeline (a calendar)."
81
81
  }
82
82
  }
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.6.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.7.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// components/SupportChat.jsx — client: one ticket's conversation\n\"use client\";\nimport { useState } from \"react\";\nimport { insertRow } from \"../lib/actions\";\nimport { ChatThread } from \"./blocks/chat\";\n\nexport default function SupportChat({ ticket, initial }) {\n const [messages, setMessages] = useState(initial);\n const patch = (id, change) => setMessages((all) => all.map((m) => (m.id === id ? { ...m, ...change } : m)));\n\n async function send(text) {\n const id = crypto.randomUUID();\n const draft = { id, role: \"user\", content: text, createdAt: new Date().toISOString(), status: \"sending\" };\n setMessages((all) => [...all, draft]);\n const r = await insertRow({ source: \"public.ticket_messages\", values: { id, ticket_id: ticket.id, body: text }, detail: text });\n patch(id, r.ok ? { status: undefined } : { status: \"failed\", error: r.error });\n }\n\n return (\n <ChatThread\n messages={messages}\n onSend={send}\n onRetry={(m) => { setMessages((all) => all.filter((x) => x.id !== m.id)); send(m.content); }}\n assistantName={ticket.agent_name}\n userName=\"You\"\n label={`Ticket ${ticket.number}`}\n />\n );\n}\n```\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 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"
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.6.0",
91
+ "version": "0.7.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
  }
@@ -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.6.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json"
9
9
  ],
10
10
  "dependencies": [
11
11
  "@codemirror/state@6.7.6",
@@ -30,7 +30,7 @@
30
30
  "path": "registry/blocks/code-editor/code-editor.jsx",
31
31
  "type": "registry:component",
32
32
  "target": "@components/makersclaw/blocks/code-editor/code-editor.jsx",
33
- "content": "\"use client\";\n\n// CodeEditor — CodeMirror 6, in the workspace theme, behind one set of\n// props. SQL (with the database's tables for autocomplete), JSON (checked\n// as you type) or plain text; line numbers, problems drawn on the text,\n// find and replace, and a run hook on Cmd/Ctrl+Enter.\n//\n// Client only, one static bundle. CodeMirror needs the DOM, and the app's\n// check refuses import() (so no next/dynamic): the modules load with the\n// page, and the editor itself is created only after the first effect (the\n// mount guard). Until then — on the server and in the first client paint —\n// the same box shows the text with its line numbers, so nothing jumps.\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button, Icon, KbdGroup, cx } from \"../../ui\";\nimport {\n Compartment,\n Decoration,\n EditorSelection,\n EditorState,\n EditorView,\n MSSQL,\n MariaSQL,\n MySQL,\n PostgreSQL,\n Prec,\n SQLite,\n StandardSQL,\n StateEffect,\n StateField,\n autocompletion,\n bracketMatching,\n closeBrackets,\n closeBracketsKeymap,\n completeFromList,\n completionKeymap,\n crosshairCursor,\n defaultKeymap,\n diagnosticCount,\n drawSelection,\n dropCursor,\n foldGutter,\n foldKeymap,\n forceLinting,\n highlightActiveLineGutter,\n highlightSelectionMatches,\n highlightSpecialChars,\n history,\n historyKeymap,\n indentOnInput,\n indentUnit,\n indentWithTab,\n json,\n keymap,\n lineNumbers,\n lintGutter,\n lintKeymap,\n linter,\n nextDiagnostic,\n placeholder as placeholderExt,\n rectangularSelection,\n searchKeymap,\n sql,\n syntaxHighlighting,\n} from \"./codemirror\";\nimport { editorTheme, highlightStyle } from \"./theme\";\nimport { lineCol, normalizeDiagnostics, offsetOf } from \"./text\";\nimport { sqlNamespace, statementAt } from \"./sql\";\nimport { checkJson, formatJson } from \"./json\";\n\n/** The modes, and the name the footer shows for each. */\nexport const LANGUAGES = { sql: \"SQL\", json: \"JSON\", text: \"Plain text\" };\n\n/** SQL dialects: keywords, quoting and functions differ between them. */\nexport const SQL_DIALECTS = { postgres: PostgreSQL, mysql: MySQL, mariadb: MariaSQL, sqlite: SQLite, mssql: MSSQL, standard: StandardSQL };\n\n/** A ref for the editor's handle: `const ed = useCodeEditor()`, then\n * `<CodeEditor editorRef={ed} />` and `ed.current?.insert(\"now()\")`. */\nexport function useCodeEditor() {\n return useRef(null);\n}\n\nconst px = (v) => (typeof v === \"number\" ? `${v}px` : v);\n\n// The frame, the same for the placeholder and the live editor. Phones get\n// 16px text: iOS zooms into anything smaller when it takes focus.\nconst FRAME =\n \"relative flex min-w-0 flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground \" +\n \"[--code-editor-size:16px] sm:[--code-editor-size:13px] transition-[border-color,box-shadow] duration-150 motion-reduce:transition-none \" +\n \"focus-within:border-ring/60 focus-within:ring-2 focus-within:ring-ring/15\";\n// CodeMirror's floating panels live inside the editor; they get the kit's shadow.\nconst FLOATING = \"[&_.cm-tooltip]:shadow-lg! [&_.cm-tooltip]:text-sm motion-reduce:[&_*]:transition-none!\";\n\nexport function CodeEditor(props) {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n if (!mounted) return <CodePlaceholder {...props} />;\n return <LiveEditor {...props} />;\n}\n\n// The placeholder's line number, as wide as CodeMirror's gutter (its lint\n// dot column is the 14px the text is pushed by, beside its own 12px).\nconst NUMBER = \"w-[54px] shrink-0 pe-1 text-right tabular-nums text-muted-foreground opacity-70 select-none\";\n\n/** What the server renders (and the first client paint): the text with\n * its line numbers, in the editor's box. Not editable, not focusable. */\nfunction CodePlaceholder({ value, defaultValue, placeholder, height, minHeight = 120, maxHeight = 480, lineNumbers: numbers = true, footer = true, language = \"text\", onRun, className, frameless = false }) {\n const text = String(value ?? defaultValue ?? \"\");\n const lines = text ? text.split(\"\\n\").slice(0, 400) : [];\n return (\n <div className={cx(FRAME, frameless && \"rounded-none border-0 bg-transparent focus-within:ring-0\", className)} aria-busy=\"true\">\n <div\n className=\"min-h-0 flex-1 overflow-hidden py-2.5 font-mono text-[length:var(--code-editor-size)] leading-[1.65]\"\n style={height != null ? { height: px(height) } : { minHeight: px(minHeight), maxHeight: px(maxHeight) }}\n >\n {lines.length ? (\n lines.map((line, i) => (\n <div key={i} className=\"flex\">\n {numbers ? <span className={NUMBER}>{i + 1}</span> : null}\n <span className=\"ps-[26px] pe-4 whitespace-pre\">{line || \" \"}</span>\n </div>\n ))\n ) : (\n <div className=\"flex\">\n {numbers ? <span className={NUMBER}>1</span> : null}\n <span className=\"ps-[26px] text-muted-foreground\">{placeholder || \" \"}</span>\n </div>\n )}\n </div>\n {footer ? <StatusBar language={language} line={1} col={1} onRun={onRun ? () => {} : null} idle /> : null}\n </div>\n );\n}\n\n// Check again now: a prop the linter reads changed (outside problems, the\n// mode, the lint function). The linter re-runs on edits by itself; for\n// anything else it needs this effect, then forceLinting to skip its delay.\nconst relint = StateEffect.define();\nconst wantsRelint = (u) => u.transactions.some((tr) => tr.effects.some((e) => e.is(relint)));\nfunction lintNow(view) {\n if (!view) return;\n view.dispatch({ effects: relint.of(null) });\n forceLinting(view);\n}\n\n// The statement that ran, lit for a moment.\nconst flashEffect = StateEffect.define();\nconst flashMark = Decoration.mark({ class: \"cm-ran\" });\nconst flashField = StateField.define({\n create: () => Decoration.none,\n update(deco, tr) {\n deco = deco.map(tr.changes);\n for (const e of tr.effects) {\n if (!e.is(flashEffect)) continue;\n deco = e.value && e.value.to > e.value.from ? Decoration.set([flashMark.range(e.value.from, e.value.to)]) : Decoration.none;\n }\n return deco;\n },\n provide: (f) => EditorView.decorations.from(f),\n});\n\nfunction markerDOM(open) {\n const el = document.createElement(\"span\");\n el.className = \"cm-fold-marker\";\n el.dataset.open = String(open);\n el.setAttribute(\"aria-hidden\", \"true\");\n el.textContent = \"⌄\";\n return el;\n}\n\nfunction languageExtension({ language, dialect, schema, defaultSchema, completions }) {\n const out = [];\n if (language === \"sql\") {\n const { namespace } = sqlNamespace(schema);\n out.push(sql({ dialect: SQL_DIALECTS[dialect] ?? PostgreSQL, schema: namespace, defaultSchema: defaultSchema || undefined, upperCaseKeywords: true }));\n } else if (language === \"json\") {\n out.push(json(), foldGutter({ markerDOM }));\n }\n if (completions?.length) {\n const list = completions.map((c) => (typeof c === \"string\" ? { label: c, type: \"variable\" } : c));\n out.push(EditorState.languageData.of(() => [{ autocomplete: completeFromList(list) }]));\n }\n return out;\n}\n\nfunction sizeExtension({ height, minHeight = 120, maxHeight = 480 }) {\n if (height != null) return EditorView.theme({ \"&\": { height: px(height) }, \".cm-scroller\": { overflow: \"auto\" } });\n return EditorView.theme({\n \"&\": { maxHeight: px(maxHeight) },\n \".cm-scroller\": { overflow: \"auto\" },\n \".cm-content, .cm-gutter\": { minHeight: px(minHeight) },\n });\n}\n\nfunction LiveEditor({\n value,\n defaultValue,\n onChange,\n language = \"text\",\n dialect = \"postgres\",\n schema,\n defaultSchema,\n completions,\n onRun,\n onSave,\n runLabel = \"Run\",\n diagnostics,\n lint,\n validate = true,\n readOnly = false,\n lineNumbers: numbers = true,\n wrap = false,\n placeholder,\n height,\n minHeight = 120,\n maxHeight = 480,\n tabSize = 2,\n autoFocus = false,\n footer = true,\n frameless = false,\n label,\n editorRef,\n className,\n}) {\n const host = useRef(null);\n const viewRef = useRef(null);\n const applying = useRef(false);\n const [status, setStatus] = useState({ line: 1, col: 1, selected: 0, problems: 0 });\n\n // The latest props for the handlers CodeMirror holds on to.\n const live = useRef({});\n live.current = { onChange, onRun, onSave, lint, language, dialect, validate, diagnostics };\n\n // Problems handed in through `diagnostics` belong to the text they were\n // found in: they show until that text changes.\n const outside = useRef({ text: null, list: [] });\n\n const compartments = useMemo(\n () => ({ lang: new Compartment(), size: new Compartment(), ro: new Compartment(), wrap: new Compartment(), ph: new Compartment(), gutter: new Compartment(), aria: new Compartment(), tab: new Compartment() }),\n [],\n );\n\n function run(view, all = false) {\n const fn = live.current.onRun;\n if (!fn) return false;\n const text = view.state.doc.toString();\n const sel = view.state.selection.main;\n let range;\n if (all) range = { from: 0, to: text.length };\n else if (!sel.empty) range = { from: sel.from, to: sel.to };\n else if (live.current.language === \"sql\") range = statementAt(text, sel.head, { backslash: live.current.dialect === \"mysql\" || live.current.dialect === \"mariadb\" }) ?? { from: 0, to: text.length };\n else range = { from: 0, to: text.length };\n const statement = text.slice(range.from, range.to);\n if (!statement.trim()) return true;\n view.dispatch({ effects: flashEffect.of(range) });\n setTimeout(() => {\n if (viewRef.current === view) view.dispatch({ effects: flashEffect.of(null) });\n }, 450);\n fn({ text, statement, from: range.from, to: range.to, selection: !sel.empty, all });\n return true;\n }\n\n function format(view) {\n if (live.current.language !== \"json\") return false;\n const text = view.state.doc.toString();\n const out = formatJson(text);\n if (out == null || out === text) return true;\n view.dispatch({ changes: { from: 0, to: text.length, insert: out }, selection: EditorSelection.cursor(Math.min(view.state.selection.main.head, out.length)) });\n return true;\n }\n\n // Create the view once; props below reconfigure it.\n useEffect(() => {\n const source = async (view) => {\n const text = view.state.doc.toString();\n const { language: lang, validate: check, lint: fn } = live.current;\n const found = [];\n if (lang === \"json\" && check && text.trim()) {\n const r = checkJson(text);\n if (!r.ok) found.push({ from: r.error.from, to: r.error.to, message: r.error.message, severity: \"error\" });\n }\n if (fn) {\n try {\n found.push(...((await fn(text)) ?? []));\n } catch {\n /* a lint that throws marks nothing */\n }\n }\n if (outside.current.text === text) found.push(...outside.current.list);\n return normalizeDiagnostics(text, found);\n };\n\n const state = EditorState.create({\n doc: String(value ?? defaultValue ?? \"\"),\n extensions: [\n compartments.gutter.of(numbers ? [lineNumbers(), highlightActiveLineGutter()] : []),\n lintGutter(),\n highlightSpecialChars(),\n history(),\n drawSelection(),\n dropCursor(),\n EditorState.allowMultipleSelections.of(true),\n indentOnInput(),\n syntaxHighlighting(highlightStyle),\n bracketMatching(),\n closeBrackets(),\n autocompletion({ icons: true, activateOnTypingDelay: 80 }),\n rectangularSelection(),\n crosshairCursor(),\n highlightSelectionMatches(),\n flashField,\n linter(source, { delay: 300, needsRefresh: wantsRelint }),\n Prec.highest(\n keymap.of([\n { key: \"Mod-Enter\", run: (v) => run(v, false) },\n { key: \"Shift-Mod-Enter\", run: (v) => run(v, true) },\n {\n key: \"Mod-s\",\n preventDefault: true,\n run: (v) => {\n const fn = live.current.onSave;\n if (!fn) return false;\n fn(v.state.doc.toString());\n return true;\n },\n },\n { key: \"Shift-Alt-f\", run: (v) => format(v) },\n ]),\n ),\n keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, ...lintKeymap, indentWithTab]),\n editorTheme,\n compartments.lang.of(languageExtension({ language, dialect, schema, defaultSchema, completions })),\n compartments.size.of(sizeExtension({ height, minHeight, maxHeight })),\n compartments.ro.of(EditorState.readOnly.of(readOnly)),\n compartments.wrap.of(wrap ? EditorView.lineWrapping : []),\n compartments.ph.of(placeholder ? placeholderExt(placeholder) : []),\n compartments.aria.of(EditorView.contentAttributes.of({ \"aria-label\": label || LANGUAGES[language] || \"Code\", ...(readOnly ? { \"aria-readonly\": \"true\" } : {}) })),\n compartments.tab.of([EditorState.tabSize.of(tabSize), indentUnit.of(\" \".repeat(tabSize))]),\n EditorView.updateListener.of((u) => {\n if (u.docChanged && !applying.current) live.current.onChange?.(u.state.doc.toString());\n const head = u.state.selection.main.head;\n const line = u.state.doc.lineAt(head);\n const selected = u.state.selection.ranges.reduce((n, r) => n + (r.to - r.from), 0);\n const problems = diagnosticCount(u.state);\n setStatus((s) =>\n s.line === line.number && s.col === head - line.from + 1 && s.selected === selected && s.problems === problems\n ? s\n : { line: line.number, col: head - line.from + 1, selected, problems },\n );\n }),\n ],\n });\n const view = new EditorView({ state, parent: host.current });\n viewRef.current = view;\n if (autoFocus) view.focus();\n return () => {\n viewRef.current = null;\n view.destroy();\n };\n // Created once: every prop it reads is reconfigured below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const reconfigure = (compartment, ext) => viewRef.current?.dispatch({ effects: compartment.reconfigure(ext) });\n const schemaKey = JSON.stringify(schema ?? null) + JSON.stringify(completions ?? null);\n useEffect(() => {\n reconfigure(compartments.lang, languageExtension({ language, dialect, schema, defaultSchema, completions }));\n lintNow(viewRef.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [language, dialect, defaultSchema, schemaKey, validate]);\n useEffect(() => reconfigure(compartments.size, sizeExtension({ height, minHeight, maxHeight })), [height, minHeight, maxHeight, compartments]);\n useEffect(() => reconfigure(compartments.ro, EditorState.readOnly.of(readOnly)), [readOnly, compartments]);\n useEffect(() => reconfigure(compartments.wrap, wrap ? EditorView.lineWrapping : []), [wrap, compartments]);\n useEffect(() => reconfigure(compartments.ph, placeholder ? placeholderExt(placeholder) : []), [placeholder, compartments]);\n useEffect(() => reconfigure(compartments.gutter, numbers ? [lineNumbers(), highlightActiveLineGutter()] : []), [numbers, compartments]);\n useEffect(\n () => reconfigure(compartments.aria, EditorView.contentAttributes.of({ \"aria-label\": label || LANGUAGES[language] || \"Code\", ...(readOnly ? { \"aria-readonly\": \"true\" } : {}) })),\n [label, language, readOnly, compartments],\n );\n useEffect(() => reconfigure(compartments.tab, [EditorState.tabSize.of(tabSize), indentUnit.of(\" \".repeat(tabSize))]), [tabSize, compartments]);\n\n // Problems from outside: pinned to the text as it is now.\n const diagKey = JSON.stringify(diagnostics ?? null);\n useEffect(() => {\n const view = viewRef.current;\n if (!view) return;\n outside.current = { text: view.state.doc.toString(), list: diagnostics ?? [] };\n lintNow(view);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [diagKey]);\n // A lint function swapped in: check again.\n useEffect(() => lintNow(viewRef.current), [lint]);\n\n // Controlled: a `value` that is not the text replaces it (loading another\n // record); the one the editor just sent through onChange changes nothing.\n useEffect(() => {\n const view = viewRef.current;\n if (!view || value === undefined) return;\n const next = String(value ?? \"\");\n const now = view.state.doc.toString();\n if (next === now) return;\n applying.current = true;\n try {\n view.dispatch({ changes: { from: 0, to: now.length, insert: next }, selection: EditorSelection.cursor(Math.min(view.state.selection.main.head, next.length)) });\n } finally {\n applying.current = false;\n }\n }, [value]);\n\n // The handle a block reaches through editorRef / useCodeEditor().\n const handle = useMemo(\n () => ({\n get view() {\n return viewRef.current;\n },\n focus: () => viewRef.current?.focus(),\n getValue: () => viewRef.current?.state.doc.toString() ?? \"\",\n setValue: (text) => {\n const v = viewRef.current;\n if (!v) return;\n v.dispatch({ changes: { from: 0, to: v.state.doc.length, insert: String(text ?? \"\") } });\n },\n getSelection: () => {\n const v = viewRef.current;\n if (!v) return { from: 0, to: 0, text: \"\" };\n const { from, to } = v.state.selection.main;\n return { from, to, text: v.state.sliceDoc(from, to) };\n },\n replaceSelection: (text) => viewRef.current?.dispatch(viewRef.current.state.replaceSelection(String(text ?? \"\"))),\n insert: (text) => {\n const v = viewRef.current;\n if (!v) return;\n v.dispatch(v.state.replaceSelection(String(text ?? \"\")));\n v.focus();\n },\n select: (from, to = from) => {\n const v = viewRef.current;\n if (!v) return;\n const len = v.state.doc.length;\n v.dispatch({ selection: EditorSelection.range(Math.min(from, len), Math.min(to, len)), scrollIntoView: true });\n v.focus();\n },\n goTo: (line, col = 1) => {\n const v = viewRef.current;\n if (!v) return;\n const at = offsetOf(v.state.doc.toString(), line, col);\n v.dispatch({ selection: EditorSelection.cursor(at), scrollIntoView: true });\n v.focus();\n },\n statementAtCursor: () => {\n const v = viewRef.current;\n if (!v) return null;\n return statementAt(v.state.doc.toString(), v.state.selection.main.head);\n },\n run: (all = false) => (viewRef.current ? run(viewRef.current, all) : false),\n format: () => (viewRef.current ? format(viewRef.current) : false),\n cursor: () => {\n const v = viewRef.current;\n return v ? lineCol(v.state.doc.toString(), v.state.selection.main.head) : { line: 1, col: 1 };\n },\n }),\n // run/format read live props through refs.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n useEffect(() => {\n if (!editorRef) return;\n editorRef.current = handle;\n return () => {\n if (editorRef.current === handle) editorRef.current = null;\n };\n }, [editorRef, handle]);\n\n return (\n <div className={cx(FRAME, FLOATING, frameless && \"rounded-none border-0 bg-transparent focus-within:ring-0\", className)}>\n {/* raw: CodeMirror's mount point — it draws its own accessible textbox inside. */}\n <div ref={host} className=\"min-h-0 min-w-0 flex-1\" />\n {footer ? (\n <StatusBar\n language={language}\n line={status.line}\n col={status.col}\n selected={status.selected}\n problems={status.problems}\n onProblems={() => {\n const v = viewRef.current;\n if (v) {\n v.focus();\n nextDiagnostic(v);\n }\n }}\n onRun={onRun ? () => viewRef.current && run(viewRef.current) : null}\n runLabel={runLabel}\n readOnly={readOnly}\n />\n ) : null}\n </div>\n );\n}\n\n/** The quiet line under the editor: problems, where the cursor is, the\n * mode, and — when the editor runs — the Run button with its keys. */\nfunction StatusBar({ language, line, col, selected = 0, problems = 0, onProblems, onRun, runLabel = \"Run\", readOnly, idle = false }) {\n const [mac, setMac] = useState(true);\n useEffect(() => {\n setMac(/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || \"\"));\n }, []);\n return (\n <div className=\"flex min-h-9 items-center gap-3 border-t border-border ps-3 pe-1 text-xs text-muted-foreground\">\n {problems > 0 ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onProblems} disabled={idle} className=\"-ms-2 gap-1.5 px-2 text-xs text-foreground\">\n <span aria-hidden=\"true\" className=\"size-1.5 rounded-full bg-destructive\" />\n {problems === 1 ? \"1 problem\" : `${problems} problems`}\n </Button>\n ) : null}\n <span className=\"tabular-nums whitespace-nowrap\" aria-live=\"off\">\n Ln {line}, Col {col}\n {selected > 0 ? <span className=\"max-sm:hidden\"> · {selected} selected</span> : null}\n </span>\n <span className=\"ms-auto whitespace-nowrap max-sm:hidden\">\n {LANGUAGES[language] ?? language}\n {readOnly ? \" · Read only\" : \"\"}\n </span>\n {onRun ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onRun} disabled={idle} className=\"gap-2 px-2 text-xs text-foreground max-sm:ms-auto\">\n <Icon name=\"play\" size={14} />\n {runLabel}\n <KbdGroup keys={[mac ? \"⌘\" : \"Ctrl\", \"↵\"]} className=\"max-sm:hidden\" />\n </Button>\n ) : (\n <span className=\"w-1\" aria-hidden=\"true\" />\n )}\n </div>\n );\n}\n"
33
+ "content": "\"use client\";\n\n// CodeEditor — CodeMirror 6, in the workspace theme, behind one set of\n// props. SQL (with the database's tables for autocomplete), JSON (checked\n// as you type) or plain text; line numbers, problems drawn on the text,\n// find and replace, and a run hook on Cmd/Ctrl+Enter.\n//\n// Client only, one bundle. CodeMirror needs the DOM, and the app's\n// check refuses import() (so no next/dynamic): the modules load with the\n// page, and the editor itself is created only after the first effect (the\n// mount guard). Until then — on the server and in the first client paint —\n// the same box shows the text with its line numbers, so nothing jumps.\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button, Icon, KbdGroup, cx } from \"../../ui\";\nimport {\n Compartment,\n Decoration,\n EditorSelection,\n EditorState,\n EditorView,\n MSSQL,\n MariaSQL,\n MySQL,\n PostgreSQL,\n Prec,\n SQLite,\n StandardSQL,\n StateEffect,\n StateField,\n autocompletion,\n bracketMatching,\n closeBrackets,\n closeBracketsKeymap,\n completeFromList,\n completionKeymap,\n crosshairCursor,\n defaultKeymap,\n diagnosticCount,\n drawSelection,\n dropCursor,\n foldGutter,\n foldKeymap,\n forceLinting,\n highlightActiveLineGutter,\n highlightSelectionMatches,\n highlightSpecialChars,\n history,\n historyKeymap,\n indentOnInput,\n indentUnit,\n indentWithTab,\n json,\n keymap,\n lineNumbers,\n lintGutter,\n lintKeymap,\n linter,\n nextDiagnostic,\n placeholder as placeholderExt,\n rectangularSelection,\n searchKeymap,\n sql,\n syntaxHighlighting,\n} from \"./codemirror\";\nimport { editorTheme, highlightStyle } from \"./theme\";\nimport { lineCol, normalizeDiagnostics, offsetOf } from \"./text\";\nimport { sqlNamespace, statementAt } from \"./sql\";\nimport { checkJson, formatJson } from \"./json\";\n\n/** The modes, and the name the footer shows for each. */\nexport const LANGUAGES = { sql: \"SQL\", json: \"JSON\", text: \"Plain text\" };\n\n/** SQL dialects: keywords, quoting and functions differ between them. */\nexport const SQL_DIALECTS = { postgres: PostgreSQL, mysql: MySQL, mariadb: MariaSQL, sqlite: SQLite, mssql: MSSQL, standard: StandardSQL };\n\n/** A ref for the editor's handle: `const ed = useCodeEditor()`, then\n * `<CodeEditor editorRef={ed} />` and `ed.current?.insert(\"now()\")`. */\nexport function useCodeEditor() {\n return useRef(null);\n}\n\nconst px = (v) => (typeof v === \"number\" ? `${v}px` : v);\n\n// The frame, the same for the placeholder and the live editor. Phones get\n// 16px text: iOS zooms into anything smaller when it takes focus.\nconst FRAME =\n \"relative flex min-w-0 flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground \" +\n \"[--code-editor-size:16px] sm:[--code-editor-size:13px] transition-[border-color,box-shadow] duration-150 motion-reduce:transition-none \" +\n \"focus-within:border-ring/60 focus-within:ring-2 focus-within:ring-ring/15\";\n// CodeMirror's floating panels live inside the editor; they get the kit's shadow.\nconst FLOATING = \"[&_.cm-tooltip]:shadow-lg! [&_.cm-tooltip]:text-sm motion-reduce:[&_*]:transition-none!\";\n\nexport function CodeEditor(props) {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n if (!mounted) return <CodePlaceholder {...props} />;\n return <LiveEditor {...props} />;\n}\n\n// The placeholder's line number, as wide as CodeMirror's gutter (its lint\n// dot column is the 14px the text is pushed by, beside its own 12px).\nconst NUMBER = \"w-[54px] shrink-0 pe-1 text-right tabular-nums text-muted-foreground opacity-70 select-none\";\n\n/** What the server renders (and the first client paint): the text with\n * its line numbers, in the editor's box. Not editable, not focusable. */\nfunction CodePlaceholder({ value, defaultValue, placeholder, height, minHeight = 120, maxHeight = 480, lineNumbers: numbers = true, footer = true, language = \"text\", onRun, className, frameless = false }) {\n const text = String(value ?? defaultValue ?? \"\");\n const lines = text ? text.split(\"\\n\").slice(0, 400) : [];\n return (\n <div className={cx(FRAME, frameless && \"rounded-none border-0 bg-transparent focus-within:ring-0\", className)} aria-busy=\"true\">\n <div\n className=\"min-h-0 flex-1 overflow-hidden py-2.5 font-mono text-[length:var(--code-editor-size)] leading-[1.65]\"\n style={height != null ? { height: px(height) } : { minHeight: px(minHeight), maxHeight: px(maxHeight) }}\n >\n {lines.length ? (\n lines.map((line, i) => (\n <div key={i} className=\"flex\">\n {numbers ? <span className={NUMBER}>{i + 1}</span> : null}\n <span className=\"ps-[26px] pe-4 whitespace-pre\">{line || \" \"}</span>\n </div>\n ))\n ) : (\n <div className=\"flex\">\n {numbers ? <span className={NUMBER}>1</span> : null}\n <span className=\"ps-[26px] text-muted-foreground\">{placeholder || \" \"}</span>\n </div>\n )}\n </div>\n {footer ? <StatusBar language={language} line={1} col={1} onRun={onRun ? () => {} : null} idle /> : null}\n </div>\n );\n}\n\n// Check again now: a prop the linter reads changed (outside problems, the\n// mode, the lint function). The linter re-runs on edits by itself; for\n// anything else it needs this effect, then forceLinting to skip its delay.\nconst relint = StateEffect.define();\nconst wantsRelint = (u) => u.transactions.some((tr) => tr.effects.some((e) => e.is(relint)));\nfunction lintNow(view) {\n if (!view) return;\n view.dispatch({ effects: relint.of(null) });\n forceLinting(view);\n}\n\n// The statement that ran, lit for a moment.\nconst flashEffect = StateEffect.define();\nconst flashMark = Decoration.mark({ class: \"cm-ran\" });\nconst flashField = StateField.define({\n create: () => Decoration.none,\n update(deco, tr) {\n deco = deco.map(tr.changes);\n for (const e of tr.effects) {\n if (!e.is(flashEffect)) continue;\n deco = e.value && e.value.to > e.value.from ? Decoration.set([flashMark.range(e.value.from, e.value.to)]) : Decoration.none;\n }\n return deco;\n },\n provide: (f) => EditorView.decorations.from(f),\n});\n\nfunction markerDOM(open) {\n const el = document.createElement(\"span\");\n el.className = \"cm-fold-marker\";\n el.dataset.open = String(open);\n el.setAttribute(\"aria-hidden\", \"true\");\n el.textContent = \"⌄\";\n return el;\n}\n\nfunction languageExtension({ language, dialect, schema, defaultSchema, completions }) {\n const out = [];\n if (language === \"sql\") {\n const { namespace } = sqlNamespace(schema);\n out.push(sql({ dialect: SQL_DIALECTS[dialect] ?? PostgreSQL, schema: namespace, defaultSchema: defaultSchema || undefined, upperCaseKeywords: true }));\n } else if (language === \"json\") {\n out.push(json(), foldGutter({ markerDOM }));\n }\n if (completions?.length) {\n const list = completions.map((c) => (typeof c === \"string\" ? { label: c, type: \"variable\" } : c));\n out.push(EditorState.languageData.of(() => [{ autocomplete: completeFromList(list) }]));\n }\n return out;\n}\n\nfunction sizeExtension({ height, minHeight = 120, maxHeight = 480 }) {\n if (height != null) return EditorView.theme({ \"&\": { height: px(height) }, \".cm-scroller\": { overflow: \"auto\" } });\n return EditorView.theme({\n \"&\": { maxHeight: px(maxHeight) },\n \".cm-scroller\": { overflow: \"auto\" },\n \".cm-content, .cm-gutter\": { minHeight: px(minHeight) },\n });\n}\n\nfunction LiveEditor({\n value,\n defaultValue,\n onChange,\n language = \"text\",\n dialect = \"postgres\",\n schema,\n defaultSchema,\n completions,\n onRun,\n onSave,\n runLabel = \"Run\",\n diagnostics,\n lint,\n validate = true,\n readOnly = false,\n lineNumbers: numbers = true,\n wrap = false,\n placeholder,\n height,\n minHeight = 120,\n maxHeight = 480,\n tabSize = 2,\n autoFocus = false,\n footer = true,\n frameless = false,\n label,\n editorRef,\n className,\n}) {\n const host = useRef(null);\n const viewRef = useRef(null);\n const applying = useRef(false);\n const [status, setStatus] = useState({ line: 1, col: 1, selected: 0, problems: 0 });\n\n // The latest props for the handlers CodeMirror holds on to.\n const live = useRef({});\n live.current = { onChange, onRun, onSave, lint, language, dialect, validate, diagnostics };\n\n // Problems handed in through `diagnostics` belong to the text they were\n // found in: they show until that text changes.\n const outside = useRef({ text: null, list: [] });\n\n const compartments = useMemo(\n () => ({ lang: new Compartment(), size: new Compartment(), ro: new Compartment(), wrap: new Compartment(), ph: new Compartment(), gutter: new Compartment(), aria: new Compartment(), tab: new Compartment() }),\n [],\n );\n\n function run(view, all = false) {\n const fn = live.current.onRun;\n if (!fn) return false;\n const text = view.state.doc.toString();\n const sel = view.state.selection.main;\n let range;\n if (all) range = { from: 0, to: text.length };\n else if (!sel.empty) range = { from: sel.from, to: sel.to };\n else if (live.current.language === \"sql\") range = statementAt(text, sel.head, { backslash: live.current.dialect === \"mysql\" || live.current.dialect === \"mariadb\" }) ?? { from: 0, to: text.length };\n else range = { from: 0, to: text.length };\n const statement = text.slice(range.from, range.to);\n if (!statement.trim()) return true;\n view.dispatch({ effects: flashEffect.of(range) });\n setTimeout(() => {\n if (viewRef.current === view) view.dispatch({ effects: flashEffect.of(null) });\n }, 450);\n fn({ text, statement, from: range.from, to: range.to, selection: !sel.empty, all });\n return true;\n }\n\n function format(view) {\n if (live.current.language !== \"json\") return false;\n const text = view.state.doc.toString();\n const out = formatJson(text);\n if (out == null || out === text) return true;\n view.dispatch({ changes: { from: 0, to: text.length, insert: out }, selection: EditorSelection.cursor(Math.min(view.state.selection.main.head, out.length)) });\n return true;\n }\n\n // Create the view once; props below reconfigure it.\n useEffect(() => {\n const source = async (view) => {\n const text = view.state.doc.toString();\n const { language: lang, validate: check, lint: fn } = live.current;\n const found = [];\n if (lang === \"json\" && check && text.trim()) {\n const r = checkJson(text);\n if (!r.ok) found.push({ from: r.error.from, to: r.error.to, message: r.error.message, severity: \"error\" });\n }\n if (fn) {\n try {\n found.push(...((await fn(text)) ?? []));\n } catch {\n /* a lint that throws marks nothing */\n }\n }\n if (outside.current.text === text) found.push(...outside.current.list);\n return normalizeDiagnostics(text, found);\n };\n\n const state = EditorState.create({\n doc: String(value ?? defaultValue ?? \"\"),\n extensions: [\n compartments.gutter.of(numbers ? [lineNumbers(), highlightActiveLineGutter()] : []),\n lintGutter(),\n highlightSpecialChars(),\n history(),\n drawSelection(),\n dropCursor(),\n EditorState.allowMultipleSelections.of(true),\n indentOnInput(),\n syntaxHighlighting(highlightStyle),\n bracketMatching(),\n closeBrackets(),\n autocompletion({ icons: true, activateOnTypingDelay: 80 }),\n rectangularSelection(),\n crosshairCursor(),\n highlightSelectionMatches(),\n flashField,\n linter(source, { delay: 300, needsRefresh: wantsRelint }),\n Prec.highest(\n keymap.of([\n { key: \"Mod-Enter\", run: (v) => run(v, false) },\n { key: \"Shift-Mod-Enter\", run: (v) => run(v, true) },\n {\n key: \"Mod-s\",\n preventDefault: true,\n run: (v) => {\n const fn = live.current.onSave;\n if (!fn) return false;\n fn(v.state.doc.toString());\n return true;\n },\n },\n { key: \"Shift-Alt-f\", run: (v) => format(v) },\n ]),\n ),\n keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, ...lintKeymap, indentWithTab]),\n editorTheme,\n compartments.lang.of(languageExtension({ language, dialect, schema, defaultSchema, completions })),\n compartments.size.of(sizeExtension({ height, minHeight, maxHeight })),\n compartments.ro.of(EditorState.readOnly.of(readOnly)),\n compartments.wrap.of(wrap ? EditorView.lineWrapping : []),\n compartments.ph.of(placeholder ? placeholderExt(placeholder) : []),\n compartments.aria.of(EditorView.contentAttributes.of({ \"aria-label\": label || LANGUAGES[language] || \"Code\", ...(readOnly ? { \"aria-readonly\": \"true\" } : {}) })),\n compartments.tab.of([EditorState.tabSize.of(tabSize), indentUnit.of(\" \".repeat(tabSize))]),\n EditorView.updateListener.of((u) => {\n if (u.docChanged && !applying.current) live.current.onChange?.(u.state.doc.toString());\n const head = u.state.selection.main.head;\n const line = u.state.doc.lineAt(head);\n const selected = u.state.selection.ranges.reduce((n, r) => n + (r.to - r.from), 0);\n const problems = diagnosticCount(u.state);\n setStatus((s) =>\n s.line === line.number && s.col === head - line.from + 1 && s.selected === selected && s.problems === problems\n ? s\n : { line: line.number, col: head - line.from + 1, selected, problems },\n );\n }),\n ],\n });\n const view = new EditorView({ state, parent: host.current });\n viewRef.current = view;\n if (autoFocus) view.focus();\n return () => {\n viewRef.current = null;\n view.destroy();\n };\n // Created once: every prop it reads is reconfigured below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const reconfigure = (compartment, ext) => viewRef.current?.dispatch({ effects: compartment.reconfigure(ext) });\n const schemaKey = JSON.stringify(schema ?? null) + JSON.stringify(completions ?? null);\n useEffect(() => {\n reconfigure(compartments.lang, languageExtension({ language, dialect, schema, defaultSchema, completions }));\n lintNow(viewRef.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [language, dialect, defaultSchema, schemaKey, validate]);\n useEffect(() => reconfigure(compartments.size, sizeExtension({ height, minHeight, maxHeight })), [height, minHeight, maxHeight, compartments]);\n useEffect(() => reconfigure(compartments.ro, EditorState.readOnly.of(readOnly)), [readOnly, compartments]);\n useEffect(() => reconfigure(compartments.wrap, wrap ? EditorView.lineWrapping : []), [wrap, compartments]);\n useEffect(() => reconfigure(compartments.ph, placeholder ? placeholderExt(placeholder) : []), [placeholder, compartments]);\n useEffect(() => reconfigure(compartments.gutter, numbers ? [lineNumbers(), highlightActiveLineGutter()] : []), [numbers, compartments]);\n useEffect(\n () => reconfigure(compartments.aria, EditorView.contentAttributes.of({ \"aria-label\": label || LANGUAGES[language] || \"Code\", ...(readOnly ? { \"aria-readonly\": \"true\" } : {}) })),\n [label, language, readOnly, compartments],\n );\n useEffect(() => reconfigure(compartments.tab, [EditorState.tabSize.of(tabSize), indentUnit.of(\" \".repeat(tabSize))]), [tabSize, compartments]);\n\n // Problems from outside: pinned to the text as it is now.\n const diagKey = JSON.stringify(diagnostics ?? null);\n useEffect(() => {\n const view = viewRef.current;\n if (!view) return;\n outside.current = { text: view.state.doc.toString(), list: diagnostics ?? [] };\n lintNow(view);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [diagKey]);\n // A lint function swapped in: check again.\n useEffect(() => lintNow(viewRef.current), [lint]);\n\n // Controlled: a `value` that is not the text replaces it (loading another\n // record); the one the editor just sent through onChange changes nothing.\n useEffect(() => {\n const view = viewRef.current;\n if (!view || value === undefined) return;\n const next = String(value ?? \"\");\n const now = view.state.doc.toString();\n if (next === now) return;\n applying.current = true;\n try {\n view.dispatch({ changes: { from: 0, to: now.length, insert: next }, selection: EditorSelection.cursor(Math.min(view.state.selection.main.head, next.length)) });\n } finally {\n applying.current = false;\n }\n }, [value]);\n\n // The handle a block reaches through editorRef / useCodeEditor().\n const handle = useMemo(\n () => ({\n get view() {\n return viewRef.current;\n },\n focus: () => viewRef.current?.focus(),\n getValue: () => viewRef.current?.state.doc.toString() ?? \"\",\n setValue: (text) => {\n const v = viewRef.current;\n if (!v) return;\n v.dispatch({ changes: { from: 0, to: v.state.doc.length, insert: String(text ?? \"\") } });\n },\n getSelection: () => {\n const v = viewRef.current;\n if (!v) return { from: 0, to: 0, text: \"\" };\n const { from, to } = v.state.selection.main;\n return { from, to, text: v.state.sliceDoc(from, to) };\n },\n replaceSelection: (text) => viewRef.current?.dispatch(viewRef.current.state.replaceSelection(String(text ?? \"\"))),\n insert: (text) => {\n const v = viewRef.current;\n if (!v) return;\n v.dispatch(v.state.replaceSelection(String(text ?? \"\")));\n v.focus();\n },\n select: (from, to = from) => {\n const v = viewRef.current;\n if (!v) return;\n const len = v.state.doc.length;\n v.dispatch({ selection: EditorSelection.range(Math.min(from, len), Math.min(to, len)), scrollIntoView: true });\n v.focus();\n },\n goTo: (line, col = 1) => {\n const v = viewRef.current;\n if (!v) return;\n const at = offsetOf(v.state.doc.toString(), line, col);\n v.dispatch({ selection: EditorSelection.cursor(at), scrollIntoView: true });\n v.focus();\n },\n statementAtCursor: () => {\n const v = viewRef.current;\n if (!v) return null;\n return statementAt(v.state.doc.toString(), v.state.selection.main.head);\n },\n run: (all = false) => (viewRef.current ? run(viewRef.current, all) : false),\n format: () => (viewRef.current ? format(viewRef.current) : false),\n cursor: () => {\n const v = viewRef.current;\n return v ? lineCol(v.state.doc.toString(), v.state.selection.main.head) : { line: 1, col: 1 };\n },\n }),\n // run/format read live props through refs.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n useEffect(() => {\n if (!editorRef) return;\n editorRef.current = handle;\n return () => {\n if (editorRef.current === handle) editorRef.current = null;\n };\n }, [editorRef, handle]);\n\n return (\n <div className={cx(FRAME, FLOATING, frameless && \"rounded-none border-0 bg-transparent focus-within:ring-0\", className)}>\n {/* raw: CodeMirror's mount point — it draws its own accessible textbox inside. */}\n <div ref={host} className=\"min-h-0 min-w-0 flex-1\" />\n {footer ? (\n <StatusBar\n language={language}\n line={status.line}\n col={status.col}\n selected={status.selected}\n problems={status.problems}\n onProblems={() => {\n const v = viewRef.current;\n if (v) {\n v.focus();\n nextDiagnostic(v);\n }\n }}\n onRun={onRun ? () => viewRef.current && run(viewRef.current) : null}\n runLabel={runLabel}\n readOnly={readOnly}\n />\n ) : null}\n </div>\n );\n}\n\n/** The quiet line under the editor: problems, where the cursor is, the\n * mode, and — when the editor runs — the Run button with its keys. */\nfunction StatusBar({ language, line, col, selected = 0, problems = 0, onProblems, onRun, runLabel = \"Run\", readOnly, idle = false }) {\n const [mac, setMac] = useState(true);\n useEffect(() => {\n setMac(/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || \"\"));\n }, []);\n return (\n <div className=\"flex min-h-9 items-center gap-3 border-t border-border ps-3 pe-1 text-xs text-muted-foreground\">\n {problems > 0 ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onProblems} disabled={idle} className=\"-ms-2 gap-1.5 px-2 text-xs text-foreground\">\n <span aria-hidden=\"true\" className=\"size-1.5 rounded-full bg-destructive\" />\n {problems === 1 ? \"1 problem\" : `${problems} problems`}\n </Button>\n ) : null}\n <span className=\"tabular-nums whitespace-nowrap\" aria-live=\"off\">\n Ln {line}, Col {col}\n {selected > 0 ? <span className=\"max-sm:hidden\"> · {selected} selected</span> : null}\n </span>\n <span className=\"ms-auto whitespace-nowrap max-sm:hidden\">\n {LANGUAGES[language] ?? language}\n {readOnly ? \" · Read only\" : \"\"}\n </span>\n {onRun ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onRun} disabled={idle} className=\"gap-2 px-2 text-xs text-foreground max-sm:ms-auto\">\n <Icon name=\"play\" size={14} />\n {runLabel}\n <KbdGroup keys={[mac ? \"⌘\" : \"Ctrl\", \"↵\"]} className=\"max-sm:hidden\" />\n </Button>\n ) : (\n <span className=\"w-1\" aria-hidden=\"true\" />\n )}\n </div>\n );\n}\n"
34
34
  },
35
35
  {
36
36
  "path": "registry/blocks/code-editor/codemirror.js",
@@ -66,7 +66,7 @@
66
66
  "path": "registry/blocks/code-editor/README.md",
67
67
  "type": "registry:file",
68
68
  "target": "@components/makersclaw/blocks/code-editor/README.md",
69
- "content": "# CodeEditor\n\nA code editor in the workspace theme. Three modes: **SQL** (keywords,\nfunctions, and your database's tables and columns in autocomplete),\n**JSON** (checked as you type; mistakes are underlined and said in plain\nwords — \"A comma is missing before this\", \"Keys need double quotes\") and\n**plain text**. Line numbers, find and replace (Cmd/Ctrl+F), folding in\nJSON, multiple cursors, and a run hook: Cmd/Ctrl+Enter runs the selection,\nor the SQL statement under the cursor. A quiet status line under the text\nshows problems (click to jump to the next one), the cursor's line and\ncolumn, the mode and — when there is a run hook — a Run button, which is\nhow a phone runs.\n\nIt is [CodeMirror 6](https://codemirror.net) (MIT) underneath. Every\ncolour is a workspace token read by the browser (`var(--color-…)`), so a\nchange of theme or of light/dark repaints it with no work. Syntax colours\ncome from the chart hues: keywords `chart-1` (the primary), text in\nquotes `chart-2`, numbers and true/false/null `chart-3`, functions\n`chart-4`, columns, keys and types `chart-5`; comments step back into\n`muted-foreground`. `json-editor` and `sql-console` are built on this\nblock: they import it, never `@codemirror/*`.\n\n```jsx\nimport { CodeEditor, useCodeEditor } from \"../components/blocks/code-editor\"; // a Makersclaw app (\"./blocks/code-editor\" from components/)\nimport { CodeEditor, useCodeEditor } from \"@/components/makersclaw/blocks/code-editor\"; // a shadcn project\n```\n\n`CodeEditor` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`splitStatements`, `statementAt`, `checkJson`,\n`formatJson`…) are plain functions and work on the server too.\n\n`makersclaw add code-editor` adds the CodeMirror packages\n(`@codemirror/state`, `view`, `commands`, `language`, `autocomplete`,\n`search`, `lint`, `lang-sql`, `lang-json`, and `@lezer/highlight` — pinned,\nexact) to package.json: run `npm install` after. They load with the page\n(no `import()`): the editor is created after the first render, and until\nthen — on the server — the box shows the text with its line numbers.\n\n## Props\n\n- `value` — the text, controlled. A new `value` that is not the editor's\n text replaces it (loading another query); pass back exactly what\n `onChange` gave you and nothing resets, the cursor included.\n- `defaultValue` — the starting text, uncontrolled. With neither, empty.\n- `onChange(text)` — every edit, with the whole text. Debounce a save\n yourself.\n- `language` — `\"text\"` (default) · `\"sql\"` · `\"json\"`.\n- `dialect` — SQL only: `\"postgres\"` (default) · `\"mysql\"` · `\"mariadb\"` ·\n `\"sqlite\"` · `\"mssql\"` · `\"standard\"`. Sets keywords, functions and\n quoting (and whether `\\'` escapes a quote when finding the statement to\n run: MySQL and MariaDB only).\n- `schema` — SQL only: the tables autocomplete offers.\n `[{ name, schema?, description?, columns: [{ name, type?, description?,\n primaryKey? } | \"name\"] }]`. A table completes with its column count\n beside it and its description in a side note; `deals.` offers its\n columns with their types (`uuid · key`); in a query that names a table,\n its columns complete bare. Tables with a `schema` nest under it\n (`auth.users`); `defaultSchema` (e.g. `\"public\"`) lets that schema's\n tables complete without the prefix. Change it and autocomplete follows.\n- `completions` — extra words to complete in any mode: strings, or\n `{ label, type?, detail?, info?, apply? }` (`type` picks the little icon:\n `\"variable\"`, `\"function\"`, `\"keyword\"`, `\"property\"`, `\"type\"`).\n Saved snippets, `{{variables}}`, function names.\n- `onRun({ text, statement, from, to, selection, all })` — Cmd/Ctrl+Enter\n and the Run button. `statement` is what to run: the selection when there\n is one; else, in SQL, the statement under the cursor (split on `;` —\n quotes, comments and `$$` bodies respected; a cursor just after a `;`\n runs the statement before it); else the whole text. Shift+Cmd/Ctrl+Enter\n runs everything (`all: true`). `from`/`to` are its offsets in `text`, so\n a caller can map an error position back. The range lights up for a\n moment. Without `onRun` there is no Run button and the keys do nothing.\n- `runLabel` — the button's word, default `\"Run\"` (`\"Run query\"`,\n `\"Send\"`).\n- `onSave(text)` — Cmd/Ctrl+S (the browser's save dialog is held back).\n Without it the keys are the browser's.\n- `diagnostics` — problems found outside the editor (a database error, a\n server-side check), drawn on the text: `[{ message, severity?, …where }]`\n where \"where\" is `{ from, to }` offsets, `{ line, col?, endLine?,\n endCol? }` (1-based; no `col` underlines the whole line) or\n `{ position }` (a 1-based character position, as Postgres reports\n `error.position`). `severity` is `\"error\"` (default), `\"warning\"` or\n `\"info\"`. They belong to the text they were found in: **they show until\n the text changes**, then clear themselves. Pass `[]` to clear sooner.\n- `lint(text) → problems` — a check of your own, run 300ms after typing\n stops; same shape as `diagnostics`; may be async.\n- `validate` — JSON only: `true` (default) checks the text as you type.\n- `readOnly` — `false` (default). `true` shows the text read-only: it can\n still be selected, searched and copied.\n- `lineNumbers` — `true` (default).\n- `wrap` — `false` (default): long lines scroll sideways. `true` wraps.\n- `placeholder` — shown while the text is empty: say what goes here\n (\"Write a query — try SELECT * FROM deals\").\n- `height` — a fixed height (px number or CSS length); the text scrolls\n inside. Without it the editor grows with its text from `minHeight`\n (120) to `maxHeight` (480), then scrolls.\n- `tabSize` — `2` (default). Tab indents; **Esc then Tab** leaves the\n editor, so it never traps the keyboard.\n- `autoFocus` — `false` (default).\n- `footer` — `true` (default) shows the status line. `false` hides it\n (a block that draws its own).\n- `frameless` — `false` (default). `true` drops the border and card fill,\n for an editor inside a panel that already has them.\n- `label` — what a screen reader calls the editor (default: the mode's\n name).\n- `editorRef` — a ref from `useCodeEditor()`; `.current` is the handle.\n- `className` — the frame's classes (a width, a margin, `h-full`).\n\n### The handle\n\n`const ed = useCodeEditor()`, `<CodeEditor editorRef={ed} />`, then:\n\n- `ed.current.getValue()` / `setValue(text)`\n- `getSelection()` → `{ from, to, text }` · `select(from, to?)` ·\n `replaceSelection(text)` · `insert(text)` (at the cursor, then focus)\n- `goTo(line, col?)` — move the cursor there, scroll, focus (a results\n panel's \"show me the error\")\n- `cursor()` → `{ line, col }`\n- `statementAtCursor()` → `{ from, to, text }` or null\n- `run(all?)` — as the keys; `format()` — JSON only, lays it out again\n (also Shift+Alt+F)\n- `focus()`; `view` — the CodeMirror view, for the rare thing no prop does\n\n### Keys\n\nCmd/Ctrl+Enter run · Shift+Cmd/Ctrl+Enter run all · Cmd/Ctrl+S save ·\nCmd/Ctrl+F find · Cmd/Ctrl+Z / Shift+Cmd/Ctrl+Z undo/redo ·\nCtrl+Space complete · Cmd/Ctrl+/ comment · Alt+drag a column ·\nShift+Alt+F format JSON · Esc, Tab leave the editor.\n\n## The helpers\n\nPlain functions, on the server too.\n\n- `splitStatements(sql, { backslash? })` → `[{ from, to, text }]` — the\n statements in a script, trimmed, without their `;`. Comment-only\n stretches are not statements.\n- `statementAt(sql, pos, { backslash? })` → the statement to run for a\n cursor at `pos`, or null.\n- `sqlNamespace(tables)` → `{ namespace, tables }` — the `schema` prop as\n the SQL mode reads it (you do not need it to use the editor).\n- `checkJson(text, { allowEmpty? })` → `{ ok: true, value }` or\n `{ ok: false, error: { message, from, to, line, col } }` — the same\n words in every browser.\n- `formatJson(text, { indent = 2, width = 80 })` → the text laid out\n again, keys, order and numbers exactly as written (`indent: 0`\n minifies); null when it is not valid JSON.\n- `stringifyJson(value)` → a value as 2-space JSON, short lists on one\n line.\n- `lineCol(text, offset)`, `offsetOf(text, line, col)`,\n `normalizeDiagnostics(text, problems)` — positions both ways.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { CodeEditor, useCodeEditor } from \"../components/blocks/code-editor\";\n\nconst TABLES = [\n { name: \"deals\", description: \"Every open and closed deal\", columns: [\n { name: \"id\", type: \"uuid\", primaryKey: true },\n { name: \"account_id\", type: \"uuid\" },\n { name: \"amount\", type: \"numeric\" },\n { name: \"stage\", type: \"text\", description: \"lead · qualified · proposal · won · lost\" },\n { name: \"closed_at\", type: \"timestamptz\" },\n ] },\n { name: \"accounts\", columns: [{ name: \"id\", type: \"uuid\", primaryKey: true }, { name: \"name\", type: \"text\" }] },\n];\n\nexport function QueryBox({ runQuery }) {\n const [sql, setSql] = useState(\"SELECT stage, sum(amount)\\nFROM deals\\nGROUP BY stage;\");\n const [problems, setProblems] = useState([]);\n const ed = useCodeEditor();\n\n async function run({ statement, from }) {\n const r = await runQuery(statement); // your server action: { ok, rows } or { ok: false, error, position }\n if (r.ok) setProblems([]);\n else setProblems([{ position: from + (r.position ?? 1), message: r.error }]);\n }\n\n return (\n <CodeEditor\n language=\"sql\"\n schema={TABLES}\n value={sql}\n onChange={setSql}\n onRun={run}\n diagnostics={problems}\n placeholder=\"Write a query — try SELECT * FROM deals\"\n editorRef={ed}\n />\n );\n}\n```\n\nJSON, read-only, fixed height:\n\n```jsx\n<CodeEditor language=\"json\" value={JSON.stringify(payload, null, 2)} readOnly height={240} />\n```\n\n## What it will not do\n\n- It is not an IDE: no language server, no go-to-definition, no type\n checking. SQL is checked by running it (hand the database's error back\n through `diagnostics`); JSON is checked here.\n- Modes are SQL, JSON and plain text. Another language is another mode\n in this block, not a package a screen imports.\n- It never runs anything: `onRun` hands you the statement.\n- No live collaboration (that needs a sync server).\n"
69
+ "content": "# CodeEditor\n\nA code editor in the workspace theme. Three modes: **SQL** (keywords,\nfunctions, and your database's tables and columns in autocomplete),\n**JSON** (checked as you type; mistakes are underlined and said in plain\nwords — \"A comma is missing before this\", \"Keys need double quotes\") and\n**plain text**. Line numbers, find and replace (Cmd/Ctrl+F), folding in\nJSON, multiple cursors, and a run hook: Cmd/Ctrl+Enter runs the selection,\nor the SQL statement under the cursor. A quiet status line under the text\nshows problems (click to jump to the next one), the cursor's line and\ncolumn, the mode and — when there is a run hook — a Run button, which is\nhow a phone runs.\n\nIt is [CodeMirror 6](https://codemirror.net) (MIT) underneath. Every\ncolour is a workspace token read by the browser (`var(--color-…)`), so a\nchange of theme or of light/dark repaints it with no work. Syntax colours\ncome from the chart hues: keywords `chart-1` (the primary), text in\nquotes `chart-2`, numbers and true/false/null `chart-3`, functions\n`chart-4`, columns, keys and types `chart-5`; comments step back into\n`muted-foreground`. `json-editor` and `sql-console` are built on this\nblock: they import it, never `@codemirror/*`.\n\n```jsx\nimport { CodeEditor, useCodeEditor } from \"../components/blocks/code-editor\"; // a Makersclaw app (\"./blocks/code-editor\" from components/)\nimport { CodeEditor, useCodeEditor } from \"@/components/makersclaw/blocks/code-editor\"; // a shadcn project\n```\n\n`CodeEditor` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`splitStatements`, `statementAt`, `checkJson`,\n`formatJson`…) are plain functions and work on the server too.\n\n`makersclaw add code-editor` adds the CodeMirror packages\n(`@codemirror/state`, `view`, `commands`, `language`, `autocomplete`,\n`search`, `lint`, `lang-sql`, `lang-json`, and `@lezer/highlight` — pinned,\nexact) to package.json: run `npm install` after. They load with the page\n(no `import()`): the editor is created after the first render, and until\nthen — on the server — the box shows the text with its line numbers.\n\n## Props\n\n- `value` — the text, controlled. A new `value` that is not the editor's\n text replaces it (loading another query); pass back exactly what\n `onChange` gave you and nothing resets, the cursor included.\n- `defaultValue` — the starting text, uncontrolled. With neither, empty.\n- `onChange(text)` — every edit, with the whole text. Debounce a save\n yourself.\n- `language` — `\"text\"` (default) · `\"sql\"` · `\"json\"`.\n- `dialect` — SQL only: `\"postgres\"` (default) · `\"mysql\"` · `\"mariadb\"` ·\n `\"sqlite\"` · `\"mssql\"` · `\"standard\"`. Sets keywords, functions and\n quoting (and whether `\\'` escapes a quote when finding the statement to\n run: MySQL and MariaDB only).\n- `schema` — SQL only: the tables autocomplete offers.\n `[{ name, schema?, description?, columns: [{ name, type?, description?,\n primaryKey? } | \"name\"] }]`. A table completes with its column count\n beside it and its description in a side note; `deals.` offers its\n columns with their types (`uuid · key`); in a query that names a table,\n its columns complete bare. Tables with a `schema` nest under it\n (`auth.users`); `defaultSchema` (e.g. `\"public\"`) lets that schema's\n tables complete without the prefix. Change it and autocomplete follows.\n- `completions` — extra words to complete in any mode: strings, or\n `{ label, type?, detail?, info?, apply? }` (`type` picks the little icon:\n `\"variable\"`, `\"function\"`, `\"keyword\"`, `\"property\"`, `\"type\"`).\n Saved snippets, `{{variables}}`, function names.\n- `onRun({ text, statement, from, to, selection, all })` — Cmd/Ctrl+Enter\n and the Run button. `statement` is what to run: the selection when there\n is one; else, in SQL, the statement under the cursor (split on `;` —\n quotes, comments and `$$` bodies respected; a cursor just after a `;`\n runs the statement before it); else the whole text. Shift+Cmd/Ctrl+Enter\n runs everything (`all: true`). `from`/`to` are its offsets in `text`, so\n a caller can map an error position back. The range lights up for a\n moment. Without `onRun` there is no Run button and the keys do nothing.\n- `runLabel` — the button's word, default `\"Run\"` (`\"Run query\"`,\n `\"Send\"`).\n- `onSave(text)` — Cmd/Ctrl+S (the browser's save dialog is held back).\n Without it the keys are the browser's.\n- `diagnostics` — problems found outside the editor (a database error, a\n server-side check), drawn on the text: `[{ message, severity?, …where }]`\n where \"where\" is `{ from, to }` offsets, `{ line, col?, endLine?,\n endCol? }` (1-based; no `col` underlines the whole line) or\n `{ position }` (a 1-based character position, as Postgres reports\n `error.position`). `severity` is `\"error\"` (default), `\"warning\"` or\n `\"info\"`. They belong to the text they were found in: **they show until\n the text changes**, then clear themselves. Pass `[]` to clear sooner.\n- `lint(text) → problems` — a check of your own, run 300ms after typing\n stops; same shape as `diagnostics`; may be async.\n- `validate` — JSON only: `true` (default) checks the text as you type.\n- `readOnly` — `false` (default). `true` shows the text read-only: it can\n still be selected, searched and copied.\n- `lineNumbers` — `true` (default).\n- `wrap` — `false` (default): long lines scroll sideways. `true` wraps.\n- `placeholder` — shown while the text is empty: say what goes here\n (\"Write a query — try SELECT * FROM deals\").\n- `height` — a fixed height (px number or CSS length); the text scrolls\n inside. Without it the editor grows with its text from `minHeight`\n (120) to `maxHeight` (480), then scrolls.\n- `tabSize` — `2` (default). Tab indents; **Esc then Tab** leaves the\n editor, so it never traps the keyboard.\n- `autoFocus` — `false` (default).\n- `footer` — `true` (default) shows the status line. `false` hides it\n (a block that draws its own).\n- `frameless` — `false` (default). `true` drops the border and card fill,\n for an editor inside a panel that already has them.\n- `label` — what a screen reader calls the editor (default: the mode's\n name).\n- `editorRef` — a ref from `useCodeEditor()`; `.current` is the handle.\n- `className` — the frame's classes (a width, a margin, `h-full`).\n\n### The handle\n\n`const ed = useCodeEditor()`, `<CodeEditor editorRef={ed} />`, then:\n\n- `ed.current.getValue()` / `setValue(text)`\n- `getSelection()` → `{ from, to, text }` · `select(from, to?)` ·\n `replaceSelection(text)` · `insert(text)` (at the cursor, then focus)\n- `goTo(line, col?)` — move the cursor there, scroll, focus (a results\n panel's \"show me the error\")\n- `cursor()` → `{ line, col }`\n- `statementAtCursor()` → `{ from, to, text }` or null\n- `run(all?)` — as the keys; `format()` — JSON only, lays it out again\n (also Shift+Alt+F)\n- `focus()`; `view` — the CodeMirror view, for the rare thing no prop does\n\n### Keys\n\nCmd/Ctrl+Enter run · Shift+Cmd/Ctrl+Enter run all · Cmd/Ctrl+S save ·\nCmd/Ctrl+F find · Cmd/Ctrl+Z / Shift+Cmd/Ctrl+Z undo/redo ·\nCtrl+Space complete · Cmd/Ctrl+/ comment · Alt+drag a column ·\nShift+Alt+F format JSON · Esc, Tab leave the editor.\n\n## The helpers\n\nPlain functions, on the server too.\n\n- `splitStatements(sql, { backslash? })` → `[{ from, to, text }]` — the\n statements in a script, trimmed, without their `;`. Comment-only\n stretches are not statements.\n- `statementAt(sql, pos, { backslash? })` → the statement to run for a\n cursor at `pos`, or null.\n- `sqlNamespace(tables)` → `{ namespace, tables }` — the `schema` prop as\n the SQL mode reads it (you do not need it to use the editor).\n- `checkJson(text, { allowEmpty? })` → `{ ok: true, value }` or\n `{ ok: false, error: { message, from, to, line, col } }` — the same\n words in every browser.\n- `formatJson(text, { indent = 2, width = 80 })` → the text laid out\n again, keys, order and numbers exactly as written (`indent: 0`\n minifies); null when it is not valid JSON.\n- `stringifyJson(value)` → a value as 2-space JSON, short lists on one\n line.\n- `lineCol(text, offset)`, `offsetOf(text, line, col)`,\n `normalizeDiagnostics(text, problems)` — positions both ways.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { CodeEditor, useCodeEditor } from \"../components/blocks/code-editor\";\n\nconst TABLES = [\n { name: \"deals\", description: \"Every open and closed deal\", columns: [\n { name: \"id\", type: \"uuid\", primaryKey: true },\n { name: \"account_id\", type: \"uuid\" },\n { name: \"amount\", type: \"numeric\" },\n { name: \"stage\", type: \"text\", description: \"lead · qualified · proposal · won · lost\" },\n { name: \"closed_at\", type: \"timestamptz\" },\n ] },\n { name: \"accounts\", columns: [{ name: \"id\", type: \"uuid\", primaryKey: true }, { name: \"name\", type: \"text\" }] },\n];\n\nexport function QueryBox({ runQuery }) {\n const [sql, setSql] = useState(\"SELECT stage, sum(amount)\\nFROM deals\\nGROUP BY stage;\");\n const [problems, setProblems] = useState([]);\n const ed = useCodeEditor();\n\n async function run({ statement, from }) {\n const r = await runQuery(statement); // your server action: { ok, rows } or { ok: false, error, position }\n if (r.ok) setProblems([]);\n else setProblems([{ position: from + (r.position ?? 1), message: r.error }]);\n }\n\n return (\n <CodeEditor\n language=\"sql\"\n schema={TABLES}\n value={sql}\n onChange={setSql}\n onRun={run}\n diagnostics={problems}\n placeholder=\"Write a query — try SELECT * FROM deals\"\n editorRef={ed}\n />\n );\n}\n```\n\nRunning SQL a member typed is a server app's only: `runQuery` is a\nServer Action of the app's own. A browser app has no such call, since no\nroute of the data door runs SQL a member typed.\n\nJSON, read-only, fixed height:\n\n```jsx\n<CodeEditor language=\"json\" value={JSON.stringify(payload, null, 2)} readOnly height={240} />\n```\n\n## What it will not do\n\n- It is not an IDE: no language server, no go-to-definition, no type\n checking. SQL is checked by running it (hand the database's error back\n through `diagnostics`); JSON is checked here.\n- Modes are SQL, JSON and plain text. Another language is another mode\n in this block, not a package a screen imports.\n- It never runs anything: `onRun` hands you the statement.\n- No live collaboration (that needs a sync server).\n"
70
70
  }
71
71
  ],
72
72
  "categories": [
@@ -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.6.0",
90
+ "version": "0.7.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
  }
@@ -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.6.0/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/doc-editor.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/doc-editor.json"
10
10
  ],
11
11
  "dependencies": [],
12
12
  "files": [
@@ -38,7 +38,7 @@
38
38
  "path": "registry/blocks/comment-box/README.md",
39
39
  "type": "registry:file",
40
40
  "target": "@components/makersclaw/blocks/comment-box/README.md",
41
- "content": "# CommentBox\n\nA comment field that takes rich text: select a word for bold, italic or a\nlink, start a line with `-` for a list, type `@` to mention a teammate\n(they go in as a chip). Cmd/Ctrl+Enter posts, as does the Post button.\nWhile a post is on its way the box is locked; when it fails the words\nstay and the box says why; when it lands the box clears. Give it\n`comments` and it draws the thread above: avatar, name, \"5 min ago\", the\nbody with its formatting and mentions.\n\nIt is `doc-editor` in its minimal mode (no `/` menu, no drag handles), so\na comment is BlockNote JSON — the same doc every text block uses.\n\n```jsx\nimport { CommentBox } from \"../components/blocks/comment-box\"; // a Makersclaw app (\"./blocks/comment-box\" from components/)\nimport { CommentBox } from \"@/components/makersclaw/blocks/comment-box\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the thread in the server surface (README §4). `makersclaw add\ncomment-box` adds `doc-editor` with it (and its npm packages: run\n`npm install` after).\n\n## The data\n\nA comment in `comments`:\n\n```js\n{\n id: \"c-81\",\n author: { id: \"u-lena\", name: \"Lena Fischer\", src: \"https://…/lena.jpg\" }, // or just \"Lena Fischer\"\n body: [ /* the doc onPost gave you */ ], // or a plain string\n createdAt: \"2026-09-23T09:12:00Z\", // ISO string, Date, ms, or a day (\"2026-09-23\": that day, never UTC's)\n edited: false, // true adds \"(edited)\"\n}\n```\n\nStore `body` as it came out of `onPost` (a `jsonb` column). Keep a plain\ncopy beside it for search and notifications: `docToText(doc)` from\n`doc-editor`. The people mentioned are `docMentions(doc)` — notify them.\n\n## Props\n\n- `onPost(doc)` — required. `async`, returns `{ ok: true }` or\n `{ ok: false, error? }` (`error` is shown under the box; without it,\n \"Couldn't post…\"). A throw counts as a failure. Only an ok clears the\n box. An empty box never posts.\n- `comments` — the thread, oldest first, drawn above the box. Leave it out\n for the field alone; `[]` shows `emptyText`.\n- `author` — `{ name, src? }`, who is writing: their avatar sits beside\n the box from 640px.\n- `mentions` — `async (query) => [{ id, label, description? }]`: turns on\n `@` (doc-editor's `mentions`).\n- `placeholder` — `\"Write a comment…\"`.\n- `postLabel` — the button, `\"Post\"` (\"Reply\", \"Send\").\n- `label` — the field's accessible name, `\"Comment\"`.\n- `threadLabel` — the list's accessible name, `\"Comments\"`.\n- `emptyText` — `\"No comments yet.\"`.\n- `autoFocus` — `false`.\n- `disabled` — `false`; `true` locks the box (no edit, no post).\n- `className` — layout around it.\n\nThe other exports, for a thread of your own:\n\n- `CommentComposer` — the field and its buttons: `onSubmit(doc)` (same\n answer as `onPost`), `author`, `mentions`, `defaultValue` (a doc to\n start from — editing a comment), `placeholder`, `submitLabel`\n (`\"Post\"`), `submitIcon` (a kit icon name, `\"send\"`; `null` for none),\n `busyLabel` (shown while it runs), `failedText`,\n `onCancel` (adds a Cancel button), `cancelLabel`, `label`,\n `autoFocus`, `disabled`, `clearOnSubmit` (`true`; `false` when the\n composer goes away after a save), `className`.\n- `CommentBody` — `{ doc, className }`: a doc drawn read-only as plain\n HTML (server too): paragraphs, headings, lists, checklists, quotes,\n code, tables, links, image links, mentions and fields as chips.\n- `CommentTime` — `{ date, className }`: a `<time>`; the absolute date on\n the server, \"5 min ago\" once mounted, kept current each minute.\n\n## Example\n\n```jsx\n// components/TaskComments.jsx — client\n\"use client\";\nimport { useState } from \"react\";\nimport { insertRow } from \"../lib/actions\";\nimport { CommentBox } from \"./blocks/comment-box\";\nimport { docToText } from \"./blocks/doc-editor\";\n\nexport default function TaskComments({ task, me, team, initial }) {\n const [comments, setComments] = useState(initial);\n const findPeople = async (q) =>\n team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));\n\n async function post(doc) {\n const res = await insertRow({\n source: \"public.comments\",\n values: { task_id: task.id, author_id: me.id, body: doc, body_text: docToText(doc) },\n });\n if (!res.ok) return { ok: false, error: res.error };\n setComments((c) => [...c, { id: res.id, author: me, body: doc, createdAt: new Date() }]);\n return { ok: true };\n }\n\n return <CommentBox comments={comments} onPost={post} author={me} mentions={findPeople} />;\n}\n```\n\n## What it will not do\n\n- Edit or delete a posted comment, react, or reply in a nested thread: the\n thread is read-only. Notes you keep, edit and delete on a record are\n `record-notes`; build a richer thread from `CommentComposer` +\n `CommentBody` in your own component.\n- Attach files or images, or offer a `/` menu: it is a comment, not a\n document (`doc-editor` for that).\n- Notify anyone: read the mentions off the doc (`docMentions`) and send\n them yourself.\n- Load older comments or scroll the thread: it draws what it is given;\n put it in a scrolling box and page it yourself.\n\nAn editor it cannot be is your own component in `components/` — not an\nedit to this block. Its files are fixed: `npm run check` refuses a\nchange, and `makersclaw add comment-box` restores them.\n"
41
+ "content": "# CommentBox\n\nA comment field that takes rich text: select a word for bold, italic or a\nlink, start a line with `-` for a list, type `@` to mention a teammate\n(they go in as a chip). Cmd/Ctrl+Enter posts, as does the Post button.\nWhile a post is on its way the box is locked; when it fails the words\nstay and the box says why; when it lands the box clears. Give it\n`comments` and it draws the thread above: avatar, name, \"5 min ago\", the\nbody with its formatting and mentions.\n\nIt is `doc-editor` in its minimal mode (no `/` menu, no drag handles), so\na comment is BlockNote JSON — the same doc every text block uses.\n\n```jsx\nimport { CommentBox } from \"../components/blocks/comment-box\"; // a Makersclaw app (\"./blocks/comment-box\" from components/)\nimport { CommentBox } from \"@/components/makersclaw/blocks/comment-box\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the thread in the server surface (README §4). `makersclaw add\ncomment-box` adds `doc-editor` with it (and its npm packages: run\n`npm install` after).\n\n## The data\n\nA comment in `comments`:\n\n```js\n{\n id: \"c-81\",\n author: { id: \"u-lena\", name: \"Lena Fischer\", src: \"https://…/lena.jpg\" }, // or just \"Lena Fischer\"\n body: [ /* the doc onPost gave you */ ], // or a plain string\n createdAt: \"2026-09-23T09:12:00Z\", // ISO string, Date, ms, or a day (\"2026-09-23\": that day, never UTC's)\n edited: false, // true adds \"(edited)\"\n}\n```\n\nStore `body` as it came out of `onPost` (a `jsonb` column). Keep a plain\ncopy beside it for search and notifications: `docToText(doc)` from\n`doc-editor`. The people mentioned are `docMentions(doc)` — notify them.\n\n## Props\n\n- `onPost(doc)` — required. `async`, returns `{ ok: true }` or\n `{ ok: false, error? }` (`error` is shown under the box; without it,\n \"Couldn't post…\"). A throw counts as a failure. Only an ok clears the\n box. An empty box never posts.\n- `comments` — the thread, oldest first, drawn above the box. Leave it out\n for the field alone; `[]` shows `emptyText`.\n- `author` — `{ name, src? }`, who is writing: their avatar sits beside\n the box from 640px.\n- `mentions` — `async (query) => [{ id, label, description? }]`: turns on\n `@` (doc-editor's `mentions`).\n- `placeholder` — `\"Write a comment…\"`.\n- `postLabel` — the button, `\"Post\"` (\"Reply\", \"Send\").\n- `label` — the field's accessible name, `\"Comment\"`.\n- `threadLabel` — the list's accessible name, `\"Comments\"`.\n- `emptyText` — `\"No comments yet.\"`.\n- `autoFocus` — `false`.\n- `disabled` — `false`; `true` locks the box (no edit, no post).\n- `className` — layout around it.\n\nThe other exports, for a thread of your own:\n\n- `CommentComposer` — the field and its buttons: `onSubmit(doc)` (same\n answer as `onPost`), `author`, `mentions`, `defaultValue` (a doc to\n start from — editing a comment), `placeholder`, `submitLabel`\n (`\"Post\"`), `submitIcon` (a kit icon name, `\"send\"`; `null` for none),\n `busyLabel` (shown while it runs), `failedText`,\n `onCancel` (adds a Cancel button), `cancelLabel`, `label`,\n `autoFocus`, `disabled`, `clearOnSubmit` (`true`; `false` when the\n composer goes away after a save), `className`.\n- `CommentBody` — `{ doc, className }`: a doc drawn read-only as plain\n HTML (server too): paragraphs, headings, lists, checklists, quotes,\n code, tables, links, image links, mentions and fields as chips.\n- `CommentTime` — `{ date, className }`: a `<time>`; the absolute date on\n the server, \"5 min ago\" once mounted, kept current each minute.\n\n## Example\n\n```jsx\n// surfaces/task.jsx — client: one task's comments\n\"use client\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useArtifact } from \"../lib/hooks\";\nimport { useRows, whenLanded } from \"../lib/data\";\nimport { insertRow } from \"../lib/actions\";\nimport { CommentBox } from \"../components/blocks/comment-box\";\nimport { docToText } from \"../components/blocks/doc-editor\";\n\nexport default function TaskComments() {\n const taskId = useSearchParams().get(\"row\");\n const { user } = useArtifact(); // the member signed in: { id, email }\n const { rows, loading } = useRows({ source: \"comments\", filter: { task_id: taskId }, orderBy: \"created_at asc\" });\n const { rows: team } = useRows({ source: \"people\", orderBy: \"name asc\" });\n // created_by: a before-insert trigger sets it from the member, never the page (the app's README §5)\n const comments = rows.map((c) => ({ id: c.id, author: c.created_by, body: c.body, createdAt: c.created_at }));\n const findPeople = async (q) =>\n team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));\n\n async function post(doc) {\n const res = await insertRow({ source: \"comments\", values: { task_id: taskId, body: doc, body_text: docToText(doc) } });\n if (!res.ok) return { ok: false, error: res.error };\n await whenLanded(); // the comment is read back before the box clears\n return { ok: true };\n }\n\n if (loading) return null;\n return <CommentBox comments={comments} onPost={post} author={user ? { name: user.email } : undefined} mentions={findPeople} />;\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\n## What it will not do\n\n- Edit or delete a posted comment, react, or reply in a nested thread: the\n thread is read-only. Notes you keep, edit and delete on a record are\n `record-notes`; build a richer thread from `CommentComposer` +\n `CommentBody` in your own component.\n- Attach files or images, or offer a `/` menu: it is a comment, not a\n document (`doc-editor` for that).\n- Notify anyone: read the mentions off the doc (`docMentions`) and send\n them yourself.\n- Load older comments or scroll the thread: it draws what it is given;\n put it in a scrolling box and page it yourself.\n\nAn editor it cannot be is your own component in `components/` — not an\nedit to this block. Its files are fixed: `npm run check` refuses a\nchange, and `makersclaw add comment-box` restores them.\n"
42
42
  }
43
43
  ],
44
44
  "categories": [
@@ -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.6.0",
60
+ "version": "0.7.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
  }
@@ -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.6.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json"
9
9
  ],
10
10
  "dependencies": [],
11
11
  "files": [
@@ -25,7 +25,7 @@
25
25
  "path": "registry/blocks/contact-section/README.md",
26
26
  "type": "registry:file",
27
27
  "target": "@components/makersclaw/blocks/contact-section/README.md",
28
- "content": "# Contact Section\n\nA public \"contact us\" section. It holds a form, the ways to reach you, or\nboth. The form asks for a name, an email and a message, and optionally a\nphone, a company, a subject, a topic (chips or radios) and a consent box.\nThe ways to reach you are details (email, phone, address, hours) and\noffices.\n\nThree layouts:\n- **form** — the form alone, with the details on one line above it, or\n a picture beside or behind it;\n- **split** — the form beside an info panel (plain, or filled with the\n primary colour);\n- **cards** — the details as icon cards, alone, above the form or beside\n it.\n\nThe form checks itself as it is sent. A missing or malformed field gets\nits message under it, and the focus moves to the first one. It then\ncalls `onSubmit(values)`, which is async and returns `{ ok }`. While it\nruns, the button spins and a second press does nothing. `{ ok: false,\nerror }` (or a throw) shows the error above the button and keeps\neverything typed. `{ ok: true }` replaces the form with a \"Message\nsent\" notice and a \"Send another\" button. It is a full-width section: it\npads itself and centres its content.\n\n```jsx\nimport { ContactSection } from \"../components/blocks/contact-section\"; // a Makersclaw app (\"./blocks/contact-section\" from components/)\nimport { ContactSection } from \"@/components/makersclaw/blocks/contact-section\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component.\n\n## The data\n\n`details` is the ways to reach you:\n\n```js\n{\n icon: \"mail\", // a name from the kit's Icon\n label: \"Email\", // what it is (\"Call sales\" on a card)\n value: \"hello@fieldline.io\", // what is shown\n href?: \"mailto:hello@fieldline.io\", // a link: mailto:, tel:, a map, a page\n description?: \"A person replies within a working day.\", // a card's and a panel row's second line\n}\n```\n\n`offices` (split only): `[{ name: \"Lisbon\", lines: [\"Rua da Prata 88\", …], icon? }]`.\n\n`onSubmit` receives the values, trimmed:\n\n```js\n{\n firstName, lastName, // or `name` with nameFields=\"one\"\n email, message,\n phone?, company?, subject?, // the ones named in `fields`\n topic?, // the chosen option's value, \"\" for none\n consent?, // true: it was required and ticked\n}\n```\n\n## Props\n\n- `onSubmit(values)` — async, returns `{ ok: true }` or `{ ok: false,\n error? }`. Without it there is no form (a cards-only or panel-only\n section).\n- `layout` — `split` (default) · `form` · `cards`.\n- `kicker`, `title`, `description` — the header. `title` defaults to\n \"Get in touch\".\n- `align` — the header, and the cards' content: `center` (default for\n form and cards) · `start` (default for split).\n- `details` — the ways to reach you, as above. A panel's rows in\n `split`, one line above the form in `form`, the cards in `cards`.\n- `offices` — `split`: the offices, under the details in the panel.\n- `panel` — `split`: `plain` (default, a quiet bordered box) · `filled`\n (the primary colour).\n- `panelTitle`, `panelBody` — `split`: the panel's heading and a line\n under it.\n- `cardsPosition` — `cards` with a form: `above` (default) · `beside`\n (the cards stacked in a column beside the form from `lg` up).\n- `cardColumns` — `cards`: `1` · `2` · `3` · `4`. Default the number of\n cards, at most 3.\n- `image`, `imageAlt` — `form`: a picture. `imageAlt` defaults to \"\"\n (decoration).\n- `imagePosition` — `side` (default: beside the form from `md` up,\n hidden on a phone) · `background` (behind the form card, under a veil\n of the background colour).\n- `nameFields` — `split` (default: first name, required, and last name)\n · `one` (a single \"Name\").\n- `fields` — the optional fields to add: any of `\"phone\"`, `\"company\"`,\n `\"subject\"`. Phone and company are marked optional; subject is not\n required either.\n- `topics` — `{ label?, options, style?, required? }`: `options` are\n strings or `{ value, label }`; `style` `chips` (default: pressable\n chips above the name) · `radio` (a radio row after the email).\n- `consent` — the label of a consent checkbox (a string or a node with\n your policy link). When given, it must be ticked to send.\n- `messageLabel` — default \"Message\". `messageRows` — default `5`.\n- `submitLabel` — default \"Send message\".\n- `sent` — `{ title?, body? }` for the sent notice.\n- `defaultValues` — values to start with (a signed-in visitor's name and\n email).\n- `labels` — override field labels: `{ firstName, lastName, name, email,\n phone, company, subject }`.\n- `label` — what a screen reader calls the section. Default the title.\n- `className` — the section's spacing and width. Never colour.\n\n## Example\n\n```jsx\n// components/Contact.jsx\n\"use client\";\nimport { ContactSection } from \"./blocks/contact-section\";\nimport { sendContactMessage } from \"../lib/actions\"; // your server action\n\nexport default function Contact() {\n return (\n <ContactSection\n title=\"Talk to a person\"\n description=\"We reply within one working day.\"\n details={[\n { icon: \"mail\", label: \"Email\", value: \"hello@fieldline.io\", href: \"mailto:hello@fieldline.io\" },\n { icon: \"phone\", label: \"Phone\", value: \"+44 20 7946 0321\", href: \"tel:+442079460321\" },\n { icon: \"clock\", label: \"Hours\", value: \"Mon–Fri, 9:00–18:00 GMT\" },\n ]}\n topics={{ options: [\"Sales\", \"Support\", \"Partnerships\"], required: true }}\n consent=\"I agree to the privacy policy.\"\n onSubmit={async (values) => {\n const res = await sendContactMessage(values);\n return res.ok ? { ok: true } : { ok: false, error: res.error };\n }}\n />\n );\n}\n```\n\n## What it will not do\n\n- Send anything: `onSubmit` is yours (save a row, email the team).\n- Stop spam: add a captcha or a honeypot in your own `onSubmit` path.\n- Upload files, or take a card number.\n- Draw a map: link the address (`href`) to one.\n\nA section it cannot be is your own component in `components/`, built\nfrom the kit — not an edit to this block. Its files are fixed: `npm run\ncheck` refuses a change, and `makersclaw add contact-section` restores\nthem.\n\n## Credits\n\nLayouts ported from Creative Tim UI's contact-section blocks\n(https://github.com/creativetimofficial/ui, MIT), rewritten on the\nMakersclaw kit.\n"
28
+ "content": "# Contact Section\n\nA public \"contact us\" section. It holds a form, the ways to reach you, or\nboth. The form asks for a name, an email and a message, and optionally a\nphone, a company, a subject, a topic (chips or radios) and a consent box.\nThe ways to reach you are details (email, phone, address, hours) and\noffices.\n\nThree layouts:\n- **form** — the form alone, with the details on one line above it, or\n a picture beside or behind it;\n- **split** — the form beside an info panel (plain, or filled with the\n primary colour);\n- **cards** — the details as icon cards, alone, above the form or beside\n it.\n\nThe form checks itself as it is sent. A missing or malformed field gets\nits message under it, and the focus moves to the first one. It then\ncalls `onSubmit(values)`, which is async and returns `{ ok }`. While it\nruns, the button spins and a second press does nothing. `{ ok: false,\nerror }` (or a throw) shows the error above the button and keeps\neverything typed. `{ ok: true }` replaces the form with a \"Message\nsent\" notice and a \"Send another\" button. It is a full-width section: it\npads itself and centres its content.\n\n```jsx\nimport { ContactSection } from \"../components/blocks/contact-section\"; // a Makersclaw app (\"./blocks/contact-section\" from components/)\nimport { ContactSection } from \"@/components/makersclaw/blocks/contact-section\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component.\n\n## The data\n\n`details` is the ways to reach you:\n\n```js\n{\n icon: \"mail\", // a name from the kit's Icon\n label: \"Email\", // what it is (\"Call sales\" on a card)\n value: \"hello@fieldline.io\", // what is shown\n href?: \"mailto:hello@fieldline.io\", // a link: mailto:, tel:, a map, a page\n description?: \"A person replies within a working day.\", // a card's and a panel row's second line\n}\n```\n\n`offices` (split only): `[{ name: \"Lisbon\", lines: [\"Rua da Prata 88\", …], icon? }]`.\n\n`onSubmit` receives the values, trimmed:\n\n```js\n{\n firstName, lastName, // or `name` with nameFields=\"one\"\n email, message,\n phone?, company?, subject?, // the ones named in `fields`\n topic?, // the chosen option's value, \"\" for none\n consent?, // true: it was required and ticked\n}\n```\n\n## Props\n\n- `onSubmit(values)` — async, returns `{ ok: true }` or `{ ok: false,\n error? }`. Without it there is no form (a cards-only or panel-only\n section).\n- `layout` — `split` (default) · `form` · `cards`.\n- `kicker`, `title`, `description` — the header. `title` defaults to\n \"Get in touch\".\n- `align` — the header, and the cards' content: `center` (default for\n form and cards) · `start` (default for split).\n- `details` — the ways to reach you, as above. A panel's rows in\n `split`, one line above the form in `form`, the cards in `cards`.\n- `offices` — `split`: the offices, under the details in the panel.\n- `panel` — `split`: `plain` (default, a quiet bordered box) · `filled`\n (the primary colour).\n- `panelTitle`, `panelBody` — `split`: the panel's heading and a line\n under it.\n- `cardsPosition` — `cards` with a form: `above` (default) · `beside`\n (the cards stacked in a column beside the form from `lg` up).\n- `cardColumns` — `cards`: `1` · `2` · `3` · `4`. Default the number of\n cards, at most 3.\n- `image`, `imageAlt` — `form`: a picture. `imageAlt` defaults to \"\"\n (decoration).\n- `imagePosition` — `side` (default: beside the form from `md` up,\n hidden on a phone) · `background` (behind the form card, under a veil\n of the background colour).\n- `nameFields` — `split` (default: first name, required, and last name)\n · `one` (a single \"Name\").\n- `fields` — the optional fields to add: any of `\"phone\"`, `\"company\"`,\n `\"subject\"`. Phone and company are marked optional; subject is not\n required either.\n- `topics` — `{ label?, options, style?, required? }`: `options` are\n strings or `{ value, label }`; `style` `chips` (default: pressable\n chips above the name) · `radio` (a radio row after the email).\n- `consent` — the label of a consent checkbox (a string or a node with\n your policy link). When given, it must be ticked to send.\n- `messageLabel` — default \"Message\". `messageRows` — default `5`.\n- `submitLabel` — default \"Send message\".\n- `sent` — `{ title?, body? }` for the sent notice.\n- `defaultValues` — values to start with (a signed-in visitor's name and\n email).\n- `labels` — override field labels: `{ firstName, lastName, name, email,\n phone, company, subject }`.\n- `label` — what a screen reader calls the section. Default the title.\n- `className` — the section's spacing and width. Never colour.\n\n## Example\n\n```jsx\n// components/Contact.jsx — client: a message is a row\n\"use client\";\nimport { insertRow } from \"../lib/actions\";\nimport { ContactSection } from \"./blocks/contact-section\";\n\nexport default function Contact() {\n return (\n <ContactSection\n title=\"Talk to a person\"\n description=\"We reply within one working day.\"\n details={[\n { icon: \"mail\", label: \"Email\", value: \"hello@fieldline.io\", href: \"mailto:hello@fieldline.io\" },\n { icon: \"phone\", label: \"Phone\", value: \"+44 20 7946 0321\", href: \"tel:+442079460321\" },\n { icon: \"clock\", label: \"Hours\", value: \"Mon–Fri, 9:00–18:00 GMT\" },\n ]}\n topics={{ options: [\"Sales\", \"Support\", \"Partnerships\"], required: true }}\n consent=\"I agree to the privacy policy.\"\n onSubmit={(values) => insertRow({ source: \"contact_messages\", values })} // { ok, id } or { ok: false, error }\n />\n );\n}\n```\n\nThe row is the message: the app's agent, or a job, answers it.\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\n## What it will not do\n\n- Send anything: `onSubmit` is yours (save a row, email the team).\n- Stop spam: add a captcha or a honeypot in your own `onSubmit` path.\n- Upload files, or take a card number.\n- Draw a map: link the address (`href`) to one.\n\nA section it cannot be is your own component in `components/`, built\nfrom the kit — not an edit to this block. Its files are fixed: `npm run\ncheck` refuses a change, and `makersclaw add contact-section` restores\nthem.\n\n## Credits\n\nLayouts ported from Creative Tim UI's contact-section blocks\n(https://github.com/creativetimofficial/ui, MIT), rewritten on the\nMakersclaw kit.\n"
29
29
  }
30
30
  ],
31
31
  "categories": [
@@ -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.6.0",
49
+ "version": "0.7.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
  }