@softr/sdk 0.0.54-alpha.0 → 0.0.55

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 (37) hide show
  1. package/dist/action.d.ts +1 -31
  2. package/dist/action.js +1 -26
  3. package/dist/auth/isomorphic.d.ts +1 -1
  4. package/dist/auth/isomorphic.js +2 -1
  5. package/dist/boot-error.js +1 -0
  6. package/dist/client.d.ts +1 -2
  7. package/dist/client.js +1 -2
  8. package/dist/default-error-overlay.d.ts +2 -2
  9. package/dist/default-error-overlay.js +86 -2
  10. package/dist/fetch.js +2 -2
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +3 -2
  13. package/dist/mcp.d.ts +2 -2
  14. package/dist/package.js +1 -1
  15. package/dist/pwa.d.ts +54 -0
  16. package/dist/pwa.js +152 -0
  17. package/dist/server.d.ts +1 -2
  18. package/dist/server.js +1 -2
  19. package/package.json +1 -3
  20. package/skills/softr-sdk/SKILL.md +7 -7
  21. package/skills/softr-sdk/auth/SKILL.md +1 -1
  22. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  23. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  24. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  25. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  26. package/skills/softr-sdk/mcp/SKILL.md +1 -1
  27. package/skills/softr-sdk/pwa/SKILL.md +77 -0
  28. package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
  29. package/skills/softr-sdk/record-queries/SKILL.md +2 -2
  30. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  31. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  32. package/skills/softr-sdk/workflows/SKILL.md +1 -1
  33. package/dist/comments/comments.d.ts +0 -137
  34. package/dist/comments/comments.js +0 -329
  35. package/dist/comments/use-comment-composer.d.ts +0 -37
  36. package/dist/comments/use-comment-composer.js +0 -127
  37. package/skills/softr-sdk/comments/SKILL.md +0 -149
@@ -1,127 +0,0 @@
1
- import { assert } from "../assertions.js";
2
- import { useLayoutEffect, useRef, useState } from "react";
3
-
4
- //#region src/comments/use-comment-composer.ts
5
- const EMPTY = {
6
- text: "",
7
- mentions: [],
8
- caret: 0
9
- };
10
- /**
11
- * The state of a comment box: the text, the people mentioned in it, and the `@` fragment being
12
- * typed. Spread `textareaProps` onto a `<textarea>`. Show a mention menu while `query` is not
13
- * `null`, and call `pick` with the person chosen from it.
14
- *
15
- * Post `text` and `mentions` as they are, then call `reset` once the post succeeds.
16
- *
17
- * @example
18
- * const composer = useCommentComposer()
19
- * <textarea {...composer.textareaProps} />
20
- * {composer.query !== null && <MentionMenu query={composer.query} onPick={composer.pick} />}
21
- */
22
- function useCommentComposer() {
23
- const [state, setState] = useState(EMPTY);
24
- const ref = useRef(null);
25
- const pendingCaret = useRef(null);
26
- useLayoutEffect(() => {
27
- const caret = pendingCaret.current;
28
- if (caret === null || ref.current === null) return;
29
- pendingCaret.current = null;
30
- ref.current.focus();
31
- ref.current.setSelectionRange(caret, caret);
32
- });
33
- const fragment = findMentionQuery(state.text, state.caret, state.mentions);
34
- return {
35
- text: state.text,
36
- mentions: state.mentions,
37
- query: fragment?.query ?? null,
38
- textareaProps: {
39
- ref,
40
- value: state.text,
41
- onChange(event) {
42
- const next = event.currentTarget.value;
43
- const caret = event.currentTarget.selectionStart;
44
- setState((previous) => ({
45
- text: next,
46
- mentions: remapMentions(previous.text, next, previous.mentions),
47
- caret
48
- }));
49
- },
50
- onSelect(event) {
51
- const caret = event.currentTarget.selectionStart;
52
- setState((previous) => previous.caret === caret ? previous : {
53
- ...previous,
54
- caret
55
- });
56
- }
57
- },
58
- pick(person) {
59
- assert(fragment, "pick needs an @ fragment under the caret");
60
- const label = `@${person.name}`;
61
- const text = `${state.text.slice(0, fragment.start)}${label} ${state.text.slice(state.caret)}`;
62
- const added = {
63
- ...person,
64
- start: fragment.start,
65
- end: fragment.start + label.length
66
- };
67
- const caret = added.end + 1;
68
- pendingCaret.current = caret;
69
- setState({
70
- text,
71
- mentions: [...remapMentions(state.text, text, state.mentions), added],
72
- caret
73
- });
74
- },
75
- reset() {
76
- setState(EMPTY);
77
- }
78
- };
79
- }
80
- /** A fragment longer than this is prose, not a name being typed. */
81
- const MENTION_QUERY_MAX_LENGTH = 32;
82
- /**
83
- * Moves mention ranges through an edit. A textarea reports only its new value, so the edit is the
84
- * span between the longest common prefix and suffix. Ranges after it shift. Ranges it reached into
85
- * are dropped, which is how deleting part of a name un-mentions that person.
86
- */
87
- function remapMentions(previous, next, mentions) {
88
- if (previous === next) return mentions;
89
- let prefix = 0;
90
- while (prefix < previous.length && prefix < next.length && previous[prefix] === next[prefix]) prefix += 1;
91
- let suffix = 0;
92
- while (suffix < previous.length - prefix && suffix < next.length - prefix && previous[previous.length - 1 - suffix] === next[next.length - 1 - suffix]) suffix += 1;
93
- const editStart = prefix;
94
- const editEnd = previous.length - suffix;
95
- const delta = next.length - previous.length;
96
- return mentions.flatMap((mention) => {
97
- if (mention.end <= editStart) return [mention];
98
- if (mention.start >= editEnd) return [{
99
- ...mention,
100
- start: mention.start + delta,
101
- end: mention.end + delta
102
- }];
103
- return [];
104
- });
105
- }
106
- /**
107
- * The `@` fragment the caret sits in, or `null`. An `@` does not count when:
108
- * - it does not start a word, so an email address opens no menu;
109
- * - it belongs to a mention already made;
110
- * - the fragment ran past a newline or is long enough to be prose. Spaces are fine, names have them.
111
- */
112
- function findMentionQuery(text, caret, mentions) {
113
- const before = text.slice(0, caret);
114
- const at = before.lastIndexOf("@");
115
- if (at === -1) return null;
116
- if (at > 0 && !/\s/.test(before.charAt(at - 1))) return null;
117
- if (mentions.some((mention) => at >= mention.start && at < mention.end)) return null;
118
- const query = before.slice(at + 1);
119
- if (query.includes("\n") || query.length > MENTION_QUERY_MAX_LENGTH) return null;
120
- return {
121
- query,
122
- start: at
123
- };
124
- }
125
-
126
- //#endregion
127
- export { useCommentComposer };
@@ -1,149 +0,0 @@
1
- ---
2
- name: comments
3
- description: >-
4
- Comment threads on records: defineComments from @softr/sdk declares which
5
- datasource has comments; comments.list, create, delete, react, subscription,
6
- setSubscription and searchMentions from @softr/sdk/server read and write one
7
- record's thread inside a server function; useCommentComposer from
8
- @softr/sdk/client keeps a textarea's text and @mentions in step. Bodies come
9
- back as sanitised bodyHtml. A null thread means render nothing.
10
- metadata:
11
- type: sub-skill
12
- library: softr-sdk
13
- library_version: 0.0.52
14
- ---
15
-
16
- **Boundary.** `comments` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler. Importing it into a component fails the build. `useCommentComposer` lives on `@softr/sdk/client` and runs in components. `defineComments` comes from `@softr/sdk` and can be imported anywhere.
17
-
18
- # Comments on records
19
-
20
- Every record of a commented datasource has one thread. The SDK reads and writes it. The app owns everything you see: layout, copy, sort order, dates.
21
-
22
- ## Setup
23
-
24
- ```ts
25
- import { createServerFn } from '@tanstack/react-start'
26
- import { defineComments, type CommentMention } from '@softr/sdk'
27
- import { comments } from '@softr/sdk/server'
28
- import { useCommentComposer } from '@softr/sdk/client'
29
- ```
30
-
31
- ## Declare the comments action
32
-
33
- ```ts
34
- export const orderComments = defineComments({
35
- id: 'orderComments',
36
- datasourceId: '8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77',
37
- })
38
- ```
39
-
40
- - `id` is a name you choose, unique across the app. `datasourceId` is the table whose records get threads.
41
- - There is no `permissions`. Who may read, post and delete is set in Softr under Settings → Comments, the same settings native comment blocks use.
42
- - Declare it once per datasource. Two declarations on the same datasource are the **same** threads, so "internal notes" next to "customer comments" on one table is not possible.
43
- - Write the object inline. Passing it through a variable hides it from the build.
44
-
45
- ## Read a thread
46
-
47
- ```ts
48
- const getThread = createServerFn({ method: 'GET' })
49
- .validator((data: { recordId: string; offset?: number | null }) => data)
50
- .handler(({ data }) => comments.list(orderComments, { recordId: data.recordId, offset: data.offset }))
51
- ```
52
-
53
- `comments.list` answers `null` when this visitor cannot see comments on the record: comments are off for the app, the table is not set up yet, or the visitor's group may not read them. Render nothing for `null`, not an error.
54
-
55
- Otherwise it answers:
56
-
57
- | Field | What it is |
58
- | --- | --- |
59
- | `items` | The comments, newest first. Each is a `CommentView` |
60
- | `canPost` | Whether this visitor may post. Hide the composer when false |
61
- | `allowMentions` | Whether `@` opens a mention menu. Do not call `searchMentions` when false |
62
- | `quickReactions` | The five `{ emoji, label }` a comment can be reacted with. There are no others |
63
- | `total`, `nextOffset` | Paging. Pass `nextOffset` back as `offset` for older comments. `null` on the last page |
64
-
65
- A `CommentView` is `{ id, bodyHtml, attachments, author: { name, avatar, isDeactivated }, createdAt, canDelete, reactions }`. `createdAt` is ISO 8601. Format it with `useUserTimezone`. Each reaction is `{ emoji, label, count, isSelected }`.
66
-
67
- `bodyHtml` is already sanitised. Render it with `dangerouslySetInnerHTML` and nothing else:
68
-
69
- ```tsx
70
- <div className="comment-body" dangerouslySetInnerHTML={{ __html: comment.bodyHtml }} />
71
- ```
72
-
73
- It holds `p`, `br`, `strong`, `em`, `u`, `s`, `del`, `a`, `ul`, `ol`, `li`, `code`, `pre`, `blockquote`, `h1`–`h3` and mention `span[data-type="mention"]`. Links open in a new tab. Style those tags under the body's class, because the reset strips list bullets, link colour and spacing.
74
-
75
- ## Post a comment
76
-
77
- ```ts
78
- const postComment = createServerFn({ method: 'POST' })
79
- .validator((data: { recordId: string; body: string; mentions: Array<CommentMention> }) => data)
80
- .handler(({ data }) => comments.create(orderComments, data))
81
- ```
82
-
83
- Pass the composer's `text` as `body` and its `mentions` as they are. The SDK stores the mentions in the format native blocks read, and the people mentioned get an email. A mention whose `@name` was edited is posted as plain text. There is no editing a posted comment and no attachment upload.
84
-
85
- ## The composer
86
-
87
- ```tsx
88
- const composer = useCommentComposer()
89
-
90
- <textarea {...composer.textareaProps} placeholder="Write a comment" />
91
- {/* While composer.query is not null, list findPeople's answer for it. Picking a person calls composer.pick(person). */}
92
- <button
93
- disabled={composer.text.trim() === ''}
94
- onClick={async () => {
95
- await postComment({ data: { recordId, body: composer.text, mentions: composer.mentions } })
96
- composer.reset()
97
- }}
98
- >
99
- Post
100
- </button>
101
- ```
102
-
103
- - Spread `textareaProps` onto the `<textarea>`. It carries the `ref`, `value`, `onChange` and `onSelect` the composer needs, so do not replace them.
104
- - `query` is what follows `@` before the caret, or `null`. It is `""` the moment `@` is typed.
105
- - `pick({ name, email })` writes `@name`, records the mention and puts the caret after it.
106
- - `reset()` empties the box. Call it after the post succeeds, never before.
107
-
108
- ## Mentions
109
-
110
- ```ts
111
- const findPeople = createServerFn({ method: 'GET' })
112
- .validator((data: { query: string }) => data)
113
- .handler(({ data }) => comments.searchMentions(orderComments, data))
114
- ```
115
-
116
- It answers `[{ name, email, avatar }]`, the people this visitor may mention whose name or email matches. Who is mentionable is set in Settings → Comments, per user group.
117
-
118
- ## Delete, react, subscribe
119
-
120
- ```ts
121
- await comments.delete(orderComments, { recordId, commentId })
122
- await comments.react(orderComments, { recordId, commentId, emoji: '👍', selected: !reaction.isSelected })
123
- const subscribed = await comments.subscription(orderComments, { recordId })
124
- await comments.setSubscription(orderComments, { recordId, subscribed: !subscribed })
125
- ```
126
-
127
- Each runs inside its own server function, like `list`.
128
-
129
- - Offer delete only where `canDelete` is true.
130
- - `react` adds the visitor's reaction when `selected` is true and removes it when false. `emoji` must be one of `quickReactions`.
131
- - A subscription is whether the visitor gets an email when someone comments on this record.
132
-
133
- Re-read the thread after each write. The writes answer nothing.
134
-
135
- ## Failure modes
136
-
137
- **Keying the thread on the URL instead of the record.** Pass the fetched `record.id` as `recordId`, never a route param or a search param. A param can be a slug, stale, or a different record's id, and the thread then shows another record's comments.
138
-
139
- **Treating `null` as an error.** A visitor who may not read comments gets `null`. Render nothing. An error box for it shows every such visitor a broken page.
140
-
141
- **Rendering a body as text, or sanitising it again.** Use `bodyHtml` with `dangerouslySetInnerHTML`. As text it shows raw tags, and a second sanitiser strips the mention markup.
142
-
143
- **Inventing reactions.** Only the five in `quickReactions` can be stored. `react` throws for any other emoji.
144
-
145
- **Calling `comments.*` from a component.** It is server only. Wrap each call in a `createServerFn`.
146
-
147
- ## References
148
-
149
- - [record-queries/SKILL.md](../record-queries/SKILL.md): fetching the record whose `id` keys the thread