@trendr/core 0.2.11 → 0.4.0
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 +284 -24
- package/index.js +6 -2
- package/package.json +29 -34
- package/src/animation.js +25 -12
- package/src/ansi.js +6 -0
- package/src/buffer.js +63 -50
- package/src/button.js +2 -2
- package/src/checkbox.js +2 -2
- package/src/diff-engine.js +313 -0
- package/src/diff-view.js +238 -0
- package/src/diff.js +2 -2
- package/src/dropdown.js +147 -0
- package/src/focus.js +89 -27
- package/src/hooks.js +43 -25
- package/src/hotkey.js +51 -16
- package/src/input.js +211 -72
- package/src/layout.js +34 -13
- package/src/list.js +29 -16
- package/src/markdown.js +177 -0
- package/src/menu.js +13 -6
- package/src/menubar.js +46 -115
- package/src/miller-nav.js +36 -11
- package/src/modal.js +3 -1
- package/src/pick-list.js +16 -17
- package/src/progress.js +3 -3
- package/src/radio.js +16 -5
- package/src/renderer.js +291 -50
- package/src/scheduler.js +28 -10
- package/src/scroll-box.js +12 -11
- package/src/scrollable-text.js +3 -2
- package/src/select.js +65 -173
- package/src/selection.js +92 -0
- package/src/shimmer.js +2 -2
- package/src/signal.js +20 -3
- package/src/table.js +8 -27
- package/src/tabs.js +10 -7
- package/src/task.js +3 -3
- package/src/text-area.js +96 -41
- package/src/text-input.js +70 -28
- package/src/wrap.js +205 -84
package/src/scrollable-text.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useInput, useMouse, useLayout, useTheme, useScrollDrag } from './hooks.
|
|
|
4
4
|
import { wordWrap } from './wrap.js'
|
|
5
5
|
|
|
6
6
|
export function ScrollableText({ content = '', focused = true, scrollOffset: offsetProp, onScroll, width: widthProp, scrollbar = false, wrap = true, thumbChar = '\u2588', trackChar = '\u2502' }) {
|
|
7
|
-
const { accent = 'cyan' } = useTheme()
|
|
7
|
+
const { accent = 'cyan', muted = 'gray' } = useTheme()
|
|
8
8
|
const [offsetInternal, setOffsetInternal] = createSignal(0)
|
|
9
9
|
const layout = useLayout()
|
|
10
10
|
|
|
@@ -43,6 +43,7 @@ export function ScrollableText({ content = '', focused = true, scrollOffset: off
|
|
|
43
43
|
if (lines.length <= (h || 1)) return
|
|
44
44
|
const { x, y } = event
|
|
45
45
|
if (x < layout.x || x >= layout.x + layout.width || y < layout.y || y >= layout.y + layout.height) return
|
|
46
|
+
if (event.direction !== 'up' && event.direction !== 'down') return
|
|
46
47
|
if (event.direction === 'up') setOffset(clamp(clamped - 3))
|
|
47
48
|
else setOffset(clamp(clamped + 3))
|
|
48
49
|
event.stopPropagation()
|
|
@@ -81,7 +82,7 @@ export function ScrollableText({ content = '', focused = true, scrollOffset: off
|
|
|
81
82
|
const children = visible.map((line, i) => {
|
|
82
83
|
const isThumb = i >= thumbStart && i < thumbStart + thumbH
|
|
83
84
|
const barChar = isThumb ? thumbChar : trackChar
|
|
84
|
-
const barColor = isThumb ? (focused ? accent :
|
|
85
|
+
const barColor = isThumb ? (focused ? accent : muted) : muted
|
|
85
86
|
|
|
86
87
|
return jsx('box', {
|
|
87
88
|
key: i,
|
package/src/select.js
CHANGED
|
@@ -1,153 +1,22 @@
|
|
|
1
1
|
import { jsx } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
3
|
import { useInput, useMouse, useLayout, useTheme } from './hooks.js'
|
|
4
|
-
import {
|
|
4
|
+
import { getInstanceLayout, getContext } from './renderer.js'
|
|
5
|
+
import { Dropdown, followCursor, placeDropdown, overlayDropdown } from './dropdown.js'
|
|
5
6
|
|
|
6
|
-
|
|
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
|
-
}
|
|
7
|
+
const itemLabel = (item) => String(item?.label ?? item)
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
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' }) {
|
|
141
|
-
const { accent = 'cyan' } = useTheme()
|
|
9
|
+
export function Select({ items = [], selected, onChange, onFocus, focused = false, overlay = false, maxVisible = 10, placeholder = 'select...', renderItem, style: userStyle, openIcon = '▲', closedIcon = '▼' }) {
|
|
10
|
+
const { accent = 'cyan', accentText = 'black', muted = 'gray' } = useTheme()
|
|
142
11
|
const defaults = {
|
|
143
12
|
border: 'single',
|
|
144
13
|
borderColor: accent,
|
|
145
14
|
bg: null,
|
|
146
15
|
cursorBg: accent,
|
|
147
|
-
cursorTextColor:
|
|
16
|
+
cursorTextColor: accentText,
|
|
148
17
|
color: null,
|
|
149
18
|
focusedBg: accent,
|
|
150
|
-
focusedColor:
|
|
19
|
+
focusedColor: accentText,
|
|
151
20
|
}
|
|
152
21
|
const s = { ...defaults, ...userStyle }
|
|
153
22
|
|
|
@@ -172,7 +41,11 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
|
|
|
172
41
|
const len = items.length
|
|
173
42
|
if (key === 'up' || key === 'k') { setCursor(c => Math.max(0, c - 1)); event.stopPropagation() }
|
|
174
43
|
else if (key === 'down' || key === 'j') { setCursor(c => Math.min(len - 1, c + 1)); event.stopPropagation() }
|
|
175
|
-
else if (key === 'return' || key === 'space') {
|
|
44
|
+
else if (key === 'return' || key === 'space') {
|
|
45
|
+
if (len > 0) onChange?.(items[Math.min(cursor(), len - 1)])
|
|
46
|
+
setOpen(false)
|
|
47
|
+
event.stopPropagation()
|
|
48
|
+
}
|
|
176
49
|
else if (key === 'escape') { setOpen(false); event.stopPropagation() }
|
|
177
50
|
})
|
|
178
51
|
|
|
@@ -181,6 +54,7 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
|
|
|
181
54
|
useMouse((event) => {
|
|
182
55
|
if (event.action === 'scroll') {
|
|
183
56
|
if (!focused || !open()) return
|
|
57
|
+
if (event.direction !== 'up' && event.direction !== 'down') return
|
|
184
58
|
const len = items.length
|
|
185
59
|
if (event.direction === 'up') setCursor(c => Math.max(0, c - 1))
|
|
186
60
|
else setCursor(c => Math.min(len - 1, c + 1))
|
|
@@ -193,6 +67,12 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
|
|
|
193
67
|
const onCollapsed = x >= layout.x && x < layout.x + layout.width && y === layout.y
|
|
194
68
|
|
|
195
69
|
if (onCollapsed) {
|
|
70
|
+
// an unfocused select would open a keyboard-dead dropdown - only allow
|
|
71
|
+
// click-to-open when focused, or when the app can move focus here
|
|
72
|
+
if (!focused) {
|
|
73
|
+
if (!onFocus) return
|
|
74
|
+
onFocus()
|
|
75
|
+
}
|
|
196
76
|
if (open()) {
|
|
197
77
|
setOpen(false)
|
|
198
78
|
} else {
|
|
@@ -204,66 +84,78 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
|
|
|
204
84
|
}
|
|
205
85
|
})
|
|
206
86
|
|
|
207
|
-
const display = selected
|
|
87
|
+
const display = selected != null ? itemLabel(selected) : placeholder
|
|
208
88
|
const collapsed = jsx('text', {
|
|
209
89
|
style: {
|
|
210
90
|
bg: focused ? s.focusedBg : null,
|
|
211
|
-
color: focused ? s.focusedColor : (selected ? s.color :
|
|
91
|
+
color: focused ? s.focusedColor : (selected != null ? s.color : muted),
|
|
212
92
|
bold: focused,
|
|
213
93
|
},
|
|
214
94
|
children: `${open() ? openIcon : closedIcon} ${display}`,
|
|
215
95
|
})
|
|
216
96
|
|
|
217
|
-
if (!open()) return collapsed
|
|
97
|
+
if (!open() || items.length === 0) return collapsed
|
|
218
98
|
|
|
219
|
-
let
|
|
99
|
+
let visibleCount = Math.min(items.length, maxVisible)
|
|
100
|
+
let direction = 'down'
|
|
101
|
+
let instLayout = null
|
|
102
|
+
let termW = 80
|
|
103
|
+
let termH = 24
|
|
220
104
|
|
|
221
105
|
if (overlay) {
|
|
222
|
-
|
|
106
|
+
instLayout = getInstanceLayout()
|
|
223
107
|
const ctx = getContext()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const visibleCount = maxRows
|
|
231
|
-
|
|
232
|
-
const cur = cursor()
|
|
233
|
-
const sc = scroll()
|
|
234
|
-
let newScroll = sc
|
|
235
|
-
if (cur < sc) newScroll = cur
|
|
236
|
-
else if (cur >= sc + visibleCount) newScroll = cur - visibleCount + 1
|
|
237
|
-
newScroll = Math.max(0, Math.min(newScroll, items.length - visibleCount))
|
|
238
|
-
if (newScroll !== sc) setScroll(newScroll)
|
|
239
|
-
|
|
240
|
-
const handleSelect = (item) => {
|
|
241
|
-
onSelect?.(item)
|
|
242
|
-
setOpen(false)
|
|
108
|
+
termH = ctx?.stream?.rows ?? 24
|
|
109
|
+
termW = ctx?.stream?.columns ?? 80
|
|
110
|
+
const placed = placeDropdown({ anchorTop: instLayout.y, itemCount: items.length, maxVisible, termH })
|
|
111
|
+
direction = placed.direction
|
|
112
|
+
visibleCount = placed.visibleCount
|
|
243
113
|
}
|
|
244
114
|
|
|
245
|
-
const
|
|
115
|
+
const cur = Math.min(cursor(), items.length - 1)
|
|
116
|
+
const newScroll = followCursor(cur, scroll(), visibleCount, items.length)
|
|
117
|
+
if (newScroll !== scroll()) setScroll(newScroll)
|
|
246
118
|
|
|
247
|
-
const maxLen = items.reduce((m, v) => Math.max(m, v.length), 0)
|
|
119
|
+
const maxLen = items.reduce((m, v) => Math.max(m, itemLabel(v).length), 0)
|
|
248
120
|
const scrollable = items.length > visibleCount
|
|
249
121
|
const dropWidth = maxLen + 4 + (scrollable ? 2 : 0)
|
|
250
122
|
|
|
251
|
-
const
|
|
123
|
+
const renderRow = (item, { index, isCursor }) => {
|
|
124
|
+
if (renderItem) {
|
|
125
|
+
return jsx('box', {
|
|
126
|
+
style: { bg: isCursor ? s.cursorBg : s.bg, flexGrow: 1 },
|
|
127
|
+
children: renderItem(item, { selected: isCursor, index }),
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
return jsx('box', {
|
|
131
|
+
style: { bg: isCursor ? s.cursorBg : s.bg, paddingX: 1, flexGrow: 1 },
|
|
132
|
+
children: jsx('text', {
|
|
133
|
+
style: { color: isCursor ? s.cursorTextColor : s.color },
|
|
134
|
+
children: itemLabel(item),
|
|
135
|
+
}),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const dropdown = jsx(Dropdown, {
|
|
252
140
|
items,
|
|
253
141
|
cursor: cur,
|
|
254
142
|
scroll: newScroll,
|
|
255
143
|
visibleCount,
|
|
256
|
-
|
|
257
|
-
|
|
144
|
+
width: dropWidth,
|
|
145
|
+
onSubmit: (item) => {
|
|
146
|
+
onChange?.(item)
|
|
147
|
+
setOpen(false)
|
|
148
|
+
},
|
|
149
|
+
onClose: () => setOpen(false),
|
|
258
150
|
onCursorChange: (idx) => setCursor(idx),
|
|
259
|
-
|
|
260
|
-
style: s,
|
|
261
|
-
accent,
|
|
262
|
-
dropWidth,
|
|
151
|
+
renderRow,
|
|
152
|
+
style: { border: s.border, borderColor: s.borderColor, bg: s.bg, accent },
|
|
263
153
|
})
|
|
264
154
|
|
|
265
155
|
if (overlay) {
|
|
266
|
-
|
|
156
|
+
const dropdownH = visibleCount + 2
|
|
157
|
+
const absY = direction === 'up' ? instLayout.y - dropdownH : instLayout.y + 1
|
|
158
|
+
overlayDropdown({ x: instLayout.x, y: absY, termW, termH, dropdown })
|
|
267
159
|
return collapsed
|
|
268
160
|
}
|
|
269
161
|
|
package/src/selection.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useMouse } from './hooks.js'
|
|
2
|
+
import { getContext, registerHook } from './renderer.js'
|
|
3
|
+
import { osc52Copy } from './ansi.js'
|
|
4
|
+
|
|
5
|
+
function normalize(a, b) {
|
|
6
|
+
const forward = a.y < b.y || (a.y === b.y && a.x <= b.x)
|
|
7
|
+
return forward
|
|
8
|
+
? { sx: a.x, sy: a.y, ex: b.x, ey: b.y }
|
|
9
|
+
: { sx: b.x, sy: b.y, ex: a.x, ey: a.y }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// reads the selected region back out of the painted cell buffer. rows flagged
|
|
13
|
+
// as soft wraps rejoin the previous row with a space, so copied prose comes
|
|
14
|
+
// out as one paragraph regardless of the terminal width; unflagged rows keep
|
|
15
|
+
// their newline, so code copies with its line structure intact. all hard rows
|
|
16
|
+
// are extracted column-aligned and share one dedent, which strips the screen
|
|
17
|
+
// padding common to the block while preserving relative code indentation
|
|
18
|
+
export function extractSelectionText(buf, sel) {
|
|
19
|
+
const rows = []
|
|
20
|
+
const lastY = Math.min(sel.ey, buf.height - 1)
|
|
21
|
+
for (let y = Math.max(0, sel.sy); y <= lastY; y++) {
|
|
22
|
+
const to = y === sel.ey ? Math.min(sel.ex, buf.width - 1) : buf.width - 1
|
|
23
|
+
let text = ''
|
|
24
|
+
for (let x = 0; x <= to; x++) {
|
|
25
|
+
const ch = buf.cells[y * buf.width + x].ch
|
|
26
|
+
if (ch === '') continue
|
|
27
|
+
text += y === sel.sy && x < sel.sx ? ' ' : ch
|
|
28
|
+
}
|
|
29
|
+
rows.push({ text: text.replace(/\s+$/, ''), soft: !!buf.softWrap[y] })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let indent = Infinity
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
if (row.soft || row.text === '') continue
|
|
35
|
+
indent = Math.min(indent, row.text.match(/^ */)[0].length)
|
|
36
|
+
}
|
|
37
|
+
if (indent === Infinity) indent = 0
|
|
38
|
+
|
|
39
|
+
let out = ''
|
|
40
|
+
for (let i = 0; i < rows.length; i++) {
|
|
41
|
+
const row = rows[i]
|
|
42
|
+
if (i === 0) out = row.text.slice(indent)
|
|
43
|
+
else if (row.soft) out += (out === '' || out.endsWith('\n') ? '' : ' ') + row.text.trim()
|
|
44
|
+
else out += '\n' + (row.text === '' ? '' : row.text.slice(indent))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return out.replace(/^\n+/, '').replace(/\s+$/, '')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// terminal-style click-drag text selection over the painted buffer. while
|
|
51
|
+
// dragging, the covered cells render inverse; on release the text is
|
|
52
|
+
// extracted, written to the system clipboard via OSC 52 (unless copy is
|
|
53
|
+
// false), and passed to onCopy. registered handlers never stop propagation
|
|
54
|
+
// on press, so clicks still reach interactive components underneath
|
|
55
|
+
export function useSelection({ onCopy, copy = true } = {}) {
|
|
56
|
+
const state = registerHook(() => ({ anchor: null, dragging: false }))
|
|
57
|
+
const ctx = getContext()
|
|
58
|
+
if (!ctx) throw new Error('useSelection must be called within a mounted component')
|
|
59
|
+
|
|
60
|
+
useMouse((event) => {
|
|
61
|
+
if (event.action === 'press' && event.button === 'left') {
|
|
62
|
+
state.anchor = { x: event.x, y: event.y }
|
|
63
|
+
state.dragging = false
|
|
64
|
+
if (ctx.selection) {
|
|
65
|
+
ctx.selection = null
|
|
66
|
+
ctx.repaint()
|
|
67
|
+
}
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (event.action === 'drag' && state.anchor) {
|
|
72
|
+
state.dragging = true
|
|
73
|
+
ctx.selection = normalize(state.anchor, { x: event.x, y: event.y })
|
|
74
|
+
ctx.repaint()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (event.action === 'release') {
|
|
79
|
+
if (state.dragging && ctx.selection) {
|
|
80
|
+
const text = extractSelectionText(ctx.getPaintBuffer(), ctx.selection)
|
|
81
|
+
ctx.selection = null
|
|
82
|
+
ctx.repaint()
|
|
83
|
+
if (text) {
|
|
84
|
+
if (copy) ctx.stream.write(osc52Copy(text))
|
|
85
|
+
if (onCopy) onCopy(text)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
state.anchor = null
|
|
89
|
+
state.dragging = false
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
}
|
package/src/shimmer.js
CHANGED
|
@@ -28,8 +28,8 @@ function lerpColor(a, b, t) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
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 ??
|
|
31
|
+
const { accent = 'cyan', muted = 'gray' } = useTheme()
|
|
32
|
+
const baseColor = color ?? muted
|
|
33
33
|
const hlColor = highlight ?? accent
|
|
34
34
|
|
|
35
35
|
const text = typeof children === 'string' ? children : String(children ?? '')
|
package/src/signal.js
CHANGED
|
@@ -38,10 +38,16 @@ export function createSignalRaw(value) {
|
|
|
38
38
|
if (v === value) return
|
|
39
39
|
value = v
|
|
40
40
|
if (batchDepth > 0) {
|
|
41
|
-
for (const s of subs)
|
|
41
|
+
for (const s of subs) {
|
|
42
|
+
if (s.disposed) subs.delete(s)
|
|
43
|
+
else pendingEffects.add(s)
|
|
44
|
+
}
|
|
42
45
|
} else {
|
|
43
46
|
const snapshot = [...subs]
|
|
44
|
-
for (const s of snapshot)
|
|
47
|
+
for (const s of snapshot) {
|
|
48
|
+
if (s.disposed) subs.delete(s)
|
|
49
|
+
else s.run()
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
if (schedulerHook && batchDepth === 0) schedulerHook()
|
|
47
53
|
}
|
|
@@ -56,11 +62,13 @@ export function createSignal(value) {
|
|
|
56
62
|
return createSignalRaw(value)
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
export function
|
|
65
|
+
export function createEffectRaw(fn) {
|
|
60
66
|
const effect = {
|
|
61
67
|
fn,
|
|
62
68
|
cleanup: null,
|
|
69
|
+
disposed: false,
|
|
63
70
|
run() {
|
|
71
|
+
if (effect.disposed) return
|
|
64
72
|
if (effect.cleanup) effect.cleanup()
|
|
65
73
|
const prev = currentEffect
|
|
66
74
|
currentEffect = effect
|
|
@@ -80,6 +88,13 @@ export function createEffect(fn) {
|
|
|
80
88
|
return effect
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
export function createEffect(fn) {
|
|
92
|
+
if (hookRegistrar) {
|
|
93
|
+
return hookRegistrar(() => createEffectRaw(fn))
|
|
94
|
+
}
|
|
95
|
+
return createEffectRaw(fn)
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
export function createMemo(fn) {
|
|
84
99
|
const [get, set] = createSignal(undefined)
|
|
85
100
|
createEffect(() => set(fn()))
|
|
@@ -144,7 +159,9 @@ export function createScope(fn) {
|
|
|
144
159
|
export function disposeScope(scope) {
|
|
145
160
|
for (const child of scope.children) disposeScope(child)
|
|
146
161
|
for (const effect of scope.effects) {
|
|
162
|
+
effect.disposed = true
|
|
147
163
|
if (effect.cleanup) effect.cleanup()
|
|
164
|
+
effect.cleanup = null
|
|
148
165
|
}
|
|
149
166
|
for (const fn of scope.cleanups) fn()
|
|
150
167
|
scope.effects.length = 0
|
package/src/table.js
CHANGED
|
@@ -4,8 +4,8 @@ import { List } from './list.js'
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_SEP = { left: '', fill: '\u2500', right: '' }
|
|
6
6
|
|
|
7
|
-
export function Table({ columns, data, selected, onSelect, focused = true, separator = false, separatorChars, renderItem, scrollbar, stickyHeader = false, gap = 0, itemHeight = 1, scrolloff = 2, columnGap = 1 }) {
|
|
8
|
-
const { accent = 'cyan' } = useTheme()
|
|
7
|
+
export function Table({ columns = [], data, selected, onSelect, focused = true, separator = false, separatorChars, renderItem, scrollbar, stickyHeader = false, gap = 0, itemHeight = 1, scrolloff = 2, columnGap = 1 }) {
|
|
8
|
+
const { accent = 'cyan', accentText = 'black', muted = 'gray' } = useTheme()
|
|
9
9
|
|
|
10
10
|
const headerRow = jsxs('box', {
|
|
11
11
|
style: { flexDirection: 'row', gap: columnGap },
|
|
@@ -44,7 +44,7 @@ export function Table({ columns, data, selected, onSelect, focused = true, separ
|
|
|
44
44
|
|
|
45
45
|
const defaultRenderItem = (row, { selected: isSel }) =>
|
|
46
46
|
jsxs('box', {
|
|
47
|
-
style: { flexDirection: 'row', gap: columnGap, bg: isSel ? (focused ? accent :
|
|
47
|
+
style: { flexDirection: 'row', gap: columnGap, bg: isSel ? (focused ? accent : muted) : null },
|
|
48
48
|
children: columns.map((col, i) =>
|
|
49
49
|
jsx('text', {
|
|
50
50
|
key: i,
|
|
@@ -53,44 +53,25 @@ export function Table({ columns, data, selected, onSelect, focused = true, separ
|
|
|
53
53
|
flexGrow: col.flexGrow,
|
|
54
54
|
overflow: 'truncate',
|
|
55
55
|
paddingX: col.paddingX ?? 1,
|
|
56
|
-
color: isSel && focused ?
|
|
56
|
+
color: isSel && focused ? accentText : (col.color ?? null),
|
|
57
57
|
},
|
|
58
58
|
children: col.render ? col.render(row, isSel) : String(row[col.key] ?? ''),
|
|
59
59
|
})
|
|
60
60
|
),
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
return jsx(List, {
|
|
64
64
|
items: data,
|
|
65
65
|
selected,
|
|
66
66
|
onSelect,
|
|
67
67
|
focused,
|
|
68
|
+
header,
|
|
69
|
+
headerHeight: separator ? 2 : 1,
|
|
70
|
+
stickyHeader,
|
|
68
71
|
scrollbar,
|
|
69
72
|
gap,
|
|
70
73
|
itemHeight,
|
|
71
74
|
scrolloff,
|
|
72
75
|
renderItem: renderItem || defaultRenderItem,
|
|
73
76
|
})
|
|
74
|
-
|
|
75
|
-
if (!stickyHeader) {
|
|
76
|
-
return jsx(List, {
|
|
77
|
-
items: data,
|
|
78
|
-
selected,
|
|
79
|
-
onSelect,
|
|
80
|
-
focused,
|
|
81
|
-
header,
|
|
82
|
-
headerHeight: separator ? 2 : 1,
|
|
83
|
-
scrollbar,
|
|
84
|
-
stickyHeader: true,
|
|
85
|
-
gap,
|
|
86
|
-
itemHeight,
|
|
87
|
-
scrolloff,
|
|
88
|
-
renderItem: renderItem || defaultRenderItem,
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return jsxs('box', {
|
|
93
|
-
style: { flexDirection: 'column', flexGrow: 1 },
|
|
94
|
-
children: [header, list],
|
|
95
|
-
})
|
|
96
77
|
}
|
package/src/tabs.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
2
|
import { useInput, useTheme } from './hooks.js'
|
|
3
3
|
|
|
4
|
-
export function Tabs({ items, selected,
|
|
5
|
-
const { accent = 'cyan' } = useTheme()
|
|
4
|
+
export function Tabs({ items = [], selected, onChange, focused = true }) {
|
|
5
|
+
const { accent = 'cyan', accentText = 'black', muted = 'gray' } = useTheme()
|
|
6
6
|
|
|
7
|
-
useInput((
|
|
7
|
+
useInput((event) => {
|
|
8
8
|
if (!focused) return
|
|
9
9
|
|
|
10
|
+
const { key } = event
|
|
10
11
|
const idx = items.indexOf(selected)
|
|
11
12
|
if (idx === -1) return
|
|
12
13
|
|
|
13
14
|
if (key === 'left' || key === 'shift-tab') {
|
|
14
|
-
|
|
15
|
+
onChange?.(items[(idx - 1 + items.length) % items.length])
|
|
16
|
+
event.stopPropagation()
|
|
15
17
|
} else if (key === 'right' || key === 'tab') {
|
|
16
|
-
|
|
18
|
+
onChange?.(items[(idx + 1) % items.length])
|
|
19
|
+
event.stopPropagation()
|
|
17
20
|
}
|
|
18
21
|
})
|
|
19
22
|
|
|
@@ -21,11 +24,11 @@ export function Tabs({ items, selected, onSelect, focused = true }) {
|
|
|
21
24
|
const isSelected = item === selected
|
|
22
25
|
let style
|
|
23
26
|
if (isSelected && focused) {
|
|
24
|
-
style = { bg: accent, color:
|
|
27
|
+
style = { bg: accent, color: accentText, bold: true }
|
|
25
28
|
} else if (isSelected) {
|
|
26
29
|
style = { inverse: true, bold: true }
|
|
27
30
|
} else {
|
|
28
|
-
style = { color:
|
|
31
|
+
style = { color: muted }
|
|
29
32
|
}
|
|
30
33
|
return jsx('text', { style, children: ` ${item} ` })
|
|
31
34
|
})
|
package/src/task.js
CHANGED
|
@@ -10,7 +10,7 @@ const DEFAULT_ICONS = {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function Task({ run, label, successLabel, errorLabel, icon, color, immediate = true }) {
|
|
13
|
-
const { accent = 'cyan' } = useTheme()
|
|
13
|
+
const { accent = 'cyan', muted = 'gray' } = useTheme()
|
|
14
14
|
const { status, data, error } = useAsync(run, { immediate })
|
|
15
15
|
|
|
16
16
|
const s = status()
|
|
@@ -37,7 +37,7 @@ export function Task({ run, label, successLabel, errorLabel, icon, color, immedi
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
return jsxs('box', { style: { flexDirection: 'row' }, children: [
|
|
40
|
-
jsx('text', { style: { color:
|
|
41
|
-
jsx('text', { style: { color:
|
|
40
|
+
jsx('text', { style: { color: muted }, children: icons.idle }),
|
|
41
|
+
jsx('text', { style: { color: muted }, children: ` ${label}` }),
|
|
42
42
|
]})
|
|
43
43
|
}
|