@softr/sdk 0.0.63-alpha.0 → 0.0.64-alpha.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 (56) hide show
  1. package/dist/action.d.ts +1 -31
  2. package/dist/action.js +1 -26
  3. package/dist/auth/isomorphic.d.ts +60 -1
  4. package/dist/auth/isomorphic.js +63 -5
  5. package/dist/auth/methods/passwordLink.d.ts +47 -0
  6. package/dist/auth/methods/passwordLink.js +86 -0
  7. package/dist/auth/methods/settings.d.ts +11 -0
  8. package/dist/auth/methods/settings.js +17 -0
  9. package/dist/auth/methods/signup.js +1 -0
  10. package/dist/auth/methods/social.js +1 -0
  11. package/dist/auth/methods/utils.d.ts +1 -1
  12. package/dist/auth/platform.d.ts +46 -0
  13. package/dist/auth/platform.js +54 -0
  14. package/dist/auth/server.d.ts +6 -0
  15. package/dist/auth/server.js +7 -1
  16. package/dist/client.d.ts +1 -2
  17. package/dist/client.js +1 -2
  18. package/dist/core/file-upload.d.ts +2 -7
  19. package/dist/core/file-upload.js +1 -6
  20. package/dist/core/use-file-upload.d.ts +2 -2
  21. package/dist/datasource/record.js +11 -5
  22. package/dist/default-error-overlay.d.ts +2 -2
  23. package/dist/formatting.d.ts +3 -3
  24. package/dist/index.d.ts +5 -5
  25. package/dist/index.js +4 -4
  26. package/dist/mcp.d.ts +2 -8
  27. package/dist/mcp.js +8 -8
  28. package/dist/package.js +1 -1
  29. package/dist/pwa.d.ts +2 -2
  30. package/dist/server.d.ts +1 -2
  31. package/dist/server.js +1 -2
  32. package/dist/softr-config.d.ts +1 -0
  33. package/dist/softr-config.js +26 -5
  34. package/dist/visibility/route.d.ts +26 -6
  35. package/dist/visibility/route.js +36 -13
  36. package/package.json +33 -12
  37. package/schema/softr.schema.json +42 -1
  38. package/skills/softr-sdk/SKILL.md +7 -10
  39. package/skills/softr-sdk/auth/SKILL.md +129 -31
  40. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  41. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  42. package/skills/softr-sdk/file-uploads/SKILL.md +4 -5
  43. package/skills/softr-sdk/formatting/SKILL.md +4 -4
  44. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  45. package/skills/softr-sdk/mcp/SKILL.md +7 -14
  46. package/skills/softr-sdk/pwa/SKILL.md +3 -3
  47. package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
  48. package/skills/softr-sdk/record-queries/SKILL.md +1 -1
  49. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  50. package/skills/softr-sdk/routes-and-visibility/SKILL.md +24 -12
  51. package/skills/softr-sdk/workflows/SKILL.md +1 -1
  52. package/dist/comments/comments.d.ts +0 -168
  53. package/dist/comments/comments.js +0 -413
  54. package/dist/comments/use-comment-composer.d.ts +0 -46
  55. package/dist/comments/use-comment-composer.js +0 -134
  56. package/skills/softr-sdk/comments/SKILL.md +0 -181
@@ -1,168 +0,0 @@
1
- import { CommentsAction } from "../action.js";
2
-
3
- //#region src/comments/comments.d.ts
4
- /** A person picked in the composer, and where their `@name` sits in the text. */
5
- type CommentMention = {
6
- name: string;
7
- email: string;
8
- start: number;
9
- end: number;
10
- };
11
- type CommentAttachmentInput = {
12
- url: string;
13
- filename: string;
14
- type?: string | null;
15
- };
16
- type CommentView = {
17
- id: string;
18
- /** The body as safe HTML. Render it with `dangerouslySetInnerHTML`, never as text. */
19
- bodyHtml: string;
20
- attachments: Array<{
21
- url: string;
22
- filename: string;
23
- type: string | null;
24
- }>;
25
- author: {
26
- name: string;
27
- avatar: string | null;
28
- isDeactivated: boolean;
29
- };
30
- /** ISO 8601. */
31
- createdAt: string;
32
- canDelete: boolean;
33
- canEdit: boolean;
34
- edited: boolean;
35
- /** The body as the visitor typed it, to fill an edit box. `null` unless `canEdit`. */
36
- source: {
37
- text: string;
38
- mentions: Array<CommentMention>;
39
- } | null;
40
- reactions: Array<{
41
- emoji: string;
42
- label: string;
43
- count: number;
44
- isSelected: boolean;
45
- }>;
46
- };
47
- type CommentThread = {
48
- canPost: boolean;
49
- allowMentions: boolean;
50
- allowAttachments: boolean;
51
- notificationsEnabled: boolean;
52
- quickReactions: ReadonlyArray<{
53
- emoji: string;
54
- label: string;
55
- }>;
56
- total: number;
57
- /** Pass it back as `offset` for the next page. `null` on the last page. */
58
- nextOffset: number | null;
59
- items: Array<CommentView>;
60
- };
61
- /**
62
- * The thread on one record, or `null` when this visitor cannot see comments on it. Render nothing
63
- * for `null`.
64
- *
65
- * @example
66
- * const thread = await comments.list(orderComments, { recordId: order.id })
67
- */
68
- declare function listComments(action: CommentsAction, options: {
69
- recordId: string;
70
- offset?: number | null;
71
- limit?: number;
72
- }): Promise<CommentThread | null>;
73
- /**
74
- * Posts a comment. `body` is the text the visitor typed. `mentions` are the people they picked,
75
- * each with the range of its `@name` in `body`. A range that no longer reads as `@name` is posted as
76
- * plain text. `attachments` are the files `useFileUpload` finished uploading.
77
- *
78
- * @example
79
- * await comments.create(orderComments, { recordId, body: "Hey @Ann", mentions, attachments })
80
- */
81
- declare function createComment(action: CommentsAction, options: {
82
- recordId: string;
83
- body: string;
84
- mentions: ReadonlyArray<CommentMention>;
85
- attachments?: ReadonlyArray<CommentAttachmentInput>;
86
- }): Promise<void>;
87
- /**
88
- * Saves an edit. Offer it only where the comment's `canEdit` is true, and fill the edit box from its
89
- * `source`. `attachments` is the full list after the edit: the files kept plus any new ones.
90
- *
91
- * @example
92
- * await comments.update(orderComments, { recordId, commentId, body, mentions, attachments })
93
- */
94
- declare function updateComment(action: CommentsAction, options: {
95
- recordId: string;
96
- commentId: string;
97
- body: string;
98
- mentions: ReadonlyArray<CommentMention>;
99
- attachments: ReadonlyArray<CommentAttachmentInput>;
100
- }): Promise<void>;
101
- /**
102
- * Deletes a comment. Offer it only where the comment's `canDelete` is true.
103
- *
104
- * @example
105
- * await comments.delete(orderComments, { recordId, commentId })
106
- */
107
- declare function deleteComment(action: CommentsAction, options: {
108
- recordId: string;
109
- commentId: string;
110
- }): Promise<void>;
111
- /**
112
- * Adds the visitor's reaction when `selected` is true, and removes it when false. Pass the
113
- * reaction's current `isSelected` negated.
114
- *
115
- * @example
116
- * await comments.react(orderComments, { recordId, commentId, emoji: "👍", selected: true })
117
- */
118
- declare function reactToComment(action: CommentsAction, options: {
119
- recordId: string;
120
- commentId: string;
121
- emoji: string;
122
- selected: boolean;
123
- }): Promise<void>;
124
- /**
125
- * Whether the visitor gets an email when someone comments on this record.
126
- *
127
- * @example
128
- * const subscribed = await comments.subscription(orderComments, { recordId })
129
- */
130
- declare function readSubscription(action: CommentsAction, options: {
131
- recordId: string;
132
- }): Promise<boolean>;
133
- /**
134
- * Turns the visitor's emails about this record's comments on or off.
135
- *
136
- * @example
137
- * await comments.setSubscription(orderComments, { recordId, subscribed: false })
138
- */
139
- declare function setSubscription(action: CommentsAction, options: {
140
- recordId: string;
141
- subscribed: boolean;
142
- }): Promise<void>;
143
- /**
144
- * The people the visitor may mention whose name or email matches `query`. Only call it when the
145
- * thread's `allowMentions` is true.
146
- *
147
- * @example
148
- * const people = await comments.searchMentions(orderComments, { query: "an" })
149
- */
150
- declare function searchMentions(action: CommentsAction, options: {
151
- query: string;
152
- }): Promise<Array<{
153
- name: string;
154
- email: string;
155
- avatar: string | null;
156
- }>>;
157
- declare const comments: {
158
- list: typeof listComments;
159
- create: typeof createComment;
160
- update: typeof updateComment;
161
- delete: typeof deleteComment;
162
- react: typeof reactToComment;
163
- subscription: typeof readSubscription;
164
- setSubscription: typeof setSubscription;
165
- searchMentions: typeof searchMentions;
166
- };
167
- //#endregion
168
- export { CommentAttachmentInput, CommentMention, CommentThread, CommentView, comments };
@@ -1,413 +0,0 @@
1
- import { assert } from "../assertions.js";
2
- import { fetch } from "../fetch.js";
3
- import { sessionConfig } from "../auth/config.js";
4
- import { readSessionToken } from "../auth/session.js";
5
- import { platformApiUrl } from "../auth/platform.js";
6
- import { describeFailure } from "../datasource/record.js";
7
- import { z } from "zod";
8
- import { Marked } from "marked";
9
- import { FilterXSS } from "xss";
10
-
11
- //#region src/comments/comments.ts
12
- /**
13
- * The emoji a comment can be reacted with. The app has no other set: an emoji outside it has no
14
- * label to store, and native blocks would show it without a name.
15
- */
16
- const QUICK_REACTIONS = [
17
- {
18
- emoji: "👍",
19
- label: "Thumbs Up"
20
- },
21
- {
22
- emoji: "🙌",
23
- label: "Raised Hands"
24
- },
25
- {
26
- emoji: "✅",
27
- label: "Check Mark"
28
- },
29
- {
30
- emoji: "🔥",
31
- label: "Fire"
32
- },
33
- {
34
- emoji: "👀",
35
- label: "Eyes"
36
- }
37
- ];
38
- const threadSchema = z.object({
39
- items: z.array(z.object({
40
- id: z.string(),
41
- comment: z.string(),
42
- attachments: z.array(z.object({
43
- url: z.string(),
44
- filename: z.string(),
45
- type: z.string().nullish()
46
- })),
47
- author: z.object({
48
- name: z.string(),
49
- avatar: z.string().nullable(),
50
- deactivated: z.boolean()
51
- }),
52
- createdAt: z.number(),
53
- canDelete: z.boolean(),
54
- canEdit: z.boolean(),
55
- edited: z.boolean(),
56
- reactions: z.array(z.object({
57
- emoji: z.string(),
58
- label: z.string(),
59
- count: z.number(),
60
- selected: z.boolean()
61
- }))
62
- })),
63
- total: z.number(),
64
- offset: z.number(),
65
- canPost: z.boolean(),
66
- allowMentions: z.boolean(),
67
- allowAttachments: z.boolean(),
68
- notificationsEnabled: z.boolean()
69
- });
70
- const subscriptionSchema = z.object({ subscribed: z.boolean() });
71
- const mentionCandidatesSchema = z.array(z.object({
72
- name: z.string(),
73
- email: z.string(),
74
- avatar: z.string().nullable()
75
- }));
76
- async function request(action, path, init) {
77
- const token = readSessionToken();
78
- const headers = {};
79
- if (token !== null) headers.Authorization = `Bearer ${token}`;
80
- if (init.body !== void 0) headers["Content-Type"] = "application/json";
81
- return fetch(`${await platformApiUrl()}/v1/vibe/applications/${sessionConfig().applicationId}/actions/${encodeURIComponent(action.id)}/comments${path}`, {
82
- method: init.method,
83
- headers,
84
- body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
85
- });
86
- }
87
- async function failure(action, what, res) {
88
- return /* @__PURE__ */ new Error(`comments ${what} failed for "${action.id}": ${await describeFailure(res)}`);
89
- }
90
- function recordPath(recordId) {
91
- assert(recordId, "a record id must not be empty");
92
- return `/records/${encodeURIComponent(recordId)}`;
93
- }
94
- /**
95
- * The thread on one record, or `null` when this visitor cannot see comments on it. Render nothing
96
- * for `null`.
97
- *
98
- * @example
99
- * const thread = await comments.list(orderComments, { recordId: order.id })
100
- */
101
- async function listComments(action, options) {
102
- const query = new URLSearchParams();
103
- if (options.offset !== void 0 && options.offset !== null) query.set("offset", String(options.offset));
104
- if (options.limit !== void 0) query.set("limit", String(options.limit));
105
- const search = query.size > 0 ? `?${query}` : "";
106
- const res = await request(action, `${recordPath(options.recordId)}${search}`, { method: "GET" });
107
- if (res.status === 403) return null;
108
- if (!res.ok) throw await failure(action, "list", res);
109
- const payload = threadSchema.parse(await res.json());
110
- const next = payload.offset + payload.items.length;
111
- return {
112
- canPost: payload.canPost,
113
- allowMentions: payload.allowMentions,
114
- allowAttachments: payload.allowAttachments,
115
- notificationsEnabled: payload.notificationsEnabled,
116
- quickReactions: QUICK_REACTIONS,
117
- total: payload.total,
118
- nextOffset: next < payload.total ? next : null,
119
- items: payload.items.map((item) => ({
120
- id: item.id,
121
- bodyHtml: renderCommentBody(item.comment),
122
- attachments: item.attachments.map(({ url, filename, type }) => ({
123
- url,
124
- filename,
125
- type: type ?? null
126
- })),
127
- author: {
128
- name: item.author.name,
129
- avatar: item.author.avatar,
130
- isDeactivated: item.author.deactivated
131
- },
132
- createdAt: new Date(item.createdAt).toISOString(),
133
- canDelete: item.canDelete,
134
- canEdit: item.canEdit,
135
- edited: item.edited,
136
- source: item.canEdit ? decodeCommentBody(item.comment) : null,
137
- reactions: item.reactions.map(({ selected,...reaction }) => ({
138
- ...reaction,
139
- isSelected: selected
140
- }))
141
- }))
142
- };
143
- }
144
- /**
145
- * Posts a comment. `body` is the text the visitor typed. `mentions` are the people they picked,
146
- * each with the range of its `@name` in `body`. A range that no longer reads as `@name` is posted as
147
- * plain text. `attachments` are the files `useFileUpload` finished uploading.
148
- *
149
- * @example
150
- * await comments.create(orderComments, { recordId, body: "Hey @Ann", mentions, attachments })
151
- */
152
- async function createComment(action, options) {
153
- const attachments = options.attachments ?? [];
154
- assert(options.body.trim() || attachments.length > 0, "a comment needs text or an attachment");
155
- const res = await request(action, recordPath(options.recordId), {
156
- method: "POST",
157
- body: {
158
- comment: encodeCommentBody(options.body, options.mentions),
159
- attachments: toWireAttachments(attachments)
160
- }
161
- });
162
- if (!res.ok) throw await failure(action, "create", res);
163
- }
164
- /**
165
- * Saves an edit. Offer it only where the comment's `canEdit` is true, and fill the edit box from its
166
- * `source`. `attachments` is the full list after the edit: the files kept plus any new ones.
167
- *
168
- * @example
169
- * await comments.update(orderComments, { recordId, commentId, body, mentions, attachments })
170
- */
171
- async function updateComment(action, options) {
172
- assert(options.body.trim() || options.attachments.length > 0, "a comment needs text or an attachment");
173
- const res = await request(action, `${recordPath(options.recordId)}/${encodeURIComponent(options.commentId)}`, {
174
- method: "PATCH",
175
- body: {
176
- comment: encodeCommentBody(options.body, options.mentions),
177
- attachments: toWireAttachments(options.attachments)
178
- }
179
- });
180
- if (!res.ok) throw await failure(action, "update", res);
181
- }
182
- /** Only the url, name and type travel. `useFileUpload` adds an `id` the platform has no use for. */
183
- function toWireAttachments(attachments) {
184
- return attachments.map(({ url, filename, type }) => ({
185
- url,
186
- filename,
187
- type: type ?? null
188
- }));
189
- }
190
- /**
191
- * Deletes a comment. Offer it only where the comment's `canDelete` is true.
192
- *
193
- * @example
194
- * await comments.delete(orderComments, { recordId, commentId })
195
- */
196
- async function deleteComment(action, options) {
197
- const res = await request(action, `${recordPath(options.recordId)}/${encodeURIComponent(options.commentId)}`, { method: "DELETE" });
198
- if (!res.ok) throw await failure(action, "delete", res);
199
- }
200
- /**
201
- * Adds the visitor's reaction when `selected` is true, and removes it when false. Pass the
202
- * reaction's current `isSelected` negated.
203
- *
204
- * @example
205
- * await comments.react(orderComments, { recordId, commentId, emoji: "👍", selected: true })
206
- */
207
- async function reactToComment(action, options) {
208
- const reaction = QUICK_REACTIONS.find(({ emoji }) => emoji === options.emoji);
209
- assert(reaction, `"${options.emoji}" is not one of the quick reactions`);
210
- const path = `${recordPath(options.recordId)}/${encodeURIComponent(options.commentId)}/react`;
211
- const res = options.selected ? await request(action, path, {
212
- method: "PUT",
213
- body: reaction
214
- }) : await request(action, `${path}?${new URLSearchParams({ emoji: reaction.emoji })}`, { method: "DELETE" });
215
- if (!res.ok) throw await failure(action, "react", res);
216
- }
217
- /**
218
- * Whether the visitor gets an email when someone comments on this record.
219
- *
220
- * @example
221
- * const subscribed = await comments.subscription(orderComments, { recordId })
222
- */
223
- async function readSubscription(action, options) {
224
- const res = await request(action, `${recordPath(options.recordId)}/subscription`, { method: "GET" });
225
- if (!res.ok) throw await failure(action, "subscription", res);
226
- return subscriptionSchema.parse(await res.json()).subscribed;
227
- }
228
- /**
229
- * Turns the visitor's emails about this record's comments on or off.
230
- *
231
- * @example
232
- * await comments.setSubscription(orderComments, { recordId, subscribed: false })
233
- */
234
- async function setSubscription(action, options) {
235
- const res = await request(action, `${recordPath(options.recordId)}/subscription`, {
236
- method: "PUT",
237
- body: { subscribed: options.subscribed }
238
- });
239
- if (!res.ok) throw await failure(action, "set subscription", res);
240
- }
241
- /**
242
- * The people the visitor may mention whose name or email matches `query`. Only call it when the
243
- * thread's `allowMentions` is true.
244
- *
245
- * @example
246
- * const people = await comments.searchMentions(orderComments, { query: "an" })
247
- */
248
- async function searchMentions(action, options) {
249
- const res = await request(action, `/mentions?${new URLSearchParams({ query: options.query })}`, { method: "GET" });
250
- if (!res.ok) throw await failure(action, "mention search", res);
251
- return mentionCandidatesSchema.parse(await res.json());
252
- }
253
- const comments = {
254
- list: listComments,
255
- create: createComment,
256
- update: updateComment,
257
- delete: deleteComment,
258
- react: reactToComment,
259
- subscription: readSubscription,
260
- setSubscription,
261
- searchMentions
262
- };
263
- /**
264
- * Splices each mention into the body as JSON. A range that does not read as its own `@name` is
265
- * left as text, so a composer that fell out of step never puts an email on the wrong words.
266
- */
267
- function encodeCommentBody(body, mentions) {
268
- const ordered = mentions.filter(({ name, start, end }) => body.slice(start, end) === `@${name}`).toSorted((a, b) => a.start - b.start);
269
- let encoded = "";
270
- let cursor = 0;
271
- for (const { name, email, start, end } of ordered) {
272
- if (start < cursor) continue;
273
- encoded += body.slice(cursor, start) + JSON.stringify({
274
- name,
275
- email
276
- });
277
- cursor = end;
278
- }
279
- return encoded + body.slice(cursor);
280
- }
281
- const MENTION = /^\{\s*(?:"name"\s*:\s*"(?:[^"\\]|\\.)*"\s*,\s*"email"\s*:\s*"(?:[^"\\]|\\.)*"|"email"\s*:\s*"(?:[^"\\]|\\.)*"\s*,\s*"name"\s*:\s*"(?:[^"\\]|\\.)*")\s*\}/;
282
- const mentionSchema = z.object({
283
- name: z.string(),
284
- email: z.string()
285
- });
286
- /** The mention's fields, or `undefined` when the match is not valid JSON, e.g. a stray `\q`. */
287
- function parseMention(raw) {
288
- try {
289
- return mentionSchema.parse(JSON.parse(raw));
290
- } catch {
291
- return;
292
- }
293
- }
294
- /**
295
- * The reverse of `encodeCommentBody`: each mention becomes its `@name` again, with its range. The
296
- * rest of the body stays as stored, so markdown from a native author shows as its source.
297
- */
298
- function decodeCommentBody(comment) {
299
- let text = "";
300
- const mentions = [];
301
- let rest = comment;
302
- while (rest.length > 0) {
303
- const brace = rest.indexOf("{");
304
- if (brace === -1) return {
305
- text: text + rest,
306
- mentions
307
- };
308
- text += rest.slice(0, brace);
309
- rest = rest.slice(brace);
310
- const match = MENTION.exec(rest);
311
- const mention = match === null ? void 0 : parseMention(match[0]);
312
- if (match === null || mention === void 0) {
313
- text += "{";
314
- rest = rest.slice(1);
315
- continue;
316
- }
317
- const name = mention.name || mention.email;
318
- const label = `@${name}`;
319
- mentions.push({
320
- name,
321
- email: mention.email,
322
- start: text.length,
323
- end: text.length + label.length
324
- });
325
- text += label;
326
- rest = rest.slice(match[0].length);
327
- }
328
- return {
329
- text,
330
- mentions
331
- };
332
- }
333
- const markdown = new Marked({
334
- gfm: true,
335
- breaks: true,
336
- async: false,
337
- extensions: [{
338
- name: "mention",
339
- level: "inline",
340
- start: (src) => src.indexOf("{"),
341
- tokenizer(src) {
342
- const match = MENTION.exec(src);
343
- if (match === null) return void 0;
344
- const mention = parseMention(match[0]);
345
- if (mention === void 0) return void 0;
346
- return {
347
- type: "mention",
348
- raw: match[0],
349
- ...mention
350
- };
351
- },
352
- renderer(token) {
353
- const label = String(token.name || token.email);
354
- return `<span data-type="mention" data-name="${escapeHtml(label)}">@${escapeHtml(label)}</span>`;
355
- }
356
- }]
357
- });
358
- const sanitizer = new FilterXSS({
359
- whiteList: Object.fromEntries([
360
- "p",
361
- "br",
362
- "strong",
363
- "em",
364
- "u",
365
- "s",
366
- "a",
367
- "ul",
368
- "ol",
369
- "li",
370
- "code",
371
- "pre",
372
- "blockquote",
373
- "h1",
374
- "h2",
375
- "h3",
376
- "span",
377
- "del"
378
- ].map((tag) => [tag, tag === "a" ? ["href", "title"] : []])),
379
- stripIgnoreTag: true,
380
- stripIgnoreTagBody: ["script", "style"],
381
- onTagAttr(tag, name, value) {
382
- if (tag !== "a") return void 0;
383
- if (name === "target" || name === "rel") return "";
384
- if (name !== "href") return void 0;
385
- const href = linkify(value);
386
- if (!SAFE_HREF.test(href)) return "";
387
- return `href="${escapeAttribute(href)}" target="_blank" rel="noopener noreferrer"`;
388
- },
389
- onIgnoreTagAttr(_tag, name, value) {
390
- if (!name.startsWith("data-")) return void 0;
391
- return `${name}="${escapeAttribute(value)}"`;
392
- }
393
- });
394
- const SAFE_HREF = /^(?:https?:\/\/|mailto:|tel:|#|\/)/i;
395
- /** A bare domain like `example.com` becomes an https link, as native blocks do. */
396
- function linkify(href) {
397
- const trimmed = href.trim();
398
- if (/^(?:[a-z][a-z0-9+.-]*:|#|\/)/i.test(trimmed)) return trimmed;
399
- return trimmed.includes(".") ? `https://${trimmed}` : "";
400
- }
401
- function escapeHtml(text) {
402
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
403
- }
404
- function escapeAttribute(value) {
405
- return value.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
406
- }
407
- function renderCommentBody(comment) {
408
- const trimmed = comment.replace(/^(?:\s*<br\s*\/?>\s*)+/i, "").replace(/(?:\s*<br\s*\/?>\s*)+$/i, "");
409
- return sanitizer.process(markdown.parse(trimmed, { async: false }));
410
- }
411
-
412
- //#endregion
413
- export { comments };
@@ -1,46 +0,0 @@
1
- import { CommentMention } from "./comments.js";
2
- import { ChangeEvent, RefObject, SyntheticEvent } from "react";
3
-
4
- //#region src/comments/use-comment-composer.d.ts
5
-
6
- /**
7
- * The state of a comment box: the text, the people mentioned in it, and the `@` fragment being
8
- * typed. Spread `textareaProps` onto a `<textarea>`. Show a mention menu while `query` is not
9
- * `null`, and call `pick` with the person chosen from it.
10
- *
11
- * Post `text` and `mentions` as they are, then call `reset` once the post succeeds.
12
- *
13
- * To edit a comment, pass its source as initial, in a composer mounted only while editing.
14
- * `initial` is read once, on mount.
15
- *
16
- * @example
17
- * const composer = useCommentComposer()
18
- * <textarea {...composer.textareaProps} />
19
- * {composer.query !== null && <MentionMenu query={composer.query} onPick={composer.pick} />}
20
- */
21
- declare function useCommentComposer(options?: {
22
- /** Fills the box to edit a comment: pass its `source`. */
23
- initial?: {
24
- text: string;
25
- mentions: ReadonlyArray<CommentMention>;
26
- };
27
- }): {
28
- text: string;
29
- mentions: Array<CommentMention>;
30
- /** What follows the `@` before the caret, or `null` when the caret is not in a mention. */
31
- query: string | null;
32
- textareaProps: {
33
- ref: RefObject<HTMLTextAreaElement | null>;
34
- value: string;
35
- onChange: (event: ChangeEvent<HTMLTextAreaElement>) => void;
36
- onSelect: (event: SyntheticEvent<HTMLTextAreaElement>) => void;
37
- };
38
- /** Replaces the `@` fragment with the person's `@name`, and puts the caret after it. */
39
- pick: (person: {
40
- name: string;
41
- email: string;
42
- }) => void;
43
- reset: () => void;
44
- };
45
- //#endregion
46
- export { useCommentComposer };