@dxos/react-ui-thread 0.8.4-main.1da679c → 0.8.4-main.21d9917
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/lib/browser/index.mjs +130 -176
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/Message/Message.d.ts.map +1 -1
- package/dist/types/src/Message/Message.stories.d.ts +2 -2
- package/dist/types/src/Message/Message.stories.d.ts.map +1 -1
- package/dist/types/src/Thread/Comments.stories.d.ts +1 -2
- package/dist/types/src/Thread/Comments.stories.d.ts.map +1 -1
- package/dist/types/src/Thread/Thread.d.ts +1 -1
- package/dist/types/src/Thread/Thread.d.ts.map +1 -1
- package/dist/types/src/Thread/Thread.stories.d.ts +2 -2
- package/dist/types/src/Thread/Thread.stories.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +26 -23
- package/src/Message/Message.stories.tsx +7 -6
- package/src/Message/Message.tsx +6 -6
- package/src/Thread/Comments.stories.tsx +55 -30
- package/src/Thread/Thread.stories.tsx +8 -7
- package/src/Thread/Thread.tsx +3 -3
- package/src/testing.tsx +7 -6
- package/src/translations.ts +1 -1
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-thread",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.21d9917",
|
|
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
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
|
-
"sideEffects":
|
|
13
|
+
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
11
15
|
"exports": {
|
|
12
16
|
".": {
|
|
@@ -24,33 +28,32 @@
|
|
|
24
28
|
"src"
|
|
25
29
|
],
|
|
26
30
|
"dependencies": {
|
|
27
|
-
"@preact-signals/safe-react": "^0.9.0",
|
|
28
31
|
"@radix-ui/react-primitive": "2.0.2",
|
|
29
32
|
"date-fns": "^3.3.1",
|
|
30
|
-
"@dxos/react-ui-editor": "0.8.4-main.
|
|
31
|
-
"@dxos/
|
|
33
|
+
"@dxos/react-ui-editor": "0.8.4-main.21d9917",
|
|
34
|
+
"@dxos/ui-editor": "0.8.4-main.21d9917",
|
|
35
|
+
"@dxos/util": "0.8.4-main.21d9917"
|
|
32
36
|
},
|
|
33
37
|
"devDependencies": {
|
|
34
|
-
"@types/react": "~
|
|
35
|
-
"@types/react-dom": "~
|
|
36
|
-
"react": "~
|
|
37
|
-
"react-dom": "~
|
|
38
|
-
"vite": "7.1.
|
|
39
|
-
"@dxos/echo-db": "0.8.4-main.
|
|
40
|
-
"@dxos/echo
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/random": "0.8.4-main.
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/storybook-utils": "0.8.4-main.
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/log": "0.8.4-main.1da679c"
|
|
38
|
+
"@types/react": "~19.2.7",
|
|
39
|
+
"@types/react-dom": "~19.2.3",
|
|
40
|
+
"react": "~19.2.3",
|
|
41
|
+
"react-dom": "~19.2.3",
|
|
42
|
+
"vite": "7.1.9",
|
|
43
|
+
"@dxos/echo-db": "0.8.4-main.21d9917",
|
|
44
|
+
"@dxos/echo": "0.8.4-main.21d9917",
|
|
45
|
+
"@dxos/log": "0.8.4-main.21d9917",
|
|
46
|
+
"@dxos/react-ui": "0.8.4-main.21d9917",
|
|
47
|
+
"@dxos/random": "0.8.4-main.21d9917",
|
|
48
|
+
"@dxos/ui-theme": "0.8.4-main.21d9917",
|
|
49
|
+
"@dxos/storybook-utils": "0.8.4-main.21d9917",
|
|
50
|
+
"@dxos/keys": "0.8.4-main.21d9917"
|
|
48
51
|
},
|
|
49
52
|
"peerDependencies": {
|
|
50
|
-
"react": "~
|
|
51
|
-
"react-dom": "~
|
|
52
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
53
|
-
"@dxos/
|
|
53
|
+
"react": "~19.2.3",
|
|
54
|
+
"react-dom": "~19.2.3",
|
|
55
|
+
"@dxos/react-ui": "0.8.4-main.21d9917",
|
|
56
|
+
"@dxos/ui-theme": "0.8.4-main.21d9917"
|
|
54
57
|
},
|
|
55
58
|
"publishConfig": {
|
|
56
59
|
"access": "public"
|
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React, { useState } from 'react';
|
|
9
7
|
|
|
10
8
|
import { PublicKey } from '@dxos/keys';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/ui-theme';
|
|
13
11
|
|
|
14
12
|
import { type MessageEntity, MessageStoryText } from '../testing';
|
|
15
13
|
import { Thread } from '../Thread';
|
|
@@ -41,8 +39,11 @@ const meta = {
|
|
|
41
39
|
title: 'ui/react-ui-thread/Message',
|
|
42
40
|
component: MessageRoot as any,
|
|
43
41
|
render: DefaultStory,
|
|
44
|
-
decorators: [withTheme
|
|
45
|
-
parameters: {
|
|
42
|
+
decorators: [withTheme],
|
|
43
|
+
parameters: {
|
|
44
|
+
layout: 'fullscreen',
|
|
45
|
+
translations,
|
|
46
|
+
},
|
|
46
47
|
} satisfies Meta<typeof DefaultStory>;
|
|
47
48
|
|
|
48
49
|
export default meta;
|
package/src/Message/Message.tsx
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
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 {
|
|
12
|
-
import {
|
|
9
|
+
import { type UseTextEditorProps, useTextEditor } from '@dxos/react-ui-editor';
|
|
10
|
+
import { keymap, listener } from '@dxos/ui-editor';
|
|
11
|
+
import { focusRing, mx } from '@dxos/ui-theme';
|
|
12
|
+
import { hexToEmoji, hexToHue, isTruthy } from '@dxos/util';
|
|
13
13
|
|
|
14
14
|
import { translationKey } from '../translations';
|
|
15
15
|
import { type MessageMetadata } from '../types';
|
|
@@ -149,14 +149,14 @@ export const MessageTextbox = ({
|
|
|
149
149
|
extensions: [
|
|
150
150
|
keymap.of(keyBindings({ onSend, onClear })),
|
|
151
151
|
listener({
|
|
152
|
-
onFocus: (focusing) => {
|
|
152
|
+
onFocus: ({ focusing }) => {
|
|
153
153
|
if (focusing) {
|
|
154
154
|
onEditorFocus?.();
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
157
|
}),
|
|
158
158
|
extensions,
|
|
159
|
-
].filter(
|
|
159
|
+
].filter(isTruthy),
|
|
160
160
|
...editorProps,
|
|
161
161
|
}),
|
|
162
162
|
[id, extensions],
|
|
@@ -2,33 +2,31 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React, { type FC, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
7
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { Obj } from '@dxos/echo';
|
|
9
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
12
10
|
import { PublicKey } from '@dxos/keys';
|
|
13
|
-
import { live } from '@dxos/live-object';
|
|
14
11
|
import { log } from '@dxos/log';
|
|
15
12
|
import { faker } from '@dxos/random';
|
|
16
|
-
import {
|
|
13
|
+
import { Icon, IconButton, useThemeContext } from '@dxos/react-ui';
|
|
14
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
15
|
+
import { useTextEditor } from '@dxos/react-ui-editor';
|
|
17
16
|
import {
|
|
18
17
|
type Comment,
|
|
19
18
|
type CommentsOptions,
|
|
19
|
+
type EditorView,
|
|
20
20
|
type Range,
|
|
21
|
-
automerge,
|
|
22
21
|
comments,
|
|
23
22
|
createBasicExtensions,
|
|
24
23
|
createThemeExtensions,
|
|
24
|
+
documentId,
|
|
25
25
|
listener,
|
|
26
26
|
scrollThreadIntoView,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from '@dxos/
|
|
30
|
-
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/react-ui-theme';
|
|
31
|
-
import { withTheme } from '@dxos/storybook-utils';
|
|
27
|
+
setComments,
|
|
28
|
+
} from '@dxos/ui-editor';
|
|
29
|
+
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/ui-theme';
|
|
32
30
|
|
|
33
31
|
import { MessageBody, MessageHeading, MessageRoot, MessageTextbox } from '../Message';
|
|
34
32
|
import { type MessageEntity } from '../testing';
|
|
@@ -40,13 +38,31 @@ faker.seed(101);
|
|
|
40
38
|
|
|
41
39
|
const authorId = PublicKey.random().toHex();
|
|
42
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated This hook will be removed in future versions. Use the new comment sync extension instead.
|
|
43
|
+
* Update comments state field.
|
|
44
|
+
*/
|
|
45
|
+
const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (view) {
|
|
48
|
+
// Check same document.
|
|
49
|
+
// NOTE: Hook might be called before editor state is updated.
|
|
50
|
+
if (id === view.state.facet(documentId)) {
|
|
51
|
+
view.dispatch({
|
|
52
|
+
effects: setComments.of({ id, comments: comments ?? [] }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
43
59
|
//
|
|
44
60
|
// Editor
|
|
45
61
|
//
|
|
46
62
|
|
|
47
63
|
const Editor: FC<{
|
|
48
64
|
id?: string;
|
|
49
|
-
|
|
65
|
+
initialValue: string;
|
|
50
66
|
comments: Comment[];
|
|
51
67
|
selected?: string;
|
|
52
68
|
onCreateComment: CommentsOptions['onCreate'];
|
|
@@ -55,7 +71,7 @@ const Editor: FC<{
|
|
|
55
71
|
onSelectComment: CommentsOptions['onSelect'];
|
|
56
72
|
}> = ({
|
|
57
73
|
id = 'test',
|
|
58
|
-
|
|
74
|
+
initialValue,
|
|
59
75
|
selected: selectedValue,
|
|
60
76
|
comments: commentRanges,
|
|
61
77
|
onCreateComment,
|
|
@@ -69,11 +85,10 @@ const Editor: FC<{
|
|
|
69
85
|
const { parentRef, view } = useTextEditor(
|
|
70
86
|
() => ({
|
|
71
87
|
id,
|
|
72
|
-
initialValue:
|
|
88
|
+
initialValue: initialValue,
|
|
73
89
|
extensions: [
|
|
74
90
|
createBasicExtensions(),
|
|
75
91
|
createThemeExtensions({ themeMode }),
|
|
76
|
-
automerge(createDocAccessor(item, ['content'])),
|
|
77
92
|
comments({
|
|
78
93
|
id,
|
|
79
94
|
onCreate: onCreateComment,
|
|
@@ -83,7 +98,7 @@ const Editor: FC<{
|
|
|
83
98
|
}),
|
|
84
99
|
],
|
|
85
100
|
}),
|
|
86
|
-
[id,
|
|
101
|
+
[id, initialValue, themeMode],
|
|
87
102
|
);
|
|
88
103
|
useComments(view, id, commentRanges);
|
|
89
104
|
useEffect(() => {
|
|
@@ -116,19 +131,21 @@ const StoryThread: FC<{
|
|
|
116
131
|
onSelect: () => void;
|
|
117
132
|
onResolve: () => void;
|
|
118
133
|
}> = ({ thread, selected, onSelect, onResolve }) => {
|
|
119
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
120
134
|
const { themeMode } = useThemeContext();
|
|
135
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
136
|
+
|
|
121
137
|
// TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
|
|
122
|
-
const [
|
|
123
|
-
const rerenderEditor = () => _setCount((count) => count + 1);
|
|
138
|
+
const [state, setState] = useState({});
|
|
124
139
|
const messageRef = useRef('');
|
|
125
140
|
const extensions = useMemo(
|
|
126
141
|
() => [
|
|
127
142
|
createBasicExtensions({ placeholder: 'Enter comment' }),
|
|
128
143
|
createThemeExtensions({ themeMode }),
|
|
129
|
-
listener({
|
|
144
|
+
listener({
|
|
145
|
+
onChange: ({ text }) => (messageRef.current = text),
|
|
146
|
+
}),
|
|
130
147
|
],
|
|
131
|
-
[themeMode,
|
|
148
|
+
[themeMode, state],
|
|
132
149
|
);
|
|
133
150
|
|
|
134
151
|
const [autoFocus, setAutoFocus] = useState(false);
|
|
@@ -144,7 +161,7 @@ const StoryThread: FC<{
|
|
|
144
161
|
|
|
145
162
|
const handleCreateMessage = () => {
|
|
146
163
|
if (messageRef.current?.length) {
|
|
147
|
-
const message =
|
|
164
|
+
const message = Obj.make(TestSchema.Expando, {
|
|
148
165
|
timestamp: new Date().toISOString(),
|
|
149
166
|
sender: { identityKey: authorId },
|
|
150
167
|
text: messageRef.current,
|
|
@@ -153,7 +170,7 @@ const StoryThread: FC<{
|
|
|
153
170
|
|
|
154
171
|
messageRef.current = '';
|
|
155
172
|
setAutoFocus(true);
|
|
156
|
-
|
|
173
|
+
setState({});
|
|
157
174
|
}
|
|
158
175
|
};
|
|
159
176
|
|
|
@@ -187,9 +204,14 @@ const StoryThread: FC<{
|
|
|
187
204
|
onSend={handleCreateMessage}
|
|
188
205
|
/>
|
|
189
206
|
<Thread.Status />
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
<IconButton
|
|
208
|
+
variant='ghost'
|
|
209
|
+
icon='ph--check--regular'
|
|
210
|
+
iconOnly
|
|
211
|
+
classNames='pli-1'
|
|
212
|
+
label='Resolve'
|
|
213
|
+
onClick={onResolve}
|
|
214
|
+
/>
|
|
193
215
|
</div>
|
|
194
216
|
</Thread.Root>
|
|
195
217
|
);
|
|
@@ -234,7 +256,7 @@ type StoryProps = {
|
|
|
234
256
|
};
|
|
235
257
|
|
|
236
258
|
const DefaultStory = ({ text, autoCreate }: StoryProps) => {
|
|
237
|
-
const [item] = useState(
|
|
259
|
+
const [item] = useState(() => Obj.make(TestSchema.Expando, { content: text ?? '' }));
|
|
238
260
|
const [threads, setThreads] = useState<StoryCommentThread[]>([]);
|
|
239
261
|
const [selected, setSelected] = useState<string>();
|
|
240
262
|
|
|
@@ -328,7 +350,7 @@ const DefaultStory = ({ text, autoCreate }: StoryProps) => {
|
|
|
328
350
|
<main className='fixed inset-0 grid grid-cols-[1fr_24rem]'>
|
|
329
351
|
<div role='none' className='max-bs-full overflow-y-auto p-4'>
|
|
330
352
|
<Editor
|
|
331
|
-
|
|
353
|
+
initialValue={item.content}
|
|
332
354
|
selected={selected}
|
|
333
355
|
comments={comments}
|
|
334
356
|
onCreateComment={handleCreateComment}
|
|
@@ -355,7 +377,10 @@ const meta = {
|
|
|
355
377
|
component: StoryThread as any,
|
|
356
378
|
render: DefaultStory,
|
|
357
379
|
decorators: [withTheme],
|
|
358
|
-
parameters: {
|
|
380
|
+
parameters: {
|
|
381
|
+
translations,
|
|
382
|
+
layout: 'fullscreen',
|
|
383
|
+
},
|
|
359
384
|
} satisfies Meta<typeof DefaultStory>;
|
|
360
385
|
|
|
361
386
|
export default meta;
|
|
@@ -2,16 +2,14 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React, { useMemo, useRef, useState } from 'react';
|
|
9
7
|
|
|
10
8
|
import { PublicKey } from '@dxos/keys';
|
|
11
9
|
import { faker } from '@dxos/random';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
10
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
import { createBasicExtensions, createThemeExtensions } from '@dxos/ui-editor';
|
|
12
|
+
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/ui-theme';
|
|
15
13
|
|
|
16
14
|
import { MessageRoot, MessageTextbox } from '../Message';
|
|
17
15
|
import { type MessageEntity, MessageStoryText } from '../testing';
|
|
@@ -87,8 +85,11 @@ const meta = {
|
|
|
87
85
|
title: 'ui/react-ui-thread/Thread',
|
|
88
86
|
component: Thread.Root,
|
|
89
87
|
render: DefaultStory,
|
|
90
|
-
decorators: [withTheme
|
|
91
|
-
parameters: {
|
|
88
|
+
decorators: [withTheme],
|
|
89
|
+
parameters: {
|
|
90
|
+
layout: 'fullscreen',
|
|
91
|
+
translations,
|
|
92
|
+
},
|
|
92
93
|
} satisfies Meta<typeof Thread.Root>;
|
|
93
94
|
|
|
94
95
|
export default meta;
|
package/src/Thread/Thread.tsx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import React, { type ComponentPropsWithRef, forwardRef } 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
9
|
|
|
10
10
|
import { translationKey } from '../translations';
|
|
11
11
|
import type { ThreadEntity } from '../types';
|
|
@@ -45,10 +45,10 @@ const ThreadRoot = forwardRef<HTMLDivElement, ThreadRootProps>(
|
|
|
45
45
|
// Heading
|
|
46
46
|
//
|
|
47
47
|
|
|
48
|
-
type ThreadHeaderProps =
|
|
48
|
+
type ThreadHeaderProps = ComponentPropsWithRef<'div'> & { detached?: boolean };
|
|
49
49
|
|
|
50
50
|
const ThreadHeader = forwardRef<HTMLParagraphElement, ThreadHeaderProps>(
|
|
51
|
-
({
|
|
51
|
+
({ children, detached, ...props }, forwardedRef) => {
|
|
52
52
|
return (
|
|
53
53
|
<>
|
|
54
54
|
<div role='none' className='flex items-center justify-center text-description'>
|
package/src/testing.tsx
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/
|
|
7
|
+
import { IconButton } from '@dxos/react-ui';
|
|
8
|
+
import { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';
|
|
9
9
|
|
|
10
10
|
import { MessageBody, MessageHeading } from './Message';
|
|
11
11
|
import { type MessageMetadata } from './types';
|
|
@@ -25,13 +25,14 @@ export const MessageStoryText = ({ text, onDelete, authorName, timestamp }: Mess
|
|
|
25
25
|
<>
|
|
26
26
|
<MessageHeading authorName={authorName} timestamp={timestamp}>
|
|
27
27
|
{onDelete && (
|
|
28
|
-
<
|
|
28
|
+
<IconButton
|
|
29
|
+
icon='ph--x--regular'
|
|
30
|
+
iconOnly
|
|
31
|
+
label='Delete'
|
|
29
32
|
variant='ghost'
|
|
30
33
|
classNames={['p-1.5 min-bs-0 mie-1 items-start transition-opacity', hoverableControlItem]}
|
|
31
34
|
onClick={() => onDelete()}
|
|
32
|
-
|
|
33
|
-
<Icon icon='ph--x--regular' />
|
|
34
|
-
</Button>
|
|
35
|
+
/>
|
|
35
36
|
)}
|
|
36
37
|
</MessageHeading>
|
|
37
38
|
<MessageBody>{text}</MessageBody>
|