@athrio/loom 0.0.9 → 0.0.10
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/dist/Cache-Dv3Qogvb.js +1048 -0
- package/dist/Cache-Dv3Qogvb.js.map +1 -0
- package/dist/{LoomServer-BZL6KQOM.js → LoomServer-JbCtLtxV.js} +4 -2
- package/dist/{LoomServer-BZL6KQOM.js.map → LoomServer-JbCtLtxV.js.map} +1 -1
- package/dist/LoomStore-BGlpRj6W.js +8857 -0
- package/dist/LoomStore-BGlpRj6W.js.map +1 -0
- package/dist/NodeServices-CtEw3D6F.js +1564 -0
- package/dist/NodeServices-CtEw3D6F.js.map +1 -0
- package/dist/{LoomStore-BBRhdzet.js → SqlClient-BHo_uVyk.js} +10062 -46501
- package/dist/SqlClient-BHo_uVyk.js.map +1 -0
- package/dist/SqlError-BwVJTsti.js +408 -0
- package/dist/SqlError-BwVJTsti.js.map +1 -0
- package/dist/design-S_129JVC.js +18524 -0
- package/dist/design-S_129JVC.js.map +1 -0
- package/dist/dist-3ptbWhlX.js +202 -0
- package/dist/dist-3ptbWhlX.js.map +1 -0
- package/dist/dist-CKhPOkpR.js +27469 -0
- package/dist/dist-CKhPOkpR.js.map +1 -0
- package/dist/dist-kscTHM4c.js +151 -0
- package/dist/dist-kscTHM4c.js.map +1 -0
- package/dist/identity-DvFsrt2d.js +95 -0
- package/dist/identity-DvFsrt2d.js.map +1 -0
- package/dist/main.js +260 -1535
- package/dist/main.js.map +1 -1
- package/dist/overlay.js +80 -0
- package/dist/store-CtvrwAS8.js +3 -0
- package/dist/store-DgMmJ7v9.js +229 -0
- package/dist/store-DgMmJ7v9.js.map +1 -0
- package/dist/ui.html +103 -0
- package/dist/ui.js +79 -0
- package/package.json +3 -1
- package/dist/LoomStore-BBRhdzet.js.map +0 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Bs as gen, Fn as Union, Gc as tap, Gp as fromNullishOr, Ha as publish, Hl as provide, Il as effect, Ka as unbounded, Kl as unwrap, Ms as forEach, Mt as Boolean, Os as flatMap, Qs as map, Tl as currentTimeMillis, Vc as succeed, Wc as sync, Wn as decodeUnknownEffect, Xf as head, Ys as logWarning, Zc as try_, _f as Service, al as void_, ec as mapError, em as match, fc as orDie, hs as catchTag, ii as tag, is as andThen, kl as TaggedError, ls as asVoid, mc as promise, n as SqlClient, on as Number, qc as tapError, qf as getSomes, rm as some, tm as none, vn as String, yn as Struct } from "./SqlClient-BHo_uVyk.js";
|
|
3
|
+
import { b as when, g as exhaustive, y as value } from "./dist-CKhPOkpR.js";
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
//#region ../loom-design/src/note.ts
|
|
8
|
+
var noteFields = {
|
|
9
|
+
project: String,
|
|
10
|
+
route: String,
|
|
11
|
+
text: String
|
|
12
|
+
};
|
|
13
|
+
var RectSchema = Struct({
|
|
14
|
+
x: Number,
|
|
15
|
+
y: Number,
|
|
16
|
+
width: Number,
|
|
17
|
+
height: Number
|
|
18
|
+
});
|
|
19
|
+
var elementFields = {
|
|
20
|
+
...noteFields,
|
|
21
|
+
label: String,
|
|
22
|
+
rect: RectSchema
|
|
23
|
+
};
|
|
24
|
+
var domFields = {
|
|
25
|
+
kind: tag("dom"),
|
|
26
|
+
...elementFields,
|
|
27
|
+
selector: String
|
|
28
|
+
};
|
|
29
|
+
var LoomSourceSchema = Struct({
|
|
30
|
+
chapter: String,
|
|
31
|
+
section: String
|
|
32
|
+
});
|
|
33
|
+
var loomFields = {
|
|
34
|
+
kind: tag("loom"),
|
|
35
|
+
...elementFields,
|
|
36
|
+
source: LoomSourceSchema
|
|
37
|
+
};
|
|
38
|
+
var chatFields = {
|
|
39
|
+
kind: tag("chat"),
|
|
40
|
+
...noteFields
|
|
41
|
+
};
|
|
42
|
+
var DraftSchema = Union([
|
|
43
|
+
Struct(domFields),
|
|
44
|
+
Struct(loomFields),
|
|
45
|
+
Struct(chatFields)
|
|
46
|
+
]);
|
|
47
|
+
var stamp = {
|
|
48
|
+
seq: Number,
|
|
49
|
+
at: String,
|
|
50
|
+
addressed: Boolean
|
|
51
|
+
};
|
|
52
|
+
var DomNoteSchema = Struct({
|
|
53
|
+
...domFields,
|
|
54
|
+
...stamp
|
|
55
|
+
});
|
|
56
|
+
var LoomNoteSchema = Struct({
|
|
57
|
+
...loomFields,
|
|
58
|
+
...stamp
|
|
59
|
+
});
|
|
60
|
+
var ChatNoteSchema = Struct({
|
|
61
|
+
...chatFields,
|
|
62
|
+
...stamp
|
|
63
|
+
});
|
|
64
|
+
var NoteSchema = Union([
|
|
65
|
+
DomNoteSchema,
|
|
66
|
+
LoomNoteSchema,
|
|
67
|
+
ChatNoteSchema
|
|
68
|
+
]);
|
|
69
|
+
var stampDraft = (draft, seq, at) => value(draft).pipe(when({ kind: "dom" }, (annotation) => DomNoteSchema.make({
|
|
70
|
+
...annotation,
|
|
71
|
+
seq,
|
|
72
|
+
at,
|
|
73
|
+
addressed: false
|
|
74
|
+
})), when({ kind: "loom" }, (annotation) => LoomNoteSchema.make({
|
|
75
|
+
...annotation,
|
|
76
|
+
seq,
|
|
77
|
+
at,
|
|
78
|
+
addressed: false
|
|
79
|
+
})), when({ kind: "chat" }, (message) => ChatNoteSchema.make({
|
|
80
|
+
...message,
|
|
81
|
+
seq,
|
|
82
|
+
at,
|
|
83
|
+
addressed: false
|
|
84
|
+
})), exhaustive);
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region ../loom-design/src/store.ts
|
|
87
|
+
var DecodeError = class extends TaggedError("DecodeError") {};
|
|
88
|
+
var migrations = [
|
|
89
|
+
{
|
|
90
|
+
id: 1,
|
|
91
|
+
name: "initial",
|
|
92
|
+
run: (sql) => gen(function* () {
|
|
93
|
+
yield* sql`CREATE TABLE IF NOT EXISTS notes (
|
|
94
|
+
project TEXT NOT NULL,
|
|
95
|
+
seq INTEGER NOT NULL,
|
|
96
|
+
data TEXT NOT NULL,
|
|
97
|
+
PRIMARY KEY (project, seq)
|
|
98
|
+
)`;
|
|
99
|
+
yield* sql`CREATE TABLE IF NOT EXISTS projects (
|
|
100
|
+
id TEXT PRIMARY KEY,
|
|
101
|
+
name TEXT NOT NULL
|
|
102
|
+
)`;
|
|
103
|
+
})
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 2,
|
|
107
|
+
name: "project directory and address",
|
|
108
|
+
run: (sql) => gen(function* () {
|
|
109
|
+
yield* sql`ALTER TABLE projects ADD COLUMN directory TEXT`;
|
|
110
|
+
yield* sql`ALTER TABLE projects ADD COLUMN address TEXT`;
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 3,
|
|
115
|
+
name: "project identity",
|
|
116
|
+
run: (sql) => sql`ALTER TABLE projects ADD COLUMN identity TEXT`.pipe(asVoid)
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
var migrate = (sql) => gen(function* () {
|
|
120
|
+
yield* sql`CREATE TABLE IF NOT EXISTS _migrations (
|
|
121
|
+
id INTEGER PRIMARY KEY,
|
|
122
|
+
name TEXT NOT NULL,
|
|
123
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
124
|
+
)`;
|
|
125
|
+
const applied = yield* sql`SELECT id FROM _migrations`;
|
|
126
|
+
const done = new Set(applied.map((row) => row.id));
|
|
127
|
+
yield* forEach(migrations.filter((migration) => !done.has(migration.id)), (migration) => sql.withTransaction(migration.run(sql).pipe(andThen(sql`INSERT INTO _migrations (id, name) VALUES (${migration.id}, ${migration.name})`))), { discard: true });
|
|
128
|
+
});
|
|
129
|
+
var decodeNote = (data) => try_({
|
|
130
|
+
try: () => JSON.parse(data),
|
|
131
|
+
catch: (cause) => new DecodeError({
|
|
132
|
+
data,
|
|
133
|
+
cause
|
|
134
|
+
})
|
|
135
|
+
}).pipe(flatMap((json) => decodeUnknownEffect(NoteSchema)(json).pipe(mapError((cause) => new DecodeError({
|
|
136
|
+
data,
|
|
137
|
+
cause
|
|
138
|
+
})))));
|
|
139
|
+
var readNotes = (sql, project) => sql`
|
|
140
|
+
SELECT data FROM notes WHERE project = ${project} ORDER BY seq
|
|
141
|
+
`.pipe(flatMap((rows) => forEach(rows, (row) => decodeNote(row.data).pipe(tapError((error) => logWarning("skipping a note that no longer decodes", error)), map(some), catchTag("DecodeError", () => succeed(none()))))), map(getSomes));
|
|
142
|
+
var recordDraft = (sql, draft) => sql.withTransaction(gen(function* () {
|
|
143
|
+
const millis = yield* currentTimeMillis;
|
|
144
|
+
const note = stampDraft(draft, match(fromNullishOr((yield* sql`
|
|
145
|
+
SELECT MAX(seq) AS max FROM notes WHERE project = ${draft.project}
|
|
146
|
+
`)[0]?.max), {
|
|
147
|
+
onSome: (seq) => seq,
|
|
148
|
+
onNone: () => 0
|
|
149
|
+
}) + 1, new Date(millis).toISOString());
|
|
150
|
+
yield* sql`
|
|
151
|
+
INSERT INTO notes (project, seq, data)
|
|
152
|
+
VALUES (${note.project}, ${note.seq}, ${JSON.stringify(note)})
|
|
153
|
+
`;
|
|
154
|
+
return note;
|
|
155
|
+
}));
|
|
156
|
+
var modifyNote = (sql, project, seq, change) => gen(function* () {
|
|
157
|
+
yield* match(fromNullishOr((yield* sql`
|
|
158
|
+
SELECT data FROM notes WHERE project = ${project} AND seq = ${seq}
|
|
159
|
+
`)[0]), {
|
|
160
|
+
onNone: () => void_,
|
|
161
|
+
onSome: (row) => decodeNote(row.data).pipe(map(change), flatMap((updated) => sql`
|
|
162
|
+
UPDATE notes SET data = ${JSON.stringify(updated)}
|
|
163
|
+
WHERE project = ${project} AND seq = ${seq}
|
|
164
|
+
`))
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
var resolveNote = (sql, project, seq) => modifyNote(sql, project, seq, (note) => ({
|
|
168
|
+
...note,
|
|
169
|
+
addressed: true
|
|
170
|
+
}));
|
|
171
|
+
var editNote = (sql, project, seq, text) => modifyNote(sql, project, seq, (note) => ({
|
|
172
|
+
...note,
|
|
173
|
+
text
|
|
174
|
+
}));
|
|
175
|
+
var discardNote = (sql, project, seq) => sql`DELETE FROM notes WHERE project = ${project} AND seq = ${seq}`.pipe(asVoid);
|
|
176
|
+
var listProjects = (sql) => sql`SELECT id, name, directory, address, identity FROM projects ORDER BY name`;
|
|
177
|
+
var findProject = (sql, name) => sql`
|
|
178
|
+
SELECT id, name, directory, address, identity FROM projects WHERE id = ${name}
|
|
179
|
+
`.pipe(map(head));
|
|
180
|
+
var registerProject = (sql, name, identity, directory, address) => sql`
|
|
181
|
+
INSERT INTO projects (id, name, directory, address, identity)
|
|
182
|
+
VALUES (${name}, ${name}, ${directory}, ${address}, ${identity})
|
|
183
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
184
|
+
directory = ${directory}, address = ${address}, identity = ${identity}
|
|
185
|
+
`.pipe(asVoid);
|
|
186
|
+
var ensureProject = (sql, id) => sql`
|
|
187
|
+
INSERT INTO projects (id, name) VALUES (${id}, ${id})
|
|
188
|
+
ON CONFLICT (id) DO NOTHING
|
|
189
|
+
`.pipe(asVoid);
|
|
190
|
+
var backfillProjects = (sql) => sql`
|
|
191
|
+
INSERT INTO projects (id, name)
|
|
192
|
+
SELECT DISTINCT project, project FROM notes
|
|
193
|
+
WHERE true
|
|
194
|
+
ON CONFLICT (id) DO NOTHING
|
|
195
|
+
`.pipe(asVoid);
|
|
196
|
+
var sqliteClientLayer = (filename) => unwrap(promise(() => typeof globalThis.Bun !== "undefined" ? import("./dist-kscTHM4c.js").then((m) => m.SqliteClient.layer({
|
|
197
|
+
filename,
|
|
198
|
+
create: true
|
|
199
|
+
})) : import("./dist-3ptbWhlX.js").then((m) => m.SqliteClient.layer({ filename }))));
|
|
200
|
+
var sqliteStore = (filename) => effect(NoteStore, NoteStore.make).pipe(provide(sqliteClientLayer(filename)));
|
|
201
|
+
var notesFile = () => process.env.LOOM_NOTES_DB ?? join(homedir(), ".loom", "notes.sqlite");
|
|
202
|
+
var localStore = () => unwrap(sync(() => {
|
|
203
|
+
const file = notesFile();
|
|
204
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
205
|
+
return sqliteStore(file);
|
|
206
|
+
}));
|
|
207
|
+
var NoteStore = class extends Service()("NoteStore", { make: gen(function* () {
|
|
208
|
+
const sql = yield* SqlClient;
|
|
209
|
+
yield* migrate(sql);
|
|
210
|
+
yield* backfillProjects(sql);
|
|
211
|
+
const changes = yield* unbounded();
|
|
212
|
+
return {
|
|
213
|
+
list: (project) => readNotes(sql, project),
|
|
214
|
+
record: (draft) => ensureProject(sql, draft.project).pipe(andThen(recordDraft(sql, draft)), tap((note) => publish(changes, note))),
|
|
215
|
+
resolve: (project, seq) => resolveNote(sql, project, seq),
|
|
216
|
+
edit: (project, seq, text) => editNote(sql, project, seq, text),
|
|
217
|
+
discard: (project, seq) => discardNote(sql, project, seq),
|
|
218
|
+
projects: listProjects(sql),
|
|
219
|
+
project: (name) => findProject(sql, name),
|
|
220
|
+
register: (name, identity, directory, address) => registerProject(sql, name, identity, directory, address),
|
|
221
|
+
changes
|
|
222
|
+
};
|
|
223
|
+
}).pipe(orDie) }) {
|
|
224
|
+
static layer = localStore();
|
|
225
|
+
};
|
|
226
|
+
//#endregion
|
|
227
|
+
export { DraftSchema as a, sqliteStore as i, localStore as n, NoteSchema as o, notesFile as r, NoteStore as t };
|
|
228
|
+
|
|
229
|
+
//# sourceMappingURL=store-DgMmJ7v9.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-DgMmJ7v9.js","names":[],"sources":["../../loom-design/src/note.ts","../../loom-design/src/store.ts"],"sourcesContent":["import { Schema } from 'effect'\n\nconst noteFields = {\n project: Schema.String,\n route: Schema.String,\n text: Schema.String,\n}\n\nexport const RectSchema = Schema.Struct({\n x: Schema.Number,\n y: Schema.Number,\n width: Schema.Number,\n height: Schema.Number,\n})\nexport type Rect = typeof RectSchema.Type\n\nconst elementFields = {\n ...noteFields,\n label: Schema.String,\n rect: RectSchema,\n}\n\nconst domFields = {\n kind: Schema.tag('dom'),\n ...elementFields,\n selector: Schema.String,\n}\n\nexport const LoomSourceSchema = Schema.Struct({\n chapter: Schema.String,\n section: Schema.String,\n})\nexport type LoomSource = typeof LoomSourceSchema.Type\n\nconst loomFields = {\n kind: Schema.tag('loom'),\n ...elementFields,\n source: LoomSourceSchema,\n}\n\nconst chatFields = {\n kind: Schema.tag('chat'),\n ...noteFields,\n}\n\nexport const DomDraftSchema = Schema.Struct(domFields)\nexport const LoomDraftSchema = Schema.Struct(loomFields)\nexport const ChatDraftSchema = Schema.Struct(chatFields)\nexport const DraftSchema = Schema.Union([DomDraftSchema, LoomDraftSchema, ChatDraftSchema])\nexport type Draft = typeof DraftSchema.Type\n\nconst stamp = {\n seq: Schema.Number,\n at: Schema.String,\n addressed: Schema.Boolean,\n}\n\nexport const DomNoteSchema = Schema.Struct({ ...domFields, ...stamp })\nexport const LoomNoteSchema = Schema.Struct({ ...loomFields, ...stamp })\nexport const ChatNoteSchema = Schema.Struct({ ...chatFields, ...stamp })\nexport const NoteSchema = Schema.Union([DomNoteSchema, LoomNoteSchema, ChatNoteSchema])\nexport type Note = typeof NoteSchema.Type\n\nimport { Match } from 'effect'\n\nexport const stampDraft = (draft: Draft, seq: number, at: string): Note =>\n Match.value(draft).pipe(\n Match.when({ kind: 'dom' }, (annotation) =>\n DomNoteSchema.make({ ...annotation, seq, at, addressed: false }),\n ),\n Match.when({ kind: 'loom' }, (annotation) =>\n LoomNoteSchema.make({ ...annotation, seq, at, addressed: false }),\n ),\n Match.when({ kind: 'chat' }, (message) =>\n ChatNoteSchema.make({ ...message, seq, at, addressed: false }),\n ),\n Match.exhaustive,\n )\n","import { Effect, Layer, Option } from 'effect'\nimport { SqlClient, SqlError } from 'effect/unstable/sql'\nimport { Data } from 'effect'\n\nclass DecodeError extends Data.TaggedError('DecodeError')<{\n readonly data: string\n readonly cause: unknown\n}> {}\n\nconst migrations: ReadonlyArray<{\n readonly id: number\n readonly name: string\n readonly run: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError.SqlError>\n}> = [\n {\n id: 1,\n name: 'initial',\n run: (sql) =>\n Effect.gen(function* () {\n yield* sql`CREATE TABLE IF NOT EXISTS notes (\n project TEXT NOT NULL,\n seq INTEGER NOT NULL,\n data TEXT NOT NULL,\n PRIMARY KEY (project, seq)\n )`\n yield* sql`CREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL\n )`\n }),\n },\n {\n id: 2,\n name: 'project directory and address',\n run: (sql) =>\n Effect.gen(function* () {\n yield* sql`ALTER TABLE projects ADD COLUMN directory TEXT`\n yield* sql`ALTER TABLE projects ADD COLUMN address TEXT`\n }),\n },\n {\n id: 3,\n name: 'project identity',\n run: (sql) => sql`ALTER TABLE projects ADD COLUMN identity TEXT`.pipe(Effect.asVoid),\n },\n]\n\nconst migrate = (sql: SqlClient.SqlClient): Effect.Effect<void, SqlError.SqlError> =>\n Effect.gen(function* () {\n yield* sql`CREATE TABLE IF NOT EXISTS _migrations (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP\n )`\n const applied = yield* sql<{ readonly id: number }>`SELECT id FROM _migrations`\n const done = new Set(applied.map((row) => row.id))\n const pending = migrations.filter((migration) => !done.has(migration.id))\n yield* Effect.forEach(\n pending,\n (migration) =>\n sql.withTransaction(\n migration\n .run(sql)\n .pipe(\n Effect.andThen(\n sql`INSERT INTO _migrations (id, name) VALUES (${migration.id}, ${migration.name})`,\n ),\n ),\n ),\n { discard: true },\n )\n })\n\nimport { Array, Schema } from 'effect'\nimport { NoteSchema, type Note } from './note'\n\nconst decodeNote = (data: string): Effect.Effect<Note, DecodeError> =>\n Effect.try({\n try: () => JSON.parse(data) as unknown,\n catch: (cause) => new DecodeError({ data, cause }),\n }).pipe(\n Effect.flatMap((json) =>\n Schema.decodeUnknownEffect(NoteSchema)(json).pipe(\n Effect.mapError((cause) => new DecodeError({ data, cause })),\n ),\n ),\n )\n\nconst readNotes = (\n sql: SqlClient.SqlClient,\n project: string,\n): Effect.Effect<ReadonlyArray<Note>, SqlError.SqlError> =>\n sql<{ readonly data: string }>`\n SELECT data FROM notes WHERE project = ${project} ORDER BY seq\n `.pipe(\n Effect.flatMap((rows) =>\n Effect.forEach(rows, (row) =>\n decodeNote(row.data).pipe(\n Effect.tapError((error) =>\n Effect.logWarning('skipping a note that no longer decodes', error),\n ),\n Effect.map(Option.some),\n Effect.catchTag('DecodeError', () => Effect.succeed(Option.none<Note>())),\n ),\n ),\n ),\n Effect.map(Array.getSomes),\n )\n\nimport { Clock } from 'effect'\nimport { stampDraft, type Draft } from './note'\n\nconst recordDraft = (\n sql: SqlClient.SqlClient,\n draft: Draft,\n): Effect.Effect<Note, SqlError.SqlError> =>\n sql.withTransaction(\n Effect.gen(function* () {\n const millis = yield* Clock.currentTimeMillis\n const rows = yield* sql<{ readonly max: number | null }>`\n SELECT MAX(seq) AS max FROM notes WHERE project = ${draft.project}\n `\n const highest = Option.match(Option.fromNullishOr(rows[0]?.max), {\n onSome: (seq) => seq,\n onNone: () => 0,\n })\n const note = stampDraft(draft, highest + 1, new Date(millis).toISOString())\n yield* sql`\n INSERT INTO notes (project, seq, data)\n VALUES (${note.project}, ${note.seq}, ${JSON.stringify(note)})\n `\n return note\n }),\n )\n\nconst modifyNote = (\n sql: SqlClient.SqlClient,\n project: string,\n seq: number,\n change: (note: Note) => Note,\n): Effect.Effect<void, SqlError.SqlError | DecodeError> =>\n Effect.gen(function* () {\n const rows = yield* sql<{ readonly data: string }>`\n SELECT data FROM notes WHERE project = ${project} AND seq = ${seq}\n `\n yield* Option.match(Option.fromNullishOr(rows[0]), {\n onNone: () => Effect.void,\n onSome: (row) =>\n decodeNote(row.data).pipe(\n Effect.map(change),\n Effect.flatMap((updated) =>\n sql`\n UPDATE notes SET data = ${JSON.stringify(updated)}\n WHERE project = ${project} AND seq = ${seq}\n `,\n ),\n ),\n })\n })\n\nconst resolveNote = (sql: SqlClient.SqlClient, project: string, seq: number) =>\n modifyNote(sql, project, seq, (note) => ({ ...note, addressed: true }))\n\nconst editNote = (sql: SqlClient.SqlClient, project: string, seq: number, text: string) =>\n modifyNote(sql, project, seq, (note) => ({ ...note, text }))\n\nconst discardNote = (\n sql: SqlClient.SqlClient,\n project: string,\n seq: number,\n): Effect.Effect<void, SqlError.SqlError> =>\n sql`DELETE FROM notes WHERE project = ${project} AND seq = ${seq}`.pipe(Effect.asVoid)\n\ntype Project = {\n readonly id: string\n readonly name: string\n readonly directory: string | null\n readonly address: string | null\n readonly identity: string | null\n}\n\nconst listProjects = (\n sql: SqlClient.SqlClient,\n): Effect.Effect<ReadonlyArray<Project>, SqlError.SqlError> =>\n sql<Project>`SELECT id, name, directory, address, identity FROM projects ORDER BY name`\n\nconst findProject = (\n sql: SqlClient.SqlClient,\n name: string,\n): Effect.Effect<Option.Option<Project>, SqlError.SqlError> =>\n sql<Project>`\n SELECT id, name, directory, address, identity FROM projects WHERE id = ${name}\n `.pipe(Effect.map(Array.head))\n\nconst registerProject = (\n sql: SqlClient.SqlClient,\n name: string,\n identity: string,\n directory: string,\n address: string,\n): Effect.Effect<void, SqlError.SqlError> =>\n sql`\n INSERT INTO projects (id, name, directory, address, identity)\n VALUES (${name}, ${name}, ${directory}, ${address}, ${identity})\n ON CONFLICT (id) DO UPDATE SET\n directory = ${directory}, address = ${address}, identity = ${identity}\n `.pipe(Effect.asVoid)\n\nconst ensureProject = (\n sql: SqlClient.SqlClient,\n id: string,\n): Effect.Effect<void, SqlError.SqlError> =>\n sql`\n INSERT INTO projects (id, name) VALUES (${id}, ${id})\n ON CONFLICT (id) DO NOTHING\n `.pipe(Effect.asVoid)\n\nconst backfillProjects = (sql: SqlClient.SqlClient): Effect.Effect<void, SqlError.SqlError> =>\n sql`\n INSERT INTO projects (id, name)\n SELECT DISTINCT project, project FROM notes\n WHERE true\n ON CONFLICT (id) DO NOTHING\n `.pipe(Effect.asVoid)\n\nconst sqliteClientLayer = (\n filename: string,\n): Layer.Layer<SqlClient.SqlClient> =>\n Layer.unwrap(\n Effect.promise((): Promise<Layer.Layer<SqlClient.SqlClient>> =>\n typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'\n ? import('@effect/sql-sqlite-bun').then((m) =>\n m.SqliteClient.layer({ filename, create: true }),\n )\n : import('@effect/sql-sqlite-node').then((m) =>\n m.SqliteClient.layer({ filename }),\n ),\n ),\n )\n\nexport const sqliteStore = (filename: string): Layer.Layer<NoteStore> =>\n Layer.effect(NoteStore, NoteStore.make).pipe(\n Layer.provide(sqliteClientLayer(filename)),\n )\n\nimport { mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\nexport const notesFile = (): string =>\n process.env.LOOM_NOTES_DB ?? join(homedir(), '.loom', 'notes.sqlite')\n\nexport const localStore = (): Layer.Layer<NoteStore> =>\n Layer.unwrap(\n Effect.sync(() => {\n const file = notesFile()\n mkdirSync(dirname(file), { recursive: true })\n return sqliteStore(file)\n }),\n )\n\nimport { Context, PubSub } from 'effect'\n\nexport class NoteStore extends Context.Service<NoteStore>()('NoteStore', {\n make: Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient\n yield* migrate(sql)\n yield* backfillProjects(sql)\n const changes = yield* PubSub.unbounded<Note>()\n return {\n list: (project: string) => readNotes(sql, project),\n record: (draft: Draft) =>\n ensureProject(sql, draft.project).pipe(\n Effect.andThen(recordDraft(sql, draft)),\n Effect.tap((note) => PubSub.publish(changes, note)),\n ),\n resolve: (project: string, seq: number) => resolveNote(sql, project, seq),\n edit: (project: string, seq: number, text: string) => editNote(sql, project, seq, text),\n discard: (project: string, seq: number) => discardNote(sql, project, seq),\n projects: listProjects(sql),\n project: (name: string) => findProject(sql, name),\n register: (\n name: string,\n identity: string,\n directory: string,\n address: string,\n ) => registerProject(sql, name, identity, directory, address),\n changes,\n } as const\n }).pipe(Effect.orDie),\n}) {\n static readonly layer = localStore()\n}\n"],"mappings":"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"}
|
package/dist/ui.html
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Loom Design</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=JetBrains+Mono:wght@400;500&family=Newsreader:opsz,wght@6..72,300;6..72,400&display=swap" rel="stylesheet" />
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #0f1014;
|
|
13
|
+
--bg-2: #15171d;
|
|
14
|
+
--bg-3: #1b1e26;
|
|
15
|
+
--fg: #e6e6ea;
|
|
16
|
+
--fg-2: #b6b8c2;
|
|
17
|
+
--fg-3: #7a7e8c;
|
|
18
|
+
--rule: #ffffff1f;
|
|
19
|
+
--mint: #8fe0b6;
|
|
20
|
+
--violet: #b59bf1;
|
|
21
|
+
--display: 'Newsreader', Georgia, serif;
|
|
22
|
+
--sans: 'Inter', system-ui, -apple-system, sans-serif;
|
|
23
|
+
--mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
|
|
24
|
+
}
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
body {
|
|
27
|
+
margin: 0;
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
background: var(--bg);
|
|
30
|
+
color: var(--fg);
|
|
31
|
+
font-family: var(--sans);
|
|
32
|
+
display: grid;
|
|
33
|
+
place-items: center;
|
|
34
|
+
padding: 32px;
|
|
35
|
+
}
|
|
36
|
+
.card {
|
|
37
|
+
width: 100%;
|
|
38
|
+
max-width: 640px;
|
|
39
|
+
background: var(--bg-2);
|
|
40
|
+
border: 1px solid var(--rule);
|
|
41
|
+
border-radius: 12px;
|
|
42
|
+
padding: 36px;
|
|
43
|
+
}
|
|
44
|
+
.title {
|
|
45
|
+
font-family: var(--display);
|
|
46
|
+
font-weight: 300;
|
|
47
|
+
font-size: 34px;
|
|
48
|
+
letter-spacing: -0.02em;
|
|
49
|
+
margin-bottom: 12px;
|
|
50
|
+
}
|
|
51
|
+
.title .mint { color: var(--mint); }
|
|
52
|
+
.lede { color: var(--fg-2); line-height: 1.6; }
|
|
53
|
+
.label {
|
|
54
|
+
font-family: var(--mono);
|
|
55
|
+
font-size: 11px;
|
|
56
|
+
letter-spacing: 0.04em;
|
|
57
|
+
color: var(--fg-3);
|
|
58
|
+
margin: 28px 0 8px;
|
|
59
|
+
}
|
|
60
|
+
.input {
|
|
61
|
+
width: 100%;
|
|
62
|
+
background: var(--bg-3);
|
|
63
|
+
border: 1px solid var(--rule);
|
|
64
|
+
border-radius: 6px;
|
|
65
|
+
color: var(--fg);
|
|
66
|
+
font-family: var(--mono);
|
|
67
|
+
font-size: 14px;
|
|
68
|
+
padding: 11px 12px;
|
|
69
|
+
}
|
|
70
|
+
.input:focus { outline: none; border-color: var(--mint); }
|
|
71
|
+
.snippet {
|
|
72
|
+
margin-top: 20px;
|
|
73
|
+
background: var(--bg-3);
|
|
74
|
+
border: 1px solid var(--rule);
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
padding: 16px;
|
|
77
|
+
font-family: var(--mono);
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
line-height: 1.5;
|
|
80
|
+
color: var(--fg-2);
|
|
81
|
+
white-space: pre-wrap;
|
|
82
|
+
word-break: break-all;
|
|
83
|
+
}
|
|
84
|
+
.copy {
|
|
85
|
+
margin-top: 16px;
|
|
86
|
+
background: var(--mint);
|
|
87
|
+
color: var(--bg);
|
|
88
|
+
border: none;
|
|
89
|
+
border-radius: 6px;
|
|
90
|
+
font-family: var(--mono);
|
|
91
|
+
font-size: 13px;
|
|
92
|
+
font-weight: 500;
|
|
93
|
+
padding: 11px 18px;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
.copy:hover { background: var(--violet); }
|
|
97
|
+
</style>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<div id="app"></div>
|
|
101
|
+
<script type="module" src="/ui.js"></script>
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|