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.
Files changed (32) hide show
  1. package/AGENTS.md +86 -0
  2. package/README.md +60 -7
  3. package/assets/preview-host.ts +130 -0
  4. package/bin/_run.mjs +11 -2
  5. package/package.json +9 -4
  6. package/src/check.ts +46 -0
  7. package/src/cli.test.ts +154 -0
  8. package/src/cli.ts +12 -1
  9. package/src/client-build.ts +50 -0
  10. package/src/dev.ts +6 -5
  11. package/src/preview-capture.ts +101 -0
  12. package/src/preview.ts +189 -0
  13. package/src/push.ts +8 -47
  14. package/src/scaffold.ts +6 -0
  15. package/templates/backendless/.agents/skills/coline-app-development/SKILL.md +36 -0
  16. package/templates/backendless/.agents/skills/coline-app-development/references/live-records.md +53 -0
  17. package/templates/backendless/.agents/skills/coline-ui-design/SKILL.md +67 -0
  18. package/templates/backendless/.agents/skills/coline-ui-design/assets/board.png +0 -0
  19. package/templates/backendless/.agents/skills/coline-ui-design/assets/settings.png +0 -0
  20. package/templates/backendless/.agents/skills/coline-ui-design/assets/triage.png +0 -0
  21. package/templates/backendless/.agents/skills/coline-ui-review/SKILL.md +39 -0
  22. package/templates/backendless/AGENTS.md +79 -194
  23. package/templates/backendless/CLAUDE.md +1 -5
  24. package/templates/backendless/README.md +22 -27
  25. package/templates/backendless/app.config.ts +19 -11
  26. package/templates/backendless/app.css +84 -0
  27. package/templates/backendless/examples/board.tsx +173 -0
  28. package/templates/backendless/examples/settings.tsx +117 -0
  29. package/templates/backendless/examples/triage.tsx +253 -0
  30. package/templates/backendless/main.tsx +232 -0
  31. package/templates/backendless/package.json +16 -6
  32. package/templates/backendless/preview.seed.ts +26 -0
@@ -0,0 +1,173 @@
1
+ import * as React from "react";
2
+ import {
3
+ AppPage,
4
+ PageToolbar,
5
+ Input,
6
+ Button,
7
+ Board,
8
+ BoardColumn,
9
+ BoardCard,
10
+ StatusBadge,
11
+ AppLoadingState,
12
+ AppErrorState,
13
+ EmptyState,
14
+ } from "@colineapp/ui";
15
+ const stages = ["Exploring", "In production", "Ready to publish"];
16
+ const initial = [
17
+ {
18
+ id: "1",
19
+ title: "What a calmer workday looks like",
20
+ format: "Essay",
21
+ owner: "Amelia",
22
+ due: "Sep 12",
23
+ stage: stages[0]!,
24
+ description:
25
+ "A story about fewer interruptions, clearer decisions, and work that stays in motion.",
26
+ },
27
+ {
28
+ id: "2",
29
+ title: "Inside the studio: Forma",
30
+ format: "Interview",
31
+ owner: "Noah",
32
+ due: "Sep 15",
33
+ stage: stages[0]!,
34
+ description: "How a small design team makes room for its most ambitious ideas.",
35
+ },
36
+ {
37
+ id: "3",
38
+ title: "Small details, better handoffs",
39
+ format: "Guide",
40
+ owner: "Amelia",
41
+ due: "Sep 9",
42
+ stage: stages[1]!,
43
+ description: "Five practical ways to leave the next person with useful context.",
44
+ },
45
+ {
46
+ id: "4",
47
+ title: "September product notes",
48
+ format: "Update",
49
+ owner: "Maya",
50
+ due: "Sep 8",
51
+ stage: stages[1]!,
52
+ description: "The changes that make everyday work a little easier.",
53
+ },
54
+ {
55
+ id: "5",
56
+ title: "Meet the people behind Juniper",
57
+ format: "Interview",
58
+ owner: "Noah",
59
+ due: "Sep 6",
60
+ stage: stages[2]!,
61
+ description: "A conversation about craft, collaboration, and starting small.",
62
+ },
63
+ ];
64
+ export function Example({ state = "ready" }: { state?: string }) {
65
+ const [items, setItems] = React.useState(state === "empty" ? [] : initial);
66
+ const [search, setSearch] = React.useState("");
67
+ const [failure, setFailure] = React.useState(state === "error" || state === "denied");
68
+ const moveControls = React.useRef(new Map<string, HTMLSelectElement>());
69
+ function move(id: string, stage: string) {
70
+ setItems((current) => current.map((item) => (item.id === id ? { ...item, stage } : item)));
71
+ // Moving between columns remounts the select. Keep keyboard users on the story.
72
+ requestAnimationFrame(() => moveControls.current.get(id)?.focus());
73
+ }
74
+ function add() {
75
+ setItems((current) => [
76
+ ...current,
77
+ {
78
+ id: String(Date.now()),
79
+ title: "A new story",
80
+ format: "Essay",
81
+ owner: "You",
82
+ due: "Unscheduled",
83
+ stage: stages[0]!,
84
+ description: "Start with the idea your reader should take away.",
85
+ },
86
+ ]);
87
+ }
88
+ const visible = items.filter((item) => item.title.toLowerCase().includes(search.toLowerCase()));
89
+ return (
90
+ <AppPage
91
+ title="Editorial"
92
+ eyebrow="Studio journal"
93
+ description="Good stories take shape here. Keep the next edition moving."
94
+ actions={<Button onClick={add}>New story</Button>}
95
+ >
96
+ <PageToolbar>
97
+ <Input
98
+ aria-label="Search stories"
99
+ placeholder="Search stories…"
100
+ className="max-w-xs"
101
+ value={search}
102
+ onChange={(event) => setSearch(event.target.value)}
103
+ />
104
+ <span className="example-meta">September edition · {visible.length} stories</span>
105
+ </PageToolbar>
106
+ {state === "loading" ? (
107
+ <AppLoadingState label="Loading stories" />
108
+ ) : failure ? (
109
+ <AppErrorState
110
+ message={
111
+ state === "denied"
112
+ ? "Ask the editor for access to this board."
113
+ : "The board couldn’t be loaded."
114
+ }
115
+ onRetry={state === "denied" ? undefined : () => setFailure(false)}
116
+ />
117
+ ) : !visible.length ? (
118
+ <EmptyState
119
+ title={search ? "No matching stories" : "Make room for the next story"}
120
+ description="Add an idea, develop it with your team, and move it through to publication."
121
+ action={<Button onClick={add}>Add a story</Button>}
122
+ />
123
+ ) : (
124
+ <Board label="Editorial pipeline">
125
+ {stages.map((stage, index) => (
126
+ <BoardColumn
127
+ key={stage}
128
+ title={stage}
129
+ count={visible.filter((item) => item.stage === stage).length}
130
+ >
131
+ {visible
132
+ .filter((item) => item.stage === stage)
133
+ .map((item) => (
134
+ <BoardCard
135
+ key={item.id}
136
+ title={item.title}
137
+ footer={
138
+ <>
139
+ <span className="example-meta">{item.owner}</span>
140
+ <span className="example-meta">{item.due}</span>
141
+ </>
142
+ }
143
+ >
144
+ <StatusBadge tone={index === 2 ? "success" : "neutral"}>
145
+ {item.format}
146
+ </StatusBadge>
147
+ <p className="example-description">{item.description}</p>
148
+ <label className="example-field">
149
+ Move to
150
+ <select
151
+ className="example-select"
152
+ aria-label={`Move ${item.title} to`}
153
+ value={item.stage}
154
+ ref={(element) => {
155
+ if (element) moveControls.current.set(item.id, element);
156
+ else moveControls.current.delete(item.id);
157
+ }}
158
+ onChange={(event) => move(item.id, event.currentTarget.value)}
159
+ >
160
+ {stages.map((value) => (
161
+ <option key={value}>{value}</option>
162
+ ))}
163
+ </select>
164
+ </label>
165
+ </BoardCard>
166
+ ))}
167
+ </BoardColumn>
168
+ ))}
169
+ </Board>
170
+ )}
171
+ </AppPage>
172
+ );
173
+ }
@@ -0,0 +1,117 @@
1
+ import * as React from "react";
2
+ import {
3
+ AppPage,
4
+ SettingsSection,
5
+ Input,
6
+ Textarea,
7
+ Button,
8
+ AppLoadingState,
9
+ AppErrorState,
10
+ } from "@colineapp/ui";
11
+ export function Example({ state = "ready" }: { state?: string }) {
12
+ const initial = {
13
+ name: state === "empty" ? "" : "Studio journal",
14
+ description: state === "empty" ? "" : "Stories about thoughtful work and the people behind it.",
15
+ digest: true,
16
+ cadence: "Weekly",
17
+ };
18
+ const [form, setForm] = React.useState(initial);
19
+ const [saved, setSaved] = React.useState(initial);
20
+ const [notice, setNotice] = React.useState("");
21
+ const [failure, setFailure] = React.useState(state === "error" || state === "denied");
22
+ const dirty = JSON.stringify(form) !== JSON.stringify(saved);
23
+ return (
24
+ <AppPage
25
+ title="Journal settings"
26
+ eyebrow="Studio journal"
27
+ width="reading"
28
+ description="The essentials for your publication and the people following it."
29
+ >
30
+ {state === "loading" ? (
31
+ <AppLoadingState label="Loading settings" />
32
+ ) : failure ? (
33
+ <AppErrorState
34
+ message={
35
+ state === "denied"
36
+ ? "Only editors can change journal settings."
37
+ : "Settings couldn’t be loaded."
38
+ }
39
+ onRetry={state === "denied" ? undefined : () => setFailure(false)}
40
+ />
41
+ ) : (
42
+ <form
43
+ onSubmit={(event) => {
44
+ event.preventDefault();
45
+ setSaved({ ...form });
46
+ setNotice("Settings saved for this preview session.");
47
+ }}
48
+ >
49
+ <SettingsSection
50
+ title="Publication"
51
+ description="A clear name and description help readers know what to expect."
52
+ >
53
+ <label className="example-field">
54
+ Name
55
+ <Input
56
+ required
57
+ value={form.name}
58
+ onChange={(event) => setForm({ ...form, name: event.target.value })}
59
+ />
60
+ </label>
61
+ <label className="example-field">
62
+ Description
63
+ <Textarea
64
+ value={form.description}
65
+ onChange={(event) => setForm({ ...form, description: event.target.value })}
66
+ />
67
+ </label>
68
+ </SettingsSection>
69
+ <SettingsSection
70
+ title="Reader updates"
71
+ description="Set a comfortable rhythm. Readers can always manage their own subscription."
72
+ >
73
+ <label className="example-toggle">
74
+ <input
75
+ type="checkbox"
76
+ checked={form.digest}
77
+ onChange={(event) => setForm({ ...form, digest: event.target.checked })}
78
+ />
79
+ Send a journal digest
80
+ </label>
81
+ <label className="example-field">
82
+ Frequency
83
+ <select
84
+ className="example-select"
85
+ disabled={!form.digest}
86
+ value={form.cadence}
87
+ onChange={(event) => setForm({ ...form, cadence: event.target.value })}
88
+ >
89
+ <option>Weekly</option>
90
+ <option>Monthly</option>
91
+ </select>
92
+ </label>
93
+ </SettingsSection>
94
+ <div className="flex items-center justify-end gap-3 py-6">
95
+ <span role="status" className="example-meta">
96
+ {dirty ? "Unsaved changes" : notice}
97
+ </span>
98
+ <Button
99
+ type="button"
100
+ variant="outline"
101
+ disabled={!dirty}
102
+ onClick={() => {
103
+ setForm(saved);
104
+ setNotice("");
105
+ }}
106
+ >
107
+ Discard
108
+ </Button>
109
+ <Button type="submit" disabled={!dirty}>
110
+ Save changes
111
+ </Button>
112
+ </div>
113
+ </form>
114
+ )}
115
+ </AppPage>
116
+ );
117
+ }
@@ -0,0 +1,253 @@
1
+ import * as React from "react";
2
+ import {
3
+ AppPage,
4
+ PageToolbar,
5
+ Input,
6
+ Button,
7
+ RecordTable,
8
+ SplitView,
9
+ DetailPanel,
10
+ StatusBadge,
11
+ EmptyState,
12
+ AppErrorState,
13
+ AppLoadingState,
14
+ } from "@colineapp/ui";
15
+
16
+ const initial = [
17
+ {
18
+ id: "REQ-108",
19
+ title: "Invite links expire before the team joins",
20
+ team: "Juniper",
21
+ category: "Access",
22
+ status: "Needs review",
23
+ detail:
24
+ "Our contractors often join a few days after an invitation. Could we extend the window or resend an invitation from the same screen?",
25
+ date: "Today",
26
+ },
27
+ {
28
+ id: "REQ-107",
29
+ title: "Export a filtered view",
30
+ team: "Forma",
31
+ category: "Workflow",
32
+ status: "In progress",
33
+ detail:
34
+ "We share a weekly report with partners. It would help to export just the rows in the current view, with the same column order.",
35
+ date: "Today",
36
+ },
37
+ {
38
+ id: "REQ-106",
39
+ title: "Remember my sidebar width",
40
+ team: "Juniper",
41
+ category: "Experience",
42
+ status: "Needs review",
43
+ detail:
44
+ "The sidebar resets every morning. Keep the width I chose so longer project names stay readable.",
45
+ date: "Yesterday",
46
+ },
47
+ {
48
+ id: "REQ-105",
49
+ title: "Include context in email notifications",
50
+ team: "Acorn",
51
+ category: "Notifications",
52
+ status: "Planned",
53
+ detail:
54
+ "Show a short excerpt with each notification so we know whether a reply is urgent before opening the app.",
55
+ date: "Yesterday",
56
+ },
57
+ {
58
+ id: "REQ-104",
59
+ title: "Bulk archive completed requests",
60
+ team: "Forma",
61
+ category: "Workflow",
62
+ status: "Needs review",
63
+ detail: "Let us select completed requests and archive them together at the end of the week.",
64
+ date: "Sep 3",
65
+ },
66
+ ];
67
+ export function Example({ state = "ready" }: { state?: string }) {
68
+ const [records, setRecords] = React.useState(state === "empty" ? [] : initial);
69
+ const [selected, setSelected] = React.useState<string | null>(
70
+ state === "ready" ? initial[0]!.id : null,
71
+ );
72
+ const [search, setSearch] = React.useState("");
73
+ const [status, setStatus] = React.useState("All requests");
74
+ const [failure, setFailure] = React.useState(state === "error" || state === "denied");
75
+ const detail = records.find((record) => record.id === selected);
76
+ const visible = records.filter(
77
+ (record) =>
78
+ `${record.title} ${record.team}`.toLowerCase().includes(search.toLowerCase()) &&
79
+ (status === "All requests" || record.status === status),
80
+ );
81
+ const panel = React.useRef<HTMLDivElement>(null);
82
+ const opener = React.useRef<HTMLElement | null>(null);
83
+ function close() {
84
+ setSelected(null);
85
+ requestAnimationFrame(() => opener.current?.focus());
86
+ }
87
+ function open(id: string) {
88
+ opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
89
+ setSelected(id);
90
+ requestAnimationFrame(() => {
91
+ panel.current?.focus();
92
+ panel.current?.scrollIntoView({ block: "nearest" });
93
+ });
94
+ }
95
+ function add() {
96
+ const id = `REQ-${109 + records.length}`;
97
+ setRecords((current) => [
98
+ ...current,
99
+ {
100
+ id,
101
+ title: "New request",
102
+ team: "Your team",
103
+ category: "Workflow",
104
+ status: "Needs review",
105
+ detail: "Describe the problem, who it affects, and what a good outcome looks like.",
106
+ date: "Today",
107
+ },
108
+ ]);
109
+ open(id);
110
+ }
111
+ return (
112
+ <AppPage
113
+ title="Requests"
114
+ eyebrow="Customer feedback"
115
+ description="Understand what matters. Turn the next useful idea into a decision."
116
+ actions={<Button onClick={add}>New request</Button>}
117
+ >
118
+ <PageToolbar>
119
+ <Input
120
+ aria-label="Search requests"
121
+ placeholder="Search requests…"
122
+ className="max-w-xs"
123
+ value={search}
124
+ onChange={(event) => setSearch(event.target.value)}
125
+ />
126
+ <select
127
+ aria-label="Filter by status"
128
+ className="example-select"
129
+ value={status}
130
+ onChange={(event) => setStatus(event.target.value)}
131
+ >
132
+ {["All requests", "Needs review", "In progress", "Planned"].map((value) => (
133
+ <option key={value}>{value}</option>
134
+ ))}
135
+ </select>
136
+ <span className="example-meta">{visible.length} requests</span>
137
+ </PageToolbar>
138
+ {state === "loading" ? (
139
+ <AppLoadingState label="Loading requests" />
140
+ ) : failure ? (
141
+ <AppErrorState
142
+ message={
143
+ state === "denied"
144
+ ? "You don’t have access to these requests. Ask a workspace owner for access."
145
+ : "Requests couldn’t be loaded. Your changes are safe."
146
+ }
147
+ onRetry={state === "denied" ? undefined : () => setFailure(false)}
148
+ />
149
+ ) : (
150
+ <SplitView
151
+ list={
152
+ <RecordTable
153
+ records={visible}
154
+ rowKey={(record) => record.id}
155
+ onOpen={(record) => open(record.id)}
156
+ label="Customer requests"
157
+ columns={[
158
+ {
159
+ key: "title",
160
+ label: "Request",
161
+ render: (record) => (
162
+ <>
163
+ <span className="example-meta">{record.id}</span>
164
+ <span>{record.title}</span>
165
+ </>
166
+ ),
167
+ },
168
+ { key: "team", label: "Team", render: (record) => record.team, secondary: true },
169
+ {
170
+ key: "status",
171
+ label: "Status",
172
+ render: (record) => (
173
+ <StatusBadge
174
+ tone={
175
+ record.status === "In progress"
176
+ ? "info"
177
+ : record.status === "Planned"
178
+ ? "success"
179
+ : "warning"
180
+ }
181
+ >
182
+ {record.status}
183
+ </StatusBadge>
184
+ ),
185
+ },
186
+ ]}
187
+ empty={
188
+ <EmptyState
189
+ title={search ? "No matching requests" : "A clear queue"}
190
+ description="Capture feedback when it arrives. Every request gets one place for context and a decision."
191
+ action={<Button onClick={add}>Add a request</Button>}
192
+ />
193
+ }
194
+ />
195
+ }
196
+ detail={
197
+ detail && (
198
+ <div ref={panel} tabIndex={-1}>
199
+ <DetailPanel
200
+ title={detail.title}
201
+ subtitle={detail.id}
202
+ onClose={close}
203
+ actions={
204
+ <Button
205
+ variant="outline"
206
+ onClick={() =>
207
+ setRecords((current) =>
208
+ current.map((record) =>
209
+ record.id === detail.id ? { ...record, status: "Planned" } : record,
210
+ ),
211
+ )
212
+ }
213
+ >
214
+ Mark as planned
215
+ </Button>
216
+ }
217
+ >
218
+ <div className="example-meta">
219
+ <span>{detail.team}</span>
220
+ <span>·</span>
221
+ <span>{detail.date}</span>
222
+ </div>
223
+ <p className="example-description">{detail.detail}</p>
224
+ <label className="example-field">
225
+ Status
226
+ <select
227
+ className="example-select"
228
+ value={detail.status}
229
+ onChange={(event) =>
230
+ setRecords((current) =>
231
+ current.map((record) =>
232
+ record.id === detail.id
233
+ ? { ...record, status: event.target.value }
234
+ : record,
235
+ ),
236
+ )
237
+ }
238
+ >
239
+ {["Needs review", "In progress", "Planned"].map((value) => (
240
+ <option key={value}>{value}</option>
241
+ ))}
242
+ </select>
243
+ </label>
244
+ <div className="example-meta">{detail.category}</div>
245
+ </DetailPanel>
246
+ </div>
247
+ )
248
+ }
249
+ />
250
+ )}
251
+ </AppPage>
252
+ );
253
+ }