@dxos/react-ui-thread 0.8.4-main.ead640a → 0.8.4-main.effb148878
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/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +121 -198
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +25 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/types/src/Message/Message.d.ts +2 -2
- package/dist/types/src/Message/Message.d.ts.map +1 -1
- package/dist/types/src/Message/Message.stories.d.ts +11 -11
- package/dist/types/src/Message/Message.stories.d.ts.map +1 -1
- package/dist/types/src/Thread/Comments.stories.d.ts +13 -13
- package/dist/types/src/Thread/Comments.stories.d.ts.map +1 -1
- package/dist/types/src/Thread/Thread.d.ts +25 -11
- package/dist/types/src/Thread/Thread.d.ts.map +1 -1
- package/dist/types/src/Thread/Thread.stories.d.ts +16 -12
- package/dist/types/src/Thread/Thread.stories.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +12 -12
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +1 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -28
- package/src/Message/Message.stories.tsx +6 -6
- package/src/Message/Message.tsx +17 -20
- package/src/Thread/Comments.stories.tsx +68 -43
- package/src/Thread/Thread.stories.tsx +10 -10
- package/src/Thread/Thread.tsx +23 -21
- package/src/index.ts +0 -3
- package/src/testing.tsx +10 -12
- package/src/translations.ts +11 -11
- package/src/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,56 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-thread",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.effb148878",
|
|
4
4
|
"description": "A set of components to use with threads, including comments and messages.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
|
-
"sideEffects":
|
|
13
|
+
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
15
|
+
"imports": {
|
|
16
|
+
"#translations": "./src/translations.ts"
|
|
17
|
+
},
|
|
11
18
|
"exports": {
|
|
12
19
|
".": {
|
|
13
20
|
"source": "./src/index.ts",
|
|
14
21
|
"types": "./dist/types/src/index.d.ts",
|
|
15
22
|
"browser": "./dist/lib/browser/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./translations": {
|
|
25
|
+
"source": "./src/translations.ts",
|
|
26
|
+
"types": "./dist/types/src/translations.d.ts",
|
|
27
|
+
"browser": "./dist/lib/browser/translations.mjs",
|
|
28
|
+
"node": "./dist/lib/node-esm/translations.mjs"
|
|
16
29
|
}
|
|
17
30
|
},
|
|
18
31
|
"types": "dist/types/src/index.d.ts",
|
|
19
|
-
"typesVersions": {
|
|
20
|
-
"*": {}
|
|
21
|
-
},
|
|
22
32
|
"files": [
|
|
23
33
|
"dist",
|
|
24
34
|
"src"
|
|
25
35
|
],
|
|
26
36
|
"dependencies": {
|
|
27
|
-
"@preact-signals/safe-react": "^0.9.0",
|
|
28
37
|
"@radix-ui/react-primitive": "2.0.2",
|
|
29
|
-
"date-fns": "^3.
|
|
30
|
-
"@dxos/react-ui-editor": "0.8.4-main.
|
|
31
|
-
"@dxos/util": "0.8.4-main.
|
|
38
|
+
"date-fns": "^3.6.0",
|
|
39
|
+
"@dxos/react-ui-editor": "0.8.4-main.effb148878",
|
|
40
|
+
"@dxos/util": "0.8.4-main.effb148878",
|
|
41
|
+
"@dxos/ui-editor": "0.8.4-main.effb148878"
|
|
32
42
|
},
|
|
33
43
|
"devDependencies": {
|
|
34
|
-
"@types/react": "~19.2.
|
|
35
|
-
"@types/react-dom": "~19.2.
|
|
36
|
-
"react": "~19.2.
|
|
37
|
-
"react-dom": "~19.2.
|
|
38
|
-
"vite": "
|
|
39
|
-
"@dxos/echo": "0.8.4-main.
|
|
40
|
-
"@dxos/keys": "0.8.4-main.
|
|
41
|
-
"@dxos/echo-db": "0.8.4-main.
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/storybook-utils": "0.8.4-main.ead640a"
|
|
44
|
+
"@types/react": "~19.2.7",
|
|
45
|
+
"@types/react-dom": "~19.2.3",
|
|
46
|
+
"react": "~19.2.3",
|
|
47
|
+
"react-dom": "~19.2.3",
|
|
48
|
+
"vite": "^8.0.14",
|
|
49
|
+
"@dxos/echo": "0.8.4-main.effb148878",
|
|
50
|
+
"@dxos/keys": "0.8.4-main.effb148878",
|
|
51
|
+
"@dxos/echo-db": "0.8.4-main.effb148878",
|
|
52
|
+
"@dxos/random": "0.8.4-main.effb148878",
|
|
53
|
+
"@dxos/react-ui": "0.8.4-main.effb148878",
|
|
54
|
+
"@dxos/log": "0.8.4-main.effb148878",
|
|
55
|
+
"@dxos/storybook-utils": "0.8.4-main.effb148878",
|
|
56
|
+
"@dxos/ui-theme": "0.8.4-main.effb148878"
|
|
48
57
|
},
|
|
49
58
|
"peerDependencies": {
|
|
50
|
-
"react": "
|
|
51
|
-
"react-dom": "
|
|
52
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
53
|
-
"@dxos/
|
|
59
|
+
"react": "~19.2.3",
|
|
60
|
+
"react-dom": "~19.2.3",
|
|
61
|
+
"@dxos/react-ui": "0.8.4-main.effb148878",
|
|
62
|
+
"@dxos/ui-theme": "0.8.4-main.effb148878"
|
|
54
63
|
},
|
|
55
64
|
"publishConfig": {
|
|
56
65
|
"access": "public"
|
|
@@ -6,13 +6,13 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
6
6
|
import React, { useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { PublicKey } from '@dxos/keys';
|
|
9
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
|
-
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/ui-theme';
|
|
11
|
+
|
|
12
|
+
import { translations } from '#translations';
|
|
11
13
|
|
|
12
14
|
import { type MessageEntity, MessageStoryText } from '../testing';
|
|
13
15
|
import { Thread } from '../Thread';
|
|
14
|
-
import { translations } from '../translations';
|
|
15
|
-
|
|
16
16
|
import { MessageRoot } from './Message';
|
|
17
17
|
|
|
18
18
|
const DefaultStory = () => {
|
|
@@ -25,7 +25,7 @@ const DefaultStory = () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
|
-
<div className='
|
|
28
|
+
<div className='mx-auto w-96 overflow-y-auto'>
|
|
29
29
|
<Thread.Root id='t1'>
|
|
30
30
|
<MessageRoot {...message} classNames={[hoverableControls, hoverableFocusedWithinControls]}>
|
|
31
31
|
<MessageStoryText {...message} onDelete={() => console.log('delete')} />
|
|
@@ -39,7 +39,7 @@ const meta = {
|
|
|
39
39
|
title: 'ui/react-ui-thread/Message',
|
|
40
40
|
component: MessageRoot as any,
|
|
41
41
|
render: DefaultStory,
|
|
42
|
-
decorators: [withTheme],
|
|
42
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
43
43
|
parameters: {
|
|
44
44
|
layout: 'fullscreen',
|
|
45
45
|
translations,
|
package/src/Message/Message.tsx
CHANGED
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
|
|
6
6
|
import React, { type ComponentPropsWithRef, type ComponentPropsWithoutRef, forwardRef } from 'react';
|
|
7
7
|
|
|
8
|
-
// TODO(burdon): Remove dep.
|
|
9
8
|
import { Avatar, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
10
|
-
import { type UseTextEditorProps,
|
|
11
|
-
import {
|
|
9
|
+
import { type UseTextEditorProps, useTextEditor } from '@dxos/react-ui-editor';
|
|
10
|
+
import { keymap, listener } from '@dxos/ui-editor';
|
|
11
|
+
import { mx } from '@dxos/ui-theme';
|
|
12
12
|
import { hexToEmoji, hexToHue, isTruthy } from '@dxos/util';
|
|
13
13
|
|
|
14
|
-
import { translationKey } from '
|
|
14
|
+
import { translationKey } from '#translations';
|
|
15
|
+
|
|
15
16
|
import { type MessageMetadata } from '../types';
|
|
16
17
|
|
|
17
18
|
const avatarSize = 7;
|
|
18
19
|
|
|
19
|
-
export type MessageRootProps = ThemedClassName<
|
|
20
|
-
MessageMetadata &
|
|
21
|
-
|
|
20
|
+
export type MessageRootProps = ThemedClassName<
|
|
21
|
+
ComponentPropsWithRef<'div'> & MessageMetadata & Partial<{ continues: boolean }>
|
|
22
|
+
>;
|
|
22
23
|
|
|
23
24
|
// TODO(burdon): Show authorName on tooltip.
|
|
24
25
|
export const MessageRoot = forwardRef<HTMLDivElement, MessageRootProps>(
|
|
@@ -26,28 +27,25 @@ export const MessageRoot = forwardRef<HTMLDivElement, MessageRootProps>(
|
|
|
26
27
|
{ authorImgSrc, authorId, authorName, authorAvatarProps, continues = true, children, classNames, ...rootProps },
|
|
27
28
|
forwardedRef,
|
|
28
29
|
) => {
|
|
30
|
+
// Must wrap the message since Avatar.Label may be used in the content.
|
|
29
31
|
return (
|
|
30
|
-
// Must wrap the message since Avatar.Label may be used in the content.
|
|
31
32
|
<Avatar.Root>
|
|
32
33
|
<div
|
|
33
|
-
role='none'
|
|
34
34
|
data-testid='thread.message'
|
|
35
35
|
{...rootProps}
|
|
36
36
|
className={mx('grid grid-cols-subgrid col-span-2', classNames)}
|
|
37
37
|
ref={forwardedRef}
|
|
38
38
|
>
|
|
39
|
-
<div
|
|
39
|
+
<div className='flex flex-col items-center gap-2 pt-1'>
|
|
40
40
|
<Avatar.Content
|
|
41
41
|
size={avatarSize}
|
|
42
42
|
hue={authorAvatarProps?.hue || hexToHue(authorId ?? '0')}
|
|
43
43
|
fallback={authorAvatarProps?.emoji || hexToEmoji(authorId ?? '0')}
|
|
44
44
|
{...(authorImgSrc && { imgSrc: authorImgSrc })}
|
|
45
45
|
/>
|
|
46
|
-
{continues && <div
|
|
47
|
-
</div>
|
|
48
|
-
<div role='none' className='plb-1 min-is-0'>
|
|
49
|
-
{children}
|
|
46
|
+
{continues && <div className='w-px grow bg-separator' />}
|
|
50
47
|
</div>
|
|
48
|
+
<div className='py-1 min-w-0'>{children}</div>
|
|
51
49
|
</div>
|
|
52
50
|
</Avatar.Root>
|
|
53
51
|
);
|
|
@@ -59,7 +57,7 @@ export type MessageHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'div'
|
|
|
59
57
|
|
|
60
58
|
export const MessageHeading = ({ children, classNames, timestamp, authorName, ...props }: MessageHeadingProps) => {
|
|
61
59
|
return (
|
|
62
|
-
<div
|
|
60
|
+
<div {...props} className={mx('flex gap-2 items-start', classNames)}>
|
|
63
61
|
<p className='grow'>
|
|
64
62
|
<MessageAuthorName authorName={authorName} />
|
|
65
63
|
{timestamp && <MessageTime timestamp={timestamp} />}
|
|
@@ -74,7 +72,7 @@ export type MessageAuthorNameProps = Pick<MessageMetadata, 'authorName'>;
|
|
|
74
72
|
export const MessageAuthorName = ({ authorName }: MessageAuthorNameProps) => {
|
|
75
73
|
const { t } = useTranslation(translationKey);
|
|
76
74
|
return (
|
|
77
|
-
<Avatar.Label classNames='block truncate text-sm text-subdued'>{authorName ?? t('anonymous
|
|
75
|
+
<Avatar.Label classNames='block truncate text-sm text-subdued'>{authorName ?? t('anonymous.label')}</Avatar.Label>
|
|
78
76
|
);
|
|
79
77
|
};
|
|
80
78
|
|
|
@@ -84,7 +82,7 @@ export const MessageTime = ({ timestamp }: MessageTimeProps) => {
|
|
|
84
82
|
const { dtLocale } = useTranslation(translationKey);
|
|
85
83
|
const dt = timestamp ? new Date(timestamp) : undefined;
|
|
86
84
|
return (
|
|
87
|
-
<time className='block text-subdued text-xs
|
|
85
|
+
<time className='block text-subdued text-xs pb-0.5' dateTime={dt?.toISOString()}>
|
|
88
86
|
{dt ? formatDistanceToNow(dt, { locale: dtLocale, addSuffix: true }) : ''}
|
|
89
87
|
</time>
|
|
90
88
|
);
|
|
@@ -149,7 +147,7 @@ export const MessageTextbox = ({
|
|
|
149
147
|
extensions: [
|
|
150
148
|
keymap.of(keyBindings({ onSend, onClear })),
|
|
151
149
|
listener({
|
|
152
|
-
onFocus: (focusing) => {
|
|
150
|
+
onFocus: ({ focusing }) => {
|
|
153
151
|
if (focusing) {
|
|
154
152
|
onEditorFocus?.();
|
|
155
153
|
}
|
|
@@ -165,9 +163,8 @@ export const MessageTextbox = ({
|
|
|
165
163
|
return (
|
|
166
164
|
<MessageRoot {...{ id, authorId, authorName, authorImgSrc, authorAvatarProps }} continues={false}>
|
|
167
165
|
<div
|
|
168
|
-
role='none'
|
|
169
166
|
ref={parentRef}
|
|
170
|
-
className={mx('
|
|
167
|
+
className={mx('py-0.5 me-1 rounded-xs dx-focus-ring', disabled && 'opacity-50')}
|
|
171
168
|
{...focusAttributes}
|
|
172
169
|
/>
|
|
173
170
|
</MessageRoot>
|
|
@@ -5,46 +5,63 @@
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
6
|
import React, { type FC, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { createDocAccessor, createObject } from '@dxos/echo-db';
|
|
8
|
+
import { Obj } from '@dxos/echo';
|
|
9
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
11
10
|
import { PublicKey } from '@dxos/keys';
|
|
12
11
|
import { log } from '@dxos/log';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { random } from '@dxos/random';
|
|
13
|
+
import { Icon, IconButton, useThemeContext } from '@dxos/react-ui';
|
|
14
|
+
import { useTextEditor } from '@dxos/react-ui-editor';
|
|
15
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
16
16
|
import {
|
|
17
|
-
type Comment,
|
|
18
17
|
type CommentsOptions,
|
|
19
|
-
type
|
|
20
|
-
automerge,
|
|
18
|
+
type EditorView,
|
|
21
19
|
comments,
|
|
22
20
|
createBasicExtensions,
|
|
23
21
|
createThemeExtensions,
|
|
22
|
+
documentId,
|
|
24
23
|
listener,
|
|
25
24
|
scrollThreadIntoView,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} from '@dxos/
|
|
29
|
-
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/
|
|
25
|
+
setComments,
|
|
26
|
+
} from '@dxos/ui-editor';
|
|
27
|
+
import { type Comment, type Range } from '@dxos/ui-editor/types';
|
|
28
|
+
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/ui-theme';
|
|
29
|
+
|
|
30
|
+
import { translations } from '#translations';
|
|
30
31
|
|
|
31
32
|
import { MessageBody, MessageHeading, MessageRoot, MessageTextbox } from '../Message';
|
|
32
33
|
import { type MessageEntity } from '../testing';
|
|
33
|
-
import { translations } from '../translations';
|
|
34
|
-
|
|
35
34
|
import { Thread } from './Thread';
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
random.seed(101);
|
|
38
37
|
|
|
39
38
|
const authorId = PublicKey.random().toHex();
|
|
40
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @deprecated This hook will be removed in future versions. Use the new comment sync extension instead.
|
|
42
|
+
* Update comments state field.
|
|
43
|
+
*/
|
|
44
|
+
const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (view) {
|
|
47
|
+
// Check same document.
|
|
48
|
+
// NOTE: Hook might be called before editor state is updated.
|
|
49
|
+
if (id === view.state.facet(documentId)) {
|
|
50
|
+
view.dispatch({
|
|
51
|
+
effects: setComments.of({ id, comments: comments ?? [] }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
41
58
|
//
|
|
42
59
|
// Editor
|
|
43
60
|
//
|
|
44
61
|
|
|
45
62
|
const Editor: FC<{
|
|
46
63
|
id?: string;
|
|
47
|
-
|
|
64
|
+
initialValue: string;
|
|
48
65
|
comments: Comment[];
|
|
49
66
|
selected?: string;
|
|
50
67
|
onCreateComment: CommentsOptions['onCreate'];
|
|
@@ -53,7 +70,7 @@ const Editor: FC<{
|
|
|
53
70
|
onSelectComment: CommentsOptions['onSelect'];
|
|
54
71
|
}> = ({
|
|
55
72
|
id = 'test',
|
|
56
|
-
|
|
73
|
+
initialValue,
|
|
57
74
|
selected: selectedValue,
|
|
58
75
|
comments: commentRanges,
|
|
59
76
|
onCreateComment,
|
|
@@ -67,11 +84,10 @@ const Editor: FC<{
|
|
|
67
84
|
const { parentRef, view } = useTextEditor(
|
|
68
85
|
() => ({
|
|
69
86
|
id,
|
|
70
|
-
initialValue:
|
|
87
|
+
initialValue: initialValue,
|
|
71
88
|
extensions: [
|
|
72
89
|
createBasicExtensions(),
|
|
73
90
|
createThemeExtensions({ themeMode }),
|
|
74
|
-
automerge(createDocAccessor(item, ['content'])),
|
|
75
91
|
comments({
|
|
76
92
|
id,
|
|
77
93
|
onCreate: onCreateComment,
|
|
@@ -81,7 +97,7 @@ const Editor: FC<{
|
|
|
81
97
|
}),
|
|
82
98
|
],
|
|
83
99
|
}),
|
|
84
|
-
[id,
|
|
100
|
+
[id, initialValue, themeMode],
|
|
85
101
|
);
|
|
86
102
|
useComments(view, id, commentRanges);
|
|
87
103
|
useEffect(() => {
|
|
@@ -114,19 +130,21 @@ const StoryThread: FC<{
|
|
|
114
130
|
onSelect: () => void;
|
|
115
131
|
onResolve: () => void;
|
|
116
132
|
}> = ({ thread, selected, onSelect, onResolve }) => {
|
|
117
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
118
133
|
const { themeMode } = useThemeContext();
|
|
134
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
135
|
+
|
|
119
136
|
// TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
|
|
120
|
-
const [
|
|
121
|
-
const rerenderEditor = () => _setCount((count) => count + 1);
|
|
137
|
+
const [state, setState] = useState({});
|
|
122
138
|
const messageRef = useRef('');
|
|
123
139
|
const extensions = useMemo(
|
|
124
140
|
() => [
|
|
125
141
|
createBasicExtensions({ placeholder: 'Enter comment' }),
|
|
126
142
|
createThemeExtensions({ themeMode }),
|
|
127
|
-
listener({
|
|
143
|
+
listener({
|
|
144
|
+
onChange: ({ text }) => (messageRef.current = text),
|
|
145
|
+
}),
|
|
128
146
|
],
|
|
129
|
-
[themeMode,
|
|
147
|
+
[themeMode, state],
|
|
130
148
|
);
|
|
131
149
|
|
|
132
150
|
const [autoFocus, setAutoFocus] = useState(false);
|
|
@@ -142,7 +160,7 @@ const StoryThread: FC<{
|
|
|
142
160
|
|
|
143
161
|
const handleCreateMessage = () => {
|
|
144
162
|
if (messageRef.current?.length) {
|
|
145
|
-
const message =
|
|
163
|
+
const message = Obj.make(TestSchema.Expando, {
|
|
146
164
|
timestamp: new Date().toISOString(),
|
|
147
165
|
sender: { identityKey: authorId },
|
|
148
166
|
text: messageRef.current,
|
|
@@ -151,7 +169,7 @@ const StoryThread: FC<{
|
|
|
151
169
|
|
|
152
170
|
messageRef.current = '';
|
|
153
171
|
setAutoFocus(true);
|
|
154
|
-
|
|
172
|
+
setState({});
|
|
155
173
|
}
|
|
156
174
|
};
|
|
157
175
|
|
|
@@ -185,20 +203,27 @@ const StoryThread: FC<{
|
|
|
185
203
|
onSend={handleCreateMessage}
|
|
186
204
|
/>
|
|
187
205
|
<Thread.Status />
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
206
|
+
<IconButton
|
|
207
|
+
variant='ghost'
|
|
208
|
+
icon='ph--check--regular'
|
|
209
|
+
iconOnly
|
|
210
|
+
classNames='px-1'
|
|
211
|
+
label='Resolve'
|
|
212
|
+
onClick={onResolve}
|
|
213
|
+
/>
|
|
191
214
|
</div>
|
|
192
215
|
</Thread.Root>
|
|
193
216
|
);
|
|
194
217
|
};
|
|
195
218
|
|
|
196
|
-
|
|
219
|
+
type SidebarProps = {
|
|
197
220
|
threads: StoryCommentThread[];
|
|
198
221
|
selected?: string;
|
|
199
222
|
onSelect: (thread: string) => void;
|
|
200
223
|
onResolve: (thread: string) => void;
|
|
201
|
-
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const Sidebar = ({ threads, selected, onSelect, onResolve }: SidebarProps) => {
|
|
202
227
|
// Sort by y-position.
|
|
203
228
|
const sortedThreads = useMemo(() => {
|
|
204
229
|
const sorted = [...threads];
|
|
@@ -226,13 +251,13 @@ const Sidebar: FC<{
|
|
|
226
251
|
// Story container.
|
|
227
252
|
//
|
|
228
253
|
|
|
229
|
-
type
|
|
254
|
+
type DefaultStoryProps = {
|
|
230
255
|
text?: string;
|
|
231
256
|
autoCreate?: boolean;
|
|
232
257
|
};
|
|
233
258
|
|
|
234
|
-
const DefaultStory = ({ text, autoCreate }:
|
|
235
|
-
const [item] = useState(
|
|
259
|
+
const DefaultStory = ({ text, autoCreate }: DefaultStoryProps) => {
|
|
260
|
+
const [item] = useState(() => Obj.make(TestSchema.Expando, { content: text ?? '' }));
|
|
236
261
|
const [threads, setThreads] = useState<StoryCommentThread[]>([]);
|
|
237
262
|
const [selected, setSelected] = useState<string>();
|
|
238
263
|
|
|
@@ -251,12 +276,12 @@ const DefaultStory = ({ text, autoCreate }: StoryProps) => {
|
|
|
251
276
|
cursor,
|
|
252
277
|
yPos: location ? Math.floor(location.top) : undefined,
|
|
253
278
|
messages: autoCreate
|
|
254
|
-
?
|
|
279
|
+
? random.helpers.multiple(
|
|
255
280
|
() => ({
|
|
256
281
|
id: PublicKey.random().toHex(),
|
|
257
282
|
timestamp: new Date().toISOString(),
|
|
258
283
|
authorId: PublicKey.random().toHex(),
|
|
259
|
-
text:
|
|
284
|
+
text: random.lorem.sentence(),
|
|
260
285
|
}),
|
|
261
286
|
{ count: { min: 1, max: 3 } },
|
|
262
287
|
)
|
|
@@ -324,9 +349,9 @@ const DefaultStory = ({ text, autoCreate }: StoryProps) => {
|
|
|
324
349
|
|
|
325
350
|
return (
|
|
326
351
|
<main className='fixed inset-0 grid grid-cols-[1fr_24rem]'>
|
|
327
|
-
<div
|
|
352
|
+
<div className='max-h-full overflow-y-auto p-4'>
|
|
328
353
|
<Editor
|
|
329
|
-
|
|
354
|
+
initialValue={item.content}
|
|
330
355
|
selected={selected}
|
|
331
356
|
comments={comments}
|
|
332
357
|
onCreateComment={handleCreateComment}
|
|
@@ -336,7 +361,7 @@ const DefaultStory = ({ text, autoCreate }: StoryProps) => {
|
|
|
336
361
|
/>
|
|
337
362
|
</div>
|
|
338
363
|
|
|
339
|
-
<div
|
|
364
|
+
<div className='max-h-full overflow-y-auto p-4'>
|
|
340
365
|
<Sidebar
|
|
341
366
|
threads={visibleThreads}
|
|
342
367
|
selected={selected}
|
|
@@ -352,7 +377,7 @@ const meta = {
|
|
|
352
377
|
title: 'ui/react-ui-thread/Comments',
|
|
353
378
|
component: StoryThread as any,
|
|
354
379
|
render: DefaultStory,
|
|
355
|
-
decorators: [withTheme],
|
|
380
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
356
381
|
parameters: {
|
|
357
382
|
translations,
|
|
358
383
|
layout: 'fullscreen',
|
|
@@ -370,7 +395,7 @@ export const Default: Story = {
|
|
|
370
395
|
text: str(
|
|
371
396
|
'# Comments',
|
|
372
397
|
'',
|
|
373
|
-
str(...
|
|
398
|
+
str(...random.helpers.multiple(() => [random.lorem.paragraph({ min: 5, max: 10 }), ''], { count: 20 }).flat()),
|
|
374
399
|
'',
|
|
375
400
|
),
|
|
376
401
|
},
|
|
@@ -6,18 +6,18 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
6
6
|
import React, { useMemo, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { PublicKey } from '@dxos/keys';
|
|
9
|
-
import {
|
|
10
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
|
-
import { createBasicExtensions, createThemeExtensions } from '@dxos/
|
|
12
|
-
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/
|
|
9
|
+
import { random } from '@dxos/random';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
import { createBasicExtensions, createThemeExtensions } from '@dxos/ui-editor';
|
|
12
|
+
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/ui-theme';
|
|
13
|
+
|
|
14
|
+
import { translations } from '#translations';
|
|
13
15
|
|
|
14
16
|
import { MessageRoot, MessageTextbox } from '../Message';
|
|
15
17
|
import { type MessageEntity, MessageStoryText } from '../testing';
|
|
16
|
-
import { translations } from '../translations';
|
|
17
|
-
|
|
18
18
|
import { Thread } from './Thread';
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
random.seed(1);
|
|
21
21
|
|
|
22
22
|
const DefaultStory = () => {
|
|
23
23
|
const [pending, setPending] = useState(false);
|
|
@@ -28,7 +28,7 @@ const DefaultStory = () => {
|
|
|
28
28
|
id: `m${i + 1}`,
|
|
29
29
|
timestamp: new Date().toISOString(),
|
|
30
30
|
authorId: [identityKey1.toHex(), identityKey2.toHex()][i % 2],
|
|
31
|
-
text:
|
|
31
|
+
text: random.lorem.paragraph(),
|
|
32
32
|
})),
|
|
33
33
|
);
|
|
34
34
|
|
|
@@ -59,7 +59,7 @@ const DefaultStory = () => {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
|
-
<div className='
|
|
62
|
+
<div className='mx-auto w-96 overflow-y-auto'>
|
|
63
63
|
<Thread.Root id='t1'>
|
|
64
64
|
{messages.map((message) => (
|
|
65
65
|
<MessageRoot key={message.id} classNames={[hoverableControls, hoverableFocusedWithinControls]} {...message}>
|
|
@@ -85,7 +85,7 @@ const meta = {
|
|
|
85
85
|
title: 'ui/react-ui-thread/Thread',
|
|
86
86
|
component: Thread.Root,
|
|
87
87
|
render: DefaultStory,
|
|
88
|
-
decorators: [withTheme],
|
|
88
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
89
89
|
parameters: {
|
|
90
90
|
layout: 'fullscreen',
|
|
91
91
|
translations,
|
package/src/Thread/Thread.tsx
CHANGED
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { ComponentPropsWithRef, forwardRef, PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
7
|
import { Icon, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
8
|
-
import { hoverableControlItem, hoverableFocusedWithinControls, mx } from '@dxos/
|
|
8
|
+
import { hoverableControlItem, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';
|
|
9
|
+
|
|
10
|
+
import { translationKey } from '#translations';
|
|
9
11
|
|
|
10
|
-
import { translationKey } from '../translations';
|
|
11
12
|
import type { ThreadEntity } from '../types';
|
|
12
13
|
|
|
13
|
-
// TODO(burdon):
|
|
14
|
-
export const threadLayout = '
|
|
14
|
+
// TODO(burdon): Avoid exporting fragments.
|
|
15
|
+
export const threadLayout = 'w-full place-self-start grid grid-cols-[var(--dx-rail-size)_1fr]';
|
|
15
16
|
|
|
16
17
|
//
|
|
17
18
|
// Root
|
|
18
19
|
//
|
|
19
20
|
|
|
20
|
-
type ThreadRootProps = ThemedClassName<
|
|
21
|
+
type ThreadRootProps = ThemedClassName<
|
|
22
|
+
PropsWithChildren<
|
|
23
|
+
ThreadEntity & { current?: boolean } & Pick<ComponentPropsWithRef<'div'>, 'onClickCapture' | 'onFocusCapture'>
|
|
24
|
+
>
|
|
25
|
+
>;
|
|
21
26
|
|
|
22
27
|
const ThreadRoot = forwardRef<HTMLDivElement, ThreadRootProps>(
|
|
23
28
|
({ current, children, classNames, ...props }, forwardedRef) => {
|
|
@@ -30,7 +35,7 @@ const ThreadRoot = forwardRef<HTMLDivElement, ThreadRootProps>(
|
|
|
30
35
|
className={mx(
|
|
31
36
|
threadLayout,
|
|
32
37
|
hoverableFocusedWithinControls,
|
|
33
|
-
'bg-[var(--surface-bg)] current-related attention-surface [--controls-opacity:0]',
|
|
38
|
+
'bg-[var(--surface-bg)] current-related dx-attention-surface [--controls-opacity:0]',
|
|
34
39
|
classNames,
|
|
35
40
|
)}
|
|
36
41
|
ref={forwardedRef}
|
|
@@ -45,24 +50,21 @@ const ThreadRoot = forwardRef<HTMLDivElement, ThreadRootProps>(
|
|
|
45
50
|
// Heading
|
|
46
51
|
//
|
|
47
52
|
|
|
48
|
-
type ThreadHeaderProps =
|
|
53
|
+
type ThreadHeaderProps = PropsWithChildren<{ detached?: boolean }>;
|
|
49
54
|
|
|
50
55
|
const ThreadHeader = forwardRef<HTMLParagraphElement, ThreadHeaderProps>(
|
|
51
|
-
({
|
|
56
|
+
({ children, detached, ...props }, forwardedRef) => {
|
|
52
57
|
return (
|
|
53
58
|
<>
|
|
54
|
-
<div
|
|
59
|
+
<div className='flex items-center justify-center text-description'>
|
|
55
60
|
<Icon icon='ph--caret-double-right--regular' />
|
|
56
61
|
</div>
|
|
57
|
-
<div
|
|
62
|
+
<div className='flex items-center overflow-hidden'>
|
|
58
63
|
<p
|
|
59
64
|
role='heading'
|
|
60
65
|
data-testid='thread.heading'
|
|
61
66
|
{...props}
|
|
62
|
-
className={mx(
|
|
63
|
-
'mie-2 text-description font-medium truncate italic',
|
|
64
|
-
detached && 'line-through decoration-1',
|
|
65
|
-
)}
|
|
67
|
+
className={mx('me-2 text-description font-medium truncate italic', detached && 'line-through decoration-1')}
|
|
66
68
|
ref={forwardedRef}
|
|
67
69
|
>
|
|
68
70
|
{children}
|
|
@@ -77,7 +79,7 @@ const ThreadHeader = forwardRef<HTMLParagraphElement, ThreadHeaderProps>(
|
|
|
77
79
|
// Status
|
|
78
80
|
//
|
|
79
81
|
|
|
80
|
-
type ThreadStatusProps = ThemedClassName<
|
|
82
|
+
type ThreadStatusProps = ThemedClassName<PropsWithChildren> & {
|
|
81
83
|
activity?: boolean;
|
|
82
84
|
};
|
|
83
85
|
|
|
@@ -88,20 +90,20 @@ const ThreadStatus = forwardRef<HTMLDivElement, ThreadStatusProps>(
|
|
|
88
90
|
<div
|
|
89
91
|
{...props}
|
|
90
92
|
className={mx(
|
|
91
|
-
'col-start-2 grid grid-cols-[min-content_1fr_max-content] pb-2
|
|
93
|
+
'col-start-2 grid grid-cols-[min-content_1fr_max-content] pb-2 pe-2 text-xs text-description',
|
|
92
94
|
classNames,
|
|
93
95
|
)}
|
|
94
96
|
ref={forwardedRef}
|
|
95
97
|
>
|
|
96
98
|
<Icon
|
|
97
99
|
icon='ph--spinner--bold'
|
|
98
|
-
classNames='
|
|
100
|
+
classNames='w-6 h-4 invisible data-[visible=show]:visible animate-spin-slow'
|
|
99
101
|
data-visible={activity ? 'show' : 'hide'}
|
|
100
102
|
/>
|
|
101
|
-
<span className='truncate min-
|
|
103
|
+
<span className='truncate min-w-0' aria-live='polite'>
|
|
102
104
|
{activity ? children : null}
|
|
103
105
|
</span>
|
|
104
|
-
<span className={mx('text-end', hoverableControlItem)}>{t('enter
|
|
106
|
+
<span className={mx('text-end', hoverableControlItem)}>{t('enter-to-send.message')}</span>
|
|
105
107
|
</div>
|
|
106
108
|
);
|
|
107
109
|
},
|
|
@@ -113,4 +115,4 @@ export const Thread = {
|
|
|
113
115
|
Status: ThreadStatus,
|
|
114
116
|
};
|
|
115
117
|
|
|
116
|
-
export type { ThreadRootProps, ThreadHeaderProps
|
|
118
|
+
export type { ThreadRootProps, ThreadHeaderProps, ThreadStatusProps };
|