makersclaw 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +41 -1
- package/package.json +13 -7
- package/r/board.json +51 -0
- package/r/core.json +144 -0
- package/r/job-panel.json +51 -0
- package/r/registry.json +230 -0
- package/registry/blocks/board/README.md +9 -8
- package/registry/blocks/board/board.jsx +7 -8
- package/registry/blocks/job-panel/README.md +6 -2
- package/src/shadcn.mjs +163 -0
- package/templates/next/files/check/run.mjs +1 -1
- package/templates/next/files/components/parts/fixture.jsx +1 -1
- package/templates/next/files/components/parts/sample.js +15 -0
- package/templates/next/files/lib/sample.js +4 -12
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cogaid Solutions Private Limited
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -13,6 +13,37 @@ router, JSX, server actions), Postgres, Tailwind v4 mapped to the
|
|
|
13
13
|
workspace's theme, the data layer and write actions, the `_claw` platform
|
|
14
14
|
client, and a kit of parts. The app designs its own screens on top.
|
|
15
15
|
|
|
16
|
+
## In a shadcn project
|
|
17
|
+
|
|
18
|
+
The kit and the free blocks are also a shadcn registry. In any Next.js
|
|
19
|
+
15+ project with Tailwind v4 and shadcn set up:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npx shadcn@latest add https://cdn.jsdelivr.net/npm/makersclaw@0.2/r/board.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The first block brings the kit with it (`core`: into
|
|
26
|
+
`components/makersclaw/ui` and `components/makersclaw/parts`, plus the
|
|
27
|
+
success, warning and info colours your theme may not have). Blocks land
|
|
28
|
+
in `components/makersclaw/blocks/<name>/`, with their README:
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
import { Board } from "@/components/makersclaw/blocks/board";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or name the registry once in `components.json` and add by name:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{ "registries": { "@makersclaw": "https://cdn.jsdelivr.net/npm/makersclaw@0.2/r/{name}.json" } }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx shadcn@latest add @makersclaw/board
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Makersclaw builds whole apps from these blocks, with an agent that runs
|
|
45
|
+
them: [makersclaw.com](https://makersclaw.com).
|
|
46
|
+
|
|
16
47
|
## Components come with the template; blocks come when a surface needs one
|
|
17
48
|
|
|
18
49
|
- **Components** — the kit (`components/ui`: Button, Field, Select, Sheet,
|
|
@@ -58,7 +89,8 @@ against the old one may need rebuilding.
|
|
|
58
89
|
templates/next/template.json which files are fixed, docs, starters or merged; the kit's entries
|
|
59
90
|
templates/next/files/ the app — what `init` lays
|
|
60
91
|
registry/blocks/<name>/ block.json, the files `add` lays, README.md (what view/add print), demo.jsx
|
|
61
|
-
src/ the CLI (no dependencies)
|
|
92
|
+
src/ the CLI (no dependencies); src/shadcn.mjs builds the shadcn registry
|
|
93
|
+
r/ built at `npm pack` (never committed): the registry jsDelivr serves
|
|
62
94
|
test/ npm test · npm run check:regress · npm run test:app
|
|
63
95
|
```
|
|
64
96
|
|
|
@@ -66,3 +98,11 @@ test/ npm test · npm run check:regress · npm run test
|
|
|
66
98
|
- `npm run check:regress` — the template's `npm run check`, case by case.
|
|
67
99
|
- `npm run test:app` — a real app: init, every block added with its demo as
|
|
68
100
|
a surface, `npm install`, `npm run check` (build, start, every surface).
|
|
101
|
+
- `npm run build:registry` — `r/`, the shadcn registry (`--base <url>
|
|
102
|
+
--out <dir>` to serve it locally for a test).
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT (see LICENSE) — the template, the kit and the free blocks. Premium
|
|
107
|
+
blocks, when they come, are served by Makersclaw's registry under their own
|
|
108
|
+
license and never ship in this package.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makersclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The Makersclaw app template, its kit and its blocks: init an app, add the blocks a surface needs, search the registry.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"src",
|
|
12
12
|
"templates",
|
|
13
13
|
"registry",
|
|
14
|
+
"r",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
|
@@ -19,16 +20,21 @@
|
|
|
19
20
|
"scripts": {
|
|
20
21
|
"test": "node --test test/*.test.mjs",
|
|
21
22
|
"test:app": "node test/smoke-app.mjs",
|
|
22
|
-
"check:regress": "bash test/check-regress.sh"
|
|
23
|
+
"check:regress": "bash test/check-regress.sh",
|
|
24
|
+
"build:registry": "node scripts/build-registry.mjs",
|
|
25
|
+
"prepack": "node scripts/build-registry.mjs"
|
|
23
26
|
},
|
|
24
27
|
"keywords": [
|
|
25
|
-
"makersclaw",
|
|
26
|
-
"template",
|
|
27
|
-
"nextjs",
|
|
28
28
|
"blocks",
|
|
29
|
+
"cli",
|
|
29
30
|
"components",
|
|
30
|
-
"
|
|
31
|
+
"kanban",
|
|
32
|
+
"makersclaw",
|
|
33
|
+
"nextjs",
|
|
34
|
+
"registry",
|
|
35
|
+
"shadcn",
|
|
36
|
+
"template"
|
|
31
37
|
],
|
|
32
38
|
"homepage": "https://makersclaw.com",
|
|
33
|
-
"license": "
|
|
39
|
+
"license": "MIT"
|
|
34
40
|
}
|
package/r/board.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "board",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Board",
|
|
6
|
+
"description": "Records as cards in columns, one per stage; drag a card to another column (mouse, touch, or the card's menu from the keyboard) and the stage is saved.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.2.0/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/board/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/board/index.js",
|
|
16
|
+
"content": "// The Board block — `makersclaw add board`. Fixed: compose it through its\n// props (README.md beside this file); a board it cannot be is your own\n// component in components/.\nexport { Board } from \"./board\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/board/board.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/board/board.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { isSample } from \"../../parts\";\nimport { Button, Icon, Input, Menu, Notice, cx } from \"../../ui\";\n\n// Board — records as cards in columns, one column per stage, moved by\n// dragging or from the card's menu. The block owns the behaviour a board\n// gets wrong when it is drawn by hand: pointer dragging that works with a\n// mouse and on a phone, a keyboard path (the menu), the move shown at\n// once and put back with a reason when the write fails, examples that\n// move on screen and save nothing, the count in every header, and cards\n// no stage claims kept in view instead of lost. The surface owns what a\n// card says under its title (renderDetails) and how a move is saved\n// (onMove): the block imports nothing but React, the kit and the parts,\n// so it runs in a Makersclaw app and in any shadcn project alike.\n//\n// <Board rows={deals} groupBy=\"stage\" columns={STAGES}\n// onMove={(d, to) => moveRow({ source: \"public.deals\", id: d.id, stateColumn: \"stage\", to })}\n// titleOf={(d) => d.name} renderDetails={(d) => <DealFacts deal={d} />}\n// onOpen={setEditing} />\n//\n// README.md beside this file lists every prop.\n\nconst OTHER = \"__other\";\nconst DRAG_START_PX = { mouse: 5, pen: 6, touch: 8 };\n\nfunction humanize(key) {\n const text = String(key ?? \"\").replace(/[_-]+/g, \" \").trim();\n return text ? text.charAt(0).toUpperCase() + text.slice(1) : \"—\";\n}\n\nfunction normalizeColumns(columns) {\n return (columns ?? []).map((c) =>\n typeof c === \"string\" || typeof c === \"number\"\n ? { key: String(c), label: humanize(c) }\n : { ...c, key: String(c.key), label: c.label ?? humanize(c.key) },\n );\n}\n\nexport function Board({\n rows,\n groupBy,\n columns,\n idColumn = \"id\",\n onMove,\n renderDetails,\n titleOf = (row) => row.title ?? row.name ?? String(row[idColumn] ?? \"\"),\n metaOf,\n onOpen,\n actionsOf,\n onCreate,\n addLabel = \"Add\",\n createPlaceholder = \"Title, then Enter\",\n limits,\n label = \"Board\",\n emptyLabel = \"Nothing here yet\",\n otherLabel = \"No stage\",\n className,\n}) {\n const uid = useId();\n const boardRef = useRef(null);\n const dragRef = useRef(null);\n const suppressClick = useRef(false);\n const [overrides, setOverrides] = useState(() => new Map());\n const [drag, setDrag] = useState(null);\n const [error, setError] = useState(null);\n const [announce, setAnnounce] = useState(\"\");\n const [adding, setAdding] = useState(null);\n const [draft, setDraft] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [portal, setPortal] = useState(null);\n\n const movable = typeof onMove === \"function\";\n const list = rows ?? [];\n const keyOf = (row) => String(row?.[idColumn]);\n const cols = useMemo(() => normalizeColumns(columns), [columns]);\n const known = useMemo(() => new Set(cols.map((c) => c.key)), [cols]);\n const columnOf = (row) => {\n const moved = overrides.get(keyOf(row));\n const value = moved ?? (row?.[groupBy] == null ? \"\" : String(row[groupBy]));\n return known.has(value) ? value : OTHER;\n };\n\n useEffect(() => setPortal(document.body), []);\n\n // A move stays on screen until the rows the page re-reads carry it; then\n // the override has nothing left to say.\n useEffect(() => {\n setOverrides((current) => {\n if (current.size === 0) return current;\n const next = new Map(current);\n for (const [id, to] of current) {\n const row = list.find((r) => keyOf(r) === id);\n if (!row || String(row[groupBy]) === to) next.delete(id);\n }\n return next.size === current.size ? current : next;\n });\n // `list` changes identity with every read; that is the point.\n }, [rows, groupBy]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const byColumn = useMemo(() => {\n const out = new Map(cols.map((c) => [c.key, []]));\n out.set(OTHER, []);\n for (const row of list) out.get(columnOf(row)).push(row);\n return out;\n }, [list, cols, overrides]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const shown = byColumn.get(OTHER).length ? [...cols, { key: OTHER, label: otherLabel, other: true }] : cols;\n const labelOf = (key) => shown.find((c) => c.key === key)?.label ?? humanize(key);\n\n async function move(row, to) {\n const from = columnOf(row);\n if (!movable || to === from || to === OTHER || !known.has(to)) return;\n const id = keyOf(row);\n const title = titleOf(row);\n setError(null);\n setOverrides((m) => new Map(m).set(id, to));\n setAnnounce(`${title} moved to ${labelOf(to)}`);\n // An example moves on screen and saves nothing: it is not a row.\n if (isSample(row)) return;\n let result;\n try {\n result = await onMove(row, to, from === OTHER ? null : from);\n } catch (e) {\n result = { ok: false, error: e?.message ?? String(e) };\n }\n if (result && result.ok === false) {\n setOverrides((m) => {\n const next = new Map(m);\n next.delete(id);\n return next;\n });\n setError(`“${title}” is back in ${labelOf(from)}: ${result.error ?? \"the move was not saved\"}`);\n setAnnounce(`${title} could not be moved`);\n }\n }\n\n // ---- pointer dragging ---------------------------------------------------\n // A mouse drags a card from anywhere on it; touch and pen drag from the\n // grip, so the column still scrolls under a finger.\n\n function onPointerDown(e, row) {\n if (!movable) return;\n if (e.pointerType === \"mouse\" && e.button !== 0) return;\n const onGrip = Boolean(e.target.closest?.(\"[data-board-grip]\"));\n if (e.pointerType !== \"mouse\" && !onGrip) return;\n if (!onGrip && e.target.closest?.(\"input, textarea, select, a, [aria-haspopup], [role=menuitem]\")) return;\n // No text selection while a mouse drags; the click still lands.\n if (e.pointerType === \"mouse\") e.preventDefault();\n const card = e.currentTarget;\n const rect = card.getBoundingClientRect();\n dragRef.current = {\n row,\n id: keyOf(row),\n from: columnOf(row),\n pointerId: e.pointerId,\n type: e.pointerType,\n card,\n startX: e.clientX,\n startY: e.clientY,\n offsetX: e.clientX - rect.left,\n offsetY: e.clientY - rect.top,\n width: rect.width,\n active: false,\n over: null,\n x: e.clientX,\n y: e.clientY,\n };\n }\n\n function columnAt(x, y) {\n const el = document.elementFromPoint(x, y);\n const col = el?.closest?.(\"[data-board-column]\");\n return col && boardRef.current?.contains(col) ? col.getAttribute(\"data-board-column\") : null;\n }\n\n function onPointerMove(e) {\n const d = dragRef.current;\n if (!d || e.pointerId !== d.pointerId) return;\n if (!d.active) {\n const far = Math.hypot(e.clientX - d.startX, e.clientY - d.startY) >= (DRAG_START_PX[d.type] ?? 5);\n if (!far) return;\n d.active = true;\n try {\n d.card.setPointerCapture(e.pointerId);\n } catch {\n // the pointer is already gone; the move below still works\n }\n }\n e.preventDefault();\n d.x = e.clientX;\n d.y = e.clientY;\n d.over = columnAt(e.clientX, e.clientY);\n const board = boardRef.current;\n if (board) {\n const r = board.getBoundingClientRect();\n if (e.clientX < r.left + 48) board.scrollBy({ left: -18 });\n else if (e.clientX > r.right - 48) board.scrollBy({ left: 18 });\n }\n setDrag({ id: d.id, over: d.over, x: d.x, y: d.y, offsetX: d.offsetX, offsetY: d.offsetY, width: d.width, row: d.row });\n }\n\n function finish(e, drop) {\n const d = dragRef.current;\n if (!d || (e && e.pointerId !== d.pointerId)) return;\n dragRef.current = null;\n if (!d.active) return;\n suppressClick.current = true;\n setTimeout(() => {\n suppressClick.current = false;\n }, 0);\n setDrag(null);\n if (drop && d.over) move(d.row, d.over);\n }\n\n useEffect(() => {\n if (!drag) return undefined;\n const onKey = (e) => {\n if (e.key === \"Escape\") finish(null, false);\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [drag]); // eslint-disable-line react-hooks/exhaustive-deps\n\n async function create(e, column) {\n e.preventDefault();\n const title = draft.trim();\n if (!title || busy) return;\n setBusy(true);\n setError(null);\n let result;\n try {\n result = await onCreate(title, column);\n } catch (err) {\n result = { ok: false, error: err?.message ?? String(err) };\n }\n setBusy(false);\n if (result && result.ok === false) setError(`“${title}” was not added: ${result.error ?? \"the write failed\"}`);\n else {\n setDraft(\"\");\n setAnnounce(`${title} added to ${labelOf(column)}`);\n }\n }\n\n function card(row, { ghost = false } = {}) {\n const title = titleOf(row);\n const body = (\n <>\n {onOpen ? (\n <button\n type=\"button\"\n onClick={() => onOpen(row)}\n className=\"block w-full rounded-sm text-left text-sm font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {title}\n </button>\n ) : (\n <p className=\"text-sm font-medium\">{title}</p>\n )}\n {renderDetails ? (\n <div className=\"mt-2\">{renderDetails(row, { column: columnOf(row) })}</div>\n ) : metaOf ? (\n <div className=\"mt-1 text-xs text-muted-foreground\">{metaOf(row)}</div>\n ) : null}\n </>\n );\n const moveTo = movable\n ? shown\n .filter((c) => !c.other && c.key !== columnOf(row))\n .map((c) => ({ label: `Move to ${c.label}`, icon: \"arrow-right\", onSelect: () => move(row, c.key) }))\n : [];\n const items = [...(actionsOf?.(row) ?? []), ...moveTo];\n return (\n <div className=\"flex items-start gap-1\">\n {movable && !ghost ? (\n <span\n data-board-grip\n aria-hidden=\"true\"\n className=\"-ml-1 mt-0.5 hidden h-8 w-6 shrink-0 cursor-grab items-center justify-center text-muted-foreground [@media(hover:none)]:flex\"\n style={{ touchAction: \"none\" }}\n >\n <Icon name=\"grip\" size={16} />\n </span>\n ) : null}\n <div className=\"min-w-0 flex-1\">{body}</div>\n {!ghost ? <Menu items={items} label={`Actions for ${title}`} className=\"-mr-1 -mt-1 shrink-0\" /> : null}\n </div>\n );\n }\n\n return (\n <div className={cx(\"grid gap-3\", className)}>\n {error ? (\n <Notice tone=\"danger\" onDismiss={() => setError(null)}>\n {error}\n </Notice>\n ) : null}\n <div\n ref={boardRef}\n role=\"region\"\n aria-label={label}\n tabIndex={0}\n className=\"flex snap-x snap-mandatory gap-3 overflow-x-auto rounded-lg pb-2 outline-none focus-visible:ring-2 focus-visible:ring-ring sm:snap-none\"\n >\n {shown.map((col) => {\n const cards = byColumn.get(col.key) ?? [];\n const limit = limits?.[col.key];\n const over = typeof limit === \"number\" && cards.length > limit;\n const target = drag && drag.over === col.key && !col.other && columnOf(drag.row) !== col.key;\n const headingId = `${uid}-${col.key}`;\n return (\n <section\n key={col.key}\n data-board-column={col.other ? undefined : col.key}\n aria-labelledby={headingId}\n className={cx(\n \"flex max-h-[75dvh] w-[min(18rem,85vw)] shrink-0 snap-start flex-col rounded-lg border border-border bg-muted/50\",\n target && \"bg-accent ring-2 ring-ring\",\n )}\n >\n <header className=\"flex min-h-11 items-center justify-between gap-2 px-3\">\n <h3 id={headingId} className=\"truncate text-sm font-semibold\">\n {col.label}\n </h3>\n <span\n data-nums\n className={cx(\"shrink-0 font-mono text-xs\", over ? \"font-semibold text-warning\" : \"text-muted-foreground\")}\n title={typeof limit === \"number\" ? `${cards.length} of a limit of ${limit}` : undefined}\n >\n {typeof limit === \"number\" ? `${cards.length}/${limit}` : cards.length}\n </span>\n </header>\n <ul className=\"flex min-h-16 flex-1 flex-col gap-2 overflow-y-auto px-2 pb-2\">\n {cards.map((row) => {\n const id = keyOf(row);\n const lifted = drag?.id === id;\n return (\n <li\n key={id}\n data-board-card={id}\n onPointerDown={(e) => onPointerDown(e, row)}\n onPointerMove={onPointerMove}\n onPointerUp={(e) => finish(e, true)}\n onPointerCancel={(e) => finish(e, false)}\n onClickCapture={(e) => {\n if (suppressClick.current) {\n e.preventDefault();\n e.stopPropagation();\n }\n }}\n className={cx(\n \"rounded-md border border-border bg-card p-3 text-card-foreground shadow-sm\",\n movable && \"sm:cursor-grab\",\n lifted && \"opacity-40\",\n )}\n >\n {card(row)}\n </li>\n );\n })}\n {cards.length === 0 ? (\n <li className=\"rounded-md border border-dashed border-border p-3 text-xs text-muted-foreground\">{emptyLabel}</li>\n ) : null}\n </ul>\n {onCreate && !col.other ? (\n adding === col.key ? (\n <form onSubmit={(e) => create(e, col.key)} className=\"px-2 pb-2\">\n <label htmlFor={`${headingId}-new`} className=\"sr-only\">\n New in {col.label}\n </label>\n <Input\n id={`${headingId}-new`}\n autoFocus\n value={draft}\n disabled={busy}\n placeholder={createPlaceholder}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e) => {\n // Enter adds here, not through the form's implicit\n // submit: a synthetic Enter (an agent testing the\n // app) has no keypress to submit with. Never while\n // an input method is composing a word.\n if (e.key === \"Enter\" && !e.nativeEvent.isComposing) create(e, col.key);\n else if (e.key === \"Escape\") {\n setAdding(null);\n setDraft(\"\");\n }\n }}\n onBlur={() => {\n if (!draft.trim()) setAdding(null);\n }}\n />\n </form>\n ) : (\n <div className=\"px-2 pb-2\">\n <Button\n variant=\"quiet\"\n size=\"sm\"\n icon=\"plus\"\n className=\"w-full justify-start\"\n onClick={() => {\n setAdding(col.key);\n setDraft(\"\");\n }}\n >\n {addLabel}\n </Button>\n </div>\n )\n ) : null}\n </section>\n );\n })}\n </div>\n <p aria-live=\"polite\" className=\"sr-only\">\n {announce}\n </p>\n {drag && portal\n ? createPortal(\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none fixed z-50 rotate-2 rounded-md border border-border bg-card p-3 text-card-foreground shadow-lg\"\n style={{ left: drag.x - drag.offsetX, top: drag.y - drag.offsetY, width: drag.width }}\n >\n {card(drag.row, { ghost: true })}\n </div>,\n portal,\n )\n : null}\n </div>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/board/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/board/README.md",
|
|
28
|
+
"content": "# Board\n\nRecords as cards in columns, one column per stage. Drag a card to another\ncolumn — with a mouse from anywhere on the card, on a phone from its grip,\nfrom the keyboard through the card's menu (\"Move to …\") — and the stage is\nsaved. The move shows at once; when the write fails the card goes back and\nthe board says why.\n\n```jsx\nimport { Board } from \"../components/blocks/board\"; // a Makersclaw app (\"./blocks/board\" from components/)\nimport { Board } from \"@/components/makersclaw/blocks/board\"; // 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\nOne table, one column that holds the stage (`groupBy`). Each row is a card.\n`columns` is the stages in order — the values that column holds. A row\nwhose stage is not in `columns` lands in a last \"No stage\" column, so no\nrecord goes missing; it can be dragged out of it, not into it.\n\n## Props\n\n- `rows` — the records. Pass `useFixture(rows, SAMPLE).rows` and the\n example cards move on screen without saving (they are not rows).\n- `groupBy` — the column that holds the stage, e.g. `\"stage\"`.\n- `columns` — the stages in order: `[\"lead\", \"call\", \"won\"]` (labels are\n made from the keys) or `[{ key: \"lead\", label: \"New lead\" }, …]`.\n- `onMove(row, to, from)` — save the move; return `{ ok: true }` or\n `{ ok: false, error }` (a throw counts as failed). In a Makersclaw app it\n is one line over `moveRow` (lib/actions.js — the activity row included):\n `onMove={(d, to) => moveRow({ source: \"public.deals\", id: d.id, stateColumn: \"stage\", to })}`.\n Without `onMove` the board only shows; nothing can be moved.\n- `idColumn` — default `\"id\"`.\n- `titleOf(row)` — the card's title, default `row.title ?? row.name`. It\n also names the card in its menu, in announcements and in errors.\n- `onOpen(row)` — the title becomes a button: open your `Sheet` to edit\n the record. The block draws that button; you never style one.\n- `renderDetails(row, { column })` — what the card says under its title:\n the part of the board that is yours. Kit parts and tokens only.\n- `metaOf(row)` — a plain muted line under the title, when that is all\n the card needs (ignored when `renderDetails` is set).\n- `actionsOf(row)` — more menu items for a card, kit `Menu` shape:\n `[{ label, onSelect, icon?, tone?: \"danger\" }]`. The \"Move to\" items\n follow them.\n- `onCreate(title, column)` — an \"Add\" at every column's foot opens an\n inline field; Enter calls this (insert the row with `insertRow`), the\n field stays open for the next one. Return `{ ok: false, error }` to show\n why it failed.\n- `addLabel` · `createPlaceholder` · `emptyLabel` · `otherLabel` — words.\n- `limits` — a work-in-progress limit per stage: `{ doing: 3 }` shows\n `4/3` in the header, in the warning tone, when it is exceeded.\n- `label` — what the board is called to a screen reader.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// surfaces/pipeline.jsx — server: load, hand over\nimport { loadRows } from \"../lib/data\";\nimport Pipeline from \"../components/Pipeline\";\n\nexport default async function PipelineSurface() {\n const { rows, error } = await loadRows({ source: \"public.deals\", orderBy: \"updated_at desc\" });\n return <Pipeline deals={rows} error={error} />;\n}\n```\n\n```jsx\n// components/Pipeline.jsx — client: the board, the card, the edit sheet\n\"use client\";\nimport { useState } from \"react\";\nimport { insertRow, moveRow } from \"../lib/actions\";\nimport { Badge, Notice } from \"./ui\";\nimport { FixtureNotice, useFixture } from \"./parts\";\nimport { Board } from \"./blocks/board\";\n\nconst STAGES = [\n { key: \"lead\", label: \"Lead\" },\n { key: \"call\", label: \"Call booked\" },\n { key: \"proposal\", label: \"Proposal\" },\n { key: \"won\", label: \"Won\" },\n];\nconst SAMPLE = [\n { name: \"Northwind\", stage: \"call\", value: 12000, owner: \"Maya\" },\n { name: \"Acme Ltd\", stage: \"lead\", value: 4000, owner: \"Leon\" },\n];\n\nexport default function Pipeline({ deals, error }) {\n const fixture = useFixture(deals, SAMPLE);\n const [editing, setEditing] = useState(null);\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 <Board\n rows={fixture.rows}\n groupBy=\"stage\"\n columns={STAGES}\n onMove={(d, to) => moveRow({ source: \"public.deals\", id: d.id, stateColumn: \"stage\", to })}\n titleOf={(d) => d.name}\n limits={{ proposal: 5 }}\n onCreate={(name, stage) => insertRow({ source: \"public.deals\", values: { name, stage }, detail: name })}\n onOpen={setEditing}\n renderDetails={(d) => (\n <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n <span>{d.owner ?? \"Unowned\"}</span>\n <Badge>{Number(d.value ?? 0).toLocaleString()}</Badge>\n </div>\n )}\n label=\"Deals by stage\"\n />\n {/* <Sheet open={Boolean(editing)} …> the record's fields </Sheet> */}\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Order cards inside a column: they keep the order of `rows` — sort in\n the surface's `loadRows`.\n- Create a stage: `columns` is the surface's; a new stage is a new entry.\n- Colour a column: stages are not status. Put a `Badge` on the card when a\n stage means something the founder must see.\n\nA board it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add board` restores them.\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"board",
|
|
33
|
+
"kanban",
|
|
34
|
+
"pipeline",
|
|
35
|
+
"stages",
|
|
36
|
+
"columns",
|
|
37
|
+
"cards",
|
|
38
|
+
"drag",
|
|
39
|
+
"drop",
|
|
40
|
+
"status",
|
|
41
|
+
"workflow",
|
|
42
|
+
"funnel"
|
|
43
|
+
],
|
|
44
|
+
"docs": "Installed to components/makersclaw/blocks/board. Import: import { Board } from \"@/components/makersclaw/blocks/board\". Props and an example: components/makersclaw/blocks/board/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
45
|
+
"meta": {
|
|
46
|
+
"makersclaw": {
|
|
47
|
+
"version": "0.2.0",
|
|
48
|
+
"useWhen": "A process where each record sits in one stage and moves forward: a pipeline, a hiring funnel, a sprint, a content calendar by status, outreach by reply state. Not for records with no stages (a table) or items placed on a timeline."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/r/core.json
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "core",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Makersclaw kit",
|
|
6
|
+
"description": "The kit every Makersclaw block is built from — Button, Field, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction — and the parts bound to data (RecordSwitcher, useFixture). Native elements under the project's own theme tokens.",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "templates/next/files/components/parts/fixture.jsx",
|
|
10
|
+
"type": "registry:ui",
|
|
11
|
+
"target": "@components/makersclaw/parts/fixture.jsx",
|
|
12
|
+
"content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport { markSample } from \"./sample\";\nimport { Button, Notice } from \"../ui\";\n\n// The worked example, never persisted. A surface opens populated (README\n// §3: the template test) — but populated is not persisted. The example is\n// rows in the app's code, shown while the real collection is empty and\n// never INSERTed: the workspace database is shared by every app, so an\n// invented row there is a real row somewhere else. Clearing writes\n// nothing; the first real write is the user's.\n//\n// const { rows, isFixture, clear } = useFixture(realRows, SAMPLE);\n// {isFixture ? <FixtureNotice onClear={clear} /> : null}\n//\n// A fixture row carries `_sample: true` and a `sample-N` id. Editing one\n// promotes it: the surface copies its values into a new draft and saves\n// THAT — it never updateRow()s a sample id (the row does not exist).\n//\n// `rows` keeps its identity between renders (an effect may depend on it);\n// declare SAMPLE at module level, not inside the component, so it does too.\n\nconst NONE = [];\n\nexport function useFixture(rows, sample, { idColumn = \"id\" } = {}) {\n const [cleared, setCleared] = useState(false);\n const isFixture = !cleared && (!rows || rows.length === 0) && Array.isArray(sample) && sample.length > 0;\n const fixtureRows = useMemo(() => markSample(sample, idColumn), [sample, idColumn]);\n return {\n rows: isFixture ? fixtureRows : rows ?? NONE,\n isFixture,\n clear: () => setCleared(true),\n restore: () => setCleared(false),\n };\n}\n\nexport function isSample(row) {\n return Boolean(row && row._sample);\n}\n\n/** The line above a fixture, with the one control it earns. */\nexport function FixtureNotice({ onClear, children, className }) {\n return (\n <Notice\n tone=\"info\"\n title=\"Example only\"\n className={className}\n action={\n onClear ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onClear}>\n Clear the example\n </Button>\n ) : null\n }\n >\n {children ?? \"Edit it or start your own. Nothing here is saved until you save it.\"}\n </Notice>\n );\n}\n"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "templates/next/files/components/parts/index.js",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "@components/makersclaw/parts/index.js",
|
|
18
|
+
"content": "// Parts: the kit's pieces bound to this template's data layer. Each\n// exists because an app got it wrong once; each carries the behaviour\n// that makes it right. README §3b.\nexport { useFixture, isSample, FixtureNotice } from \"./fixture\";\nexport { RecordSwitcher } from \"./record-switcher\";\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "templates/next/files/components/parts/record-switcher.jsx",
|
|
22
|
+
"type": "registry:ui",
|
|
23
|
+
"target": "@components/makersclaw/parts/record-switcher.jsx",
|
|
24
|
+
"content": "\"use client\";\n\nimport { useId } from \"react\";\nimport { Button, Select } from \"../ui\";\n\n// Pick a saved record, or make a new one. With nothing saved there is\n// nothing to pick, so it shows the one control that has a job: New. A\n// disabled dropdown reading \"No saved…\" is a control that cannot be used\n// drawn at full weight — the thing this part exists to prevent.\n//\n// <RecordSwitcher rows={drafts} value={draft.id} onChange={choose}\n// labelOf={(r) => r.title} onNew={newDraft} newLabel=\"New draft\" />\n\nexport function RecordSwitcher({ rows, value, onChange, labelOf = (r) => r.title ?? r.name ?? r.id, onNew, newLabel = \"New\", label = \"Saved\", idColumn = \"id\", className }) {\n const id = useId();\n const live = (rows ?? []).filter((r) => !r._sample);\n if (live.length === 0) {\n return onNew ? (\n <div className={className}>\n <Button variant=\"secondary\" icon=\"plus\" onClick={onNew}>\n {newLabel}\n </Button>\n </div>\n ) : null;\n }\n return (\n <div className={[\"flex items-end gap-2\", className].filter(Boolean).join(\" \")}>\n <label htmlFor={id} className=\"sr-only\">\n {label}\n </label>\n <div className=\"min-w-0 flex-1\">\n <Select\n id={id}\n value={value ?? \"\"}\n onChange={(e) => onChange?.(e.target.value)}\n options={[\n ...(value ? [] : [{ value: \"\", label: `${label}…` }]),\n ...live.map((r) => ({ value: String(r[idColumn]), label: labelOf(r) })),\n ]}\n />\n </div>\n {onNew ? (\n <Button variant=\"secondary\" icon=\"plus\" onClick={onNew}>\n {newLabel}\n </Button>\n ) : null}\n </div>\n );\n}\n"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "templates/next/files/components/parts/sample.js",
|
|
28
|
+
"type": "registry:ui",
|
|
29
|
+
"target": "@components/makersclaw/parts/sample.js",
|
|
30
|
+
"content": "// Pure: importable from either side. The parts use it in the browser\n// (fixture.jsx); lib/sample.js re-exports it for server code (lib/data.js).\n// It lives beside the parts, not in lib/, so the parts travel as one folder\n// — into an app's components/parts, or a shadcn project's\n// components/makersclaw/parts — and import nothing outside it but the kit.\n\n/** Sample rows get stable ids and a `_sample: true` marker, so a surface\n * can label them until real rows exist and refuse to edit them. */\nexport function markSample(sample, idColumn = \"id\") {\n return (sample ?? []).map((r, i) => ({\n ...r,\n [idColumn]: r[idColumn] ?? `sample-${i + 1}`,\n _sample: true,\n }));\n}\n"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "templates/next/files/components/ui/button.jsx",
|
|
34
|
+
"type": "registry:ui",
|
|
35
|
+
"target": "@components/makersclaw/ui/button.jsx",
|
|
36
|
+
"content": "import Link from \"next/link\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Button — every action in the app. Four looks, one shape, 44px targets\n// (mobile first: a thumb hits it). `href` makes it a next/link (base\n// path applied); `icon` names one from ./icon. Never style a <button>\n// by hand in a surface: the look is the workspace's, the behaviour is\n// the browser's, and this is the only place both meet.\n//\n// variant primary (the one main action per surface) · secondary ·\n// quiet (no fill) · danger (destructive, red text) · link\n// size md (44px) · sm (36px, for dense rows and rails)\n\nconst VARIANTS = {\n primary: \"bg-primary text-primary-foreground hover:opacity-90 border-transparent\",\n secondary: \"bg-secondary text-secondary-foreground border-border hover:bg-accent\",\n quiet: \"bg-transparent text-foreground border-transparent hover:bg-accent\",\n danger: \"bg-transparent text-destructive border-border hover:bg-accent\",\n link: \"bg-transparent text-primary underline-offset-4 hover:underline border-transparent px-0 min-h-0\",\n};\n\nconst SIZES = {\n md: \"min-h-11 px-4 text-sm\",\n sm: \"min-h-9 px-3 text-sm\",\n};\n\nexport function Button({\n variant = \"secondary\",\n size = \"md\",\n icon,\n href,\n className,\n children,\n type = \"button\",\n ...rest\n}) {\n const classes = cx(\n \"inline-flex items-center justify-center gap-2 rounded-md border font-medium whitespace-nowrap\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n VARIANTS[variant] ?? VARIANTS.secondary,\n SIZES[size] ?? SIZES.md,\n className,\n );\n const body = (\n <>\n {icon ? <Icon name={icon} /> : null}\n {children}\n </>\n );\n if (href) {\n return (\n <Link href={href} className={classes} {...rest}>\n {body}\n </Link>\n );\n }\n return (\n <button type={type} className={classes} {...rest}>\n {body}\n </button>\n );\n}\n\n/** A square button holding only an icon; `label` is required and is what\n * a screen reader says. */\nexport function IconButton({ icon, label, size = \"md\", variant = \"quiet\", className, ...rest }) {\n return (\n <Button\n variant={variant}\n size={size}\n aria-label={label}\n title={label}\n className={cx(size === \"sm\" ? \"w-9 px-0\" : \"w-11 px-0\", className)}\n {...rest}\n >\n <Icon name={icon} size={size === \"sm\" ? 16 : 18} />\n </Button>\n );\n}\n"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "templates/next/files/components/ui/cx.js",
|
|
40
|
+
"type": "registry:ui",
|
|
41
|
+
"target": "@components/makersclaw/ui/cx.js",
|
|
42
|
+
"content": "/** Join class names, dropping falsy entries. The only helper the kit needs. */\nexport function cx(...parts) {\n return parts.flat().filter(Boolean).join(\" \");\n}\n"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"path": "templates/next/files/components/ui/depiction.jsx",
|
|
46
|
+
"type": "registry:ui",
|
|
47
|
+
"target": "@components/makersclaw/ui/depiction.jsx",
|
|
48
|
+
"content": "\"use client\";\n\nimport { useLayoutEffect, useRef, useState } from \"react\";\n\n// Depiction — the frame around a picture of something that exists\n// elsewhere: a tweet as X will show it, a LinkedIn post in the feed, the\n// row an email makes in Gmail's inbox. The picture itself is a file in\n// components/depictions/<thing>.jsx and wears THAT place's colours, face\n// and widths (README §3c); this frame is the boundary where the\n// workspace theme stops. app/globals.css resets the inherited face, size,\n// letter-spacing and density on [data-depiction] and sets color-scheme\n// from data-scheme, so nothing of the workspace reaches the picture.\n//\n// <Depiction of=\"x-post\" scheme=\"dark\" width={600} label=\"On X · Dim\">\n// <XPost text={draft.text} name={me.name} handle={me.handle} mode=\"dim\" />\n// </Depiction>\n//\n// of the depiction file's basename; `npm run check` ties them\n// scheme the TARGET's light or dark — never the workspace's\n// width the target's real column in px (X 600, LinkedIn 555, phone 375)\n// label the caption, chrome, in the workspace's tokens\n//\n// Scale, never reflow: line breaks and the \"…more\" fold ARE the preview,\n// so a narrow column shrinks the picture (transform) instead of\n// re-wrapping it. One ResizeObserver, no dependency. The caption sits\n// outside the reset boundary; the frame paints no ground inside it — the\n// depiction's root paints its target's page colour.\n\nexport function Depiction({ of, scheme, width, label, children }) {\n const outer = useRef(null);\n const inner = useRef(null);\n const [fit, setFit] = useState({ k: 1, height: undefined });\n\n useLayoutEffect(() => {\n const o = outer.current;\n const i = inner.current;\n if (!o || !i || typeof ResizeObserver === \"undefined\") return;\n const measure = () => {\n const k = Math.min(1, o.clientWidth / width);\n setFit({ k, height: Math.round(i.offsetHeight * k) });\n };\n measure();\n const ro = new ResizeObserver(measure);\n ro.observe(o);\n ro.observe(i);\n return () => ro.disconnect();\n }, [width]);\n\n return (\n <figure className=\"min-w-0\">\n <figcaption className=\"mb-2 text-2xs font-medium uppercase tracking-wider text-muted-foreground\">\n {label ?? `How it will look · ${of}`}\n </figcaption>\n <div ref={outer} className=\"overflow-hidden\" style={{ height: fit.height }}>\n <div\n ref={inner}\n data-depiction={of}\n data-scheme={scheme}\n style={{ width, transform: fit.k === 1 ? undefined : `scale(${fit.k})`, transformOrigin: \"top left\" }}\n >\n {children}\n </div>\n </div>\n </figure>\n );\n}\n"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"path": "templates/next/files/components/ui/field.jsx",
|
|
52
|
+
"type": "registry:ui",
|
|
53
|
+
"target": "@components/makersclaw/ui/field.jsx",
|
|
54
|
+
"content": "import { cx } from \"./cx\";\n\n// Form parts. Each is the native element with the workspace's look and a\n// 44px target; the browser supplies the behaviour (focus, keyboard,\n// validation, autofill). Wrap one in <Field> for its label, hint and\n// error — never a bare <input> with a <div> label beside it.\n\nconst CONTROL =\n \"w-full min-h-11 rounded-md border border-input bg-background px-3 text-sm text-foreground \" +\n \"placeholder:text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring \" +\n \"disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive\";\n\n/** Label, control, hint and error in one stack. Pass `id`; the label\n * points at it. `error` (a string) marks the control invalid. */\nexport function Field({ id, label, hint, error, required, className, children }) {\n return (\n <div className={cx(\"grid gap-1.5\", className)}>\n {label ? (\n <label htmlFor={id} className=\"text-sm font-medium text-foreground\">\n {label}\n {required ? <span aria-hidden=\"true\" className=\"text-destructive\"> *</span> : null}\n </label>\n ) : null}\n {children}\n {error ? (\n <p id={id ? `${id}-error` : undefined} role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : hint ? (\n <p id={id ? `${id}-hint` : undefined} className=\"text-xs text-muted-foreground\">\n {hint}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport function Input({ className, invalid, ...rest }) {\n return <input className={cx(CONTROL, className)} aria-invalid={invalid ? true : undefined} {...rest} />;\n}\n\nexport function Textarea({ className, invalid, rows = 4, ...rest }) {\n return (\n <textarea\n rows={rows}\n className={cx(CONTROL, \"min-h-24 resize-y py-2.5 leading-normal\", className)}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n />\n );\n}\n\n/**\n * The native <select>, styled. `options` is [{ value, label }]; children\n * work too. A select with nothing to choose from is not a control — when\n * `options` is empty it renders `empty` (a short muted line, or nothing\n * when `empty` is null) rather than a disabled dropdown. For \"pick a\n * saved record or make a new one\" use RecordSwitcher (components/parts).\n */\nexport function Select({ options, empty = \"Nothing to choose yet.\", className, invalid, children, ...rest }) {\n if (options && options.length === 0 && !children) {\n return empty ? <p className=\"text-sm text-muted-foreground\">{empty}</p> : null;\n }\n return (\n <span className=\"relative block\">\n <select\n className={cx(CONTROL, \"appearance-none pr-9\", className)}\n aria-invalid={invalid ? true : undefined}\n {...rest}\n >\n {options ? options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>) : children}\n </select>\n <svg\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </span>\n );\n}\n\n/** An on/off. A checkbox underneath (role=switch), so forms, labels and\n * keyboards work; the track is drawn from tokens. */\nexport function Switch({ id, checked, onChange, label, disabled, className }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-3 text-sm\", disabled && \"cursor-not-allowed opacity-50\", className)}>\n <span className=\"relative inline-flex h-6 w-10 shrink-0\">\n <input\n id={id}\n type=\"checkbox\"\n role=\"switch\"\n className=\"peer sr-only\"\n checked={checked}\n onChange={onChange}\n disabled={disabled}\n />\n <span className=\"absolute inset-0 rounded-full border border-input bg-muted transition-colors peer-checked:bg-primary peer-focus-visible:ring-2 peer-focus-visible:ring-ring\" />\n <span className=\"absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-foreground transition-transform peer-checked:translate-x-4 peer-checked:bg-primary-foreground\" />\n </span>\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n\nexport function Checkbox({ id, label, className, ...rest }) {\n return (\n <label htmlFor={id} className={cx(\"inline-flex min-h-11 cursor-pointer items-center gap-2 text-sm\", className)}>\n <input\n id={id}\n type=\"checkbox\"\n className=\"h-4 w-4 rounded-sm border-input accent-[var(--color-primary)] outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n {...rest}\n />\n {label ? <span>{label}</span> : null}\n </label>\n );\n}\n"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"path": "templates/next/files/components/ui/icon.jsx",
|
|
58
|
+
"type": "registry:ui",
|
|
59
|
+
"target": "@components/makersclaw/ui/icon.jsx",
|
|
60
|
+
"content": "// The kit's icons: one inline sprite, no icon package to install. Every\n// glyph is a 24-box stroke path; add one here, never an <svg> in a surface.\n// `name` is the key below; unknown names render nothing (and `npm run\n// check` names them).\n\nconst PATHS = {\n plus: \"M12 5v14M5 12h14\",\n x: \"M18 6 6 18M6 6l12 12\",\n check: \"m5 12 5 5L20 7\",\n \"chevron-down\": \"m6 9 6 6 6-6\",\n \"chevron-up\": \"m18 15-6-6-6 6\",\n \"chevron-left\": \"m15 18-6-6 6-6\",\n \"chevron-right\": \"m9 18 6-6-6-6\",\n \"arrow-left\": \"M19 12H5m7-7-7 7 7 7\",\n \"arrow-right\": \"M5 12h14m-7-7 7 7-7 7\",\n search: \"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14ZM21 21l-4.3-4.3\",\n trash: \"M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14\",\n pencil: \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\",\n copy: \"M8 8h12v12H8zM4 16V4h12\",\n more: \"M5 12h.01M12 12h.01M19 12h.01\",\n external: \"M14 3h7v7m0-7L10 14M21 14v7H3V3h7\",\n refresh: \"M21 12a9 9 0 1 1-2.6-6.4M21 3v6h-6\",\n filter: \"M3 5h18l-7 8v6l-4 2v-8Z\",\n calendar: \"M3 5h18v16H3zM16 3v4M8 3v4M3 10h18\",\n clock: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 3\",\n user: \"M20 21a8 8 0 0 0-16 0M12 3a5 5 0 1 0 0 10 5 5 0 0 0 0-10Z\",\n users: \"M17 21a4 4 0 0 0-8 0M13 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM23 21a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8M3 21a4 4 0 0 1 3-3.9M8 3.1a4 4 0 0 0 0 7.8\",\n mail: \"M3 5h18v14H3zM3 7l9 6 9-6\",\n link: \"M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1\",\n grip: \"M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01\",\n info: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 5h.01M12 12v4\",\n warning: \"M12 3 2 20h20L12 3Zm0 7v4m0 3h.01\",\n sparkles: \"m12 3 2 6 6 2-6 2-2 6-2-6-6-2 6-2 2-6ZM19 15l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2Z\",\n play: \"m6 4 14 8-14 8V4Z\",\n pause: \"M6 4h4v16H6zM14 4h4v16h-4z\",\n download: \"M12 3v12m-5-5 5 5 5-5M4 21h16\",\n upload: \"M12 21V9m-5 5 5-5 5 5M4 3h16\",\n eye: \"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Zm10-3a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z\",\n send: \"m22 2-7 20-4-9-9-4 20-7Z\",\n home: \"m3 11 9-8 9 8v10h-6v-6H9v6H3V11Z\",\n settings: \"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm8 4-1.5-.5.3-1.6 1.4-.9-1-1.7-1.6.5-1.2-1.2.5-1.6-1.7-1-.9 1.4-1.6-.3L12 4l-.5 1.5-1.6.3-.9-1.4-1.7 1 .5 1.6-1.2 1.2-1.6-.5-1 1.7 1.4.9.3 1.6L4 12l1.5.5-.3 1.6-1.4.9 1 1.7 1.6-.5 1.2 1.2-.5 1.6 1.7 1 .9-1.4 1.6.3L12 20l.5-1.5 1.6-.3.9 1.4 1.7-1-.5-1.6 1.2-1.2 1.6.5 1-1.7-1.4-.9-.3-1.6L20 12Z\",\n reply: \"M9 17 4 12l5-5M4 12h10a6 6 0 0 1 6 6v1\",\n repeat: \"m17 2 4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3\",\n heart: \"M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1.1L12 21l7.8-7.5 1.1-1.1a5.5 5.5 0 0 0-.1-7.8Z\",\n chart: \"M3 3v18h18M7 16l4-5 4 3 5-7\",\n share: \"M12 3v12M7 8l5-5 5 5M5 14v6h14v-6\",\n star: \"m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.3 6.4 20.2l1.1-6.2L3 9.6l6.2-.9L12 3Z\",\n bell: \"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0\",\n tag: \"M20 12 12 20l-9-9V3h8l9 9ZM7 7h.01\",\n folder: \"M3 7V5h6l2 2h10v13H3V7Z\",\n file: \"M14 3H6v18h12V7l-4-4ZM14 3v4h4\",\n image: \"M3 5h18v14H3zM8 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM21 15l-5-5-9 9\",\n phone: \"M22 16.9v3a2 2 0 0 1-2.2 2A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7l.8 3.6a2 2 0 0 1-.5 1.9L8.1 10.5a16 16 0 0 0 5.4 5.4l1.3-1.3a2 2 0 0 1 1.9-.5l3.6.8a2 2 0 0 1 1.7 2Z\",\n globe: \"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18\",\n lock: \"M5 11h14v10H5zM8 11V7a4 4 0 0 1 8 0v4\",\n zap: \"M13 2 3 14h8l-1 8 10-12h-8l1-8Z\",\n dollar: \"M12 2v20M17 6H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6\",\n map: \"m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6ZM9 3v15M15 6v15\",\n minus: \"M5 12h14\",\n archive: \"M3 4h18v4H3zM5 8v12h14V8M10 12h4\",\n inbox: \"M3 13h5l2 3h4l2-3h5M3 13V5h18v8M3 13v6h18v-6\",\n};\n\nexport const ICON_NAMES = Object.keys(PATHS);\n\nexport function Icon({ name, size = 16, className, label }) {\n const d = PATHS[name];\n if (!d) return null;\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden={label ? undefined : true}\n role={label ? \"img\" : undefined}\n aria-label={label}\n className={className}\n >\n <path d={d} />\n </svg>\n );\n}\n"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"path": "templates/next/files/components/ui/index.js",
|
|
64
|
+
"type": "registry:ui",
|
|
65
|
+
"target": "@components/makersclaw/ui/index.js",
|
|
66
|
+
"content": "// The kit. Import from \"../components/ui\" (a surface) or \"./ui\" (a\n// component). Every part is the workspace's look over the browser's own\n// behaviour; none of them takes a colour. README §3b.\nexport { cx } from \"./cx\";\nexport { Icon, ICON_NAMES } from \"./icon\";\nexport { Button, IconButton } from \"./button\";\nexport { Field, Input, Textarea, Select, Switch, Checkbox } from \"./field\";\nexport { Card, Notice, Badge, Kicker, Empty } from \"./notice\";\nexport { Sheet, Confirm } from \"./sheet\";\nexport { Tabs, TabPanel } from \"./tabs\";\nexport { Menu } from \"./menu\";\nexport { Depiction } from \"./depiction\";\n"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"path": "templates/next/files/components/ui/menu.jsx",
|
|
70
|
+
"type": "registry:ui",
|
|
71
|
+
"target": "@components/makersclaw/ui/menu.jsx",
|
|
72
|
+
"content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cx } from \"./cx\";\nimport { Icon } from \"./icon\";\n\n// Menu — the \"…\" of a row, the kebab of a card. The Popover API does the\n// hard part (top layer, light dismiss, Escape); this positions it under\n// its button and moves focus with the arrow keys. `items` with nothing in\n// them render nothing: a menu is earned by having actions.\n//\n// items [{ label, onSelect, icon?, tone?: \"danger\", disabled? }]\n// trigger a node to use as the button's content; default is the ⋯ icon\n\nexport function Menu({ items, label = \"More\", trigger, align = \"end\", className }) {\n const id = useId();\n const btn = useRef(null);\n const pop = useRef(null);\n const [open, setOpen] = useState(false);\n const live = (items ?? []).filter(Boolean);\n\n // A fixed-positioned popover does not follow its button; close it when\n // the page moves rather than leave it floating where the button was.\n useEffect(() => {\n if (!open) return;\n const close = () => pop.current?.hidePopover();\n window.addEventListener(\"resize\", close);\n window.addEventListener(\"scroll\", close, true);\n return () => {\n window.removeEventListener(\"resize\", close);\n window.removeEventListener(\"scroll\", close, true);\n };\n }, [open]);\n\n if (live.length === 0) return null;\n\n function place() {\n const b = btn.current?.getBoundingClientRect();\n const p = pop.current;\n if (!b || !p) return;\n p.style.position = \"fixed\";\n p.style.top = `${Math.round(b.bottom + 4)}px`;\n const width = p.offsetWidth || 200;\n const left = align === \"end\" ? b.right - width : b.left;\n p.style.left = `${Math.max(8, Math.min(left, window.innerWidth - width - 8))}px`;\n }\n\n function toggle() {\n const p = pop.current;\n if (!p) return;\n if (p.matches(\":popover-open\")) p.hidePopover();\n else {\n p.showPopover();\n place();\n p.querySelector('[role=\"menuitem\"]:not([disabled])')?.focus();\n }\n }\n\n function onKeyDown(e) {\n const nodes = [...(pop.current?.querySelectorAll('[role=\"menuitem\"]:not([disabled])') ?? [])];\n const i = nodes.indexOf(document.activeElement);\n if (e.key === \"ArrowDown\") nodes[(i + 1) % nodes.length]?.focus();\n else if (e.key === \"ArrowUp\") nodes[(i - 1 + nodes.length) % nodes.length]?.focus();\n else if (e.key === \"Home\") nodes[0]?.focus();\n else if (e.key === \"End\") nodes[nodes.length - 1]?.focus();\n else if (e.key === \"Tab\" || e.key === \"Escape\") {\n pop.current?.hidePopover();\n btn.current?.focus();\n } else return;\n e.preventDefault();\n }\n\n return (\n <>\n <button\n ref={btn}\n type=\"button\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={id}\n aria-label={trigger ? undefined : label}\n title={trigger ? undefined : label}\n onClick={toggle}\n className={cx(\n \"inline-flex min-h-9 min-w-9 items-center justify-center gap-2 rounded-md px-2 text-sm text-foreground outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring\",\n className,\n )}\n >\n {trigger ?? <Icon name=\"more\" />}\n </button>\n <div\n ref={pop}\n id={id}\n popover=\"auto\"\n role=\"menu\"\n aria-label={label}\n onToggle={(e) => setOpen(e.newState === \"open\")}\n onKeyDown={onKeyDown}\n className=\"min-w-44 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg\"\n >\n {live.map((item, i) => (\n <button\n key={i}\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n onClick={() => {\n pop.current?.hidePopover();\n item.onSelect?.();\n }}\n className={cx(\n \"flex w-full min-h-10 items-center gap-2 rounded-sm px-2 text-left text-sm outline-none hover:bg-accent focus-visible:bg-accent disabled:opacity-50\",\n item.tone === \"danger\" && \"text-destructive\",\n )}\n >\n {item.icon ? <Icon name={item.icon} /> : null}\n {item.label}\n </button>\n ))}\n </div>\n </>\n );\n}\n"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"path": "templates/next/files/components/ui/notice.jsx",
|
|
76
|
+
"type": "registry:ui",
|
|
77
|
+
"target": "@components/makersclaw/ui/notice.jsx",
|
|
78
|
+
"content": "import { cx } from \"./cx\";\nimport { Button } 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. */\nexport function Card({ title, description, actions, className, children, as: Tag = \"section\" }) {\n return (\n <Tag className={cx(\"rounded-lg border border-border bg-card p-4 text-card-foreground\", className)}>\n {title || actions ? (\n <div className=\"mb-3 flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n {title ? <h2 className=\"text-base font-semibold leading-tight\">{title}</h2> : 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 <Button variant=\"quiet\" size=\"sm\" onClick={onDismiss} aria-label=\"Dismiss\" className=\"-my-1 -mr-1 w-9 px-0\">\n <Icon name=\"x\" />\n </Button>\n ) : null}\n </div>\n );\n}\n\n/** A small label for a state or a count. */\nexport function Badge({ tone = \"neutral\", className, children }) {\n const look = {\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 }[tone];\n return <span className={cx(\"inline-flex items-center rounded-sm px-1.5 py-0.5 text-2xs font-medium uppercase tracking-wide\", look, className)}>{children}</span>;\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"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"path": "templates/next/files/components/ui/sheet.jsx",
|
|
82
|
+
"type": "registry:ui",
|
|
83
|
+
"target": "@components/makersclaw/ui/sheet.jsx",
|
|
84
|
+
"content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { cx } from \"./cx\";\nimport { Button, IconButton } from \"./button\";\n\n// Sheet — the one overlay. A native <dialog> (focus trap, Esc, scroll\n// lock and the top layer come from the browser), docked to the side the\n// surface asks for. Mobile first: on a narrow window every side becomes\n// a bottom sheet the thumb can reach. Controlled: `open` + `onClose`.\n//\n// side end (a rail beside the work; the default) · center (a dialog)\n// · bottom\n//\n// Confirm below is the \"delete this?\" case — never window.confirm().\n\nconst SIDES = {\n end:\n \"inset-y-0 right-0 left-auto h-dvh max-h-none w-full max-w-md rounded-none border-l \" +\n \"max-sm:inset-x-0 max-sm:top-auto max-sm:bottom-0 max-sm:h-auto max-sm:max-h-[85dvh] max-sm:max-w-none max-sm:rounded-t-xl max-sm:border-l-0 max-sm:border-t\",\n center: \"m-auto w-[calc(100%-2rem)] max-w-lg rounded-lg border max-sm:mb-0 max-sm:mt-auto max-sm:w-full max-sm:max-w-none max-sm:rounded-b-none max-sm:rounded-t-xl\",\n bottom: \"inset-x-0 top-auto bottom-0 w-full max-w-none max-h-[85dvh] rounded-t-xl border-t\",\n};\n\nexport function Sheet({ open, onClose, title, description, side = \"end\", footer, className, children }) {\n const ref = useRef(null);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (open && !el.open) el.showModal();\n else if (!open && el.open) el.close();\n }, [open]);\n\n return (\n <dialog\n ref={ref}\n onClose={() => onClose?.()}\n onClick={(e) => {\n if (e.target === ref.current) ref.current.close();\n }}\n className={cx(\n \"fixed m-0 flex-col bg-popover p-0 text-popover-foreground shadow-xl border-border open:flex\",\n SIDES[side] ?? SIDES.end,\n className,\n )}\n aria-labelledby={title ? \"sheet-title\" : undefined}\n >\n <div className=\"flex min-h-0 flex-1 flex-col\">\n {title || onClose ? (\n <header className=\"flex items-start justify-between gap-3 border-b border-border px-4 py-3\">\n <div className=\"min-w-0\">\n {title ? <h2 id=\"sheet-title\" className=\"text-base font-semibold leading-tight\">{title}</h2> : null}\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n <IconButton icon=\"x\" label=\"Close\" size=\"sm\" onClick={() => ref.current?.close()} className=\"-mr-2\" />\n </header>\n ) : null}\n <div className=\"min-h-0 flex-1 overflow-y-auto px-4 py-4\">{children}</div>\n {footer ? <footer className=\"flex flex-wrap justify-end gap-2 border-t border-border px-4 py-3\">{footer}</footer> : null}\n </div>\n </dialog>\n );\n}\n\n/** A yes/no with the consequence spelled out. `tone=\"danger\"` for a\n * destructive confirm. Resolves through `onConfirm`; closes either way. */\nexport function Confirm({ open, onClose, onConfirm, title, body, confirmLabel = \"Confirm\", cancelLabel = \"Cancel\", tone = \"neutral\", busy }) {\n return (\n <Sheet\n open={open}\n onClose={onClose}\n side=\"center\"\n title={title}\n footer={\n <>\n <Button variant=\"quiet\" onClick={onClose} disabled={busy}>\n {cancelLabel}\n </Button>\n <Button variant={tone === \"danger\" ? \"danger\" : \"primary\"} onClick={onConfirm} disabled={busy}>\n {busy ? \"Working…\" : confirmLabel}\n </Button>\n </>\n }\n >\n {body ? <p className=\"text-sm text-muted-foreground\">{body}</p> : null}\n </Sheet>\n );\n}\n"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"path": "templates/next/files/components/ui/tabs.jsx",
|
|
88
|
+
"type": "registry:ui",
|
|
89
|
+
"target": "@components/makersclaw/ui/tabs.jsx",
|
|
90
|
+
"content": "\"use client\";\n\nimport { useId, useRef } from \"react\";\nimport { cx } from \"./cx\";\n\n// Tabs — one strip, arrow keys move and select, Home/End jump. Controlled:\n// `value` + `onChange`. The strip is `Tabs`; each panel is a `TabPanel`\n// keyed the same way, so the caller decides what a tab shows. Tabs are\n// for views of ONE thing (a record's notes | history), never for the\n// app's surfaces — those are the shell's nav.\n//\n// tabs [{ key, label, count? }]\n\nexport function Tabs({ tabs, value, onChange, id, className, \"aria-label\": ariaLabel }) {\n const auto = useId();\n const base = id ?? auto;\n const refs = useRef([]);\n\n function onKeyDown(e, index) {\n const last = tabs.length - 1;\n let next = null;\n if (e.key === \"ArrowRight\") next = index === last ? 0 : index + 1;\n else if (e.key === \"ArrowLeft\") next = index === 0 ? last : index - 1;\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = last;\n if (next === null) return;\n e.preventDefault();\n refs.current[next]?.focus();\n onChange?.(tabs[next].key);\n }\n\n return (\n <div role=\"tablist\" aria-label={ariaLabel} className={cx(\"flex gap-1 overflow-x-auto border-b border-border\", className)}>\n {tabs.map((t, i) => {\n const selected = t.key === value;\n return (\n <button\n key={t.key}\n ref={(el) => {\n refs.current[i] = el;\n }}\n type=\"button\"\n role=\"tab\"\n id={`${base}-tab-${t.key}`}\n aria-selected={selected}\n aria-controls={`${base}-panel-${t.key}`}\n tabIndex={selected ? 0 : -1}\n onClick={() => onChange?.(t.key)}\n onKeyDown={(e) => onKeyDown(e, i)}\n className={cx(\n \"-mb-px inline-flex min-h-11 items-center gap-2 whitespace-nowrap border-b-2 px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n selected ? \"border-primary font-medium text-foreground\" : \"border-transparent text-muted-foreground hover:text-foreground\",\n )}\n >\n {t.label}\n {typeof t.count === \"number\" ? (\n <span data-nums className=\"rounded-sm bg-muted px-1.5 text-2xs text-muted-foreground\">\n {t.count}\n </span>\n ) : null}\n </button>\n );\n })}\n </div>\n );\n}\n\nexport function TabPanel({ tab, value, id, className, children }) {\n const selected = tab === value;\n return (\n <div\n role=\"tabpanel\"\n id={id ? `${id}-panel-${tab}` : undefined}\n aria-labelledby={id ? `${id}-tab-${tab}` : undefined}\n hidden={!selected}\n className={cx(selected && \"pt-4\", className)}\n >\n {selected ? children : null}\n </div>\n );\n}\n"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"cssVars": {
|
|
94
|
+
"theme": {
|
|
95
|
+
"text-2xs": "0.6875rem",
|
|
96
|
+
"text-2xs--line-height": "1rem"
|
|
97
|
+
},
|
|
98
|
+
"light": {
|
|
99
|
+
"success": "oklch(0.627 0.194 149.214)",
|
|
100
|
+
"warning": "oklch(0.666 0.179 58.318)",
|
|
101
|
+
"info": "oklch(0.546 0.245 262.881)"
|
|
102
|
+
},
|
|
103
|
+
"dark": {
|
|
104
|
+
"success": "oklch(0.792 0.209 151.711)",
|
|
105
|
+
"warning": "oklch(0.828 0.189 84.429)",
|
|
106
|
+
"info": "oklch(0.707 0.165 254.624)"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"css": {
|
|
110
|
+
"@layer base": {
|
|
111
|
+
"[data-nums]": {
|
|
112
|
+
"font-variant-numeric": "tabular-nums"
|
|
113
|
+
},
|
|
114
|
+
"dialog": {
|
|
115
|
+
"color": "inherit"
|
|
116
|
+
},
|
|
117
|
+
"dialog::backdrop": {
|
|
118
|
+
"background": "color-mix(in oklab, var(--background) 70%, transparent)"
|
|
119
|
+
},
|
|
120
|
+
"[popover]": {
|
|
121
|
+
"color": "inherit",
|
|
122
|
+
"margin": "0",
|
|
123
|
+
"padding": "0",
|
|
124
|
+
"border": "0",
|
|
125
|
+
"background": "transparent"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"@layer components": {
|
|
129
|
+
"[data-depiction]": {
|
|
130
|
+
"isolation": "isolate",
|
|
131
|
+
"color-scheme": "light",
|
|
132
|
+
"font": "16px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Apple Color Emoji\", \"Segoe UI Emoji\", sans-serif",
|
|
133
|
+
"letter-spacing": "normal",
|
|
134
|
+
"word-spacing": "normal",
|
|
135
|
+
"text-align": "start",
|
|
136
|
+
"color": "CanvasText"
|
|
137
|
+
},
|
|
138
|
+
"[data-depiction][data-scheme=\"dark\"]": {
|
|
139
|
+
"color-scheme": "dark"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"docs": "Installed to components/makersclaw/ui and components/makersclaw/parts. Import: import { Button } from \"@/components/makersclaw/ui\". Needs Next.js 15+, React 19 and Tailwind v4. https://www.npmjs.com/package/makersclaw"
|
|
144
|
+
}
|
package/r/job-panel.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "job-panel",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "JobPanel",
|
|
6
|
+
"description": "The panel that follows work the founder asked the app's agent for (useJob): each step's label as it runs, Stop, then done or the reason it stopped — and nothing on screen when no job runs.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.2.0/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/job-panel/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/job-panel/index.js",
|
|
16
|
+
"content": "// The JobPanel block — `makersclaw add job-panel`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { JobPanel } from \"./job-panel\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/job-panel/job-panel.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/job-panel/job-panel.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { Button, Icon, cx } from \"../../ui\";\n\n// JobPanel — the panel that follows work the founder asked the app's\n// agent for (useJob in lib/hooks.js, README §5 \"Commissioning work\"). It\n// keeps the four rules a hand-drawn panel breaks: it is not there when\n// nothing runs, it shows each step's label (the founder's phrase, never\n// the tool's name), the agent's text is its account and not the\n// deliverable (the rows are), and Stop / Close are always one press away.\n//\n// const job = useJob();\n// <Button onClick={() => job.start(\"Draft a launch plan for Rowhouse\")}>Generate a launch plan</Button>\n// <JobPanel job={job} title=\"Drafting the launch plan\" doneTitle=\"Launch plan drafted\" />\n\nfunction StepMark({ status }) {\n if (status === \"done\") return <Icon name=\"check\" size={14} className=\"shrink-0 text-success\" />;\n if (status === \"failed\") return <Icon name=\"warning\" size={14} className=\"shrink-0 text-destructive\" />;\n return <span aria-hidden=\"true\" className=\"mx-[3px] size-2 shrink-0 animate-pulse rounded-full bg-primary\" />;\n}\n\nexport function JobPanel({\n job,\n title = \"Working on it\",\n doneTitle = \"Done\",\n failedTitle = \"It stopped\",\n onRetry,\n autoClose = 6000,\n maxSteps = 6,\n showText = true,\n className,\n}) {\n const textRef = useRef(null);\n const running = Boolean(job?.running);\n const failed = !running && Boolean(job?.error);\n const done = !running && !failed && Boolean(job?.finishedAt);\n\n // The agent's account grows at the bottom; keep the newest line in view.\n useEffect(() => {\n const el = textRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [job?.text]);\n\n // A clean finish closes itself: the rows it made are the answer.\n useEffect(() => {\n if (!done || !autoClose || !job?.reset) return undefined;\n const t = setTimeout(() => job.reset(), autoClose);\n return () => clearTimeout(t);\n }, [done, autoClose, job?.finishedAt]); // eslint-disable-line react-hooks/exhaustive-deps\n\n if (!job || !(running || failed || done)) return null;\n\n const steps = job.steps ?? [];\n const hidden = Math.max(0, steps.length - maxSteps);\n const visible = steps.slice(hidden);\n const heading = running ? title : failed ? failedTitle : doneTitle;\n\n return (\n <section\n aria-label={heading}\n className={cx(\"rounded-lg border bg-card p-4 text-card-foreground\", failed ? \"border-destructive/50\" : \"border-border\", className)}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p aria-live=\"polite\" className=\"flex items-center gap-2 text-sm font-semibold\">\n {running ? <StepMark status=\"running\" /> : <StepMark status={failed ? \"failed\" : \"done\"} />}\n <span className=\"min-w-0\">{heading}</span>\n </p>\n {failed ? <p className=\"mt-1 text-sm text-muted-foreground\">{job.error}</p> : null}\n </div>\n <div className=\"flex shrink-0 gap-2\">\n {failed && onRetry ? (\n <Button size=\"sm\" variant=\"secondary\" onClick={onRetry}>\n Try again\n </Button>\n ) : null}\n {running ? (\n <Button size=\"sm\" variant=\"quiet\" onClick={() => job.cancel?.()}>\n Stop\n </Button>\n ) : (\n <Button size=\"sm\" variant=\"quiet\" onClick={() => job.reset?.()}>\n Close\n </Button>\n )}\n </div>\n </div>\n {visible.length ? (\n <ol className=\"mt-3 grid gap-1.5 text-sm\">\n {hidden > 0 ? <li className=\"text-xs text-muted-foreground\">{hidden} earlier {hidden === 1 ? \"step\" : \"steps\"}</li> : null}\n {visible.map((step, i) => (\n <li key={`${hidden + i}-${step.name}`} className=\"flex items-center gap-2\">\n <StepMark status={step.status} />\n <span className={step.status === \"running\" ? \"text-foreground\" : \"text-muted-foreground\"}>{step.label ?? \"Working\"}</span>\n </li>\n ))}\n </ol>\n ) : null}\n {showText && job.text ? (\n <div\n ref={textRef}\n className=\"mt-3 max-h-32 overflow-y-auto whitespace-pre-wrap border-t border-border pt-3 text-xs leading-relaxed text-muted-foreground\"\n >\n {job.text}\n </div>\n ) : null}\n </section>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/job-panel/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/job-panel/README.md",
|
|
28
|
+
"content": "# JobPanel\n\nThe panel that follows work the founder asked the app's agent for. It\nshows each step's label as the agent works, a Stop button while it runs,\nthen \"done\" (closing itself a few seconds later) or the reason it\nstopped. When no job runs it renders nothing — a panel that is always\nthere is a monitoring console, and apps do not have one.\n\n```jsx\nimport { JobPanel } from \"../components/blocks/job-panel\"; // a Makersclaw app (\"./blocks/job-panel\" from components/)\nimport { JobPanel } from \"@/components/makersclaw/blocks/job-panel\"; // a shadcn project\n```\n\nA client component. It takes the object `useJob()` returns (lib/hooks.js,\nREADME §5 \"Commissioning work\") and draws it; starting the job stays with\nyour surface.\n\n## Props\n\n- `job` — what `useJob()` returns. Required. Outside Makersclaw, any\n object of the same shape: `running`, `steps` (`[{ name, label, status:\n \"running\" | \"done\" | \"failed\" }]`), `text`, `error`, `finishedAt`, and\n `cancel()` / `reset()`.\n- `title` — while it runs, what the agent is doing, in the founder's\n words: `\"Drafting the launch plan\"`. Default `\"Working on it\"`.\n- `doneTitle` — after a clean finish: `\"Launch plan drafted\"`.\n- `failedTitle` — when it stopped with an error; the error follows it.\n- `onRetry` — shows \"Try again\" beside Close when the job failed: start\n the same job again.\n- `autoClose` — milliseconds after a clean finish before the panel\n closes itself (`job.reset()`); default 6000, `false` to keep it until\n Close.\n- `maxSteps` — the newest steps shown, default 6; older ones become one\n \"N earlier steps\" line.\n- `showText` — the agent's own account under the steps, default true. It\n is what the agent says it did, never the result: the result is rows.\n- `className` — layout around it. Never colour.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useEffect } from \"react\";\nimport { useJob } from \"../lib/hooks\";\nimport { Button } from \"./ui\";\nimport { JobPanel } from \"./blocks/job-panel\";\n\nconst ASK = \"Draft a launch plan for the next product in the list, with dates and owners.\";\n\nexport default function Launches({ launches, reload }) {\n const job = useJob();\n useEffect(() => { reload?.(); }, [job.progress]); // rows appear as they land\n\n return (\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => job.start(ASK)} disabled={job.running}>Generate a launch plan</Button>\n <Button variant=\"primary\" icon=\"plus\" onClick={openNewLaunch}>New launch plan</Button>\n </div>\n <JobPanel job={job} title=\"Drafting the launch plan\" doneTitle=\"Launch plan drafted\" onRetry={() => job.start(ASK)} />\n {/* the launches list — the job's result lands here */}\n </div>\n );\n}\n```\n\nThe verb sits on the object list beside the manual \"New\" (README §5, rule\n1), and the panel sits where the result will appear.\n\nIts files are fixed: `npm run check` refuses a change, and `makersclaw add\njob-panel` restores them. A different panel is your own component in\n`components/`, built from the kit.\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"job",
|
|
33
|
+
"agent",
|
|
34
|
+
"generate",
|
|
35
|
+
"progress",
|
|
36
|
+
"steps",
|
|
37
|
+
"running",
|
|
38
|
+
"working",
|
|
39
|
+
"commission",
|
|
40
|
+
"ai",
|
|
41
|
+
"stop",
|
|
42
|
+
"status"
|
|
43
|
+
],
|
|
44
|
+
"docs": "Installed to components/makersclaw/blocks/job-panel. Import: import { JobPanel } from \"@/components/makersclaw/blocks/job-panel\". Props and an example: components/makersclaw/blocks/job-panel/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
45
|
+
"meta": {
|
|
46
|
+
"makersclaw": {
|
|
47
|
+
"version": "0.2.0",
|
|
48
|
+
"useWhen": "A surface has a verb that asks the agent to make or find something (\"Generate a launch plan\", \"Find 20 leads\", \"Plan next sprint\") and the founder should watch it happen where the result will land."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/r/registry.json
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
|
3
|
+
"name": "makersclaw",
|
|
4
|
+
"homepage": "https://makersclaw.com",
|
|
5
|
+
"items": [
|
|
6
|
+
{
|
|
7
|
+
"name": "core",
|
|
8
|
+
"type": "registry:ui",
|
|
9
|
+
"title": "Makersclaw kit",
|
|
10
|
+
"description": "The kit every Makersclaw block is built from — Button, Field, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction — and the parts bound to data (RecordSwitcher, useFixture). Native elements under the project's own theme tokens.",
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "templates/next/files/components/parts/fixture.jsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "@components/makersclaw/parts/fixture.jsx"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "templates/next/files/components/parts/index.js",
|
|
19
|
+
"type": "registry:ui",
|
|
20
|
+
"target": "@components/makersclaw/parts/index.js"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "templates/next/files/components/parts/record-switcher.jsx",
|
|
24
|
+
"type": "registry:ui",
|
|
25
|
+
"target": "@components/makersclaw/parts/record-switcher.jsx"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "templates/next/files/components/parts/sample.js",
|
|
29
|
+
"type": "registry:ui",
|
|
30
|
+
"target": "@components/makersclaw/parts/sample.js"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "templates/next/files/components/ui/button.jsx",
|
|
34
|
+
"type": "registry:ui",
|
|
35
|
+
"target": "@components/makersclaw/ui/button.jsx"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "templates/next/files/components/ui/cx.js",
|
|
39
|
+
"type": "registry:ui",
|
|
40
|
+
"target": "@components/makersclaw/ui/cx.js"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"path": "templates/next/files/components/ui/depiction.jsx",
|
|
44
|
+
"type": "registry:ui",
|
|
45
|
+
"target": "@components/makersclaw/ui/depiction.jsx"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"path": "templates/next/files/components/ui/field.jsx",
|
|
49
|
+
"type": "registry:ui",
|
|
50
|
+
"target": "@components/makersclaw/ui/field.jsx"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"path": "templates/next/files/components/ui/icon.jsx",
|
|
54
|
+
"type": "registry:ui",
|
|
55
|
+
"target": "@components/makersclaw/ui/icon.jsx"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"path": "templates/next/files/components/ui/index.js",
|
|
59
|
+
"type": "registry:ui",
|
|
60
|
+
"target": "@components/makersclaw/ui/index.js"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"path": "templates/next/files/components/ui/menu.jsx",
|
|
64
|
+
"type": "registry:ui",
|
|
65
|
+
"target": "@components/makersclaw/ui/menu.jsx"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"path": "templates/next/files/components/ui/notice.jsx",
|
|
69
|
+
"type": "registry:ui",
|
|
70
|
+
"target": "@components/makersclaw/ui/notice.jsx"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"path": "templates/next/files/components/ui/sheet.jsx",
|
|
74
|
+
"type": "registry:ui",
|
|
75
|
+
"target": "@components/makersclaw/ui/sheet.jsx"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"path": "templates/next/files/components/ui/tabs.jsx",
|
|
79
|
+
"type": "registry:ui",
|
|
80
|
+
"target": "@components/makersclaw/ui/tabs.jsx"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"cssVars": {
|
|
84
|
+
"theme": {
|
|
85
|
+
"text-2xs": "0.6875rem",
|
|
86
|
+
"text-2xs--line-height": "1rem"
|
|
87
|
+
},
|
|
88
|
+
"light": {
|
|
89
|
+
"success": "oklch(0.627 0.194 149.214)",
|
|
90
|
+
"warning": "oklch(0.666 0.179 58.318)",
|
|
91
|
+
"info": "oklch(0.546 0.245 262.881)"
|
|
92
|
+
},
|
|
93
|
+
"dark": {
|
|
94
|
+
"success": "oklch(0.792 0.209 151.711)",
|
|
95
|
+
"warning": "oklch(0.828 0.189 84.429)",
|
|
96
|
+
"info": "oklch(0.707 0.165 254.624)"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"css": {
|
|
100
|
+
"@layer base": {
|
|
101
|
+
"[data-nums]": {
|
|
102
|
+
"font-variant-numeric": "tabular-nums"
|
|
103
|
+
},
|
|
104
|
+
"dialog": {
|
|
105
|
+
"color": "inherit"
|
|
106
|
+
},
|
|
107
|
+
"dialog::backdrop": {
|
|
108
|
+
"background": "color-mix(in oklab, var(--background) 70%, transparent)"
|
|
109
|
+
},
|
|
110
|
+
"[popover]": {
|
|
111
|
+
"color": "inherit",
|
|
112
|
+
"margin": "0",
|
|
113
|
+
"padding": "0",
|
|
114
|
+
"border": "0",
|
|
115
|
+
"background": "transparent"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"@layer components": {
|
|
119
|
+
"[data-depiction]": {
|
|
120
|
+
"isolation": "isolate",
|
|
121
|
+
"color-scheme": "light",
|
|
122
|
+
"font": "16px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Apple Color Emoji\", \"Segoe UI Emoji\", sans-serif",
|
|
123
|
+
"letter-spacing": "normal",
|
|
124
|
+
"word-spacing": "normal",
|
|
125
|
+
"text-align": "start",
|
|
126
|
+
"color": "CanvasText"
|
|
127
|
+
},
|
|
128
|
+
"[data-depiction][data-scheme=\"dark\"]": {
|
|
129
|
+
"color-scheme": "dark"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"docs": "Installed to components/makersclaw/ui and components/makersclaw/parts. Import: import { Button } from \"@/components/makersclaw/ui\". Needs Next.js 15+, React 19 and Tailwind v4. https://www.npmjs.com/package/makersclaw"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "board",
|
|
137
|
+
"type": "registry:block",
|
|
138
|
+
"title": "Board",
|
|
139
|
+
"description": "Records as cards in columns, one per stage; drag a card to another column (mouse, touch, or the card's menu from the keyboard) and the stage is saved.",
|
|
140
|
+
"registryDependencies": [
|
|
141
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.2.0/r/core.json"
|
|
142
|
+
],
|
|
143
|
+
"dependencies": [],
|
|
144
|
+
"files": [
|
|
145
|
+
{
|
|
146
|
+
"path": "registry/blocks/board/index.js",
|
|
147
|
+
"type": "registry:component",
|
|
148
|
+
"target": "@components/makersclaw/blocks/board/index.js"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"path": "registry/blocks/board/board.jsx",
|
|
152
|
+
"type": "registry:component",
|
|
153
|
+
"target": "@components/makersclaw/blocks/board/board.jsx"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"path": "registry/blocks/board/README.md",
|
|
157
|
+
"type": "registry:file",
|
|
158
|
+
"target": "@components/makersclaw/blocks/board/README.md"
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"categories": [
|
|
162
|
+
"board",
|
|
163
|
+
"kanban",
|
|
164
|
+
"pipeline",
|
|
165
|
+
"stages",
|
|
166
|
+
"columns",
|
|
167
|
+
"cards",
|
|
168
|
+
"drag",
|
|
169
|
+
"drop",
|
|
170
|
+
"status",
|
|
171
|
+
"workflow",
|
|
172
|
+
"funnel"
|
|
173
|
+
],
|
|
174
|
+
"docs": "Installed to components/makersclaw/blocks/board. Import: import { Board } from \"@/components/makersclaw/blocks/board\". Props and an example: components/makersclaw/blocks/board/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
175
|
+
"meta": {
|
|
176
|
+
"makersclaw": {
|
|
177
|
+
"version": "0.2.0",
|
|
178
|
+
"useWhen": "A process where each record sits in one stage and moves forward: a pipeline, a hiring funnel, a sprint, a content calendar by status, outreach by reply state. Not for records with no stages (a table) or items placed on a timeline."
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "job-panel",
|
|
184
|
+
"type": "registry:block",
|
|
185
|
+
"title": "JobPanel",
|
|
186
|
+
"description": "The panel that follows work the founder asked the app's agent for (useJob): each step's label as it runs, Stop, then done or the reason it stopped — and nothing on screen when no job runs.",
|
|
187
|
+
"registryDependencies": [
|
|
188
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.2.0/r/core.json"
|
|
189
|
+
],
|
|
190
|
+
"dependencies": [],
|
|
191
|
+
"files": [
|
|
192
|
+
{
|
|
193
|
+
"path": "registry/blocks/job-panel/index.js",
|
|
194
|
+
"type": "registry:component",
|
|
195
|
+
"target": "@components/makersclaw/blocks/job-panel/index.js"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"path": "registry/blocks/job-panel/job-panel.jsx",
|
|
199
|
+
"type": "registry:component",
|
|
200
|
+
"target": "@components/makersclaw/blocks/job-panel/job-panel.jsx"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"path": "registry/blocks/job-panel/README.md",
|
|
204
|
+
"type": "registry:file",
|
|
205
|
+
"target": "@components/makersclaw/blocks/job-panel/README.md"
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"categories": [
|
|
209
|
+
"job",
|
|
210
|
+
"agent",
|
|
211
|
+
"generate",
|
|
212
|
+
"progress",
|
|
213
|
+
"steps",
|
|
214
|
+
"running",
|
|
215
|
+
"working",
|
|
216
|
+
"commission",
|
|
217
|
+
"ai",
|
|
218
|
+
"stop",
|
|
219
|
+
"status"
|
|
220
|
+
],
|
|
221
|
+
"docs": "Installed to components/makersclaw/blocks/job-panel. Import: import { JobPanel } from \"@/components/makersclaw/blocks/job-panel\". Props and an example: components/makersclaw/blocks/job-panel/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
222
|
+
"meta": {
|
|
223
|
+
"makersclaw": {
|
|
224
|
+
"version": "0.2.0",
|
|
225
|
+
"useWhen": "A surface has a verb that asks the agent to make or find something (\"Generate a launch plan\", \"Find 20 leads\", \"Plan next sprint\") and the founder should watch it happen where the result will land."
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
@@ -7,7 +7,8 @@ saved. The move shows at once; when the write fails the card goes back and
|
|
|
7
7
|
the board says why.
|
|
8
8
|
|
|
9
9
|
```jsx
|
|
10
|
-
import { Board } from "../components/blocks/board"; // "./blocks/board" from components/
|
|
10
|
+
import { Board } from "../components/blocks/board"; // a Makersclaw app ("./blocks/board" from components/)
|
|
11
|
+
import { Board } from "@/components/makersclaw/blocks/board"; // a shadcn project
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
It is a client component: render it from a `"use client"` component, and
|
|
@@ -27,11 +28,11 @@ record goes missing; it can be dragged out of it, not into it.
|
|
|
27
28
|
- `groupBy` — the column that holds the stage, e.g. `"stage"`.
|
|
28
29
|
- `columns` — the stages in order: `["lead", "call", "won"]` (labels are
|
|
29
30
|
made from the keys) or `[{ key: "lead", label: "New lead" }, …]`.
|
|
30
|
-
- `
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
- `onMove(row, to, from)` — save the move; return `{ ok: true }` or
|
|
32
|
+
`{ ok: false, error }` (a throw counts as failed). In a Makersclaw app it
|
|
33
|
+
is one line over `moveRow` (lib/actions.js — the activity row included):
|
|
34
|
+
`onMove={(d, to) => moveRow({ source: "public.deals", id: d.id, stateColumn: "stage", to })}`.
|
|
35
|
+
Without `onMove` the board only shows; nothing can be moved.
|
|
35
36
|
- `idColumn` — default `"id"`.
|
|
36
37
|
- `titleOf(row)` — the card's title, default `row.title ?? row.name`. It
|
|
37
38
|
also names the card in its menu, in announcements and in errors.
|
|
@@ -71,7 +72,7 @@ export default async function PipelineSurface() {
|
|
|
71
72
|
// components/Pipeline.jsx — client: the board, the card, the edit sheet
|
|
72
73
|
"use client";
|
|
73
74
|
import { useState } from "react";
|
|
74
|
-
import { insertRow } from "../lib/actions";
|
|
75
|
+
import { insertRow, moveRow } from "../lib/actions";
|
|
75
76
|
import { Badge, Notice } from "./ui";
|
|
76
77
|
import { FixtureNotice, useFixture } from "./parts";
|
|
77
78
|
import { Board } from "./blocks/board";
|
|
@@ -98,7 +99,7 @@ export default function Pipeline({ deals, error }) {
|
|
|
98
99
|
rows={fixture.rows}
|
|
99
100
|
groupBy="stage"
|
|
100
101
|
columns={STAGES}
|
|
101
|
-
source
|
|
102
|
+
onMove={(d, to) => moveRow({ source: "public.deals", id: d.id, stateColumn: "stage", to })}
|
|
102
103
|
titleOf={(d) => d.name}
|
|
103
104
|
limits={{ proposal: 5 }}
|
|
104
105
|
onCreate={(name, stage) => insertRow({ source: "public.deals", values: { name, stage }, detail: name })}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
-
import { moveRow } from "../../../lib/actions";
|
|
6
5
|
import { isSample } from "../../parts";
|
|
7
6
|
import { Button, Icon, Input, Menu, Notice, cx } from "../../ui";
|
|
8
7
|
|
|
@@ -13,9 +12,12 @@ import { Button, Icon, Input, Menu, Notice, cx } from "../../ui";
|
|
|
13
12
|
// once and put back with a reason when the write fails, examples that
|
|
14
13
|
// move on screen and save nothing, the count in every header, and cards
|
|
15
14
|
// no stage claims kept in view instead of lost. The surface owns what a
|
|
16
|
-
// card says under its title
|
|
15
|
+
// card says under its title (renderDetails) and how a move is saved
|
|
16
|
+
// (onMove): the block imports nothing but React, the kit and the parts,
|
|
17
|
+
// so it runs in a Makersclaw app and in any shadcn project alike.
|
|
17
18
|
//
|
|
18
|
-
// <Board rows={deals} groupBy="stage" columns={STAGES}
|
|
19
|
+
// <Board rows={deals} groupBy="stage" columns={STAGES}
|
|
20
|
+
// onMove={(d, to) => moveRow({ source: "public.deals", id: d.id, stateColumn: "stage", to })}
|
|
19
21
|
// titleOf={(d) => d.name} renderDetails={(d) => <DealFacts deal={d} />}
|
|
20
22
|
// onOpen={setEditing} />
|
|
21
23
|
//
|
|
@@ -42,7 +44,6 @@ export function Board({
|
|
|
42
44
|
groupBy,
|
|
43
45
|
columns,
|
|
44
46
|
idColumn = "id",
|
|
45
|
-
source,
|
|
46
47
|
onMove,
|
|
47
48
|
renderDetails,
|
|
48
49
|
titleOf = (row) => row.title ?? row.name ?? String(row[idColumn] ?? ""),
|
|
@@ -71,7 +72,7 @@ export function Board({
|
|
|
71
72
|
const [busy, setBusy] = useState(false);
|
|
72
73
|
const [portal, setPortal] = useState(null);
|
|
73
74
|
|
|
74
|
-
const movable =
|
|
75
|
+
const movable = typeof onMove === "function";
|
|
75
76
|
const list = rows ?? [];
|
|
76
77
|
const keyOf = (row) => String(row?.[idColumn]);
|
|
77
78
|
const cols = useMemo(() => normalizeColumns(columns), [columns]);
|
|
@@ -121,9 +122,7 @@ export function Board({
|
|
|
121
122
|
if (isSample(row)) return;
|
|
122
123
|
let result;
|
|
123
124
|
try {
|
|
124
|
-
result = onMove
|
|
125
|
-
? await onMove(row, to, from === OTHER ? null : from)
|
|
126
|
-
: await moveRow({ source, idColumn, id: row[idColumn], stateColumn: groupBy, to });
|
|
125
|
+
result = await onMove(row, to, from === OTHER ? null : from);
|
|
127
126
|
} catch (e) {
|
|
128
127
|
result = { ok: false, error: e?.message ?? String(e) };
|
|
129
128
|
}
|
|
@@ -7,7 +7,8 @@ stopped. When no job runs it renders nothing — a panel that is always
|
|
|
7
7
|
there is a monitoring console, and apps do not have one.
|
|
8
8
|
|
|
9
9
|
```jsx
|
|
10
|
-
import { JobPanel } from "../components/blocks/job-panel"; // "./blocks/job-panel" from components/
|
|
10
|
+
import { JobPanel } from "../components/blocks/job-panel"; // a Makersclaw app ("./blocks/job-panel" from components/)
|
|
11
|
+
import { JobPanel } from "@/components/makersclaw/blocks/job-panel"; // a shadcn project
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
A client component. It takes the object `useJob()` returns (lib/hooks.js,
|
|
@@ -16,7 +17,10 @@ your surface.
|
|
|
16
17
|
|
|
17
18
|
## Props
|
|
18
19
|
|
|
19
|
-
- `job` — what `useJob()` returns. Required.
|
|
20
|
+
- `job` — what `useJob()` returns. Required. Outside Makersclaw, any
|
|
21
|
+
object of the same shape: `running`, `steps` (`[{ name, label, status:
|
|
22
|
+
"running" | "done" | "failed" }]`), `text`, `error`, `finishedAt`, and
|
|
23
|
+
`cancel()` / `reset()`.
|
|
20
24
|
- `title` — while it runs, what the agent is doing, in the founder's
|
|
21
25
|
words: `"Drafting the launch plan"`. Default `"Working on it"`.
|
|
22
26
|
- `doneTitle` — after a clean finish: `"Launch plan drafted"`.
|
package/src/shadcn.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// The free kit and blocks as a shadcn registry, so any Next.js + shadcn
|
|
2
|
+
// project can install them with the CLI it already has:
|
|
3
|
+
//
|
|
4
|
+
// npx shadcn@latest add https://cdn.jsdelivr.net/npm/makersclaw@<version>/r/board.json
|
|
5
|
+
//
|
|
6
|
+
// Two kinds of item:
|
|
7
|
+
// core the kit (components/ui) and the parts (components/parts),
|
|
8
|
+
// plus what a stock shadcn theme lacks: the success, warning
|
|
9
|
+
// and info colours, the text-2xs size, and four CSS rules
|
|
10
|
+
// <block> one block, which lists core as a registry dependency, so
|
|
11
|
+
// `add board` brings the kit along the first time
|
|
12
|
+
//
|
|
13
|
+
// Where it lands: components/makersclaw/{ui,parts,blocks/<name>} — the
|
|
14
|
+
// same relative shape as a Makersclaw app's components/, so every import
|
|
15
|
+
// inside the kit and the blocks (../../ui, ../../parts, ./sample) is
|
|
16
|
+
// relative and resolves unchanged; nothing needs rewriting, and nothing
|
|
17
|
+
// collides with the project's own components/ui. The files are served by
|
|
18
|
+
// jsDelivr straight from the npm tarball (`r/` is built at `npm pack`).
|
|
19
|
+
|
|
20
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { basename, join } from "node:path";
|
|
22
|
+
import { PKG_ROOT, loadBlocks, loadTemplate, packageInfo } from "./registry.mjs";
|
|
23
|
+
|
|
24
|
+
const ITEM_SCHEMA = "https://ui.shadcn.com/schema/registry-item.json";
|
|
25
|
+
const INDEX_SCHEMA = "https://ui.shadcn.com/schema/registry.json";
|
|
26
|
+
/** Under the project's components folder: @components/ is the CLI's
|
|
27
|
+
* placeholder for it, and what follows is kept as written. */
|
|
28
|
+
export const SHADCN_ROOT = "@components/makersclaw";
|
|
29
|
+
|
|
30
|
+
/** Where the built registry is served from: this version's own files on
|
|
31
|
+
* jsDelivr, so a block's dependency on core names exactly its kit. */
|
|
32
|
+
export function defaultBase(version = packageInfo().version) {
|
|
33
|
+
return `https://cdn.jsdelivr.net/npm/makersclaw@${version}/r`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// What a stock shadcn theme lacks and the kit uses. Tailwind's own
|
|
37
|
+
// green/amber/blue at 600 on light and 400 on dark — status colours,
|
|
38
|
+
// readable on either ground.
|
|
39
|
+
const CSS_VARS = {
|
|
40
|
+
theme: {
|
|
41
|
+
"text-2xs": "0.6875rem",
|
|
42
|
+
"text-2xs--line-height": "1rem",
|
|
43
|
+
},
|
|
44
|
+
light: {
|
|
45
|
+
success: "oklch(0.627 0.194 149.214)",
|
|
46
|
+
warning: "oklch(0.666 0.179 58.318)",
|
|
47
|
+
info: "oklch(0.546 0.245 262.881)",
|
|
48
|
+
},
|
|
49
|
+
dark: {
|
|
50
|
+
success: "oklch(0.792 0.209 151.711)",
|
|
51
|
+
warning: "oklch(0.828 0.189 84.429)",
|
|
52
|
+
info: "oklch(0.707 0.165 254.624)",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// The rules the kit leans on (the template's app/globals.css): tabular
|
|
57
|
+
// numbers, the native dialog and popover the Sheet and the Menu sit on,
|
|
58
|
+
// and the boundary where a depiction stops wearing the theme.
|
|
59
|
+
const CSS = {
|
|
60
|
+
"@layer base": {
|
|
61
|
+
"[data-nums]": { "font-variant-numeric": "tabular-nums" },
|
|
62
|
+
dialog: { color: "inherit" },
|
|
63
|
+
"dialog::backdrop": { background: "color-mix(in oklab, var(--background) 70%, transparent)" },
|
|
64
|
+
"[popover]": { color: "inherit", margin: "0", padding: "0", border: "0", background: "transparent" },
|
|
65
|
+
},
|
|
66
|
+
"@layer components": {
|
|
67
|
+
"[data-depiction]": {
|
|
68
|
+
isolation: "isolate",
|
|
69
|
+
"color-scheme": "light",
|
|
70
|
+
font: '16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif',
|
|
71
|
+
"letter-spacing": "normal",
|
|
72
|
+
"word-spacing": "normal",
|
|
73
|
+
"text-align": "start",
|
|
74
|
+
color: "CanvasText",
|
|
75
|
+
},
|
|
76
|
+
'[data-depiction][data-scheme="dark"]': { "color-scheme": "dark" },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const README_URL = "https://www.npmjs.com/package/makersclaw";
|
|
81
|
+
|
|
82
|
+
function coreItem() {
|
|
83
|
+
const tpl = loadTemplate("next");
|
|
84
|
+
const files = tpl.files
|
|
85
|
+
.filter((f) => /^components\/(?:ui|parts)\//.test(f.path))
|
|
86
|
+
.map((f) => ({
|
|
87
|
+
path: `templates/next/files/${f.path}`,
|
|
88
|
+
type: "registry:ui",
|
|
89
|
+
target: `${SHADCN_ROOT}/${f.path.slice("components/".length)}`,
|
|
90
|
+
content: readFileSync(f.from, "utf8"),
|
|
91
|
+
}));
|
|
92
|
+
return {
|
|
93
|
+
$schema: ITEM_SCHEMA,
|
|
94
|
+
name: "core",
|
|
95
|
+
type: "registry:ui",
|
|
96
|
+
title: "Makersclaw kit",
|
|
97
|
+
description:
|
|
98
|
+
"The kit every Makersclaw block is built from — Button, Field, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction — " +
|
|
99
|
+
"and the parts bound to data (RecordSwitcher, useFixture). Native elements under the project's own theme tokens.",
|
|
100
|
+
files,
|
|
101
|
+
cssVars: CSS_VARS,
|
|
102
|
+
css: CSS,
|
|
103
|
+
docs: `Installed to components/makersclaw/ui and components/makersclaw/parts. Import: import { Button } from "@/components/makersclaw/ui". Needs Next.js 15+, React 19 and Tailwind v4. ${README_URL}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function blockItem(block, base, version) {
|
|
108
|
+
const files = block.files.map((f) => ({
|
|
109
|
+
path: `registry/blocks/${block.name}/${basename(f.from)}`,
|
|
110
|
+
type: "registry:component",
|
|
111
|
+
target: `${SHADCN_ROOT}/blocks/${block.name}/${basename(f.from)}`,
|
|
112
|
+
content: readFileSync(f.from, "utf8"),
|
|
113
|
+
}));
|
|
114
|
+
if (block.readme) {
|
|
115
|
+
// Beside the code, where the developer and their assistant will look.
|
|
116
|
+
files.push({
|
|
117
|
+
path: `registry/blocks/${block.name}/README.md`,
|
|
118
|
+
type: "registry:file",
|
|
119
|
+
target: `${SHADCN_ROOT}/blocks/${block.name}/README.md`,
|
|
120
|
+
content: readFileSync(block.readme, "utf8"),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
$schema: ITEM_SCHEMA,
|
|
125
|
+
name: block.name,
|
|
126
|
+
type: "registry:block",
|
|
127
|
+
title: block.title,
|
|
128
|
+
description: block.description,
|
|
129
|
+
registryDependencies: [`${base}/core.json`, ...block.requires.map((r) => `${base}/${r}.json`)],
|
|
130
|
+
dependencies: Object.entries(block.dependencies ?? {}).map(([name, range]) => `${name}@${range}`),
|
|
131
|
+
files,
|
|
132
|
+
categories: block.tags,
|
|
133
|
+
docs:
|
|
134
|
+
`Installed to components/makersclaw/blocks/${block.name}. ` +
|
|
135
|
+
`Import: import { ${block.exports.join(", ")} } from "@/components/makersclaw/blocks/${block.name}". ` +
|
|
136
|
+
`Props and an example: components/makersclaw/blocks/${block.name}/README.md. ` +
|
|
137
|
+
`Makersclaw builds whole apps from these blocks: https://makersclaw.com`,
|
|
138
|
+
meta: { makersclaw: { version, useWhen: block.useWhen } },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Every item, with its files' content, and the index shadcn's `list` and
|
|
143
|
+
* `search` read (the same items, without content). */
|
|
144
|
+
export function buildRegistry({ base = defaultBase(), version = packageInfo().version } = {}) {
|
|
145
|
+
const items = [coreItem(), ...loadBlocks().map((b) => blockItem(b, base, version))];
|
|
146
|
+
const index = {
|
|
147
|
+
$schema: INDEX_SCHEMA,
|
|
148
|
+
name: "makersclaw",
|
|
149
|
+
homepage: "https://makersclaw.com",
|
|
150
|
+
items: items.map(({ $schema, ...item }) => ({ ...item, files: item.files.map(({ content, ...f }) => f) })),
|
|
151
|
+
};
|
|
152
|
+
return { base, version, items, index };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Write r/<name>.json for every item and r/registry.json. */
|
|
156
|
+
export function writeRegistry(outDir = join(PKG_ROOT, "r"), opts = {}) {
|
|
157
|
+
const built = buildRegistry(opts);
|
|
158
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
159
|
+
mkdirSync(outDir, { recursive: true });
|
|
160
|
+
for (const item of built.items) writeFileSync(join(outDir, `${item.name}.json`), `${JSON.stringify(item, null, 2)}\n`);
|
|
161
|
+
writeFileSync(join(outDir, "registry.json"), `${JSON.stringify(built.index, null, 2)}\n`);
|
|
162
|
+
return { ...built, outDir };
|
|
163
|
+
}
|
|
@@ -48,7 +48,7 @@ const STATIC_ONLY = process.argv.includes("--static");
|
|
|
48
48
|
// update` lays both folders again.
|
|
49
49
|
// Exact names AND extensions: Next resolves .js before .jsx, so a
|
|
50
50
|
// components/ui/depiction.js beside depiction.jsx would win unseen.
|
|
51
|
-
const KIT = /^components\/(?:ui\/(?:cx|index)\.js|ui\/(?:button|depiction|field|icon|menu|notice|sheet|tabs)\.jsx|parts\/index\.js|parts\/(?:fixture|record-switcher)\.jsx)$/;
|
|
51
|
+
const KIT = /^components\/(?:ui\/(?:cx|index)\.js|ui\/(?:button|depiction|field|icon|menu|notice|sheet|tabs)\.jsx|parts\/(?:index|sample)\.js|parts\/(?:fixture|record-switcher)\.jsx)$/;
|
|
52
52
|
|
|
53
53
|
// The pins: sha256 of every fixed file, written by `makersclaw init` and
|
|
54
54
|
// `makersclaw add` (check/pins.json). Read first, because a BLOCK is
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
|
-
import { markSample } from "
|
|
4
|
+
import { markSample } from "./sample";
|
|
5
5
|
import { Button, Notice } from "../ui";
|
|
6
6
|
|
|
7
7
|
// The worked example, never persisted. A surface opens populated (README
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Pure: importable from either side. The parts use it in the browser
|
|
2
|
+
// (fixture.jsx); lib/sample.js re-exports it for server code (lib/data.js).
|
|
3
|
+
// It lives beside the parts, not in lib/, so the parts travel as one folder
|
|
4
|
+
// — into an app's components/parts, or a shadcn project's
|
|
5
|
+
// components/makersclaw/parts — and import nothing outside it but the kit.
|
|
6
|
+
|
|
7
|
+
/** Sample rows get stable ids and a `_sample: true` marker, so a surface
|
|
8
|
+
* can label them until real rows exist and refuse to edit them. */
|
|
9
|
+
export function markSample(sample, idColumn = "id") {
|
|
10
|
+
return (sample ?? []).map((r, i) => ({
|
|
11
|
+
...r,
|
|
12
|
+
[idColumn]: r[idColumn] ?? `sample-${i + 1}`,
|
|
13
|
+
_sample: true,
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* can label them until real rows exist and refuse to edit them. */
|
|
6
|
-
export function markSample(sample, idColumn = "id") {
|
|
7
|
-
return (sample ?? []).map((r, i) => ({
|
|
8
|
-
...r,
|
|
9
|
-
[idColumn]: r[idColumn] ?? `sample-${i + 1}`,
|
|
10
|
-
_sample: true,
|
|
11
|
-
}));
|
|
12
|
-
}
|
|
1
|
+
// markSample, for server code (lib/data.js re-exports it). The function
|
|
2
|
+
// lives with the parts (components/parts/sample.js) so they travel as one
|
|
3
|
+
// folder; this file keeps the lib/ import working.
|
|
4
|
+
export { markSample } from "../components/parts/sample";
|