@softr/sdk 0.0.74 → 0.0.75-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 (41) hide show
  1. package/dist/action.d.ts +31 -1
  2. package/dist/action.js +26 -1
  3. package/dist/client.d.ts +2 -1
  4. package/dist/client.js +2 -1
  5. package/dist/codegen/datasource-types.js +1 -14
  6. package/dist/comments/comments.d.ts +168 -0
  7. package/dist/comments/comments.js +413 -0
  8. package/dist/comments/use-comment-composer.d.ts +46 -0
  9. package/dist/comments/use-comment-composer.js +134 -0
  10. package/dist/core/file-upload.d.ts +7 -2
  11. package/dist/core/file-upload.js +6 -1
  12. package/dist/core/options.d.ts +17 -2
  13. package/dist/core/options.js +30 -2
  14. package/dist/core/use-file-upload.d.ts +2 -2
  15. package/dist/datasource/datasource.d.ts +2 -1
  16. package/dist/datasource/datasource.js +3 -2
  17. package/dist/datasource/fields.d.ts +16 -24
  18. package/dist/default-error-overlay.d.ts +2 -2
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.js +2 -2
  21. package/dist/package.js +1 -1
  22. package/dist/pwa.d.ts +2 -2
  23. package/dist/server.d.ts +3 -2
  24. package/dist/server.js +2 -1
  25. package/package.json +3 -1
  26. package/skills/softr-sdk/SKILL.md +8 -4
  27. package/skills/softr-sdk/auth/SKILL.md +1 -1
  28. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  29. package/skills/softr-sdk/comments/SKILL.md +181 -0
  30. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  31. package/skills/softr-sdk/file-uploads/SKILL.md +5 -4
  32. package/skills/softr-sdk/formatting/SKILL.md +1 -1
  33. package/skills/softr-sdk/integrations/SKILL.md +1 -1
  34. package/skills/softr-sdk/mcp/SKILL.md +1 -1
  35. package/skills/softr-sdk/pwa/SKILL.md +1 -1
  36. package/skills/softr-sdk/record-mutations/SKILL.md +2 -2
  37. package/skills/softr-sdk/record-queries/SKILL.md +7 -1
  38. package/skills/softr-sdk/record-queries/references/filters-and-sorting.md +1 -1
  39. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  40. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  41. package/skills/softr-sdk/workflows/SKILL.md +1 -1
@@ -0,0 +1,46 @@
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 };
@@ -0,0 +1,134 @@
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
+ * To edit a comment, pass its source as initial, in a composer mounted only while editing.
18
+ * `initial` is read once, on mount.
19
+ *
20
+ * @example
21
+ * const composer = useCommentComposer()
22
+ * <textarea {...composer.textareaProps} />
23
+ * {composer.query !== null && <MentionMenu query={composer.query} onPick={composer.pick} />}
24
+ */
25
+ function useCommentComposer(options) {
26
+ const [state, setState] = useState(() => options?.initial === void 0 ? EMPTY : {
27
+ text: options.initial.text,
28
+ mentions: [...options.initial.mentions],
29
+ caret: options.initial.text.length
30
+ });
31
+ const ref = useRef(null);
32
+ const pendingCaret = useRef(null);
33
+ useLayoutEffect(() => {
34
+ const caret = pendingCaret.current;
35
+ if (caret === null || ref.current === null) return;
36
+ pendingCaret.current = null;
37
+ ref.current.focus();
38
+ ref.current.setSelectionRange(caret, caret);
39
+ });
40
+ const fragment = findMentionQuery(state.text, state.caret, state.mentions);
41
+ return {
42
+ text: state.text,
43
+ mentions: state.mentions,
44
+ query: fragment?.query ?? null,
45
+ textareaProps: {
46
+ ref,
47
+ value: state.text,
48
+ onChange(event) {
49
+ const next = event.currentTarget.value;
50
+ const caret = event.currentTarget.selectionStart;
51
+ setState((previous) => ({
52
+ text: next,
53
+ mentions: remapMentions(previous.text, next, previous.mentions),
54
+ caret
55
+ }));
56
+ },
57
+ onSelect(event) {
58
+ const caret = event.currentTarget.selectionStart;
59
+ setState((previous) => previous.caret === caret ? previous : {
60
+ ...previous,
61
+ caret
62
+ });
63
+ }
64
+ },
65
+ pick(person) {
66
+ assert(fragment, "pick needs an @ fragment under the caret");
67
+ const label = `@${person.name}`;
68
+ const text = `${state.text.slice(0, fragment.start)}${label} ${state.text.slice(state.caret)}`;
69
+ const added = {
70
+ ...person,
71
+ start: fragment.start,
72
+ end: fragment.start + label.length
73
+ };
74
+ const caret = added.end + 1;
75
+ pendingCaret.current = caret;
76
+ setState({
77
+ text,
78
+ mentions: [...remapMentions(state.text, text, state.mentions), added],
79
+ caret
80
+ });
81
+ },
82
+ reset() {
83
+ setState(EMPTY);
84
+ }
85
+ };
86
+ }
87
+ /** A fragment longer than this is prose, not a name being typed. */
88
+ const MENTION_QUERY_MAX_LENGTH = 32;
89
+ /**
90
+ * Moves mention ranges through an edit. A textarea reports only its new value, so the edit is the
91
+ * span between the longest common prefix and suffix. Ranges after it shift. Ranges it reached into
92
+ * are dropped, which is how deleting part of a name un-mentions that person.
93
+ */
94
+ function remapMentions(previous, next, mentions) {
95
+ if (previous === next) return mentions;
96
+ let prefix = 0;
97
+ while (prefix < previous.length && prefix < next.length && previous[prefix] === next[prefix]) prefix += 1;
98
+ let suffix = 0;
99
+ while (suffix < previous.length - prefix && suffix < next.length - prefix && previous[previous.length - 1 - suffix] === next[next.length - 1 - suffix]) suffix += 1;
100
+ const editStart = prefix;
101
+ const editEnd = previous.length - suffix;
102
+ const delta = next.length - previous.length;
103
+ return mentions.flatMap((mention) => {
104
+ if (mention.end <= editStart) return [mention];
105
+ if (mention.start >= editEnd) return [{
106
+ ...mention,
107
+ start: mention.start + delta,
108
+ end: mention.end + delta
109
+ }];
110
+ return [];
111
+ });
112
+ }
113
+ /**
114
+ * The `@` fragment the caret sits in, or `null`. An `@` does not count when:
115
+ * - it does not start a word, so an email address opens no menu;
116
+ * - it belongs to a mention already made;
117
+ * - the fragment ran past a newline or is long enough to be prose. Spaces are fine, names have them.
118
+ */
119
+ function findMentionQuery(text, caret, mentions) {
120
+ const before = text.slice(0, caret);
121
+ const at = before.lastIndexOf("@");
122
+ if (at === -1) return null;
123
+ if (at > 0 && !/\s/.test(before.charAt(at - 1))) return null;
124
+ if (mentions.some((mention) => at >= mention.start && at < mention.end)) return null;
125
+ const query = before.slice(at + 1);
126
+ if (query.includes("\n") || query.length > MENTION_QUERY_MAX_LENGTH) return null;
127
+ return {
128
+ query,
129
+ start: at
130
+ };
131
+ }
132
+
133
+ //#endregion
134
+ export { useCommentComposer };
@@ -1,4 +1,4 @@
1
- import { DatasourceAction } from "../action.js";
1
+ import { CommentsAction, DatasourceAction } from "../action.js";
2
2
 
3
3
  //#region src/core/file-upload.d.ts
4
4
 
@@ -10,7 +10,12 @@ type FileAttachment = {
10
10
  url: string;
11
11
  filename: string;
12
12
  id: string;
13
+ type?: string | null;
13
14
  };
14
- declare function upload(action: DatasourceAction, file: File): Promise<FileAttachment>;
15
+ /**
16
+ * A COMMENTS action uploads a comment attachment.
17
+ * The platform checks the comment settings.
18
+ */
19
+ declare function upload(action: DatasourceAction | CommentsAction, file: File): Promise<FileAttachment>;
15
20
  //#endregion
16
21
  export { FileAttachment, upload };
@@ -5,6 +5,10 @@ import { platformApiUrl } from "../auth/platform.js";
5
5
  import { randomUUID } from "node:crypto";
6
6
 
7
7
  //#region src/core/file-upload.ts
8
+ /**
9
+ * A COMMENTS action uploads a comment attachment.
10
+ * The platform checks the comment settings.
11
+ */
8
12
  async function upload(action, file) {
9
13
  const { applicationId } = sessionConfig();
10
14
  const baseUrl = await platformApiUrl();
@@ -23,7 +27,8 @@ async function upload(action, file) {
23
27
  return {
24
28
  url: resultJSON.url,
25
29
  filename: resultJSON.filename || "Untitled file",
26
- id: resultJSON.id || randomUUID()
30
+ id: resultJSON.id || randomUUID(),
31
+ type: resultJSON.type ?? null
27
32
  };
28
33
  }
29
34
 
@@ -1,7 +1,22 @@
1
- import { DatasourceAction, ReadShape } from "../action.js";
1
+ import { ActionShape, DatasourceAction, ReadShape } from "../action.js";
2
2
  import "@tanstack/react-start/server-only";
3
3
 
4
4
  //#region src/core/options.d.ts
5
+ /** One choice of a SELECT field, as the field holds it now. */
6
+ type FieldChoice = {
7
+ id: string;
8
+ label: string;
9
+ color: string | null;
10
+ };
11
+ /**
12
+ * The choices a SELECT field holds right now, read from Softr on every call. A choice the
13
+ * builder adds or renames later shows up without a new build. The field must be one the
14
+ * action declares. Any query or mutation over the table works.
15
+ *
16
+ * @example
17
+ * const loadStatuses = createServerFn().handler(() => ds.choices(listProjects, 'status'))
18
+ */
19
+ declare function listChoices<TShape extends ActionShape>(action: DatasourceAction<TShape>, field: keyof TShape["fields"] & string): Promise<Array<FieldChoice>>;
5
20
  /** One row a linked-record picker offers. */
6
21
  type RecordOption = {
7
22
  id: string;
@@ -33,4 +48,4 @@ declare function listOptions<TShape extends ReadShape>(query: DatasourceAction<T
33
48
  limit?: number;
34
49
  }): Promise<RecordOptions>;
35
50
  //#endregion
36
- export { RecordOption, RecordOptions, listOptions };
51
+ export { FieldChoice, RecordOption, RecordOptions, listChoices, listOptions };
@@ -1,9 +1,37 @@
1
+ import { assert } from "../assertions.js";
2
+ import { fetch } from "../fetch.js";
3
+ import { readSessionToken } from "../auth/session.js";
1
4
  import { makeChain } from "../datasource/fields.js";
2
- import { listRecords } from "../datasource/record.js";
5
+ import { describeFailure, dsUrl, listRecords, refererHeader, resolveTarget } from "../datasource/record.js";
3
6
  import "@tanstack/react-start/server-only";
4
7
 
5
8
  //#region src/core/options.ts
6
9
  /**
10
+ * The choices a SELECT field holds right now, read from Softr on every call. A choice the
11
+ * builder adds or renames later shows up without a new build. The field must be one the
12
+ * action declares. Any query or mutation over the table works.
13
+ *
14
+ * @example
15
+ * const loadStatuses = createServerFn().handler(() => ds.choices(listProjects, 'status'))
16
+ */
17
+ async function listChoices(action, field) {
18
+ const { actionId, select } = resolveTarget(action, action.resource.operation);
19
+ const fieldId = select[field];
20
+ assert(fieldId, () => `action "${actionId}" declares no field "${field}"`);
21
+ const res = await fetch(`${dsUrl(actionId)}/metadata`, { headers: {
22
+ Authorization: `Bearer ${readSessionToken()}`,
23
+ ...refererHeader()
24
+ } });
25
+ if (!res.ok) throw new Error(`datasource metadata failed for "${actionId}": ${await describeFailure(res)}`);
26
+ const metadataField = (await res.json()).fields.find(({ id }) => id === fieldId);
27
+ assert(metadataField?.type === "SELECT", () => `"${field}" on action "${actionId}" is not a SELECT field`);
28
+ return (metadataField.options?.choices ?? []).map(({ id, label, color }) => ({
29
+ id,
30
+ label,
31
+ color: color ?? null
32
+ }));
33
+ }
34
+ /**
7
35
  * The rows a linked field can point at, for a picker: each record of the query as its id and the text
8
36
  * of one field, searched on that field when a term is given, a page at a time.
9
37
  *
@@ -36,4 +64,4 @@ function labelOf(value) {
36
64
  }
37
65
 
38
66
  //#endregion
39
- export { listOptions };
67
+ export { listChoices, listOptions };
@@ -1,4 +1,4 @@
1
- import { DatasourceAction } from "../action.js";
1
+ import { CommentsAction, DatasourceAction } from "../action.js";
2
2
  import { FileAttachment } from "./file-upload.js";
3
3
 
4
4
  //#region src/core/use-file-upload.d.ts
@@ -16,7 +16,7 @@ type FileItem = {
16
16
  filename: string;
17
17
  error: Error;
18
18
  };
19
- declare function useFileUpload(action: DatasourceAction, options?: {
19
+ declare function useFileUpload(action: DatasourceAction | CommentsAction, options?: {
20
20
  initialAttachments?: FileAttachment[];
21
21
  }): {
22
22
  files: FileItem[];
@@ -1,5 +1,5 @@
1
1
  import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
- import { listOptions } from "../core/options.js";
2
+ import { listChoices, listOptions } from "../core/options.js";
3
3
  import { chart, metric } from "./chart.js";
4
4
  import "@tanstack/react-start/server-only";
5
5
 
@@ -13,6 +13,7 @@ declare const ds: {
13
13
  metric: typeof metric;
14
14
  chart: typeof chart;
15
15
  options: typeof listOptions;
16
+ choices: typeof listChoices;
16
17
  };
17
18
  //#endregion
18
19
  export { ds };
@@ -1,6 +1,6 @@
1
1
  import { createRecord, deleteRecord, getRecord, listRecords, updateRecord } from "./record.js";
2
2
  import { chart, metric } from "./chart.js";
3
- import { listOptions } from "../core/options.js";
3
+ import { listChoices, listOptions } from "../core/options.js";
4
4
  import "@tanstack/react-start/server-only";
5
5
 
6
6
  //#region src/datasource/datasource.ts
@@ -12,7 +12,8 @@ const ds = {
12
12
  delete: deleteRecord,
13
13
  metric,
14
14
  chart,
15
- options: listOptions
15
+ options: listOptions,
16
+ choices: listChoices
16
17
  };
17
18
 
18
19
  //#endregion
@@ -19,17 +19,21 @@ type NumberChain = EmptyChain & {
19
19
  lte(value: number): ConstructableFilter;
20
20
  between(start: number, end: number): ConstructableFilter;
21
21
  };
22
- type ArrayChain<TMember$1 extends string> = EmptyChain & {
23
- is(values: Array<TMember$1>): ConstructableFilter;
24
- isOneOf(values: Array<TMember$1>): ConstructableFilter;
25
- isNoneOf(values: Array<TMember$1>): ConstructableFilter;
26
- hasAllOf(values: Array<TMember$1>): ConstructableFilter;
22
+ type ArrayChain = EmptyChain & {
23
+ is(values: Array<string>): ConstructableFilter;
24
+ isOneOf(values: Array<string>): ConstructableFilter;
25
+ isNoneOf(values: Array<string>): ConstructableFilter;
26
+ hasAllOf(values: Array<string>): ConstructableFilter;
27
27
  };
28
- type ChoiceChain<TId$1 extends string> = EmptyChain & {
29
- is(value: TId$1): ConstructableFilter;
30
- isNot(value: TId$1): ConstructableFilter;
31
- isOneOf(values: Array<TId$1>): ConstructableFilter;
32
- isNoneOf(values: Array<TId$1>): ConstructableFilter;
28
+ /**
29
+ * Compares choice ids as plain strings. A select's choices change in Studio after the build,
30
+ * and an id read at runtime from ds.choices has to be a valid filter value.
31
+ */
32
+ type ChoiceChain = EmptyChain & {
33
+ is(value: string): ConstructableFilter;
34
+ isNot(value: string): ConstructableFilter;
35
+ isOneOf(values: Array<string>): ConstructableFilter;
36
+ isNoneOf(values: Array<string>): ConstructableFilter;
33
37
  };
34
38
  /** What DSv2 filters generically: matched whole or scanned, never against a set. */
35
39
  type GenericChain = EmptyChain & {
@@ -73,32 +77,20 @@ type AnyChain = EmptyChain & {
73
77
  between(start: number | string, end: number | string): ConstructableFilter;
74
78
  isNotBetween(start: number | string, end: number | string): ConstructableFilter;
75
79
  };
76
- type IdOf<TValue> = TValue extends {
77
- id: infer TId extends string;
78
- } ? TId : string;
79
- /**
80
- * What a choice-shaped field compares against: its own ids where codegen read the choice
81
- * list, and any string where it did not. A linked record is the open case, and it is the
82
- * only thing distinguishing one from a select at the type level.
83
- */
84
- type ChoiceIds<TValue> = string extends IdOf<TValue> ? string : IdOf<TValue>;
85
80
  type TextFieldType = 'SINGLE_LINE_TEXT' | 'LONG_TEXT' | 'URL' | 'EMAIL' | 'PHONE' | 'ADDRESS' | 'RECORD_ID' | 'LOCATION' | 'TIME' | 'BUTTON';
86
81
  type NumberFieldType = 'NUMBER' | 'PERCENT' | 'CURRENCY' | 'PROGRESS' | 'RATING' | 'DURATION' | 'COUNT' | 'AUTONUMBER';
87
82
  type DateFieldType = 'DATE' | 'DATETIME' | 'CREATED_AT' | 'UPDATED_AT';
88
83
  type ChoiceFieldType = 'SELECT' | 'LINKED_RECORD' | 'CREATED_BY' | 'UPDATED_BY';
89
84
  /** The types with no single value to compare: only whether the field holds one. */
90
85
  type EmptyFieldType = 'ATTACHMENT' | 'DATE_RANGE';
91
- type ElementOf<TValue> = TValue extends Array<infer TMember> ? TMember : TValue;
92
86
  /**
93
87
  * The operators one alias offers, chosen from the type codegen declared for the field rather
94
88
  * than from what it reads as: a `DATETIME` and a text field both read as `string`, and only
95
89
  * the declared type tells them apart.
96
90
  *
97
- * `TValue` is still needed for the literal choice ids, which live on the read type and
98
- * nowhere else. A field with no declared type lands on the permissive chain rather than on
99
- * none.
91
+ * A field with no declared type lands on the permissive chain rather than on none.
100
92
  */
101
- type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain<ChoiceIds<ElementOf<TValue>>> : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain<ChoiceIds<TValue>> : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
93
+ type ChainFor<TFieldType, TValue> = [TFieldType] extends [never] ? AnyChain : TFieldType extends `${infer TBase}[]` ? TBase extends EmptyFieldType ? EmptyChain : ArrayChain : TFieldType extends TextFieldType ? TextChain<TValue & string> : TFieldType extends NumberFieldType ? NumberChain : TFieldType extends 'USER' ? GenericChain : TFieldType extends 'CHECKBOX' ? BooleanChain : TFieldType extends DateFieldType ? DateChain : TFieldType extends ChoiceFieldType ? ChoiceChain : TFieldType extends EmptyFieldType ? EmptyChain : AnyChain;
102
94
  type Combinators = {
103
95
  $and(...members: Array<AnyConstructable>): ConstructableFilterGroup;
104
96
  $or(...members: Array<AnyConstructable>): ConstructableFilterGroup;
@@ -1,4 +1,4 @@
1
- import * as react6 from "react";
1
+ import * as react4 from "react";
2
2
 
3
3
  //#region src/default-error-overlay.d.ts
4
4
  type Props = {
@@ -8,6 +8,6 @@ type Props = {
8
8
  declare function DefaultErrorOverlay({
9
9
  error,
10
10
  reset
11
- }: Props): react6.JSX.Element;
11
+ }: Props): react4.JSX.Element;
12
12
  //#endregion
13
13
  export { DefaultErrorOverlay };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
2
- import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
2
+ import { ActionPermissions, SoftrDatasources, defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
3
3
  import { FileAttachment } from "./core/file-upload.js";
4
4
  import { FileItem, useFileUpload } from "./core/use-file-upload.js";
5
+ import { CommentAttachmentInput, CommentMention, CommentThread, CommentView } from "./comments/comments.js";
5
6
  import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
6
7
  import { SoftrContext } from "./auth/context.js";
7
8
  import { AuthSettings } from "./auth/platform.js";
@@ -17,4 +18,4 @@ import { useUserTimezone } from "./timezone.js";
17
18
  import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
18
19
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
19
20
  import { Audience } from "./route/audience.js";
20
- export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
21
+ export { type ActionPermissions, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type CommentAttachmentInput, type CommentMention, type CommentThread, type CommentView, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type User, type WeekDay, assertRouteAccess, checkAccess, defineComments, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useFileUpload } from "./core/use-file-upload.js";
2
- import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
2
+ import { defineComments, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
3
3
  import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
4
4
  import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
5
5
  import { definePage, pageHead } from "./route/page.js";
@@ -14,4 +14,4 @@ import { useUserTimezone } from "./timezone.js";
14
14
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
15
15
  import { Audience } from "./route/audience.js";
16
16
 
17
- export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
17
+ export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineComments, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.74";
2
+ var version = "0.0.75-alpha.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/pwa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as react4 from "react";
1
+ import * as react5 from "react";
2
2
  import "@tanstack/react-start";
3
3
 
4
4
  //#region src/pwa.d.ts
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
23
23
  * )
24
24
  * }
25
25
  */
26
- declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
26
+ declare function SoftrPwaHead(): react5.FunctionComponentElement<react5.FragmentProps>;
27
27
  type PwaPlatform = "ios" | "android" | "desktop";
28
28
  type PwaInstall = {
29
29
  available: false;
package/dist/server.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { upload } from "./core/file-upload.js";
2
+ import { comments } from "./comments/comments.js";
2
3
  import { auth } from "./auth/server.js";
3
- import { RecordOption, RecordOptions } from "./core/options.js";
4
+ import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
4
5
  import { ds } from "./datasource/datasource.js";
5
6
  import { workflow } from "./core/workflow.js";
6
7
  import { integration } from "./core/integration.js";
7
8
  import "@tanstack/react-start/server-only";
8
- export { type RecordOption, type RecordOptions, auth, ds, integration, upload, workflow };
9
+ export { type FieldChoice, type RecordOption, type RecordOptions, auth, comments, ds, integration, upload, workflow };
package/dist/server.js CHANGED
@@ -3,6 +3,7 @@ import { ds } from "./datasource/datasource.js";
3
3
  import { auth } from "./auth/server.js";
4
4
  import { workflow } from "./core/workflow.js";
5
5
  import { integration } from "./core/integration.js";
6
+ import { comments } from "./comments/comments.js";
6
7
  import "@tanstack/react-start/server-only";
7
8
 
8
- export { auth, ds, integration, upload, workflow };
9
+ export { auth, comments, ds, integration, upload, workflow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.74",
3
+ "version": "0.0.75-alpha.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -17,6 +17,8 @@
17
17
  "typecheck": "tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
+ "marked": "^18.0.14",
21
+ "xss": "^1.0.15",
20
22
  "zod": "^4.4.3"
21
23
  },
22
24
  "devDependencies": {
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: core
14
14
  library: softr-sdk
15
- library_version: 0.0.74
15
+ library_version: 0.0.75
16
16
  ---
17
17
 
18
18
  # Softr SDK core
@@ -30,9 +30,9 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
30
30
 
31
31
  | Import | Runs | Holds |
32
32
  | --- | --- | --- |
33
- | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
34
- | `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
35
- | `@softr/sdk/client` | Browser only | Nothing yet, reserved for browser-only utilities |
33
+ | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineComments`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `FormattedDate`, `FormattingScope`, `useFormatting`, `formatDate`, `formatDateTime`, `formatNumber`, `formatValue`, `formatField`, `fieldShape`, `registerSoftrFields`, `useOverrides`, `SoftrPwaHead`, `usePwaInstall`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
34
+ | `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration`, `comments` |
35
+ | `@softr/sdk/client` | Browser only | `useFileUpload`, `useCommentComposer` |
36
36
  | `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
37
37
 
38
38
  ## Sub-Skills
@@ -51,6 +51,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
51
51
  | Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
52
52
  | Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
53
53
  | Show a date, a time or a number | [formatting/SKILL.md](./formatting/SKILL.md) |
54
+ | Add comments to a record | [comments/SKILL.md](./comments/SKILL.md) |
54
55
 
55
56
  ## Quick Decision Tree
56
57
 
@@ -91,6 +92,9 @@ Adding an install button, or making the app installable?
91
92
  Showing a date, a time or a number on a page?
92
93
  load softr-sdk/formatting
93
94
 
95
+ Letting visitors comment on a record?
96
+ load softr-sdk/comments
97
+
94
98
  Writing a server function, loader, or middleware in general?
95
99
  not a Softr concern. Use the TanStack Start skills.
96
100
  ```
@@ -18,7 +18,7 @@ description: >-
18
18
  metadata:
19
19
  type: sub-skill
20
20
  library: softr-sdk
21
- library_version: 0.0.74
21
+ library_version: 0.0.75
22
22
  ---
23
23
 
24
24
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.74
15
+ library_version: 0.0.75
16
16
  ---
17
17
 
18
18
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.