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.
- package/README.md +68 -0
- package/bin/makersclaw.mjs +10 -0
- package/package.json +34 -0
- package/registry/blocks/board/README.md +130 -0
- package/registry/blocks/board/block.json +10 -0
- package/registry/blocks/board/board.jsx +435 -0
- package/registry/blocks/board/demo.jsx +50 -0
- package/registry/blocks/board/index.js +4 -0
- package/registry/blocks/job-panel/README.md +68 -0
- package/registry/blocks/job-panel/block.json +10 -0
- package/registry/blocks/job-panel/demo.jsx +75 -0
- package/registry/blocks/job-panel/index.js +3 -0
- package/registry/blocks/job-panel/job-panel.jsx +110 -0
- package/src/cli.mjs +301 -0
- package/src/fsutil.mjs +76 -0
- package/src/install.mjs +457 -0
- package/src/registry.mjs +191 -0
- package/templates/next/files/README.md +626 -0
- package/templates/next/files/app/auto-refresh.jsx +26 -0
- package/templates/next/files/app/error-relay.jsx +53 -0
- package/templates/next/files/app/error.jsx +44 -0
- package/templates/next/files/app/global-error.jsx +36 -0
- package/templates/next/files/app/globals.css +178 -0
- package/templates/next/files/app/layout.jsx +51 -0
- package/templates/next/files/app/not-found.jsx +23 -0
- package/templates/next/files/app/page.jsx +22 -0
- package/templates/next/files/app/s/[key]/page.jsx +47 -0
- package/templates/next/files/app/shell.jsx +64 -0
- package/templates/next/files/check/run.mjs +683 -0
- package/templates/next/files/components/parts/fixture.jsx +60 -0
- package/templates/next/files/components/parts/index.js +5 -0
- package/templates/next/files/components/parts/record-switcher.jsx +49 -0
- package/templates/next/files/components/ui/button.jsx +81 -0
- package/templates/next/files/components/ui/cx.js +4 -0
- package/templates/next/files/components/ui/depiction.jsx +66 -0
- package/templates/next/files/components/ui/field.jsx +126 -0
- package/templates/next/files/components/ui/icon.jsx +87 -0
- package/templates/next/files/components/ui/index.js +12 -0
- package/templates/next/files/components/ui/menu.jsx +123 -0
- package/templates/next/files/components/ui/notice.jsx +86 -0
- package/templates/next/files/components/ui/sheet.jsx +89 -0
- package/templates/next/files/components/ui/tabs.jsx +81 -0
- package/templates/next/files/lib/actions.js +187 -0
- package/templates/next/files/lib/claw.js +219 -0
- package/templates/next/files/lib/data.js +171 -0
- package/templates/next/files/lib/db.js +72 -0
- package/templates/next/files/lib/hooks.js +218 -0
- package/templates/next/files/lib/platform.js +59 -0
- package/templates/next/files/lib/sample.js +12 -0
- package/templates/next/files/middleware.js +42 -0
- package/templates/next/files/next.config.mjs +41 -0
- package/templates/next/files/package.json +19 -0
- package/templates/next/files/postcss.config.mjs +3 -0
- package/templates/next/files/schema.sql +22 -0
- package/templates/next/files/surfaces/README.md +32 -0
- package/templates/next/files/surfaces.js +53 -0
- 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
|
+
}
|