makersclaw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +68 -0
  2. package/bin/makersclaw.mjs +10 -0
  3. package/package.json +34 -0
  4. package/registry/blocks/board/README.md +130 -0
  5. package/registry/blocks/board/block.json +10 -0
  6. package/registry/blocks/board/board.jsx +435 -0
  7. package/registry/blocks/board/demo.jsx +50 -0
  8. package/registry/blocks/board/index.js +4 -0
  9. package/registry/blocks/job-panel/README.md +68 -0
  10. package/registry/blocks/job-panel/block.json +10 -0
  11. package/registry/blocks/job-panel/demo.jsx +75 -0
  12. package/registry/blocks/job-panel/index.js +3 -0
  13. package/registry/blocks/job-panel/job-panel.jsx +110 -0
  14. package/src/cli.mjs +301 -0
  15. package/src/fsutil.mjs +76 -0
  16. package/src/install.mjs +457 -0
  17. package/src/registry.mjs +191 -0
  18. package/templates/next/files/README.md +626 -0
  19. package/templates/next/files/app/auto-refresh.jsx +26 -0
  20. package/templates/next/files/app/error-relay.jsx +53 -0
  21. package/templates/next/files/app/error.jsx +44 -0
  22. package/templates/next/files/app/global-error.jsx +36 -0
  23. package/templates/next/files/app/globals.css +178 -0
  24. package/templates/next/files/app/layout.jsx +51 -0
  25. package/templates/next/files/app/not-found.jsx +23 -0
  26. package/templates/next/files/app/page.jsx +22 -0
  27. package/templates/next/files/app/s/[key]/page.jsx +47 -0
  28. package/templates/next/files/app/shell.jsx +64 -0
  29. package/templates/next/files/check/run.mjs +683 -0
  30. package/templates/next/files/components/parts/fixture.jsx +60 -0
  31. package/templates/next/files/components/parts/index.js +5 -0
  32. package/templates/next/files/components/parts/record-switcher.jsx +49 -0
  33. package/templates/next/files/components/ui/button.jsx +81 -0
  34. package/templates/next/files/components/ui/cx.js +4 -0
  35. package/templates/next/files/components/ui/depiction.jsx +66 -0
  36. package/templates/next/files/components/ui/field.jsx +126 -0
  37. package/templates/next/files/components/ui/icon.jsx +87 -0
  38. package/templates/next/files/components/ui/index.js +12 -0
  39. package/templates/next/files/components/ui/menu.jsx +123 -0
  40. package/templates/next/files/components/ui/notice.jsx +86 -0
  41. package/templates/next/files/components/ui/sheet.jsx +89 -0
  42. package/templates/next/files/components/ui/tabs.jsx +81 -0
  43. package/templates/next/files/lib/actions.js +187 -0
  44. package/templates/next/files/lib/claw.js +219 -0
  45. package/templates/next/files/lib/data.js +171 -0
  46. package/templates/next/files/lib/db.js +72 -0
  47. package/templates/next/files/lib/hooks.js +218 -0
  48. package/templates/next/files/lib/platform.js +59 -0
  49. package/templates/next/files/lib/sample.js +12 -0
  50. package/templates/next/files/middleware.js +42 -0
  51. package/templates/next/files/next.config.mjs +41 -0
  52. package/templates/next/files/package.json +19 -0
  53. package/templates/next/files/postcss.config.mjs +3 -0
  54. package/templates/next/files/schema.sql +22 -0
  55. package/templates/next/files/surfaces/README.md +32 -0
  56. package/templates/next/files/surfaces.js +53 -0
  57. package/templates/next/template.json +118 -0
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { claw, publishStatus, reportError } from "../lib/claw";
5
+
6
+ // Errors React does not catch — a handler throwing, a rejected promise —
7
+ // still reach Makersclaw: window.onerror and unhandledrejection post them
8
+ // to _claw/events, where the platform agent reads them as console events.
9
+ // The error boundaries (error.jsx, global-error.jsx) cover render errors.
10
+ //
11
+ // It also keeps <html data-claw-status> current from _claw/me (on mount
12
+ // and whenever the tab comes back), so reportError can tag an error
13
+ // "preview" while the artifact is a draft. This is the fixed layer's job:
14
+ // the shell is the agent's file and may never call useArtifact().
15
+ export default function ErrorRelay() {
16
+ useEffect(() => {
17
+ const controller = new AbortController();
18
+ const publish = async () => {
19
+ const r = await claw("me", { signal: controller.signal });
20
+ if (r.ok) publishStatus(r.data?.artifact?.status);
21
+ };
22
+ const onVisible = () => {
23
+ if (document.visibilityState === "visible") publish();
24
+ };
25
+ const onError = (event) => {
26
+ const err = event.error;
27
+ reportError({
28
+ message: err?.message || event.message || "error",
29
+ stack: err?.stack,
30
+ url: location.href,
31
+ });
32
+ };
33
+ const onRejection = (event) => {
34
+ const r = event.reason;
35
+ reportError({
36
+ message: r?.message || (typeof r === "string" ? r : "unhandled rejection"),
37
+ stack: r?.stack,
38
+ url: location.href,
39
+ });
40
+ };
41
+ publish();
42
+ document.addEventListener("visibilitychange", onVisible);
43
+ window.addEventListener("error", onError);
44
+ window.addEventListener("unhandledrejection", onRejection);
45
+ return () => {
46
+ controller.abort();
47
+ document.removeEventListener("visibilitychange", onVisible);
48
+ window.removeEventListener("error", onError);
49
+ window.removeEventListener("unhandledrejection", onRejection);
50
+ };
51
+ }, []);
52
+ return null;
53
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { reportError } from "../lib/claw";
5
+
6
+ // The route-level error boundary: a surface that throws while rendering
7
+ // lands here, inside the shell, and the error is posted to _claw/events so
8
+ // the platform agent sees it. Next.js strips server error messages in
9
+ // production; `digest` is what the server log can be searched for.
10
+ export default function SurfaceError({ error, reset }) {
11
+ const [reported, setReported] = useState(null);
12
+ useEffect(() => {
13
+ reportError({
14
+ message: error?.message || "render error",
15
+ stack: [error?.stack, error?.digest ? `digest: ${error.digest}` : ""].filter(Boolean).join("\n"),
16
+ url: location.href,
17
+ }).then((r) => setReported(Boolean(r?.ok)));
18
+ }, [error]);
19
+
20
+ return (
21
+ <div className="max-w-prose">
22
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">Something broke on this surface</p>
23
+ <h1 className="mt-1 text-lg font-semibold">The page could not render</h1>
24
+ <p className="mt-2 text-sm text-muted-foreground">
25
+ {error?.message || "An error happened while rendering."}
26
+ {error?.digest ? <span className="font-mono text-xs text-muted-foreground"> ({error.digest})</span> : null}
27
+ </p>
28
+ <p className="mt-2 text-xs text-muted-foreground">
29
+ {reported === true
30
+ ? "Makersclaw has been told and will fix it."
31
+ : reported === false
32
+ ? "Makersclaw could not be reached to report this."
33
+ : "Reporting to Makersclaw…"}
34
+ </p>
35
+ <button
36
+ type="button"
37
+ onClick={() => reset()}
38
+ className="mt-4 rounded-md border border-border px-3 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
39
+ >
40
+ Try again
41
+ </button>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import "./globals.css";
4
+ import { useEffect } from "react";
5
+ import { reportError } from "../lib/claw";
6
+
7
+ // The last boundary: an error in the root layout itself. It must render its
8
+ // own <html> and <body> (the layout is gone, so it imports globals.css
9
+ // itself), and it reports the same way as error.jsx.
10
+ export default function GlobalError({ error, reset }) {
11
+ useEffect(() => {
12
+ reportError({
13
+ message: error?.message || "layout error",
14
+ stack: [error?.stack, error?.digest ? `digest: ${error.digest}` : ""].filter(Boolean).join("\n"),
15
+ url: location.href,
16
+ });
17
+ }, [error]);
18
+
19
+ return (
20
+ <html lang="en" data-theme="dark">
21
+ <body className="bg-background p-10 font-sans text-foreground">
22
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">Something broke in the shell</p>
23
+ <h1 className="mt-1 text-lg font-semibold">The app could not render</h1>
24
+ <p className="mt-2 text-sm text-muted-foreground">{error?.message || "An error happened while rendering."}</p>
25
+ <p className="mt-2 text-xs text-muted-foreground">Makersclaw has been told.</p>
26
+ <button
27
+ type="button"
28
+ onClick={() => reset()}
29
+ className="mt-4 rounded-md border border-border px-3 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
30
+ >
31
+ Try again
32
+ </button>
33
+ </body>
34
+ </html>
35
+ );
36
+ }
@@ -0,0 +1,178 @@
1
+ /* Tailwind reads exactly the files `npm run check` reads — never a .json,
2
+ a dot-file or public/ — so a class name can only reach the page from a
3
+ file the rules have seen. Paths are relative to this file. */
4
+ @import "tailwindcss" source(none);
5
+ @source "../app/**/*.{js,jsx}";
6
+ @source "../components/**/*.{js,jsx}";
7
+ @source "../surfaces/**/*.{js,jsx}";
8
+ @source "../lib/**/*.js";
9
+ @source "../surfaces.js";
10
+
11
+ /* Dark is a data-theme on <html>, set by app/layout.jsx from the desk
12
+ (x-claw-theme, then the claw_theme cookie), never the system preference:
13
+ the OS window decides. Kept for `dark:`; with the workspace theme below
14
+ you should not need it — every token already flips with the preset. */
15
+ @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
16
+
17
+ /* ------------------------------------------------------------------------
18
+ THE WORKSPACE THEME. Not yours to edit.
19
+
20
+ Every colour, face and radius in this app comes from the workspace —
21
+ System Settings → Appearance on the desk — as the stylesheet
22
+ `_claw/theme.css`, linked from app/layout.jsx. That sheet sets the
23
+ variables of the first :root block; the values here are the Makersclaw
24
+ preset, so the app looks right with no sheet (local `next dev`) and
25
+ every app in a workspace looks like the desk and like each other.
26
+
27
+ The slots (--card, --primary, …) are what a preset may pin; when it
28
+ does not, the fallback derives them from the three-colour palette, the
29
+ same maths the desk uses. Use the utilities (bg-card, text-primary,
30
+ border-border, rounded-lg, font-sans…) and nothing else: never a hex,
31
+ rgb(), hsl() or oklch() in this app. `npm run check` refuses one.
32
+ ------------------------------------------------------------------------ */
33
+ @layer base {
34
+ :root {
35
+ color-scheme: dark;
36
+
37
+ --background-base: #1c1c1c;
38
+ --background-alpha: 1;
39
+ --background: color-mix(in srgb, #1c1c1c 100%, transparent);
40
+ --midground-base: #fbe9cf;
41
+ --midground-alpha: 1;
42
+ --midground: color-mix(in srgb, #fbe9cf 100%, transparent);
43
+ --foreground-base: #ffffff;
44
+ --foreground-alpha: 0;
45
+ --foreground: color-mix(in srgb, #ffffff 0%, transparent);
46
+
47
+ --theme-font-sans: "Schibsted Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
48
+ --theme-font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
49
+ --theme-font-display: var(--theme-font-sans);
50
+ --theme-base-size: 15px;
51
+ --theme-line-height: 1.55;
52
+ --theme-letter-spacing: -0.01em;
53
+
54
+ --radius: 0.5rem;
55
+ --theme-radius: 0.5rem;
56
+ --theme-spacing-mul: 1;
57
+ --theme-density: comfortable;
58
+ }
59
+
60
+ html {
61
+ font-family: var(--theme-font-sans);
62
+ font-size: var(--theme-base-size);
63
+ line-height: var(--theme-line-height);
64
+ letter-spacing: var(--theme-letter-spacing);
65
+ }
66
+
67
+ body {
68
+ min-height: 100dvh;
69
+ background: var(--color-background);
70
+ color: var(--color-foreground);
71
+ }
72
+
73
+ [data-nums] {
74
+ font-variant-numeric: tabular-nums;
75
+ }
76
+
77
+ /* Native controls the kit wraps (components/ui): the workspace scheme,
78
+ not the browser's default chrome. */
79
+ select,
80
+ input,
81
+ textarea,
82
+ button {
83
+ font: inherit;
84
+ letter-spacing: inherit;
85
+ color: inherit;
86
+ }
87
+
88
+ dialog {
89
+ color: inherit;
90
+ }
91
+
92
+ dialog::backdrop {
93
+ background: color-mix(in srgb, var(--background-base) 70%, transparent);
94
+ }
95
+
96
+ [popover] {
97
+ color: inherit;
98
+ margin: 0;
99
+ padding: 0;
100
+ border: 0;
101
+ background: transparent;
102
+ }
103
+ }
104
+
105
+ /* The boundary where the workspace theme stops: a depiction (README §3c),
106
+ framed by the kit's <Depiction>, is a picture of something that exists
107
+ elsewhere and wears that place's colours and face. `font` shorthand
108
+ resets family, size, weight, line-height and every font-variant in one
109
+ go (undoing the preset's face and [data-nums]); letter-spacing and
110
+ line-height undo html's; CanvasText follows the box's own color-scheme;
111
+ the density multiplier is pinned so a slipped p-4 scales less wrongly
112
+ (depictions are px-only regardless — rem is the workspace's base size
113
+ and no wrapper can undo it). Never `all: initial`: it would not reset
114
+ custom properties, and font-family: initial is Times. */
115
+ @layer components {
116
+ [data-depiction] {
117
+ isolation: isolate;
118
+ color-scheme: light;
119
+ font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
120
+ letter-spacing: normal;
121
+ word-spacing: normal;
122
+ text-align: start;
123
+ color: CanvasText;
124
+ --theme-spacing-mul: 1;
125
+ --default-mono-font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
126
+ }
127
+ [data-depiction][data-scheme="dark"] {
128
+ color-scheme: dark;
129
+ }
130
+ }
131
+
132
+ /* The tokens the utilities read. A slot the preset pinned wins; otherwise
133
+ the palette derives it. Mixes are opaque (mixed WITH the canvas, never
134
+ alpha on text) so type keeps its contrast on every ground. */
135
+ @theme inline {
136
+ --color-background: var(--background-base);
137
+ --color-foreground: var(--midground-base);
138
+
139
+ --color-card: var(--card, color-mix(in srgb, var(--midground-base) 4%, var(--background-base)));
140
+ --color-card-foreground: var(--card-foreground, var(--midground-base));
141
+ --color-popover: var(--popover, color-mix(in srgb, var(--midground-base) 6%, var(--background-base)));
142
+ --color-popover-foreground: var(--popover-foreground, var(--midground-base));
143
+
144
+ --color-primary: var(--primary, var(--midground-base));
145
+ --color-primary-foreground: var(--primary-foreground, var(--background-base));
146
+ --color-secondary: var(--secondary, color-mix(in srgb, var(--midground-base) 8%, var(--background-base)));
147
+ --color-secondary-foreground: var(--secondary-foreground, var(--midground-base));
148
+ --color-muted: var(--muted, color-mix(in srgb, var(--midground-base) 8%, var(--background-base)));
149
+ --color-muted-foreground: var(--muted-foreground, color-mix(in srgb, var(--midground-base) 72%, var(--background-base)));
150
+ --color-accent: var(--accent, color-mix(in srgb, var(--midground-base) 12%, var(--background-base)));
151
+ --color-accent-foreground: var(--accent-foreground, var(--midground-base));
152
+
153
+ --color-destructive: var(--destructive, #fb2c36);
154
+ --color-destructive-foreground: var(--destructive-foreground, #ffffff);
155
+ --color-success: var(--success, #4ade80);
156
+ --color-warning: var(--warning, #ffbd38);
157
+ --color-info: var(--info, #60a5fa);
158
+
159
+ --color-border: var(--border, color-mix(in srgb, var(--midground-base) 15%, transparent));
160
+ --color-input: var(--input, color-mix(in srgb, var(--midground-base) 15%, transparent));
161
+ --color-ring: var(--ring, var(--midground-base));
162
+
163
+ --radius-sm: calc(var(--theme-radius) - 4px);
164
+ --radius-md: calc(var(--theme-radius) - 2px);
165
+ --radius-lg: var(--theme-radius);
166
+ --radius-xl: calc(var(--theme-radius) + 4px);
167
+
168
+ --font-sans: var(--theme-font-sans);
169
+ --font-mono: var(--theme-font-mono);
170
+ --font-display: var(--theme-font-display);
171
+
172
+ /* Density: the preset's multiplier scales every p-N / gap-N / space-N. */
173
+ --spacing: calc(0.25rem * var(--theme-spacing-mul, 1));
174
+
175
+ /* One sub-xs tier for dense captions; the only size below text-xs. */
176
+ --text-2xs: 0.6875rem;
177
+ --text-2xs--line-height: 1rem;
178
+ }
@@ -0,0 +1,51 @@
1
+ import "./globals.css";
2
+ import { cookies, headers } from "next/headers";
3
+ import { surfaceNav } from "../surfaces";
4
+ import AutoRefresh from "./auto-refresh";
5
+ import ErrorRelay from "./error-relay";
6
+ import Shell from "./shell";
7
+
8
+ // FIXED — the platform agent does not edit this file. Design lives in
9
+ // app/shell.jsx and the surfaces; colour lives nowhere in the app.
10
+ //
11
+ // Nothing here is prerendered: the theme comes from a cookie and the
12
+ // surfaces read the workspace database, so `next build` never needs data.
13
+ export const dynamic = "force-dynamic";
14
+
15
+ export const metadata = { title: process.env.CLAW_PRODUCT || "Artifact" };
16
+
17
+ // <html> carries what the browser-side code reads: the base path for
18
+ // lib/claw.js, the product name, and data-theme. The theme follows the
19
+ // desk: the OS window opens the app with ?theme=light|dark and the
20
+ // product proxy repeats it as the x-claw-theme header on every request
21
+ // (middleware.js explains); the claw_theme cookie covers a run without
22
+ // the proxy. Dark when neither says — the desk is dark.
23
+ //
24
+ // The palette follows the WORKSPACE: <head> links `_claw/theme.css`, the
25
+ // preset picked in System Settings → Appearance, served by the product
26
+ // proxy with the page's own cookie as identity (never the sandbox). A
27
+ // stylesheet in <head> blocks first paint, so there is no flash of the
28
+ // fallback; app/globals.css holds the same preset as defaults for a run
29
+ // with no proxy (local `next dev`, where the link 404s harmlessly). The
30
+ // href is prefixed by hand: Next's basePath applies to next/link and
31
+ // assets, not to a literal <link>.
32
+ export default async function RootLayout({ children }) {
33
+ const basePath = process.env.CLAW_BASE_PATH || "";
34
+ const name = process.env.CLAW_PRODUCT || "Artifact";
35
+ const wanted = (await headers()).get("x-claw-theme") || (await cookies()).get("claw_theme")?.value;
36
+ const theme = wanted === "light" ? "light" : "dark";
37
+ return (
38
+ <html lang="en" data-claw-base-path={basePath} data-claw-product={name} data-theme={theme}>
39
+ <head>
40
+ <link rel="stylesheet" href={`${basePath}/_claw/theme.css`} />
41
+ </head>
42
+ <body>
43
+ <AutoRefresh />
44
+ <ErrorRelay />
45
+ <Shell name={name} surfaces={surfaceNav()}>
46
+ {children}
47
+ </Shell>
48
+ </body>
49
+ </html>
50
+ );
51
+ }
@@ -0,0 +1,23 @@
1
+ import Link from "next/link";
2
+ import { homeSurface } from "../surfaces";
3
+
4
+ // The manifest can be empty (app/page.jsx explains), so the way back is
5
+ // "/" then — never a link built from an entry that is not there.
6
+ export default function NotFound() {
7
+ const home = homeSurface();
8
+ return (
9
+ <div className="max-w-prose">
10
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">Not here</p>
11
+ <h1 className="mt-1 text-lg font-semibold">No such surface</h1>
12
+ <p className="mt-2 text-sm text-muted-foreground">This app has no surface at this address.</p>
13
+ <p className="mt-4 text-sm">
14
+ <Link
15
+ href={home ? `/s/${encodeURIComponent(home.key)}` : "/"}
16
+ className="underline outline-none focus-visible:ring-2 focus-visible:ring-ring"
17
+ >
18
+ {home ? `Go to ${home.label}` : "Go to the start"}
19
+ </Link>
20
+ </p>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,22 @@
1
+ import { redirect } from "next/navigation";
2
+ import { homeSurface } from "../surfaces";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ // "/" is never a page of its own: it sends the viewer to the home surface —
7
+ // the entry flagged `home` in surfaces.js, else the first one. With no
8
+ // entry at all (the template as shipped, or an app whose blueprint lists
9
+ // nothing yet) there is nowhere to send them, so it says so instead of
10
+ // redirecting to /s/undefined. Plain Tailwind: this page is seen for
11
+ // seconds, before the platform agent writes the manifest.
12
+ export default function Home() {
13
+ const home = homeSurface();
14
+ if (home) redirect(`/s/${encodeURIComponent(home.key)}`);
15
+ return (
16
+ <div className="max-w-prose">
17
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">Empty</p>
18
+ <h1 className="mt-1 text-lg font-semibold">No surfaces yet.</h1>
19
+ <p className="mt-2 text-sm text-muted-foreground">Makersclaw is listing them.</p>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,47 @@
1
+ import { notFound } from "next/navigation";
2
+ import surfaces from "../../../surfaces";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export async function generateMetadata({ params }) {
7
+ const { key } = await params;
8
+ const s = surfaces.find((x) => x.key === key);
9
+ const name = process.env.CLAW_PRODUCT || "Artifact";
10
+ return { title: s ? `${s.label} · ${name}` : name };
11
+ }
12
+
13
+ // One route for every surface. A manifest entry without a component is
14
+ // planned — the platform agent has named it but not built it yet — and
15
+ // says so; a built one renders its component with the entry's plain data
16
+ // (never the component itself: a client surface cannot receive a function).
17
+ export default async function SurfacePage({ params, searchParams }) {
18
+ const { key } = await params;
19
+ const s = surfaces.find((x) => x.key === key);
20
+ if (!s) notFound();
21
+ if (!s.component) return <Planned surface={s} />;
22
+ const Surface = s.component;
23
+ const sp = (await searchParams) ?? {};
24
+ return (
25
+ <Surface surface={{ key: s.key, kind: s.kind ?? null, label: s.label, source: s.source ?? null }} searchParams={sp} />
26
+ );
27
+ }
28
+
29
+ function Planned({ surface }) {
30
+ return (
31
+ <div className="max-w-prose">
32
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">Planned</p>
33
+ <h1 className="mt-1 text-lg font-semibold">Makersclaw is building this</h1>
34
+ <p className="mt-2 text-sm text-muted-foreground">
35
+ {surface.label}
36
+ {surface.kind ? ` will be a ${surface.kind}` : null}
37
+ {surface.source ? (
38
+ <>
39
+ {" "}
40
+ over <code className="font-mono text-xs">{surface.source}</code>
41
+ </>
42
+ ) : null}
43
+ . It appears here as soon as it is built.
44
+ </p>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ // STARTER — replace this file first. You design the inside of the window:
4
+ // nav, layout, type, density. NOT colour: every colour here is a token
5
+ // (bg-background, text-muted-foreground, border-border…) that the
6
+ // workspace's theme fills in, so keep using tokens in what you write.
7
+ // The OS window already shows the app's name and its run controls
8
+ // (Pause, Run now, Edit), and its tab strip carries Agent, Runs and
9
+ // Activity; never repeat any of them. This shell is the App tab: the
10
+ // domain surfaces and nothing else.
11
+ //
12
+ // Mobile first: this starter is one column with the surfaces as a strip
13
+ // along the top, and grows a side rail from 640px. Start narrow, widen.
14
+ //
15
+ // ONE THING TO KEEP when you replace it: `next/link`. The app runs under
16
+ // a base path and <Link> applies it; a plain <a href="/s/..."> resolves
17
+ // against the origin instead and the click leaves the app, loading the
18
+ // Makersclaw dashboard inside this frame. The page still answers 200, so
19
+ // nothing catches it but a click (README §4).
20
+ //
21
+ // Props: name (the app's name — for aria labels, not for display),
22
+ // surfaces (surfaceNav() from surfaces.js: { key, label, kind, source,
23
+ // home, built }), children (the current surface).
24
+
25
+ import Link from "next/link";
26
+ import { usePathname } from "next/navigation";
27
+ import { cx } from "../components/ui";
28
+
29
+ export default function Shell({ name, surfaces, children }) {
30
+ const pathname = usePathname() ?? "";
31
+ const activeKey = decodeURIComponent(/^\/s\/([^/?#]+)/.exec(pathname)?.[1] ?? "");
32
+
33
+ return (
34
+ <div className="flex min-h-dvh flex-col bg-background text-foreground sm:flex-row">
35
+ <nav
36
+ aria-label={`${name} surfaces`}
37
+ className="shrink-0 overflow-x-auto border-b border-border p-2 sm:w-44 sm:overflow-visible sm:border-b-0 sm:border-r sm:p-3"
38
+ >
39
+ <ul className="flex gap-1 sm:flex-col">
40
+ {surfaces.map((s) => {
41
+ const active = s.key === activeKey;
42
+ return (
43
+ <li key={s.key} className="shrink-0">
44
+ <Link
45
+ href={`/s/${encodeURIComponent(s.key)}`}
46
+ aria-current={active ? "page" : undefined}
47
+ className={cx(
48
+ "flex min-h-9 items-center justify-between gap-2 rounded-md px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
49
+ active ? "bg-accent text-accent-foreground" : "hover:bg-accent",
50
+ !s.built && "text-muted-foreground",
51
+ )}
52
+ >
53
+ <span>{s.label}</span>
54
+ {s.built ? null : <span className="text-2xs uppercase tracking-wide">planned</span>}
55
+ </Link>
56
+ </li>
57
+ );
58
+ })}
59
+ </ul>
60
+ </nav>
61
+ <main className="min-w-0 flex-1 p-4 sm:p-6">{children}</main>
62
+ </div>
63
+ );
64
+ }