@dxos/react-ui-thread 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-thread",
3
- "version": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
3
+ "version": "0.13.0",
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",
@@ -13,7 +13,11 @@
13
13
  "sideEffects": false,
14
14
  "type": "module",
15
15
  "imports": {
16
- "#translations": "./src/translations.ts"
16
+ "#translations": {
17
+ "source": "./src/translations.ts",
18
+ "types": "./dist/types/src/translations.d.ts",
19
+ "default": "./dist/lib/translations.mjs"
20
+ }
17
21
  },
18
22
  "exports": {
19
23
  ".": {
@@ -34,35 +38,36 @@
34
38
  ],
35
39
  "dependencies": {
36
40
  "@codemirror/language": "^6.12.4",
37
- "@codemirror/state": "^6.7.1",
38
- "@codemirror/view": "^6.43.8",
39
- "@radix-ui/react-context": "1.1.1",
41
+ "@codemirror/state": "^6.7.6",
42
+ "@codemirror/view": "^6.43.13",
40
43
  "date-fns": "^3.6.0",
41
- "@dxos/echo": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
42
- "@dxos/react-ui-dnd": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
43
- "@dxos/react-ui-editor": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
44
- "@dxos/react-ui-mosaic": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
45
- "@dxos/types": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
46
- "@dxos/ui-editor": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
47
- "@dxos/util": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264"
44
+ "@dxos/echo": "0.13.0",
45
+ "@dxos/react-ui-dnd": "0.13.0",
46
+ "@dxos/react-ui-editor": "0.13.0",
47
+ "@dxos/react-hooks": "0.13.0",
48
+ "@dxos/react-ui-mosaic": "0.13.0",
49
+ "@dxos/types": "0.13.0",
50
+ "@dxos/ui-editor": "0.13.0",
51
+ "@dxos/util": "0.13.0",
52
+ "@dxos/ui-types": "0.13.0"
48
53
  },
49
54
  "devDependencies": {
50
- "@storybook/react-vite": "^10.5.8",
55
+ "@storybook/react-vite": "^10.6.0",
51
56
  "@types/react": "~19.2.17",
52
57
  "@types/react-dom": "~19.2.3",
53
58
  "react": "~19.2.7",
54
59
  "react-dom": "~19.2.7",
55
- "vite": "^8.2.1",
56
- "@dxos/random": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
57
- "@dxos/react-ui": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
58
- "@dxos/schema": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
59
- "@dxos/ui-theme": "0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264"
60
+ "vite": "^8.2.2",
61
+ "@dxos/random": "0.13.0",
62
+ "@dxos/ui-theme": "0.13.0",
63
+ "@dxos/react-ui": "0.13.0",
64
+ "@dxos/schema": "0.13.0"
60
65
  },
61
66
  "peerDependencies": {
62
67
  "react": "~19.2.7",
63
68
  "react-dom": "~19.2.7",
64
- "@dxos/react-ui": "^0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264",
65
- "@dxos/ui-theme": "^0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264"
69
+ "@dxos/react-ui": "^0.13.0",
70
+ "@dxos/ui-theme": "^0.13.0"
66
71
  },
67
72
  "publishConfig": {
68
73
  "access": "public"
@@ -12,9 +12,9 @@ import { Message as MessageType } from '@dxos/types';
12
12
 
13
13
  import { translations } from '#translations';
14
14
 
15
- import { createMessages, getStoryMetadata } from '../testing';
16
- import { Thread } from '../Thread';
17
- import { Message } from './Message';
15
+ import { createMessages, getStoryMetadata } from '../testing.ts';
16
+ import { Thread } from '../Thread/index.ts';
17
+ import { Message } from './Message.tsx';
18
18
 
19
19
  type StoryArgs = { editable: boolean };
20
20
 
@@ -17,15 +17,11 @@ import React, {
17
17
  } from 'react';
18
18
 
19
19
  import { Obj, Ref } from '@dxos/echo';
20
- import {
21
- Avatar,
22
- IconButton,
23
- type ThemedClassName,
24
- useOnTransition,
25
- useThemeContext,
26
- useTranslation,
27
- } from '@dxos/react-ui';
28
20
  import { type UseTextEditorProps, useTextEditor } from '@dxos/react-ui-editor';
21
+ import * as Avatar from '@dxos/react-ui/Avatar';
22
+ import * as Button from '@dxos/react-ui/Button';
23
+ import * as Hooks from '@dxos/react-ui/Hooks';
24
+ import type * as Util from '@dxos/react-ui/Util';
29
25
  import { type ContentBlock, type Message as MessageType } from '@dxos/types';
30
26
  import { createBasicExtensions, createThemeExtensions, keymap, listener } from '@dxos/ui-editor';
31
27
  import { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';
@@ -33,11 +29,11 @@ import { hexToEmoji, hexToHue, isTruthy } from '@dxos/util';
33
29
 
34
30
  import { translationKey } from '#translations';
35
31
 
36
- import { command } from '../command';
37
- import { useThreadContext } from '../context';
38
- import { type MessageMetadata } from '../types';
32
+ import { command } from '../command.ts';
33
+ import { useThreadContext } from '../context.ts';
34
+ import { type MessageMetadata } from '../types.ts';
39
35
 
40
- const avatarSize = 7;
36
+ const avatarSize = 'md';
41
37
 
42
38
  const buttonGroupClassNames = 'flex flex-row items-center gap-0.5 pe-2';
43
39
  const buttonClassNames = 'p-1! transition-opacity';
@@ -46,7 +42,7 @@ const buttonClassNames = 'p-1! transition-opacity';
46
42
  // Root
47
43
  //
48
44
 
49
- export type MessageRootProps = ThemedClassName<
45
+ export type MessageRootProps = Util.ThemedClassName<
50
46
  ComponentPropsWithRef<'div'> & MessageMetadata & Partial<{ continues: boolean; controls: ReactNode }>
51
47
  >;
52
48
 
@@ -66,30 +62,28 @@ const MessageRoot = forwardRef<HTMLDivElement, MessageRootProps>(
66
62
  },
67
63
  forwardedRef,
68
64
  ) => {
69
- // Must wrap the message since Avatar.Label may be used in the content.
70
65
  // Columns mirror Thread.Header (avatar/rail · content · controls) so trailing
71
66
  // controls align with the thread header's controls.
72
67
  return (
73
- <Avatar.Root>
74
- <div
75
- data-testid='thread.message'
76
- {...rootProps}
77
- className={mx('grid grid-cols-[var(--dx-rail-size)_1fr_min-content] w-full', classNames)}
78
- ref={forwardedRef}
79
- >
80
- <div className='flex flex-col items-center gap-2 pt-1'>
81
- <Avatar.Content
82
- size={avatarSize}
83
- hue={authorAvatarProps?.hue || hexToHue(authorId ?? '0')}
84
- fallback={authorAvatarProps?.emoji || hexToEmoji(authorId ?? '0')}
85
- {...(authorImgSrc && { imgSrc: authorImgSrc })}
86
- />
87
- {continues && <div className='w-px grow bg-separator' />}
88
- </div>
89
- <div className='py-1 min-w-0'>{children}</div>
90
- {controls && <div className='self-start'>{controls}</div>}
68
+ <div
69
+ data-testid='thread.message'
70
+ {...rootProps}
71
+ className={mx('grid grid-cols-[var(--dx-rail-size)_1fr_min-content] w-full', classNames)}
72
+ ref={forwardedRef}
73
+ >
74
+ <div className='flex flex-col items-center gap-2 pt-1'>
75
+ <Avatar.Root
76
+ size={avatarSize}
77
+ hue={authorAvatarProps?.hue || hexToHue(authorId ?? '0')}
78
+ fallback={authorAvatarProps?.emoji || hexToEmoji(authorId ?? '0')}
79
+ src={authorImgSrc}
80
+ label={authorName}
81
+ />
82
+ {continues && <div className='w-px grow bg-separator' />}
91
83
  </div>
92
- </Avatar.Root>
84
+ <div className='py-1 min-w-0'>{children}</div>
85
+ {controls && <div className='self-start'>{controls}</div>}
86
+ </div>
93
87
  );
94
88
  },
95
89
  );
@@ -100,7 +94,7 @@ MessageRoot.displayName = 'Message.Root';
100
94
  // Heading
101
95
  //
102
96
 
103
- export type MessageHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> &
97
+ export type MessageHeadingProps = Util.ThemedClassName<ComponentPropsWithoutRef<'div'>> &
104
98
  Pick<MessageMetadata, 'authorName' | 'timestamp'>;
105
99
 
106
100
  const MessageHeading = ({ children, classNames, timestamp, authorName, ...props }: MessageHeadingProps) => {
@@ -120,11 +114,9 @@ MessageHeading.displayName = 'Message.Heading';
120
114
  export type MessageAuthorNameProps = Pick<MessageMetadata, 'authorName'>;
121
115
 
122
116
  const MessageAuthorName = ({ authorName }: MessageAuthorNameProps) => {
123
- const { t } = useTranslation(translationKey);
117
+ const { t } = Hooks.useTranslation(translationKey);
124
118
  return (
125
- <Avatar.Label classNames='block truncate min-w-0 shrink text-sm text-subdued'>
126
- {authorName ?? t('anonymous.label')}
127
- </Avatar.Label>
119
+ <span className='block truncate min-w-0 shrink text-sm text-fg-subtle'>{authorName ?? t('anonymous.label')}</span>
128
120
  );
129
121
  };
130
122
 
@@ -133,10 +125,10 @@ MessageAuthorName.displayName = 'Message.AuthorName';
133
125
  export type MessageTimeProps = Pick<MessageMetadata, 'timestamp'>;
134
126
 
135
127
  const MessageTime = ({ timestamp }: MessageTimeProps) => {
136
- const { dtLocale } = useTranslation(translationKey);
128
+ const { dtLocale } = Hooks.useTranslation(translationKey);
137
129
  const dt = timestamp ? new Date(timestamp) : undefined;
138
130
  return (
139
- <time className='shrink-0 text-subdued text-xs' dateTime={dt?.toISOString()}>
131
+ <time className='shrink-0 text-fg-subtle text-xs' dateTime={dt?.toISOString()}>
140
132
  {dt ? format(dt, 'p', { locale: dtLocale }) : ''}
141
133
  </time>
142
134
  );
@@ -206,7 +198,7 @@ const TextBlock = ({
206
198
  editing?: boolean;
207
199
  onSave?: (text: string) => void;
208
200
  }) => {
209
- const { themeMode } = useThemeContext();
201
+ const themeMode = Hooks.useThemeMode();
210
202
  const inMemoryContentRef = useRef(block.text);
211
203
 
212
204
  const handleDocumentChange = useCallback((next: string) => {
@@ -217,7 +209,7 @@ const TextBlock = ({
217
209
  onSave?.(inMemoryContentRef.current);
218
210
  }, [onSave]);
219
211
 
220
- useOnTransition(editing, true, false, saveDocumentChange);
212
+ Hooks.useOnTransition(editing, true, false, saveDocumentChange);
221
213
 
222
214
  const { parentRef, focusAttributes, view } = useTextEditor(
223
215
  () => ({
@@ -361,7 +353,7 @@ export type MessageTileProps = {
361
353
  * the unit rendered by `Thread.Messages`.
362
354
  */
363
355
  const MessageTile = ({ message, classNames, continues = true }: MessageTileProps) => {
364
- const { t } = useTranslation(translationKey);
356
+ const { t } = Hooks.useTranslation(translationKey);
365
357
  const {
366
358
  getMetadata,
367
359
  identityDid,
@@ -407,7 +399,7 @@ const MessageTile = ({ message, classNames, continues = true }: MessageTileProps
407
399
  showEdit || showAccept || showAcceptChange || showRejectChange || showDelete ? (
408
400
  <div className={buttonGroupClassNames}>
409
401
  {showEdit && (
410
- <IconButton
402
+ <Button.Root
411
403
  data-testid={editing ? 'thread.message.save' : 'thread.message.edit'}
412
404
  variant='ghost'
413
405
  icon={editing ? 'ph--check--regular' : 'ph--pencil-simple--regular'}
@@ -418,7 +410,7 @@ const MessageTile = ({ message, classNames, continues = true }: MessageTileProps
418
410
  />
419
411
  )}
420
412
  {showAccept && (
421
- <IconButton
413
+ <Button.Root
422
414
  data-testid='thread.message.accept'
423
415
  variant='ghost'
424
416
  icon='ph--check--regular'
@@ -429,7 +421,7 @@ const MessageTile = ({ message, classNames, continues = true }: MessageTileProps
429
421
  />
430
422
  )}
431
423
  {showAcceptChange && (
432
- <IconButton
424
+ <Button.Root
433
425
  data-testid='thread.message.accept-change'
434
426
  variant='ghost'
435
427
  icon='ph--check--regular'
@@ -440,7 +432,7 @@ const MessageTile = ({ message, classNames, continues = true }: MessageTileProps
440
432
  />
441
433
  )}
442
434
  {showRejectChange && (
443
- <IconButton
435
+ <Button.Root
444
436
  data-testid='thread.message.reject-change'
445
437
  variant='ghost'
446
438
  icon='ph--x--regular'
@@ -451,7 +443,7 @@ const MessageTile = ({ message, classNames, continues = true }: MessageTileProps
451
443
  />
452
444
  )}
453
445
  {showDelete && (
454
- <IconButton
446
+ <Button.Root
455
447
  data-testid='thread.message.delete'
456
448
  variant='ghost'
457
449
  icon='ph--x--regular'
@@ -510,7 +502,7 @@ export type MessageGroupProps = {
510
502
  * to a single message.
511
503
  */
512
504
  const MessageGroup = ({ messages, continues = true, classNames }: MessageGroupProps) => {
513
- const { t } = useTranslation(translationKey);
505
+ const { t } = Hooks.useTranslation(translationKey);
514
506
  const { getMetadata, identityDid, editable, onMessageDelete, onAcceptProposal } = useThreadContext('Message.Group');
515
507
  const [editing, setEditing] = useState(false);
516
508
 
@@ -541,7 +533,7 @@ const MessageGroup = ({ messages, continues = true, classNames }: MessageGroupPr
541
533
  showEdit || showAccept || showDelete ? (
542
534
  <div className={buttonGroupClassNames}>
543
535
  {showEdit && (
544
- <IconButton
536
+ <Button.Root
545
537
  data-testid={editing ? 'thread.message.save' : 'thread.message.edit'}
546
538
  variant='ghost'
547
539
  icon={editing ? 'ph--check--regular' : 'ph--pencil-simple--regular'}
@@ -552,7 +544,7 @@ const MessageGroup = ({ messages, continues = true, classNames }: MessageGroupPr
552
544
  />
553
545
  )}
554
546
  {showAccept && (
555
- <IconButton
547
+ <Button.Root
556
548
  data-testid='thread.message.accept'
557
549
  variant='ghost'
558
550
  icon='ph--check--regular'
@@ -563,7 +555,7 @@ const MessageGroup = ({ messages, continues = true, classNames }: MessageGroupPr
563
555
  />
564
556
  )}
565
557
  {showDelete && (
566
- <IconButton
558
+ <Button.Root
567
559
  data-testid='thread.message.delete'
568
560
  variant='ghost'
569
561
  icon='ph--x--regular'
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export { Message } from './Message';
5
+ export { Message } from './Message.tsx';
6
6
  export type {
7
7
  MessageAuthorNameProps,
8
8
  MessageBodyProps,
@@ -12,4 +12,4 @@ export type {
12
12
  MessageTextboxProps,
13
13
  MessageTileProps,
14
14
  MessageTimeProps,
15
- } from './Message';
15
+ } from './Message.tsx';
@@ -11,8 +11,8 @@ import { Message as MessageType } from '@dxos/types';
11
11
 
12
12
  import { translations } from '#translations';
13
13
 
14
- import { createMessages, getStoryMetadata } from '../testing';
15
- import { Thread } from './Thread';
14
+ import { createMessages, getStoryMetadata } from '../testing.ts';
15
+ import { Thread } from './Thread.tsx';
16
16
 
17
17
  const IDENTITY = { role: 'user' as const, identityDid: 'did:key:alice', name: 'Alice' };
18
18
 
@@ -26,7 +26,7 @@ const DefaultStory = () => {
26
26
 
27
27
  return (
28
28
  <Thread.Root getMetadata={getStoryMetadata} identityDid={IDENTITY.identityDid} editable onMessageDelete={() => {}}>
29
- <Thread.Content classNames='grow min-h-0'>
29
+ <Thread.Content classNames='dx-grow'>
30
30
  <Thread.Messages messages={messages} />
31
31
  <Thread.Textbox id='composer' authorId={IDENTITY.identityDid} authorName={IDENTITY.name} onSend={handleSend} />
32
32
  <Thread.Status />
@@ -59,7 +59,7 @@ const MixedSendersStory = () => {
59
59
 
60
60
  return (
61
61
  <Thread.Root getMetadata={getStoryMetadata} identityDid={IDENTITY.identityDid} editable={false}>
62
- <Thread.Content classNames='grow min-h-0'>
62
+ <Thread.Content classNames='dx-grow'>
63
63
  <Thread.Messages messages={messages} />
64
64
  </Thread.Content>
65
65
  </Thread.Root>
@@ -96,7 +96,7 @@ const GroupedStory = () => {
96
96
 
97
97
  return (
98
98
  <Thread.Root getMetadata={getStoryMetadata} identityDid={IDENTITY.identityDid} editable={false}>
99
- <Thread.Content classNames='grow min-h-0'>
99
+ <Thread.Content classNames='dx-grow'>
100
100
  <Thread.Messages messages={messages} />
101
101
  </Thread.Content>
102
102
  </Thread.Root>
@@ -19,31 +19,24 @@ import React, {
19
19
  } from 'react';
20
20
 
21
21
  import { Obj } from '@dxos/echo';
22
- import {
23
- type ComposableProps,
24
- Icon,
25
- IconButton,
26
- ScrollArea,
27
- type ThemedClassName,
28
- composable,
29
- composableProps,
30
- useThemeContext,
31
- useTranslation,
32
- } from '@dxos/react-ui';
33
22
  import { type DndContainerHandler } from '@dxos/react-ui-dnd';
34
23
  import { Mosaic, type MosaicTileProps } from '@dxos/react-ui-mosaic';
24
+ import * as Button from '@dxos/react-ui/Button';
25
+ import * as Hooks from '@dxos/react-ui/Hooks';
26
+ import * as Icon from '@dxos/react-ui/Icon';
27
+ import * as ScrollArea from '@dxos/react-ui/ScrollArea';
28
+ import * as Util from '@dxos/react-ui/Util';
35
29
  import { type Message as MessageType } from '@dxos/types';
36
30
  import { type Extension, createBasicExtensions, createThemeExtensions, listener } from '@dxos/ui-editor';
37
31
  import { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';
32
+ import { type ComposableProps } from '@dxos/ui-types';
38
33
 
39
34
  import { translationKey } from '#translations';
40
35
 
41
- import { command } from '../command';
42
- import { ThreadContextProvider, useThreadContext } from '../context';
43
- import { Message } from '../Message';
44
- import { type MessageMetadata, type ThreadContextValue } from '../types';
45
-
46
- const getMessageId = (message: MessageType.Message) => Obj.getURI(message);
36
+ import { command } from '../command.ts';
37
+ import { ThreadContextProvider, useThreadContext } from '../context.ts';
38
+ import { Message } from '../Message/index.ts';
39
+ import { type MessageMetadata, type ThreadContextValue } from '../types.ts';
47
40
 
48
41
  //
49
42
  // Root
@@ -115,11 +108,11 @@ type ThreadContentExtra = {
115
108
  export type ThreadContentProps = ComposableProps<ThreadContentExtra>;
116
109
 
117
110
  /** Visible thread container (the attention surface that hosts header / messages / composer). */
118
- const ThreadContent = composable<HTMLDivElement, ThreadContentExtra>(
111
+ const ThreadContent = Util.composable<HTMLDivElement, ThreadContentExtra>(
119
112
  ({ children, current, id, ...props }, forwardedRef) => {
120
113
  return (
121
114
  <div
122
- {...composableProps(props, {
115
+ {...Util.composableProps(props, {
123
116
  role: 'group',
124
117
  classNames: [
125
118
  'flex flex-col bg-[var(--surface-bg)] current-related dx-attention-surface [--controls-opacity:0]',
@@ -163,9 +156,9 @@ export type ThreadHeaderProps = Omit<
163
156
  * `[rail · 1fr · controls]` grid so the trailing controls align with message-tile
164
157
  * controls (which use the same template) — no grid is leaked in from the caller.
165
158
  */
166
- const ThreadHeader = composable<HTMLDivElement, ThreadHeaderProps>(
159
+ const ThreadHeader = Util.composable<HTMLDivElement, ThreadHeaderProps>(
167
160
  ({ title, detached, current, controls, onSelect, ...props }, forwardedRef) => {
168
- const { t } = useTranslation(translationKey);
161
+ const { t } = Hooks.useTranslation(translationKey);
169
162
  const { focusComposer } = useThreadContext('Thread.Header');
170
163
  const handleSelect = useCallback(() => {
171
164
  onSelect?.();
@@ -174,7 +167,7 @@ const ThreadHeader = composable<HTMLDivElement, ThreadHeaderProps>(
174
167
 
175
168
  return (
176
169
  <div
177
- {...composableProps(props, {
170
+ {...Util.composableProps(props, {
178
171
  classNames: [
179
172
  'grid grid-cols-[var(--dx-rail-size)_1fr_min-content] items-center',
180
173
  hoverableControls,
@@ -184,13 +177,13 @@ const ThreadHeader = composable<HTMLDivElement, ThreadHeaderProps>(
184
177
  ref={forwardedRef}
185
178
  >
186
179
  <div className='flex items-center justify-center'>
187
- <IconButton
180
+ <Button.Root
188
181
  iconOnly
189
182
  variant='ghost'
190
- density='sm'
183
+ size='sm'
191
184
  icon='ph--caret-double-right--regular'
192
185
  label={t('select-thread.label')}
193
- classNames='text-description'
186
+ classNames='text-fg-muted'
194
187
  onClick={handleSelect}
195
188
  />
196
189
  </div>
@@ -200,7 +193,7 @@ const ThreadHeader = composable<HTMLDivElement, ThreadHeaderProps>(
200
193
  data-testid='thread.heading'
201
194
  className={mx(
202
195
  'me-2 font-medium truncate italic',
203
- current ? 'text-accent-text' : 'text-description',
196
+ current ? 'text-accent-text' : 'text-fg-muted',
204
197
  detached && 'line-through decoration-1',
205
198
  )}
206
199
  >
@@ -297,7 +290,7 @@ const groupMessages = (
297
290
  if (currentGroup && sameSenderAsGroup && withinGroupWindow) {
298
291
  currentGroup.messages.push(message);
299
292
  } else {
300
- currentGroup = { kind: 'group', id: getMessageId(message), messages: [message] };
293
+ currentGroup = { kind: 'group', id: Obj.getURI(message), messages: [message] };
301
294
  items.push(currentGroup);
302
295
  }
303
296
  lastGroupMessage = message;
@@ -320,7 +313,7 @@ const groupMessages = (
320
313
 
321
314
  const ThreadDivider = ({ label }: { label?: string }) =>
322
315
  label ? (
323
- <div className='flex items-center gap-2 px-2 py-2 text-xs text-description'>
316
+ <div className='flex items-center gap-2 px-2 py-2 text-xs text-fg-muted'>
324
317
  <div className='h-px grow bg-separator' />
325
318
  <span className='shrink-0'>{label}</span>
326
319
  <div className='h-px grow bg-separator' />
@@ -343,7 +336,7 @@ const ThreadItemAdapter = ({ id, data, location, draggable, current, selected }:
343
336
  </Mosaic.Tile>
344
337
  );
345
338
 
346
- export type ThreadMessagesProps = ThemedClassName<{
339
+ export type ThreadMessagesProps = Util.ThemedClassName<{
347
340
  messages: readonly MessageType.Message[];
348
341
  /** Stable id of the owning thread; scopes the Mosaic container so multiple threads don't collide. */
349
342
  id?: string;
@@ -372,7 +365,7 @@ const ThreadMessages = ({
372
365
  gapDividerMs = DEFAULT_GAP_DIVIDER_MS,
373
366
  classNames,
374
367
  }: ThreadMessagesProps) => {
375
- const { dtLocale } = useTranslation(translationKey);
368
+ const { dtLocale } = Hooks.useTranslation(translationKey);
376
369
  const [viewport, setViewport] = useState<HTMLElement | null>(null);
377
370
  const items = useMemo(
378
371
  () => groupMessages(messages.filter(Boolean), { groupWindowMs, dayDivider, gapDividerMs, dtLocale }),
@@ -393,7 +386,7 @@ const ThreadMessages = ({
393
386
  currentId={currentId}
394
387
  eventHandler={eventHandler}
395
388
  >
396
- <ScrollArea.Root classNames={mx('col-span-2 flex-1 min-h-0', classNames)} orientation='vertical'>
389
+ <ScrollArea.Root classNames={mx('col-span-2 dx-grow', classNames)} orientation='vertical'>
397
390
  <ScrollArea.Viewport ref={setViewport}>
398
391
  <Mosaic.VirtualStack
399
392
  Tile={ThreadItemAdapter}
@@ -429,8 +422,8 @@ export type ThreadTextboxProps = MessageMetadata & {
429
422
 
430
423
  /** Message composer pinned at the foot of a thread. */
431
424
  const ThreadTextbox = ({ placeholder, autoFocus, disabled, extensions, onSend, ...metadata }: ThreadTextboxProps) => {
432
- const { t } = useTranslation(translationKey);
433
- const { themeMode } = useThemeContext();
425
+ const { t } = Hooks.useTranslation(translationKey);
426
+ const themeMode = Hooks.useThemeMode();
434
427
  const { registerComposerFocus } = useThreadContext('Thread.Textbox');
435
428
  const composerRef = useRef<{ focus: () => void } | null>(null);
436
429
  const messageRef = useRef('');
@@ -483,23 +476,23 @@ ThreadTextbox.displayName = 'Thread.Textbox';
483
476
  // Status
484
477
  //
485
478
 
486
- export type ThreadStatusProps = ThemedClassName<PropsWithChildren> & {
479
+ export type ThreadStatusProps = Util.ThemedClassName<PropsWithChildren> & {
487
480
  activity?: boolean;
488
481
  };
489
482
 
490
483
  const ThreadStatus = forwardRef<HTMLDivElement, ThreadStatusProps>(
491
484
  ({ activity, classNames, children, ...props }, forwardedRef) => {
492
- const { t } = useTranslation(translationKey);
485
+ const { t } = Hooks.useTranslation(translationKey);
493
486
  return (
494
487
  <div
495
488
  {...props}
496
489
  className={mx(
497
- 'col-start-2 grid grid-cols-[min-content_1fr_max-content] pb-2 pe-2 text-xs text-description',
490
+ 'col-start-2 grid grid-cols-[min-content_1fr_max-content] pb-2 pe-2 text-xs text-fg-muted',
498
491
  classNames,
499
492
  )}
500
493
  ref={forwardedRef}
501
494
  >
502
- <Icon
495
+ <Icon.Icon
503
496
  icon='ph--spinner--bold'
504
497
  classNames='w-6 h-4 invisible data-[visible=show]:visible animate-spin-slow'
505
498
  data-visible={activity ? 'show' : 'hide'}
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export { Thread } from './Thread';
5
+ export { Thread } from './Thread.tsx';
6
6
  export type {
7
7
  ThreadContentProps,
8
8
  ThreadHeaderProps,
@@ -10,4 +10,4 @@ export type {
10
10
  ThreadRootProps,
11
11
  ThreadStatusProps,
12
12
  ThreadTextboxProps,
13
- } from './Thread';
13
+ } from './Thread.tsx';
package/src/command.ts CHANGED
@@ -37,15 +37,15 @@ const parser = StreamLanguage.define<{ count: number }>({
37
37
  const styles = HighlightStyle.define([
38
38
  {
39
39
  tag: tags.tagName,
40
- class: mx('dx-tag dx-tag--indigo'),
40
+ class: mx('dx-tag dx-tag-inline dx-tag-indigo mx-0.5'),
41
41
  },
42
42
  {
43
43
  tag: tags.labelName,
44
- class: mx('dx-tag dx-tag--blue'),
44
+ class: mx('dx-tag dx-tag-inline dx-tag-blue mx-0.5'),
45
45
  },
46
46
  ]);
47
47
 
48
- // Center the inline `.dx-tag` pills on the text line. `.dx-tag` is `inline-block`
48
+ // Center the inline `.dx-tag` pills on the text line. An inline tag is an inline box
49
49
  // with vertical padding, so on a CodeMirror line it baseline-aligns and rides
50
50
  // high; overriding `vertical-align` recentres it (cf. the `cm-reference-pill`
51
51
  // precedent in `@dxos/react-ui-chat`).
package/src/context.ts CHANGED
@@ -2,14 +2,14 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- import { createContext } from '@radix-ui/react-context';
6
5
  import { type FC, type ReactNode } from 'react';
7
6
 
7
+ import { createContext } from '@dxos/react-hooks';
8
8
  // Imported so TypeScript can name this type in the emitted .d.ts (ThreadContext → Message).
9
9
  // eslint-disable-next-line unused-imports/no-unused-imports
10
10
  import { type Message } from '@dxos/types';
11
11
 
12
- import { type ThreadContextValue } from './types';
12
+ import { type ThreadContextValue } from './types.ts';
13
13
 
14
14
  /**
15
15
  * Context provided by `Thread.Root` and consumed by its parts. Extends the
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export { Thread } from './Thread';
5
+ export { Thread } from './Thread/index.ts';
6
6
  export type {
7
7
  ThreadContentProps,
8
8
  ThreadHeaderProps,
@@ -10,8 +10,8 @@ export type {
10
10
  ThreadRootProps,
11
11
  ThreadStatusProps,
12
12
  ThreadTextboxProps,
13
- } from './Thread';
14
- export { Message } from './Message';
13
+ } from './Thread/index.ts';
14
+ export { Message } from './Message/index.ts';
15
15
  export type {
16
16
  MessageAuthorNameProps,
17
17
  MessageBodyProps,
@@ -21,13 +21,13 @@ export type {
21
21
  MessageTextboxProps,
22
22
  MessageTileProps,
23
23
  MessageTimeProps,
24
- } from './Message';
25
- export { command } from './command';
26
- export { useThreadContext } from './context';
24
+ } from './Message/index.ts';
25
+ export { command } from './command.ts';
26
+ export { useThreadContext } from './context.ts';
27
27
  export type {
28
28
  MessageCallbacks,
29
29
  MessageMetadata,
30
30
  ObjectTileComponent,
31
31
  ThreadComponents,
32
32
  ThreadContextValue,
33
- } from './types';
33
+ } from './types.ts';
package/src/testing.ts CHANGED
@@ -8,7 +8,7 @@ import { type ValueGenerator, createGenerator } from '@dxos/schema/testing';
8
8
  import { Message } from '@dxos/types';
9
9
  import { hexToFallback } from '@dxos/util';
10
10
 
11
- import { type MessageMetadata } from './types';
11
+ import { type MessageMetadata } from './types.ts';
12
12
 
13
13
  const generator: ValueGenerator = random as any;
14
14