@trendr/core 0.1.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/LICENSE +21 -0
- package/README.md +526 -0
- package/index.js +22 -0
- package/jsx-runtime.js +9 -0
- package/package.json +35 -0
- package/src/ansi.js +149 -0
- package/src/buffer.js +101 -0
- package/src/button.js +26 -0
- package/src/checkbox.js +28 -0
- package/src/components.js +13 -0
- package/src/diff.js +51 -0
- package/src/element.js +81 -0
- package/src/focus.js +135 -0
- package/src/hooks.js +63 -0
- package/src/hotkey.js +35 -0
- package/src/input.js +143 -0
- package/src/layout.js +312 -0
- package/src/list.js +46 -0
- package/src/modal.js +44 -0
- package/src/progress.js +21 -0
- package/src/radio.js +52 -0
- package/src/renderer.js +478 -0
- package/src/scheduler.js +55 -0
- package/src/scrollable-text.js +75 -0
- package/src/select.js +151 -0
- package/src/signal.js +159 -0
- package/src/spinner.js +21 -0
- package/src/table.js +51 -0
- package/src/tabs.js +30 -0
- package/src/text-area.js +286 -0
- package/src/text-input.js +131 -0
- package/src/toast.js +61 -0
- package/src/wrap.js +133 -0
package/src/input.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const SPECIAL_KEYS = {
|
|
2
|
+
'\x1b[A': 'up',
|
|
3
|
+
'\x1b[B': 'down',
|
|
4
|
+
'\x1b[C': 'right',
|
|
5
|
+
'\x1b[D': 'left',
|
|
6
|
+
'\x1b[H': 'home',
|
|
7
|
+
'\x1b[F': 'end',
|
|
8
|
+
'\x1b[2~': 'insert',
|
|
9
|
+
'\x1b[3~': 'delete',
|
|
10
|
+
'\x1b[5~': 'pageup',
|
|
11
|
+
'\x1b[6~': 'pagedown',
|
|
12
|
+
'\x1bOP': 'f1',
|
|
13
|
+
'\x1bOQ': 'f2',
|
|
14
|
+
'\x1bOR': 'f3',
|
|
15
|
+
'\x1bOS': 'f4',
|
|
16
|
+
'\x1b[15~': 'f5',
|
|
17
|
+
'\x1b[17~': 'f6',
|
|
18
|
+
'\x1b[18~': 'f7',
|
|
19
|
+
'\x1b[19~': 'f8',
|
|
20
|
+
'\x1b[20~': 'f9',
|
|
21
|
+
'\x1b[21~': 'f10',
|
|
22
|
+
'\x1b[23~': 'f11',
|
|
23
|
+
'\x1b[24~': 'f12',
|
|
24
|
+
'\r': 'return',
|
|
25
|
+
'\n': 'return',
|
|
26
|
+
'\t': 'tab',
|
|
27
|
+
'\x1b[Z': 'shift-tab',
|
|
28
|
+
'\x7f': 'backspace',
|
|
29
|
+
'\x1b': 'escape',
|
|
30
|
+
' ': 'space',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseKey(data) {
|
|
34
|
+
const raw = typeof data === 'string' ? data : data.toString()
|
|
35
|
+
|
|
36
|
+
if (SPECIAL_KEYS[raw]) {
|
|
37
|
+
return { key: SPECIAL_KEYS[raw], ctrl: false, meta: false, shift: false, raw }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (raw.length === 1) {
|
|
41
|
+
const code = raw.charCodeAt(0)
|
|
42
|
+
|
|
43
|
+
if (code >= 1 && code <= 26) {
|
|
44
|
+
return {
|
|
45
|
+
key: String.fromCharCode(code + 96),
|
|
46
|
+
ctrl: true,
|
|
47
|
+
meta: false,
|
|
48
|
+
shift: false,
|
|
49
|
+
raw,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { key: raw, ctrl: false, meta: false, shift: false, raw }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (raw.startsWith('\x1b') && raw.length === 2) {
|
|
57
|
+
return { key: raw[1], ctrl: false, meta: true, shift: false, raw }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { key: raw, ctrl: false, meta: false, shift: false, raw }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function splitKeys(data) {
|
|
64
|
+
const raw = typeof data === 'string' ? data : data.toString()
|
|
65
|
+
const keys = []
|
|
66
|
+
let i = 0
|
|
67
|
+
|
|
68
|
+
while (i < raw.length) {
|
|
69
|
+
if (raw[i] === '\x1b') {
|
|
70
|
+
if (i + 1 < raw.length && raw[i + 1] === '[') {
|
|
71
|
+
let j = i + 2
|
|
72
|
+
while (j < raw.length && raw[j] >= '0' && raw[j] <= '9') j++
|
|
73
|
+
if (j < raw.length && raw[j] === ';') {
|
|
74
|
+
j++
|
|
75
|
+
while (j < raw.length && raw[j] >= '0' && raw[j] <= '9') j++
|
|
76
|
+
}
|
|
77
|
+
if (j < raw.length) j++
|
|
78
|
+
keys.push(raw.slice(i, j))
|
|
79
|
+
i = j
|
|
80
|
+
} else if (i + 1 < raw.length && raw[i + 1] === 'O') {
|
|
81
|
+
const end = Math.min(i + 3, raw.length)
|
|
82
|
+
keys.push(raw.slice(i, end))
|
|
83
|
+
i = end
|
|
84
|
+
} else if (i + 1 < raw.length) {
|
|
85
|
+
keys.push(raw.slice(i, i + 2))
|
|
86
|
+
i += 2
|
|
87
|
+
} else {
|
|
88
|
+
keys.push(raw[i])
|
|
89
|
+
i++
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
keys.push(raw[i])
|
|
93
|
+
i++
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return keys
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createInputHandler(stream) {
|
|
101
|
+
const listeners = new Set()
|
|
102
|
+
|
|
103
|
+
function dispatch(keyStr) {
|
|
104
|
+
const event = parseKey(keyStr)
|
|
105
|
+
event.stopPropagation = () => { event._stopped = true }
|
|
106
|
+
const snapshot = [...listeners].reverse()
|
|
107
|
+
for (const fn of snapshot) {
|
|
108
|
+
fn(event)
|
|
109
|
+
if (event._stopped) break
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onData(data) {
|
|
114
|
+
for (const key of splitKeys(data)) {
|
|
115
|
+
dispatch(key)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let attached = false
|
|
120
|
+
|
|
121
|
+
function attach() {
|
|
122
|
+
if (attached) return
|
|
123
|
+
attached = true
|
|
124
|
+
stream.on('data', onData)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function detach() {
|
|
128
|
+
if (!attached) return
|
|
129
|
+
attached = false
|
|
130
|
+
stream.off('data', onData)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function onKey(fn) {
|
|
134
|
+
listeners.add(fn)
|
|
135
|
+
if (listeners.size === 1) attach()
|
|
136
|
+
return () => {
|
|
137
|
+
listeners.delete(fn)
|
|
138
|
+
if (listeners.size === 0) detach()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { onKey, attach, detach }
|
|
143
|
+
}
|
package/src/layout.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { wordWrap, measureText } from './wrap.js'
|
|
2
|
+
import { Fragment } from './element.js'
|
|
3
|
+
|
|
4
|
+
export function computeLayout(node, rect) {
|
|
5
|
+
if (!node) return
|
|
6
|
+
|
|
7
|
+
if (node._resolved) {
|
|
8
|
+
computeLayout(node._resolved, rect)
|
|
9
|
+
node._layout = node._resolved._layout
|
|
10
|
+
node._availableRect = rect
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (node.type === Fragment) {
|
|
15
|
+
node._layout = rect
|
|
16
|
+
if (node._resolvedChildren) {
|
|
17
|
+
for (const child of node._resolvedChildren) {
|
|
18
|
+
computeLayout(child, rect)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const style = node.props?.style ?? {}
|
|
25
|
+
|
|
26
|
+
const absW = typeof style.width === 'number' ? style.width : null
|
|
27
|
+
const absH = typeof style.height === 'number' ? style.height : null
|
|
28
|
+
|
|
29
|
+
const box = {
|
|
30
|
+
x: rect.x,
|
|
31
|
+
y: rect.y,
|
|
32
|
+
width: clampSize(absW != null ? Math.min(absW, rect.width) : rect.width, style.minWidth, style.maxWidth),
|
|
33
|
+
height: clampSize(absH != null ? Math.min(absH, rect.height) : rect.height, style.minHeight, style.maxHeight),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (node.type === 'text') {
|
|
37
|
+
const text = extractText(node)
|
|
38
|
+
if (text) {
|
|
39
|
+
const lines = wordWrap(text, box.width)
|
|
40
|
+
if (style.height == null) box.height = Math.min(lines.length, rect.height)
|
|
41
|
+
} else {
|
|
42
|
+
if (style.height == null) box.height = 1
|
|
43
|
+
}
|
|
44
|
+
node._layout = box
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
node._layout = box
|
|
49
|
+
|
|
50
|
+
const children = node._resolvedChildren
|
|
51
|
+
if (!children || children.length === 0) return
|
|
52
|
+
|
|
53
|
+
const pad = resolvePadding(style)
|
|
54
|
+
const border = style.border ? 1 : 0
|
|
55
|
+
const innerX = box.x + pad.left + border
|
|
56
|
+
const innerY = box.y + pad.top + border
|
|
57
|
+
const innerW = Math.max(0, box.width - pad.left - pad.right - border * 2)
|
|
58
|
+
const innerH = Math.max(0, box.height - pad.top - pad.bottom - border * 2)
|
|
59
|
+
|
|
60
|
+
const isRow = style.flexDirection === 'row'
|
|
61
|
+
const gap = style.gap ?? 0
|
|
62
|
+
|
|
63
|
+
layoutFlex(children, {
|
|
64
|
+
x: innerX,
|
|
65
|
+
y: innerY,
|
|
66
|
+
width: innerW,
|
|
67
|
+
height: innerH,
|
|
68
|
+
isRow,
|
|
69
|
+
gap,
|
|
70
|
+
justifyContent: style.justifyContent ?? 'flex-start',
|
|
71
|
+
alignItems: style.alignItems ?? 'stretch',
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function layoutFlex(children, ctx) {
|
|
76
|
+
const { x, y, width, height, isRow, gap, justifyContent, alignItems } = ctx
|
|
77
|
+
const mainSize = isRow ? width : height
|
|
78
|
+
const crossSize = isRow ? height : width
|
|
79
|
+
const totalGaps = Math.max(0, children.length - 1) * gap
|
|
80
|
+
|
|
81
|
+
let usedMain = totalGaps
|
|
82
|
+
let totalFlex = 0
|
|
83
|
+
const childInfo = []
|
|
84
|
+
|
|
85
|
+
for (const child of children) {
|
|
86
|
+
const cs = childStyle(child)
|
|
87
|
+
const grow = cs.flexGrow ?? cs.flex ?? 0
|
|
88
|
+
const margin = resolveMargin(cs)
|
|
89
|
+
const marginMain = isRow ? margin.left + margin.right : margin.top + margin.bottom
|
|
90
|
+
const marginCross = isRow ? margin.top + margin.bottom : margin.left + margin.right
|
|
91
|
+
|
|
92
|
+
if (grow > 0) {
|
|
93
|
+
const minMain = isRow ? (cs.minWidth ?? 0) : (cs.minHeight ?? 0)
|
|
94
|
+
usedMain += minMain + marginMain
|
|
95
|
+
totalFlex += grow
|
|
96
|
+
childInfo.push({ child, cs, grow, minMain, marginMain, marginCross, margin, measured: null })
|
|
97
|
+
} else {
|
|
98
|
+
const measured = measureChild(child, cs, isRow, width, height)
|
|
99
|
+
const childMain = isRow ? measured.width : measured.height
|
|
100
|
+
usedMain += childMain + marginMain
|
|
101
|
+
childInfo.push({ child, cs, grow: 0, minMain: childMain, marginMain, marginCross, margin, measured })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const remaining = Math.max(0, mainSize - usedMain)
|
|
106
|
+
|
|
107
|
+
let mainOffset = 0
|
|
108
|
+
let spaceBetween = 0
|
|
109
|
+
|
|
110
|
+
if (totalFlex === 0) {
|
|
111
|
+
switch (justifyContent) {
|
|
112
|
+
case 'center':
|
|
113
|
+
mainOffset = Math.floor(remaining / 2)
|
|
114
|
+
break
|
|
115
|
+
case 'flex-end':
|
|
116
|
+
mainOffset = remaining
|
|
117
|
+
break
|
|
118
|
+
case 'space-between':
|
|
119
|
+
spaceBetween = children.length > 1 ? remaining / (children.length - 1) : 0
|
|
120
|
+
break
|
|
121
|
+
case 'space-around':
|
|
122
|
+
spaceBetween = remaining / children.length
|
|
123
|
+
mainOffset = Math.floor(spaceBetween / 2)
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let pos = mainOffset
|
|
129
|
+
|
|
130
|
+
for (const info of childInfo) {
|
|
131
|
+
const { child, cs, grow, minMain, marginMain, marginCross, margin, measured } = info
|
|
132
|
+
|
|
133
|
+
let childMain
|
|
134
|
+
if (grow > 0) {
|
|
135
|
+
const extra = totalFlex > 0 ? Math.floor(remaining * (grow / totalFlex)) : 0
|
|
136
|
+
childMain = minMain + extra
|
|
137
|
+
} else {
|
|
138
|
+
childMain = minMain
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const mainRemaining = mainSize - pos - marginMain
|
|
142
|
+
if (childMain > mainRemaining) childMain = Math.max(0, mainRemaining)
|
|
143
|
+
|
|
144
|
+
const explicitCross = isRow
|
|
145
|
+
? resolveSize(cs.height, crossSize)
|
|
146
|
+
: resolveSize(cs.width, crossSize)
|
|
147
|
+
let childCross = crossSize - marginCross
|
|
148
|
+
if (alignItems !== 'stretch' || explicitCross != null) {
|
|
149
|
+
const measuredCross = measured
|
|
150
|
+
? (isRow ? measured.height : measured.width)
|
|
151
|
+
: childCross
|
|
152
|
+
childCross = Math.min(measuredCross, childCross)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const marginBefore = isRow ? margin.left : margin.top
|
|
156
|
+
const marginCrossBefore = isRow ? margin.top : margin.left
|
|
157
|
+
|
|
158
|
+
let crossOffset = marginCrossBefore
|
|
159
|
+
switch (alignItems) {
|
|
160
|
+
case 'center':
|
|
161
|
+
crossOffset = Math.floor((crossSize - marginCross - childCross) / 2) + marginCrossBefore
|
|
162
|
+
break
|
|
163
|
+
case 'flex-end':
|
|
164
|
+
crossOffset = crossSize - marginCross - childCross + marginCrossBefore
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const childRect = isRow
|
|
169
|
+
? { x: x + pos + marginBefore, y: y + crossOffset, width: childMain, height: childCross }
|
|
170
|
+
: { x: x + crossOffset, y: y + pos + marginBefore, width: childCross, height: childMain }
|
|
171
|
+
|
|
172
|
+
computeLayout(child, childRect)
|
|
173
|
+
|
|
174
|
+
pos += childMain + marginMain + gap + spaceBetween
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function measureChild(child, cs, isRow, availW, availH) {
|
|
179
|
+
const leaf = getLeaf(child)
|
|
180
|
+
if (!leaf) return { width: 0, height: 0 }
|
|
181
|
+
|
|
182
|
+
const explicitW = resolveSize(cs.width, availW)
|
|
183
|
+
const explicitH = resolveSize(cs.height, availH)
|
|
184
|
+
|
|
185
|
+
let w, h
|
|
186
|
+
|
|
187
|
+
if (leaf.type === 'text') {
|
|
188
|
+
const text = extractText(leaf)
|
|
189
|
+
if (!text) { w = explicitW ?? 0; h = explicitH ?? 1 }
|
|
190
|
+
else {
|
|
191
|
+
const maxW = explicitW ?? availW
|
|
192
|
+
const lines = wordWrap(text, maxW)
|
|
193
|
+
const textWidth = Math.min(maxW, Math.max(...lines.map(l => measureText(l))))
|
|
194
|
+
w = explicitW ?? textWidth
|
|
195
|
+
h = explicitH ?? lines.length
|
|
196
|
+
}
|
|
197
|
+
} else if (explicitW != null && explicitH != null) {
|
|
198
|
+
w = explicitW
|
|
199
|
+
h = explicitH
|
|
200
|
+
} else {
|
|
201
|
+
const intrinsic = measureIntrinsic(leaf, availW, availH)
|
|
202
|
+
w = explicitW ?? intrinsic.width
|
|
203
|
+
h = explicitH ?? intrinsic.height
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
width: clampSize(w, cs.minWidth, cs.maxWidth),
|
|
208
|
+
height: clampSize(h, cs.minHeight, cs.maxHeight),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function measureIntrinsic(node, availW, availH) {
|
|
213
|
+
if (!node) return { width: 0, height: 0 }
|
|
214
|
+
|
|
215
|
+
const style = node.props?.style ?? {}
|
|
216
|
+
const pad = resolvePadding(style)
|
|
217
|
+
const border = style.border ? 1 : 0
|
|
218
|
+
const chrome = { x: pad.left + pad.right + border * 2, y: pad.top + pad.bottom + border * 2 }
|
|
219
|
+
const innerW = availW - chrome.x
|
|
220
|
+
const innerH = availH - chrome.y
|
|
221
|
+
|
|
222
|
+
const children = node._resolvedChildren
|
|
223
|
+
if (!children || children.length === 0) {
|
|
224
|
+
return { width: chrome.x, height: chrome.y }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const childIsRow = style.flexDirection === 'row'
|
|
228
|
+
const gap = style.gap ?? 0
|
|
229
|
+
|
|
230
|
+
let mainTotal = 0
|
|
231
|
+
let crossMax = 0
|
|
232
|
+
|
|
233
|
+
for (const child of children) {
|
|
234
|
+
const cs = childStyle(child)
|
|
235
|
+
const grow = cs.flexGrow ?? cs.flex ?? 0
|
|
236
|
+
|
|
237
|
+
const measured = measureChild(child, cs, childIsRow, innerW, innerH)
|
|
238
|
+
const margin = resolveMargin(cs)
|
|
239
|
+
const marginMain = childIsRow ? margin.left + margin.right : margin.top + margin.bottom
|
|
240
|
+
const marginCross = childIsRow ? margin.top + margin.bottom : margin.left + margin.right
|
|
241
|
+
|
|
242
|
+
const childMain = (childIsRow ? measured.width : measured.height) + marginMain
|
|
243
|
+
mainTotal += childMain
|
|
244
|
+
|
|
245
|
+
const childCross = (childIsRow ? measured.height : measured.width) + marginCross
|
|
246
|
+
if (childCross > crossMax) crossMax = childCross
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
mainTotal += Math.max(0, children.length - 1) * gap
|
|
250
|
+
|
|
251
|
+
return childIsRow
|
|
252
|
+
? { width: mainTotal + chrome.x, height: crossMax + chrome.y }
|
|
253
|
+
: { width: crossMax + chrome.x, height: mainTotal + chrome.y }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getLeaf(node) {
|
|
257
|
+
if (!node) return null
|
|
258
|
+
if (node._resolved) return getLeaf(node._resolved)
|
|
259
|
+
return node
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function childStyle(child) {
|
|
263
|
+
const leaf = getLeaf(child)
|
|
264
|
+
return leaf?.props?.style ?? {}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveSize(value, available) {
|
|
268
|
+
if (value == null) return null
|
|
269
|
+
if (typeof value === 'number') return value
|
|
270
|
+
if (typeof value === 'string' && value.endsWith('%')) {
|
|
271
|
+
return Math.floor(available * parseFloat(value) / 100)
|
|
272
|
+
}
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function clampSize(value, min, max) {
|
|
277
|
+
if (min != null && value < min) value = min
|
|
278
|
+
if (max != null && value > max) value = max
|
|
279
|
+
return Math.max(0, Math.floor(value))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function resolvePadding(style) {
|
|
283
|
+
const p = style.padding ?? 0
|
|
284
|
+
return {
|
|
285
|
+
top: style.paddingTop ?? style.paddingY ?? p,
|
|
286
|
+
bottom: style.paddingBottom ?? style.paddingY ?? p,
|
|
287
|
+
left: style.paddingLeft ?? style.paddingX ?? p,
|
|
288
|
+
right: style.paddingRight ?? style.paddingX ?? p,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function resolveMargin(style) {
|
|
293
|
+
const m = style.margin ?? 0
|
|
294
|
+
return {
|
|
295
|
+
top: style.marginTop ?? style.marginY ?? m,
|
|
296
|
+
bottom: style.marginBottom ?? style.marginY ?? m,
|
|
297
|
+
left: style.marginLeft ?? style.marginX ?? m,
|
|
298
|
+
right: style.marginRight ?? style.marginX ?? m,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function extractText(node) {
|
|
303
|
+
if (node == null || node === true || node === false) return ''
|
|
304
|
+
if (typeof node === 'string') return node
|
|
305
|
+
if (typeof node === 'number') return String(node)
|
|
306
|
+
const children = node.props?.children
|
|
307
|
+
if (children == null || children === true || children === false) return ''
|
|
308
|
+
if (typeof children === 'string') return children
|
|
309
|
+
if (typeof children === 'number') return String(children)
|
|
310
|
+
if (Array.isArray(children)) return children.map(c => extractText(c)).join('')
|
|
311
|
+
return ''
|
|
312
|
+
}
|
package/src/list.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx } from '../jsx-runtime.js'
|
|
2
|
+
import { createSignal } from './signal.js'
|
|
3
|
+
import { useInput, useLayout } from './hooks.js'
|
|
4
|
+
|
|
5
|
+
export function List({ items, selected: selectedProp, onSelect, height, renderItem, header, focused = true }) {
|
|
6
|
+
const [selectedInternal, setSelectedInternal] = createSignal(0)
|
|
7
|
+
const layout = useLayout()
|
|
8
|
+
|
|
9
|
+
const selected = selectedProp ?? selectedInternal()
|
|
10
|
+
const setSelected = onSelect ?? setSelectedInternal
|
|
11
|
+
|
|
12
|
+
const rawH = height ?? layout.height
|
|
13
|
+
const visibleH = header && rawH > 0 ? rawH - 1 : rawH
|
|
14
|
+
|
|
15
|
+
useInput(({ key, ctrl }) => {
|
|
16
|
+
if (!focused) return
|
|
17
|
+
|
|
18
|
+
const len = items.length
|
|
19
|
+
if (len === 0) return
|
|
20
|
+
|
|
21
|
+
const h = visibleH || 10
|
|
22
|
+
const half = Math.max(1, Math.floor(h / 2))
|
|
23
|
+
|
|
24
|
+
if (key === 'up' || key === 'k') setSelected(Math.max(0, selected - 1))
|
|
25
|
+
else if (key === 'down' || key === 'j') setSelected(Math.min(len - 1, selected + 1))
|
|
26
|
+
else if (key === 'pageup' || (ctrl && key === 'b')) setSelected(Math.max(0, selected - h))
|
|
27
|
+
else if (key === 'pagedown' || (ctrl && key === 'f')) setSelected(Math.min(len - 1, selected + h))
|
|
28
|
+
else if (ctrl && key === 'u') setSelected(Math.max(0, selected - half))
|
|
29
|
+
else if (ctrl && key === 'd') setSelected(Math.min(len - 1, selected + half))
|
|
30
|
+
else if (key === 'home' || key === 'g') setSelected(0)
|
|
31
|
+
else if (key === 'end' || key === 'G') setSelected(len - 1)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const scrollOffset = visibleH > 0 && items.length > visibleH
|
|
35
|
+
? Math.max(0, Math.min(selected - Math.floor(visibleH / 2), items.length - visibleH))
|
|
36
|
+
: 0
|
|
37
|
+
|
|
38
|
+
const children = []
|
|
39
|
+
if (header) children.push(header)
|
|
40
|
+
|
|
41
|
+
for (let i = scrollOffset; i < items.length; i++) {
|
|
42
|
+
children.push(renderItem(items[i], { selected: i === selected, index: i, focused }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return jsx('box', { style: { flexDirection: 'column', flexGrow: 1 }, children })
|
|
46
|
+
}
|
package/src/modal.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx } from '../jsx-runtime.js'
|
|
2
|
+
import { useInput, useTheme } from './hooks.js'
|
|
3
|
+
import { registerOverlay } from './renderer.js'
|
|
4
|
+
|
|
5
|
+
export function Modal({ open, onClose, title, children, width: w = 40 }) {
|
|
6
|
+
const { accent = 'cyan' } = useTheme()
|
|
7
|
+
|
|
8
|
+
useInput((event) => {
|
|
9
|
+
if (!open) return
|
|
10
|
+
if (event.key === 'escape') {
|
|
11
|
+
onClose?.()
|
|
12
|
+
event.stopPropagation()
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
if (!open) return null
|
|
17
|
+
|
|
18
|
+
const content = jsx('box', {
|
|
19
|
+
style: {
|
|
20
|
+
width: w,
|
|
21
|
+
border: 'round',
|
|
22
|
+
borderColor: accent,
|
|
23
|
+
flexDirection: 'column',
|
|
24
|
+
paddingX: 1,
|
|
25
|
+
},
|
|
26
|
+
children: [
|
|
27
|
+
title && jsx('text', { style: { bold: true, color: accent }, children: title }),
|
|
28
|
+
...(Array.isArray(children) ? children : [children]),
|
|
29
|
+
].filter(Boolean),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const overlay = jsx('box', {
|
|
33
|
+
style: {
|
|
34
|
+
height: '100%',
|
|
35
|
+
width: '100%',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
},
|
|
39
|
+
children: content,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
registerOverlay(overlay, { backdrop: true })
|
|
43
|
+
return null
|
|
44
|
+
}
|
package/src/progress.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
|
+
import { useTheme } from './hooks.js'
|
|
3
|
+
|
|
4
|
+
export function ProgressBar({ value = 0, width = 20, color, label }) {
|
|
5
|
+
const { accent = 'cyan' } = useTheme()
|
|
6
|
+
const c = color ?? accent
|
|
7
|
+
const clamped = Math.max(0, Math.min(1, value))
|
|
8
|
+
const filled = Math.round(clamped * width)
|
|
9
|
+
const empty = width - filled
|
|
10
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty)
|
|
11
|
+
|
|
12
|
+
const children = [
|
|
13
|
+
jsx('text', { style: { color: c }, children: bar }),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
if (label != null) {
|
|
17
|
+
children.push(jsx('text', { style: { color: 'gray', dim: true }, children: ` ${label}` }))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return jsxs('box', { style: { flexDirection: 'row' }, children })
|
|
21
|
+
}
|
package/src/radio.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
|
+
import { createSignal } from './signal.js'
|
|
3
|
+
import { useInput, useTheme } from './hooks.js'
|
|
4
|
+
|
|
5
|
+
export function Radio({ options, selected, onSelect, focused = false }) {
|
|
6
|
+
const { accent = 'cyan' } = useTheme()
|
|
7
|
+
const [cursor, setCursor] = createSignal(Math.max(0, options.indexOf(selected)))
|
|
8
|
+
|
|
9
|
+
useInput((event) => {
|
|
10
|
+
if (!focused) return
|
|
11
|
+
|
|
12
|
+
const len = options.length
|
|
13
|
+
if (len === 0) return
|
|
14
|
+
const { key } = event
|
|
15
|
+
|
|
16
|
+
if (key === 'up' || key === 'k') {
|
|
17
|
+
if (cursor() > 0) {
|
|
18
|
+
setCursor(c => c - 1)
|
|
19
|
+
event.stopPropagation()
|
|
20
|
+
}
|
|
21
|
+
} else if (key === 'down' || key === 'j') {
|
|
22
|
+
if (cursor() < len - 1) {
|
|
23
|
+
setCursor(c => c + 1)
|
|
24
|
+
event.stopPropagation()
|
|
25
|
+
}
|
|
26
|
+
} else if (key === 'return' || key === 'space') {
|
|
27
|
+
onSelect?.(options[cursor()])
|
|
28
|
+
event.stopPropagation()
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const c = cursor()
|
|
33
|
+
|
|
34
|
+
const children = options.map((option, i) => {
|
|
35
|
+
const isSelected = option === selected
|
|
36
|
+
const isCursor = focused && i === c
|
|
37
|
+
const icon = isSelected ? '\u25c9' : '\u25cb'
|
|
38
|
+
const bg = isCursor ? accent : null
|
|
39
|
+
const color = isCursor ? 'black' : null
|
|
40
|
+
|
|
41
|
+
return jsxs('box', {
|
|
42
|
+
key: option,
|
|
43
|
+
style: { flexDirection: 'row', bg },
|
|
44
|
+
children: [
|
|
45
|
+
jsx('text', { style: { color, bold: isCursor }, children: icon }),
|
|
46
|
+
jsx('text', { style: { color }, children: ` ${option}` }),
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return jsx('box', { style: { flexDirection: 'column' }, children })
|
|
52
|
+
}
|