Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Thomas G. Lopes
commited on
Commit
·
7bfbc58
1
Parent(s):
73e0644
virtualization wip
Browse files
package.json
CHANGED
@@ -52,7 +52,7 @@
|
|
52 |
"highlight.js": "^11.10.0",
|
53 |
"jiti": "^2.4.2",
|
54 |
"jsdom": "^26.0.0",
|
55 |
-
"melt": "^0.
|
56 |
"openai": "^4.90.0",
|
57 |
"playwright": "^1.52.0",
|
58 |
"postcss": "^8.4.38",
|
|
|
52 |
"highlight.js": "^11.10.0",
|
53 |
"jiti": "^2.4.2",
|
54 |
"jsdom": "^26.0.0",
|
55 |
+
"melt": "^0.40.0",
|
56 |
"openai": "^4.90.0",
|
57 |
"playwright": "^1.52.0",
|
58 |
"postcss": "^8.4.38",
|
pnpm-lock.yaml
CHANGED
@@ -136,8 +136,8 @@ importers:
|
|
136 |
specifier: ^26.0.0
|
137 |
version: 26.1.0
|
138 |
melt:
|
139 |
-
specifier: ^0.
|
140 |
-
version: 0.
|
141 |
openai:
|
142 |
specifier: ^4.90.0
|
143 |
version: 4.90.0([email protected])([email protected])
|
@@ -1833,6 +1833,9 @@ packages:
|
|
1833 | |
1834 |
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
1835 |
|
|
|
|
|
|
|
1836 | |
1837 |
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
1838 |
|
@@ -2278,8 +2281,8 @@ packages:
|
|
2278 |
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
2279 |
engines: {node: '>= 0.8'}
|
2280 |
|
2281 |
-
melt@0.
|
2282 |
-
resolution: {integrity: sha512-
|
2283 |
peerDependencies:
|
2284 |
'@floating-ui/dom': ^1.6.0
|
2285 |
svelte: ^5.30.1
|
@@ -2380,11 +2383,6 @@ packages:
|
|
2380 |
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
2381 |
hasBin: true
|
2382 |
|
2383 | |
2384 |
-
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
|
2385 |
-
engines: {node: ^18 || >=20}
|
2386 |
-
hasBin: true
|
2387 |
-
|
2388 | |
2389 |
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
2390 |
|
@@ -2990,6 +2988,9 @@ packages:
|
|
2990 |
resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
|
2991 |
engines: {node: ^14.18.0 || >=16.0.0}
|
2992 |
|
|
|
|
|
|
|
2993 | |
2994 |
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
|
2995 |
|
@@ -4954,6 +4955,10 @@ snapshots:
|
|
4954 |
|
4955 | |
4956 |
|
|
|
|
|
|
|
|
|
4957 | |
4958 |
|
4959 | |
@@ -5394,12 +5399,12 @@ snapshots:
|
|
5394 |
|
5395 | |
5396 |
|
5397 |
-
melt@0.
|
5398 |
dependencies:
|
5399 |
'@floating-ui/dom': 1.6.13
|
5400 |
dequal: 2.0.3
|
|
|
5401 |
jest-axe: 9.0.0
|
5402 |
-
nanoid: 5.1.5
|
5403 |
runed: 0.23.4([email protected])
|
5404 |
svelte: 5.38.7
|
5405 |
|
@@ -5480,8 +5485,6 @@ snapshots:
|
|
5480 |
|
5481 | |
5482 |
|
5483 |
-
[email protected]: {}
|
5484 |
-
|
5485 | |
5486 |
|
5487 | |
@@ -6120,6 +6123,8 @@ snapshots:
|
|
6120 |
'@pkgr/core': 0.1.1
|
6121 |
tslib: 2.8.1
|
6122 |
|
|
|
|
|
6123 | |
6124 |
|
6125 |
|
|
136 |
specifier: ^26.0.0
|
137 |
version: 26.1.0
|
138 |
melt:
|
139 |
+
specifier: ^0.40.0
|
140 |
+
version: 0.40.0(@floating-ui/[email protected])([email protected])
|
141 |
openai:
|
142 |
specifier: ^4.90.0
|
143 |
version: 4.90.0([email protected])([email protected])
|
|
|
1833 | |
1834 |
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
1835 |
|
1836 | |
1837 |
+
resolution: {integrity: sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==}
|
1838 |
+
|
1839 | |
1840 |
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
1841 |
|
|
|
2281 |
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
2282 |
engines: {node: '>= 0.8'}
|
2283 |
|
2284 |
+
melt@0.40.0:
|
2285 |
+
resolution: {integrity: sha512-G+urf5f2Cy62cXiPQ+nf3muscjIE9e38kjDWZckU4x08bDaP+hryJ9rrsu81V1x0ZAgEwnMAMKVAFrEdomOYWw==}
|
2286 |
peerDependencies:
|
2287 |
'@floating-ui/dom': ^1.6.0
|
2288 |
svelte: ^5.30.1
|
|
|
2383 |
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
2384 |
hasBin: true
|
2385 |
|
|
|
|
|
|
|
|
|
|
|
2386 | |
2387 |
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
2388 |
|
|
|
2988 |
resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
|
2989 |
engines: {node: ^14.18.0 || >=16.0.0}
|
2990 |
|
2991 | |
2992 |
+
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
2993 |
+
|
2994 | |
2995 |
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
|
2996 |
|
|
|
4955 |
|
4956 | |
4957 |
|
4958 | |
4959 |
+
dependencies:
|
4960 |
+
tabbable: 6.2.0
|
4961 |
+
|
4962 | |
4963 |
|
4964 | |
|
|
5399 |
|
5400 | |
5401 |
|
5402 |
+
melt@0.40.0(@floating-ui/[email protected])([email protected]):
|
5403 |
dependencies:
|
5404 |
'@floating-ui/dom': 1.6.13
|
5405 |
dequal: 2.0.3
|
5406 |
+
focus-trap: 7.6.5
|
5407 |
jest-axe: 9.0.0
|
|
|
5408 |
runed: 0.23.4([email protected])
|
5409 |
svelte: 5.38.7
|
5410 |
|
|
|
5485 |
|
5486 | |
5487 |
|
|
|
|
|
5488 | |
5489 |
|
5490 | |
|
|
6123 |
'@pkgr/core': 0.1.1
|
6124 |
tslib: 2.8.1
|
6125 |
|
6126 |
+
[email protected]: {}
|
6127 |
+
|
6128 | |
6129 |
|
6130 |
src/lib/components/inference-playground/model-selector-modal.svelte
CHANGED
@@ -2,6 +2,7 @@
|
|
2 |
import { autofocus } from "$lib/attachments/autofocus.js";
|
3 |
import type { ConversationClass } from "$lib/state/conversations.svelte";
|
4 |
import { models } from "$lib/state/models.svelte.js";
|
|
|
5 |
import type { CustomModel, Model } from "$lib/types.js";
|
6 |
import { noop } from "$lib/utils/noop.js";
|
7 |
import fuzzysearch from "$lib/utils/search.js";
|
@@ -26,6 +27,56 @@
|
|
26 |
|
27 |
let { onModelSelect, onClose, conversation }: Props = $props();
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
const combobox = new Combobox<string | undefined>({
|
30 |
onOpenChange(o) {
|
31 |
if (!o) onClose?.();
|
@@ -40,6 +91,16 @@
|
|
40 |
onModelSelect?.(modelId);
|
41 |
onClose?.();
|
42 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
});
|
44 |
$effect(() => {
|
45 |
untrack(() => combobox.highlight(conversation.model.id));
|
@@ -48,25 +109,6 @@
|
|
48 |
combobox.open = true;
|
49 |
});
|
50 |
});
|
51 |
-
|
52 |
-
let backdropEl = $state<HTMLDivElement>();
|
53 |
-
let query = $state("");
|
54 |
-
|
55 |
-
const trending = $derived(fuzzysearch({ needle: query, haystack: models.trending, property: "id" }));
|
56 |
-
const other = $derived(fuzzysearch({ needle: query, haystack: models.nonTrending, property: "id" }));
|
57 |
-
const custom = $derived(fuzzysearch({ needle: query, haystack: models.custom, property: "id" }));
|
58 |
-
|
59 |
-
function handleBackdropClick(event: MouseEvent) {
|
60 |
-
event.stopPropagation();
|
61 |
-
if (window?.getSelection()?.toString()) {
|
62 |
-
return;
|
63 |
-
}
|
64 |
-
if (event.target === backdropEl) {
|
65 |
-
onClose?.();
|
66 |
-
}
|
67 |
-
}
|
68 |
-
|
69 |
-
const isCustom = typia.createIs<CustomModel>();
|
70 |
</script>
|
71 |
|
72 |
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
@@ -91,6 +133,7 @@
|
|
91 |
class="max-h-[220px] overflow-x-hidden overflow-y-auto md:max-h-[300px]"
|
92 |
{...combobox.content}
|
93 |
popover={undefined}
|
|
|
94 |
>
|
95 |
{#snippet modelEntry(model: Model | CustomModel, trending?: boolean)}
|
96 |
{@const [nameSpace, modelName] = model.id.split("/")}
|
@@ -168,38 +211,34 @@
|
|
168 |
{/if}
|
169 |
</div>
|
170 |
{/snippet}
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
|
|
|
|
196 |
</div>
|
197 |
-
{#if other.length > 0}
|
198 |
-
<div class="px-2 py-1.5 text-xs font-medium text-gray-500">Other models</div>
|
199 |
-
{#each other as model}
|
200 |
-
{@render modelEntry(model, false)}
|
201 |
-
{/each}
|
202 |
-
{/if}
|
203 |
</div>
|
204 |
</div>
|
205 |
</div>
|
|
|
2 |
import { autofocus } from "$lib/attachments/autofocus.js";
|
3 |
import type { ConversationClass } from "$lib/state/conversations.svelte";
|
4 |
import { models } from "$lib/state/models.svelte.js";
|
5 |
+
import { VirtualScroll } from "$lib/spells/virtual-scroll.svelte.js";
|
6 |
import type { CustomModel, Model } from "$lib/types.js";
|
7 |
import { noop } from "$lib/utils/noop.js";
|
8 |
import fuzzysearch from "$lib/utils/search.js";
|
|
|
27 |
|
28 |
let { onModelSelect, onClose, conversation }: Props = $props();
|
29 |
|
30 |
+
let backdropEl = $state<HTMLDivElement>();
|
31 |
+
let query = $state("");
|
32 |
+
|
33 |
+
const trending = $derived(fuzzysearch({ needle: query, haystack: models.trending, property: "id" }));
|
34 |
+
const other = $derived(fuzzysearch({ needle: query, haystack: models.nonTrending, property: "id" }));
|
35 |
+
const custom = $derived(fuzzysearch({ needle: query, haystack: models.custom, property: "id" }));
|
36 |
+
|
37 |
+
// Combine all filtered models into sections for virtualization
|
38 |
+
type SectionItem =
|
39 |
+
| { type: "header"; content: string }
|
40 |
+
| { type: "model"; content: Model | CustomModel | "__custom__"; trending?: boolean };
|
41 |
+
|
42 |
+
const allFilteredModels = $derived.by((): SectionItem[] => {
|
43 |
+
const sections: SectionItem[] = [];
|
44 |
+
|
45 |
+
if (trending.length > 0) {
|
46 |
+
sections.push({ type: "header", content: "Trending" });
|
47 |
+
trending.forEach(model => sections.push({ type: "model", content: model, trending: true }));
|
48 |
+
}
|
49 |
+
|
50 |
+
sections.push({ type: "header", content: "Custom endpoints" });
|
51 |
+
custom.forEach(model => sections.push({ type: "model", content: model }));
|
52 |
+
sections.push({ type: "model", content: "__custom__" }); // Add custom button
|
53 |
+
|
54 |
+
if (other.length > 0) {
|
55 |
+
sections.push({ type: "header", content: "Other models" });
|
56 |
+
other.forEach(model => sections.push({ type: "model", content: model }));
|
57 |
+
}
|
58 |
+
|
59 |
+
return sections;
|
60 |
+
});
|
61 |
+
|
62 |
+
const virtualScroll = new VirtualScroll({
|
63 |
+
itemHeight: 30, // Approximate height of each item
|
64 |
+
overscan: 5,
|
65 |
+
totalItems: () => allFilteredModels.length,
|
66 |
+
});
|
67 |
+
|
68 |
+
function handleBackdropClick(event: MouseEvent) {
|
69 |
+
event.stopPropagation();
|
70 |
+
if (window?.getSelection()?.toString()) {
|
71 |
+
return;
|
72 |
+
}
|
73 |
+
if (event.target === backdropEl) {
|
74 |
+
onClose?.();
|
75 |
+
}
|
76 |
+
}
|
77 |
+
|
78 |
+
const isCustom = typia.createIs<CustomModel>();
|
79 |
+
|
80 |
const combobox = new Combobox<string | undefined>({
|
81 |
onOpenChange(o) {
|
82 |
if (!o) onClose?.();
|
|
|
91 |
onModelSelect?.(modelId);
|
92 |
onClose?.();
|
93 |
},
|
94 |
+
onNavigate(current, direction) {
|
95 |
+
const currIdx = allFilteredModels.findIndex(item => item.type === "model" && item.content === current);
|
96 |
+
// TODO: get next/prev item, scroll to it, and return its content. Make sure
|
97 |
+
// to wrap around.
|
98 |
+
if (direction === "next") {
|
99 |
+
}
|
100 |
+
if (direction === "prev") {
|
101 |
+
}
|
102 |
+
return null;
|
103 |
+
},
|
104 |
});
|
105 |
$effect(() => {
|
106 |
untrack(() => combobox.highlight(conversation.model.id));
|
|
|
109 |
combobox.open = true;
|
110 |
});
|
111 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
</script>
|
113 |
|
114 |
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
133 |
class="max-h-[220px] overflow-x-hidden overflow-y-auto md:max-h-[300px]"
|
134 |
{...combobox.content}
|
135 |
popover={undefined}
|
136 |
+
{...virtualScroll.container}
|
137 |
>
|
138 |
{#snippet modelEntry(model: Model | CustomModel, trending?: boolean)}
|
139 |
{@const [nameSpace, modelName] = model.id.split("/")}
|
|
|
211 |
{/if}
|
212 |
</div>
|
213 |
{/snippet}
|
214 |
+
|
215 |
+
<!-- Virtual scroll container -->
|
216 |
+
<div style="height: {virtualScroll.totalHeight}px; position: relative;">
|
217 |
+
<div style="transform: translateY({virtualScroll.offsetY}px);">
|
218 |
+
{#each virtualScroll.getVisibleItems(allFilteredModels) as { item }}
|
219 |
+
{#if item.type === "header"}
|
220 |
+
<div class="px-2 py-1.5 text-xs font-medium text-gray-500">{item.content}</div>
|
221 |
+
{:else if item.content === "__custom__"}
|
222 |
+
<div
|
223 |
+
class="flex w-full cursor-pointer items-center gap-2 px-2 py-1.5 text-sm text-gray-500 data-[highlighted]:bg-blue-500/15 data-[highlighted]:text-blue-600 dark:text-gray-400 dark:data-[highlighted]:text-blue-300"
|
224 |
+
{...combobox.getOption("__custom__", "custom", () => {
|
225 |
+
onClose?.();
|
226 |
+
openCustomModelConfig({
|
227 |
+
onSubmit: model => {
|
228 |
+
onModelSelect?.(model.id);
|
229 |
+
},
|
230 |
+
});
|
231 |
+
})}
|
232 |
+
>
|
233 |
+
<IconAdd class="rounded bg-blue-500/10 text-blue-600" />
|
234 |
+
Add a custom endpoint
|
235 |
+
</div>
|
236 |
+
{:else}
|
237 |
+
{@render modelEntry(item.content, item.trending)}
|
238 |
+
{/if}
|
239 |
+
{/each}
|
240 |
+
</div>
|
241 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
242 |
</div>
|
243 |
</div>
|
244 |
</div>
|
src/lib/spells/virtual-scroll.svelte.ts
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { MaybeGetter } from "$lib/types.js";
|
2 |
+
import { ElementSize } from "runed";
|
3 |
+
import { createAttachmentKey } from "svelte/attachments";
|
4 |
+
import type { HTMLAttributes } from "svelte/elements";
|
5 |
+
import { extract } from "./extract.svelte";
|
6 |
+
|
7 |
+
interface VirtualScrollOptions {
|
8 |
+
totalItems?: MaybeGetter<number>;
|
9 |
+
itemHeight: MaybeGetter<number>;
|
10 |
+
overscan?: MaybeGetter<number | undefined>;
|
11 |
+
}
|
12 |
+
|
13 |
+
export class VirtualScroll {
|
14 |
+
#options: VirtualScrollOptions;
|
15 |
+
itemHeight = $derived.by(() => extract(this.#options.itemHeight));
|
16 |
+
overscan = $derived.by(() => extract(this.#options.overscan, 10));
|
17 |
+
totalItems = $derived.by(() => extract(this.#options.totalItems, 0));
|
18 |
+
|
19 |
+
#scrollTop = $state(0);
|
20 |
+
|
21 |
+
#containerEl = $state<HTMLElement>();
|
22 |
+
#containerSize = new ElementSize(() => this.#containerEl);
|
23 |
+
|
24 |
+
constructor(options: VirtualScrollOptions) {
|
25 |
+
this.#options = options;
|
26 |
+
}
|
27 |
+
|
28 |
+
get scrollTop() {
|
29 |
+
return this.#scrollTop;
|
30 |
+
}
|
31 |
+
|
32 |
+
set scrollTop(value: number) {
|
33 |
+
this.#scrollTop = value;
|
34 |
+
}
|
35 |
+
|
36 |
+
get visibleRange() {
|
37 |
+
const startIndex = Math.floor(this.#scrollTop / this.itemHeight);
|
38 |
+
const endIndex = Math.min(
|
39 |
+
startIndex + Math.ceil(this.#containerSize.height / this.itemHeight),
|
40 |
+
this.totalItems - 1,
|
41 |
+
);
|
42 |
+
|
43 |
+
return {
|
44 |
+
start: Math.max(0, startIndex - this.overscan),
|
45 |
+
end: Math.min(this.totalItems - 1, endIndex + this.overscan),
|
46 |
+
};
|
47 |
+
}
|
48 |
+
|
49 |
+
get totalHeight() {
|
50 |
+
return this.totalItems * this.itemHeight;
|
51 |
+
}
|
52 |
+
|
53 |
+
get offsetY() {
|
54 |
+
return this.visibleRange.start * this.itemHeight;
|
55 |
+
}
|
56 |
+
|
57 |
+
getVisibleItems<T>(items: T[]): Array<{ item: T; index: number }> {
|
58 |
+
const { start, end } = this.visibleRange;
|
59 |
+
return items.slice(start, end + 1).map((item, i) => ({
|
60 |
+
item,
|
61 |
+
index: start + i,
|
62 |
+
}));
|
63 |
+
}
|
64 |
+
|
65 |
+
#attachmentKey = createAttachmentKey();
|
66 |
+
get container() {
|
67 |
+
return {
|
68 |
+
onscroll: e => {
|
69 |
+
this.scrollTop = e.currentTarget.scrollTop;
|
70 |
+
},
|
71 |
+
[this.#attachmentKey]: node => {
|
72 |
+
this.#containerEl = node;
|
73 |
+
return () => {
|
74 |
+
this.#containerEl = undefined;
|
75 |
+
};
|
76 |
+
},
|
77 |
+
} as const satisfies HTMLAttributes<HTMLElement>;
|
78 |
+
}
|
79 |
+
}
|