@assistant-ui/vue 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +90 -2
- package/dist/AuiIf.d.ts +27 -0
- package/dist/AuiIf.d.ts.map +1 -0
- package/dist/AuiIf.js +30 -0
- package/dist/AuiIf.js.map +1 -0
- package/dist/AuiProvider.d.ts +41 -0
- package/dist/AuiProvider.d.ts.map +1 -0
- package/dist/AuiProvider.js +61 -0
- package/dist/AuiProvider.js.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +17 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/primitives/AttachmentByIndexProvider.d.ts +30 -0
- package/dist/primitives/AttachmentByIndexProvider.d.ts.map +1 -0
- package/dist/primitives/AttachmentByIndexProvider.js +63 -0
- package/dist/primitives/AttachmentByIndexProvider.js.map +1 -0
- package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts +11 -0
- package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts.map +1 -0
- package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js +31 -0
- package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js.map +1 -0
- package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts +19 -0
- package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts.map +1 -0
- package/dist/primitives/ChainOfThoughtPrimitiveParts.js +85 -0
- package/dist/primitives/ChainOfThoughtPrimitiveParts.js.map +1 -0
- package/dist/primitives/ComposerPrimitiveCancel.d.ts +12 -0
- package/dist/primitives/ComposerPrimitiveCancel.d.ts.map +1 -0
- package/dist/primitives/ComposerPrimitiveCancel.js +33 -0
- package/dist/primitives/ComposerPrimitiveCancel.js.map +1 -0
- package/dist/primitives/ComposerPrimitiveInput.d.ts +26 -0
- package/dist/primitives/ComposerPrimitiveInput.d.ts.map +1 -0
- package/dist/primitives/ComposerPrimitiveInput.js +57 -0
- package/dist/primitives/ComposerPrimitiveInput.js.map +1 -0
- package/dist/primitives/ComposerPrimitiveSend.d.ts +13 -0
- package/dist/primitives/ComposerPrimitiveSend.d.ts.map +1 -0
- package/dist/primitives/ComposerPrimitiveSend.js +31 -0
- package/dist/primitives/ComposerPrimitiveSend.js.map +1 -0
- package/dist/primitives/MessageByIdProvider.d.ts +23 -0
- package/dist/primitives/MessageByIdProvider.d.ts.map +1 -0
- package/dist/primitives/MessageByIdProvider.js +70 -0
- package/dist/primitives/MessageByIdProvider.js.map +1 -0
- package/dist/primitives/MessagePrimitiveParts.d.ts +50 -0
- package/dist/primitives/MessagePrimitiveParts.d.ts.map +1 -0
- package/dist/primitives/MessagePrimitiveParts.js +93 -0
- package/dist/primitives/MessagePrimitiveParts.js.map +1 -0
- package/dist/primitives/PartByIndexProvider.d.ts +21 -0
- package/dist/primitives/PartByIndexProvider.d.ts.map +1 -0
- package/dist/primitives/PartByIndexProvider.js +50 -0
- package/dist/primitives/PartByIndexProvider.js.map +1 -0
- package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts +7 -0
- package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts.map +1 -0
- package/dist/primitives/ThreadListItemPrimitiveRoot.js +36 -0
- package/dist/primitives/ThreadListItemPrimitiveRoot.js.map +1 -0
- package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts +14 -0
- package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts.map +1 -0
- package/dist/primitives/ThreadListItemPrimitiveTrigger.js +58 -0
- package/dist/primitives/ThreadListItemPrimitiveTrigger.js.map +1 -0
- package/dist/primitives/ThreadListPrimitiveRoot.d.ts +7 -0
- package/dist/primitives/ThreadListPrimitiveRoot.d.ts.map +1 -0
- package/dist/primitives/ThreadListPrimitiveRoot.js +16 -0
- package/dist/primitives/ThreadListPrimitiveRoot.js.map +1 -0
- package/dist/primitives/ThreadPrimitiveMessages.d.ts +23 -0
- package/dist/primitives/ThreadPrimitiveMessages.d.ts.map +1 -0
- package/dist/primitives/ThreadPrimitiveMessages.js +37 -0
- package/dist/primitives/ThreadPrimitiveMessages.js.map +1 -0
- package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts +25 -0
- package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts.map +1 -0
- package/dist/primitives/ThreadPrimitiveScrollToBottom.js +43 -0
- package/dist/primitives/ThreadPrimitiveScrollToBottom.js.map +1 -0
- package/dist/primitives/ThreadPrimitiveViewport.d.ts +57 -0
- package/dist/primitives/ThreadPrimitiveViewport.d.ts.map +1 -0
- package/dist/primitives/ThreadPrimitiveViewport.js +174 -0
- package/dist/primitives/ThreadPrimitiveViewport.js.map +1 -0
- package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts +13 -0
- package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts.map +1 -0
- package/dist/primitives/ThreadPrimitiveViewportFooter.js +46 -0
- package/dist/primitives/ThreadPrimitiveViewportFooter.js.map +1 -0
- package/dist/primitives/actionBar.d.ts +41 -0
- package/dist/primitives/actionBar.d.ts.map +1 -0
- package/dist/primitives/actionBar.js +110 -0
- package/dist/primitives/actionBar.js.map +1 -0
- package/dist/primitives/attachment.d.ts +27 -0
- package/dist/primitives/attachment.d.ts.map +1 -0
- package/dist/primitives/attachment.js +63 -0
- package/dist/primitives/attachment.js.map +1 -0
- package/dist/primitives/attrDisabled.d.ts +2 -0
- package/dist/primitives/attrDisabled.d.ts.map +1 -0
- package/dist/primitives/attrDisabled.js +6 -0
- package/dist/primitives/attrDisabled.js.map +1 -0
- package/dist/primitives/branchPicker.d.ts +18 -0
- package/dist/primitives/branchPicker.d.ts.map +1 -0
- package/dist/primitives/branchPicker.js +48 -0
- package/dist/primitives/branchPicker.js.map +1 -0
- package/dist/primitives/composerAttachments.d.ts +57 -0
- package/dist/primitives/composerAttachments.d.ts.map +1 -0
- package/dist/primitives/composerAttachments.js +132 -0
- package/dist/primitives/composerAttachments.js.map +1 -0
- package/dist/primitives/error.d.ts +15 -0
- package/dist/primitives/error.d.ts.map +1 -0
- package/dist/primitives/error.js +29 -0
- package/dist/primitives/error.js.map +1 -0
- package/dist/primitives/lastValidCache.d.ts +6 -0
- package/dist/primitives/lastValidCache.d.ts.map +1 -0
- package/dist/primitives/lastValidCache.js +9 -0
- package/dist/primitives/lastValidCache.js.map +1 -0
- package/dist/primitives/message.d.ts +12 -0
- package/dist/primitives/message.d.ts.map +1 -0
- package/dist/primitives/message.js +44 -0
- package/dist/primitives/message.js.map +1 -0
- package/dist/primitives/messageAttachments.d.ts +12 -0
- package/dist/primitives/messageAttachments.d.ts.map +1 -0
- package/dist/primitives/messageAttachments.js +27 -0
- package/dist/primitives/messageAttachments.js.map +1 -0
- package/dist/primitives/stableKeys.d.ts +5 -0
- package/dist/primitives/stableKeys.d.ts.map +1 -0
- package/dist/primitives/stableKeys.js +14 -0
- package/dist/primitives/stableKeys.js.map +1 -0
- package/dist/primitives/suggestions.d.ts +66 -0
- package/dist/primitives/suggestions.d.ts.map +1 -0
- package/dist/primitives/suggestions.js +137 -0
- package/dist/primitives/suggestions.js.map +1 -0
- package/dist/primitives/thread.d.ts +11 -0
- package/dist/primitives/thread.d.ts.map +1 -0
- package/dist/primitives/thread.js +37 -0
- package/dist/primitives/thread.js.map +1 -0
- package/dist/primitives/threadList.d.ts +82 -0
- package/dist/primitives/threadList.d.ts.map +1 -0
- package/dist/primitives/threadList.js +128 -0
- package/dist/primitives/threadList.js.map +1 -0
- package/dist/primitives/threadListFocusGroup.d.ts +14 -0
- package/dist/primitives/threadListFocusGroup.d.ts.map +1 -0
- package/dist/primitives/threadListFocusGroup.js +32 -0
- package/dist/primitives/threadListFocusGroup.js.map +1 -0
- package/dist/primitives/threadListStructural.d.ts +29 -0
- package/dist/primitives/threadListStructural.d.ts.map +1 -0
- package/dist/primitives/threadListStructural.js +55 -0
- package/dist/primitives/threadListStructural.js.map +1 -0
- package/dist/primitives/useComposerSendState.d.ts +6 -0
- package/dist/primitives/useComposerSendState.d.ts.map +1 -0
- package/dist/primitives/useComposerSendState.js +15 -0
- package/dist/primitives/useComposerSendState.js.map +1 -0
- package/dist/primitives/viewportContext.d.ts +12 -0
- package/dist/primitives/viewportContext.d.ts.map +1 -0
- package/dist/primitives/viewportContext.js +6 -0
- package/dist/primitives/viewportContext.js.map +1 -0
- package/dist/useAui.d.ts +15 -0
- package/dist/useAui.d.ts.map +1 -0
- package/dist/useAui.js +21 -0
- package/dist/useAui.js.map +1 -0
- package/dist/useAuiEvent.d.ts +17 -0
- package/dist/useAuiEvent.d.ts.map +1 -0
- package/dist/useAuiEvent.js +52 -0
- package/dist/useAuiEvent.js.map +1 -0
- package/dist/useAuiState.d.ts +23 -0
- package/dist/useAuiState.d.ts.map +1 -0
- package/dist/useAuiState.js +42 -0
- package/dist/useAuiState.js.map +1 -0
- package/dist/useScrollLock.d.ts +3 -0
- package/dist/useScrollLock.d.ts.map +1 -0
- package/dist/useScrollLock.js +53 -0
- package/dist/useScrollLock.js.map +1 -0
- package/package.json +64 -3
- package/src/AuiIf.ts +34 -0
- package/src/AuiProvider.ts +90 -0
- package/src/__tests__/AuiIf.test.ts +54 -0
- package/src/__tests__/AuiProvider.test.ts +266 -0
- package/src/__tests__/byindex-shrink-race.test.ts +411 -0
- package/src/__tests__/dist-graph.test.ts +49 -0
- package/src/__tests__/fixtures.ts +51 -0
- package/src/__tests__/primitives-actionbar.test.ts +469 -0
- package/src/__tests__/primitives-attachment.test.ts +493 -0
- package/src/__tests__/primitives-content.test.ts +749 -0
- package/src/__tests__/primitives-keyed-items.test.ts +270 -0
- package/src/__tests__/primitives-keyed-messages.test.ts +264 -0
- package/src/__tests__/primitives-structural.test.ts +573 -0
- package/src/__tests__/primitives-suggestions.test.ts +374 -0
- package/src/__tests__/primitives-threadlist.test.ts +443 -0
- package/src/__tests__/primitives-tool-ui.test.ts +579 -0
- package/src/__tests__/primitives.test.ts +361 -0
- package/src/__tests__/useAuiEvent.test.ts +159 -0
- package/src/__tests__/useAuiState.test.ts +124 -0
- package/src/context.ts +31 -0
- package/src/index.ts +87 -0
- package/src/primitives/AttachmentByIndexProvider.ts +88 -0
- package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.test.ts +105 -0
- package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.ts +38 -0
- package/src/primitives/ChainOfThoughtPrimitiveParts.test.ts +254 -0
- package/src/primitives/ChainOfThoughtPrimitiveParts.ts +112 -0
- package/src/primitives/ComposerPrimitiveCancel.ts +41 -0
- package/src/primitives/ComposerPrimitiveInput.ts +64 -0
- package/src/primitives/ComposerPrimitiveSend.ts +39 -0
- package/src/primitives/MessageByIdProvider.ts +89 -0
- package/src/primitives/MessagePrimitiveParts.tool-interaction.test.ts +88 -0
- package/src/primitives/MessagePrimitiveParts.ts +166 -0
- package/src/primitives/PartByIndexProvider.ts +62 -0
- package/src/primitives/ThreadListItemPrimitiveRoot.ts +57 -0
- package/src/primitives/ThreadListItemPrimitiveTrigger.ts +74 -0
- package/src/primitives/ThreadListKeyboardNav.test.ts +298 -0
- package/src/primitives/ThreadListPrimitiveRoot.ts +18 -0
- package/src/primitives/ThreadPrimitiveMessages.ts +40 -0
- package/src/primitives/ThreadPrimitiveScrollToBottom.ts +65 -0
- package/src/primitives/ThreadPrimitiveViewport.ts +233 -0
- package/src/primitives/ThreadPrimitiveViewportFooter.test.ts +305 -0
- package/src/primitives/ThreadPrimitiveViewportFooter.ts +60 -0
- package/src/primitives/actionBar.ts +159 -0
- package/src/primitives/attachment.ts +76 -0
- package/src/primitives/attrDisabled.ts +6 -0
- package/src/primitives/branchPicker.ts +77 -0
- package/src/primitives/composerAttachments.ts +181 -0
- package/src/primitives/error.ts +36 -0
- package/src/primitives/lastValidCache.ts +12 -0
- package/src/primitives/message.ts +54 -0
- package/src/primitives/messageAttachments.ts +32 -0
- package/src/primitives/stableKeys.test.ts +19 -0
- package/src/primitives/stableKeys.ts +21 -0
- package/src/primitives/suggestions.ts +179 -0
- package/src/primitives/thread.ts +46 -0
- package/src/primitives/threadList.ts +163 -0
- package/src/primitives/threadListFocusGroup.ts +56 -0
- package/src/primitives/threadListStructural.ts +86 -0
- package/src/primitives/useComposerSendState.ts +13 -0
- package/src/primitives/viewportContext.ts +16 -0
- package/src/useAui.ts +18 -0
- package/src/useAuiEvent.ts +64 -0
- package/src/useAuiState.ts +55 -0
- package/src/useScrollLock.test.ts +207 -0
- package/src/useScrollLock.ts +79 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
defineComponent,
|
|
4
|
+
h,
|
|
5
|
+
mergeProps,
|
|
6
|
+
onScopeDispose,
|
|
7
|
+
type SlotsType,
|
|
8
|
+
type VNodeChild,
|
|
9
|
+
} from "vue";
|
|
10
|
+
import { AuiConfig, Derived } from "@assistant-ui/store/client";
|
|
11
|
+
import type { ThreadListItemMethods } from "@assistant-ui/core/store";
|
|
12
|
+
import { AuiProvider } from "../AuiProvider";
|
|
13
|
+
import { isAttrDisabled } from "./attrDisabled";
|
|
14
|
+
import { createLastValidCache, createStaleReporter } from "./lastValidCache";
|
|
15
|
+
import { useAui } from "../useAui";
|
|
16
|
+
import { useAuiState } from "../useAuiState";
|
|
17
|
+
export { ThreadListItemPrimitiveTrigger } from "./ThreadListItemPrimitiveTrigger";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scopes the subtree to the thread-list item at `index`: descendants read it
|
|
21
|
+
* through `s.threadListItem`.
|
|
22
|
+
*/
|
|
23
|
+
export const ThreadListItemByIndexProvider = defineComponent({
|
|
24
|
+
name: "ThreadListItemByIndexProvider",
|
|
25
|
+
props: {
|
|
26
|
+
index: {
|
|
27
|
+
type: Number,
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
archived: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
36
|
+
setup(props, { slots }) {
|
|
37
|
+
const aui = useAui();
|
|
38
|
+
let disposed = false;
|
|
39
|
+
onScopeDispose(() => {
|
|
40
|
+
disposed = true;
|
|
41
|
+
});
|
|
42
|
+
const config = computed(() => {
|
|
43
|
+
const index = props.index;
|
|
44
|
+
const archived = props.archived;
|
|
45
|
+
const idsOf = (state: {
|
|
46
|
+
archivedThreadIds: readonly string[];
|
|
47
|
+
threadIds: readonly string[];
|
|
48
|
+
}) => (archived ? state.archivedThreadIds : state.threadIds);
|
|
49
|
+
const cache = createLastValidCache<ThreadListItemMethods>(
|
|
50
|
+
createStaleReporter({
|
|
51
|
+
name: "ThreadListItemByIndexProvider",
|
|
52
|
+
index,
|
|
53
|
+
isCurrent: () =>
|
|
54
|
+
!disposed && index === props.index && archived === props.archived,
|
|
55
|
+
isValid: () => index < idsOf(aui.threads.getState()).length,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
return AuiConfig({
|
|
59
|
+
threadListItem: Derived({
|
|
60
|
+
source: "threads",
|
|
61
|
+
query: {
|
|
62
|
+
type: "index",
|
|
63
|
+
index,
|
|
64
|
+
archived,
|
|
65
|
+
},
|
|
66
|
+
get: (aui) =>
|
|
67
|
+
cache.resolve(index < idsOf(aui.threads.getState()).length, () =>
|
|
68
|
+
aui.threads.item({ index, archived }),
|
|
69
|
+
),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return () =>
|
|
74
|
+
h(
|
|
75
|
+
AuiProvider,
|
|
76
|
+
{ config: config.value, extends: aui },
|
|
77
|
+
{ default: () => slots.default?.() },
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Renders the default slot once per thread in the list (or per archived
|
|
84
|
+
* thread with `archived`), each instance scoped through
|
|
85
|
+
* {@link ThreadListItemByIndexProvider}.
|
|
86
|
+
*/
|
|
87
|
+
export const ThreadListPrimitiveItems = defineComponent({
|
|
88
|
+
name: "ThreadListPrimitiveItems",
|
|
89
|
+
props: {
|
|
90
|
+
archived: {
|
|
91
|
+
type: Boolean,
|
|
92
|
+
default: false,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
96
|
+
setup(props, { slots }) {
|
|
97
|
+
const threadIds = useAuiState((s) =>
|
|
98
|
+
props.archived ? s.threads.archivedThreadIds : s.threads.threadIds,
|
|
99
|
+
);
|
|
100
|
+
return () =>
|
|
101
|
+
threadIds.value.map((threadId, index) =>
|
|
102
|
+
h(
|
|
103
|
+
ThreadListItemByIndexProvider,
|
|
104
|
+
{ index, archived: props.archived, key: threadId },
|
|
105
|
+
{ default: () => slots.default?.() },
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A button that switches to a new thread. Carries `data-active` and
|
|
113
|
+
* `aria-current` while the new thread is the main one.
|
|
114
|
+
*/
|
|
115
|
+
export const ThreadListPrimitiveNew = defineComponent({
|
|
116
|
+
name: "ThreadListPrimitiveNew",
|
|
117
|
+
inheritAttrs: false,
|
|
118
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
119
|
+
setup(_, { attrs, slots }) {
|
|
120
|
+
const aui = useAui();
|
|
121
|
+
const active = useAuiState(
|
|
122
|
+
(s) => s.threads.newThreadId === s.threads.mainThreadId,
|
|
123
|
+
);
|
|
124
|
+
const onClick = (event: MouseEvent) => {
|
|
125
|
+
if (event.defaultPrevented || isAttrDisabled(attrs)) return;
|
|
126
|
+
aui.threads.switchToNewThread();
|
|
127
|
+
};
|
|
128
|
+
return () =>
|
|
129
|
+
h(
|
|
130
|
+
"button",
|
|
131
|
+
mergeProps(attrs, {
|
|
132
|
+
type: "button",
|
|
133
|
+
disabled: isAttrDisabled(attrs),
|
|
134
|
+
...(active.value && {
|
|
135
|
+
"data-active": "true",
|
|
136
|
+
"aria-current": "true",
|
|
137
|
+
}),
|
|
138
|
+
onClick,
|
|
139
|
+
}),
|
|
140
|
+
slots.default?.(),
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Renders the current thread-list item's title. When the title is empty,
|
|
147
|
+
* renders the default slot, or the `fallback` string.
|
|
148
|
+
*/
|
|
149
|
+
export const ThreadListItemPrimitiveTitle = defineComponent({
|
|
150
|
+
name: "ThreadListItemPrimitiveTitle",
|
|
151
|
+
props: {
|
|
152
|
+
fallback: {
|
|
153
|
+
type: String,
|
|
154
|
+
default: "",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
158
|
+
setup(props, { slots }) {
|
|
159
|
+
const title = useAuiState((s) => s.threadListItem.title);
|
|
160
|
+
return () =>
|
|
161
|
+
title.value || (slots.default ? slots.default() : props.fallback);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inject,
|
|
3
|
+
provide,
|
|
4
|
+
shallowRef,
|
|
5
|
+
type InjectionKey,
|
|
6
|
+
type ShallowRef,
|
|
7
|
+
} from "vue";
|
|
8
|
+
|
|
9
|
+
type ThreadListCollection = {
|
|
10
|
+
registerTrigger: (key: symbol, trigger: HTMLButtonElement) => () => void;
|
|
11
|
+
getTriggers: () => HTMLButtonElement[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ThreadListItemFocus = {
|
|
15
|
+
trigger: ShallowRef<HTMLButtonElement | null>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const threadListCollectionKey: InjectionKey<ThreadListCollection> = Symbol(
|
|
19
|
+
"assistant-ui.vue.thread-list-collection",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const threadListItemFocusKey: InjectionKey<ThreadListItemFocus> = Symbol(
|
|
23
|
+
"assistant-ui.vue.thread-list-item-focus",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const provideThreadListCollection = () => {
|
|
27
|
+
const triggers = new Map<symbol, HTMLButtonElement>();
|
|
28
|
+
const collection: ThreadListCollection = {
|
|
29
|
+
registerTrigger: (key, trigger) => {
|
|
30
|
+
triggers.set(key, trigger);
|
|
31
|
+
return () => {
|
|
32
|
+
if (triggers.get(key) === trigger) triggers.delete(key);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
getTriggers: () =>
|
|
36
|
+
[...triggers.values()].sort((first, second) => {
|
|
37
|
+
const position = first.compareDocumentPosition(second);
|
|
38
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
39
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
40
|
+
return 0;
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
provide(threadListCollectionKey, collection);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const useThreadListCollection = () =>
|
|
47
|
+
inject(threadListCollectionKey, null);
|
|
48
|
+
|
|
49
|
+
export const provideThreadListItemFocus = (): ThreadListItemFocus => {
|
|
50
|
+
const focus = { trigger: shallowRef<HTMLButtonElement | null>(null) };
|
|
51
|
+
provide(threadListItemFocusKey, focus);
|
|
52
|
+
return focus;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const useThreadListItemFocus = () =>
|
|
56
|
+
inject(threadListItemFocusKey, null);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineComponent,
|
|
3
|
+
h,
|
|
4
|
+
mergeProps,
|
|
5
|
+
type SlotsType,
|
|
6
|
+
type VNodeChild,
|
|
7
|
+
} from "vue";
|
|
8
|
+
import type { AssistantClient } from "@assistant-ui/store/client";
|
|
9
|
+
import { threadListLoadMoreDisabled } from "@assistant-ui/core/store/internal";
|
|
10
|
+
import { isAttrDisabled } from "./attrDisabled";
|
|
11
|
+
import { useAui } from "../useAui";
|
|
12
|
+
import { useAuiState } from "../useAuiState";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A button that loads more threads. Stays mounted and disabled while no
|
|
16
|
+
* more threads are available or a load is in flight.
|
|
17
|
+
*/
|
|
18
|
+
export const ThreadListPrimitiveLoadMore = defineComponent({
|
|
19
|
+
name: "ThreadListPrimitiveLoadMore",
|
|
20
|
+
inheritAttrs: false,
|
|
21
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
22
|
+
setup(_, { attrs, slots }) {
|
|
23
|
+
const aui = useAui();
|
|
24
|
+
const disabled = useAuiState(threadListLoadMoreDisabled);
|
|
25
|
+
const onClick = (event: MouseEvent) => {
|
|
26
|
+
if (event.defaultPrevented || disabled.value || isAttrDisabled(attrs))
|
|
27
|
+
return;
|
|
28
|
+
void aui.threads.loadMore();
|
|
29
|
+
};
|
|
30
|
+
return () =>
|
|
31
|
+
h(
|
|
32
|
+
"button",
|
|
33
|
+
mergeProps(attrs, {
|
|
34
|
+
type: "button",
|
|
35
|
+
disabled: disabled.value || isAttrDisabled(attrs),
|
|
36
|
+
onClick,
|
|
37
|
+
}),
|
|
38
|
+
slots.default?.(),
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const threadListItemAction = (
|
|
44
|
+
name: string,
|
|
45
|
+
action: (aui: AssistantClient) => void,
|
|
46
|
+
) =>
|
|
47
|
+
defineComponent({
|
|
48
|
+
name,
|
|
49
|
+
inheritAttrs: false,
|
|
50
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
51
|
+
setup(_, { attrs, slots }) {
|
|
52
|
+
const aui = useAui();
|
|
53
|
+
const onClick = (event: MouseEvent) => {
|
|
54
|
+
if (event.defaultPrevented || isAttrDisabled(attrs)) return;
|
|
55
|
+
action(aui);
|
|
56
|
+
};
|
|
57
|
+
return () =>
|
|
58
|
+
h(
|
|
59
|
+
"button",
|
|
60
|
+
mergeProps(attrs, {
|
|
61
|
+
type: "button",
|
|
62
|
+
disabled: isAttrDisabled(attrs),
|
|
63
|
+
onClick,
|
|
64
|
+
}),
|
|
65
|
+
slots.default?.(),
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** A button that archives the current thread list item. */
|
|
71
|
+
export const ThreadListItemPrimitiveArchive = threadListItemAction(
|
|
72
|
+
"ThreadListItemPrimitiveArchive",
|
|
73
|
+
(aui) => aui.threadListItem.archive(),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/** A button that unarchives the current thread list item. */
|
|
77
|
+
export const ThreadListItemPrimitiveUnarchive = threadListItemAction(
|
|
78
|
+
"ThreadListItemPrimitiveUnarchive",
|
|
79
|
+
(aui) => aui.threadListItem.unarchive(),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
/** A button that deletes the current thread list item. */
|
|
83
|
+
export const ThreadListItemPrimitiveDelete = threadListItemAction(
|
|
84
|
+
"ThreadListItemPrimitiveDelete",
|
|
85
|
+
(aui) => aui.threadListItem.delete(),
|
|
86
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComputedRef } from "vue";
|
|
2
|
+
import { useAui } from "../useAui";
|
|
3
|
+
import { composerSendDisabled } from "@assistant-ui/core/store/internal";
|
|
4
|
+
import { useAuiState } from "../useAuiState";
|
|
5
|
+
|
|
6
|
+
export const useComposerSendState = (): {
|
|
7
|
+
disabled: ComputedRef<boolean>;
|
|
8
|
+
send: () => void;
|
|
9
|
+
} => {
|
|
10
|
+
const aui = useAui();
|
|
11
|
+
const disabled = useAuiState(composerSendDisabled);
|
|
12
|
+
return { disabled, send: () => aui.composer.send() };
|
|
13
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InjectionKey, ShallowRef } from "vue";
|
|
2
|
+
|
|
3
|
+
export type ViewportContentInsetHandle = {
|
|
4
|
+
setHeight: (height: number) => void;
|
|
5
|
+
unregister: () => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ViewportContext = {
|
|
9
|
+
isAtBottom: ShallowRef<boolean>;
|
|
10
|
+
scrollToBottom: (behavior?: ScrollBehavior) => void;
|
|
11
|
+
registerContentInset: () => ViewportContentInsetHandle;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const viewportInjectionKey: InjectionKey<ViewportContext> = Symbol(
|
|
15
|
+
"assistant-ui.vue.viewport",
|
|
16
|
+
);
|
package/src/useAui.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AssistantClient } from "@assistant-ui/store/client";
|
|
2
|
+
import { useAuiContext } from "./context";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the `AssistantClient` provided by the nearest {@link AuiProvider}.
|
|
6
|
+
*
|
|
7
|
+
* The returned object is stable for the lifetime of the provider and always
|
|
8
|
+
* forwards to the provider's current client, so it can be captured once and
|
|
9
|
+
* used in event handlers: `aui.composer.send()`, `aui.thread.cancelRun()`.
|
|
10
|
+
* Pair with {@link useAuiState} to read reactive state and
|
|
11
|
+
* {@link useAuiEvent} to subscribe to events.
|
|
12
|
+
*
|
|
13
|
+
* Called outside a provider, the returned client's scope accessors throw a
|
|
14
|
+
* descriptive error whenever they are used.
|
|
15
|
+
*/
|
|
16
|
+
export const useAui = (): AssistantClient => {
|
|
17
|
+
return useAuiContext().aui;
|
|
18
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { onScopeDispose } from "vue";
|
|
2
|
+
import { isDevelopment } from "@assistant-ui/core/store/internal";
|
|
3
|
+
import {
|
|
4
|
+
normalizeEventSelector,
|
|
5
|
+
type AssistantClient,
|
|
6
|
+
type AssistantEventCallback,
|
|
7
|
+
type AssistantEventName,
|
|
8
|
+
type AssistantEventSelector,
|
|
9
|
+
type Unsubscribe,
|
|
10
|
+
} from "@assistant-ui/store/client";
|
|
11
|
+
import { useAuiContext } from "./context";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Subscribes to an assistant event for the lifetime of the current effect
|
|
15
|
+
* scope.
|
|
16
|
+
*
|
|
17
|
+
* The subscription follows the provider's current client across structural
|
|
18
|
+
* changes. Use `scope: "*"` to receive emissions from any descendant scope.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* useAuiEvent("thread.modelContextUpdate", ({ threadId }) => {
|
|
23
|
+
* analytics.track("model_context_update", { threadId });
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const useAuiEvent = <TEvent extends AssistantEventName>(
|
|
28
|
+
selector: AssistantEventSelector<TEvent>,
|
|
29
|
+
callback: AssistantEventCallback<TEvent>,
|
|
30
|
+
): void => {
|
|
31
|
+
const { source } = useAuiContext();
|
|
32
|
+
const { scope, event } = normalizeEventSelector(selector);
|
|
33
|
+
|
|
34
|
+
let bound: AssistantClient | undefined;
|
|
35
|
+
let off: Unsubscribe | undefined;
|
|
36
|
+
|
|
37
|
+
const bind = () => {
|
|
38
|
+
const client = source.getClient();
|
|
39
|
+
if (client === bound) return;
|
|
40
|
+
let next: Unsubscribe;
|
|
41
|
+
try {
|
|
42
|
+
next = client.on(
|
|
43
|
+
{ scope, event } as AssistantEventSelector<TEvent>,
|
|
44
|
+
callback,
|
|
45
|
+
);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// A live config may drop the selected scope; keep the current binding and
|
|
48
|
+
// retry once a later structural change makes the scope available again. A
|
|
49
|
+
// selector that never resolves (a typo'd scope) surfaces here in dev.
|
|
50
|
+
if (isDevelopment) console.warn(error);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
off?.();
|
|
54
|
+
bound = client;
|
|
55
|
+
off = next;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
bind();
|
|
59
|
+
const unsubscribe = source.subscribe(bind);
|
|
60
|
+
onScopeDispose(() => {
|
|
61
|
+
unsubscribe();
|
|
62
|
+
off?.();
|
|
63
|
+
});
|
|
64
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { computed, onScopeDispose, shallowRef, type ComputedRef } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
getProxiedAssistantState,
|
|
4
|
+
type AssistantState,
|
|
5
|
+
} from "@assistant-ui/store/client";
|
|
6
|
+
import { useAuiContext } from "./context";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Subscribes to a slice of `AssistantState` and returns it as a computed ref.
|
|
10
|
+
*
|
|
11
|
+
* The `selector` runs on every store update; dependents re-run only when the
|
|
12
|
+
* selected slice changes by `Object.is`. Returning the entire state object is
|
|
13
|
+
* not supported and throws — select a specific field instead, or compose
|
|
14
|
+
* multiple `useAuiState` calls. Returning a fresh object or array literal
|
|
15
|
+
* re-triggers dependents on every update; select primitives or a memoized
|
|
16
|
+
* reference.
|
|
17
|
+
*
|
|
18
|
+
* Scopes that may be unavailable can be read via `s.optional.<scope>`, which
|
|
19
|
+
* resolves to `undefined` instead of throwing.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const isRunning = useAuiState((s) => s.thread.isRunning);
|
|
24
|
+
* // template: <button :disabled="isRunning">…</button>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const useAuiState = <T>(
|
|
28
|
+
selector: (state: AssistantState) => T,
|
|
29
|
+
): ComputedRef<T> => {
|
|
30
|
+
const { source } = useAuiContext();
|
|
31
|
+
|
|
32
|
+
const version = shallowRef(0);
|
|
33
|
+
const unsubscribe = source.subscribe(() => {
|
|
34
|
+
version.value++;
|
|
35
|
+
});
|
|
36
|
+
onScopeDispose(unsubscribe);
|
|
37
|
+
|
|
38
|
+
return computed(() => {
|
|
39
|
+
void version.value;
|
|
40
|
+
const state = getProxiedAssistantState(source.getClient());
|
|
41
|
+
const slice = selector(state);
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
typeof slice === "object" &&
|
|
45
|
+
slice !== null &&
|
|
46
|
+
((slice as unknown) === state || (slice as unknown) === state.optional)
|
|
47
|
+
) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"You tried to return the entire AssistantState. This is not supported due to technical limitations.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return slice;
|
|
54
|
+
});
|
|
55
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { effectScope, ref } from "vue";
|
|
3
|
+
import { useScrollLock } from "./useScrollLock";
|
|
4
|
+
|
|
5
|
+
const scopes: ReturnType<typeof effectScope>[] = [];
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
scopes.forEach((scope) => scope.stop());
|
|
9
|
+
scopes.length = 0;
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
vi.unstubAllGlobals();
|
|
12
|
+
document.documentElement.removeAttribute("style");
|
|
13
|
+
document.body.removeAttribute("style");
|
|
14
|
+
document.body.replaceChildren();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function stubWidths(
|
|
18
|
+
element: HTMLElement,
|
|
19
|
+
{ offsetWidth, clientWidth }: { offsetWidth: number; clientWidth: number },
|
|
20
|
+
) {
|
|
21
|
+
Object.defineProperty(element, "offsetWidth", {
|
|
22
|
+
configurable: true,
|
|
23
|
+
value: offsetWidth,
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(element, "clientWidth", {
|
|
26
|
+
configurable: true,
|
|
27
|
+
value: clientWidth,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createLock(animated: HTMLElement) {
|
|
32
|
+
const scope = effectScope();
|
|
33
|
+
scopes.push(scope);
|
|
34
|
+
const target = ref<HTMLElement | null>(animated);
|
|
35
|
+
const lock = scope.run(() => useScrollLock(target, 200))!;
|
|
36
|
+
return { lock, target, scope };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function lockWithin(container: HTMLElement) {
|
|
40
|
+
const animated = document.createElement("div");
|
|
41
|
+
container.appendChild(animated);
|
|
42
|
+
return createLock(animated).lock;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("useScrollLock", () => {
|
|
46
|
+
it("compensates for the scrollbar it hides on an element scroller", () => {
|
|
47
|
+
const scroller = document.createElement("div");
|
|
48
|
+
scroller.style.overflowY = "auto";
|
|
49
|
+
scroller.style.borderWidth = "0px";
|
|
50
|
+
scroller.style.paddingRight = "4px";
|
|
51
|
+
document.body.appendChild(scroller);
|
|
52
|
+
stubWidths(scroller, { offsetWidth: 306, clientWidth: 300 });
|
|
53
|
+
|
|
54
|
+
lockWithin(scroller)();
|
|
55
|
+
|
|
56
|
+
expect(scroller.style.scrollbarWidth).toBe("none");
|
|
57
|
+
expect(scroller.style.paddingRight).toBe("10px");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("compensates when the scroller is the root element", () => {
|
|
61
|
+
const root = document.documentElement;
|
|
62
|
+
root.style.overflowY = "auto";
|
|
63
|
+
stubWidths(root, { offsetWidth: 1599, clientWidth: 1599 });
|
|
64
|
+
vi.stubGlobal("innerWidth", 1605);
|
|
65
|
+
|
|
66
|
+
lockWithin(document.body)();
|
|
67
|
+
|
|
68
|
+
expect(root.style.scrollbarWidth).toBe("none");
|
|
69
|
+
expect(root.style.paddingRight).toBe("6px");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("compensates when the body is the scroller rather than the viewport", () => {
|
|
73
|
+
const root = document.documentElement;
|
|
74
|
+
stubWidths(root, { offsetWidth: 1600, clientWidth: 1600 });
|
|
75
|
+
vi.stubGlobal("innerWidth", 1600);
|
|
76
|
+
const body = document.body;
|
|
77
|
+
body.style.overflowY = "auto";
|
|
78
|
+
body.style.borderWidth = "0px";
|
|
79
|
+
stubWidths(body, { offsetWidth: 1606, clientWidth: 1600 });
|
|
80
|
+
|
|
81
|
+
lockWithin(body)();
|
|
82
|
+
|
|
83
|
+
expect(body.style.paddingRight).toBe("6px");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("pads a scrolling body by its own gutter, not the viewport's", () => {
|
|
87
|
+
const root = document.documentElement;
|
|
88
|
+
stubWidths(root, { offsetWidth: 1585, clientWidth: 1585 });
|
|
89
|
+
vi.stubGlobal("innerWidth", 1600);
|
|
90
|
+
const body = document.body;
|
|
91
|
+
body.style.overflowY = "auto";
|
|
92
|
+
body.style.borderWidth = "0px";
|
|
93
|
+
stubWidths(body, { offsetWidth: 1591, clientWidth: 1585 });
|
|
94
|
+
|
|
95
|
+
lockWithin(body)();
|
|
96
|
+
|
|
97
|
+
expect(body.style.paddingRight).toBe("6px");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("restores the padding it added once the animation is over", () => {
|
|
101
|
+
vi.useFakeTimers();
|
|
102
|
+
const scroller = document.createElement("div");
|
|
103
|
+
scroller.style.overflowY = "auto";
|
|
104
|
+
scroller.style.borderWidth = "0px";
|
|
105
|
+
scroller.style.paddingRight = "4px";
|
|
106
|
+
document.body.appendChild(scroller);
|
|
107
|
+
stubWidths(scroller, { offsetWidth: 306, clientWidth: 300 });
|
|
108
|
+
|
|
109
|
+
lockWithin(scroller)();
|
|
110
|
+
expect(scroller.style.paddingRight).toBe("10px");
|
|
111
|
+
|
|
112
|
+
vi.advanceTimersByTime(200);
|
|
113
|
+
|
|
114
|
+
expect(scroller.style.paddingRight).toBe("4px");
|
|
115
|
+
expect(scroller.style.scrollbarWidth).toBe("");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("leaves the padding alone when no scrollbar takes space", () => {
|
|
119
|
+
const scroller = document.createElement("div");
|
|
120
|
+
scroller.style.overflowY = "auto";
|
|
121
|
+
scroller.style.borderWidth = "0px";
|
|
122
|
+
document.body.appendChild(scroller);
|
|
123
|
+
stubWidths(scroller, { offsetWidth: 300, clientWidth: 300 });
|
|
124
|
+
|
|
125
|
+
lockWithin(scroller)();
|
|
126
|
+
|
|
127
|
+
expect(scroller.style.paddingRight).toBe("");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("locks the current scroll container after the element is reparented", () => {
|
|
131
|
+
vi.useFakeTimers();
|
|
132
|
+
const first = document.createElement("div");
|
|
133
|
+
const second = document.createElement("div");
|
|
134
|
+
first.style.overflowY = "auto";
|
|
135
|
+
second.style.overflowY = "auto";
|
|
136
|
+
document.body.append(first, second);
|
|
137
|
+
|
|
138
|
+
const animated = document.createElement("div");
|
|
139
|
+
first.appendChild(animated);
|
|
140
|
+
const { lock, scope } = createLock(animated);
|
|
141
|
+
|
|
142
|
+
lock();
|
|
143
|
+
expect(first.style.scrollbarWidth).toBe("none");
|
|
144
|
+
|
|
145
|
+
second.appendChild(animated);
|
|
146
|
+
lock();
|
|
147
|
+
|
|
148
|
+
expect(first.style.scrollbarWidth).toBe("");
|
|
149
|
+
expect(second.style.scrollbarWidth).toBe("none");
|
|
150
|
+
|
|
151
|
+
scope.stop();
|
|
152
|
+
expect(second.style.scrollbarWidth).toBe("");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("forgets the old cleanup after moving outside a scroll container", () => {
|
|
156
|
+
const scroller = document.createElement("div");
|
|
157
|
+
const plainContainer = document.createElement("div");
|
|
158
|
+
scroller.style.overflowY = "auto";
|
|
159
|
+
document.body.append(scroller, plainContainer);
|
|
160
|
+
|
|
161
|
+
const animated = document.createElement("div");
|
|
162
|
+
scroller.appendChild(animated);
|
|
163
|
+
const { lock, scope } = createLock(animated);
|
|
164
|
+
|
|
165
|
+
lock();
|
|
166
|
+
plainContainer.appendChild(animated);
|
|
167
|
+
lock();
|
|
168
|
+
scroller.style.scrollbarWidth = "thin";
|
|
169
|
+
|
|
170
|
+
scope.stop();
|
|
171
|
+
|
|
172
|
+
expect(scroller.style.scrollbarWidth).toBe("thin");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("holds scroll position during the animation and releases it afterward", () => {
|
|
176
|
+
vi.useFakeTimers();
|
|
177
|
+
const scroller = document.createElement("div");
|
|
178
|
+
scroller.style.overflowY = "auto";
|
|
179
|
+
document.body.appendChild(scroller);
|
|
180
|
+
scroller.scrollTop = 120;
|
|
181
|
+
|
|
182
|
+
lockWithin(scroller)();
|
|
183
|
+
scroller.scrollTop = 90;
|
|
184
|
+
scroller.dispatchEvent(new Event("scroll"));
|
|
185
|
+
expect(scroller.scrollTop).toBe(120);
|
|
186
|
+
|
|
187
|
+
vi.advanceTimersByTime(200);
|
|
188
|
+
scroller.scrollTop = 90;
|
|
189
|
+
scroller.dispatchEvent(new Event("scroll"));
|
|
190
|
+
expect(scroller.scrollTop).toBe(90);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("compensates the left gutter in RTL", () => {
|
|
194
|
+
const scroller = document.createElement("div");
|
|
195
|
+
scroller.style.overflowY = "auto";
|
|
196
|
+
scroller.style.direction = "rtl";
|
|
197
|
+
scroller.style.borderWidth = "0px";
|
|
198
|
+
scroller.style.paddingLeft = "3px";
|
|
199
|
+
document.body.appendChild(scroller);
|
|
200
|
+
stubWidths(scroller, { offsetWidth: 306, clientWidth: 300 });
|
|
201
|
+
|
|
202
|
+
lockWithin(scroller)();
|
|
203
|
+
|
|
204
|
+
expect(scroller.style.paddingLeft).toBe("9px");
|
|
205
|
+
expect(scroller.style.paddingRight).toBe("");
|
|
206
|
+
});
|
|
207
|
+
});
|