makersclaw 0.5.2 → 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.
- package/README.md +73 -7
- package/package.json +3 -2
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +3 -3
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +3 -3
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +121 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +14 -5
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chat/README.md +43 -13
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +16 -5
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +4 -2
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +16 -5
- package/registry/blocks/org-chart/README.md +10 -7
- package/registry/blocks/page-builder/README.md +13 -6
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +22 -20
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +13 -4
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +19 -13
- package/src/cli.mjs +65 -9
- package/src/install.mjs +360 -21
- package/src/lockfile.mjs +468 -0
- package/src/pm.mjs +473 -0
- package/src/registry.mjs +125 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +818 -82
- package/templates/next/files/app/claw-alive/route.js +2 -2
- package/templates/next/files/app/frame.js +134 -9
- package/templates/next/files/app/layout.jsx +2 -2
- package/templates/next/files/check/run.mjs +1491 -53
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +171 -52
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +11 -0
- package/templates/next/files/lib/data.js +120 -30
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/next.config.mjs +85 -9
- package/templates/next/files/package.json +3 -3
- package/templates/next/files/{middleware.js → proxy.js} +13 -6
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/pnpm-lock.yaml +5870 -0
- package/templates/next/template.json +13 -6
- package/templates/next/files/schema.sql +0 -22
package/r/comment-box.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "CommentBox",
|
|
6
6
|
"description": "A minimal rich comment field (DocEditor in minimal mode): bold, italic, links and lists from the selection toolbar or markdown shortcuts, @mentions as chips, Cmd/Ctrl+Enter to post. It posts through an async onPost(doc) that answers { ok }, keeps the draft and says why when it fails, and clears when it lands. With `comments` it draws the thread above it: avatar, name, relative time, the rich body. Also exports the composer alone, the read-only body renderer and the timestamp.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.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//
|
|
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.
|
|
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
|
}
|
package/r/contact-section.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "Contact Section",
|
|
6
6
|
"description": "A public \"contact us\" section: a form (name, email, message; optional phone, company, subject, a topic as chips or radios, a consent box) and/or the ways to reach you (email, phone, address, hours, offices). The form alone, beside an info panel (plain or filled), or with info cards above or beside it; an optional picture beside or behind the form. Validates in place, submits through an async onSubmit that returns { ok }, and shows the failure or a sent state.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.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 {
|
|
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.
|
|
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
|
}
|
package/r/core.json
CHANGED
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"path": "templates/next/files/components/ui/item.jsx",
|
|
172
172
|
"type": "registry:ui",
|
|
173
173
|
"target": "@components/makersclaw/ui/item.jsx",
|
|
174
|
-
"content": "import Link from \"next/link\";\nimport { Children } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Item — the list row: a contact, a file, a run, a setting. Media on the\n// left, title and description, meta on the right, then its own actions.\n// With `href` or `onClick` the WHOLE row is the target — the title is the\n// real <a> or <button> and stretches over the row, so the row's own\n// actions stay separate buttons (never a button inside a link) and a\n// screen reader hears the title, not the whole row read out.\n//\n// media a node on the left: an avatar, an <img>, a logo\n// icon or a glyph from ./icon, drawn in a muted square\n// title the one line that names the thing (truncates)\n// description up to two lines under it\n// meta a count, a date, a Badge — muted, on the right\n// actions buttons on the far right (IconButton, Menu); they sit above\n// the row's target, so they click on their own\n// href a next/link row · `onClick` a button row · neither:
|
|
174
|
+
"content": "import Link from \"next/link\";\nimport { Children } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Item — the list row: a contact, a file, a run, a setting. Media on the\n// left, title and description, meta on the right, then its own actions.\n// With `href` or `onClick` the WHOLE row is the target — the title is the\n// real <a> or <button> and stretches over the row, so the row's own\n// actions stay separate buttons (never a button inside a link) and a\n// screen reader hears the title, not the whole row read out.\n//\n// media a node on the left: an avatar, an <img>, a logo\n// icon or a glyph from ./icon, drawn in a muted square\n// title the one line that names the thing (truncates)\n// description up to two lines under it\n// meta a count, a date, a Badge — muted, on the right\n// actions buttons on the far right (IconButton, Menu); they sit above\n// the row's target, so they click on their own\n// href a next/link row · `onClick` a button row · neither: a plain row\n// size default (56px) · sm (44px, for dense lists and rails) ·\n// lg (64px, roomier: a pipeline's runs, a list read at leisure)\n// variant plain (no fill) · outline (a bordered card) · muted (a filled panel)\n// layout row (default) · stacked — a card for a grid: icon or media on\n// top, then the title (up to two lines), the description (up\n// to three), children, and meta at the foot; actions sit in the\n// top-right corner. It fills its grid cell's height, so a row\n// of cards lines up. Pair it with variant=\"outline\" or \"muted\".\n// The link or button target and what goes where are the row's\n//\n// Where the other props go: `aria-current`, `onClick`, `target` and `rel`\n// belong to the row's target, so they go on its <a> or <button> (with\n// `href`, `onClick` rides on the link: analytics, closing a sheet).\n// aria-current on the target fills the row, so the current page looks\n// like what a screen reader says it is. Every other prop (data-*, id,\n// aria-label…) goes on the row's <div>.\n//\n// ItemGroup stacks plain Items with a rule between each, as a list.\n\nconst VARIANTS = {\n plain: \"\",\n outline: \"border border-border bg-card text-card-foreground\",\n muted: \"bg-muted\",\n};\n\nconst SIZES = {\n default: { row: \"min-h-14 gap-3 px-3 py-2.5\", icon: \"size-10\", glyph: 18, desc: \"line-clamp-2 text-sm\" },\n sm: { row: \"min-h-11 gap-2.5 px-2.5 py-1.5\", icon: \"size-8\", glyph: 16, desc: \"truncate text-xs\" },\n lg: { row: \"min-h-16 gap-3.5 px-4 py-3\", icon: \"size-11\", glyph: 20, desc: \"line-clamp-2 text-sm\" },\n};\n\n// layout=\"stacked\": the card's padding and gaps, and its icon square.\nconst STACKED = {\n default: { box: \"gap-3 p-4\", icon: \"size-10\", glyph: 18 },\n sm: { box: \"gap-2 p-3\", icon: \"size-8\", glyph: 16 },\n lg: { box: \"gap-4 p-5\", icon: \"size-12\", glyph: 22 },\n};\n\nconst TARGET = \"outline-none after:absolute after:inset-0\";\n\n// The row filled while its target (or a plain row itself) is current.\nconst CURRENT =\n \"has-[[data-item-target][aria-current]:not([aria-current=false])]:bg-accent \" +\n \"[&[aria-current]:not([aria-current=false])]:bg-accent\";\n\nexport function Item({\n media,\n icon,\n title,\n description,\n meta,\n actions,\n href,\n onClick,\n size = \"default\",\n variant = \"plain\",\n layout = \"row\",\n disabled,\n className,\n children,\n \"aria-current\": ariaCurrent,\n target,\n rel,\n ...rest\n}) {\n const s = SIZES[size] ?? SIZES.default;\n const interactive = Boolean(href || onClick) && !disabled;\n\n let name = title;\n let targeted = false;\n if (href && !disabled) {\n targeted = true;\n name = (\n <Link href={href} onClick={onClick} target={target} rel={rel} aria-current={ariaCurrent} data-item-target className={TARGET}>\n {title}\n </Link>\n );\n } else if (onClick) {\n targeted = true;\n name = (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-current={ariaCurrent}\n data-item-target\n className={cx(TARGET, \"text-left disabled:cursor-not-allowed\")}\n >\n {title}\n </button>\n );\n }\n\n const ring =\n interactive &&\n \"hover:bg-accent has-[[data-item-target]:focus-visible]:ring-2 has-[[data-item-target]:focus-visible]:ring-inset has-[[data-item-target]:focus-visible]:ring-ring\";\n\n if (layout === \"stacked\") {\n const k = STACKED[size] ?? STACKED.default;\n return (\n <div\n className={cx(\"relative flex h-full flex-col items-start rounded-lg text-sm\", VARIANTS[variant] ?? VARIANTS.plain, k.box, ring, CURRENT, disabled && \"opacity-50\", className)}\n aria-current={targeted ? undefined : ariaCurrent}\n {...rest}\n >\n {icon ? (\n <span className={cx(\"flex shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\", variant === \"muted\" && \"bg-background\", k.icon)}>\n <Icon name={icon} size={k.glyph} />\n </span>\n ) : media ? (\n <span className=\"flex shrink-0 items-center\">{media}</span>\n ) : null}\n <div className={cx(\"w-full min-w-0 flex-1\", actions && !icon && !media && \"pr-10\")}>\n <p className=\"line-clamp-2 font-medium text-foreground\">{name}</p>\n {description ? <p className=\"mt-1 line-clamp-3 text-sm text-muted-foreground\">{description}</p> : null}\n {children ? <div className=\"mt-2\">{children}</div> : null}\n </div>\n {meta !== undefined && meta !== null ? (\n <div data-nums className=\"text-xs text-muted-foreground\">\n {meta}\n </div>\n ) : null}\n {actions ? <div className=\"absolute right-2 top-2 z-10 flex items-center gap-1\">{actions}</div> : null}\n </div>\n );\n }\n\n return (\n <div\n className={cx(\n \"relative flex items-center rounded-lg text-sm\",\n VARIANTS[variant] ?? VARIANTS.plain,\n s.row,\n ring,\n CURRENT,\n disabled && \"opacity-50\",\n className,\n )}\n aria-current={targeted ? undefined : ariaCurrent}\n {...rest}\n >\n {icon ? (\n <span className={cx(\"flex shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\", variant === \"muted\" && \"bg-background\", s.icon)}>\n <Icon name={icon} size={s.glyph} />\n </span>\n ) : media ? (\n <span className=\"flex shrink-0 items-center\">{media}</span>\n ) : null}\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium text-foreground\">{name}</p>\n {description ? <p className={cx(\"text-muted-foreground\", s.desc)}>{description}</p> : null}\n {children ? <div className=\"mt-1.5\">{children}</div> : null}\n </div>\n {meta !== undefined && meta !== null ? (\n <div data-nums className=\"shrink-0 text-xs text-muted-foreground\">\n {meta}\n </div>\n ) : null}\n {actions ? <div className=\"relative z-10 flex shrink-0 items-center gap-1\">{actions}</div> : null}\n </div>\n );\n}\n\n/** Items as a list, a rule between each. `variant=\"outline\"` boxes the\n * whole list; put plain Items in it. */\nexport function ItemGroup({ variant = \"plain\", className, children, \"aria-label\": ariaLabel }) {\n const rows = Children.toArray(children).filter(Boolean);\n if (rows.length === 0) return null;\n return (\n <ul\n role=\"list\"\n aria-label={ariaLabel}\n className={cx(\n \"flex flex-col\",\n variant === \"outline\" && \"overflow-hidden rounded-lg border border-border bg-card text-card-foreground [&>li>*]:rounded-none\",\n className,\n )}\n >\n {rows.map((row, i) => (\n <li key={row.key ?? i} className={cx(i > 0 && \"border-t border-border\")}>\n {row}\n </li>\n ))}\n </ul>\n );\n}\n"
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
"path": "templates/next/files/components/ui/menu.jsx",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
"path": "templates/next/files/components/ui/notice.jsx",
|
|
184
184
|
"type": "registry:ui",
|
|
185
185
|
"target": "@components/makersclaw/ui/notice.jsx",
|
|
186
|
-
"content": "import { cx } from \"./cx\";\nimport { IconButton } from \"./button\";\nimport { Icon } from \"./icon\";\n\n// Static parts: a card, a notice, a badge, a kicker, the empty state.\n// Server-safe (no hooks), so a server surface can use them directly.\n\nconst TONES = {\n neutral: { box: \"border-border bg-card text-card-foreground\", icon: \"info\" },\n info: { box: \"border-info/40 bg-card text-card-foreground\", icon: \"info\" },\n success: { box: \"border-success/40 bg-card text-card-foreground\", icon: \"check\" },\n warning: { box: \"border-warning/50 bg-card text-card-foreground\", icon: \"warning\" },\n danger: { box: \"border-destructive/50 bg-card text-card-foreground\", icon: \"warning\" },\n};\n\n/** A raised panel with an optional heading row.\n * padding md (default, 16px) · sm (12px, a dense rail) · none (the\n * body runs to the border — a table, a list of rows; the\n * heading row keeps its own 16px inset)\n * titleSize md (default, 16px) · lg (20px, a page-level card's title)\n * Pick these here, never with a className: cx does not merge classes,\n * so a `p-0` laid over the default padding wins only by CSS order. */\nconst CARD_PADDING = { none: \"\", sm: \"p-3\", md: \"p-4\" };\nconst CARD_TITLE = { md: \"text-base\", lg: \"text-xl\" };\n\nexport function Card({\n title,\n description,\n actions,\n padding = \"md\",\n titleSize = \"md\",\n className,\n children,\n as: Tag = \"section\",\n}) {\n const pad = CARD_PADDING[padding] ?? CARD_PADDING.md;\n const bare = padding === \"none\";\n return (\n <Tag className={cx(\"rounded-lg border border-border bg-card text-card-foreground\", pad, bare && \"overflow-hidden\", className)}>\n {title || actions ? (\n <div className={cx(\"flex items-start justify-between gap-3\", bare ? \"px-4 pt-4 pb-3\" : \"mb-3\")}>\n <div className=\"min-w-0\">\n {title ? (\n <h2 className={cx(\"font-semibold leading-tight\", CARD_TITLE[titleSize] ?? CARD_TITLE.md)}>{title}</h2>\n ) : null}\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {actions ? <div className=\"flex shrink-0 gap-2\">{actions}</div> : null}\n </div>\n ) : null}\n {children}\n </Tag>\n );\n}\n\n/** One line the user should read, with an optional action. `tone`\n * neutral · info · success · warning · danger. Colour is the signal, so\n * status tones are for status only — never decoration. */\nexport function Notice({ tone = \"neutral\", title, action, onDismiss, className, children }) {\n const t = TONES[tone] ?? TONES.neutral;\n return (\n <div role={tone === \"danger\" ? \"alert\" : \"status\"} className={cx(\"flex items-start gap-3 rounded-lg border p-3 text-sm\", t.box, className)}>\n <Icon name={t.icon} className=\"mt-0.5 shrink-0 text-muted-foreground\" />\n <div className=\"min-w-0 flex-1\">\n {title ? <p className=\"font-medium\">{title}</p> : null}\n {children ? <div className={cx(title && \"mt-0.5\", \"text-muted-foreground\")}>{children}</div> : null}\n </div>\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n {onDismiss ? (\n <IconButton icon=\"x\" label=\"Dismiss\" size=\"sm\" onClick={onDismiss} className=\"-my-1 -mr-1\" />\n ) : null}\n </div>\n );\n}\n\n/** A small label for a state or a count.\n * tone neutral · primary · success · warning · danger · info\n * variant label (default: small caps, wide tracking; primary is a\n * solid fill, every other tone a tint) · soft (the case as\n * written, normal tracking, EVERY tone a tint — primary\n * included: \"Ready\", \"Daily over\", \"Feature\") · outline (the\n * case as written, a tone-coloured hairline on no fill) ·\n * code (the mono face, the case as written, normal tracking\n * — a version tag \"v2.4.0\", a commit \"a3f8c2d\", a path)\n * hue 1..5 — a categorical colour, read from --color-chart-N,\n * for KINDS of things (a DNS type, a platform, a content\n * type). Tone is for status; hue is for category, and a\n * tone given with it wins. With every variant: soft is the\n * case as written on a tint of the hue with a hue dot and\n * foreground text; label (and code) is filled with the hue\n * and black or white text, whichever reads; outline is a\n * hue hairline with foreground text. The text is never the\n * hue itself: a chart colour is not made to be read as type\n * Case is a variant, not its own prop: uppercase needs wide tracking and\n * sentence case needs none, so the two travel together, and a `case`\n * prop would have to be ignored by code. A soft primary lives on `soft`\n * because `label` primary is a solid fill apps already rely on. The\n * outline's ring is inset, so every variant is the same size in a row. */\nconst BADGE_LOOK = {\n label: {\n neutral: \"bg-muted text-muted-foreground\",\n primary: \"bg-primary text-primary-foreground\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/15 text-warning\",\n danger: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n },\n soft: {\n neutral: \"bg-muted text-muted-foreground\",\n primary: \"bg-primary/15 text-primary\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/15 text-warning\",\n danger: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n },\n outline: {\n neutral: \"bg-transparent text-foreground ring-1 ring-inset ring-border\",\n primary: \"bg-transparent text-primary ring-1 ring-inset ring-primary/50\",\n success: \"bg-transparent text-success ring-1 ring-inset ring-success/50\",\n warning: \"bg-transparent text-warning ring-1 ring-inset ring-warning/50\",\n danger: \"bg-transparent text-destructive ring-1 ring-inset ring-destructive/50\",\n info: \"bg-transparent text-info ring-1 ring-inset ring-info/50\",\n },\n};\nBADGE_LOOK.code = BADGE_LOOK.label;\n\nconst BADGE_FACE = {\n label: \"font-medium uppercase tracking-wide\",\n soft: \"font-medium\",\n outline: \"font-medium\",\n code: \"font-mono font-normal\",\n};\n\n// Categorical hues, literal so Tailwind sees every class. A filled hue's\n// text is black or white, picked from the fill's relative luminance (the\n// WCAG formula, on the clipped sRGB channels): what CSS contrast-color()\n// does, which no token can. A chart colour sits mid-lightness (chart-2…5\n// are clamped to OKLCH L 0.55–0.8, often past sRGB), where the theme's\n// light text fails on a light theme and its dark text on a dark primary.\n// Black below Y 0.179, white above: the better of the two is always\n// 4.58:1 or more. The soft tint is light (8%) so foreground text keeps\n// its contrast.\nconst HUE_LOOK = {\n label: {\n 1: \"bg-chart-1 text-[color:color(from_var(--color-chart-1)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 2: \"bg-chart-2 text-[color:color(from_var(--color-chart-2)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 3: \"bg-chart-3 text-[color:color(from_var(--color-chart-3)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 4: \"bg-chart-4 text-[color:color(from_var(--color-chart-4)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 5: \"bg-chart-5 text-[color:color(from_var(--color-chart-5)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n },\n soft: {\n 1: \"bg-chart-1/8 text-foreground\",\n 2: \"bg-chart-2/8 text-foreground\",\n 3: \"bg-chart-3/8 text-foreground\",\n 4: \"bg-chart-4/8 text-foreground\",\n 5: \"bg-chart-5/8 text-foreground\",\n },\n outline: {\n 1: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-1\",\n 2: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-2\",\n 3: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-3\",\n 4: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-4\",\n 5: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-5\",\n },\n};\nHUE_LOOK.code = HUE_LOOK.label;\nconst HUE_DOT = { 1: \"bg-chart-1\", 2: \"bg-chart-2\", 3: \"bg-chart-3\", 4: \"bg-chart-4\", 5: \"bg-chart-5\" };\n\nexport function Badge({ tone, hue, variant = \"label\", className, children }) {\n const v = BADGE_FACE[variant] ? variant : \"label\";\n const h = tone == null && HUE_DOT[hue] ? hue : null;\n const look = h ? HUE_LOOK[v][h] : BADGE_LOOK[v][tone ?? \"neutral\"] ?? BADGE_LOOK[v].neutral;\n return (\n <span className={cx(\"inline-flex items-center rounded-sm px-1.5 py-0.5 text-2xs\", h && v === \"soft\" && \"gap-1\", BADGE_FACE[v], look, className)}>\n {h && v === \"soft\" ? <span aria-hidden=\"true\" className={cx(\"size-1.5 shrink-0 rounded-full\", HUE_DOT[h])} /> : null}\n {children}\n </span>\n );\n}\n\n/** The small uppercase line above a heading. */\nexport function Kicker({ className, children }) {\n return <p className={cx(\"text-2xs font-medium uppercase tracking-wider text-muted-foreground\", className)}>{children}</p>;\n}\n\n/** What a surface shows when a real collection is empty AND there is no\n * fixture to show (a filter with no matches, a search with no results).\n * Says what would appear here and offers the one way to make it. */\nexport function Empty({ icon = \"search\", title, body, action, className }) {\n return (\n <div className={cx(\"grid place-items-center gap-2 rounded-lg border border-dashed border-border px-6 py-10 text-center\", className)}>\n <Icon name={icon} size={22} className=\"text-muted-foreground\" />\n <p className=\"text-sm font-medium\">{title}</p>\n {body ? <p className=\"max-w-prose text-sm text-muted-foreground\">{body}</p> : null}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n );\n}\n"
|
|
186
|
+
"content": "import { cx } from \"./cx\";\nimport { IconButton } from \"./button\";\nimport { Icon } from \"./icon\";\n\n// Plain parts: a card, a notice, a badge, a kicker, the empty state.\n// Server-safe (no hooks), so a server surface can use them directly.\n\nconst TONES = {\n neutral: { box: \"border-border bg-card text-card-foreground\", icon: \"info\" },\n info: { box: \"border-info/40 bg-card text-card-foreground\", icon: \"info\" },\n success: { box: \"border-success/40 bg-card text-card-foreground\", icon: \"check\" },\n warning: { box: \"border-warning/50 bg-card text-card-foreground\", icon: \"warning\" },\n danger: { box: \"border-destructive/50 bg-card text-card-foreground\", icon: \"warning\" },\n};\n\n/** A raised panel with an optional heading row.\n * padding md (default, 16px) · sm (12px, a dense rail) · none (the\n * body runs to the border — a table, a list of rows; the\n * heading row keeps its own 16px inset)\n * titleSize md (default, 16px) · lg (20px, a page-level card's title)\n * Pick these here, never with a className: cx does not merge classes,\n * so a `p-0` laid over the default padding wins only by CSS order. */\nconst CARD_PADDING = { none: \"\", sm: \"p-3\", md: \"p-4\" };\nconst CARD_TITLE = { md: \"text-base\", lg: \"text-xl\" };\n\nexport function Card({\n title,\n description,\n actions,\n padding = \"md\",\n titleSize = \"md\",\n className,\n children,\n as: Tag = \"section\",\n}) {\n const pad = CARD_PADDING[padding] ?? CARD_PADDING.md;\n const bare = padding === \"none\";\n return (\n <Tag className={cx(\"rounded-lg border border-border bg-card text-card-foreground\", pad, bare && \"overflow-hidden\", className)}>\n {title || actions ? (\n <div className={cx(\"flex items-start justify-between gap-3\", bare ? \"px-4 pt-4 pb-3\" : \"mb-3\")}>\n <div className=\"min-w-0\">\n {title ? (\n <h2 className={cx(\"font-semibold leading-tight\", CARD_TITLE[titleSize] ?? CARD_TITLE.md)}>{title}</h2>\n ) : null}\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {actions ? <div className=\"flex shrink-0 gap-2\">{actions}</div> : null}\n </div>\n ) : null}\n {children}\n </Tag>\n );\n}\n\n/** One line the user should read, with an optional action. `tone`\n * neutral · info · success · warning · danger. Colour is the signal, so\n * status tones are for status only — never decoration. */\nexport function Notice({ tone = \"neutral\", title, action, onDismiss, className, children }) {\n const t = TONES[tone] ?? TONES.neutral;\n return (\n <div role={tone === \"danger\" ? \"alert\" : \"status\"} className={cx(\"flex items-start gap-3 rounded-lg border p-3 text-sm\", t.box, className)}>\n <Icon name={t.icon} className=\"mt-0.5 shrink-0 text-muted-foreground\" />\n <div className=\"min-w-0 flex-1\">\n {title ? <p className=\"font-medium\">{title}</p> : null}\n {children ? <div className={cx(title && \"mt-0.5\", \"text-muted-foreground\")}>{children}</div> : null}\n </div>\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n {onDismiss ? (\n <IconButton icon=\"x\" label=\"Dismiss\" size=\"sm\" onClick={onDismiss} className=\"-my-1 -mr-1\" />\n ) : null}\n </div>\n );\n}\n\n/** A small label for a state or a count.\n * tone neutral · primary · success · warning · danger · info\n * variant label (default: small caps, wide tracking; primary is a\n * solid fill, every other tone a tint) · soft (the case as\n * written, normal tracking, EVERY tone a tint — primary\n * included: \"Ready\", \"Daily over\", \"Feature\") · outline (the\n * case as written, a tone-coloured hairline on no fill) ·\n * code (the mono face, the case as written, normal tracking\n * — a version tag \"v2.4.0\", a commit \"a3f8c2d\", a path)\n * hue 1..5 — a categorical colour, read from --color-chart-N,\n * for KINDS of things (a DNS type, a platform, a content\n * type). Tone is for status; hue is for category, and a\n * tone given with it wins. With every variant: soft is the\n * case as written on a tint of the hue with a hue dot and\n * foreground text; label (and code) is filled with the hue\n * and black or white text, whichever reads; outline is a\n * hue hairline with foreground text. The text is never the\n * hue itself: a chart colour is not made to be read as type\n * Case is a variant, not its own prop: uppercase needs wide tracking and\n * sentence case needs none, so the two travel together, and a `case`\n * prop would have to be ignored by code. A soft primary lives on `soft`\n * because `label` primary is a solid fill apps already rely on. The\n * outline's ring is inset, so every variant is the same size in a row. */\nconst BADGE_LOOK = {\n label: {\n neutral: \"bg-muted text-muted-foreground\",\n primary: \"bg-primary text-primary-foreground\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/15 text-warning\",\n danger: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n },\n soft: {\n neutral: \"bg-muted text-muted-foreground\",\n primary: \"bg-primary/15 text-primary\",\n success: \"bg-success/15 text-success\",\n warning: \"bg-warning/15 text-warning\",\n danger: \"bg-destructive/15 text-destructive\",\n info: \"bg-info/15 text-info\",\n },\n outline: {\n neutral: \"bg-transparent text-foreground ring-1 ring-inset ring-border\",\n primary: \"bg-transparent text-primary ring-1 ring-inset ring-primary/50\",\n success: \"bg-transparent text-success ring-1 ring-inset ring-success/50\",\n warning: \"bg-transparent text-warning ring-1 ring-inset ring-warning/50\",\n danger: \"bg-transparent text-destructive ring-1 ring-inset ring-destructive/50\",\n info: \"bg-transparent text-info ring-1 ring-inset ring-info/50\",\n },\n};\nBADGE_LOOK.code = BADGE_LOOK.label;\n\nconst BADGE_FACE = {\n label: \"font-medium uppercase tracking-wide\",\n soft: \"font-medium\",\n outline: \"font-medium\",\n code: \"font-mono font-normal\",\n};\n\n// Categorical hues, literal so Tailwind sees every class. A filled hue's\n// text is black or white, picked from the fill's relative luminance (the\n// WCAG formula, on the clipped sRGB channels): what CSS contrast-color()\n// does, which no token can. A chart colour sits mid-lightness (chart-2…5\n// are clamped to OKLCH L 0.55–0.8, often past sRGB), where the theme's\n// light text fails on a light theme and its dark text on a dark primary.\n// Black below Y 0.179, white above: the better of the two is always\n// 4.58:1 or more. The soft tint is light (8%) so foreground text keeps\n// its contrast.\nconst HUE_LOOK = {\n label: {\n 1: \"bg-chart-1 text-[color:color(from_var(--color-chart-1)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 2: \"bg-chart-2 text-[color:color(from_var(--color-chart-2)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 3: \"bg-chart-3 text-[color:color(from_var(--color-chart-3)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 4: \"bg-chart-4 text-[color:color(from_var(--color-chart-4)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n 5: \"bg-chart-5 text-[color:color(from_var(--color-chart-5)_srgb_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1)_clamp(0,(0.179_-_(0.2126*pow(clamp(0,r,1),2.2)_+_0.7152*pow(clamp(0,g,1),2.2)_+_0.0722*pow(clamp(0,b,1),2.2)))*999,1))]\",\n },\n soft: {\n 1: \"bg-chart-1/8 text-foreground\",\n 2: \"bg-chart-2/8 text-foreground\",\n 3: \"bg-chart-3/8 text-foreground\",\n 4: \"bg-chart-4/8 text-foreground\",\n 5: \"bg-chart-5/8 text-foreground\",\n },\n outline: {\n 1: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-1\",\n 2: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-2\",\n 3: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-3\",\n 4: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-4\",\n 5: \"bg-transparent text-foreground ring-1 ring-inset ring-chart-5\",\n },\n};\nHUE_LOOK.code = HUE_LOOK.label;\nconst HUE_DOT = { 1: \"bg-chart-1\", 2: \"bg-chart-2\", 3: \"bg-chart-3\", 4: \"bg-chart-4\", 5: \"bg-chart-5\" };\n\nexport function Badge({ tone, hue, variant = \"label\", className, children }) {\n const v = BADGE_FACE[variant] ? variant : \"label\";\n const h = tone == null && HUE_DOT[hue] ? hue : null;\n const look = h ? HUE_LOOK[v][h] : BADGE_LOOK[v][tone ?? \"neutral\"] ?? BADGE_LOOK[v].neutral;\n return (\n <span className={cx(\"inline-flex items-center rounded-sm px-1.5 py-0.5 text-2xs\", h && v === \"soft\" && \"gap-1\", BADGE_FACE[v], look, className)}>\n {h && v === \"soft\" ? <span aria-hidden=\"true\" className={cx(\"size-1.5 shrink-0 rounded-full\", HUE_DOT[h])} /> : null}\n {children}\n </span>\n );\n}\n\n/** The small uppercase line above a heading. */\nexport function Kicker({ className, children }) {\n return <p className={cx(\"text-2xs font-medium uppercase tracking-wider text-muted-foreground\", className)}>{children}</p>;\n}\n\n/** What a surface shows when a real collection is empty AND there is no\n * fixture to show (a filter with no matches, a search with no results).\n * Says what would appear here and offers the one way to make it. */\nexport function Empty({ icon = \"search\", title, body, action, className }) {\n return (\n <div className={cx(\"grid place-items-center gap-2 rounded-lg border border-dashed border-border px-6 py-10 text-center\", className)}>\n <Icon name={icon} size={22} className=\"text-muted-foreground\" />\n <p className=\"text-sm font-medium\">{title}</p>\n {body ? <p className=\"max-w-prose text-sm text-muted-foreground\">{body}</p> : null}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n );\n}\n"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"path": "templates/next/files/components/ui/pagination.jsx",
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
"path": "templates/next/files/components/ui/separator.jsx",
|
|
226
226
|
"type": "registry:ui",
|
|
227
227
|
"target": "@components/makersclaw/ui/separator.jsx",
|
|
228
|
-
"content": "import { cx } from \"./cx\";\n\n// Small
|
|
228
|
+
"content": "import { cx } from \"./cx\";\n\n// Small plain parts that shape a layout: a separator, a key cap, a box\n// that keeps its ratio. Server-safe (no hooks).\n\n/**\n * A hairline between groups. Decorative by default — most lines only\n * group what spacing already groups, and a screen reader should not\n * announce them. Pass `decorative={false}` when the line divides two\n * sections a reader should know about (role=separator).\n *\n * orientation horizontal (full width) · vertical (full height of a\n * flex row: toolbar groups, breadcrumb-like rows)\n * decorative true (hidden from readers) · false (a separator)\n * label horizontal only: a word on the line (\"or\")\n */\nexport function Separator({ orientation = \"horizontal\", decorative = true, label, className }) {\n const vertical = orientation === \"vertical\";\n const a11y = decorative\n ? { role: \"none\" }\n : { role: \"separator\", \"aria-orientation\": vertical ? \"vertical\" : undefined };\n if (vertical) {\n return <div {...a11y} className={cx(\"w-px shrink-0 self-stretch bg-border\", className)} />;\n }\n if (label) {\n return (\n <div {...a11y} className={cx(\"flex items-center gap-3 text-xs text-muted-foreground\", className)}>\n <span className=\"h-px flex-1 bg-border\" />\n <span>{label}</span>\n <span className=\"h-px flex-1 bg-border\" />\n </div>\n );\n }\n return <div {...a11y} className={cx(\"h-px w-full shrink-0 bg-border\", className)} />;\n}\n\n// Symbols a reader cannot say; each is shown to the eye and spelled out\n// to the ear.\nconst KEY_NAMES = {\n \"⌘\": \"Command\",\n \"⌥\": \"Option\",\n \"⇧\": \"Shift\",\n \"⌃\": \"Control\",\n \"↵\": \"Enter\",\n \"⏎\": \"Return\",\n \"⌫\": \"Backspace\",\n \"⌦\": \"Delete\",\n \"⎋\": \"Escape\",\n \"⇥\": \"Tab\",\n \"↑\": \"Up\",\n \"↓\": \"Down\",\n \"←\": \"Left\",\n \"→\": \"Right\",\n};\n\n/** One key, as printed on the keyboard: ⌘, Esc, K. A native <kbd>. */\nexport function Kbd({ className, children }) {\n const name = typeof children === \"string\" ? KEY_NAMES[children] : undefined;\n return (\n <kbd\n className={cx(\n \"inline-flex h-5 min-w-5 items-center justify-center rounded-sm border border-border bg-muted px-1\",\n \"font-sans text-2xs font-medium leading-none text-muted-foreground\",\n className,\n )}\n >\n {name ? (\n <>\n <span aria-hidden=\"true\">{children}</span>\n <span className=\"sr-only\">{name}</span>\n </>\n ) : (\n children\n )}\n </kbd>\n );\n}\n\n/** A combo pressed together (⌘ K). `keys` is [\"⌘\", \"K\"]; children work\n * too. Nested <kbd>s, which is how HTML spells a key combination. */\nexport function KbdGroup({ keys, className, children }) {\n return (\n <kbd className={cx(\"inline-flex items-center gap-1 font-sans\", className)}>\n {keys ? keys.map((k, i) => <Kbd key={i}>{k}</Kbd>) : children}\n </kbd>\n );\n}\n\nconst RATIOS = {\n square: \"aspect-square\",\n video: \"aspect-video\",\n portrait: \"aspect-3/4\",\n photo: \"aspect-4/3\",\n wide: \"aspect-21/9\",\n};\n\n/**\n * A box that keeps its shape as its width changes — a thumbnail, a video,\n * a map. Its one child fills it (an <img> is cropped to cover).\n *\n * ratio square · video (16:9) · portrait (3:4) · photo (4:3) · wide\n * (21:9), or a number (width / height, e.g. 2)\n */\nexport function AspectRatio({ ratio = \"video\", className, children }) {\n const named = typeof ratio === \"string\" ? RATIOS[ratio] : undefined;\n return (\n <div\n className={cx(\n \"relative w-full overflow-hidden\",\n named ?? (typeof ratio === \"number\" ? undefined : RATIOS.video),\n \"[&>*]:absolute [&>*]:inset-0 [&>*]:size-full [&>img]:object-cover\",\n className,\n )}\n style={typeof ratio === \"number\" && ratio > 0 ? { aspectRatio: ratio } : undefined}\n >\n {children}\n </div>\n );\n}\n"
|
|
229
229
|
},
|
|
230
230
|
{
|
|
231
231
|
"path": "templates/next/files/components/ui/sheet.jsx",
|
package/r/csv-import.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "CsvImport",
|
|
6
6
|
"description": "Import a CSV in three calm steps: drop a file (or paste text, or download a template), match each file column to a field with automatic matches and sample values (or, with no fields, name and type a new table's fields from guessed types), then review every row in an editable spreadsheet where cells that do not fit their type are marked red and fixed in place, delete rows or leave the bad ones out, and import through one async onImport that answers { ok }. Quoted commas and newlines, TSV and semicolon files, byte-order marks, 50,000 rows.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/spreadsheet.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [
|
|
12
12
|
"papaparse@5.7.0"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"path": "registry/blocks/csv-import/README.md",
|
|
35
35
|
"type": "registry:file",
|
|
36
36
|
"target": "@components/makersclaw/blocks/csv-import/README.md",
|
|
37
|
-
"content": "# CsvImport\n\nBring rows in from a CSV in three steps. **Upload**: drop a file or browse\n(or paste the text, or download a template of your fields to fill in).\n**Match columns**: each column in the file, with a few of its values,\nbeside the field it goes to — matched for you by name and common synonyms\n(\"E-mail address\" → Email, \"Organization\" → Company), changeable, or\n\"Don't import\"; required fields that are still unmatched are named.\n**Review**: every row in an editable spreadsheet. A cell that does not fit\nits field (\"last spring\" in a date, \"abc\" in an amount, a missing required\nvalue) keeps its text and gets a red corner; click it and type the right\nvalue, delete the row, or leave the rows with problems out. Then one\n**Import** — nothing is saved before it.\n\nWith no `fields`, it makes a new table instead: each column becomes a\nfield whose name you can change and whose type is guessed from its values\n(number, currency, date, checkbox, link, single or multiple select, text).\n\nThe CSV is read by [papaparse](https://www.papaparse.com) (MIT): commas,\ntabs, semicolons or pipes (guessed), quoted cells with commas and\nnewlines, a byte-order mark. Typed values come from the spreadsheet\nblock's column types, so \"$1,250\" is 1250 and \"11/02/2024\" is a day.\n\n```jsx\nimport { CsvImport } from \"../components/blocks/csv-import\"; // a Makersclaw app (\"./blocks/csv-import\" from components/)\nimport { CsvImport } from \"@/components/makersclaw/blocks/csv-import\"; // a shadcn project\n```\n\nIt is a client component. `makersclaw add csv-import` adds the\nspreadsheet block (it requires it) and `papaparse` (pinned, exact): run\n`npm install` after.\n\n## The data\n\n`fields` are the spreadsheet's columns — the same shape, the same types\n(its README, \"The data\"):\n\n```js\nconst FIELDS = [\n { key: \"name\", name: \"Name\", type: \"text\", required: true },\n { key: \"email\", name: \"Email\", type: \"text\", required: true },\n { key: \"stage\", name: \"Stage\", type: \"select\", options: [{ value: \"lead\", label: \"Lead\", hue: 2 }] },\n { key: \"mrr\", name: \"MRR\", type: \"currency\", precision: 0 },\n { key: \"owner\", name: \"Owner\", type: \"person\" }, // matched by name, first name, email or id in `people`\n { key: \"since\", name: \"Customer since\", type: \"date\" },\n];\n```\n\n`onImport` receives `{ rows, columns, skipped }`:\n\n- `rows` — typed values keyed by field (`{ id, name: \"Ada\", mrr: 1250,\n since: \"2025-03-14\", … }`). The `id` is a temporary one (\"import-3\");\n drop it and let the database make its own.\n- `columns` — the fields the rows use. Into existing fields, a select\n value that was not an option yet (\"Trial\") arrives as a new option on\n its field (`hue` set): save the options with the rows. As a new table,\n these are the new table's fields.\n- `skipped` — how many rows with problems were left out.\n\n## Props\n\n- `onImport({ rows, columns, skipped })` — required; async, answers\n `{ ok: true }` (optionally `imported`: the count you actually saved) or\n `{ ok: false, error }`. A failure keeps everything as it was and shows\n the error; the person can try again.\n- `fields` — the table to import into. Leave it out to make a new table.\n Read-only and computed fields are not offered.\n- `people` — `[{ id, name, email? }]`, for `person` fields.\n- `noun` — what a row is (`\"contact\"`): \"Import 8 contacts\". Default\n `\"row\"`.\n- `title` — the heading (default \"Import contacts from a CSV\");\n `description` — the line under it.\n- `maxRows` — the most rows one file may hold (`50000`).\n- `example` — `{ key: value }` for the template's example line (with\n `fields`, \"Download a template\" writes the field names and this line).\n `templateName` — the template's file name (`\"contacts-template.csv\"`).\n- `initialText` — CSV text to start from (skips the upload step);\n `initialName` — what to call it (`\"Pasted text\"`).\n- `onCancel` — shows Cancel on the first step.\n- `onDone` — the success screen's main button (`doneLabel`, default\n \"View contacts\"); without it only \"Import another file\" shows.\n- `locale` — for amounts and dates; `today` — `\"YYYY-MM-DD\"` for the date\n picker.\n\nThe helpers, for a flow of your own: `parseCsvText(text)` → `{ data,\ndelimiter, errors }` · `readCsvFile(file)` · `headersOf(data, hasHeader)` ·\n`autoMatch(headers, fields)` → a field key (or null) per column ·\n`guessColumns(headers, body)` → new fields with types and options ·\n`buildRows(body, mapping, fields, { ctx })` → `{ rows, problems,\ncreates, fields }` (each row's `__problems` is `{ key: message }`) ·\n`cleanRow(row)` · `templateCsv(fields, example)`.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useRouter } from \"next/navigation\";\nimport {
|
|
37
|
+
"content": "# CsvImport\n\nBring rows in from a CSV in three steps. **Upload**: drop a file or browse\n(or paste the text, or download a template of your fields to fill in).\n**Match columns**: each column in the file, with a few of its values,\nbeside the field it goes to — matched for you by name and common synonyms\n(\"E-mail address\" → Email, \"Organization\" → Company), changeable, or\n\"Don't import\"; required fields that are still unmatched are named.\n**Review**: every row in an editable spreadsheet. A cell that does not fit\nits field (\"last spring\" in a date, \"abc\" in an amount, a missing required\nvalue) keeps its text and gets a red corner; click it and type the right\nvalue, delete the row, or leave the rows with problems out. Then one\n**Import** — nothing is saved before it.\n\nWith no `fields`, it makes a new table instead: each column becomes a\nfield whose name you can change and whose type is guessed from its values\n(number, currency, date, checkbox, link, single or multiple select, text).\n\nThe CSV is read by [papaparse](https://www.papaparse.com) (MIT): commas,\ntabs, semicolons or pipes (guessed), quoted cells with commas and\nnewlines, a byte-order mark. Typed values come from the spreadsheet\nblock's column types, so \"$1,250\" is 1250 and \"11/02/2024\" is a day.\n\n```jsx\nimport { CsvImport } from \"../components/blocks/csv-import\"; // a Makersclaw app (\"./blocks/csv-import\" from components/)\nimport { CsvImport } from \"@/components/makersclaw/blocks/csv-import\"; // a shadcn project\n```\n\nIt is a client component. `makersclaw add csv-import` adds the\nspreadsheet block (it requires it) and `papaparse` (pinned, exact): run\n`npm install` after.\n\n## The data\n\n`fields` are the spreadsheet's columns — the same shape, the same types\n(its README, \"The data\"):\n\n```js\nconst FIELDS = [\n { key: \"name\", name: \"Name\", type: \"text\", required: true },\n { key: \"email\", name: \"Email\", type: \"text\", required: true },\n { key: \"stage\", name: \"Stage\", type: \"select\", options: [{ value: \"lead\", label: \"Lead\", hue: 2 }] },\n { key: \"mrr\", name: \"MRR\", type: \"currency\", precision: 0 },\n { key: \"owner\", name: \"Owner\", type: \"person\" }, // matched by name, first name, email or id in `people`\n { key: \"since\", name: \"Customer since\", type: \"date\" },\n];\n```\n\n`onImport` receives `{ rows, columns, skipped }`:\n\n- `rows` — typed values keyed by field (`{ id, name: \"Ada\", mrr: 1250,\n since: \"2025-03-14\", … }`). The `id` is a temporary one (\"import-3\");\n drop it and let the database make its own.\n- `columns` — the fields the rows use. Into existing fields, a select\n value that was not an option yet (\"Trial\") arrives as a new option on\n its field (`hue` set): save the options with the rows. As a new table,\n these are the new table's fields.\n- `skipped` — how many rows with problems were left out.\n\n## Props\n\n- `onImport({ rows, columns, skipped })` — required; async, answers\n `{ ok: true }` (optionally `imported`: the count you actually saved) or\n `{ ok: false, error }`. A failure keeps everything as it was and shows\n the error; the person can try again.\n- `fields` — the table to import into. Leave it out to make a new table.\n Read-only and computed fields are not offered.\n- `people` — `[{ id, name, email? }]`, for `person` fields.\n- `noun` — what a row is (`\"contact\"`): \"Import 8 contacts\". Default\n `\"row\"`.\n- `title` — the heading (default \"Import contacts from a CSV\");\n `description` — the line under it.\n- `maxRows` — the most rows one file may hold (`50000`).\n- `example` — `{ key: value }` for the template's example line (with\n `fields`, \"Download a template\" writes the field names and this line).\n `templateName` — the template's file name (`\"contacts-template.csv\"`).\n- `initialText` — CSV text to start from (skips the upload step);\n `initialName` — what to call it (`\"Pasted text\"`).\n- `onCancel` — shows Cancel on the first step.\n- `onDone` — the success screen's main button (`doneLabel`, default\n \"View contacts\"); without it only \"Import another file\" shows.\n- `locale` — for amounts and dates; `today` — `\"YYYY-MM-DD\"` for the date\n picker.\n\nThe helpers, for a flow of your own: `parseCsvText(text)` → `{ data,\ndelimiter, errors }` · `readCsvFile(file)` · `headersOf(data, hasHeader)` ·\n`autoMatch(headers, fields)` → a field key (or null) per column ·\n`guessColumns(headers, body)` → new fields with types and options ·\n`buildRows(body, mapping, fields, { ctx })` → `{ rows, problems,\ncreates, fields }` (each row's `__problems` is `{ key: message }`) ·\n`cleanRow(row)` · `templateCsv(fields, example)`.\n\n## Example\n\n```jsx\n// surfaces/import.jsx — client: contacts from a spreadsheet\n\"use client\";\nimport { useRouter } from \"next/navigation\";\nimport { useRows } from \"../lib/data\";\nimport { runAction } from \"../lib/actions\";\nimport { CsvImport } from \"../components/blocks/csv-import\";\n\nconst FIELDS = [\n { key: \"name\", name: \"Name\", type: \"text\", required: true },\n { key: \"email\", name: \"Email\", type: \"text\", required: true },\n { key: \"owner\", name: \"Owner\", type: \"person\" },\n];\n\nexport default function ImportContacts() {\n const router = useRouter();\n const { rows: team } = useRows({ source: \"people\", orderBy: \"name asc\" });\n return (\n <CsvImport\n fields={FIELDS}\n people={team}\n noun=\"contact\"\n onImport={async ({ rows }) => {\n // One action, one transaction: a write per row would stop halfway,\n // and the door takes 300 writes a minute.\n const r = await runAction({ name: \"import_contacts\", args: { rows: rows.map(({ id, ...values }) => values) } });\n return r.ok ? { ok: true, imported: r.result?.imported } : r;\n }}\n onDone={() => router.push(\"/s/contacts\")}\n />\n );\n}\n```\n\nThe action, named in `references/actions.md` and made with `sql`:\n\n```sql\ncreate or replace function import_contacts(args jsonb) returns jsonb\nlanguage sql as $$\n with made as (\n insert into contacts (name, email, owner)\n select name, email, owner from jsonb_populate_recordset(null::contacts, args->'rows')\n returning 1\n )\n select jsonb_build_object('imported', count(*)) from made\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- Excel files (.xlsx) are not read: save the sheet as CSV first (the\n upload step says so when one is dropped).\n- No merging with rows already in the table (update by email, skip\n duplicates): every imported row is new. Match on your side in\n `onImport` if you need it.\n- One file at a time; nothing is uploaded or kept — the rows go to\n `onImport` and nowhere else.\n- The review can fix cells and delete rows, not add rows or change the\n target fields (as a new table, it can rename and retype them).\n"
|
|
38
38
|
}
|
|
39
39
|
],
|
|
40
40
|
"categories": [
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"docs": "Installed to components/makersclaw/blocks/csv-import. Import: import { CsvImport, parseCsvText, readCsvFile, headersOf, autoMatch, guessColumns, buildRows, cleanRow, templateCsv } from \"@/components/makersclaw/blocks/csv-import\". Props and an example: components/makersclaw/blocks/csv-import/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
57
57
|
"meta": {
|
|
58
58
|
"makersclaw": {
|
|
59
|
-
"version": "0.
|
|
59
|
+
"version": "0.7.0",
|
|
60
60
|
"useWhen": "People bring records in from a spreadsheet or another tool: contacts, leads, products, a price list, an old CRM export, a first table for a new app. Built on spreadsheet. Not for uploading files to keep (file-upload) or for editing records already in the app (spreadsheet, data-table)."
|
|
61
61
|
}
|
|
62
62
|
}
|
package/r/data-table.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "Data table",
|
|
6
6
|
"description": "Records as a table the founder can work: sort by a column (shift for several), search every column, faceted filters with counts, column visibility, row selection with a bulk-actions bar, a menu per row or its first actions as buttons in the row (async, with a spinner and a failure notice), row click, pagination with a page size, loading, empty and no-matches states, and cards on a phone. Opt in to expandable rows, row groups with subtotals, an aligned totals row, arrow-key navigation and a sticky first column. Client-side over rows, or driven by a server.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"path": "registry/blocks/data-table/README.md",
|
|
32
32
|
"type": "registry:file",
|
|
33
33
|
"target": "@components/makersclaw/blocks/data-table/README.md",
|
|
34
|
-
"content": "# Data table\n\nRecords as a table the founder can work. Click a header to sort (shift-click\nto sort by more than one column), search every column, narrow by facets that\nshow how many rows each choice leaves, pick which columns show, select rows\nand act on them together from a bar that appears, act on one row from its\nmenu or from buttons in the row, open a row with a click, and move through\npages of a chosen size. Opt in to rows that open to show their details,\nrows filed under group headings with subtotals, an aligned totals row,\narrow keys over the rows, and a first column that stays put while a wide table scrolls sideways.\nLoading shows skeleton rows; \"no rows yet\" and \"nothing matches\" are two\ndifferent states, the second with a way out. It runs over `rows` in the\nbrowser, or — with `manual` — shows the page a server chose.\n\n```jsx\nimport { DataTable } from \"../components/blocks/data-table\"; // a Makersclaw app (\"./blocks/data-table\" from components/)\nimport { DataTable } from \"@/components/makersclaw/blocks/data-table\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the rows in the server surface (README §4).\n\n## The data\n\nAny array of objects with a unique id (`idColumn`, default `\"id\"`). Each\ncolumn reads one key of a row, or draws the cell itself with `cell(row)`.\n\n```js\nconst COLUMNS = [\n { key: \"number\", header: \"Invoice\", sortable: true, hideable: false },\n { key: \"customer\", header: \"Customer\", sortable: true, cell: (r) => <CustomerCell r={r} /> },\n { key: \"status\", header: \"Status\", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },\n { key: \"amount\", header: \"Amount\", sortable: true, align: \"end\", cell: (r) => money(r.amount) },\n { key: \"method\", header: \"Method\", hidden: true },\n];\n```\n\nA column:\n\n- `key` — the row key it reads (and its id for sort, filters, visibility).\n- `header` — what the header says; default made from the key.\n- `cell(row)` — what the cell draws; default the value as text. Kit parts\n and tokens only.\n- `sortable` — the header becomes a sort button.\n- `sortValue(row)` — what it sorts by, when the cell is not the value\n (a status in its own order, a date string, a nested field). Numbers sort\n as numbers, strings naturally (\"INV-9\" before \"INV-10\"); blanks sort\n last either way.\n- `searchValue(row)` — what the search matches; default `sortValue` or\n the value. `searchable: false` leaves the column out of the search.\n- `align` — `\"start\"` (default) or `\"end\"` (amounts: right-aligned,\n tabular figures). `numeric: true` means the same.\n- `width` — a CSS width for the column (`\"8rem\"`).\n- `hideable` — default true; false keeps it out of the Columns menu.\n- `hidden` — starts hidden (the Columns menu can show it).\n- `className` · `headerClassName` — layout on its cells or its header.\n Never colour.\n\nAny other key on a column is kept and ignored, so a block built on this\none can carry its own (report-table's `kind`).\n\n## Props\n\n- `rows` — the records. Pass `useFixture(rows, SAMPLE).rows` and the\n examples show, sort and filter like rows, but a bulk action never\n receives them (they are not rows): selecting only examples and pressing\n one says so and changes nothing.\n- `columns` — as above.\n- `idColumn` — default `\"id\"`.\n- `label` — what the table is called to a screen reader, default `\"Table\"`.\n- `noun` — what a row is (\"invoice\"): used in \"1–10 of 60 invoices\", the\n facet counts and the announcements. Default `\"row\"`.\n- `nounPlural` — the plural, when the regular ending is wrong for it:\n `noun=\"person\" nounPlural=\"people\"`. Default: `noun` with the regular\n English ending — \"invoice\" → \"invoices\", \"company\" → \"companies\",\n \"address\" → \"addresses\", \"batch\" → \"batches\", \"analysis\" → \"analyses\".\n\nSearch\n\n- `searchable` — default true: a search field with a glyph leads the\n toolbar. Every word must appear somewhere in the row.\n- `searchPlaceholder` — default `\"Search…\"`.\n- `search` · `defaultSearch` · `onSearchChange(text)` — the text.\n\nFacets\n\n- `facets` — `[{ key, label?, options?, value?(row) }]`: one multi-choice\n Combobox per facet, each option with the number of rows it would show\n at its right (counted over the search and the other facets). `options`\n `[{ value, label }]` fixes the choices and their order; without it they\n are the values found in `rows`. `value(row)` when the facet is not a\n plain key.\n- `filters` · `defaultFilters` · `onFiltersChange(filters)` — the chosen\n values, `{ status: [\"paid\", \"overdue\"], owner: [\"Maya\"] }`.\n\nSort\n\n- `sort` · `defaultSort` · `onSortChange(sort)` — `[{ key, dir: \"asc\" | \"desc\" }]`,\n the first entry sorts first. A press cycles a column asc → desc → off.\n- `multiSort` — default true: shift-click adds a column to the sort (each\n sorted header shows its place). False: one column at a time.\n\nSelection\n\n- `selectable` — checkboxes on every row and a select-this-page box in\n the header. On by default when `bulkActions`, `selection` or\n `onSelectionChange` is given.\n- `selection` · `defaultSelection` · `onSelectionChange(ids)` — the chosen\n ids, as strings. A selection survives search, filters and paging.\n- `bulkActions` — `[{ label, onSelect(rows, ids), icon?, tone?: \"danger\", disabled?, keepSelection? }]`,\n or `(selectedRows) => [...]`. With rows selected a bar shows the count,\n \"Select all N\" (when the whole page is chosen and more match), Clear,\n and these as buttons. `onSelect` may be async; return\n `{ ok: false, error }` (or throw) and the bar says why it failed and\n keeps the selection. Anything else clears the selection, unless\n `keepSelection: true` (an export).\n\nColumns\n\n- `hiddenColumns` · `defaultHiddenColumns` · `onHiddenColumnsChange(keys)` —\n the keys not shown. Default: the columns marked `hidden`. The Columns\n menu (checkbox items) sets it; the last column shown cannot be hidden.\n\nPages\n\n- `page` · `defaultPage` (1) · `onPageChange(page)` — 1-based. A new\n search, filter, sort or page size goes back to page 1; a page past the\n end (rows removed) goes to the last.\n- `pageSize` · `defaultPageSize` (10) · `onPageSizeChange(n)`, `pageSizes`\n (default `[10, 25, 50, 100]`) — the \"Rows per page\" choice.\n- `paginate` — default true; false shows every row and no pager.\n\nRows\n\n- `rowActions(row)` — the row's \"…\" menu, in kit `Menu` items\n (`[{ label, onSelect, icon?, tone?: \"danger\" }, { type: \"separator\" }, …]`;\n a falsy item is skipped). Nothing returned, no menu.\n- `inlineActions` — draw a row's first actions as buttons in the row\n instead of in the menu: a number (the first N of `rowActions(row)`) or\n `true` (all of them). An action with `inline: true` is always a button,\n one with `inline: false` never is; separators and labels stay in the\n menu, and the \"…\" menu holds whatever is left. A button with an `icon`\n is a square glyph button with a tooltip (on a phone card, the glyph and\n the words); without one it is a small text button (\"View\"). Its\n `onSelect(row)` may be async: the button spins and the row's other\n buttons wait, and `{ ok: false, error }` (or a throw) shows a danger\n notice above the table naming the action and the row. `tone: \"danger\"`\n and `disabled` work as in the menu. Default: none — every action in the\n menu, as before.\n- `onRowClick(row)` — a click on the row (not on its checkbox, menu or a\n link inside a cell), or Enter on a focused row: open your `Sheet`.\n- `rowTone(row)` — `\"success\" | \"info\" | \"warning\" | \"danger\" | \"highlighted\"`\n or nothing: a tint on the row's cells from the theme's status colours\n (hover deepens it). `\"highlighted\"` is a pinned or best row, not a\n status: a light primary tint and a bar at its leading edge. Status tones\n are for status only.\n- `rowClassName(row)` — more classes on the row (layout, weight). Never\n colour; that is `rowTone`.\n\nDetails (expandable rows)\n\n- `renderExpanded(row)` — what shows under a row when it is open. Adds a\n leading chevron column; the chevron turns when the row is open, and is\n a button with `aria-expanded`. A row with no `onRowClick` also opens on\n a click or Enter.\n- `canExpand(row)` — false leaves a row without a chevron. Default: every\n row can open.\n- `expanded` · `defaultExpanded` · `onExpandedChange(ids)` — the open ids,\n as strings.\n\nGroups\n\n- `groupBy` — a key or `(row) => value`: each group gets a heading row\n with its label and how many rows it holds. Sort first, then group: a\n sort orders rows inside their groups and never moves a group. Paging\n counts rows; a group split across pages repeats its heading.\n- `groupOrder` — the group values to put first, in order\n (`[\"overdue\", \"pending\"]`; the rest follow in the order they first\n appear in `rows`), or a compare function over two group values.\n Default: the order groups first appear in `rows`.\n- `groupLabel(value, rows)` — the heading's label. Default the value\n (`\"None\"` for a blank).\n- `groupSummary(rows)` — a node at the heading's right: a sum, a count.\n `rows` is the whole group, not only this page's part.\n- `groupFooter(rows)` — a subtotal row under the group, bold on a tinted\n background, on the page where the group ends. Return a map of column\n key to node (`{ amount: \"$4,200\" }`) for cells aligned under their\n columns (\"Subtotal\" fills the first column unless the map has it), or\n one node for a single right-aligned cell.\n\nTotals\n\n- `totals` — a map of column key to node, or `(rows) => map` over every\n row that matches the search and filters (all pages; in `manual`, the\n rows given): one row in the table's `<tfoot>`, each value under its\n column. `footer` is still the free node under the table.\n- `totalsLabel` — default `\"Total\"`, in the first column when the map\n has nothing for it; `null` for none.\n\nKeyboard\n\n- `keyboard` — arrow keys over the rows. One row is in the tab order at\n a time (the last one focused, else the first) and shows a focus ring.\n ↑ ↓ move, Home and End go to the first and last row on the page, Space\n selects the row (when `selectable`), Enter opens it as a click does,\n → and ← open and close its details, Esc clears the selection. Works on\n the phone cards too.\n\nLayout\n\n- `stickyFirstColumn` — the first shown column (and the chevron and\n checkbox before it) stays put while the table scrolls sideways, with an\n edge line. A wide table always scrolls inside its own box, never the\n page; details rows and group headings are drawn at the box's visible\n width.\n\nSlots and states\n\n- `toolbar` — your controls at the toolbar's end, before Columns: the one\n \"New invoice\" button, an export.\n- `footer` — a node under the table, above the pager: a total, a note.\n- `loading` — skeleton rows in place of the body (the toolbar stays).\n- `error` — a danger Notice above the table (a failed read).\n- `emptyTitle` · `emptyBody` · `emptyAction` · `emptyIcon` — the state for\n no rows at all. No matches has its own state with \"Clear filters\".\n- `mobile` — `\"scroll\"` (default): under 640px the table scrolls sideways\n inside its own box; the page never does. `\"cards\"`: under 640px each row\n is a card — the first shown column as its title, the others as\n label/value pairs, with its checkbox and menu. Pick cards when rows are\n read more than compared; scroll when columns are compared.\n- `cardTitle(row)` — the card's title instead of the first column (the\n card then lists every shown column).\n- `variant` — `\"outline\"` (a bordered card; default) or `\"plain\"` (no\n border, for a table already inside a `Card`).\n- `className` — layout around it (width, margin). Never colour.\n\n### Controlled or not — letting a server drive it\n\nEvery piece of state has three props: the value, a `default…` and an\n`on…Change`. Pass only the default (or nothing) and the table keeps it.\nPass the value and the table shows what you pass and calls `on…Change`\nwith what the founder asked for; you set it back. Each is independent, so\nyou can own just the page and leave the rest to the table.\n\nFor a table too big for the browser, set `manual`: the table stops\nsearching, filtering, sorting and slicing, `rows` is the current page, and\n`rowCount` is how many rows the server has. Own `search`, `filters`,\n`sort`, `page` and `pageSize`, re-read on each change, and give each facet\nits `options` with a `count` (`{ value, label, count }`), since the table\ncannot count rows it does not have. With `manual`, a bulk action's second\nargument, `ids`, is every selected id, including rows not on this page.\n\n```jsx\nconst [q, setQ] = useState({ search: \"\", filters: {}, sort: [], page: 1, pageSize: 25 });\n// …read rows + total for q on the server (a Server Action, a route)…\n<DataTable manual rows={pageRows} rowCount={total} loading={pending}\n search={q.search} onSearchChange={(search) => setQ((s) => ({ ...s, search }))}\n filters={q.filters} onFiltersChange={(filters) => setQ((s) => ({ ...s, filters }))}\n sort={q.sort} onSortChange={(sort) => setQ((s) => ({ ...s, sort }))}\n page={q.page} onPageChange={(page) => setQ((s) => ({ ...s, page }))}\n pageSize={q.pageSize} onPageSizeChange={(pageSize) => setQ((s) => ({ ...s, pageSize }))}\n columns={COLUMNS} facets={[{ key: \"status\", options: statusCounts }]} />\n```\n\nThe pure helpers the table uses are exported too — `processRows`,\n`searchRows`, `filterRows`, `sortRows`, `facetOptions`, `nextSort`,\n`groupRows`, `groupValueOf` and `useControllable` — so a surface can compute the same answer the table\nshows (a total over the filtered rows) without drawing it twice.\n\n## Example\n\n```jsx\n// components/Invoices.jsx — client: the table, the row sheet\n\"use client\";\nimport { useState } from \"react\";\nimport { updateRow } from \"../lib/actions\";\nimport { Badge, Button, Sheet } from \"./ui\";\nimport { FixtureNotice, useFixture } from \"./parts\";\nimport { DataTable } from \"./blocks/data-table\";\n\nconst TONE = { paid: \"success\", pending: \"info\", overdue: \"danger\", draft: \"neutral\" };\nconst COLUMNS = [\n { key: \"number\", header: \"Invoice\", sortable: true, hideable: false },\n { key: \"customer\", header: \"Customer\", sortable: true },\n { key: \"status\", header: \"Status\", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },\n { key: \"amount\", header: \"Amount\", sortable: true, align: \"end\", cell: (r) => `$${r.amount.toLocaleString()}` },\n { key: \"due\", header: \"Due\", sortable: true },\n];\nconst SAMPLE = [\n { number: \"INV-1001\", customer: \"Northwind\", status: \"paid\", amount: 1200, due: \"2026-10-01\" },\n { number: \"INV-1002\", customer: \"Acme Ltd\", status: \"overdue\", amount: 450, due: \"2026-09-01\" },\n];\n\nexport default function Invoices({ invoices }) {\n const fixture = useFixture(invoices, SAMPLE);\n const [open, setOpen] = useState(null);\n const markPaid = (rows) =>\n Promise.all(rows.map((r) => updateRow({ source: \"public.invoices\", id: r.id, patch: { status: \"paid\" } })))\n .then((all) => all.find((r) => r.ok === false) ?? { ok: true });\n return (\n <div className=\"grid gap-4\">\n {fixture.isFixture ? <FixtureNotice onClear={fixture.clear} /> : null}\n <DataTable\n label=\"Invoices\"\n noun=\"invoice\"\n rows={fixture.rows}\n columns={COLUMNS}\n facets={[{ key: \"status\", label: \"Status\" }]}\n defaultSort={[{ key: \"due\", dir: \"asc\" }]}\n bulkActions={[{ label: \"Mark paid\", icon: \"check\", onSelect: markPaid }]}\n rowActions={(r) => [{ label: \"Open\", icon: \"eye\", onSelect: () => setOpen(r) }]}\n onRowClick={setOpen}\n rowTone={(r) => (r.status === \"overdue\" ? \"warning\" : null)}\n toolbar={<Button variant=\"primary\" icon=\"plus\">New invoice</Button>}\n mobile=\"cards\"\n emptyTitle=\"No invoices yet\"\n />\n <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.number}>\n {/* the invoice's fields */}\n </Sheet>\n </div>\n );\n}\n```\n\n### Details, groups and totals\n\nA budget: line items under departments, sorted within them, a subtotal\nper department, a total, the months scrolling under the line item.\n\n```jsx\nconst MONTHS = [\"jan\", \"feb\", \"mar\"];\nconst money = (n) => `$${n.toLocaleString()}`;\nconst sumOf = (rows, key) => rows.reduce((s, r) => s + r[key], 0);\nconst sums = (rows) => Object.fromEntries([...MONTHS, \"total\"].map((k) => [k, money(sumOf(rows, k))]));\nconst COLUMNS = [\n { key: \"item\", header: \"Line item\", sortable: true },\n ...MONTHS.map((m) => ({ key: m, align: \"end\", sortable: true, cell: (r) => money(r[m]) })),\n { key: \"total\", align: \"end\", sortable: true, cell: (r) => money(r.total) },\n];\n\n<DataTable label=\"Budget\" noun=\"line item\" rows={lines} columns={COLUMNS}\n paginate={false} keyboard stickyFirstColumn\n groupBy=\"department\" groupOrder={[\"Engineering\"]}\n groupSummary={(rows) => `${money(sumOf(rows, \"total\"))} this quarter`}\n groupFooter={sums} totals={sums}\n renderExpanded={(r) => <p className=\"text-sm\">{r.note}</p>}\n canExpand={(r) => Boolean(r.note)}\n defaultSort={[{ key: \"total\", dir: \"desc\" }]} />\n```\n\n### Buttons in the row\n\nTasks whose actions depend on their state, the common ones as buttons in\nthe row and the rest in the menu:\n\n```jsx\n<DataTable label=\"Tasks\" noun=\"task\" rows={tasks} columns={COLUMNS}\n rowActions={(t) => [\n t.status === \"todo\" && { label: \"Start\", icon: \"play\", inline: true, onSelect: (r) => setStatus(r, \"doing\") },\n t.status === \"doing\" && { label: \"Complete\", icon: \"check\", inline: true, onSelect: (r) => setStatus(r, \"done\") },\n { label: \"Delete\", icon: \"trash\", tone: \"danger\", inline: true, onSelect: remove }, // async, may return { ok: false, error }\n { label: \"Copy ID\", icon: \"copy\", onSelect: copyId },\n ]} />\n```\n\n`setStatus` and `remove` return a promise of `{ ok }`, as a Server Action\ndoes; a failure is said above the table.\n\n## What it will not do\n\n- Edit a cell in place: open the row (`onRowClick`) in a `Sheet`.\n- Fetch: rows come in as props; with `manual` you read each page.\n- Collapse a group, pin more than the first column, resize or reorder\n columns, or virtualise thousands of rows — page them (`manual`)\n instead.\n- Colour a column: a cell says what it means with kit parts (`Badge`,\n `StatusDot`); a row's tone is `rowTone`, and only for status.\n\nA table it cannot be is your own component in `components/`, built from\nthe kit's `Table` parts — not an edit to this block. Its files are fixed:\n`npm run check` refuses a change, and `makersclaw add data-table` restores\nthem.\n\n## Credits\n\nThe row buttons and the orders example follow blocks.so\n(https://github.com/ephraimduncan/blocks, MIT) and Creative Tim UI\n(https://github.com/creativetimofficial/ui, MIT); this is a rewrite on the\nMakersclaw kit, not their code.\n"
|
|
34
|
+
"content": "# Data table\n\nRecords as a table the founder can work. Click a header to sort (shift-click\nto sort by more than one column), search every column, narrow by facets that\nshow how many rows each choice leaves, pick which columns show, select rows\nand act on them together from a bar that appears, act on one row from its\nmenu or from buttons in the row, open a row with a click, and move through\npages of a chosen size. Opt in to rows that open to show their details,\nrows filed under group headings with subtotals, an aligned totals row,\narrow keys over the rows, and a first column that stays put while a wide table scrolls sideways.\nLoading shows skeleton rows; \"no rows yet\" and \"nothing matches\" are two\ndifferent states, the second with a way out. It runs over `rows` in the\nbrowser, or — with `manual` — shows the page a server chose.\n\n```jsx\nimport { DataTable } from \"../components/blocks/data-table\"; // a Makersclaw app (\"./blocks/data-table\" from components/)\nimport { DataTable } from \"@/components/makersclaw/blocks/data-table\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the rows in the server surface (README §4).\n\n## The data\n\nAny array of objects with a unique id (`idColumn`, default `\"id\"`). Each\ncolumn reads one key of a row, or draws the cell itself with `cell(row)`.\n\n```js\nconst COLUMNS = [\n { key: \"number\", header: \"Invoice\", sortable: true, hideable: false },\n { key: \"customer\", header: \"Customer\", sortable: true, cell: (r) => <CustomerCell r={r} /> },\n { key: \"status\", header: \"Status\", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },\n { key: \"amount\", header: \"Amount\", sortable: true, align: \"end\", cell: (r) => money(r.amount) },\n { key: \"method\", header: \"Method\", hidden: true },\n];\n```\n\nA column:\n\n- `key` — the row key it reads (and its id for sort, filters, visibility).\n- `header` — what the header says; default made from the key.\n- `cell(row)` — what the cell draws; default the value as text. Kit parts\n and tokens only.\n- `sortable` — the header becomes a sort button.\n- `sortValue(row)` — what it sorts by, when the cell is not the value\n (a status in its own order, a date string, a nested field). Numbers sort\n as numbers, strings naturally (\"INV-9\" before \"INV-10\"); blanks sort\n last either way.\n- `searchValue(row)` — what the search matches; default `sortValue` or\n the value. `searchable: false` leaves the column out of the search.\n- `align` — `\"start\"` (default) or `\"end\"` (amounts: right-aligned,\n tabular figures). `numeric: true` means the same.\n- `width` — a CSS width for the column (`\"8rem\"`).\n- `hideable` — default true; false keeps it out of the Columns menu.\n- `hidden` — starts hidden (the Columns menu can show it).\n- `className` · `headerClassName` — layout on its cells or its header.\n Never colour.\n\nAny other key on a column is kept and ignored, so a block built on this\none can carry its own (report-table's `kind`).\n\n## Props\n\n- `rows` — the records. Pass `useFixture(rows, SAMPLE).rows` and the\n examples show, sort and filter like rows, but a bulk action never\n receives them (they are not rows): selecting only examples and pressing\n one says so and changes nothing.\n- `columns` — as above.\n- `idColumn` — default `\"id\"`.\n- `label` — what the table is called to a screen reader, default `\"Table\"`.\n- `noun` — what a row is (\"invoice\"): used in \"1–10 of 60 invoices\", the\n facet counts and the announcements. Default `\"row\"`.\n- `nounPlural` — the plural, when the regular ending is wrong for it:\n `noun=\"person\" nounPlural=\"people\"`. Default: `noun` with the regular\n English ending — \"invoice\" → \"invoices\", \"company\" → \"companies\",\n \"address\" → \"addresses\", \"batch\" → \"batches\", \"analysis\" → \"analyses\".\n\nSearch\n\n- `searchable` — default true: a search field with a glyph leads the\n toolbar. Every word must appear somewhere in the row.\n- `searchPlaceholder` — default `\"Search…\"`.\n- `search` · `defaultSearch` · `onSearchChange(text)` — the text.\n\nFacets\n\n- `facets` — `[{ key, label?, options?, value?(row) }]`: one multi-choice\n Combobox per facet, each option with the number of rows it would show\n at its right (counted over the search and the other facets). `options`\n `[{ value, label }]` fixes the choices and their order; without it they\n are the values found in `rows`. `value(row)` when the facet is not a\n plain key.\n- `filters` · `defaultFilters` · `onFiltersChange(filters)` — the chosen\n values, `{ status: [\"paid\", \"overdue\"], owner: [\"Maya\"] }`.\n\nSort\n\n- `sort` · `defaultSort` · `onSortChange(sort)` — `[{ key, dir: \"asc\" | \"desc\" }]`,\n the first entry sorts first. A press cycles a column asc → desc → off.\n- `multiSort` — default true: shift-click adds a column to the sort (each\n sorted header shows its place). False: one column at a time.\n\nSelection\n\n- `selectable` — checkboxes on every row and a select-this-page box in\n the header. On by default when `bulkActions`, `selection` or\n `onSelectionChange` is given.\n- `selection` · `defaultSelection` · `onSelectionChange(ids)` — the chosen\n ids, as strings. A selection survives search, filters and paging.\n- `bulkActions` — `[{ label, onSelect(rows, ids), icon?, tone?: \"danger\", disabled?, keepSelection? }]`,\n or `(selectedRows) => [...]`. With rows selected a bar shows the count,\n \"Select all N\" (when the whole page is chosen and more match), Clear,\n and these as buttons. `onSelect` may be async; return\n `{ ok: false, error }` (or throw) and the bar says why it failed and\n keeps the selection. Anything else clears the selection, unless\n `keepSelection: true` (an export).\n\nColumns\n\n- `hiddenColumns` · `defaultHiddenColumns` · `onHiddenColumnsChange(keys)` —\n the keys not shown. Default: the columns marked `hidden`. The Columns\n menu (checkbox items) sets it; the last column shown cannot be hidden.\n\nPages\n\n- `page` · `defaultPage` (1) · `onPageChange(page)` — 1-based. A new\n search, filter, sort or page size goes back to page 1; a page past the\n end (rows removed) goes to the last.\n- `pageSize` · `defaultPageSize` (10) · `onPageSizeChange(n)`, `pageSizes`\n (default `[10, 25, 50, 100]`) — the \"Rows per page\" choice.\n- `paginate` — default true; false shows every row and no pager.\n\nRows\n\n- `rowActions(row)` — the row's \"…\" menu, in kit `Menu` items\n (`[{ label, onSelect, icon?, tone?: \"danger\" }, { type: \"separator\" }, …]`;\n a falsy item is skipped). Nothing returned, no menu.\n- `inlineActions` — draw a row's first actions as buttons in the row\n instead of in the menu: a number (the first N of `rowActions(row)`) or\n `true` (all of them). An action with `inline: true` is always a button,\n one with `inline: false` never is; separators and labels stay in the\n menu, and the \"…\" menu holds whatever is left. A button with an `icon`\n is a square glyph button with a tooltip (on a phone card, the glyph and\n the words); without one it is a small text button (\"View\"). Its\n `onSelect(row)` may be async: the button spins and the row's other\n buttons wait, and `{ ok: false, error }` (or a throw) shows a danger\n notice above the table naming the action and the row. `tone: \"danger\"`\n and `disabled` work as in the menu. Default: none — every action in the\n menu, as before.\n- `onRowClick(row)` — a click on the row (not on its checkbox, menu or a\n link inside a cell), or Enter on a focused row: open your `Sheet`.\n- `rowTone(row)` — `\"success\" | \"info\" | \"warning\" | \"danger\" | \"highlighted\"`\n or nothing: a tint on the row's cells from the theme's status colours\n (hover deepens it). `\"highlighted\"` is a pinned or best row, not a\n status: a light primary tint and a bar at its leading edge. Status tones\n are for status only.\n- `rowClassName(row)` — more classes on the row (layout, weight). Never\n colour; that is `rowTone`.\n\nDetails (expandable rows)\n\n- `renderExpanded(row)` — what shows under a row when it is open. Adds a\n leading chevron column; the chevron turns when the row is open, and is\n a button with `aria-expanded`. A row with no `onRowClick` also opens on\n a click or Enter.\n- `canExpand(row)` — false leaves a row without a chevron. Default: every\n row can open.\n- `expanded` · `defaultExpanded` · `onExpandedChange(ids)` — the open ids,\n as strings.\n\nGroups\n\n- `groupBy` — a key or `(row) => value`: each group gets a heading row\n with its label and how many rows it holds. Sort first, then group: a\n sort orders rows inside their groups and never moves a group. Paging\n counts rows; a group split across pages repeats its heading.\n- `groupOrder` — the group values to put first, in order\n (`[\"overdue\", \"pending\"]`; the rest follow in the order they first\n appear in `rows`), or a compare function over two group values.\n Default: the order groups first appear in `rows`.\n- `groupLabel(value, rows)` — the heading's label. Default the value\n (`\"None\"` for a blank).\n- `groupSummary(rows)` — a node at the heading's right: a sum, a count.\n `rows` is the whole group, not only this page's part.\n- `groupFooter(rows)` — a subtotal row under the group, bold on a tinted\n background, on the page where the group ends. Return a map of column\n key to node (`{ amount: \"$4,200\" }`) for cells aligned under their\n columns (\"Subtotal\" fills the first column unless the map has it), or\n one node for a single right-aligned cell.\n\nTotals\n\n- `totals` — a map of column key to node, or `(rows) => map` over every\n row that matches the search and filters (all pages; in `manual`, the\n rows given): one row in the table's `<tfoot>`, each value under its\n column. `footer` is still the free node under the table.\n- `totalsLabel` — default `\"Total\"`, in the first column when the map\n has nothing for it; `null` for none.\n\nKeyboard\n\n- `keyboard` — arrow keys over the rows. One row is in the tab order at\n a time (the last one focused, else the first) and shows a focus ring.\n ↑ ↓ move, Home and End go to the first and last row on the page, Space\n selects the row (when `selectable`), Enter opens it as a click does,\n → and ← open and close its details, Esc clears the selection. Works on\n the phone cards too.\n\nLayout\n\n- `stickyFirstColumn` — the first shown column (and the chevron and\n checkbox before it) stays put while the table scrolls sideways, with an\n edge line. A wide table always scrolls inside its own box, never the\n page; details rows and group headings are drawn at the box's visible\n width.\n\nSlots and states\n\n- `toolbar` — your controls at the toolbar's end, before Columns: the one\n \"New invoice\" button, an export.\n- `footer` — a node under the table, above the pager: a total, a note.\n- `loading` — skeleton rows in place of the body (the toolbar stays).\n- `error` — a danger Notice above the table (a failed read).\n- `emptyTitle` · `emptyBody` · `emptyAction` · `emptyIcon` — the state for\n no rows at all. No matches has its own state with \"Clear filters\".\n- `mobile` — `\"scroll\"` (default): under 640px the table scrolls sideways\n inside its own box; the page never does. `\"cards\"`: under 640px each row\n is a card — the first shown column as its title, the others as\n label/value pairs, with its checkbox and menu. Pick cards when rows are\n read more than compared; scroll when columns are compared.\n- `cardTitle(row)` — the card's title instead of the first column (the\n card then lists every shown column).\n- `variant` — `\"outline\"` (a bordered card; default) or `\"plain\"` (no\n border, for a table already inside a `Card`).\n- `className` — layout around it (width, margin). Never colour.\n\n### Controlled or not — letting a server drive it\n\nEvery piece of state has three props: the value, a `default…` and an\n`on…Change`. Pass only the default (or nothing) and the table keeps it.\nPass the value and the table shows what you pass and calls `on…Change`\nwith what the founder asked for; you set it back. Each is independent, so\nyou can own just the page and leave the rest to the table.\n\nFor a table too big for the browser, set `manual`: the table stops\nsearching, filtering, sorting and slicing, `rows` is the current page, and\n`rowCount` is how many rows the server has. Own `search`, `filters`,\n`sort`, `page` and `pageSize`, re-read on each change, and give each facet\nits `options` with a `count` (`{ value, label, count }`), since the table\ncannot count rows it does not have. With `manual`, a bulk action's second\nargument, `ids`, is every selected id, including rows not on this page.\n\n```jsx\nconst [q, setQ] = useState({ search: \"\", filters: {}, sort: [], page: 1, pageSize: 25 });\n// One page through the door: the filter grammar, an order, limit and offset.\nconst filter = {\n ...Object.fromEntries(Object.entries(q.filters).filter(([, values]) => values.length)),\n customer: q.search ? { prefix: q.search } : undefined, // starts with, case ignored\n};\nconst page = useRows({ source: \"invoices\", filter, orderBy: q.sort.map((s) => `${s.key} ${s.dir}`).join(\", \") || undefined,\n limit: q.pageSize, offset: (q.page - 1) * q.pageSize });\nconst total = useCount({ source: \"invoices\", filter });\nconst { status, ...others } = filter; // a facet counts over the other filters\nconst byStatus = useCountBy({ source: \"invoices\", column: \"status\", filter: others });\nconst statusCounts = Object.entries(byStatus.counts).map(([value, count]) => ({ value, label: value, count }));\n\n<DataTable manual rows={page.rows} rowCount={total.count} loading={page.loading || total.loading}\n search={q.search} onSearchChange={(search) => setQ((s) => ({ ...s, search }))}\n filters={q.filters} onFiltersChange={(filters) => setQ((s) => ({ ...s, filters }))}\n sort={q.sort} onSortChange={(sort) => setQ((s) => ({ ...s, sort }))}\n page={q.page} onPageChange={(page) => setQ((s) => ({ ...s, page }))}\n pageSize={q.pageSize} onPageSizeChange={(pageSize) => setQ((s) => ({ ...s, pageSize }))}\n columns={COLUMNS} facets={[{ key: \"status\", options: statusCounts }]} />\n```\n\n`useRows`, `useCount` and `useCountBy` are `lib/data.js`'s in a browser\napp. `offset` goes up to 10,000; past it, page with `cursor` (the last\nanswer's `next`, given when `orderBy` names one column). A server app's\nown `loadRows` takes no `offset`: its `\"use client\"` component takes\nthese hooks from `lib/door.js`.\n\nThe pure helpers the table uses are exported too — `processRows`,\n`searchRows`, `filterRows`, `sortRows`, `facetOptions`, `nextSort`,\n`groupRows`, `groupValueOf` and `useControllable` — so a surface can compute the same answer the table\nshows (a total over the filtered rows) without drawing it twice.\n\n## Example\n\n```jsx\n// surfaces/invoices.jsx — client: read, hand over\n\"use client\";\nimport { useRows } from \"../lib/data\";\nimport Invoices from \"../components/Invoices\";\n\nexport default function InvoicesSurface() {\n const { rows, error, loading } = useRows({ source: \"invoices\", orderBy: \"due asc\" });\n if (loading) return null; // rows only once read: to useFixture, a read in flight looks like an empty table\n return <Invoices invoices={rows} error={error} />;\n}\n```\n\n```jsx\n// components/Invoices.jsx — client: the table, the row sheet\n\"use client\";\nimport { useState } from \"react\";\nimport { updateRow } from \"../lib/actions\";\nimport { Badge, Button, Notice, Sheet } from \"./ui\";\nimport { FixtureNotice, useFixture } from \"./parts\";\nimport { DataTable } from \"./blocks/data-table\";\n\nconst TONE = { paid: \"success\", pending: \"info\", overdue: \"danger\", draft: \"neutral\" };\nconst COLUMNS = [\n { key: \"number\", header: \"Invoice\", sortable: true, hideable: false },\n { key: \"customer\", header: \"Customer\", sortable: true },\n { key: \"status\", header: \"Status\", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },\n { key: \"amount\", header: \"Amount\", sortable: true, align: \"end\", cell: (r) => `$${Number(r.amount).toLocaleString()}` }, // numeric arrives as text\n { key: \"due\", header: \"Due\", sortable: true },\n];\nconst SAMPLE = [\n { number: \"INV-1001\", customer: \"Northwind\", status: \"paid\", amount: 1200, due: \"2026-10-01\" },\n { number: \"INV-1002\", customer: \"Acme Ltd\", status: \"overdue\", amount: 450, due: \"2026-09-01\" },\n];\n\nexport default function Invoices({ invoices, error }) {\n const fixture = useFixture(invoices, SAMPLE);\n const [open, setOpen] = useState(null);\n const markPaid = (rows) =>\n Promise.all(rows.map((r) => updateRow({ source: \"invoices\", id: r.id, patch: { status: \"paid\" } })))\n .then((all) => all.find((r) => r.ok === false) ?? { ok: true });\n return (\n <div className=\"grid gap-4\">\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n {fixture.isFixture ? <FixtureNotice onClear={fixture.clear} /> : null}\n <DataTable\n label=\"Invoices\"\n noun=\"invoice\"\n rows={fixture.rows}\n columns={COLUMNS}\n facets={[{ key: \"status\", label: \"Status\" }]}\n defaultSort={[{ key: \"due\", dir: \"asc\" }]}\n bulkActions={[{ label: \"Mark paid\", icon: \"check\", onSelect: markPaid }]}\n rowActions={(r) => [{ label: \"Open\", icon: \"eye\", onSelect: () => setOpen(r) }]}\n onRowClick={setOpen}\n rowTone={(r) => (r.status === \"overdue\" ? \"warning\" : null)}\n toolbar={<Button variant=\"primary\" icon=\"plus\">New invoice</Button>}\n mobile=\"cards\"\n emptyTitle=\"No invoices yet\"\n />\n <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.number}>\n {/* the invoice's fields */}\n </Sheet>\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### Details, groups and totals\n\nA budget: line items under departments, sorted within them, a subtotal\nper department, a total, the months scrolling under the line item.\n\n```jsx\nconst MONTHS = [\"jan\", \"feb\", \"mar\"];\nconst money = (n) => `$${n.toLocaleString()}`;\nconst sumOf = (rows, key) => rows.reduce((s, r) => s + r[key], 0);\nconst sums = (rows) => Object.fromEntries([...MONTHS, \"total\"].map((k) => [k, money(sumOf(rows, k))]));\nconst COLUMNS = [\n { key: \"item\", header: \"Line item\", sortable: true },\n ...MONTHS.map((m) => ({ key: m, align: \"end\", sortable: true, cell: (r) => money(r[m]) })),\n { key: \"total\", align: \"end\", sortable: true, cell: (r) => money(r.total) },\n];\n\n<DataTable label=\"Budget\" noun=\"line item\" rows={lines} columns={COLUMNS}\n paginate={false} keyboard stickyFirstColumn\n groupBy=\"department\" groupOrder={[\"Engineering\"]}\n groupSummary={(rows) => `${money(sumOf(rows, \"total\"))} this quarter`}\n groupFooter={sums} totals={sums}\n renderExpanded={(r) => <p className=\"text-sm\">{r.note}</p>}\n canExpand={(r) => Boolean(r.note)}\n defaultSort={[{ key: \"total\", dir: \"desc\" }]} />\n```\n\n### Buttons in the row\n\nTasks whose actions depend on their state, the common ones as buttons in\nthe row and the rest in the menu:\n\n```jsx\n<DataTable label=\"Tasks\" noun=\"task\" rows={tasks} columns={COLUMNS}\n rowActions={(t) => [\n t.status === \"todo\" && { label: \"Start\", icon: \"play\", inline: true, onSelect: (r) => setStatus(r, \"doing\") },\n t.status === \"doing\" && { label: \"Complete\", icon: \"check\", inline: true, onSelect: (r) => setStatus(r, \"done\") },\n { label: \"Delete\", icon: \"trash\", tone: \"danger\", inline: true, onSelect: remove }, // async, may return { ok: false, error }\n { label: \"Copy ID\", icon: \"copy\", onSelect: copyId },\n ]} />\n```\n\n`setStatus` and `remove` return a promise of `{ ok }`, as `updateRow` and\n`deleteRow` do; a failure is said above the table.\n\n## What it will not do\n\n- Edit a cell in place: open the row (`onRowClick`) in a `Sheet`.\n- Fetch: rows come in as props; with `manual` you read each page.\n- Collapse a group, pin more than the first column, resize or reorder\n columns, or virtualise thousands of rows — page them (`manual`)\n instead.\n- Colour a column: a cell says what it means with kit parts (`Badge`,\n `StatusDot`); a row's tone is `rowTone`, and only for status.\n\nA table it cannot be is your own component in `components/`, built from\nthe kit's `Table` parts — not an edit to this block. Its files are fixed:\n`npm run check` refuses a change, and `makersclaw add data-table` restores\nthem.\n\n## Credits\n\nThe row buttons and the orders example follow blocks.so\n(https://github.com/ephraimduncan/blocks, MIT) and Creative Tim UI\n(https://github.com/creativetimofficial/ui, MIT); this is a rewrite on the\nMakersclaw kit, not their code.\n"
|
|
35
35
|
}
|
|
36
36
|
],
|
|
37
37
|
"categories": [
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"docs": "Installed to components/makersclaw/blocks/data-table. Import: import { DataTable, useControllable, processRows, sortRows, searchRows, filterRows, facetOptions, nextSort, groupRows, groupValueOf } from \"@/components/makersclaw/blocks/data-table\". Props and an example: components/makersclaw/blocks/data-table/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
63
63
|
"meta": {
|
|
64
64
|
"makersclaw": {
|
|
65
|
-
"version": "0.
|
|
65
|
+
"version": "0.7.0",
|
|
66
66
|
"useWhen": "A list of records to scan, find, sort and act on: invoices, customers, orders, leads, users, tickets. Not for records that move through stages (a board), a few key numbers (stats), or a grid of cards whose picture matters more than its columns."
|
|
67
67
|
}
|
|
68
68
|
}
|