create-coline-app 2.2.0 → 2.5.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/AGENTS.md +86 -0
- package/README.md +60 -7
- package/assets/preview-host.ts +130 -0
- package/bin/_run.mjs +11 -2
- package/package.json +9 -4
- package/src/check.ts +46 -0
- package/src/cli.test.ts +154 -0
- package/src/cli.ts +12 -1
- package/src/client-build.ts +50 -0
- package/src/dev.ts +6 -5
- package/src/preview-capture.ts +101 -0
- package/src/preview.ts +189 -0
- package/src/push.ts +8 -47
- package/src/scaffold.ts +6 -0
- package/templates/backendless/.agents/skills/coline-app-development/SKILL.md +36 -0
- package/templates/backendless/.agents/skills/coline-app-development/references/live-records.md +53 -0
- package/templates/backendless/.agents/skills/coline-ui-design/SKILL.md +67 -0
- package/templates/backendless/.agents/skills/coline-ui-design/assets/board.png +0 -0
- package/templates/backendless/.agents/skills/coline-ui-design/assets/settings.png +0 -0
- package/templates/backendless/.agents/skills/coline-ui-design/assets/triage.png +0 -0
- package/templates/backendless/.agents/skills/coline-ui-review/SKILL.md +39 -0
- package/templates/backendless/AGENTS.md +79 -194
- package/templates/backendless/CLAUDE.md +1 -5
- package/templates/backendless/README.md +22 -27
- package/templates/backendless/app.config.ts +19 -11
- package/templates/backendless/app.css +84 -0
- package/templates/backendless/examples/board.tsx +173 -0
- package/templates/backendless/examples/settings.tsx +117 -0
- package/templates/backendless/examples/triage.tsx +253 -0
- package/templates/backendless/main.tsx +232 -0
- package/templates/backendless/package.json +16 -6
- package/templates/backendless/preview.seed.ts +26 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import {
|
|
4
|
+
Input,
|
|
5
|
+
Textarea,
|
|
6
|
+
useColineContext,
|
|
7
|
+
Button,
|
|
8
|
+
ColineAppProvider,
|
|
9
|
+
useColine,
|
|
10
|
+
useColineQuery,
|
|
11
|
+
AppPage,
|
|
12
|
+
PageToolbar,
|
|
13
|
+
RecordTable,
|
|
14
|
+
EmptyState,
|
|
15
|
+
AppErrorState,
|
|
16
|
+
AppLoadingState,
|
|
17
|
+
StatusBadge,
|
|
18
|
+
} from "@colineapp/ui";
|
|
19
|
+
import type { ColineFileSummaryV2 } from "@colineapp/sdk/v2";
|
|
20
|
+
import "@colineapp/ui/styles.css";
|
|
21
|
+
import "./app.css";
|
|
22
|
+
|
|
23
|
+
function StarterHome() {
|
|
24
|
+
const coline = useColine();
|
|
25
|
+
const files = useColineQuery(
|
|
26
|
+
(capabilities) => capabilities.files.list({ typeKey: "__APP_KEY__.note", limit: 50 }),
|
|
27
|
+
[],
|
|
28
|
+
);
|
|
29
|
+
const [search, setSearch] = React.useState("");
|
|
30
|
+
const notes = (files.data?.files ?? []).filter((file) =>
|
|
31
|
+
file.name.toLowerCase().includes(search.toLowerCase()),
|
|
32
|
+
);
|
|
33
|
+
const [creating, setCreating] = React.useState(false);
|
|
34
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
35
|
+
|
|
36
|
+
async function createNote(): Promise<void> {
|
|
37
|
+
setCreating(true);
|
|
38
|
+
setError(null);
|
|
39
|
+
try {
|
|
40
|
+
const file = await coline.files.create({
|
|
41
|
+
typeKey: "__APP_KEY__.note",
|
|
42
|
+
name: "Untitled note",
|
|
43
|
+
document: { title: "Untitled note", body: "" },
|
|
44
|
+
});
|
|
45
|
+
await coline.navigation.openFile(file.fileId);
|
|
46
|
+
} catch (cause) {
|
|
47
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
48
|
+
} finally {
|
|
49
|
+
setCreating(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<AppPage
|
|
55
|
+
title="Notes"
|
|
56
|
+
eyebrow="__APP_NAME__"
|
|
57
|
+
description="A shared place for ideas, decisions, and the details worth keeping."
|
|
58
|
+
actions={
|
|
59
|
+
<Button disabled={creating} onClick={() => void createNote()}>
|
|
60
|
+
{creating ? "Creating…" : "New note"}
|
|
61
|
+
</Button>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<PageToolbar>
|
|
65
|
+
<Input
|
|
66
|
+
aria-label="Search notes"
|
|
67
|
+
placeholder="Search notes…"
|
|
68
|
+
value={search}
|
|
69
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
70
|
+
className="max-w-xs"
|
|
71
|
+
/>
|
|
72
|
+
<span className="text-xs text-muted-foreground">{notes.length} notes</span>
|
|
73
|
+
</PageToolbar>
|
|
74
|
+
{error && <AppErrorState message={error} onRetry={() => void createNote()} />}
|
|
75
|
+
{files.isLoading ? (
|
|
76
|
+
<AppLoadingState label="Loading notes" />
|
|
77
|
+
) : files.error ? (
|
|
78
|
+
<AppErrorState message={files.error} onRetry={files.refetch} />
|
|
79
|
+
) : (
|
|
80
|
+
<RecordTable<ColineFileSummaryV2>
|
|
81
|
+
label="Notes"
|
|
82
|
+
records={notes}
|
|
83
|
+
rowKey={(file) => file.fileId}
|
|
84
|
+
onOpen={(file) => void coline.navigation.openFile(file.fileId)}
|
|
85
|
+
columns={[
|
|
86
|
+
{
|
|
87
|
+
key: "name",
|
|
88
|
+
label: "Name",
|
|
89
|
+
render: (file) => <span className="font-medium">{file.name}</span>,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "type",
|
|
93
|
+
label: "Type",
|
|
94
|
+
render: () => <StatusBadge>Note</StatusBadge>,
|
|
95
|
+
secondary: true,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "action",
|
|
99
|
+
label: "",
|
|
100
|
+
render: () => <span className="text-muted-foreground text-xs">Open →</span>,
|
|
101
|
+
align: "end",
|
|
102
|
+
},
|
|
103
|
+
]}
|
|
104
|
+
empty={
|
|
105
|
+
<EmptyState
|
|
106
|
+
title={search ? "No matching notes" : "Your next idea starts here"}
|
|
107
|
+
description={
|
|
108
|
+
search
|
|
109
|
+
? "Try another name or clear your search."
|
|
110
|
+
: "Create a note to capture a decision or share an idea."
|
|
111
|
+
}
|
|
112
|
+
action={
|
|
113
|
+
!search && (
|
|
114
|
+
<Button disabled={creating} onClick={() => void createNote()}>
|
|
115
|
+
Create a note
|
|
116
|
+
</Button>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
/>
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
</AppPage>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function NoteEditor() {
|
|
128
|
+
const coline = useColine();
|
|
129
|
+
const context = useColineContext();
|
|
130
|
+
const [title, setTitle] = React.useState(
|
|
131
|
+
String(context.document?.title ?? context.file?.name ?? ""),
|
|
132
|
+
);
|
|
133
|
+
const [body, setBody] = React.useState(String(context.document?.body ?? ""));
|
|
134
|
+
const version = React.useRef(context.file?.version ?? 1);
|
|
135
|
+
const [saved, setSaved] = React.useState({ title, body });
|
|
136
|
+
const [saving, setSaving] = React.useState(false);
|
|
137
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
138
|
+
const dirty = title !== saved.title || body !== saved.body;
|
|
139
|
+
React.useEffect(() => {
|
|
140
|
+
function warn(event: BeforeUnloadEvent) {
|
|
141
|
+
if (dirty) {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
event.returnValue = "";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
window.addEventListener("beforeunload", warn);
|
|
147
|
+
return () => window.removeEventListener("beforeunload", warn);
|
|
148
|
+
}, [dirty]);
|
|
149
|
+
async function save() {
|
|
150
|
+
if (!context.file) return;
|
|
151
|
+
setSaving(true);
|
|
152
|
+
setError(null);
|
|
153
|
+
try {
|
|
154
|
+
const result = await coline.files.updateDocument(
|
|
155
|
+
context.file.fileId,
|
|
156
|
+
{ title, body },
|
|
157
|
+
{ expectedVersion: version.current },
|
|
158
|
+
);
|
|
159
|
+
version.current = result.version;
|
|
160
|
+
await coline.files.update(context.file.fileId, { name: title.trim() || "Untitled note" });
|
|
161
|
+
setSaved({ title, body });
|
|
162
|
+
} catch (cause) {
|
|
163
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
164
|
+
} finally {
|
|
165
|
+
setSaving(false);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function back() {
|
|
169
|
+
if (
|
|
170
|
+
dirty &&
|
|
171
|
+
!(await coline.pickers.confirm({
|
|
172
|
+
title: "Leave without saving?",
|
|
173
|
+
description: "Your edits will be lost.",
|
|
174
|
+
confirmLabel: "Leave",
|
|
175
|
+
danger: true,
|
|
176
|
+
}))
|
|
177
|
+
)
|
|
178
|
+
return;
|
|
179
|
+
await coline.navigation.openAppHome();
|
|
180
|
+
}
|
|
181
|
+
return (
|
|
182
|
+
<AppPage title="Edit note" width="reading" description="Keep your thoughts together.">
|
|
183
|
+
<div className="note-editor">
|
|
184
|
+
<header className="flex items-center justify-between gap-4">
|
|
185
|
+
<Button variant="outline" onClick={() => void back()}>
|
|
186
|
+
All notes
|
|
187
|
+
</Button>
|
|
188
|
+
<Button disabled={!dirty || saving} onClick={() => void save()}>
|
|
189
|
+
{saving ? "Saving…" : "Save"}
|
|
190
|
+
</Button>
|
|
191
|
+
</header>
|
|
192
|
+
<label>
|
|
193
|
+
Title
|
|
194
|
+
<Input
|
|
195
|
+
value={title}
|
|
196
|
+
disabled={saving}
|
|
197
|
+
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTitle(event.target.value)}
|
|
198
|
+
/>
|
|
199
|
+
</label>
|
|
200
|
+
<label>
|
|
201
|
+
Note
|
|
202
|
+
<Textarea
|
|
203
|
+
value={body}
|
|
204
|
+
disabled={saving}
|
|
205
|
+
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
206
|
+
setBody(event.target.value)
|
|
207
|
+
}
|
|
208
|
+
className="min-h-64"
|
|
209
|
+
/>
|
|
210
|
+
</label>
|
|
211
|
+
{error ? (
|
|
212
|
+
<p role="alert">{error}</p>
|
|
213
|
+
) : (
|
|
214
|
+
<p role="status">{dirty ? "Unsaved changes" : "Saved"}</p>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</AppPage>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
function AppSurface() {
|
|
221
|
+
const context = useColineContext();
|
|
222
|
+
return context.surface === "editor" ? <NoteEditor key={context.file?.fileId} /> : <StarterHome />;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const root = document.getElementById("root");
|
|
226
|
+
if (root) {
|
|
227
|
+
createRoot(root).render(
|
|
228
|
+
<ColineAppProvider>
|
|
229
|
+
<AppSurface />
|
|
230
|
+
</ColineAppProvider>,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -6,16 +6,26 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "vitest run",
|
|
8
8
|
"typecheck": "tsc --noEmit",
|
|
9
|
+
"check": "coline-app check",
|
|
9
10
|
"push": "coline-app push",
|
|
10
|
-
"dev": "coline-app dev"
|
|
11
|
+
"dev": "coline-app dev",
|
|
12
|
+
"preview": "coline-app preview",
|
|
13
|
+
"screenshots": "coline-app preview --screenshots .coline/screenshots"
|
|
11
14
|
},
|
|
12
15
|
"dependencies": {
|
|
13
|
-
"@colineapp/sdk": "^0.
|
|
14
|
-
"
|
|
16
|
+
"@colineapp/sdk": "^0.4.0",
|
|
17
|
+
"@colineapp/ui": "^0.4.0",
|
|
18
|
+
"react": "^19.0.0",
|
|
19
|
+
"react-dom": "^19.0.0",
|
|
20
|
+
"zod": "^4.3.6"
|
|
15
21
|
},
|
|
16
22
|
"devDependencies": {
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
23
|
+
"@types/react": "^19.0.0",
|
|
24
|
+
"@types/react-dom": "^19.0.0",
|
|
25
|
+
"create-coline-app": "^2.5.0",
|
|
26
|
+
"typescript": "^5.8.2",
|
|
27
|
+
"vitest": "^3.2.4",
|
|
28
|
+
"tailwindcss": "^4.3.0",
|
|
29
|
+
"@colineapp/app-runtime": "^0.4.0"
|
|
20
30
|
}
|
|
21
31
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TestWorkspace } from "@colineapp/sdk/testing";
|
|
2
|
+
// Local-only fixtures, never included in the uploaded logic or client entry.
|
|
3
|
+
export async function seed(workspace: TestWorkspace) {
|
|
4
|
+
for (const [title, body] of [
|
|
5
|
+
[
|
|
6
|
+
"A better first five minutes",
|
|
7
|
+
"The first session should end with something useful. Give people one clear next step, then let them explore.",
|
|
8
|
+
],
|
|
9
|
+
[
|
|
10
|
+
"Decisions from Friday",
|
|
11
|
+
"Keep the launch small. Start with the people who already ask for this, listen closely, and ship the next improvement.",
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
"Research: what teams keep losing",
|
|
15
|
+
"Context gets scattered across messages. Decisions need a home, an owner, and a way to find them again.",
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"September release checklist",
|
|
19
|
+
"Review the mobile flow. Confirm keyboard navigation. Write the update in plain English.",
|
|
20
|
+
],
|
|
21
|
+
["Ideas for the next iteration", "Make the common path faster before adding another setting."],
|
|
22
|
+
])
|
|
23
|
+
await workspace
|
|
24
|
+
.client()
|
|
25
|
+
.files.create({ typeKey: "__APP_KEY__.note", name: title!, document: { title, body } });
|
|
26
|
+
}
|