@lapismd/design-core 0.1.2 → 0.2.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE.md +3 -3
  3. package/README.md +20 -0
  4. package/package.json +24 -9
  5. package/spec/src/styling-and-themes.md +2 -0
  6. package/src/shared/ai/chat.css +23 -0
  7. package/src/shared/ai/chat.ts +12 -0
  8. package/src/shared/ai/context.svelte.ts +2 -0
  9. package/src/shared/ai/layout/Layout.svelte +116 -10
  10. package/src/shared/ai/message-list/MessageList.svelte +123 -43
  11. package/src/shared/ai/message-list/MessageListHistoryFixture.svelte +112 -0
  12. package/src/shared/ai/message-list/MessageListLegacyFixture.svelte +52 -0
  13. package/src/shared/ai/message-list/VirtualizedMessageRows.svelte +299 -0
  14. package/src/shared/ai/message-list/position.ts +7 -0
  15. package/src/shared/ai/new-messages.svelte.ts +24 -5
  16. package/src/shared/ai/stream-scroll.svelte.ts +135 -6
  17. package/src/shared/ai/tool-call-detail/ToolCallDetail.svelte +71 -0
  18. package/src/shared/ai/tool-call-detail/tool-call-detail.ts +209 -0
  19. package/src/shared/diff/core/merge/actions.ts +4 -3
  20. package/src/shared/diff/core/merge/render-model.ts +14 -10
  21. package/src/shared/diff/file-diff/FileDiff.css +68 -1
  22. package/src/shared/diff/file-diff/FileDiff.svelte +22 -6
  23. package/src/shared/diff/file-diff/FileDiffComposer.svelte +98 -16
  24. package/src/shared/diff/file-diff/index.ts +2 -0
  25. package/src/shared/diff/file-diff/types.ts +13 -0
  26. package/src/shared/diff/index.ts +2 -0
  27. package/src/shared/forms/confirm-button/ConfirmButton.svelte +38 -0
  28. package/src/shared/forms/confirm-button/ConfirmButtonExample.svelte +14 -0
  29. package/src/shared/forms/confirm-button/index.ts +1 -0
  30. package/src/shared/forms/confirm-dialog/ConfirmDialog.css +21 -0
  31. package/src/shared/forms/confirm-dialog/ConfirmDialog.svelte +116 -0
  32. package/src/shared/forms/confirm-dialog/ConfirmDialogExample.svelte +17 -0
  33. package/src/shared/forms/confirm-dialog/index.ts +2 -0
  34. package/src/shared/forms/dialog-frame/DialogFrame.css +130 -0
  35. package/src/shared/forms/dialog-frame/DialogFrame.svelte +108 -0
  36. package/src/shared/forms/dialog-frame/DialogFrameExample.svelte +37 -0
  37. package/src/shared/forms/dialog-frame/index.ts +6 -0
  38. package/src/shared/forms/dialog.ts +8 -0
  39. package/src/shared/forms/index.ts +11 -0
  40. package/src/shared/shadcn/button/button.svelte +4 -0
  41. package/src/shared/shadcn/code-block/highlightRanges.ts +10 -6
  42. package/src/shared/shadcn/code-block/syntax/defineSyntaxTheme.ts +4 -2
  43. package/src/shared/shadcn/code-block/tokenizer.ts +3 -3
  44. package/src/shared/shadcn/dialog/dialog-content.svelte +3 -4
  45. package/src/shared/shadcn/dialog/dialog-portal.svelte +10 -2
  46. package/src/shared/shadcn/tooltip/tooltip-content.svelte +10 -4
  47. package/src/shared/shadcn/tooltip/tooltip.tokens.ts +4 -0
  48. package/src/shared/shell/app-shell/AppShell.css +112 -32
  49. package/src/shared/shell/app-shell/AppShellMain.svelte +12 -1
  50. package/src/shared/shell/app-shell/AppShellRoot.svelte +27 -13
  51. package/src/shared/shell/app-shell/AppShellSidebar.svelte +121 -19
  52. package/src/shared/shell/app-shell/AppShellSidebarClose.svelte +17 -17
  53. package/src/shared/shell/app-shell/AppShellSidebarToggle.svelte +38 -58
  54. package/src/shared/shell/app-shell/AppShellSurfaceLayer.svelte +173 -0
  55. package/src/shared/shell/app-shell/AppShellSurfaceLayerBody.svelte +32 -0
  56. package/src/shared/shell/app-shell/AppShellSurfaceLayerHeader.svelte +26 -0
  57. package/src/shared/shell/app-shell/app-shell-context.svelte.ts +6 -3
  58. package/src/shared/shell/app-shell/app-shell-controller.svelte.ts +246 -2
  59. package/src/shared/shell/app-shell/app-shell-sidebar-context.svelte.ts +1 -0
  60. package/src/shared/shell/app-shell/examples/AppShellExamples.css +103 -0
  61. package/src/shared/shell/app-shell/examples/AppShellMultiPanelDemo.svelte +187 -0
  62. package/src/shared/shell/app-shell/index.ts +14 -0
  63. package/src/shared/shell/shell.tokens.css +2 -0
  64. package/src/shared/shell/shell.tokens.ts +5 -0
  65. package/src/theme.css +10 -0
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ import { Button } from "@lapismd/design-core/shadcn/button";
3
+ import Layout from "../layout/Layout.svelte";
4
+ import MessageList from "./MessageList.svelte";
5
+ import Message from "../message/Message.svelte";
6
+ import MessageBubble from "../message-bubble/MessageBubble.svelte";
7
+ import type { MessageListPosition } from "./position.js";
8
+ let items = $state(
9
+ Array.from({ length: 1000 }, (_, index) => ({
10
+ id: `message-${index}`,
11
+ text: `Message ${index}: ${"Variable-height cached conversation content. ".repeat((index % 7) + 1)}`,
12
+ })),
13
+ );
14
+ let unread = $state(false);
15
+ let expanded = $state(false);
16
+ let position = $state<MessageListPosition>();
17
+ let presentation = $state<{ key: string; position?: MessageListPosition }>({
18
+ key: "channel-a",
19
+ });
20
+ let loads = $state(0);
21
+ let key = $state("channel-a");
22
+ let delay = $state(false);
23
+ let statusUpdates = $state(0);
24
+ let positionReports = $state(0);
25
+ function append(incoming: boolean) {
26
+ items = [
27
+ ...items,
28
+ {
29
+ id: `message-${items.length}`,
30
+ text: incoming ? "Incoming message" : "My own message",
31
+ },
32
+ ];
33
+ if (incoming) unread = true;
34
+ }
35
+ </script>
36
+
37
+ <div data-story="history-controls">
38
+ <Button onclick={() => append(true)}>Receive message</Button>
39
+ <Button onclick={() => append(false)}>Send own message</Button>
40
+ <Button onclick={() => (expanded = !expanded)}>Resize content</Button>
41
+ <Button
42
+ onclick={() => (key = key === "channel-a" ? "channel-b" : "channel-a")}
43
+ >Switch conversation</Button
44
+ >
45
+ <Button onclick={() => (delay = !delay)}>Toggle delayed history</Button>
46
+ <Button onclick={() => statusUpdates++}>Update unrelated status</Button>
47
+ <output aria-label="Position reports">{positionReports}</output>
48
+ <output aria-label="History loads">{loads}</output>
49
+ <output aria-label="Reading anchor">{position?.messageId ?? "none"}</output>
50
+ </div>
51
+ <div
52
+ data-story="virtual-history-frame"
53
+ style:width={expanded ? "360px" : "640px"}
54
+ style="height: 420px; max-width: 100%; display: flex; flex-direction: column;"
55
+ >
56
+ <Layout
57
+ scrollMode="history"
58
+ conversationKey={presentation.key === key ? presentation.key : key}
59
+ hasNewMessages={unread}
60
+ onViewportChange={({ atLatest, active }) => {
61
+ if (atLatest && active) unread = false;
62
+ }}
63
+ >
64
+ {#snippet composer()}<span>Conversation composer</span>{/snippet}
65
+ <MessageList
66
+ {...{
67
+ items,
68
+ overscan: 6,
69
+ estimateSize: 112,
70
+ retainRowIds: [],
71
+ "data-status": statusUpdates,
72
+ "data-position-reports": positionReports,
73
+ }}
74
+ virtualize
75
+ conversationKey={presentation.key === key ? presentation.key : key}
76
+ onPositionChange={(next) => {
77
+ positionReports++;
78
+ position = next;
79
+ if (
80
+ JSON.stringify(presentation.position) !== JSON.stringify(next) ||
81
+ presentation.key !== key
82
+ )
83
+ presentation = { key, position: next };
84
+ }}
85
+ scrollToTopAction={async () => {
86
+ loads++;
87
+ if (delay) await new Promise((resolve) => setTimeout(resolve, 250));
88
+ const prefix = `older-${loads}`;
89
+ items = [
90
+ ...Array.from({ length: 50 }, (_, index) => ({
91
+ id: `${prefix}-${index}`,
92
+ text: `Earlier message ${index}`,
93
+ })),
94
+ ...items,
95
+ ];
96
+ }}
97
+ >
98
+ {#snippet row(item)}
99
+ <Message sender="user"
100
+ ><MessageBubble>
101
+ <p>{item.text}</p>
102
+ {#if expanded}<p>
103
+ Additional image or expanded text changes this row's height
104
+ without adding a message.
105
+ </p>{/if}
106
+ <Button size="xs" variant="ghost">Action for {item.id}</Button>
107
+ </MessageBubble></Message
108
+ >
109
+ {/snippet}
110
+ </MessageList>
111
+ </Layout>
112
+ </div>
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import { Button } from "@lapismd/design-core/shadcn/button";
3
+ import Layout from "../layout/Layout.svelte";
4
+ import MessageList from "./MessageList.svelte";
5
+ import Message from "../message/Message.svelte";
6
+ import MessageBubble from "../message-bubble/MessageBubble.svelte";
7
+ let items = $state(
8
+ Array.from({ length: 100 }, (_, index) => ({
9
+ id: `legacy-${index}`,
10
+ text: `Existing message ${index}`,
11
+ })),
12
+ );
13
+ let loads = $state(0);
14
+ let expanded = $state(false);
15
+ </script>
16
+
17
+ <Button
18
+ onclick={() =>
19
+ (items = [...items, { id: "incoming", text: "New incoming message" }])}
20
+ >Receive explicit arrival</Button
21
+ >
22
+ <Button onclick={() => (expanded = !expanded)}>Reflow existing content</Button>
23
+ <output aria-label="Legacy history requests">{loads}</output>
24
+ <div
25
+ data-story="legacy-history-frame"
26
+ style="height: 420px; width: 540px; display: flex; flex-direction: column;"
27
+ >
28
+ <Layout>
29
+ {#snippet composer()}<span>Existing composer snippet</span>{/snippet}
30
+ <MessageList
31
+ density="compact"
32
+ gap="8px"
33
+ latestMessageId={items.at(-1)?.id}
34
+ scrollToTopAction={async () => {
35
+ loads++;
36
+ items = [
37
+ { id: `earlier-${loads}`, text: `Earlier context ${loads}` },
38
+ ...items,
39
+ ];
40
+ }}
41
+ >
42
+ {#each items as item (item.id)}
43
+ <Message sender="user"
44
+ ><MessageBubble>
45
+ {item.text}
46
+ {#if expanded}<p>Reflow without a new message</p>{/if}
47
+ </MessageBubble></Message
48
+ >
49
+ {/each}
50
+ </MessageList>
51
+ </Layout>
52
+ </div>
@@ -0,0 +1,299 @@
1
+ <script lang="ts" generics="T extends { id: string }">
2
+ import {
3
+ createVirtualizer,
4
+ defaultRangeExtractor,
5
+ } from "@tanstack/svelte-virtual";
6
+ import { get } from "svelte/store";
7
+ import { onDestroy, tick, untrack, type Snippet } from "svelte";
8
+ import { useLayoutContext } from "../context.svelte.js";
9
+ import type { MessageListPosition } from "./position.js";
10
+
11
+ let {
12
+ items,
13
+ row,
14
+ overscan,
15
+ estimateSize,
16
+ conversationKey,
17
+ initialPosition,
18
+ onPositionChange,
19
+ retainRowIds,
20
+ hasOlder,
21
+ loading,
22
+ onLoadOlder,
23
+ }: {
24
+ items: readonly T[];
25
+ row: Snippet<[T, number]>;
26
+ overscan: number;
27
+ estimateSize: number;
28
+ conversationKey: string;
29
+ initialPosition?: MessageListPosition | undefined;
30
+ onPositionChange?: ((position: MessageListPosition) => void) | undefined;
31
+ retainRowIds: readonly string[];
32
+ hasOlder: boolean;
33
+ loading: boolean;
34
+ onLoadOlder?: () => Promise<void>;
35
+ } = $props();
36
+ const layout = useLayoutContext();
37
+ const stableKey = $derived(conversationKey);
38
+ const stableItems = $derived(items);
39
+ let virtualRef = $state<HTMLDivElement | null>(null);
40
+ let activeRowId = $state<string>();
41
+ let margin = $state(0);
42
+ let restoredKey: string | undefined;
43
+ let priorKey: string | undefined;
44
+ let priorItems: readonly T[] = [];
45
+ let paginationArmed = false;
46
+ let lastTop = 0;
47
+ let alive = true;
48
+ let priorConfiguration:
49
+ | {
50
+ key: string;
51
+ items: readonly T[];
52
+ margin: number;
53
+ size: number;
54
+ overscan: number;
55
+ retained: string;
56
+ }
57
+ | undefined;
58
+ onDestroy(() => {
59
+ alive = false;
60
+ });
61
+ const virtualizer = createVirtualizer<HTMLElement, HTMLDivElement>({
62
+ count: 0,
63
+ getScrollElement: () => layout?.getScrollContainer() ?? null,
64
+ estimateSize: () => estimateSize,
65
+ enabled: false,
66
+ useAnimationFrameWithResizeObserver: true,
67
+ scrollToFn: (offset, options) =>
68
+ layout?.streamScroll.scrollToOffset?.(
69
+ offset + (options.adjustments ?? 0),
70
+ true,
71
+ ),
72
+ });
73
+
74
+ $effect(() => {
75
+ layout?.setVirtualized?.(true);
76
+ return () => layout?.setVirtualized?.(false);
77
+ });
78
+
79
+ function readPosition(): MessageListPosition | undefined {
80
+ const instance = get(virtualizer);
81
+ const viewport = layout?.getScrollContainer();
82
+ if (!viewport || !items.length) return;
83
+ const first = instance
84
+ .getVirtualItems()
85
+ .find((item) => item.end > viewport.scrollTop);
86
+ const item = first && items[first.index];
87
+ if (!first || !item) return;
88
+ return {
89
+ messageId: item.id,
90
+ index: first.index,
91
+ offset: viewport.scrollTop - first.start,
92
+ atLatest: layout?.streamScroll.isLocked ?? false,
93
+ };
94
+ }
95
+
96
+ function reportPosition() {
97
+ if (
98
+ restoredKey !== conversationKey ||
99
+ document.visibilityState === "hidden"
100
+ )
101
+ return;
102
+ const position = readPosition();
103
+ if (position) untrack(() => onPositionChange?.(position));
104
+ }
105
+
106
+ function measure(node: HTMLDivElement) {
107
+ untrack(() => get(virtualizer).measureElement(node));
108
+ return {
109
+ destroy() {
110
+ untrack(() => get(virtualizer).measureElement(null));
111
+ },
112
+ };
113
+ }
114
+
115
+ // Inputs alone invalidate the virtualizer. Its returned ranges must never restart this effect.
116
+ $effect(() => {
117
+ const nextItems = stableItems,
118
+ key = stableKey,
119
+ enabled = true,
120
+ scrollMargin = margin;
121
+ const keep = [...retainRowIds, ...(activeRowId ? [activeRowId] : [])];
122
+ const extra = keep
123
+ .map((id) => nextItems.findIndex((item) => item.id === id))
124
+ .filter((index) => index >= 0);
125
+ const size = estimateSize,
126
+ scan = overscan;
127
+ untrack(() => {
128
+ const retained = JSON.stringify(extra);
129
+ if (
130
+ priorConfiguration?.key === key &&
131
+ priorConfiguration.items === nextItems &&
132
+ priorConfiguration.margin === scrollMargin &&
133
+ priorConfiguration.size === size &&
134
+ priorConfiguration.overscan === scan &&
135
+ priorConfiguration.retained === retained
136
+ )
137
+ return;
138
+ priorConfiguration = {
139
+ key,
140
+ items: nextItems,
141
+ margin: scrollMargin,
142
+ size,
143
+ overscan: scan,
144
+ retained,
145
+ };
146
+ const instance = get(virtualizer);
147
+ const viewport = layout?.getScrollContainer();
148
+ const oldRow = instance
149
+ .getVirtualItems()
150
+ .find((item) => item.end > (viewport?.scrollTop ?? 0));
151
+ const oldItem = oldRow && priorItems[oldRow.index];
152
+ const offset = oldRow ? (viewport?.scrollTop ?? 0) - oldRow.start : 0;
153
+ const revision = layout?.streamScroll.positionRevision;
154
+ const sameKey = priorKey === key;
155
+ const restore = restoredKey !== key ? initialPosition : undefined;
156
+ const preserve =
157
+ sameKey &&
158
+ nextItems !== priorItems &&
159
+ !layout?.streamScroll.isLocked &&
160
+ oldItem;
161
+ priorKey = key;
162
+ priorItems = nextItems;
163
+ if (!sameKey) {
164
+ restoredKey = undefined;
165
+ paginationArmed = false;
166
+ activeRowId = undefined;
167
+ }
168
+ instance.setOptions({
169
+ enabled,
170
+ count: nextItems.length,
171
+ scrollMargin,
172
+ getScrollElement: () => layout?.getScrollContainer() ?? null,
173
+ getItemKey: (index) => nextItems[index]!.id,
174
+ estimateSize: () => size,
175
+ overscan: scan,
176
+ rangeExtractor: (range) =>
177
+ [...new Set([...defaultRangeExtractor(range), ...extra])].sort(
178
+ (a, b) => a - b,
179
+ ),
180
+ });
181
+ void tick().then(() => {
182
+ if (!alive) return;
183
+ if (
184
+ key !== conversationKey ||
185
+ revision !== layout?.streamScroll.positionRevision
186
+ )
187
+ return;
188
+ if (enabled && nextItems.length) {
189
+ const position =
190
+ restore ??
191
+ (preserve
192
+ ? {
193
+ messageId: oldItem.id,
194
+ index: oldRow!.index,
195
+ offset,
196
+ atLatest: false,
197
+ }
198
+ : undefined);
199
+ if (position && !position.atLatest) {
200
+ const found = nextItems.findIndex(
201
+ (item) => item.id === position.messageId,
202
+ );
203
+ const index =
204
+ found < 0
205
+ ? Math.min(position.index ?? 0, nextItems.length - 1)
206
+ : found;
207
+ const start = get(virtualizer).getOffsetForIndex(
208
+ index,
209
+ "start",
210
+ )?.[0];
211
+ if (start !== undefined)
212
+ layout?.streamScroll.scrollToOffset?.(
213
+ start + position.offset,
214
+ Boolean(preserve),
215
+ );
216
+ } else if (!sameKey || layout?.streamScroll.isLocked)
217
+ layout?.streamScroll.scrollIfLocked();
218
+ }
219
+ if (nextItems.length) {
220
+ restoredKey = key;
221
+ reportPosition();
222
+ }
223
+ });
224
+ });
225
+ });
226
+ $effect(() => {
227
+ const content = virtualRef;
228
+ const viewport = layout?.getScrollContainer();
229
+ if (!content || !viewport) return;
230
+ const measureMargin = () => {
231
+ const next =
232
+ content.getBoundingClientRect().top -
233
+ viewport.getBoundingClientRect().top +
234
+ viewport.scrollTop;
235
+ if (Math.abs(next - margin) > 0.5) margin = next;
236
+ };
237
+ measureMargin();
238
+ const observer = new ResizeObserver(measureMargin);
239
+ observer.observe(content);
240
+ return () => observer.disconnect();
241
+ });
242
+ $effect(() => {
243
+ const viewport = layout?.getScrollContainer();
244
+ if (!viewport) return;
245
+ const onScroll = () => {
246
+ const top = viewport.scrollTop;
247
+ if (top > 600) paginationArmed = true;
248
+ if (
249
+ top <= 600 &&
250
+ top < lastTop &&
251
+ paginationArmed &&
252
+ hasOlder &&
253
+ !loading
254
+ ) {
255
+ paginationArmed = false;
256
+ void onLoadOlder?.();
257
+ }
258
+ lastTop = top;
259
+ reportPosition();
260
+ };
261
+ const rememberInteraction = (event: Event) => {
262
+ const target = event.target;
263
+ if (target instanceof Element)
264
+ activeRowId =
265
+ target.closest<HTMLElement>("[data-message-id]")?.dataset.messageId;
266
+ };
267
+ viewport.addEventListener("scroll", onScroll, { passive: true });
268
+ viewport.addEventListener("focusin", rememberInteraction);
269
+ viewport.addEventListener("pointerdown", rememberInteraction, {
270
+ passive: true,
271
+ });
272
+ return () => {
273
+ viewport.removeEventListener("scroll", onScroll);
274
+ viewport.removeEventListener("focusin", rememberInteraction);
275
+ viewport.removeEventListener("pointerdown", rememberInteraction);
276
+ };
277
+ });
278
+ </script>
279
+
280
+ <div
281
+ bind:this={virtualRef}
282
+ data-ui-part="virtual-content"
283
+ style:height={`${$virtualizer.getTotalSize()}px`}
284
+ >
285
+ {#each $virtualizer.getVirtualItems() as virtualRow (virtualRow.key)}
286
+ {@const item = items[virtualRow.index]}
287
+ {#if item}
288
+ <div
289
+ data-ui-part="virtual-row"
290
+ data-message-id={item.id}
291
+ data-index={virtualRow.index}
292
+ style:transform={`translateY(${virtualRow.start - margin}px)`}
293
+ use:measure
294
+ >
295
+ {@render row(item, virtualRow.index)}
296
+ </div>
297
+ {/if}
298
+ {/each}
299
+ </div>
@@ -0,0 +1,7 @@
1
+ /** A consumer-owned reading position, relative to a stable message row. */
2
+ export interface MessageListPosition {
3
+ messageId: string;
4
+ index?: number;
5
+ offset: number;
6
+ atLatest: boolean;
7
+ }
@@ -1,6 +1,10 @@
1
1
  export type NewMessagesOptions = {
2
2
  isLocked: () => boolean;
3
3
  onResize?: () => void;
4
+ /** Opt in to coalescing geometry work outside the ResizeObserver delivery. */
5
+ deferResize?: boolean | (() => boolean);
6
+ requestAnimationFrame?: typeof globalThis.requestAnimationFrame;
7
+ cancelAnimationFrame?: typeof globalThis.cancelAnimationFrame;
4
8
  createResizeObserver?: (
5
9
  callback: ResizeObserverCallback,
6
10
  ) => Pick<ResizeObserver, "observe" | "disconnect">;
@@ -19,8 +23,8 @@ export function createNewMessages(
19
23
  ): NewMessagesController {
20
24
  let element: HTMLElement | null = null;
21
25
  let observer: Pick<ResizeObserver, "observe" | "disconnect"> | null = null;
22
- let initialized = false;
23
26
  let hasNewMessages = $state(false);
27
+ let resizeFrame = 0;
24
28
  const seenMessageIds = new Set<string>();
25
29
 
26
30
  const makeObserver =
@@ -44,20 +48,35 @@ export function createNewMessages(
44
48
 
45
49
  observer = makeObserver((entries) => {
46
50
  if (!entries.some((entry) => entry.target === element)) return;
47
- if (!initialized) {
48
- initialized = true;
51
+ const deferred =
52
+ typeof options.deferResize === "function"
53
+ ? options.deferResize()
54
+ : options.deferResize;
55
+ if (!deferred) {
56
+ options.onResize?.();
49
57
  return;
50
58
  }
51
- markNew();
59
+ if (resizeFrame) return;
60
+ const current = element;
61
+ resizeFrame = (options.requestAnimationFrame ?? requestAnimationFrame)(
62
+ () => {
63
+ resizeFrame = 0;
64
+ if (element === current) options.onResize?.();
65
+ },
66
+ );
52
67
  });
53
68
  observer?.observe(element);
54
69
  }
55
70
 
56
71
  function cleanup(): void {
72
+ if (resizeFrame)
73
+ (options.cancelAnimationFrame ?? cancelAnimationFrame)(resizeFrame);
74
+ resizeFrame = 0;
57
75
  observer?.disconnect();
58
76
  observer = null;
59
77
  element = null;
60
- initialized = false;
78
+ hasNewMessages = false;
79
+ seenMessageIds.clear();
61
80
  }
62
81
 
63
82
  return {