@protobase/ui 0.53.3 → 0.53.5
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/app/change-confirm/asks-to-confirm.js +10 -0
- package/dist/app/change-confirm/messages.js +16 -0
- package/dist/app/change-confirm/use-change-confirm.js +51 -0
- package/dist/app/comments/comment-body.js +5 -8
- package/dist/app/comments/comment-composer.js +29 -114
- package/dist/app/comments/messages.js +0 -4
- package/dist/app/field-editor.js +15 -6
- package/dist/app/list-edit/cell-field-editor.js +3 -1
- package/dist/app/list-page.js +1 -0
- package/dist/app-shell/page-header.js +34 -10
- package/dist/chat/chat-day.d.ts +6 -0
- package/dist/chat/chat-day.js +17 -0
- package/dist/chat/chat-divider.d.ts +15 -0
- package/dist/chat/chat-divider.js +25 -0
- package/dist/chat/chat-message.d.ts +26 -4
- package/dist/chat/chat-message.js +65 -5
- package/dist/chat/chat-thread.d.ts +13 -3
- package/dist/chat/chat-thread.js +85 -16
- package/dist/chat/composer.d.ts +12 -4
- package/dist/chat/composer.js +30 -15
- package/dist/chat/index.d.ts +4 -2
- package/dist/chat/messages.js +10 -2
- package/dist/chat/thread-scroll.js +15 -0
- package/dist/data/query-keys.js +0 -7
- package/dist/index.d.ts +10 -3
- package/dist/index.js +6 -1
- package/dist/mentions/index.d.ts +5 -0
- package/dist/mentions/mention-box.d.ts +27 -0
- package/dist/mentions/mention-box.js +125 -0
- package/dist/mentions/mention-text.d.ts +9 -0
- package/dist/{app/comments → mentions}/mention-text.js +1 -1
- package/dist/mentions/mentioned-text.d.ts +9 -0
- package/dist/mentions/mentioned-text.js +13 -0
- package/dist/mentions/messages.js +16 -0
- package/dist/mentions/people-list.js +26 -0
- package/dist/mentions/use-people-search.d.ts +5 -0
- package/dist/mentions/use-people-search.js +26 -0
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-Bro4-Qj6.js +9 -0
- package/dist/shell/assets/{index-CymocR26.css → index-BX1N2n2W.css} +1 -1
- package/dist/shell/assets/{index-DuPd6UFq.js → index-vxLWACI4.js} +2 -2
- package/dist/shell/assets/{records-map-D74AlE1K.js → records-map-HVtj4Vna.js} +2 -2
- package/dist/shell/assets/shared-protobase-ui-BXqStot-.js +2 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-Bro4-Qj6.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-DuPd6UFq.js.map → index-vxLWACI4.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-D74AlE1K.js.map → records-map-HVtj4Vna.js.map} +1 -1
- package/dist/shell-source-maps/assets/{shared-protobase-ui-DyYG0cfq.js.map → shared-protobase-ui-BXqStot-.js.map} +1 -1
- package/package.json +4 -4
- package/dist/app/comments/use-mentionable.js +0 -18
- package/dist/shell/assets/app-BgvLGqeW.js +0 -9
- package/dist/shell/assets/shared-protobase-ui-DyYG0cfq.js +0 -2
- package/dist/shell-source-maps/assets/app-BgvLGqeW.js.map +0 -1
package/dist/chat/chat-thread.js
CHANGED
|
@@ -1,22 +1,91 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { text } from "./messages.js";
|
|
4
|
+
import { isAtEnd, scrollAfterChange } from "./thread-scroll.js";
|
|
5
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
7
|
//#region src/chat/chat-thread.tsx
|
|
4
|
-
/** The
|
|
5
|
-
const
|
|
8
|
+
/** The message the reader is reading: the first whose bottom shows. */
|
|
9
|
+
const readPlace = (box, list, followKey) => {
|
|
10
|
+
const top = box.getBoundingClientRect().top;
|
|
11
|
+
const element = [...list.children].find((child) => child.getBoundingClientRect().bottom > top);
|
|
12
|
+
const anchor = element && { anchor: {
|
|
13
|
+
element,
|
|
14
|
+
top: element.getBoundingClientRect().top - top
|
|
15
|
+
} };
|
|
16
|
+
return {
|
|
17
|
+
atEnd: isAtEnd(box),
|
|
18
|
+
first: list.firstElementChild ?? void 0,
|
|
19
|
+
followKey,
|
|
20
|
+
...anchor
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The scrolling list of a chat's messages. A reader at the end follows new messages; one who scrolled back stays where
|
|
25
|
+
* they are until `followKey` changes, and earlier messages loaded above keep them on the message they were reading. It
|
|
26
|
+
* scrolls inside itself, not with the page, since a chat keeps its composer in view beneath it.
|
|
27
|
+
*/
|
|
28
|
+
const ChatThread = ({ children, empty, length, onLoadEarlier, loadingEarlier, followKey }) => {
|
|
29
|
+
const box = useRef(null);
|
|
6
30
|
const list = useRef(null);
|
|
31
|
+
const place = useRef({
|
|
32
|
+
atEnd: true,
|
|
33
|
+
first: void 0,
|
|
34
|
+
followKey
|
|
35
|
+
});
|
|
36
|
+
const remember = () => {
|
|
37
|
+
if (box.current && list.current) place.current = readPlace(box.current, list.current, followKey);
|
|
38
|
+
};
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const element = box.current;
|
|
41
|
+
if (!element || !list.current) return;
|
|
42
|
+
const before = place.current;
|
|
43
|
+
const anchorTop = before.anchor && before.anchor.element.isConnected ? before.anchor.element.getBoundingClientRect().top - element.getBoundingClientRect().top : void 0;
|
|
44
|
+
const next = scrollAfterChange(before, {
|
|
45
|
+
first: list.current.firstElementChild ?? void 0,
|
|
46
|
+
anchorTop,
|
|
47
|
+
scrollTop: element.scrollTop,
|
|
48
|
+
scrollHeight: element.scrollHeight,
|
|
49
|
+
followKey
|
|
50
|
+
});
|
|
51
|
+
if (next !== void 0) element.scrollTop = next;
|
|
52
|
+
remember();
|
|
53
|
+
}, [length, followKey]);
|
|
7
54
|
useEffect(() => {
|
|
8
|
-
const element =
|
|
9
|
-
if (element
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
55
|
+
const element = box.current;
|
|
56
|
+
if (!element || !list.current) return;
|
|
57
|
+
const observer = new ResizeObserver(() => {
|
|
58
|
+
if (place.current.atEnd) element.scrollTop = element.scrollHeight;
|
|
59
|
+
});
|
|
60
|
+
observer.observe(element);
|
|
61
|
+
observer.observe(list.current);
|
|
62
|
+
return () => observer.disconnect();
|
|
63
|
+
}, []);
|
|
64
|
+
const showsEmpty = length === 0 && Boolean(empty);
|
|
65
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
66
|
+
ref: box,
|
|
67
|
+
onScroll: remember,
|
|
68
|
+
className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 [overflow-anchor:none]",
|
|
69
|
+
children: [
|
|
70
|
+
onLoadEarlier && /* @__PURE__ */ jsx(Button, {
|
|
71
|
+
size: "sm",
|
|
72
|
+
className: "self-center",
|
|
73
|
+
loading: loadingEarlier,
|
|
74
|
+
onClick: onLoadEarlier,
|
|
75
|
+
children: text("loadEarlier")
|
|
76
|
+
}),
|
|
77
|
+
showsEmpty && /* @__PURE__ */ jsx("div", {
|
|
78
|
+
className: "m-auto max-w-60 text-center text-muted-foreground",
|
|
79
|
+
children: empty
|
|
80
|
+
}),
|
|
81
|
+
/* @__PURE__ */ jsx("div", {
|
|
82
|
+
ref: list,
|
|
83
|
+
role: "log",
|
|
84
|
+
"aria-live": "polite",
|
|
85
|
+
className: cn("flex flex-col gap-4", showsEmpty && "hidden"),
|
|
86
|
+
children
|
|
87
|
+
})
|
|
88
|
+
]
|
|
20
89
|
});
|
|
21
90
|
};
|
|
22
91
|
//#endregion
|
package/dist/chat/composer.d.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import { PeopleSearch } from "../mentions/use-people-search.js";
|
|
2
|
+
import "../mentions/index.js";
|
|
1
3
|
import { Ref } from "react";
|
|
2
4
|
//#region src/chat/composer.d.ts
|
|
3
5
|
export type ComposerProps = {
|
|
4
|
-
|
|
6
|
+
/** Gets the trimmed text and the ids of the people it mentions, each once. */
|
|
7
|
+
onSend?: (text: string, mentions: string[]) => void;
|
|
5
8
|
/** The other side is still answering: the text can be written but not sent. */
|
|
6
9
|
busy?: boolean;
|
|
7
10
|
placeholder?: string;
|
|
8
|
-
inputRef?: Ref<
|
|
11
|
+
inputRef?: Ref<HTMLTextAreaElement>;
|
|
12
|
+
/** Finds the people `@` lists by the name typed after it, such as a channel's members; without it, `@` is plain text. */
|
|
13
|
+
searchPeople?: PeopleSearch;
|
|
9
14
|
};
|
|
10
|
-
/**
|
|
11
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The text box under a chat, which grows with its text. Enter or the button sends the trimmed text and clears the box,
|
|
17
|
+
* Shift+Enter starts a new line; while the list of people to mention is open, Enter picks from it instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare const Composer: ({ onSend, busy, placeholder, inputRef, searchPeople }: ComposerProps) => import("react").JSX.Element;
|
|
12
20
|
//#endregion
|
package/dist/chat/composer.js
CHANGED
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
import { Button } from "../primitives/button.js";
|
|
2
|
-
import { Input } from "../primitives/input.js";
|
|
3
2
|
import { text } from "./messages.js";
|
|
3
|
+
import { mentionedIds } from "../mentions/mention-text.js";
|
|
4
|
+
import { MentionBox, emptyDraft } from "../mentions/mention-box.js";
|
|
4
5
|
import { useState } from "react";
|
|
5
6
|
import { SendHorizontal } from "lucide-react";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/chat/composer.tsx
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The text box under a chat, which grows with its text. Enter or the button sends the trimmed text and clears the box,
|
|
11
|
+
* Shift+Enter starts a new line; while the list of people to mention is open, Enter picks from it instead.
|
|
12
|
+
*/
|
|
13
|
+
const Composer = ({ onSend, busy, placeholder, inputRef, searchPeople }) => {
|
|
14
|
+
const [draft, setDraft] = useState(emptyDraft);
|
|
15
|
+
const message = draft.text.trim();
|
|
12
16
|
const send = () => {
|
|
13
17
|
if (!message || busy) return;
|
|
14
|
-
onSend?.(message);
|
|
15
|
-
setDraft(
|
|
18
|
+
onSend?.(message, mentionedIds(message, draft.picked));
|
|
19
|
+
setDraft(emptyDraft);
|
|
20
|
+
};
|
|
21
|
+
const onKeyDown = (event) => {
|
|
22
|
+
if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
send();
|
|
16
25
|
};
|
|
17
26
|
return /* @__PURE__ */ jsxs("form", {
|
|
18
|
-
className: "flex gap-2",
|
|
27
|
+
className: "flex items-end gap-2",
|
|
19
28
|
onSubmit: (event) => {
|
|
20
29
|
event.preventDefault();
|
|
21
30
|
send();
|
|
22
31
|
},
|
|
23
|
-
children: [/* @__PURE__ */ jsx(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
33
|
+
className: "min-w-0 flex-1",
|
|
34
|
+
children: /* @__PURE__ */ jsx(MentionBox, {
|
|
35
|
+
ref: inputRef,
|
|
36
|
+
rows: 1,
|
|
37
|
+
"aria-label": text("message"),
|
|
38
|
+
placeholder,
|
|
39
|
+
value: draft,
|
|
40
|
+
onChange: setDraft,
|
|
41
|
+
searchPeople,
|
|
42
|
+
listAt: "above",
|
|
43
|
+
onKeyDown
|
|
44
|
+
})
|
|
30
45
|
}), /* @__PURE__ */ jsx(Button, {
|
|
31
46
|
type: "submit",
|
|
32
47
|
variant: "primary",
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { chatDayLabel, sameChatDay } from "./chat-day.js";
|
|
2
|
+
import { ChatDivider, ChatDividerProps } from "./chat-divider.js";
|
|
3
|
+
import { ChatAuthor, ChatMessage, ChatMessageProps } from "./chat-message.js";
|
|
2
4
|
import { ChatThread, ChatThreadProps } from "./chat-thread.js";
|
|
3
5
|
import { Composer, ComposerProps } from "./composer.js";
|
|
4
|
-
export { ChatMessage, ChatThread, type ChatThreadProps, Composer, type ComposerProps };
|
|
6
|
+
export { type ChatAuthor, ChatDivider, type ChatDividerProps, ChatMessage, type ChatMessageProps, ChatThread, type ChatThreadProps, Composer, type ComposerProps, chatDayLabel, sameChatDay };
|
package/dist/chat/messages.js
CHANGED
|
@@ -3,11 +3,19 @@ import { defineMessages } from "../locale/messages.js";
|
|
|
3
3
|
const text = defineMessages({
|
|
4
4
|
en: {
|
|
5
5
|
message: "Message",
|
|
6
|
-
send: "Send"
|
|
6
|
+
send: "Send",
|
|
7
|
+
today: "Today",
|
|
8
|
+
yesterday: "Yesterday",
|
|
9
|
+
newMessages: "New messages",
|
|
10
|
+
loadEarlier: "Load earlier messages"
|
|
7
11
|
},
|
|
8
12
|
nl: {
|
|
9
13
|
message: "Bericht",
|
|
10
|
-
send: "Versturen"
|
|
14
|
+
send: "Versturen",
|
|
15
|
+
today: "Vandaag",
|
|
16
|
+
yesterday: "Gisteren",
|
|
17
|
+
newMessages: "Nieuwe berichten",
|
|
18
|
+
loadEarlier: "Eerdere berichten laden"
|
|
11
19
|
}
|
|
12
20
|
});
|
|
13
21
|
//#endregion
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Whether a scroll box shows its end. */
|
|
2
|
+
const isAtEnd = (box) => box.scrollHeight - box.scrollTop - box.clientHeight <= 24;
|
|
3
|
+
/**
|
|
4
|
+
* The scroll position a chat takes once its messages or follow key changed, or `undefined` to stay put. A changed follow
|
|
5
|
+
* key, such as the reader sending a message, goes to the end wherever the reader was. Messages loaded before the first
|
|
6
|
+
* keep the reader on the message they were reading, moved by as much as it moved; otherwise a reader at the end follows
|
|
7
|
+
* the newest message, and one who scrolled back stays where they are.
|
|
8
|
+
*/
|
|
9
|
+
const scrollAfterChange = (before, now) => {
|
|
10
|
+
if (now.followKey !== before.followKey) return now.scrollHeight;
|
|
11
|
+
if (before.first !== void 0 && now.first !== before.first && before.anchor && now.anchorTop !== void 0) return now.scrollTop + now.anchorTop - before.anchor.top;
|
|
12
|
+
if (before.atEnd) return now.scrollHeight;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { isAtEnd, scrollAfterChange };
|
package/dist/data/query-keys.js
CHANGED
|
@@ -74,13 +74,6 @@ const keys = {
|
|
|
74
74
|
"comments"
|
|
75
75
|
],
|
|
76
76
|
/** The people a comment on a record may mention whose name contains `query`. */
|
|
77
|
-
mentionable: (resource, key, query) => [
|
|
78
|
-
"record",
|
|
79
|
-
resource,
|
|
80
|
-
key,
|
|
81
|
-
"mentionable",
|
|
82
|
-
query
|
|
83
|
-
],
|
|
84
77
|
facets: (resource, field, filter) => [
|
|
85
78
|
"facets",
|
|
86
79
|
resource,
|
package/dist/index.d.ts
CHANGED
|
@@ -37,8 +37,16 @@ import { useAssistant } from "./assistant-dock/use-assistant.js";
|
|
|
37
37
|
import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
|
|
38
38
|
import { BackendData, BackendRefresh, useBackendData } from "./assistant-dock/use-backend-data.js";
|
|
39
39
|
import "./assistant-dock/index.js";
|
|
40
|
-
import {
|
|
40
|
+
import { chatDayLabel, sameChatDay } from "./chat/chat-day.js";
|
|
41
|
+
import { ChatDivider, ChatDividerProps } from "./chat/chat-divider.js";
|
|
42
|
+
import { ChatAuthor, ChatMessage, ChatMessageProps } from "./chat/chat-message.js";
|
|
41
43
|
import { ChatThread, ChatThreadProps } from "./chat/chat-thread.js";
|
|
44
|
+
import { Textarea, TextareaProps } from "./primitives/textarea.js";
|
|
45
|
+
import { PickedUser, mentionedIds } from "./mentions/mention-text.js";
|
|
46
|
+
import { PeopleSearch } from "./mentions/use-people-search.js";
|
|
47
|
+
import { MentionBox, MentionBoxProps, MentionDraft, emptyDraft } from "./mentions/mention-box.js";
|
|
48
|
+
import { MentionedText } from "./mentions/mentioned-text.js";
|
|
49
|
+
import "./mentions/index.js";
|
|
42
50
|
import { Composer, ComposerProps } from "./chat/composer.js";
|
|
43
51
|
import "./chat/index.js";
|
|
44
52
|
import { SidePanel, SidePanelProps } from "./side-panel/side-panel.js";
|
|
@@ -61,7 +69,6 @@ import { ResendCodeButton, ResendCodeButtonProps } from "./auth/resend-code-butt
|
|
|
61
69
|
import { allowDelayMs, isFramed, useArmedAfterFocus, useGuardedPress } from "./connect/interaction-guard.js";
|
|
62
70
|
import { Button, ButtonProps, buttonClass } from "./primitives/button.js";
|
|
63
71
|
import { Input, InputProps } from "./primitives/input.js";
|
|
64
|
-
import { Textarea, TextareaProps } from "./primitives/textarea.js";
|
|
65
72
|
import { Segmented, SegmentedOption, SegmentedProps } from "./primitives/segmented.js";
|
|
66
73
|
import { MenuSelect, MenuSelectProps } from "./primitives/menu-select.js";
|
|
67
74
|
import { ChoiceOption, Choices, ChoicesProps } from "./primitives/choices.js";
|
|
@@ -72,4 +79,4 @@ import { Section } from "./record-view/section.js";
|
|
|
72
79
|
import { Form, FormProps } from "./app/create/form.js";
|
|
73
80
|
import { RecordPicker, RecordPickerProps } from "./app/record-picker.js";
|
|
74
81
|
import { Spinner } from "./primitives/spinner.js";
|
|
75
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ActionInputError, type ActionInputProps, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, AuthLayout, type AuthLayoutProps, type BackendData, type BackendRefresh, type BeforeSignInProps, type BoardDrop, type BoardDropContext, type BoardDropUndo, type BoardDropWrite, Button, type ButtonProps, type CardAction, type CardTone, Carousel, type CarouselImage, type CarouselProps, ChatMessage, ChatThread, type ChatThreadProps, type ChoiceOption, Choices, type ChoicesProps, CodeForm, type CodeFormProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, Dialog, type DialogProps, type DiffLine, DiffView, type DiffViewProps, type Drop, type DropRule, Field, FieldList, type FieldProps, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, Form, type FormProps, Input, type InputProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, LogoMark, Markdown, type MarkdownContentProps as MarkdownProps, MenuSelect, type MenuSelectProps, type MetaData, OtpInput, type OtpInputProps, PartView, type PluginInfo, type ProjectUi, type PushState, type RecordCardProps, RecordLink, RecordPicker, type RecordPickerProps, ResendCodeButton, type ResendCodeButtonProps, ResponsiveDialog, type ResponsiveDialogProps, Section, Segmented, type SegmentedOption, type SegmentedProps, type ShellLogoProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, Spinner, type Step, StepList, type StepState, Textarea, type TextareaProps, allowDelayMs, buttonClass, definePlugin, defineUi, diffStats, isConflict, isFramed, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMeta, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useUnreadNotifications, useUpdateRecord };
|
|
82
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ActionInputError, type ActionInputProps, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, AuthLayout, type AuthLayoutProps, type BackendData, type BackendRefresh, type BeforeSignInProps, type BoardDrop, type BoardDropContext, type BoardDropUndo, type BoardDropWrite, Button, type ButtonProps, type CardAction, type CardTone, Carousel, type CarouselImage, type CarouselProps, type ChatAuthor, ChatDivider, type ChatDividerProps, ChatMessage, type ChatMessageProps, ChatThread, type ChatThreadProps, type ChoiceOption, Choices, type ChoicesProps, CodeForm, type CodeFormProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, Dialog, type DialogProps, type DiffLine, DiffView, type DiffViewProps, type Drop, type DropRule, Field, FieldList, type FieldProps, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, Form, type FormProps, Input, type InputProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, LogoMark, Markdown, type MarkdownContentProps as MarkdownProps, MentionBox, type MentionBoxProps, type MentionDraft, MentionedText, MenuSelect, type MenuSelectProps, type MetaData, OtpInput, type OtpInputProps, PartView, type PeopleSearch, type PickedUser, type PluginInfo, type ProjectUi, type PushState, type RecordCardProps, RecordLink, RecordPicker, type RecordPickerProps, ResendCodeButton, type ResendCodeButtonProps, ResponsiveDialog, type ResponsiveDialogProps, Section, Segmented, type SegmentedOption, type SegmentedProps, type ShellLogoProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, Spinner, type Step, StepList, type StepState, Textarea, type TextareaProps, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMeta, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useUnreadNotifications, useUpdateRecord };
|
package/dist/index.js
CHANGED
|
@@ -32,8 +32,13 @@ import { usePlugin } from "./plugins/plugin-context.js";
|
|
|
32
32
|
import { pluginApi, protobaseVersion } from "./plugins/plugin-api.js";
|
|
33
33
|
import { useApiToken } from "./auth/use-api-token.js";
|
|
34
34
|
import { useRecord } from "./data/use-record.js";
|
|
35
|
+
import { chatDayLabel, sameChatDay } from "./chat/chat-day.js";
|
|
36
|
+
import { ChatDivider } from "./chat/chat-divider.js";
|
|
35
37
|
import { ChatMessage } from "./chat/chat-message.js";
|
|
36
38
|
import { ChatThread } from "./chat/chat-thread.js";
|
|
39
|
+
import { mentionedIds } from "./mentions/mention-text.js";
|
|
40
|
+
import { MentionBox, emptyDraft } from "./mentions/mention-box.js";
|
|
41
|
+
import { MentionedText } from "./mentions/mentioned-text.js";
|
|
37
42
|
import { Composer } from "./chat/composer.js";
|
|
38
43
|
import { SidePanel } from "./side-panel/side-panel.js";
|
|
39
44
|
import { ActionCard } from "./action-card/action-card.js";
|
|
@@ -56,4 +61,4 @@ import { RecordLink } from "./app/displays/record-link.js";
|
|
|
56
61
|
import { Carousel } from "./carousel/carousel.js";
|
|
57
62
|
import { Choices } from "./primitives/choices.js";
|
|
58
63
|
import { RecordPicker } from "./app/record-picker.js";
|
|
59
|
-
export { ActionCard, ActionInputError, ApiProvider, App, AppLink, AssistantDock, AuthLayout, Button, Carousel, ChatMessage, ChatThread, Choices, CodeForm, CompactTable, Composer, Dialog, DiffView, Field, FieldList, Form, Input, LogoMark, Markdown, MenuSelect, OtpInput, PartView, RecordLink, RecordPicker, ResendCodeButton, ResponsiveDialog, Section, Segmented, SidePanel, SignInPrompt, Spinner, StepList, Textarea, allowDelayMs, buttonClass, definePlugin, defineUi, diffStats, isConflict, isFramed, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMeta, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useUnreadNotifications, useUpdateRecord };
|
|
64
|
+
export { ActionCard, ActionInputError, ApiProvider, App, AppLink, AssistantDock, AuthLayout, Button, Carousel, ChatDivider, ChatMessage, ChatThread, Choices, CodeForm, CompactTable, Composer, Dialog, DiffView, Field, FieldList, Form, Input, LogoMark, Markdown, MentionBox, MentionedText, MenuSelect, OtpInput, PartView, RecordLink, RecordPicker, ResendCodeButton, ResponsiveDialog, Section, Segmented, SidePanel, SignInPrompt, Spinner, StepList, Textarea, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMeta, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useUnreadNotifications, useUpdateRecord };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { PickedUser, mentionedIds } from "./mention-text.js";
|
|
2
|
+
import { PeopleSearch } from "./use-people-search.js";
|
|
3
|
+
import { MentionBox, MentionBoxProps, MentionDraft, emptyDraft } from "./mention-box.js";
|
|
4
|
+
import { MentionedText } from "./mentioned-text.js";
|
|
5
|
+
export { MentionBox, type MentionBoxProps, type MentionDraft, MentionedText, type PeopleSearch, type PickedUser, emptyDraft, mentionedIds };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TextareaProps } from "../primitives/textarea.js";
|
|
2
|
+
import { PickedUser } from "./mention-text.js";
|
|
3
|
+
import { PeopleSearch } from "./use-people-search.js";
|
|
4
|
+
import { KeyboardEvent } from "react";
|
|
5
|
+
//#region src/mentions/mention-box.d.ts
|
|
6
|
+
/** What is written in a mention box: the text, and the people picked from its list, whom `@{name}` in it mentions. */
|
|
7
|
+
export type MentionDraft = {
|
|
8
|
+
text: string;
|
|
9
|
+
picked: readonly PickedUser[];
|
|
10
|
+
};
|
|
11
|
+
export declare const emptyDraft: MentionDraft;
|
|
12
|
+
export type MentionBoxProps = Omit<TextareaProps, 'value' | 'onChange' | 'onKeyDown' | 'role'> & {
|
|
13
|
+
value: MentionDraft;
|
|
14
|
+
onChange: (draft: MentionDraft) => void;
|
|
15
|
+
/** Finds the people `@` lists by the name typed after it; without it, `@` is plain text. */
|
|
16
|
+
searchPeople?: PeopleSearch;
|
|
17
|
+
/** Where the list opens: under the box, or above it for a box at the bottom of a chat. */
|
|
18
|
+
listAt?: 'below' | 'above';
|
|
19
|
+
/** The keys the open list does not take, such as Enter to send. */
|
|
20
|
+
onKeyDown?: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A text box of several lines in which typing `@` lists the people `searchPeople` finds by the name typed after it:
|
|
24
|
+
* the arrows move, Enter or Tab picks, Escape closes. A picked name is written out as `@{name} `.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MentionBox: ({ value, onChange, searchPeople, listAt, onKeyDown, onFocus, onBlur, ref: given, ...rest }: MentionBoxProps) => import("react").JSX.Element;
|
|
27
|
+
//#endregion
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Textarea } from "../primitives/textarea.js";
|
|
2
|
+
import { menuKeyTarget } from "../primitives/menu-keys.js";
|
|
3
|
+
import { activeMention, insertMention } from "./mention-text.js";
|
|
4
|
+
import { PeopleList } from "./people-list.js";
|
|
5
|
+
import { usePeopleSearch } from "./use-people-search.js";
|
|
6
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/mentions/mention-box.tsx
|
|
9
|
+
const emptyDraft = {
|
|
10
|
+
text: "",
|
|
11
|
+
picked: []
|
|
12
|
+
};
|
|
13
|
+
/** The most people the list offers; few enough that it never needs a scrollbar of its own. */
|
|
14
|
+
const shownPeople = 8;
|
|
15
|
+
/**
|
|
16
|
+
* A text box of several lines in which typing `@` lists the people `searchPeople` finds by the name typed after it:
|
|
17
|
+
* the arrows move, Enter or Tab picks, Escape closes. A picked name is written out as `@{name} `.
|
|
18
|
+
*/
|
|
19
|
+
const MentionBox = ({ value, onChange, searchPeople, listAt = "below", onKeyDown, onFocus, onBlur, ref: given, ...rest }) => {
|
|
20
|
+
const [caret, setCaret] = useState(0);
|
|
21
|
+
const [focused, setFocused] = useState(false);
|
|
22
|
+
const [active, setActive] = useState(0);
|
|
23
|
+
const [closed, setClosed] = useState();
|
|
24
|
+
const box = useRef(null);
|
|
25
|
+
const setBox = useCallback((element) => {
|
|
26
|
+
box.current = element;
|
|
27
|
+
if (typeof given === "function") given(element);
|
|
28
|
+
else if (given) given.current = element;
|
|
29
|
+
}, [given]);
|
|
30
|
+
const nextCaret = useRef(void 0);
|
|
31
|
+
const listId = useId();
|
|
32
|
+
const mention = searchPeople ? activeMention(value.text, caret) : void 0;
|
|
33
|
+
const open = focused && mention !== void 0 && mention.start !== closed;
|
|
34
|
+
const found = usePeopleSearch(searchPeople, open ? mention.query : void 0);
|
|
35
|
+
const options = open && found && "people" in found ? found.people.slice(0, shownPeople) : [];
|
|
36
|
+
const listed = open && found !== void 0;
|
|
37
|
+
const optionId = (index) => `${listId}-${index}`;
|
|
38
|
+
useEffect(() => setActive(0), [mention?.query]);
|
|
39
|
+
const mentionStart = mention?.start;
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (mentionStart === void 0) setClosed(void 0);
|
|
42
|
+
}, [mentionStart]);
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
if (nextCaret.current === void 0 || !box.current) return;
|
|
45
|
+
box.current.setSelectionRange(nextCaret.current, nextCaret.current);
|
|
46
|
+
setCaret(nextCaret.current);
|
|
47
|
+
nextCaret.current = void 0;
|
|
48
|
+
}, [value.text]);
|
|
49
|
+
const pick = (user) => {
|
|
50
|
+
if (!mention) return;
|
|
51
|
+
const next = insertMention(value.text, mention, user.name);
|
|
52
|
+
nextCaret.current = next.caret;
|
|
53
|
+
onChange({
|
|
54
|
+
text: next.text,
|
|
55
|
+
picked: [...value.picked, user]
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const keyDown = (event) => {
|
|
59
|
+
if (open) {
|
|
60
|
+
const target = menuKeyTarget(event.key, active, options.length);
|
|
61
|
+
if (target !== void 0 && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
setActive(target);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if ((event.key === "Enter" || event.key === "Tab") && options[active]) {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
pick(options[active]);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (event.key === "Escape") {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
event.stopPropagation();
|
|
74
|
+
setClosed(mention.start);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
onKeyDown?.(event);
|
|
79
|
+
};
|
|
80
|
+
const combobox = searchPeople && {
|
|
81
|
+
role: "combobox",
|
|
82
|
+
"aria-multiline": true,
|
|
83
|
+
"aria-autocomplete": "list",
|
|
84
|
+
"aria-expanded": listed,
|
|
85
|
+
"aria-controls": listId,
|
|
86
|
+
"aria-activedescendant": listed && options[active] ? optionId(active) : void 0
|
|
87
|
+
};
|
|
88
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
89
|
+
className: "relative",
|
|
90
|
+
children: [/* @__PURE__ */ jsx(Textarea, {
|
|
91
|
+
...rest,
|
|
92
|
+
...combobox,
|
|
93
|
+
ref: setBox,
|
|
94
|
+
value: value.text,
|
|
95
|
+
onChange: (event) => {
|
|
96
|
+
onChange({
|
|
97
|
+
...value,
|
|
98
|
+
text: event.target.value
|
|
99
|
+
});
|
|
100
|
+
setCaret(event.target.selectionStart);
|
|
101
|
+
},
|
|
102
|
+
onSelect: (event) => setCaret(event.currentTarget.selectionStart),
|
|
103
|
+
onFocus: (event) => {
|
|
104
|
+
setFocused(true);
|
|
105
|
+
onFocus?.(event);
|
|
106
|
+
},
|
|
107
|
+
onBlur: (event) => {
|
|
108
|
+
setFocused(false);
|
|
109
|
+
onBlur?.(event);
|
|
110
|
+
},
|
|
111
|
+
onKeyDown: keyDown
|
|
112
|
+
}), listed && /* @__PURE__ */ jsx(PeopleList, {
|
|
113
|
+
id: listId,
|
|
114
|
+
optionId,
|
|
115
|
+
people: options,
|
|
116
|
+
active,
|
|
117
|
+
onActive: setActive,
|
|
118
|
+
onPick: pick,
|
|
119
|
+
failed: "failed" in found,
|
|
120
|
+
at: listAt
|
|
121
|
+
})]
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
export { MentionBox, emptyDraft };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/mentions/mention-text.d.ts
|
|
2
|
+
/** A user picked from the list while writing, whom `@{name}` in the text mentions. */
|
|
3
|
+
export type PickedUser = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
/** The ids of the picked users whose `@{name}` is still in the text, each once. */
|
|
8
|
+
export declare const mentionedIds: (text: string, picked: readonly PickedUser[]) => string[];
|
|
9
|
+
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region src/
|
|
1
|
+
//#region src/mentions/mention-text.ts
|
|
2
2
|
/** The mention the caret is in: an `@` that starts the text or follows whitespace, with no whitespace up to the caret. */
|
|
3
3
|
const activeMention = (text, caret) => {
|
|
4
4
|
const match = /(?:^|\s)@([^\s@]*)$/.exec(text.slice(0, caret));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/mentions/mentioned-text.d.ts
|
|
2
|
+
/** Text as written, line breaks kept, each `@{name}` of someone it mentions picked out; a name not in `mentions` stays plain. */
|
|
3
|
+
export declare const MentionedText: ({ text, mentions }: {
|
|
4
|
+
text: string;
|
|
5
|
+
mentions: readonly {
|
|
6
|
+
name?: string;
|
|
7
|
+
}[];
|
|
8
|
+
}) => import("react").JSX.Element;
|
|
9
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { bodyParts } from "./mention-text.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/mentions/mentioned-text.tsx
|
|
4
|
+
/** Text as written, line breaks kept, each `@{name}` of someone it mentions picked out; a name not in `mentions` stays plain. */
|
|
5
|
+
const MentionedText = ({ text, mentions }) => /* @__PURE__ */ jsx("p", {
|
|
6
|
+
className: "whitespace-pre-wrap break-words",
|
|
7
|
+
children: bodyParts(text, mentions).map((part, index) => "mention" in part ? /* @__PURE__ */ jsxs("span", {
|
|
8
|
+
className: "font-medium text-primary",
|
|
9
|
+
children: ["@", part.mention]
|
|
10
|
+
}, index) : part.text)
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { MentionedText };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineMessages } from "../locale/messages.js";
|
|
2
|
+
//#region src/mentions/messages.ts
|
|
3
|
+
const text = defineMessages({
|
|
4
|
+
en: {
|
|
5
|
+
peopleToMention: "People to mention",
|
|
6
|
+
noOneFound: "No one found",
|
|
7
|
+
couldNotSearch: "Could not search for people"
|
|
8
|
+
},
|
|
9
|
+
nl: {
|
|
10
|
+
peopleToMention: "Mensen om te noemen",
|
|
11
|
+
noOneFound: "Niemand gevonden",
|
|
12
|
+
couldNotSearch: "Kon niet naar mensen zoeken"
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export { text };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/mentions/people-list.tsx
|
|
5
|
+
/** The people a mention box offers for the `@` being typed; a press keeps the focus in the box. */
|
|
6
|
+
const PeopleList = ({ id, optionId, people, active, onActive, onPick, failed, at }) => /* @__PURE__ */ jsxs("ul", {
|
|
7
|
+
id,
|
|
8
|
+
role: "listbox",
|
|
9
|
+
"aria-label": text("peopleToMention"),
|
|
10
|
+
className: cn("absolute z-30 w-full max-w-sm rounded-md border bg-background py-1 shadow-pop", at === "above" ? "bottom-full mb-1" : "mt-1"),
|
|
11
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
12
|
+
children: [people.map((user, index) => /* @__PURE__ */ jsx("li", {
|
|
13
|
+
id: optionId(index),
|
|
14
|
+
role: "option",
|
|
15
|
+
"aria-selected": index === active,
|
|
16
|
+
className: cn("cursor-pointer truncate px-3 py-1.5", index === active && "bg-muted"),
|
|
17
|
+
onMouseMove: () => index !== active && onActive(index),
|
|
18
|
+
onClick: () => onPick(user),
|
|
19
|
+
children: user.name
|
|
20
|
+
}, user.id)), people.length === 0 && /* @__PURE__ */ jsx("li", {
|
|
21
|
+
className: "px-3 py-1.5 text-muted-foreground",
|
|
22
|
+
children: failed ? text("couldNotSearch") : text("noOneFound")
|
|
23
|
+
})]
|
|
24
|
+
});
|
|
25
|
+
//#endregion
|
|
26
|
+
export { PeopleList };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { PickedUser } from "./mention-text.js";
|
|
2
|
+
//#region src/mentions/use-people-search.d.ts
|
|
3
|
+
/** Finds the people whose name matches the text typed after `@`, such as the members of a channel. */
|
|
4
|
+
export type PeopleSearch = (query: string) => Promise<readonly PickedUser[]>;
|
|
5
|
+
//#endregion
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useDebounced } from "../lib/use-debounced.js";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
//#region src/mentions/use-people-search.ts
|
|
4
|
+
/**
|
|
5
|
+
* The people `search` finds for `query`, asked 150 ms after it last changed; `undefined` until the first answer and
|
|
6
|
+
* while `query` is `undefined`. The last answer stays up while the next one is on its way, so the list does not flicker
|
|
7
|
+
* as a name is typed, and an answer to a query since changed is dropped.
|
|
8
|
+
*/
|
|
9
|
+
const usePeopleSearch = (search, query) => {
|
|
10
|
+
const settled = useDebounced(query, 150);
|
|
11
|
+
const [found, setFound] = useState();
|
|
12
|
+
const latest = useRef(search);
|
|
13
|
+
latest.current = search;
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const ask = latest.current;
|
|
16
|
+
if (settled === void 0 || !ask) return;
|
|
17
|
+
let current = true;
|
|
18
|
+
ask(settled).then((people) => current && setFound({ people }), () => current && setFound({ failed: true }));
|
|
19
|
+
return () => {
|
|
20
|
+
current = false;
|
|
21
|
+
};
|
|
22
|
+
}, [settled]);
|
|
23
|
+
return query === void 0 ? void 0 : found;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { usePeopleSearch };
|
package/dist/package.js
CHANGED