create-coline-app 2.3.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 +26 -2
  2. package/README.md +51 -4
  3. package/assets/preview-host.ts +130 -0
  4. package/bin/_run.mjs +11 -2
  5. package/package.json +8 -4
  6. package/src/check.ts +46 -0
  7. package/src/cli.test.ts +112 -6
  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 +30 -64
  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 +70 -158
  23. package/templates/backendless/CLAUDE.md +1 -6
  24. package/templates/backendless/README.md +22 -29
  25. package/templates/backendless/app.config.ts +12 -8
  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 +198 -66
  31. package/templates/backendless/package.json +10 -5
  32. package/templates/backendless/preview.seed.ts +26 -0
@@ -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
+ }
@@ -1,100 +1,232 @@
1
+ import * as React from "react";
1
2
  import { createRoot } from "react-dom/client";
2
3
  import {
4
+ Input,
5
+ Textarea,
6
+ useColineContext,
3
7
  Button,
4
- Card,
5
- CardContent,
6
- CardDescription,
7
- CardHeader,
8
- CardTitle,
9
8
  ColineAppProvider,
10
- Skeleton,
11
9
  useColine,
12
10
  useColineQuery,
11
+ AppPage,
12
+ PageToolbar,
13
+ RecordTable,
14
+ EmptyState,
15
+ AppErrorState,
16
+ AppLoadingState,
17
+ StatusBadge,
13
18
  } from "@colineapp/ui";
14
19
  import type { ColineFileSummaryV2 } from "@colineapp/sdk/v2";
15
20
  import "@colineapp/ui/styles.css";
21
+ import "./app.css";
16
22
 
17
23
  function StarterHome() {
18
24
  const coline = useColine();
19
25
  const files = useColineQuery(
20
- (capabilities) =>
21
- capabilities.files.list({ typeKey: "__APP_KEY__.note", limit: 50 }),
26
+ (capabilities) => capabilities.files.list({ typeKey: "__APP_KEY__.note", limit: 50 }),
22
27
  [],
23
28
  );
24
- const notes = files.data?.files ?? [];
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);
25
35
 
26
36
  async function createNote(): Promise<void> {
27
- const file = await coline.files.create({
28
- typeKey: "__APP_KEY__.note",
29
- name: "Untitled note",
30
- document: { title: "Untitled note", body: "" },
31
- });
32
- await coline.navigation.openFile(file.fileId);
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
+ }
33
51
  }
34
52
 
35
53
  return (
36
- <main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col gap-6 bg-background p-6 text-foreground">
37
- <header className="flex items-center justify-between gap-4">
38
- <div>
39
- <h1 className="text-xl font-semibold">__APP_NAME__ is running</h1>
40
- <p className="mt-1 text-sm text-muted-foreground">
41
- Edit app.config.ts or main.tsx and save to hot-swap this app.
42
- </p>
43
- </div>
44
- <Button onClick={() => void createNote()}>New note</Button>
45
- </header>
46
-
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()} />}
47
75
  {files.isLoading ? (
48
- <div className="flex flex-col gap-2">
49
- <Skeleton className="h-16 w-full" />
50
- <Skeleton className="h-16 w-full" />
51
- </div>
76
+ <AppLoadingState label="Loading notes" />
52
77
  ) : files.error ? (
53
- <Card>
54
- <CardHeader>
55
- <CardTitle>Couldn&apos;t load notes</CardTitle>
56
- <CardDescription>{files.error}</CardDescription>
57
- </CardHeader>
58
- <CardContent>
59
- <Button variant="outline" onClick={files.refetch}>
60
- Retry
61
- </Button>
62
- </CardContent>
63
- </Card>
64
- ) : notes.length === 0 ? (
65
- <Card>
66
- <CardHeader>
67
- <CardTitle>Edit me</CardTitle>
68
- <CardDescription>
69
- This is a real React surface. Replace this starter with your app,
70
- then ask Kairo to create a note called Hello.
71
- </CardDescription>
72
- </CardHeader>
73
- </Card>
78
+ <AppErrorState message={files.error} onRetry={files.refetch} />
74
79
  ) : (
75
- <div className="flex flex-col gap-2">
76
- {notes.map((file: ColineFileSummaryV2) => (
77
- <button
78
- key={file.fileId}
79
- type="button"
80
- onClick={() => void coline.navigation.openFile(file.fileId)}
81
- className="rounded-xl border border-border bg-card px-4 py-3 text-left hover:bg-muted/50"
82
- >
83
- <p className="text-sm font-medium">{file.name}</p>
84
- <p className="mt-1 text-xs text-muted-foreground">Open note</p>
85
- </button>
86
- ))}
87
- </div>
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
+ />
88
122
  )}
89
- </main>
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 ?? ""),
90
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 />;
91
223
  }
92
224
 
93
225
  const root = document.getElementById("root");
94
226
  if (root) {
95
227
  createRoot(root).render(
96
228
  <ColineAppProvider>
97
- <StarterHome />
229
+ <AppSurface />
98
230
  </ColineAppProvider>,
99
231
  );
100
232
  }