@opencode-ai/ui 1.18.1 → 1.18.3
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.
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import { type ComponentProps } from "solid-js";
|
|
1
|
+
import { type Accessor, type ComponentProps } from "solid-js";
|
|
2
2
|
export type ScrollViewThumbVisibility = "hover" | "scroll";
|
|
3
3
|
export interface ScrollViewProps extends ComponentProps<"div"> {
|
|
4
4
|
viewportRef?: (el: HTMLDivElement) => void;
|
|
5
5
|
orientation?: "vertical" | "horizontal";
|
|
6
|
+
/**
|
|
7
|
+
* `hover`: show while hovered or scrolling. `scroll`: show only while scrolling.
|
|
8
|
+
*
|
|
9
|
+
* In most cases, scrolling a container = hovering over it, so this change has no effect.
|
|
10
|
+
* This is a special case to account for the home page scroll, where scrolling a container != hovering over it
|
|
11
|
+
* */
|
|
6
12
|
thumbVisibility?: ScrollViewThumbVisibility;
|
|
13
|
+
/** Mount the thumb into an external track. Scroll metrics still come from this ScrollView. */
|
|
14
|
+
thumbContainer?: HTMLElement | Accessor<HTMLElement | undefined>;
|
|
15
|
+
/** Element whose hover reveals the thumb. Defaults to the ScrollView root when unset. */
|
|
16
|
+
thumbHoverTarget?: HTMLElement | Accessor<HTMLElement | undefined>;
|
|
7
17
|
}
|
|
8
18
|
export declare const scrollKey: (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => "end" | "home" | "page-down" | "page-up" | "up" | "down" | undefined;
|
|
9
19
|
export declare function canScrollKey(element: HTMLElement, key: NonNullable<ReturnType<typeof scrollKey>>): boolean;
|
|
@@ -16,5 +26,7 @@ export declare function scrollTopFromThumbPointer(input: {
|
|
|
16
26
|
clientHeight: number;
|
|
17
27
|
scrollHeight: number;
|
|
18
28
|
thumbHeight: number;
|
|
29
|
+
/** Viewport height used for max scroll. Defaults to `clientHeight` (track == viewport). */
|
|
30
|
+
scrollClientHeight?: number;
|
|
19
31
|
}): number;
|
|
20
32
|
export declare function ScrollView(props: ScrollViewProps): import("solid-js").JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createEffect,
|
|
3
|
+
createMemo,
|
|
4
|
+
mergeProps,
|
|
5
|
+
onCleanup,
|
|
6
|
+
onMount,
|
|
7
|
+
Show,
|
|
8
|
+
splitProps,
|
|
9
|
+
type Accessor,
|
|
10
|
+
type ComponentProps,
|
|
11
|
+
} from "solid-js"
|
|
12
|
+
import { Portal } from "solid-js/web"
|
|
2
13
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
3
14
|
import { createStore } from "solid-js/store"
|
|
4
15
|
import { useI18n } from "../context/i18n"
|
|
@@ -8,7 +19,17 @@ export type ScrollViewThumbVisibility = "hover" | "scroll"
|
|
|
8
19
|
export interface ScrollViewProps extends ComponentProps<"div"> {
|
|
9
20
|
viewportRef?: (el: HTMLDivElement) => void
|
|
10
21
|
orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb
|
|
22
|
+
/**
|
|
23
|
+
* `hover`: show while hovered or scrolling. `scroll`: show only while scrolling.
|
|
24
|
+
*
|
|
25
|
+
* In most cases, scrolling a container = hovering over it, so this change has no effect.
|
|
26
|
+
* This is a special case to account for the home page scroll, where scrolling a container != hovering over it
|
|
27
|
+
* */
|
|
11
28
|
thumbVisibility?: ScrollViewThumbVisibility
|
|
29
|
+
/** Mount the thumb into an external track. Scroll metrics still come from this ScrollView. */
|
|
30
|
+
thumbContainer?: HTMLElement | Accessor<HTMLElement | undefined>
|
|
31
|
+
/** Element whose hover reveals the thumb. Defaults to the ScrollView root when unset. */
|
|
32
|
+
thumbHoverTarget?: HTMLElement | Accessor<HTMLElement | undefined>
|
|
12
33
|
}
|
|
13
34
|
|
|
14
35
|
export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => {
|
|
@@ -65,12 +86,14 @@ export function scrollTopFromThumbPointer(input: {
|
|
|
65
86
|
clientHeight: number
|
|
66
87
|
scrollHeight: number
|
|
67
88
|
thumbHeight: number
|
|
89
|
+
/** Viewport height used for max scroll. Defaults to `clientHeight` (track == viewport). */
|
|
90
|
+
scrollClientHeight?: number
|
|
68
91
|
}) {
|
|
69
92
|
const padding = 8
|
|
70
93
|
const maxThumbTop = input.clientHeight - padding * 2 - input.thumbHeight
|
|
71
94
|
if (maxThumbTop <= 0) return 0
|
|
72
95
|
const thumbTop = Math.max(0, Math.min(input.pointer - input.viewportTop - padding - input.grabOffset, maxThumbTop))
|
|
73
|
-
return (thumbTop / maxThumbTop) * Math.max(0, input.scrollHeight - input.clientHeight)
|
|
96
|
+
return (thumbTop / maxThumbTop) * Math.max(0, input.scrollHeight - (input.scrollClientHeight ?? input.clientHeight))
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
export function ScrollView(props: ScrollViewProps) {
|
|
@@ -78,7 +101,16 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
78
101
|
const merged = mergeProps({ orientation: "vertical", thumbVisibility: "hover" }, props)
|
|
79
102
|
const [local, events, rest] = splitProps(
|
|
80
103
|
merged,
|
|
81
|
-
[
|
|
104
|
+
[
|
|
105
|
+
"class",
|
|
106
|
+
"children",
|
|
107
|
+
"viewportRef",
|
|
108
|
+
"orientation",
|
|
109
|
+
"thumbVisibility",
|
|
110
|
+
"thumbContainer",
|
|
111
|
+
"thumbHoverTarget",
|
|
112
|
+
"style",
|
|
113
|
+
],
|
|
82
114
|
[
|
|
83
115
|
"onScroll",
|
|
84
116
|
"onWheel",
|
|
@@ -96,6 +128,15 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
96
128
|
let viewportRef!: HTMLDivElement
|
|
97
129
|
let thumbRef!: HTMLDivElement
|
|
98
130
|
|
|
131
|
+
const resolveEl = (value: HTMLElement | Accessor<HTMLElement | undefined> | undefined) => {
|
|
132
|
+
if (typeof value === "function") return value()
|
|
133
|
+
return value
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const thumbMount = createMemo(() => resolveEl(local.thumbContainer))
|
|
137
|
+
const thumbHover = createMemo(() => resolveEl(local.thumbHoverTarget))
|
|
138
|
+
const hoverRoot = () => !local.thumbHoverTarget && !local.thumbContainer
|
|
139
|
+
|
|
99
140
|
const [state, setState] = createStore({
|
|
100
141
|
isHovered: false,
|
|
101
142
|
isDragging: false,
|
|
@@ -114,7 +155,6 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
114
155
|
let scrollIdleTimer: ReturnType<typeof setTimeout> | undefined
|
|
115
156
|
|
|
116
157
|
const markScrolling = () => {
|
|
117
|
-
if (local.thumbVisibility !== "scroll") return
|
|
118
158
|
setState("isScrolling", true)
|
|
119
159
|
if (scrollIdleTimer !== undefined) clearTimeout(scrollIdleTimer)
|
|
120
160
|
scrollIdleTimer = setTimeout(() => setState("isScrolling", false), 800)
|
|
@@ -122,8 +162,8 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
122
162
|
|
|
123
163
|
const thumbVisible = () => {
|
|
124
164
|
if (isDragging()) return true
|
|
125
|
-
if (
|
|
126
|
-
return isHovered()
|
|
165
|
+
if (isScrolling()) return true
|
|
166
|
+
return local.thumbVisibility === "hover" && isHovered()
|
|
127
167
|
}
|
|
128
168
|
|
|
129
169
|
onCleanup(() => {
|
|
@@ -141,7 +181,8 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
141
181
|
|
|
142
182
|
setState("showThumb", true)
|
|
143
183
|
const trackPadding = 8
|
|
144
|
-
const
|
|
184
|
+
const trackClientHeight = thumbMount()?.clientHeight || clientHeight
|
|
185
|
+
const trackHeight = trackClientHeight - trackPadding * 2
|
|
145
186
|
|
|
146
187
|
const minThumbHeight = 32
|
|
147
188
|
// Calculate raw thumb height based on ratio
|
|
@@ -165,16 +206,40 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
165
206
|
local.viewportRef(viewportRef)
|
|
166
207
|
}
|
|
167
208
|
|
|
168
|
-
createResizeObserver(
|
|
209
|
+
createResizeObserver(
|
|
210
|
+
() => [viewportRef, viewportRef.firstElementChild, thumbMount()].filter(Boolean) as HTMLElement[],
|
|
211
|
+
updateThumb,
|
|
212
|
+
)
|
|
169
213
|
|
|
170
214
|
updateThumb()
|
|
171
215
|
})
|
|
172
216
|
|
|
217
|
+
createEffect(() => {
|
|
218
|
+
thumbMount()
|
|
219
|
+
updateThumb()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
createEffect(() => {
|
|
223
|
+
const target = thumbHover()
|
|
224
|
+
if (!target) return
|
|
225
|
+
|
|
226
|
+
const enter = () => setState("isHovered", true)
|
|
227
|
+
const leave = () => setState("isHovered", false)
|
|
228
|
+
target.addEventListener("pointerenter", enter)
|
|
229
|
+
target.addEventListener("pointerleave", leave)
|
|
230
|
+
onCleanup(() => {
|
|
231
|
+
target.removeEventListener("pointerenter", enter)
|
|
232
|
+
target.removeEventListener("pointerleave", leave)
|
|
233
|
+
setState("isHovered", false)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
173
237
|
const onThumbPointerDown = (e: PointerEvent) => {
|
|
174
238
|
e.preventDefault()
|
|
175
239
|
e.stopPropagation()
|
|
176
240
|
setState("isDragging", true)
|
|
177
241
|
const grabOffset = e.clientY - thumbRef.getBoundingClientRect().top
|
|
242
|
+
const track = thumbMount() ?? viewportRef
|
|
178
243
|
|
|
179
244
|
thumbRef.setPointerCapture(e.pointerId)
|
|
180
245
|
|
|
@@ -182,9 +247,10 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
182
247
|
const { scrollHeight, clientHeight } = viewportRef
|
|
183
248
|
viewportRef.scrollTop = scrollTopFromThumbPointer({
|
|
184
249
|
pointer: e.clientY,
|
|
185
|
-
viewportTop:
|
|
250
|
+
viewportTop: track.getBoundingClientRect().top,
|
|
186
251
|
grabOffset,
|
|
187
|
-
clientHeight,
|
|
252
|
+
clientHeight: track.clientHeight,
|
|
253
|
+
scrollClientHeight: clientHeight,
|
|
188
254
|
scrollHeight,
|
|
189
255
|
thumbHeight: thumbHeight(),
|
|
190
256
|
})
|
|
@@ -203,6 +269,23 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
203
269
|
thumbRef.addEventListener("pointercancel", done)
|
|
204
270
|
}
|
|
205
271
|
|
|
272
|
+
const renderThumb = () => (
|
|
273
|
+
<div
|
|
274
|
+
ref={(el) => {
|
|
275
|
+
thumbRef = el
|
|
276
|
+
}}
|
|
277
|
+
onPointerDown={onThumbPointerDown}
|
|
278
|
+
class="scroll-view__thumb"
|
|
279
|
+
data-visible={thumbVisible()}
|
|
280
|
+
data-dragging={isDragging()}
|
|
281
|
+
style={{
|
|
282
|
+
height: `${thumbHeight()}px`,
|
|
283
|
+
transform: `translateY(${thumbTop()}px)`,
|
|
284
|
+
"z-index": 100, // ensure it displays over content
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
)
|
|
288
|
+
|
|
206
289
|
// Keybinds implementation
|
|
207
290
|
// We ensure the viewport has a tabindex so it can receive focus
|
|
208
291
|
// We can also explicitly catch PageUp/Down if we want smooth scroll or specific behavior,
|
|
@@ -253,8 +336,12 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
253
336
|
ref={rootRef}
|
|
254
337
|
class={`scroll-view ${local.class || ""}`}
|
|
255
338
|
style={local.style}
|
|
256
|
-
onPointerEnter={() =>
|
|
257
|
-
|
|
339
|
+
onPointerEnter={() => {
|
|
340
|
+
if (hoverRoot()) setState("isHovered", true)
|
|
341
|
+
}}
|
|
342
|
+
onPointerLeave={() => {
|
|
343
|
+
if (hoverRoot()) setState("isHovered", false)
|
|
344
|
+
}}
|
|
258
345
|
{...rest}
|
|
259
346
|
>
|
|
260
347
|
{/* Viewport */}
|
|
@@ -290,20 +377,11 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
290
377
|
{local.children}
|
|
291
378
|
</div>
|
|
292
379
|
|
|
293
|
-
{/* Thumb Overlay */}
|
|
380
|
+
{/* Thumb Overlay — optionally portaled into an external track */}
|
|
294
381
|
<Show when={showThumb()}>
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
class="scroll-view__thumb"
|
|
299
|
-
data-visible={thumbVisible()}
|
|
300
|
-
data-dragging={isDragging()}
|
|
301
|
-
style={{
|
|
302
|
-
height: `${thumbHeight()}px`,
|
|
303
|
-
transform: `translateY(${thumbTop()}px)`,
|
|
304
|
-
"z-index": 100, // ensure it displays over content
|
|
305
|
-
}}
|
|
306
|
-
/>
|
|
382
|
+
<Show when={thumbMount()} fallback={renderThumb()}>
|
|
383
|
+
{(mount) => <Portal mount={mount()}>{renderThumb()}</Portal>}
|
|
384
|
+
</Show>
|
|
307
385
|
</Show>
|
|
308
386
|
</div>
|
|
309
387
|
)
|