@opencode-ai/ui 0.0.0-dev-19182 → 0.0.0-dev-19184

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.
@@ -2,7 +2,7 @@ import { 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
- orientation?: "vertical" | "horizontal";
5
+ orientation?: "vertical" | "horizontal" | "both";
6
6
  /**
7
7
  * `hover`: show while hovered or scrolling. `scroll`: show only while scrolling.
8
8
  *
@@ -29,4 +29,14 @@ export declare function scrollTopFromThumbPointer(input: {
29
29
  /** Viewport height used for max scroll. Defaults to `clientHeight` (track == viewport). */
30
30
  scrollClientHeight?: number;
31
31
  }): number;
32
+ export declare function scrollOffsetFromThumbPointer(input: {
33
+ pointer: number;
34
+ viewportStart: number;
35
+ grabOffset: number;
36
+ clientSize: number;
37
+ scrollSize: number;
38
+ thumbSize: number;
39
+ scrollClientSize?: number;
40
+ reverse?: boolean;
41
+ }): number;
32
42
  export declare function ScrollView(props: ScrollViewProps): import("solid-js").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-ai/ui",
3
- "version": "0.0.0-dev-19182",
3
+ "version": "0.0.0-dev-19184",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,15 +15,21 @@
15
15
  outline: none;
16
16
  }
17
17
 
18
+ .scroll-view[data-orientation="horizontal"] .scroll-view__viewport {
19
+ overflow-x: auto;
20
+ overflow-y: hidden;
21
+ }
22
+
23
+ .scroll-view[data-orientation="both"] .scroll-view__viewport {
24
+ overflow: auto;
25
+ }
26
+
18
27
  .scroll-view__viewport::-webkit-scrollbar {
19
28
  display: none;
20
29
  }
21
30
 
22
31
  .scroll-view__thumb {
23
32
  position: absolute;
24
- inset-inline-end: 0;
25
- top: 0;
26
- width: 12px;
27
33
  transition: opacity 200ms ease;
28
34
  cursor: default;
29
35
  user-select: none;
@@ -31,7 +37,19 @@
31
37
  opacity: 0;
32
38
  }
33
39
 
34
- .scroll-view__thumb::after {
40
+ .scroll-view__thumb[data-orientation="vertical"] {
41
+ inset-inline-end: 0;
42
+ top: 0;
43
+ width: 12px;
44
+ }
45
+
46
+ .scroll-view__thumb[data-orientation="horizontal"] {
47
+ left: 0;
48
+ bottom: 0;
49
+ height: 12px;
50
+ }
51
+
52
+ .scroll-view__thumb[data-orientation="vertical"]::after {
35
53
  content: "";
36
54
  position: absolute;
37
55
  left: 50%;
@@ -45,6 +63,20 @@
45
63
  transition: background-color 150ms ease;
46
64
  }
47
65
 
66
+ .scroll-view__thumb[data-orientation="horizontal"]::after {
67
+ content: "";
68
+ position: absolute;
69
+ left: 0;
70
+ right: 0;
71
+ top: 50%;
72
+ height: 4px;
73
+ transform: translateY(-50%);
74
+ border-radius: 9999px;
75
+ background-color: var(--border-weak-base);
76
+ backdrop-filter: blur(4px);
77
+ transition: background-color 150ms ease;
78
+ }
79
+
48
80
  .scroll-view__thumb:hover::after,
49
81
  .scroll-view__thumb[data-dragging="true"]::after {
50
82
  background-color: var(--border-strong-base);
@@ -8,7 +8,7 @@ export type ScrollViewThumbVisibility = "hover" | "scroll"
8
8
 
9
9
  export interface ScrollViewProps extends ComponentProps<"div"> {
10
10
  viewportRef?: (el: HTMLDivElement) => void
11
- orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb
11
+ orientation?: "vertical" | "horizontal" | "both"
12
12
  /**
13
13
  * `hover`: show while hovered or scrolling. `scroll`: show only while scrolling.
14
14
  *
@@ -78,12 +78,37 @@ export function scrollTopFromThumbPointer(input: {
78
78
  thumbHeight: number
79
79
  /** Viewport height used for max scroll. Defaults to `clientHeight` (track == viewport). */
80
80
  scrollClientHeight?: number
81
+ }) {
82
+ return scrollOffsetFromThumbPointer({
83
+ pointer: input.pointer,
84
+ viewportStart: input.viewportTop,
85
+ grabOffset: input.grabOffset,
86
+ clientSize: input.clientHeight,
87
+ scrollSize: input.scrollHeight,
88
+ thumbSize: input.thumbHeight,
89
+ scrollClientSize: input.scrollClientHeight,
90
+ })
91
+ }
92
+
93
+ export function scrollOffsetFromThumbPointer(input: {
94
+ pointer: number
95
+ viewportStart: number
96
+ grabOffset: number
97
+ clientSize: number
98
+ scrollSize: number
99
+ thumbSize: number
100
+ scrollClientSize?: number
101
+ reverse?: boolean
81
102
  }) {
82
103
  const padding = 8
83
- const maxThumbTop = input.clientHeight - padding * 2 - input.thumbHeight
84
- if (maxThumbTop <= 0) return 0
85
- const thumbTop = Math.max(0, Math.min(input.pointer - input.viewportTop - padding - input.grabOffset, maxThumbTop))
86
- return (thumbTop / maxThumbTop) * Math.max(0, input.scrollHeight - (input.scrollClientHeight ?? input.clientHeight))
104
+ const maxThumbStart = input.clientSize - padding * 2 - input.thumbSize
105
+ if (maxThumbStart <= 0) return 0
106
+ const thumbStart = Math.max(
107
+ 0,
108
+ Math.min(input.pointer - input.viewportStart - padding - input.grabOffset, maxThumbStart),
109
+ )
110
+ const progress = input.reverse ? 1 - thumbStart / maxThumbStart : thumbStart / maxThumbStart
111
+ return progress * Math.max(0, input.scrollSize - (input.scrollClientSize ?? input.clientSize))
87
112
  }
88
113
 
89
114
  export function ScrollView(props: ScrollViewProps) {
@@ -116,7 +141,8 @@ export function ScrollView(props: ScrollViewProps) {
116
141
 
117
142
  let rootRef!: HTMLDivElement
118
143
  let viewportRef!: HTMLDivElement
119
- let thumbRef!: HTMLDivElement
144
+ let verticalThumbRef!: HTMLDivElement
145
+ let horizontalThumbRef!: HTMLDivElement
120
146
 
121
147
  const thumbMount = () => local.thumbContainer
122
148
  const thumbHover = () => local.thumbHoverTarget
@@ -124,18 +150,20 @@ export function ScrollView(props: ScrollViewProps) {
124
150
 
125
151
  const [state, setState] = createStore({
126
152
  isHovered: false,
127
- isDragging: false,
153
+ dragging: undefined as "vertical" | "horizontal" | undefined,
128
154
  isScrolling: false,
129
- thumbHeight: 0,
130
- thumbTop: 0,
131
- showThumb: false,
155
+ verticalThumbSize: 0,
156
+ verticalThumbStart: 0,
157
+ showVerticalThumb: false,
158
+ horizontalThumbSize: 0,
159
+ horizontalThumbStart: 0,
160
+ showHorizontalThumb: false,
132
161
  })
133
162
  const isHovered = () => state.isHovered
134
- const isDragging = () => state.isDragging
163
+ const isDragging = () => state.dragging !== undefined
135
164
  const isScrolling = () => state.isScrolling
136
- const thumbHeight = () => state.thumbHeight
137
- const thumbTop = () => state.thumbTop
138
- const showThumb = () => state.showThumb
165
+ const vertical = () => local.orientation === "vertical" || local.orientation === "both"
166
+ const horizontal = () => local.orientation === "horizontal" || local.orientation === "both"
139
167
 
140
168
  let scrollIdleTimer: ReturnType<typeof setTimeout> | undefined
141
169
 
@@ -157,33 +185,42 @@ export function ScrollView(props: ScrollViewProps) {
157
185
 
158
186
  const updateThumb = () => {
159
187
  if (!viewportRef) return
160
- const { scrollTop, scrollHeight, clientHeight } = viewportRef
161
-
162
- if (scrollHeight <= clientHeight || scrollHeight === 0) {
163
- setState("showThumb", false)
164
- return
165
- }
166
-
167
- setState("showThumb", true)
168
188
  const trackPadding = 8
169
- const trackClientHeight = thumbMount()?.clientHeight || clientHeight
170
- const trackHeight = trackClientHeight - trackPadding * 2
171
-
172
- const minThumbHeight = 32
173
- // Calculate raw thumb height based on ratio
174
- let height = (clientHeight / scrollHeight) * trackHeight
175
- height = Math.max(height, minThumbHeight)
176
-
177
- const maxScrollTop = scrollHeight - clientHeight
178
- const maxThumbTop = trackHeight - height
179
-
180
- const top = maxScrollTop > 0 ? (scrollTop / maxScrollTop) * maxThumbTop : 0
181
-
182
- // Ensure thumb stays within bounds (shouldn't be necessary due to math above, but good for safety)
183
- const boundedTop = trackPadding + Math.max(0, Math.min(top, maxThumbTop))
189
+ const minThumbSize = 32
190
+
191
+ if (vertical()) {
192
+ const trackSize = Math.max(0, (thumbMount()?.clientHeight || viewportRef.clientHeight) - trackPadding * 2)
193
+ const size = trackSize
194
+ ? Math.min(trackSize, Math.max((viewportRef.clientHeight / viewportRef.scrollHeight) * trackSize, minThumbSize))
195
+ : 0
196
+ const maxScroll = viewportRef.scrollHeight - viewportRef.clientHeight
197
+ const maxStart = trackSize - size
198
+ setState("showVerticalThumb", maxScroll > 0)
199
+ setState("verticalThumbSize", size)
200
+ setState(
201
+ "verticalThumbStart",
202
+ trackPadding + (maxScroll > 0 ? (viewportRef.scrollTop / maxScroll) * maxStart : 0),
203
+ )
204
+ } else {
205
+ setState("showVerticalThumb", false)
206
+ }
184
207
 
185
- setState("thumbHeight", height)
186
- setState("thumbTop", boundedTop)
208
+ if (horizontal()) {
209
+ const trackSize = Math.max(0, (thumbMount()?.clientWidth || viewportRef.clientWidth) - trackPadding * 2)
210
+ const size = trackSize
211
+ ? Math.min(trackSize, Math.max((viewportRef.clientWidth / viewportRef.scrollWidth) * trackSize, minThumbSize))
212
+ : 0
213
+ const maxScroll = viewportRef.scrollWidth - viewportRef.clientWidth
214
+ const maxStart = trackSize - size
215
+ const rtl = getComputedStyle(viewportRef).direction === "rtl"
216
+ const offset = Math.max(0, Math.min(rtl ? -viewportRef.scrollLeft : viewportRef.scrollLeft, maxScroll))
217
+ const start = maxScroll > 0 ? (offset / maxScroll) * maxStart : 0
218
+ setState("showHorizontalThumb", maxScroll > 0)
219
+ setState("horizontalThumbSize", size)
220
+ setState("horizontalThumbStart", trackPadding + (rtl ? maxStart - start : start))
221
+ } else {
222
+ setState("showHorizontalThumb", false)
223
+ }
187
224
  }
188
225
 
189
226
  onMount(() => {
@@ -204,6 +241,13 @@ export function ScrollView(props: ScrollViewProps) {
204
241
  updateThumb()
205
242
  })
206
243
 
244
+ createEffect(() => {
245
+ if (!horizontal() || !viewportRef) return
246
+ const observer = new MutationObserver(updateThumb)
247
+ observer.observe(viewportRef, { childList: true, subtree: true, characterData: true })
248
+ onCleanup(() => observer.disconnect())
249
+ })
250
+
207
251
  createEffect(() => {
208
252
  const target = thumbHover()
209
253
  if (!target) return
@@ -219,58 +263,88 @@ export function ScrollView(props: ScrollViewProps) {
219
263
  })
220
264
  })
221
265
 
222
- const onThumbPointerDown = (e: PointerEvent) => {
266
+ const onThumbPointerDown = (axis: "vertical" | "horizontal", e: PointerEvent) => {
223
267
  e.preventDefault()
224
268
  e.stopPropagation()
225
- setState("isDragging", true)
226
- const grabOffset = e.clientY - thumbRef.getBoundingClientRect().top
269
+ setState("dragging", axis)
270
+ const thumb = axis === "vertical" ? verticalThumbRef : horizontalThumbRef
271
+ const grabOffset =
272
+ axis === "vertical"
273
+ ? e.clientY - thumb.getBoundingClientRect().top
274
+ : e.clientX - thumb.getBoundingClientRect().left
227
275
  const track = thumbMount() ?? viewportRef
228
276
 
229
- thumbRef.setPointerCapture(e.pointerId)
277
+ thumb.setPointerCapture(e.pointerId)
230
278
 
231
279
  const onPointerMove = (e: PointerEvent) => {
232
- const { scrollHeight, clientHeight } = viewportRef
233
- viewportRef.scrollTop = scrollTopFromThumbPointer({
234
- pointer: e.clientY,
235
- viewportTop: track.getBoundingClientRect().top,
280
+ const vertical = axis === "vertical"
281
+ const rtl = !vertical && getComputedStyle(viewportRef).direction === "rtl"
282
+ const offset = scrollOffsetFromThumbPointer({
283
+ pointer: vertical ? e.clientY : e.clientX,
284
+ viewportStart: vertical ? track.getBoundingClientRect().top : track.getBoundingClientRect().left,
236
285
  grabOffset,
237
- clientHeight: track.clientHeight,
238
- scrollClientHeight: clientHeight,
239
- scrollHeight,
240
- thumbHeight: thumbHeight(),
286
+ clientSize: vertical ? track.clientHeight : track.clientWidth,
287
+ scrollClientSize: vertical ? viewportRef.clientHeight : viewportRef.clientWidth,
288
+ scrollSize: vertical ? viewportRef.scrollHeight : viewportRef.scrollWidth,
289
+ thumbSize: vertical ? state.verticalThumbSize : state.horizontalThumbSize,
290
+ reverse: rtl,
241
291
  })
292
+ if (vertical) {
293
+ viewportRef.scrollTop = offset
294
+ return
295
+ }
296
+ viewportRef.scrollLeft = rtl ? -offset : offset
242
297
  }
243
298
 
244
299
  const done = (e: PointerEvent) => {
245
- setState("isDragging", false)
246
- thumbRef.releasePointerCapture(e.pointerId)
247
- thumbRef.removeEventListener("pointermove", onPointerMove)
248
- thumbRef.removeEventListener("pointerup", done)
249
- thumbRef.removeEventListener("pointercancel", done)
300
+ setState("dragging", undefined)
301
+ thumb.releasePointerCapture(e.pointerId)
302
+ thumb.removeEventListener("pointermove", onPointerMove)
303
+ thumb.removeEventListener("pointerup", done)
304
+ thumb.removeEventListener("pointercancel", done)
250
305
  }
251
306
 
252
- thumbRef.addEventListener("pointermove", onPointerMove)
253
- thumbRef.addEventListener("pointerup", done)
254
- thumbRef.addEventListener("pointercancel", done)
307
+ thumb.addEventListener("pointermove", onPointerMove)
308
+ thumb.addEventListener("pointerup", done)
309
+ thumb.addEventListener("pointercancel", done)
255
310
  }
256
311
 
257
- const renderThumb = () => (
312
+ const renderVerticalThumb = () => (
258
313
  <div
259
314
  ref={(el) => {
260
- thumbRef = el
315
+ verticalThumbRef = el
261
316
  }}
262
- onPointerDown={onThumbPointerDown}
317
+ onPointerDown={(event) => onThumbPointerDown("vertical", event)}
263
318
  class="scroll-view__thumb"
319
+ data-orientation="vertical"
264
320
  data-visible={thumbVisible()}
265
- data-dragging={isDragging()}
321
+ data-dragging={state.dragging === "vertical"}
266
322
  style={{
267
- height: `${thumbHeight()}px`,
268
- transform: `translateY(${thumbTop()}px)`,
323
+ height: `${state.verticalThumbSize}px`,
324
+ transform: `translateY(${state.verticalThumbStart}px)`,
269
325
  "z-index": 100, // ensure it displays over content
270
326
  }}
271
327
  />
272
328
  )
273
329
 
330
+ const renderHorizontalThumb = () => (
331
+ <div
332
+ ref={(el) => {
333
+ horizontalThumbRef = el
334
+ }}
335
+ onPointerDown={(event) => onThumbPointerDown("horizontal", event)}
336
+ class="scroll-view__thumb"
337
+ data-orientation="horizontal"
338
+ data-visible={thumbVisible()}
339
+ data-dragging={state.dragging === "horizontal"}
340
+ style={{
341
+ width: `${state.horizontalThumbSize}px`,
342
+ transform: `translateX(${state.horizontalThumbStart}px)`,
343
+ "z-index": 100,
344
+ }}
345
+ />
346
+ )
347
+
274
348
  // Keybinds implementation
275
349
  // We ensure the viewport has a tabindex so it can receive focus
276
350
  // We can also explicitly catch PageUp/Down if we want smooth scroll or specific behavior,
@@ -320,6 +394,7 @@ export function ScrollView(props: ScrollViewProps) {
320
394
  <div
321
395
  ref={rootRef}
322
396
  class={`scroll-view ${local.class || ""}`}
397
+ data-orientation={local.orientation}
323
398
  style={local.style}
324
399
  onPointerEnter={() => {
325
400
  if (hoverRoot()) setState("isHovered", true)
@@ -363,9 +438,14 @@ export function ScrollView(props: ScrollViewProps) {
363
438
  </div>
364
439
 
365
440
  {/* Thumb Overlay — optionally portaled into an external track */}
366
- <Show when={showThumb()}>
367
- <Show when={thumbMount()} fallback={renderThumb()}>
368
- {(mount) => <Portal mount={mount()}>{renderThumb()}</Portal>}
441
+ <Show when={state.showVerticalThumb}>
442
+ <Show when={thumbMount()} fallback={renderVerticalThumb()}>
443
+ {(mount) => <Portal mount={mount()}>{renderVerticalThumb()}</Portal>}
444
+ </Show>
445
+ </Show>
446
+ <Show when={state.showHorizontalThumb}>
447
+ <Show when={thumbMount()} fallback={renderHorizontalThumb()}>
448
+ {(mount) => <Portal mount={mount()}>{renderHorizontalThumb()}</Portal>}
369
449
  </Show>
370
450
  </Show>
371
451
  </div>
@@ -1,20 +1,30 @@
1
+ @property --file-tree-v2-row-overlay {
2
+ syntax: "<color>";
3
+ inherits: true;
4
+ initial-value: transparent;
5
+ }
6
+
1
7
  [data-component="file-tree-v2"] {
2
8
  display: flex;
3
9
  flex-direction: column;
4
10
  gap: 2px;
11
+ width: max-content;
12
+ min-width: 100%;
5
13
  }
6
14
 
7
15
  [data-component="file-tree-v2"] [data-slot="file-tree-v2-row"] {
16
+ --file-tree-v2-row-overlay: transparent;
17
+
8
18
  box-sizing: border-box;
9
19
  width: 100%;
10
- min-width: 0;
20
+ min-width: max-content;
11
21
  height: 28px;
12
22
  display: flex;
13
23
  align-items: center;
14
24
  justify-content: flex-start;
15
25
  gap: 6px;
16
26
  padding-inline-end: 8px;
17
- overflow: visible;
27
+ overflow: clip;
18
28
  border: none;
19
29
  border-radius: 6px;
20
30
  background-color: transparent;
@@ -24,7 +34,12 @@
24
34
  scroll-margin-block: 8px;
25
35
  transition:
26
36
  background-color 120ms ease,
27
- color 120ms ease;
37
+ color 120ms ease,
38
+ --file-tree-v2-row-overlay 120ms ease;
39
+ }
40
+
41
+ [data-component="file-tree-v2"] [data-slot="file-tree-v2-label"] {
42
+ margin-inline-end: 12px;
28
43
  }
29
44
 
30
45
  [data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-ignored] {
@@ -32,10 +47,14 @@
32
47
  }
33
48
 
34
49
  [data-component="file-tree-v2"] [data-slot="file-tree-v2-row"]:hover {
50
+ --file-tree-v2-row-overlay: var(--v2-overlay-simple-overlay-hover);
51
+
35
52
  background-color: var(--v2-overlay-simple-overlay-hover);
36
53
  }
37
54
 
38
55
  [data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-selected] {
56
+ --file-tree-v2-row-overlay: var(--v2-overlay-simple-overlay-pressed);
57
+
39
58
  color: var(--v2-text-text-base);
40
59
  background-color: var(--v2-overlay-simple-overlay-pressed);
41
60
  }
@@ -44,6 +63,14 @@
44
63
  background-color: var(--v2-overlay-simple-overlay-pressed);
45
64
  }
46
65
 
66
+ [data-component="file-tree-v2"]
67
+ [data-slot="file-tree-v2-context-trigger"][data-context-menu-open]
68
+ [data-slot="file-tree-v2-row"]:not([data-selected]) {
69
+ --file-tree-v2-row-overlay: var(--v2-overlay-simple-overlay-hover);
70
+
71
+ background-color: var(--v2-overlay-simple-overlay-hover);
72
+ }
73
+
47
74
  [data-component="file-tree-v2"] [data-slot="file-tree-v2-guide"] {
48
75
  position: absolute;
49
76
  top: 0;
@@ -110,6 +137,9 @@
110
137
  [data-component="file-tree-v2"] [data-slot="file-tree-v2-change"] {
111
138
  box-sizing: border-box;
112
139
  flex: none;
140
+ position: sticky;
141
+ inset-inline-end: 8px;
142
+ z-index: 1;
113
143
  display: flex;
114
144
  flex-direction: row;
115
145
  align-items: center;
@@ -126,6 +156,37 @@
126
156
  font-feature-settings:
127
157
  "tnum" on,
128
158
  "lnum" on;
159
+ background:
160
+ linear-gradient(var(--file-tree-v2-row-overlay), var(--file-tree-v2-row-overlay)), var(--v2-background-bg-base);
161
+ }
162
+
163
+ [data-component="file-tree-v2"] [data-slot="file-tree-v2-change"]::before {
164
+ content: "";
165
+ position: absolute;
166
+ inset-block: 0;
167
+ inset-inline-end: 100%;
168
+ width: 16px;
169
+ pointer-events: none;
170
+ background:
171
+ linear-gradient(to right, transparent, var(--file-tree-v2-row-overlay)),
172
+ linear-gradient(to right, transparent, var(--v2-background-bg-base));
173
+ }
174
+
175
+ [data-component="file-tree-v2"]:dir(rtl) [data-slot="file-tree-v2-change"]::before {
176
+ background:
177
+ linear-gradient(to left, transparent, var(--file-tree-v2-row-overlay)),
178
+ linear-gradient(to left, transparent, var(--v2-background-bg-base));
179
+ }
180
+
181
+ [data-component="file-tree-v2"] [data-slot="file-tree-v2-change"]::after {
182
+ content: "";
183
+ position: absolute;
184
+ inset-block: 0;
185
+ inset-inline-start: 100%;
186
+ width: 16px;
187
+ pointer-events: none;
188
+ background:
189
+ linear-gradient(var(--file-tree-v2-row-overlay), var(--file-tree-v2-row-overlay)), var(--v2-background-bg-base);
129
190
  }
130
191
 
131
192
  [data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="modified"] {