@opencode/ui 2.0.23 → 2.0.25
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/dist/context/marked-parser.d.ts +2 -0
- package/dist/i18n/en.d.ts +1 -0
- package/dist/icons/catalog.d.ts +36 -0
- package/dist/navigation/segmented-control/segmented-control.d.ts +21 -2
- package/package.json +2 -1
- package/src/actions/button/button.css +11 -9
- package/src/actions/button/button.tsx +1 -0
- package/src/actions/icon-button/icon-button.css +1 -5
- package/src/actions/icon-button/icon-button.tsx +1 -0
- package/src/actions/split-button/split-button.tsx +3 -0
- package/src/actions/submit.css +14 -12
- package/src/components/animated-number.tsx +13 -0
- package/src/components/app-icon.tsx +2 -0
- package/src/components/card.tsx +21 -0
- package/src/components/collapsible.tsx +1 -0
- package/src/components/context-menu.tsx +34 -0
- package/src/components/dock-surface.tsx +3 -0
- package/src/components/file-icon.tsx +12 -0
- package/src/components/image-preview.tsx +1 -0
- package/src/components/list.tsx +32 -0
- package/src/components/motion-spring.tsx +5 -0
- package/src/components/popover.tsx +12 -0
- package/src/components/provider-icon.tsx +1 -0
- package/src/components/resize-handle.tsx +8 -0
- package/src/components/scroll-view.tsx +39 -0
- package/src/components/spinner.tsx +2 -0
- package/src/components/text-field.tsx +6 -0
- package/src/components/text-reveal.tsx +19 -0
- package/src/components/text-strikethrough.tsx +8 -0
- package/src/components/typewriter.tsx +4 -0
- package/src/context/dialog.tsx +12 -0
- package/src/context/file.tsx +1 -0
- package/src/context/helper.tsx +4 -0
- package/src/context/i18n.tsx +20 -0
- package/src/context/index.ts +3 -0
- package/src/context/marked-base.tsx +9 -1
- package/src/context/marked-parser.tsx +57 -18
- package/src/context/marked.tsx +3 -0
- package/src/context/worker-pool.tsx +2 -0
- package/src/data-display/avatar/avatar.tsx +4 -0
- package/src/data-display/badge/badge.tsx +1 -0
- package/src/data-display/diff-changes/diff-changes.tsx +2 -0
- package/src/data-display/keybind/keybind.tsx +1 -0
- package/src/data-display/line-comment/line-comment.tsx +22 -0
- package/src/data-display/project-avatar/project-avatar.tsx +10 -0
- package/src/feedback/loader/loader.tsx +1 -0
- package/src/feedback/toast/description.ts +1 -0
- package/src/forms/checkbox/checkbox.tsx +1 -0
- package/src/forms/field/field.tsx +8 -0
- package/src/forms/inline-input/inline-input.tsx +4 -0
- package/src/forms/radio/radio.tsx +2 -0
- package/src/forms/select/select.tsx +11 -0
- package/src/forms/switch/switch.tsx +1 -0
- package/src/forms/text-input/text-input.tsx +3 -0
- package/src/hooks/create-auto-scroll.tsx +34 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-filtered-list.tsx +14 -0
- package/src/i18n/en.ts +1 -0
- package/src/i18n/no.ts +1 -0
- package/src/icons/catalog.ts +36 -0
- package/src/icons/icon/icon.tsx +2 -0
- package/src/layout/divider/divider.tsx +1 -0
- package/src/navigation/menu/menu.tsx +13 -0
- package/src/navigation/segmented-control/segmented-control.css +37 -4
- package/src/navigation/segmented-control/segmented-control.tsx +210 -14
- package/src/navigation/tab-state-indicator.tsx +1 -0
- package/src/navigation/tabs/tabs-current.css +1 -5
- package/src/navigation/tabs/tabs.css +5 -22
- package/src/navigation/tabs/tabs.tsx +18 -0
- package/src/overlays/dialog/dialog.tsx +3 -0
- package/src/overlays/tooltip/tooltip.tsx +15 -0
- package/src/storybook/scaffold.tsx +3 -0
- package/src/styles/theme.css +1 -1
- package/src/theme/color.ts +27 -0
- package/src/theme/context.tsx +58 -0
- package/src/theme/default-themes.ts +35 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/loader.ts +11 -0
- package/src/theme/resolve.ts +28 -0
- package/src/theme/v2/foreground.ts +8 -0
- package/src/theme/v2/resolve.ts +10 -0
- package/src/typography/text-shimmer/text-shimmer.tsx +1 -0
package/src/components/list.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { TextField } from "./text-field"
|
|
|
9
9
|
|
|
10
10
|
function findByKey(container: HTMLElement, key: string) {
|
|
11
11
|
const nodes = container.querySelectorAll<HTMLElement>('[data-slot="list-item"][data-key]')
|
|
12
|
+
|
|
12
13
|
for (const node of nodes) {
|
|
13
14
|
if (node.getAttribute("data-key") === key) return node
|
|
14
15
|
}
|
|
@@ -58,11 +59,13 @@ export interface ListRef {
|
|
|
58
59
|
export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) {
|
|
59
60
|
const i18n = useI18n()
|
|
60
61
|
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
|
62
|
+
|
|
61
63
|
const [store, setStore] = createStore({
|
|
62
64
|
mouseActive: false,
|
|
63
65
|
scrollRef: undefined as HTMLDivElement | undefined,
|
|
64
66
|
internalFilter: "",
|
|
65
67
|
})
|
|
68
|
+
|
|
66
69
|
const scrollRef = () => store.scrollRef
|
|
67
70
|
const setScrollRef = (el: HTMLDivElement | undefined) => setStore("scrollRef", el)
|
|
68
71
|
const internalFilter = () => store.internalFilter
|
|
@@ -75,6 +78,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
75
78
|
const bottom = top + nodeRect.height
|
|
76
79
|
const viewTop = container.scrollTop
|
|
77
80
|
const viewBottom = viewTop + container.clientHeight
|
|
81
|
+
|
|
78
82
|
const target =
|
|
79
83
|
block === "center"
|
|
80
84
|
? top - container.clientHeight / 2 + nodeRect.height / 2
|
|
@@ -83,6 +87,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
83
87
|
: bottom > viewBottom
|
|
84
88
|
? bottom - container.clientHeight
|
|
85
89
|
: viewTop
|
|
90
|
+
|
|
86
91
|
const max = Math.max(0, container.scrollHeight - container.clientHeight)
|
|
87
92
|
container.scrollTop = Math.max(0, Math.min(target, max))
|
|
88
93
|
}
|
|
@@ -108,13 +113,16 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
108
113
|
// This is important for programmatic changes like Tab completion.
|
|
109
114
|
if (prev === value) {
|
|
110
115
|
void refetch()
|
|
116
|
+
|
|
111
117
|
return
|
|
112
118
|
}
|
|
119
|
+
|
|
113
120
|
queueMicrotask(() => refetch())
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
createEffect(() => {
|
|
117
124
|
if (props.filter === undefined) return
|
|
125
|
+
|
|
118
126
|
if (props.filter === internalFilter()) return
|
|
119
127
|
setInternalFilter(props.filter)
|
|
120
128
|
onInput(props.filter)
|
|
@@ -132,11 +140,14 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
132
140
|
|
|
133
141
|
createEffect(() => {
|
|
134
142
|
const scroll = scrollRef()
|
|
143
|
+
|
|
135
144
|
if (!scroll) return
|
|
145
|
+
|
|
136
146
|
if (!props.current) return
|
|
137
147
|
const key = props.key(props.current)
|
|
138
148
|
requestAnimationFrame(() => {
|
|
139
149
|
const element = findByKey(scroll, key)
|
|
150
|
+
|
|
140
151
|
if (!element) return
|
|
141
152
|
scrollIntoView(scroll, element, "center")
|
|
142
153
|
})
|
|
@@ -144,16 +155,23 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
144
155
|
|
|
145
156
|
createEffect(() => {
|
|
146
157
|
const all = flat()
|
|
158
|
+
|
|
147
159
|
if (store.mouseActive || all.length === 0) return
|
|
148
160
|
const scroll = scrollRef()
|
|
161
|
+
|
|
149
162
|
if (!scroll) return
|
|
163
|
+
|
|
150
164
|
if (active() === props.key(all[0])) {
|
|
151
165
|
scroll.scrollTo(0, 0)
|
|
166
|
+
|
|
152
167
|
return
|
|
153
168
|
}
|
|
169
|
+
|
|
154
170
|
const key = active()
|
|
171
|
+
|
|
155
172
|
if (!key) return
|
|
156
173
|
const element = findByKey(scroll, key)
|
|
174
|
+
|
|
157
175
|
if (!element) return
|
|
158
176
|
scrollIntoView(scroll, element, "center")
|
|
159
177
|
})
|
|
@@ -171,6 +189,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
171
189
|
|
|
172
190
|
const handleKey = (e: KeyboardEvent) => {
|
|
173
191
|
setStore("mouseActive", false)
|
|
192
|
+
|
|
174
193
|
if (e.key === "Escape") return
|
|
175
194
|
|
|
176
195
|
const all = flat()
|
|
@@ -182,12 +201,15 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
182
201
|
|
|
183
202
|
if (e.key === "Enter" && !e.isComposing) {
|
|
184
203
|
e.preventDefault()
|
|
204
|
+
|
|
185
205
|
if (selected) handleSelect(selected, index)
|
|
186
206
|
} else if (props.search) {
|
|
187
207
|
if (e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && (e.key === "n" || e.key === "p")) {
|
|
188
208
|
onKeyDown(e)
|
|
209
|
+
|
|
189
210
|
return
|
|
190
211
|
}
|
|
212
|
+
|
|
191
213
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
192
214
|
onKeyDown(e)
|
|
193
215
|
}
|
|
@@ -204,7 +226,9 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
204
226
|
|
|
205
227
|
const renderAdd = () => {
|
|
206
228
|
const add = addProps()
|
|
229
|
+
|
|
207
230
|
if (!add) return null
|
|
231
|
+
|
|
208
232
|
return (
|
|
209
233
|
<div data-slot="list-item-add" classList={{ [add.class ?? ""]: !!add.class }}>
|
|
210
234
|
{add.render()}
|
|
@@ -221,6 +245,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
221
245
|
createEffect(() => {
|
|
222
246
|
const scroll = scrollRef()
|
|
223
247
|
const node = state.header
|
|
248
|
+
|
|
224
249
|
if (!scroll || !node) return
|
|
225
250
|
|
|
226
251
|
const handler = () => {
|
|
@@ -242,12 +267,15 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
242
267
|
|
|
243
268
|
const emptyMessage = () => {
|
|
244
269
|
if (grouped.loading) return props.loadingMessage ?? i18n.t("ui.list.loading")
|
|
270
|
+
|
|
245
271
|
if (props.emptyMessage) return props.emptyMessage
|
|
246
272
|
|
|
247
273
|
const query = filter()
|
|
274
|
+
|
|
248
275
|
if (!query) return i18n.t("ui.list.empty")
|
|
249
276
|
|
|
250
277
|
const suffix = i18n.t("ui.list.emptyWithFilter.suffix")
|
|
278
|
+
|
|
251
279
|
return (
|
|
252
280
|
<>
|
|
253
281
|
<span>{i18n.t("ui.list.emptyWithFilter.prefix")}</span>
|
|
@@ -268,6 +296,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
268
296
|
classList={{ [searchProps().class ?? ""]: !!searchProps().class }}
|
|
269
297
|
onPointerDown={(event) => {
|
|
270
298
|
const container = event.currentTarget
|
|
299
|
+
|
|
271
300
|
if (!(container instanceof HTMLElement)) return
|
|
272
301
|
|
|
273
302
|
const node = container.querySelector("input, textarea")
|
|
@@ -327,6 +356,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
327
356
|
<For each={grouped.latest}>
|
|
328
357
|
{(group, groupIndex) => {
|
|
329
358
|
const isLastGroup = () => groupIndex() === grouped.latest.length - 1
|
|
359
|
+
|
|
330
360
|
return (
|
|
331
361
|
<div data-slot="list-group">
|
|
332
362
|
<Show when={group.category}>
|
|
@@ -374,7 +404,9 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|
|
374
404
|
</Show>
|
|
375
405
|
</button>
|
|
376
406
|
)
|
|
407
|
+
|
|
377
408
|
if (props.itemWrapper) return props.itemWrapper(item, node)
|
|
409
|
+
|
|
378
410
|
return node
|
|
379
411
|
}}
|
|
380
412
|
</For>
|
|
@@ -3,6 +3,7 @@ import type { SpringOptions } from "motion"
|
|
|
3
3
|
import { createComputed, createEffect, createSignal, onCleanup } from "solid-js"
|
|
4
4
|
|
|
5
5
|
type Opt = Partial<Pick<SpringOptions, "visualDuration" | "bounce" | "stiffness" | "damping" | "mass" | "velocity">>
|
|
6
|
+
|
|
6
7
|
const eq = (a: Opt | undefined, b: Opt | undefined) =>
|
|
7
8
|
a?.visualDuration === b?.visualDuration &&
|
|
8
9
|
a?.bounce === b?.bounce &&
|
|
@@ -24,6 +25,7 @@ export function useSpring(target: () => number, options?: Opt | (() => Opt), sna
|
|
|
24
25
|
createComputed(() => {
|
|
25
26
|
const next = target()
|
|
26
27
|
const nextSnap = snapKey?.()
|
|
28
|
+
|
|
27
29
|
if (snapKey && nextSnap !== snapValue) {
|
|
28
30
|
// State boundaries should adopt their target without animating from the previous context.
|
|
29
31
|
snapValue = nextSnap
|
|
@@ -32,14 +34,17 @@ export function useSpring(target: () => number, options?: Opt | (() => Opt), sna
|
|
|
32
34
|
source.jump(next)
|
|
33
35
|
stop = attachSpring(spring, source, config)
|
|
34
36
|
setValue(next)
|
|
37
|
+
|
|
35
38
|
return
|
|
36
39
|
}
|
|
40
|
+
|
|
37
41
|
source.set(next)
|
|
38
42
|
})
|
|
39
43
|
|
|
40
44
|
createEffect(() => {
|
|
41
45
|
if (!options) return
|
|
42
46
|
const next = read()
|
|
47
|
+
|
|
43
48
|
if (eq(config, next)) return
|
|
44
49
|
config = next
|
|
45
50
|
stop()
|
|
@@ -22,6 +22,7 @@ export interface PopoverProps<T extends ValidComponent = "div">
|
|
|
22
22
|
|
|
23
23
|
export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>) {
|
|
24
24
|
const i18n = useI18n()
|
|
25
|
+
|
|
25
26
|
const [local, rest] = splitProps(props, [
|
|
26
27
|
"trigger",
|
|
27
28
|
"triggerAs",
|
|
@@ -47,14 +48,18 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
|
|
|
47
48
|
})
|
|
48
49
|
|
|
49
50
|
const controlled = () => local.open !== undefined
|
|
51
|
+
|
|
50
52
|
const opened = () => {
|
|
51
53
|
if (controlled()) return local.open ?? false
|
|
54
|
+
|
|
52
55
|
return state.uncontrolledOpen
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
const onOpenChange = (next: boolean) => {
|
|
56
59
|
if (next) setState("dismiss", null)
|
|
60
|
+
|
|
57
61
|
if (local.onOpenChange) local.onOpenChange(next)
|
|
62
|
+
|
|
58
63
|
if (controlled()) return
|
|
59
64
|
setState("uncontrolledOpen", next)
|
|
60
65
|
}
|
|
@@ -65,9 +70,12 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
|
|
|
65
70
|
const inside = (node: Node | null | undefined) => {
|
|
66
71
|
if (!node) return false
|
|
67
72
|
const content = state.contentRef
|
|
73
|
+
|
|
68
74
|
if (content && content.contains(node)) return true
|
|
69
75
|
const trigger = state.triggerRef
|
|
76
|
+
|
|
70
77
|
if (trigger && trigger.contains(node)) return true
|
|
78
|
+
|
|
71
79
|
return false
|
|
72
80
|
}
|
|
73
81
|
|
|
@@ -85,14 +93,18 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
|
|
|
85
93
|
|
|
86
94
|
const onPointerDown = (event: PointerEvent) => {
|
|
87
95
|
const target = event.target
|
|
96
|
+
|
|
88
97
|
if (!(target instanceof Node)) return
|
|
98
|
+
|
|
89
99
|
if (inside(target)) return
|
|
90
100
|
close("outside")
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
const onFocusIn = (event: FocusEvent) => {
|
|
94
104
|
const target = event.target
|
|
105
|
+
|
|
95
106
|
if (!(target instanceof Node)) return
|
|
107
|
+
|
|
96
108
|
if (inside(target)) return
|
|
97
109
|
close("outside")
|
|
98
110
|
}
|
|
@@ -10,6 +10,7 @@ export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
|
|
|
10
10
|
export const ProviderIcon: Component<ProviderIconProps> = (props) => {
|
|
11
11
|
const [local, rest] = splitProps(props, ["id", "class", "classList"])
|
|
12
12
|
const resolved = createMemo(() => (iconNames.includes(local.id as IconName) ? local.id : "synthetic"))
|
|
13
|
+
|
|
13
14
|
return (
|
|
14
15
|
<svg
|
|
15
16
|
data-component="provider-icon"
|
|
@@ -33,10 +33,12 @@ export function ResizeHandle(props: ResizeHandleProps) {
|
|
|
33
33
|
e.preventDefault()
|
|
34
34
|
const edge = local.edge ?? (local.direction === "vertical" ? "start" : "end")
|
|
35
35
|
const start = local.direction === "horizontal" ? e.clientX : e.clientY
|
|
36
|
+
|
|
36
37
|
const rtl =
|
|
37
38
|
local.direction === "horizontal" &&
|
|
38
39
|
e.currentTarget instanceof Element &&
|
|
39
40
|
getComputedStyle(e.currentTarget).direction === "rtl"
|
|
41
|
+
|
|
40
42
|
const startSize = local.size
|
|
41
43
|
const min = local.min
|
|
42
44
|
const max = local.max
|
|
@@ -52,6 +54,7 @@ export function ResizeHandle(props: ResizeHandleProps) {
|
|
|
52
54
|
|
|
53
55
|
const onMouseMove = (moveEvent: MouseEvent) => {
|
|
54
56
|
const pos = local.direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY
|
|
57
|
+
|
|
55
58
|
const delta =
|
|
56
59
|
local.direction === "vertical"
|
|
57
60
|
? edge === "end"
|
|
@@ -60,12 +63,15 @@ export function ResizeHandle(props: ResizeHandleProps) {
|
|
|
60
63
|
: (edge === "start") !== rtl
|
|
61
64
|
? start - pos
|
|
62
65
|
: pos - start
|
|
66
|
+
|
|
63
67
|
current = startSize + delta
|
|
64
68
|
const nextCollapsed = threshold > 0 && current < threshold
|
|
69
|
+
|
|
65
70
|
if (nextCollapsed !== collapsed) {
|
|
66
71
|
collapsed = nextCollapsed
|
|
67
72
|
onCollapseChange?.(collapsed)
|
|
68
73
|
}
|
|
74
|
+
|
|
69
75
|
onResize(Math.min(max, Math.max(min, current)))
|
|
70
76
|
}
|
|
71
77
|
|
|
@@ -77,8 +83,10 @@ export function ResizeHandle(props: ResizeHandleProps) {
|
|
|
77
83
|
|
|
78
84
|
if (collapsed) {
|
|
79
85
|
onCollapse?.()
|
|
86
|
+
|
|
80
87
|
return
|
|
81
88
|
}
|
|
89
|
+
|
|
82
90
|
onCollapseChange?.(false)
|
|
83
91
|
}
|
|
84
92
|
|
|
@@ -28,6 +28,7 @@ export interface ScrollViewProps extends ComponentProps<"div"> {
|
|
|
28
28
|
|
|
29
29
|
export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => {
|
|
30
30
|
if (event.altKey || event.ctrlKey || event.metaKey) return
|
|
31
|
+
|
|
31
32
|
if (event.shiftKey && event.key !== " ") return
|
|
32
33
|
|
|
33
34
|
switch (event.key) {
|
|
@@ -50,6 +51,7 @@ export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey
|
|
|
50
51
|
|
|
51
52
|
export function canScrollKey(element: HTMLElement, key: NonNullable<ReturnType<typeof scrollKey>>) {
|
|
52
53
|
const up = key === "up" || key === "page-up" || key === "home"
|
|
54
|
+
|
|
53
55
|
return up ? element.scrollTop > 0 : element.scrollTop + element.clientHeight < element.scrollHeight
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -60,16 +62,23 @@ export function scrollKeyOwner(
|
|
|
60
62
|
) {
|
|
61
63
|
const element = target instanceof Element ? target : undefined
|
|
62
64
|
const owner = element?.closest<HTMLElement>("[data-scrollable]")
|
|
65
|
+
|
|
63
66
|
if (!owner || owner === root) return root
|
|
67
|
+
|
|
64
68
|
if (!root.contains(owner)) return owner
|
|
69
|
+
|
|
65
70
|
return canScrollKey(owner, key) ? owner : root
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
export function isScrollKeyTarget(target: EventTarget | null, key: NonNullable<ReturnType<typeof scrollKey>>) {
|
|
69
74
|
const element = target instanceof HTMLElement ? target : undefined
|
|
75
|
+
|
|
70
76
|
if (!element) return true
|
|
77
|
+
|
|
71
78
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) || element.isContentEditable) return false
|
|
79
|
+
|
|
72
80
|
if ((key === "page-up" || key === "page-down") && element.closest('button, a[href], [role="button"]')) return false
|
|
81
|
+
|
|
73
82
|
return true
|
|
74
83
|
}
|
|
75
84
|
|
|
@@ -106,18 +115,23 @@ export function scrollOffsetFromThumbPointer(input: {
|
|
|
106
115
|
}) {
|
|
107
116
|
const padding = 8
|
|
108
117
|
const maxThumbStart = input.clientSize - padding * 2 - input.thumbSize
|
|
118
|
+
|
|
109
119
|
if (maxThumbStart <= 0) return 0
|
|
120
|
+
|
|
110
121
|
const thumbStart = Math.max(
|
|
111
122
|
0,
|
|
112
123
|
Math.min(input.pointer - input.viewportStart - padding - input.grabOffset, maxThumbStart),
|
|
113
124
|
)
|
|
125
|
+
|
|
114
126
|
const progress = input.reverse ? 1 - thumbStart / maxThumbStart : thumbStart / maxThumbStart
|
|
127
|
+
|
|
115
128
|
return progress * Math.max(0, input.scrollSize - (input.scrollClientSize ?? input.clientSize))
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
export function ScrollView(props: ScrollViewProps) {
|
|
119
132
|
const i18n = useI18n()
|
|
120
133
|
const merged = mergeProps({ orientation: "vertical", thumbVisibility: "hover" }, props)
|
|
134
|
+
|
|
121
135
|
const [local, events, rest] = splitProps(
|
|
122
136
|
merged,
|
|
123
137
|
[
|
|
@@ -165,6 +179,7 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
165
179
|
horizontalThumbStart: 0,
|
|
166
180
|
showHorizontalThumb: false,
|
|
167
181
|
})
|
|
182
|
+
|
|
168
183
|
const isHovered = () => state.isHovered
|
|
169
184
|
const isDragging = () => state.dragging !== undefined
|
|
170
185
|
const isScrolling = () => state.isScrolling
|
|
@@ -175,13 +190,16 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
175
190
|
|
|
176
191
|
const markScrolling = () => {
|
|
177
192
|
setState("isScrolling", true)
|
|
193
|
+
|
|
178
194
|
if (scrollIdleTimer !== undefined) clearTimeout(scrollIdleTimer)
|
|
179
195
|
scrollIdleTimer = setTimeout(() => setState("isScrolling", false), 800)
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
const thumbVisible = () => {
|
|
183
199
|
if (isDragging()) return true
|
|
200
|
+
|
|
184
201
|
if (isScrolling()) return true
|
|
202
|
+
|
|
185
203
|
return local.thumbVisibility === "hover" && isHovered()
|
|
186
204
|
}
|
|
187
205
|
|
|
@@ -198,9 +216,11 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
198
216
|
const adjustment = local.verticalScrollAdjustment ?? 0
|
|
199
217
|
const scrollHeight = viewportRef.scrollHeight + adjustment
|
|
200
218
|
const trackSize = Math.max(0, (thumbMount()?.clientHeight || viewportRef.clientHeight) - trackPadding * 2)
|
|
219
|
+
|
|
201
220
|
const size = trackSize
|
|
202
221
|
? Math.min(trackSize, Math.max((viewportRef.clientHeight / scrollHeight) * trackSize, minThumbSize))
|
|
203
222
|
: 0
|
|
223
|
+
|
|
204
224
|
const maxScroll = scrollHeight - viewportRef.clientHeight
|
|
205
225
|
const maxStart = trackSize - size
|
|
206
226
|
setState("showVerticalThumb", maxScroll > 0)
|
|
@@ -215,9 +235,11 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
215
235
|
|
|
216
236
|
if (horizontal()) {
|
|
217
237
|
const trackSize = Math.max(0, (thumbMount()?.clientWidth || viewportRef.clientWidth) - trackPadding * 2)
|
|
238
|
+
|
|
218
239
|
const size = trackSize
|
|
219
240
|
? Math.min(trackSize, Math.max((viewportRef.clientWidth / viewportRef.scrollWidth) * trackSize, minThumbSize))
|
|
220
241
|
: 0
|
|
242
|
+
|
|
221
243
|
const maxScroll = viewportRef.scrollWidth - viewportRef.clientWidth
|
|
222
244
|
const maxStart = trackSize - size
|
|
223
245
|
const rtl = getComputedStyle(viewportRef).direction === "rtl"
|
|
@@ -262,6 +284,7 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
262
284
|
|
|
263
285
|
createEffect(() => {
|
|
264
286
|
const target = thumbHover()
|
|
287
|
+
|
|
265
288
|
if (!target) return
|
|
266
289
|
|
|
267
290
|
const enter = () => setState("isHovered", true)
|
|
@@ -288,10 +311,12 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
288
311
|
prepareScroll()
|
|
289
312
|
setState("dragging", axis)
|
|
290
313
|
const thumb = axis === "vertical" ? verticalThumbRef : horizontalThumbRef
|
|
314
|
+
|
|
291
315
|
const grabOffset =
|
|
292
316
|
axis === "vertical"
|
|
293
317
|
? e.clientY - thumb.getBoundingClientRect().top
|
|
294
318
|
: e.clientX - thumb.getBoundingClientRect().left
|
|
319
|
+
|
|
295
320
|
const track = thumbMount() ?? viewportRef
|
|
296
321
|
|
|
297
322
|
thumb.setPointerCapture(e.pointerId)
|
|
@@ -300,6 +325,7 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
300
325
|
prepareScroll()
|
|
301
326
|
const vertical = axis === "vertical"
|
|
302
327
|
const rtl = !vertical && getComputedStyle(viewportRef).direction === "rtl"
|
|
328
|
+
|
|
303
329
|
const offset = scrollOffsetFromThumbPointer({
|
|
304
330
|
pointer: vertical ? e.clientY : e.clientX,
|
|
305
331
|
viewportStart: vertical ? track.getBoundingClientRect().top : track.getBoundingClientRect().left,
|
|
@@ -310,10 +336,13 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
310
336
|
thumbSize: vertical ? state.verticalThumbSize : state.horizontalThumbSize,
|
|
311
337
|
reverse: rtl,
|
|
312
338
|
})
|
|
339
|
+
|
|
313
340
|
if (vertical) {
|
|
314
341
|
viewportRef.scrollTop = offset
|
|
342
|
+
|
|
315
343
|
return
|
|
316
344
|
}
|
|
345
|
+
|
|
317
346
|
viewportRef.scrollLeft = rtl ? -offset : offset
|
|
318
347
|
}
|
|
319
348
|
|
|
@@ -375,7 +404,9 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
375
404
|
if (document.activeElement && ["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
|
|
376
405
|
return
|
|
377
406
|
}
|
|
407
|
+
|
|
378
408
|
const next = scrollKey(e)
|
|
409
|
+
|
|
379
410
|
// Modified navigation (for example Ctrl+Home) stays native, but must read
|
|
380
411
|
// the same reconciled geometry as the keys handled by this component.
|
|
381
412
|
const intent =
|
|
@@ -387,11 +418,15 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
387
418
|
ctrlKey: false,
|
|
388
419
|
metaKey: false,
|
|
389
420
|
})
|
|
421
|
+
|
|
390
422
|
if (!intent) return
|
|
423
|
+
|
|
391
424
|
if (!isScrollKeyTarget(e.target, intent)) return
|
|
425
|
+
|
|
392
426
|
if (scrollKeyOwner(viewportRef, e.target, intent) !== viewportRef) return
|
|
393
427
|
|
|
394
428
|
prepareScroll()
|
|
429
|
+
|
|
395
430
|
if (!next) return
|
|
396
431
|
const scrollAmount = viewportRef.clientHeight * 0.8
|
|
397
432
|
const lineAmount = 40
|
|
@@ -446,12 +481,15 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
446
481
|
onScroll={(e) => {
|
|
447
482
|
updateThumb()
|
|
448
483
|
markScrolling()
|
|
484
|
+
|
|
449
485
|
if (typeof events.onScroll === "function") events.onScroll(e as any)
|
|
450
486
|
}}
|
|
451
487
|
onWheel={(e) => {
|
|
452
488
|
markScrolling()
|
|
453
489
|
const handler = events.onWheel
|
|
490
|
+
|
|
454
491
|
if (typeof handler === "function") handler(e as any)
|
|
492
|
+
|
|
455
493
|
if (Array.isArray(handler)) handler[0](handler[1], e as any)
|
|
456
494
|
}}
|
|
457
495
|
onTouchStart={events.onTouchStart as any}
|
|
@@ -465,6 +503,7 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
465
503
|
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
|
466
504
|
onKeyDown={(e) => {
|
|
467
505
|
onKeyDown(e)
|
|
506
|
+
|
|
468
507
|
if (typeof events.onKeyDown === "function") events.onKeyDown(e as any)
|
|
469
508
|
}}
|
|
470
509
|
>
|
|
@@ -34,6 +34,7 @@ export interface TextFieldProps
|
|
|
34
34
|
|
|
35
35
|
export function TextField(props: TextFieldProps) {
|
|
36
36
|
const i18n = useI18n()
|
|
37
|
+
|
|
37
38
|
const [local, others] = splitProps(props, [
|
|
38
39
|
"name",
|
|
39
40
|
"defaultValue",
|
|
@@ -54,17 +55,22 @@ export function TextField(props: TextFieldProps) {
|
|
|
54
55
|
"copyKind",
|
|
55
56
|
"multiline",
|
|
56
57
|
])
|
|
58
|
+
|
|
57
59
|
const [copied, setCopied] = createSignal(false)
|
|
58
60
|
|
|
59
61
|
const label = () => {
|
|
60
62
|
if (copied()) return i18n.t("ui.textField.copied")
|
|
63
|
+
|
|
61
64
|
if (local.copyKind === "link") return i18n.t("ui.textField.copyLink")
|
|
65
|
+
|
|
62
66
|
return i18n.t("ui.textField.copyToClipboard")
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
const icon = () => {
|
|
66
70
|
if (copied()) return "check"
|
|
71
|
+
|
|
67
72
|
if (local.copyKind === "link") return "link"
|
|
73
|
+
|
|
68
74
|
return "copy"
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -3,18 +3,23 @@ import { createStore } from "solid-js/store"
|
|
|
3
3
|
|
|
4
4
|
const px = (value: number | string | undefined, fallback: number) => {
|
|
5
5
|
if (typeof value === "number") return `${value}px`
|
|
6
|
+
|
|
6
7
|
if (typeof value === "string") return value
|
|
8
|
+
|
|
7
9
|
return `${fallback}px`
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
const ms = (value: number | string | undefined, fallback: number) => {
|
|
11
13
|
if (typeof value === "number") return `${value}ms`
|
|
14
|
+
|
|
12
15
|
if (typeof value === "string") return value
|
|
16
|
+
|
|
13
17
|
return `${fallback}ms`
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
const pct = (value: number | undefined, fallback: number) => {
|
|
17
21
|
const v = value ?? fallback
|
|
22
|
+
|
|
18
23
|
return `${v}%`
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -38,6 +43,7 @@ export function TextReveal(props: {
|
|
|
38
43
|
ready: false,
|
|
39
44
|
swapping: false,
|
|
40
45
|
})
|
|
46
|
+
|
|
41
47
|
const cur = () => state.cur
|
|
42
48
|
const old = () => state.old
|
|
43
49
|
const width = () => state.width
|
|
@@ -53,10 +59,13 @@ export function TextReveal(props: {
|
|
|
53
59
|
|
|
54
60
|
const widen = (next: number) => {
|
|
55
61
|
if (next <= 0) return
|
|
62
|
+
|
|
56
63
|
if (props.growOnly ?? true) {
|
|
57
64
|
const prev = Number.parseFloat(width())
|
|
65
|
+
|
|
58
66
|
if (Number.isFinite(prev) && next <= prev) return
|
|
59
67
|
}
|
|
68
|
+
|
|
60
69
|
setState("width", `${next}px`)
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -65,11 +74,14 @@ export function TextReveal(props: {
|
|
|
65
74
|
() => props.text,
|
|
66
75
|
(next, prev) => {
|
|
67
76
|
if (next === prev) return
|
|
77
|
+
|
|
68
78
|
if (typeof next === "string" && typeof prev === "string" && next.startsWith(prev)) {
|
|
69
79
|
setState("cur", next)
|
|
70
80
|
widen(win())
|
|
81
|
+
|
|
71
82
|
return
|
|
72
83
|
}
|
|
84
|
+
|
|
73
85
|
setState("swapping", true)
|
|
74
86
|
setState("old", prev)
|
|
75
87
|
setState("cur", next)
|
|
@@ -78,8 +90,10 @@ export function TextReveal(props: {
|
|
|
78
90
|
widen(Math.max(win(), wout()))
|
|
79
91
|
rootRef?.offsetHeight
|
|
80
92
|
setState("swapping", false)
|
|
93
|
+
|
|
81
94
|
return
|
|
82
95
|
}
|
|
96
|
+
|
|
83
97
|
if (frame !== undefined && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame)
|
|
84
98
|
frame = requestAnimationFrame(() => {
|
|
85
99
|
widen(Math.max(win(), wout()))
|
|
@@ -94,14 +108,19 @@ export function TextReveal(props: {
|
|
|
94
108
|
onMount(() => {
|
|
95
109
|
widen(win())
|
|
96
110
|
const fonts = typeof document !== "undefined" ? document.fonts : undefined
|
|
111
|
+
|
|
97
112
|
if (typeof requestAnimationFrame !== "function") {
|
|
98
113
|
setState("ready", true)
|
|
114
|
+
|
|
99
115
|
return
|
|
100
116
|
}
|
|
117
|
+
|
|
101
118
|
if (!fonts) {
|
|
102
119
|
requestAnimationFrame(() => setState("ready", true))
|
|
120
|
+
|
|
103
121
|
return
|
|
104
122
|
}
|
|
123
|
+
|
|
105
124
|
void fonts.ready.finally(() => {
|
|
106
125
|
widen(win())
|
|
107
126
|
requestAnimationFrame(() => setState("ready", true))
|