@trendr/core 0.1.0 → 0.2.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/README.md +420 -56
- package/index.js +9 -1
- package/package.json +23 -3
- package/src/animation.js +204 -0
- package/src/ansi.js +3 -0
- package/src/buffer.js +49 -15
- package/src/button.js +10 -1
- package/src/checkbox.js +12 -3
- package/src/diff.js +115 -12
- package/src/hooks.js +142 -2
- package/src/input.js +59 -7
- package/src/layout.js +90 -18
- package/src/list.js +142 -17
- package/src/modal.js +2 -2
- package/src/pick-list.js +204 -0
- package/src/progress.js +57 -9
- package/src/radio.js +16 -2
- package/src/renderer.js +364 -68
- package/src/scroll-box.js +105 -0
- package/src/scrollable-text.js +28 -6
- package/src/select.js +188 -68
- package/src/shimmer.js +95 -0
- package/src/signal.js +12 -0
- package/src/spinner.js +13 -4
- package/src/split-pane.js +66 -0
- package/src/table.js +68 -23
- package/src/task.js +43 -0
- package/src/text-area.js +8 -5
- package/src/text-input.js +9 -6
- package/src/toast.js +4 -6
- package/src/wrap.js +110 -11
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
|
+
import { createSignal } from './signal.js'
|
|
3
|
+
import { useInput, useMouse, useLayout, useTheme, useScrollDrag } from './hooks.js'
|
|
4
|
+
|
|
5
|
+
export function ScrollBox({ children, focused = true, scrollOffset: offsetProp, onScroll, scrollbar = false, thumbChar = '\u2588', trackChar = '\u2502', style: userStyle }) {
|
|
6
|
+
const { accent = 'cyan' } = useTheme()
|
|
7
|
+
const [offsetInternal, setOffsetInternal] = createSignal(0)
|
|
8
|
+
const layout = useLayout()
|
|
9
|
+
|
|
10
|
+
const offset = offsetProp ?? offsetInternal()
|
|
11
|
+
const setOffset = onScroll ?? setOffsetInternal
|
|
12
|
+
|
|
13
|
+
const visibleH = layout.height
|
|
14
|
+
const contentH = layout.contentHeight ?? 0
|
|
15
|
+
const maxOffset = Math.max(0, contentH - visibleH)
|
|
16
|
+
|
|
17
|
+
const clamp = (v) => Math.max(0, Math.min(maxOffset, v))
|
|
18
|
+
|
|
19
|
+
useInput(({ key, ctrl }) => {
|
|
20
|
+
if (!focused) return
|
|
21
|
+
if (contentH <= visibleH) return
|
|
22
|
+
|
|
23
|
+
const pageH = visibleH || 10
|
|
24
|
+
const half = Math.max(1, Math.floor(pageH / 2))
|
|
25
|
+
|
|
26
|
+
if (key === 'up' || key === 'k') setOffset(clamp(offset - 1))
|
|
27
|
+
else if (key === 'down' || key === 'j') setOffset(clamp(offset + 1))
|
|
28
|
+
else if (key === 'pageup' || (ctrl && key === 'b')) setOffset(clamp(offset - pageH))
|
|
29
|
+
else if (key === 'pagedown' || (ctrl && key === 'f')) setOffset(clamp(offset + pageH))
|
|
30
|
+
else if (ctrl && key === 'u') setOffset(clamp(offset - half))
|
|
31
|
+
else if (ctrl && key === 'd') setOffset(clamp(offset + half))
|
|
32
|
+
else if (key === 'home' || key === 'g') setOffset(0)
|
|
33
|
+
else if (key === 'end' || key === 'G') setOffset(maxOffset)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
useMouse((event) => {
|
|
37
|
+
if (!focused) return
|
|
38
|
+
if (event.action !== 'scroll') return
|
|
39
|
+
if (contentH <= visibleH) return
|
|
40
|
+
const { x, y } = event
|
|
41
|
+
if (x < layout.x || x >= layout.x + layout.width || y < layout.y || y >= layout.y + layout.height) return
|
|
42
|
+
if (event.direction === 'up') setOffset(clamp(offset - 3))
|
|
43
|
+
else setOffset(clamp(offset + 3))
|
|
44
|
+
event.stopPropagation()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const hasBar = scrollbar && visibleH > 0 && contentH > visibleH
|
|
48
|
+
const barThumbH = hasBar ? Math.max(1, Math.round((visibleH / contentH) * visibleH)) : 0
|
|
49
|
+
const barThumbStart = hasBar && maxOffset > 0 ? Math.round((offset / maxOffset) * (visibleH - barThumbH)) : 0
|
|
50
|
+
|
|
51
|
+
useScrollDrag({
|
|
52
|
+
barX: hasBar ? layout.x + layout.width - 1 : null,
|
|
53
|
+
barY: layout.y + barThumbStart,
|
|
54
|
+
thumbHeight: barThumbH,
|
|
55
|
+
trackHeight: visibleH,
|
|
56
|
+
maxOffset,
|
|
57
|
+
scrollOffset: offset,
|
|
58
|
+
onScroll: (v) => setOffset(clamp(v)),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const scrollBox = jsx('box', {
|
|
62
|
+
style: {
|
|
63
|
+
flexDirection: 'column',
|
|
64
|
+
flexGrow: 1,
|
|
65
|
+
...userStyle,
|
|
66
|
+
overflow: 'scroll',
|
|
67
|
+
scrollOffset: offset,
|
|
68
|
+
},
|
|
69
|
+
children,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (!scrollbar) return scrollBox
|
|
73
|
+
|
|
74
|
+
const hasOverflow = visibleH > 0 && contentH > visibleH
|
|
75
|
+
const barChildren = []
|
|
76
|
+
|
|
77
|
+
if (hasOverflow) {
|
|
78
|
+
const thumbH = Math.max(1, Math.round((visibleH / contentH) * visibleH))
|
|
79
|
+
const thumbStart = maxOffset > 0
|
|
80
|
+
? Math.round((offset / maxOffset) * (visibleH - thumbH))
|
|
81
|
+
: 0
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < visibleH; i++) {
|
|
84
|
+
const isThumb = i >= thumbStart && i < thumbStart + thumbH
|
|
85
|
+
barChildren.push(
|
|
86
|
+
jsx('text', {
|
|
87
|
+
key: i,
|
|
88
|
+
style: { color: isThumb ? (focused ? accent : 'gray') : 'gray', dim: !isThumb },
|
|
89
|
+
children: isThumb ? thumbChar : trackChar,
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return jsxs('box', {
|
|
96
|
+
style: { flexDirection: 'row', flexGrow: userStyle?.flexGrow, gap: 1 },
|
|
97
|
+
children: [
|
|
98
|
+
scrollBox,
|
|
99
|
+
jsx('box', {
|
|
100
|
+
style: { width: 1, flexDirection: 'column' },
|
|
101
|
+
children: barChildren,
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
}
|
package/src/scrollable-text.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { jsx } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
|
-
import { useInput, useLayout, useTheme } from './hooks.js'
|
|
3
|
+
import { useInput, useMouse, useLayout, useTheme, useScrollDrag } from './hooks.js'
|
|
4
4
|
import { wordWrap } from './wrap.js'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const THUMB = '\u2588'
|
|
8
|
-
|
|
9
|
-
export function ScrollableText({ content = '', focused = true, scrollOffset: offsetProp, onScroll, width: widthProp, scrollbar = false, wrap = true }) {
|
|
6
|
+
export function ScrollableText({ content = '', focused = true, scrollOffset: offsetProp, onScroll, width: widthProp, scrollbar = false, wrap = true, thumbChar = '\u2588', trackChar = '\u2502' }) {
|
|
10
7
|
const { accent = 'cyan' } = useTheme()
|
|
11
8
|
const [offsetInternal, setOffsetInternal] = createSignal(0)
|
|
12
9
|
const layout = useLayout()
|
|
@@ -40,6 +37,31 @@ export function ScrollableText({ content = '', focused = true, scrollOffset: off
|
|
|
40
37
|
else if (key === 'end' || key === 'G') setOffset(maxOffset)
|
|
41
38
|
})
|
|
42
39
|
|
|
40
|
+
useMouse((event) => {
|
|
41
|
+
if (!focused) return
|
|
42
|
+
if (event.action !== 'scroll') return
|
|
43
|
+
if (lines.length <= (h || 1)) return
|
|
44
|
+
const { x, y } = event
|
|
45
|
+
if (x < layout.x || x >= layout.x + layout.width || y < layout.y || y >= layout.y + layout.height) return
|
|
46
|
+
if (event.direction === 'up') setOffset(clamp(offset - 3))
|
|
47
|
+
else setOffset(clamp(offset + 3))
|
|
48
|
+
event.stopPropagation()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const hasBar = scrollbar && h > 0 && lines.length > h
|
|
52
|
+
const barThumbH = hasBar ? Math.max(1, Math.round((h / lines.length) * h)) : 0
|
|
53
|
+
const barThumbStart = hasBar && maxOffset > 0 ? Math.round((offset / maxOffset) * (h - barThumbH)) : 0
|
|
54
|
+
|
|
55
|
+
useScrollDrag({
|
|
56
|
+
barX: hasBar ? layout.x + layout.width - 1 : null,
|
|
57
|
+
barY: layout.y + barThumbStart,
|
|
58
|
+
thumbHeight: barThumbH,
|
|
59
|
+
trackHeight: h,
|
|
60
|
+
maxOffset,
|
|
61
|
+
scrollOffset: offset,
|
|
62
|
+
onScroll: (v) => setOffset(clamp(v)),
|
|
63
|
+
})
|
|
64
|
+
|
|
43
65
|
const visible = lines.slice(offset, h > 0 ? offset + h : undefined)
|
|
44
66
|
|
|
45
67
|
const textStyle = wrap ? undefined : { overflow: 'truncate' }
|
|
@@ -58,7 +80,7 @@ export function ScrollableText({ content = '', focused = true, scrollOffset: off
|
|
|
58
80
|
|
|
59
81
|
const children = visible.map((line, i) => {
|
|
60
82
|
const isThumb = i >= thumbStart && i < thumbStart + thumbH
|
|
61
|
-
const barChar = isThumb ?
|
|
83
|
+
const barChar = isThumb ? thumbChar : trackChar
|
|
62
84
|
const barColor = isThumb ? (focused ? accent : 'gray') : 'gray'
|
|
63
85
|
|
|
64
86
|
return jsx('box', {
|
package/src/select.js
CHANGED
|
@@ -1,9 +1,143 @@
|
|
|
1
1
|
import { jsx } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
|
-
import { useInput, useTheme } from './hooks.js'
|
|
4
|
-
import { registerOverlay, getInstanceLayout, getContext } from './renderer.js'
|
|
3
|
+
import { useInput, useMouse, useLayout, useTheme } from './hooks.js'
|
|
4
|
+
import { registerOverlay, getInstanceLayout, getContext, registerHook } from './renderer.js'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function SelectDropdown({ items, cursor, scroll, visibleCount, onSelect, onClose, onCursorChange, renderItem, style: s, accent, dropWidth }) {
|
|
7
|
+
const layout = useLayout()
|
|
8
|
+
const drag = registerHook(() => ({ active: false, startY: 0, startCursor: 0 }))
|
|
9
|
+
|
|
10
|
+
useMouse((event) => {
|
|
11
|
+
if (event.action === 'release') {
|
|
12
|
+
if (drag.active) drag.active = false
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (event.action === 'drag' && drag.active) {
|
|
17
|
+
const thumbH = Math.max(1, Math.round((visibleCount / items.length) * visibleCount))
|
|
18
|
+
const dy = event.y - drag.startY
|
|
19
|
+
const travel = Math.max(1, visibleCount - thumbH)
|
|
20
|
+
const ratio = (items.length - 1) / travel
|
|
21
|
+
const idx = Math.max(0, Math.min(items.length - 1, Math.round(drag.startCursor + dy * ratio)))
|
|
22
|
+
onCursorChange(idx)
|
|
23
|
+
event.stopPropagation()
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// layout not yet computed - consume event but don't act
|
|
28
|
+
if (layout.width === 0 || layout.height === 0) {
|
|
29
|
+
event.stopPropagation()
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { x, y } = event
|
|
34
|
+
const boxRight = layout.x + dropWidth
|
|
35
|
+
const inside = x >= layout.x && x < boxRight && y >= layout.y && y < layout.y + layout.height
|
|
36
|
+
|
|
37
|
+
if (event.action === 'scroll') {
|
|
38
|
+
if (!inside) return
|
|
39
|
+
if (event.direction === 'up') onCursorChange(Math.max(0, cursor - 1))
|
|
40
|
+
else onCursorChange(Math.min(items.length - 1, cursor + 1))
|
|
41
|
+
event.stopPropagation()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (event.action !== 'press' || event.button !== 'left') return
|
|
46
|
+
|
|
47
|
+
if (!inside) {
|
|
48
|
+
onClose()
|
|
49
|
+
event.stopPropagation()
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const scrollable = items.length > visibleCount
|
|
54
|
+
if (scrollable && x >= boxRight - 4) {
|
|
55
|
+
const maxSc = items.length - visibleCount
|
|
56
|
+
const thumbH = Math.max(1, Math.round((visibleCount / items.length) * visibleCount))
|
|
57
|
+
const thumbStart = maxSc > 0 ? Math.round((scroll / maxSc) * (visibleCount - thumbH)) : 0
|
|
58
|
+
const barY = layout.y + 1 + thumbStart
|
|
59
|
+
if (y >= barY && y < barY + thumbH) {
|
|
60
|
+
drag.active = true
|
|
61
|
+
drag.startY = y
|
|
62
|
+
drag.startCursor = cursor
|
|
63
|
+
}
|
|
64
|
+
event.stopPropagation()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const relY = y - layout.y - 1
|
|
69
|
+
if (relY >= 0 && relY < visibleCount) {
|
|
70
|
+
const clickedIdx = relY + scroll
|
|
71
|
+
if (clickedIdx >= 0 && clickedIdx < items.length) {
|
|
72
|
+
onSelect(items[clickedIdx])
|
|
73
|
+
event.stopPropagation()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const scrollable = items.length > visibleCount
|
|
79
|
+
const visible = items.slice(scroll, scroll + visibleCount)
|
|
80
|
+
const thumbH = scrollable ? Math.max(1, Math.round((visibleCount / items.length) * visibleCount)) : 0
|
|
81
|
+
const maxSc = items.length - visibleCount
|
|
82
|
+
const thumbStart = scrollable && maxSc > 0 ? Math.round((scroll / maxSc) * (visibleCount - thumbH)) : 0
|
|
83
|
+
const maxLen = items.reduce((m, v) => Math.max(m, v.length), 0)
|
|
84
|
+
|
|
85
|
+
const dropdownChildren = visible.map((item, vi) => {
|
|
86
|
+
const i = vi + scroll
|
|
87
|
+
const isCursor = i === cursor
|
|
88
|
+
|
|
89
|
+
const row = (content) => {
|
|
90
|
+
if (!scrollable) return content
|
|
91
|
+
const barIsThumb = vi >= thumbStart && vi < thumbStart + thumbH
|
|
92
|
+
return jsx('box', {
|
|
93
|
+
key: i,
|
|
94
|
+
style: { flexDirection: 'row' },
|
|
95
|
+
children: [
|
|
96
|
+
content,
|
|
97
|
+
jsx('text', {
|
|
98
|
+
style: { color: barIsThumb ? accent : 'gray', dim: !barIsThumb },
|
|
99
|
+
children: ' ' + (barIsThumb ? '\u2588' : '\u2502'),
|
|
100
|
+
}),
|
|
101
|
+
],
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (renderItem) {
|
|
106
|
+
const content = jsx('box', {
|
|
107
|
+
key: scrollable ? undefined : i,
|
|
108
|
+
style: { bg: isCursor ? s.cursorBg : s.bg, flexGrow: 1 },
|
|
109
|
+
children: renderItem(item, { selected: isCursor, index: i }),
|
|
110
|
+
})
|
|
111
|
+
return row(content)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const content = jsx('box', {
|
|
115
|
+
key: scrollable ? undefined : i,
|
|
116
|
+
style: { bg: isCursor ? s.cursorBg : s.bg, paddingX: 1, flexGrow: 1 },
|
|
117
|
+
children: jsx('text', {
|
|
118
|
+
style: { color: isCursor ? s.cursorTextColor : s.color },
|
|
119
|
+
children: item,
|
|
120
|
+
}),
|
|
121
|
+
})
|
|
122
|
+
return row(content)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const dropdownH = visibleCount + 2
|
|
126
|
+
|
|
127
|
+
return jsx('box', {
|
|
128
|
+
style: {
|
|
129
|
+
flexDirection: 'column',
|
|
130
|
+
border: s.border,
|
|
131
|
+
borderColor: s.borderColor,
|
|
132
|
+
height: dropdownH,
|
|
133
|
+
width: maxLen + 4 + (scrollable ? 2 : 0),
|
|
134
|
+
bg: s.bg,
|
|
135
|
+
},
|
|
136
|
+
children: dropdownChildren,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function Select({ items, selected, onSelect, focused = false, overlay = false, maxVisible = 10, placeholder = 'select...', renderItem, style: userStyle, openIcon = '\u25b2', closedIcon = '\u25bc' }) {
|
|
7
141
|
const { accent = 'cyan' } = useTheme()
|
|
8
142
|
const defaults = {
|
|
9
143
|
border: 'single',
|
|
@@ -42,6 +176,34 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
|
|
|
42
176
|
else if (key === 'escape') { setOpen(false); event.stopPropagation() }
|
|
43
177
|
})
|
|
44
178
|
|
|
179
|
+
const layout = useLayout()
|
|
180
|
+
|
|
181
|
+
useMouse((event) => {
|
|
182
|
+
if (event.action === 'scroll') {
|
|
183
|
+
if (!focused || !open()) return
|
|
184
|
+
const len = items.length
|
|
185
|
+
if (event.direction === 'up') setCursor(c => Math.max(0, c - 1))
|
|
186
|
+
else setCursor(c => Math.min(len - 1, c + 1))
|
|
187
|
+
event.stopPropagation()
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (event.action !== 'press' || event.button !== 'left') return
|
|
192
|
+
const { x, y } = event
|
|
193
|
+
const onCollapsed = x >= layout.x && x < layout.x + layout.width && y === layout.y
|
|
194
|
+
|
|
195
|
+
if (onCollapsed) {
|
|
196
|
+
if (open()) {
|
|
197
|
+
setOpen(false)
|
|
198
|
+
} else {
|
|
199
|
+
const idx = items.indexOf(selected)
|
|
200
|
+
setCursor(idx >= 0 ? idx : 0)
|
|
201
|
+
setOpen(true)
|
|
202
|
+
}
|
|
203
|
+
event.stopPropagation()
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
45
207
|
const display = selected ?? placeholder
|
|
46
208
|
const collapsed = jsx('text', {
|
|
47
209
|
style: {
|
|
@@ -49,25 +211,22 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
|
|
|
49
211
|
color: focused ? s.focusedColor : (selected ? s.color : 'gray'),
|
|
50
212
|
bold: focused,
|
|
51
213
|
},
|
|
52
|
-
children:
|
|
214
|
+
children: `${open() ? openIcon : closedIcon} ${display}`,
|
|
53
215
|
})
|
|
54
216
|
|
|
55
217
|
if (!open()) return collapsed
|
|
56
218
|
|
|
57
|
-
const maxLen = items.reduce((m, v) => Math.max(m, v.length), 0)
|
|
58
|
-
|
|
59
219
|
let maxRows = Math.min(items.length, maxVisible)
|
|
60
220
|
|
|
61
221
|
if (overlay) {
|
|
62
|
-
const
|
|
222
|
+
const instLayout = getInstanceLayout()
|
|
63
223
|
const ctx = getContext()
|
|
64
224
|
const termH = ctx?.stream?.rows ?? 24
|
|
65
|
-
const anchorY =
|
|
225
|
+
const anchorY = instLayout.y + 1
|
|
66
226
|
const available = termH - anchorY - 2
|
|
67
227
|
if (available > 0) maxRows = Math.min(maxRows, available)
|
|
68
228
|
}
|
|
69
229
|
|
|
70
|
-
const scrollable = items.length > maxRows
|
|
71
230
|
const visibleCount = maxRows
|
|
72
231
|
|
|
73
232
|
const cur = cursor()
|
|
@@ -78,68 +237,29 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
|
|
|
78
237
|
newScroll = Math.max(0, Math.min(newScroll, items.length - visibleCount))
|
|
79
238
|
if (newScroll !== sc) setScroll(newScroll)
|
|
80
239
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const thumbStart = scrollable && maxSc > 0 ? Math.round((newScroll / maxSc) * (visibleCount - thumbH)) : 0
|
|
86
|
-
|
|
87
|
-
const dropdownChildren = visible.map((item, vi) => {
|
|
88
|
-
const i = vi + newScroll
|
|
89
|
-
const isCursor = i === cur
|
|
90
|
-
|
|
91
|
-
const barChar = scrollable
|
|
92
|
-
? (vi >= thumbStart && vi < thumbStart + thumbH ? '\u2588' : '\u2502')
|
|
93
|
-
: null
|
|
94
|
-
|
|
95
|
-
const row = (content) => {
|
|
96
|
-
if (!scrollable) return content
|
|
97
|
-
const barIsThumb = vi >= thumbStart && vi < thumbStart + thumbH
|
|
98
|
-
return jsx('box', {
|
|
99
|
-
key: i,
|
|
100
|
-
style: { flexDirection: 'row' },
|
|
101
|
-
children: [
|
|
102
|
-
content,
|
|
103
|
-
jsx('text', {
|
|
104
|
-
style: { color: barIsThumb ? accent : 'gray', dim: !barIsThumb },
|
|
105
|
-
children: ' ' + barChar,
|
|
106
|
-
}),
|
|
107
|
-
],
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (renderItem) {
|
|
112
|
-
const content = jsx('box', {
|
|
113
|
-
key: scrollable ? undefined : i,
|
|
114
|
-
style: { bg: isCursor ? s.cursorBg : s.bg, flexGrow: 1 },
|
|
115
|
-
children: renderItem(item, { selected: isCursor, index: i }),
|
|
116
|
-
})
|
|
117
|
-
return row(content)
|
|
118
|
-
}
|
|
240
|
+
const handleSelect = (item) => {
|
|
241
|
+
onSelect?.(item)
|
|
242
|
+
setOpen(false)
|
|
243
|
+
}
|
|
119
244
|
|
|
120
|
-
|
|
121
|
-
key: scrollable ? undefined : i,
|
|
122
|
-
style: { bg: isCursor ? s.cursorBg : s.bg, paddingX: 1, flexGrow: 1 },
|
|
123
|
-
children: jsx('text', {
|
|
124
|
-
style: { color: isCursor ? s.cursorTextColor : s.color },
|
|
125
|
-
children: item,
|
|
126
|
-
}),
|
|
127
|
-
})
|
|
128
|
-
return row(content)
|
|
129
|
-
})
|
|
245
|
+
const handleClose = () => setOpen(false)
|
|
130
246
|
|
|
131
|
-
const
|
|
247
|
+
const maxLen = items.reduce((m, v) => Math.max(m, v.length), 0)
|
|
248
|
+
const scrollable = items.length > visibleCount
|
|
249
|
+
const dropWidth = maxLen + 4 + (scrollable ? 2 : 0)
|
|
132
250
|
|
|
133
|
-
const dropdown = jsx(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
251
|
+
const dropdown = jsx(SelectDropdown, {
|
|
252
|
+
items,
|
|
253
|
+
cursor: cur,
|
|
254
|
+
scroll: newScroll,
|
|
255
|
+
visibleCount,
|
|
256
|
+
onSelect: handleSelect,
|
|
257
|
+
onClose: handleClose,
|
|
258
|
+
onCursorChange: (idx) => setCursor(idx),
|
|
259
|
+
renderItem,
|
|
260
|
+
style: s,
|
|
261
|
+
accent,
|
|
262
|
+
dropWidth,
|
|
143
263
|
})
|
|
144
264
|
|
|
145
265
|
if (overlay) {
|
package/src/shimmer.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
|
+
import { createSignalRaw } from './signal.js'
|
|
3
|
+
import { useInterval, useTheme } from './hooks.js'
|
|
4
|
+
import { registerHook } from './renderer.js'
|
|
5
|
+
|
|
6
|
+
const NAMED_RGB = {
|
|
7
|
+
black: [0, 0, 0], red: [205, 0, 0], green: [0, 205, 0], yellow: [205, 205, 0],
|
|
8
|
+
blue: [0, 0, 238], magenta: [205, 0, 205], cyan: [0, 205, 205], white: [229, 229, 229],
|
|
9
|
+
gray: [127, 127, 127], grey: [127, 127, 127],
|
|
10
|
+
brightRed: [255, 0, 0], brightGreen: [0, 255, 0], brightYellow: [255, 255, 0],
|
|
11
|
+
brightBlue: [92, 92, 255], brightMagenta: [255, 0, 255], brightCyan: [0, 255, 255],
|
|
12
|
+
brightWhite: [255, 255, 255],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toRgb(color) {
|
|
16
|
+
if (NAMED_RGB[color]) return NAMED_RGB[color]
|
|
17
|
+
if (color?.startsWith('#') && color.length === 7) {
|
|
18
|
+
return [parseInt(color.slice(1, 3), 16), parseInt(color.slice(3, 5), 16), parseInt(color.slice(5, 7), 16)]
|
|
19
|
+
}
|
|
20
|
+
return [127, 127, 127]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lerpColor(a, b, t) {
|
|
24
|
+
const r = Math.round(a[0] + (b[0] - a[0]) * t)
|
|
25
|
+
const g = Math.round(a[1] + (b[1] - a[1]) * t)
|
|
26
|
+
const bl = Math.round(a[2] + (b[2] - a[2]) * t)
|
|
27
|
+
return '#' + ((1 << 24) + (r << 16) + (g << 8) + bl).toString(16).slice(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Shimmer({ children, color, highlight, size = 3, gradient = 3, duration = 1000, delay = 500, reverse = false }) {
|
|
31
|
+
const { accent = 'cyan' } = useTheme()
|
|
32
|
+
const baseColor = color ?? 'gray'
|
|
33
|
+
const hlColor = highlight ?? accent
|
|
34
|
+
|
|
35
|
+
const text = typeof children === 'string' ? children : String(children ?? '')
|
|
36
|
+
const len = text.length
|
|
37
|
+
const windowSize = size + gradient * 2
|
|
38
|
+
const totalFrames = len + windowSize
|
|
39
|
+
const msPerFrame = duration / totalFrames
|
|
40
|
+
const delayFrames = Math.ceil(delay / msPerFrame)
|
|
41
|
+
const cycleLength = totalFrames + delayFrames
|
|
42
|
+
|
|
43
|
+
const [pos, setPos] = registerHook(() => {
|
|
44
|
+
const [get, set] = createSignalRaw(0)
|
|
45
|
+
return [get, set]
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
useInterval(() => setPos(p => (p + 1) % cycleLength), msPerFrame)
|
|
49
|
+
|
|
50
|
+
const p = pos()
|
|
51
|
+
|
|
52
|
+
if (p >= totalFrames) {
|
|
53
|
+
return jsx('text', { style: { color: baseColor }, children: text })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const baseRgb = toRgb(baseColor)
|
|
57
|
+
const hlRgb = toRgb(hlColor)
|
|
58
|
+
const center = reverse ? (len + windowSize / 2) - p : p - windowSize / 2
|
|
59
|
+
const halfSize = (size - 1) / 2
|
|
60
|
+
const parts = []
|
|
61
|
+
let run = { color: null, bold: false, chars: '' }
|
|
62
|
+
|
|
63
|
+
function flushRun() {
|
|
64
|
+
if (run.chars) {
|
|
65
|
+
parts.push(jsx('text', { style: { color: run.color, bold: run.bold }, children: run.chars }))
|
|
66
|
+
run = { color: null, bold: false, chars: '' }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < len; i++) {
|
|
71
|
+
const dist = Math.abs(i - center)
|
|
72
|
+
let charColor
|
|
73
|
+
let bold = false
|
|
74
|
+
|
|
75
|
+
if (dist <= halfSize) {
|
|
76
|
+
charColor = lerpColor(baseRgb, hlRgb, 1)
|
|
77
|
+
bold = true
|
|
78
|
+
} else if (gradient > 0 && dist <= halfSize + gradient) {
|
|
79
|
+
const t = 1 - (dist - halfSize) / gradient
|
|
80
|
+
charColor = lerpColor(baseRgb, hlRgb, t)
|
|
81
|
+
} else {
|
|
82
|
+
charColor = baseColor
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (charColor !== run.color || bold !== run.bold) {
|
|
86
|
+
flushRun()
|
|
87
|
+
run.color = charColor
|
|
88
|
+
run.bold = bold
|
|
89
|
+
}
|
|
90
|
+
run.chars += text[i]
|
|
91
|
+
}
|
|
92
|
+
flushRun()
|
|
93
|
+
|
|
94
|
+
return jsxs('box', { style: { flexDirection: 'row' }, children: parts })
|
|
95
|
+
}
|
package/src/signal.js
CHANGED
|
@@ -4,6 +4,17 @@ let pendingEffects = null
|
|
|
4
4
|
let batchDepth = 0
|
|
5
5
|
let schedulerHook = null
|
|
6
6
|
let hookRegistrar = null
|
|
7
|
+
let renderTracker = null
|
|
8
|
+
|
|
9
|
+
export function startRenderTracking() {
|
|
10
|
+
renderTracker = []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stopRenderTracking() {
|
|
14
|
+
const tracked = renderTracker
|
|
15
|
+
renderTracker = null
|
|
16
|
+
return tracked
|
|
17
|
+
}
|
|
7
18
|
|
|
8
19
|
export function setSchedulerHook(fn) {
|
|
9
20
|
schedulerHook = fn
|
|
@@ -18,6 +29,7 @@ export function createSignalRaw(value) {
|
|
|
18
29
|
|
|
19
30
|
function get() {
|
|
20
31
|
if (currentEffect) subs.add(currentEffect)
|
|
32
|
+
if (renderTracker) renderTracker.push(get)
|
|
21
33
|
return value
|
|
22
34
|
}
|
|
23
35
|
|
package/src/spinner.js
CHANGED
|
@@ -3,15 +3,24 @@ import { createSignal } from './signal.js'
|
|
|
3
3
|
import { useInterval, useTheme } from './hooks.js'
|
|
4
4
|
import { registerHook } from './renderer.js'
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const VARIANTS = {
|
|
7
|
+
dots: ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'],
|
|
8
|
+
line: ['|', '/', '-', '\\'],
|
|
9
|
+
circle: ['\u25D0', '\u25D3', '\u25D1', '\u25D2'],
|
|
10
|
+
bounce: ['\u2801', '\u2802', '\u2804', '\u2802'],
|
|
11
|
+
arrow: ['\u2190', '\u2196', '\u2191', '\u2197', '\u2192', '\u2198', '\u2193', '\u2199'],
|
|
12
|
+
square: ['\u25F0', '\u25F3', '\u25F2', '\u25F1'],
|
|
13
|
+
star: ['\u2736', '\u2738', '\u2739', '\u273A', '\u2739', '\u2738'],
|
|
14
|
+
}
|
|
7
15
|
|
|
8
|
-
export function Spinner({ label, color, interval = 80 }) {
|
|
16
|
+
export function Spinner({ label, color, interval = 80, variant = 'dots', frames }) {
|
|
9
17
|
const { accent = 'cyan' } = useTheme()
|
|
10
18
|
const c = color ?? accent
|
|
19
|
+
const f = frames ?? VARIANTS[variant] ?? VARIANTS.dots
|
|
11
20
|
const [frame, setFrame] = createSignal(0)
|
|
12
|
-
useInterval(() => setFrame(
|
|
21
|
+
useInterval(() => setFrame(i => (i + 1) % f.length), interval)
|
|
13
22
|
|
|
14
|
-
const children = [jsx('text', { style: { color: c }, children:
|
|
23
|
+
const children = [jsx('text', { style: { color: c }, children: f[frame()] })]
|
|
15
24
|
|
|
16
25
|
if (label != null) {
|
|
17
26
|
children.push(jsx('text', { children: ` ${label}` }))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx } from '../jsx-runtime.js'
|
|
2
|
+
|
|
3
|
+
const DIVIDER_CHARS = {
|
|
4
|
+
single: { h: '\u2500', v: '\u2502' },
|
|
5
|
+
double: { h: '\u2550', v: '\u2551' },
|
|
6
|
+
round: { h: '\u2500', v: '\u2502' },
|
|
7
|
+
bold: { h: '\u2501', v: '\u2503' },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseSize(s) {
|
|
11
|
+
if (typeof s === 'number') return { type: 'fixed', value: s }
|
|
12
|
+
const m = String(s).match(/^(\d*\.?\d+)fr$/)
|
|
13
|
+
return m ? { type: 'fr', value: parseFloat(m[1]) } : { type: 'fixed', value: parseInt(s) || 0 }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sizeToStyle(size, isRow) {
|
|
17
|
+
const parsed = parseSize(size)
|
|
18
|
+
if (parsed.type === 'fixed') return { [isRow ? 'width' : 'height']: parsed.value }
|
|
19
|
+
return { flexGrow: parsed.value }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SplitPane({ children, direction = 'row', sizes: sizesProp, border = 'single', borderColor, borderEdges, style }) {
|
|
23
|
+
const items = Array.isArray(children) ? children.filter(c => c != null && c !== true && c !== false) : children ? [children] : []
|
|
24
|
+
const n = items.length
|
|
25
|
+
if (n === 0) return null
|
|
26
|
+
|
|
27
|
+
const isRow = direction === 'row'
|
|
28
|
+
const chars = DIVIDER_CHARS[border] ?? DIVIDER_CHARS.single
|
|
29
|
+
const sizes = sizesProp ?? items.map(() => '1fr')
|
|
30
|
+
|
|
31
|
+
const elements = []
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
elements.push(
|
|
34
|
+
jsx('box', {
|
|
35
|
+
key: `p${i}`,
|
|
36
|
+
style: { ...sizeToStyle(sizes[i] ?? '1fr', isRow), flexDirection: 'column' },
|
|
37
|
+
children: items[i],
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if (i < n - 1) {
|
|
42
|
+
elements.push(
|
|
43
|
+
jsx('box', {
|
|
44
|
+
key: `d${i}`,
|
|
45
|
+
style: {
|
|
46
|
+
[isRow ? 'width' : 'height']: 1,
|
|
47
|
+
texture: isRow ? chars.v : chars.h,
|
|
48
|
+
textureColor: borderColor,
|
|
49
|
+
_divider: isRow ? 'vertical' : 'horizontal',
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return jsx('box', {
|
|
57
|
+
style: {
|
|
58
|
+
...style,
|
|
59
|
+
border: border || undefined,
|
|
60
|
+
borderColor,
|
|
61
|
+
borderEdges,
|
|
62
|
+
flexDirection: isRow ? 'row' : 'column',
|
|
63
|
+
},
|
|
64
|
+
children: elements,
|
|
65
|
+
})
|
|
66
|
+
}
|