@dxos/react-ui-chat 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +214 -339
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +14 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +214 -339
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +16 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/ChatDialog/ChatDialog.d.ts +17 -1
  13. package/dist/types/src/components/ChatDialog/ChatDialog.d.ts.map +1 -1
  14. package/dist/types/src/components/ChatDialog/ChatDialog.stories.d.ts +35 -5
  15. package/dist/types/src/components/ChatDialog/ChatDialog.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/ChatEditor/ChatEditor.d.ts +7 -9
  17. package/dist/types/src/components/ChatEditor/ChatEditor.d.ts.map +1 -1
  18. package/dist/types/src/components/ChatEditor/ChatEditor.stories.d.ts +20 -5
  19. package/dist/types/src/components/ChatEditor/ChatEditor.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/ChatEditor/references.d.ts +4 -0
  21. package/dist/types/src/components/ChatEditor/references.d.ts.map +1 -1
  22. package/dist/types/src/components/ChatStatus/ChatStatus.d.ts +40 -0
  23. package/dist/types/src/components/ChatStatus/ChatStatus.d.ts.map +1 -0
  24. package/dist/types/src/components/ChatStatus/ChatStatus.stories.d.ts +27 -0
  25. package/dist/types/src/components/ChatStatus/ChatStatus.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/ChatStatus/format.d.ts +7 -0
  27. package/dist/types/src/components/ChatStatus/format.d.ts.map +1 -0
  28. package/dist/types/src/components/ChatStatus/format.test.d.ts +2 -0
  29. package/dist/types/src/components/ChatStatus/format.test.d.ts.map +1 -0
  30. package/dist/types/src/components/ChatStatus/index.d.ts +3 -0
  31. package/dist/types/src/components/ChatStatus/index.d.ts.map +1 -0
  32. package/dist/types/src/components/index.d.ts +1 -0
  33. package/dist/types/src/components/index.d.ts.map +1 -1
  34. package/dist/types/src/translations.d.ts +2 -35
  35. package/dist/types/src/translations.d.ts.map +1 -1
  36. package/dist/types/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +51 -43
  38. package/src/components/ChatDialog/ChatDialog.stories.tsx +7 -9
  39. package/src/components/ChatDialog/ChatDialog.tsx +28 -18
  40. package/src/components/ChatEditor/ChatEditor.stories.tsx +26 -26
  41. package/src/components/ChatEditor/ChatEditor.tsx +35 -65
  42. package/src/components/ChatEditor/references.ts +18 -12
  43. package/src/components/ChatStatus/ChatStatus.stories.tsx +130 -0
  44. package/src/components/ChatStatus/ChatStatus.tsx +183 -0
  45. package/src/components/ChatStatus/format.test.ts +48 -0
  46. package/src/components/ChatStatus/format.ts +27 -0
  47. package/src/components/ChatStatus/index.ts +6 -0
  48. package/src/components/index.ts +1 -0
  49. package/src/translations.ts +2 -41
  50. package/dist/types/src/components/ChatEditor/autocomplete.d.ts +0 -26
  51. package/dist/types/src/components/ChatEditor/autocomplete.d.ts.map +0 -1
  52. package/src/components/ChatEditor/autocomplete.ts +0 -220
@@ -1,121 +1,97 @@
1
1
  // src/components/ChatDialog/ChatDialog.tsx
2
- import { useSignals as _useSignals } from "@preact-signals/safe-react/tracking";
3
2
  import { createContext } from "@radix-ui/react-context";
4
3
  import React, { useEffect, useState } from "react";
5
4
  import { Dialog, Icon, IconButton, useControlledState } from "@dxos/react-ui";
6
5
  import { ResizeHandle, resizeAttributes, sizeStyle } from "@dxos/react-ui-dnd";
7
- import { mx } from "@dxos/react-ui-theme";
6
+ import { mx } from "@dxos/ui-theme";
8
7
  var preventDefault = (event) => event.preventDefault();
9
8
  var Endcap = ({ children }) => {
10
- var _effect = _useSignals();
11
- try {
12
- return /* @__PURE__ */ React.createElement("div", {
13
- className: "grid w-[var(--rail-action)] h-[var(--rail-action)] items-center justify-center"
14
- }, children);
15
- } finally {
16
- _effect.f();
17
- }
9
+ return /* @__PURE__ */ React.createElement("div", {
10
+ className: "grid w-[var(--dx-rail-action)] h-[var(--dx-rail-action)] items-center justify-center"
11
+ }, children);
18
12
  };
19
13
  var [ChatDialogContextProvider, useChatDialogContext] = createContext("ChatDialog");
20
- var ChatDialogRoot = ({ children, open: _open = false, expanded: _expanded = false, onOpenChange, onExpandedChange, onEscape }) => {
21
- var _effect = _useSignals();
22
- try {
23
- const [size, setSize] = useState("min-content");
24
- const [open, setOpen] = useControlledState(_open, onOpenChange);
25
- const [expanded, setExpanded] = useControlledState(_expanded, onExpandedChange);
26
- return /* @__PURE__ */ React.createElement(ChatDialogContextProvider, {
27
- open,
28
- setOpen,
29
- expanded,
30
- setExpanded,
31
- size,
32
- setSize
33
- }, /* @__PURE__ */ React.createElement(Dialog.Root, {
34
- modal: false,
35
- open,
36
- onOpenChange: setOpen
37
- }, /* @__PURE__ */ React.createElement("div", {
38
- role: "none",
39
- className: "dx-dialog__overlay bg-transparent pointer-events-none",
40
- "data-block-align": "end"
41
- }, /* @__PURE__ */ React.createElement(Dialog.Content, {
42
- inOverlayLayout: true,
43
- classNames: [
44
- "grid grid-rows-[var(--rail-action)_1fr_min-content] p-0 overflow-hidden",
45
- "box-content md:is-[35rem] md:max-is-none pointer-events-auto"
46
- ],
47
- onEscapeKeyDown: onEscape,
48
- onInteractOutside: preventDefault
49
- }, children))));
50
- } finally {
51
- _effect.f();
52
- }
14
+ var ChatDialogRoot = ({ children, open: openProp = false, expanded: expandedProp = false, onOpenChange, onExpandedChange, onEscape }) => {
15
+ const [size, setSize] = useState("min-content");
16
+ const [open, setOpen] = useControlledState(openProp, onOpenChange);
17
+ const [expanded, setExpanded] = useControlledState(expandedProp, onExpandedChange);
18
+ return /* @__PURE__ */ React.createElement(ChatDialogContextProvider, {
19
+ open,
20
+ setOpen,
21
+ expanded,
22
+ setExpanded,
23
+ size,
24
+ setSize
25
+ }, /* @__PURE__ */ React.createElement(Dialog.Root, {
26
+ modal: false,
27
+ open,
28
+ onOpenChange: setOpen
29
+ }, /* @__PURE__ */ React.createElement("div", {
30
+ className: "dx-dialog__overlay bg-transparent pointer-events-none",
31
+ "data-block-align": "end"
32
+ }, /* @__PURE__ */ React.createElement(Dialog.Content, {
33
+ size: "md",
34
+ inOverlayLayout: true,
35
+ classNames: [
36
+ "grid grid-rows-[var(--dx-rail-action)_1fr_min-content] p-0 overflow-hidden box-content pointer-events-auto"
37
+ ],
38
+ onEscapeKeyDown: onEscape,
39
+ onInteractOutside: preventDefault
40
+ }, children))));
53
41
  };
54
42
  ChatDialogRoot.displayName = "ChatDialog.Root";
43
+ var CHAT_DIALOG_HEADER_NAME = "ChatDialog.Header";
55
44
  var ChatDialogHeader = ({ classNames, title }) => {
56
- var _effect = _useSignals();
57
- try {
58
- const { expanded, setExpanded } = useChatDialogContext(ChatDialogHeader.displayName);
59
- return /* @__PURE__ */ React.createElement("div", {
60
- className: mx("grid grid-cols-[var(--rail-action)_1fr_min-content] items-center overflow-hidden", classNames)
61
- }, /* @__PURE__ */ React.createElement(Endcap, null, /* @__PURE__ */ React.createElement(Dialog.Close, null, /* @__PURE__ */ React.createElement(Icon, {
62
- icon: "ph--x--regular"
63
- }))), /* @__PURE__ */ React.createElement(Dialog.Title, {
64
- classNames: "flex w-full justify-center text-sm text-subdued select-none cursor-pointer",
65
- onClick: () => setExpanded((expanded2) => !expanded2)
66
- }, title), /* @__PURE__ */ React.createElement(Endcap, null, /* @__PURE__ */ React.createElement(IconButton, {
67
- variant: "ghost",
68
- icon: "ph--caret-up--regular",
69
- iconOnly: true,
70
- classNames: mx("!p-1 [&>svg]:transition [&>svg]:duration-200", expanded && "[&>svg]:rotate-180"),
71
- label: expanded ? "Close" : "Open",
72
- onClick: () => setExpanded((expanded2) => !expanded2)
73
- })));
74
- } finally {
75
- _effect.f();
76
- }
45
+ const { expanded, setExpanded } = useChatDialogContext(CHAT_DIALOG_HEADER_NAME);
46
+ return /* @__PURE__ */ React.createElement("div", {
47
+ className: mx("grid grid-cols-[var(--dx-rail-action)_1fr_min-content] items-center overflow-hidden", classNames)
48
+ }, /* @__PURE__ */ React.createElement(Endcap, null, /* @__PURE__ */ React.createElement(Dialog.Close, null, /* @__PURE__ */ React.createElement(Icon, {
49
+ icon: "ph--x--regular"
50
+ }))), /* @__PURE__ */ React.createElement(Dialog.Title, {
51
+ classNames: "flex w-full justify-center text-sm text-subdued select-none cursor-pointer",
52
+ onClick: () => setExpanded((expanded2) => !expanded2)
53
+ }, title), /* @__PURE__ */ React.createElement(Endcap, null, /* @__PURE__ */ React.createElement(IconButton, {
54
+ variant: "ghost",
55
+ icon: "ph--caret-up--regular",
56
+ iconOnly: true,
57
+ classNames: mx("p-1! [&>svg]:transition [&>svg]:duration-200", expanded && "[&>svg]:rotate-180"),
58
+ label: expanded ? "Close" : "Open",
59
+ onClick: () => setExpanded((expanded2) => !expanded2)
60
+ })));
77
61
  };
78
- ChatDialogHeader.displayName = "ChatDialog.Header";
62
+ ChatDialogHeader.displayName = CHAT_DIALOG_HEADER_NAME;
63
+ var CHAT_DIALOG_CONTENT_NAME = "ChatDialog.Content";
79
64
  var ChatDialogContent = ({ children, classNames }) => {
80
- var _effect = _useSignals();
81
- try {
82
- const { expanded, size, setSize } = useChatDialogContext(ChatDialogContent.displayName);
83
- useEffect(() => {
84
- setSize(expanded ? "min-content" : 0);
85
- }, [
86
- expanded
87
- ]);
88
- return /* @__PURE__ */ React.createElement("div", {
89
- className: mx("flex flex-col overflow-y-auto border-t border-b border-subduedSeparator", 'transition-[block-size] ease-in-out duration-0 [&:not([data-dx-resizing="true"])]:duration-200', classNames),
90
- style: {
91
- ...sizeStyle(size, "vertical", true),
92
- maxBlockSize: "calc(100dvh - env(safe-area-inset-bottom) - env(safe-area-inset-top) - 14rem)"
93
- },
94
- ...resizeAttributes
95
- }, /* @__PURE__ */ React.createElement(ResizeHandle, {
96
- side: "block-start",
97
- iconPosition: "center",
98
- defaultSize: "min-content",
99
- fallbackSize: 0,
100
- minSize: 0,
101
- onSizeChange: setSize
102
- }), children);
103
- } finally {
104
- _effect.f();
105
- }
65
+ const { expanded, size, setSize } = useChatDialogContext(CHAT_DIALOG_CONTENT_NAME);
66
+ useEffect(() => {
67
+ setSize(expanded ? "min-content" : 0);
68
+ }, [
69
+ expanded
70
+ ]);
71
+ return /* @__PURE__ */ React.createElement("div", {
72
+ className: mx("border-t border-b border-subdued-separator", 'transition ease-in-out duration-0 [&:not([data-dx-resizing="true"])]:duration-200', classNames),
73
+ style: {
74
+ ...sizeStyle(size, "vertical", true),
75
+ maxBlockSize: "calc(100dvh - env(safe-area-inset-bottom) - env(safe-area-inset-top) - 14rem)"
76
+ },
77
+ ...resizeAttributes
78
+ }, /* @__PURE__ */ React.createElement(ResizeHandle, {
79
+ side: "block-start",
80
+ iconPosition: "center",
81
+ defaultSize: "min-content",
82
+ fallbackSize: 0,
83
+ minSize: 0,
84
+ onSizeChange: setSize
85
+ }), children);
106
86
  };
107
- ChatDialogContent.displayName = "ChatDialog.Content";
87
+ ChatDialogContent.displayName = CHAT_DIALOG_CONTENT_NAME;
88
+ var CHAT_DIALOG_FOOTER_NAME = "ChatDialog.Footer";
108
89
  var ChatDialogFooter = ({ children, classNames }) => {
109
- var _effect = _useSignals();
110
- try {
111
- return /* @__PURE__ */ React.createElement("div", {
112
- className: mx(classNames)
113
- }, children);
114
- } finally {
115
- _effect.f();
116
- }
90
+ return /* @__PURE__ */ React.createElement("div", {
91
+ className: mx(classNames)
92
+ }, children);
117
93
  };
118
- ChatDialogFooter.displayName = "ChatDialog.Footer";
94
+ ChatDialogFooter.displayName = CHAT_DIALOG_FOOTER_NAME;
119
95
  var ChatDialog = {
120
96
  Root: ChatDialogRoot,
121
97
  Header: ChatDialogHeader,
@@ -131,15 +107,7 @@ import { Mutex } from "@dxos/async";
131
107
  import { invariant } from "@dxos/invariant";
132
108
  var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-chat/src/components/ChatEditor/references.ts";
133
109
  var references = ({ provider, triggerCharacter = "@", debug = false }) => {
134
- invariant(triggerCharacter.length === 1, void 0, {
135
- F: __dxlog_file,
136
- L: 50,
137
- S: void 0,
138
- A: [
139
- "triggerCharacter.length === 1",
140
- ""
141
- ]
142
- });
110
+ invariant(triggerCharacter.length === 1, void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 14, S: void 0, A: ["triggerCharacter.length === 1", ""] });
143
111
  const decorationField = ViewPlugin.fromClass(class ReferenceView {
144
112
  // TODO(burdon): Why?
145
113
  _mutex = new Mutex();
@@ -186,7 +154,7 @@ var references = ({ provider, triggerCharacter = "@", debug = false }) => {
186
154
  }
187
155
  }, {
188
156
  decorations: (v) => v.decorations,
189
- provide: (plugin) => [
157
+ provide: () => [
190
158
  EditorView.atomicRanges.of((view) => view.plugin(decorationField)?.decorations ?? RangeSet.empty)
191
159
  ]
192
160
  });
@@ -201,7 +169,7 @@ var references = ({ provider, triggerCharacter = "@", debug = false }) => {
201
169
  }
202
170
  }),
203
171
  autocompletion({
204
- tooltipClass: () => "shadow rounded",
172
+ tooltipClass: () => "shadow rounded-sm",
205
173
  activateOnTyping: true,
206
174
  aboveCursor: true,
207
175
  closeOnBlur: !debug,
@@ -234,251 +202,158 @@ var ReferenceWidget = class extends WidgetType {
234
202
  constructor(data) {
235
203
  super(), this.data = data;
236
204
  }
205
+ ignoreEvent() {
206
+ return true;
207
+ }
208
+ eq(other) {
209
+ return this.data.uri === other.data.uri;
210
+ }
237
211
  toDOM() {
238
212
  const span = document.createElement("span");
239
- span.textContent = `@ ${this.data.label}`;
213
+ span.textContent = `@${this.data.label}`;
240
214
  span.className = "cm-reference-pill";
241
215
  return span;
242
216
  }
243
- eq(other) {
244
- return other.data.uri === this.data.uri;
245
- }
246
- ignoreEvent() {
247
- return true;
248
- }
249
217
  };
250
218
 
251
219
  // src/components/ChatEditor/ChatEditor.tsx
252
- import { useSignals as _useSignals2 } from "@preact-signals/safe-react/tracking";
253
- import React2, { forwardRef, useImperativeHandle } from "react";
220
+ import React2, { forwardRef, useMemo } from "react";
254
221
  import { useThemeContext } from "@dxos/react-ui";
255
- import { createBasicExtensions, createThemeExtensions, useTextEditor } from "@dxos/react-ui-editor";
256
- import { mx as mx2 } from "@dxos/react-ui-theme";
257
- import { isNonNullable } from "@dxos/util";
258
-
259
- // src/components/ChatEditor/autocomplete.ts
260
- import { Prec } from "@codemirror/state";
261
- import { Decoration as Decoration2, EditorView as EditorView2, ViewPlugin as ViewPlugin2, WidgetType as WidgetType2, keymap as keymap2 } from "@codemirror/view";
262
- var autocomplete = ({ fireIfEmpty, onSubmit, onSuggest, onCancel }) => {
263
- const suggest = ViewPlugin2.fromClass(class {
264
- _decorations;
265
- _currentSuggestion = null;
266
- constructor(view) {
267
- this._decorations = this.computeDecorations(view);
268
- }
269
- update(update) {
270
- if (update.docChanged || update.selectionSet) {
271
- this._decorations = this.computeDecorations(update.view);
272
- }
273
- }
274
- computeDecorations(view) {
275
- const text = view.state.doc.toString();
276
- const suggestions = onSuggest?.(text) ?? [];
277
- if (!suggestions.length) {
278
- this._currentSuggestion = null;
279
- return Decoration2.none;
280
- }
281
- this._currentSuggestion = suggestions[0];
282
- const suffix = this._currentSuggestion.slice(text.length);
283
- if (!suffix) {
284
- return Decoration2.none;
285
- }
286
- return Decoration2.set([
287
- Decoration2.widget({
288
- widget: new InlineSuggestionWidget(suffix),
289
- side: 1
290
- }).range(view.state.doc.length)
291
- ]);
292
- }
293
- completeSuggestion(view) {
294
- if (!this._currentSuggestion) {
295
- return false;
296
- }
297
- const text = view.state.doc.toString();
298
- const suffix = this._currentSuggestion.slice(text.length);
299
- if (!suffix) {
300
- return false;
301
- }
302
- view.dispatch({
303
- changes: {
304
- from: view.state.doc.length,
305
- insert: suffix
306
- },
307
- selection: {
308
- anchor: view.state.doc.length + suffix.length
309
- }
310
- });
311
- return true;
312
- }
313
- }, {
314
- decorations: (v) => v._decorations
315
- });
316
- return [
317
- suggest,
318
- EditorView2.theme({
319
- ".cm-inline-suggestion": {
320
- opacity: 0.4
321
- }
222
+ import { Editor } from "@dxos/react-ui-editor";
223
+ import { createBasicExtensions, createThemeExtensions, submit, xmlFormatting } from "@dxos/ui-editor";
224
+ import { isTruthy } from "@dxos/util";
225
+ var useChatExtensions = ({ extensions, lineWrapping = false, placeholder, onSubmit }) => {
226
+ const { themeMode } = useThemeContext();
227
+ return useMemo(() => [
228
+ createThemeExtensions({
229
+ themeMode
322
230
  }),
323
- Prec.highest(keymap2.of([
324
- {
325
- key: "Tab",
326
- preventDefault: true,
327
- run: (view) => {
328
- const plugin = view.plugin(suggest);
329
- return plugin?.completeSuggestion(view) ?? false;
330
- }
331
- },
332
- {
333
- key: "ArrowRight",
334
- preventDefault: true,
335
- run: (view) => {
336
- if (view.state.selection.main.head !== view.state.doc.length) {
337
- return false;
338
- }
339
- const plugin = view.plugin(suggest);
340
- return plugin?.completeSuggestion(view) ?? false;
341
- }
342
- },
343
- {
344
- key: "Enter",
345
- preventDefault: true,
346
- run: (view) => {
347
- const text = view.state.doc.toString().trim();
348
- if (onSubmit && (fireIfEmpty || text.length > 0)) {
349
- const reset = onSubmit(text);
350
- if (reset) {
351
- view.dispatch({
352
- changes: {
353
- from: 0,
354
- to: view.state.doc.length,
355
- insert: ""
356
- }
357
- });
358
- }
359
- }
360
- return true;
361
- }
362
- },
363
- {
364
- key: "Shift-Enter",
365
- preventDefault: true,
366
- run: (view) => {
367
- view.dispatch({
368
- changes: {
369
- from: view.state.selection.main.head,
370
- insert: "\n"
371
- },
372
- selection: {
373
- anchor: view.state.selection.main.head + 1,
374
- head: view.state.selection.main.head + 1
375
- }
376
- });
377
- return true;
378
- }
379
- },
380
- {
381
- key: "Escape",
382
- preventDefault: true,
383
- run: (view) => {
384
- view.dispatch({
385
- changes: {
386
- from: 0,
387
- to: view.state.doc.length,
388
- insert: ""
389
- }
390
- });
391
- onCancel?.();
392
- return true;
393
- }
394
- }
395
- ]))
396
- ];
231
+ createBasicExtensions({
232
+ bracketMatching: false,
233
+ lineWrapping,
234
+ placeholder
235
+ }),
236
+ xmlFormatting(),
237
+ submit({
238
+ onSubmit
239
+ }),
240
+ extensions
241
+ ].filter(isTruthy), [
242
+ themeMode,
243
+ extensions,
244
+ onSubmit
245
+ ]);
397
246
  };
398
- var InlineSuggestionWidget = class extends WidgetType2 {
399
- suffix;
400
- constructor(suffix) {
401
- super(), this.suffix = suffix;
402
- }
403
- toDOM() {
404
- const span = document.createElement("span");
405
- span.textContent = this.suffix;
406
- span.className = "cm-inline-suggestion";
407
- return span;
247
+ var ChatEditor = /* @__PURE__ */ forwardRef(({ classNames, autoFocus, extensions: extensionsProp, lineWrapping = false, placeholder, onSubmit }, forwardRef3) => {
248
+ const extensions = useChatExtensions({
249
+ extensions: extensionsProp,
250
+ lineWrapping,
251
+ placeholder,
252
+ onSubmit
253
+ });
254
+ return /* @__PURE__ */ React2.createElement(Editor.Root, {
255
+ ref: forwardRef3
256
+ }, /* @__PURE__ */ React2.createElement(Editor.View, {
257
+ classNames,
258
+ autoFocus,
259
+ extensions
260
+ }));
261
+ });
262
+
263
+ // src/components/ChatStatus/ChatStatus.tsx
264
+ import { createContext as createContext2 } from "@radix-ui/react-context";
265
+ import React3, { forwardRef as forwardRef2, useEffect as useEffect2, useImperativeHandle, useState as useState2 } from "react";
266
+ import { mx as mx2 } from "@dxos/ui-theme";
267
+
268
+ // src/components/ChatStatus/format.ts
269
+ var SECOND = 1e3;
270
+ var MINUTE = 60 * SECOND;
271
+ var HOUR = 60 * MINUTE;
272
+ var formatElapsed = (ms) => {
273
+ const safe = Math.max(0, ms);
274
+ if (safe < MINUTE) {
275
+ return `${Math.floor(safe / SECOND)}s`;
408
276
  }
409
- eq(other) {
410
- return other.suffix === this.suffix;
277
+ if (safe < HOUR) {
278
+ const m2 = Math.floor(safe / MINUTE);
279
+ const s = Math.floor(safe % MINUTE / SECOND);
280
+ return `${m2}m ${s}s`;
411
281
  }
282
+ const h = Math.floor(safe / HOUR);
283
+ const m = Math.floor(safe % HOUR / MINUTE);
284
+ return `${h}h ${m}m`;
412
285
  };
413
286
 
414
- // src/components/ChatEditor/ChatEditor.tsx
415
- var ChatEditor = /* @__PURE__ */ forwardRef(({ classNames, extensions, references: references2, autoFocus, lineWrapping = false, placeholder, onSubmit, onSuggest, onCancel }, forwardRef2) => {
416
- var _effect = _useSignals2();
417
- try {
418
- const { themeMode } = useThemeContext();
419
- const { parentRef, view } = useTextEditor({
420
- debug: true,
421
- autoFocus,
422
- extensions: [
423
- createThemeExtensions({
424
- themeMode
425
- }),
426
- autocomplete({
427
- onSubmit,
428
- onSuggest,
429
- onCancel
430
- }),
431
- references2 ? references({
432
- provider: references2.provider
433
- }) : [],
434
- createBasicExtensions({
435
- bracketMatching: false,
436
- lineWrapping,
437
- placeholder
438
- }),
439
- extensions
440
- ].filter(isNonNullable)
441
- }, [
442
- themeMode,
443
- extensions,
444
- onSubmit,
445
- onSuggest
446
- ]);
447
- useImperativeHandle(forwardRef2, () => {
448
- return {
449
- focus: () => {
450
- view?.focus();
451
- },
452
- getText: () => view?.state.doc.toString() ?? "",
453
- setText: (text) => {
454
- view?.dispatch({
455
- changes: {
456
- from: 0,
457
- to: view.state.doc.length,
458
- insert: text
459
- },
460
- selection: {
461
- anchor: text.length,
462
- head: text.length
463
- }
464
- });
465
- }
466
- };
467
- }, [
468
- view,
469
- onSubmit
470
- ]);
471
- return /* @__PURE__ */ React2.createElement("div", {
472
- ref: parentRef,
473
- className: mx2("is-full", classNames)
474
- });
475
- } finally {
476
- _effect.f();
477
- }
287
+ // src/components/ChatStatus/ChatStatus.tsx
288
+ var TICK_MS = 1e3;
289
+ var [ChatStatusProvider, useChatStatusContext] = createContext2("ChatStatus");
290
+ var Root = /* @__PURE__ */ forwardRef2(({ classNames, children, defaultRunning = true }, forwardedRef) => {
291
+ const [elapsed, setElapsed] = useState2(0);
292
+ const [running, setRunning] = useState2(defaultRunning);
293
+ useEffect2(() => {
294
+ if (!running) {
295
+ return;
296
+ }
297
+ const startedAt = Date.now() - elapsed * TICK_MS;
298
+ let timeout;
299
+ const tick = () => {
300
+ setElapsed(Math.floor((Date.now() - startedAt) / TICK_MS));
301
+ timeout = setTimeout(tick, TICK_MS - Date.now() % TICK_MS);
302
+ };
303
+ timeout = setTimeout(tick, TICK_MS - Date.now() % TICK_MS);
304
+ return () => clearTimeout(timeout);
305
+ }, [
306
+ running
307
+ ]);
308
+ useImperativeHandle(forwardedRef, () => ({
309
+ start: () => setRunning(true),
310
+ stop: () => setRunning(false)
311
+ }), []);
312
+ return /* @__PURE__ */ React3.createElement(ChatStatusProvider, {
313
+ elapsed,
314
+ running
315
+ }, /* @__PURE__ */ React3.createElement("span", {
316
+ className: mx2("inline-flex items-center gap-2 text-description font-mono tabular-nums", classNames)
317
+ }, children));
478
318
  });
319
+ Root.displayName = "ChatChatStatus.Root";
320
+ var Icon2 = ({ classNames, children }) => {
321
+ if (children !== void 0) {
322
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, children);
323
+ }
324
+ return /* @__PURE__ */ React3.createElement("span", {
325
+ "aria-hidden": "true",
326
+ className: mx2("inline-block size-2 rounded-full bg-current animate-halo-pulse", classNames)
327
+ });
328
+ };
329
+ var Stopwatch = ({ classNames, offset = 0 }) => {
330
+ const { elapsed } = useChatStatusContext("ChatChatStatus.Stopwatch");
331
+ return /* @__PURE__ */ React3.createElement("span", {
332
+ "aria-live": "off",
333
+ className: mx2(classNames)
334
+ }, formatElapsed((elapsed + offset) * 1e3));
335
+ };
336
+ var Separator = ({ classNames }) => /* @__PURE__ */ React3.createElement("span", {
337
+ "aria-hidden": "true",
338
+ className: mx2("opacity-50", classNames)
339
+ }, "\xB7");
340
+ var Text = ({ classNames, children }) => /* @__PURE__ */ React3.createElement("span", {
341
+ className: mx2(classNames)
342
+ }, children);
343
+ var ChatStatus = {
344
+ Root,
345
+ Icon: Icon2,
346
+ Stopwatch,
347
+ Separator,
348
+ Text
349
+ };
479
350
  export {
480
351
  ChatDialog,
481
352
  ChatEditor,
482
- references
353
+ ChatStatus,
354
+ formatElapsed,
355
+ references,
356
+ useChatExtensions,
357
+ useChatStatusContext
483
358
  };
484
359
  //# sourceMappingURL=index.mjs.map