@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
|
+
import { createSignal } from './signal.js'
|
|
3
|
+
import { useInput, useLayout } from './hooks.js'
|
|
4
|
+
|
|
5
|
+
const BOX = { flexDirection: 'row', height: 1, minHeight: 1, flexGrow: 1 }
|
|
6
|
+
|
|
7
|
+
export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused = true, initialValue }) {
|
|
8
|
+
const init = initialValue ?? ''
|
|
9
|
+
const [value, setValue] = createSignal(init)
|
|
10
|
+
const [cursor, setCursor] = createSignal(init.length)
|
|
11
|
+
const layout = useLayout()
|
|
12
|
+
|
|
13
|
+
function update(v, c) {
|
|
14
|
+
setValue(v)
|
|
15
|
+
setCursor(c)
|
|
16
|
+
if (onChange) onChange(v)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
useInput((event) => {
|
|
20
|
+
if (!focused) return
|
|
21
|
+
|
|
22
|
+
const { key, raw, ctrl } = event
|
|
23
|
+
const v = value()
|
|
24
|
+
const c = cursor()
|
|
25
|
+
|
|
26
|
+
if (key === 'return') {
|
|
27
|
+
if (onSubmit) {
|
|
28
|
+
onSubmit(v)
|
|
29
|
+
update('', 0)
|
|
30
|
+
event.stopPropagation()
|
|
31
|
+
}
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (key === 'escape') {
|
|
36
|
+
if (onCancel) {
|
|
37
|
+
onCancel()
|
|
38
|
+
event.stopPropagation()
|
|
39
|
+
}
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (key === 'backspace') {
|
|
44
|
+
if (c > 0) update(v.slice(0, c - 1) + v.slice(c), c - 1)
|
|
45
|
+
event.stopPropagation()
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (key === 'delete') {
|
|
50
|
+
if (c < v.length) update(v.slice(0, c) + v.slice(c + 1), c)
|
|
51
|
+
event.stopPropagation()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (key === 'left') { setCursor(Math.max(0, c - 1)); event.stopPropagation(); return }
|
|
56
|
+
if (key === 'right') { setCursor(Math.min(v.length, c + 1)); event.stopPropagation(); return }
|
|
57
|
+
|
|
58
|
+
if (key === 'home' || (ctrl && raw === '\x01')) { setCursor(0); event.stopPropagation(); return }
|
|
59
|
+
if (key === 'end' || (ctrl && raw === '\x05')) { setCursor(v.length); event.stopPropagation(); return }
|
|
60
|
+
|
|
61
|
+
if (ctrl && raw === '\x15') { update(v.slice(c), 0); event.stopPropagation(); return }
|
|
62
|
+
if (ctrl && raw === '\x0b') { update(v.slice(0, c), c); event.stopPropagation(); return }
|
|
63
|
+
|
|
64
|
+
if (ctrl && raw === '\x17') {
|
|
65
|
+
const before = v.slice(0, c)
|
|
66
|
+
const after = v.slice(c)
|
|
67
|
+
const trimmed = before.replace(/\S+\s*$/, '')
|
|
68
|
+
update(trimmed + after, trimmed.length)
|
|
69
|
+
event.stopPropagation()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!ctrl && raw.length === 1 && raw >= ' ') {
|
|
74
|
+
update(v.slice(0, c) + raw + v.slice(c), c + raw.length)
|
|
75
|
+
event.stopPropagation()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const v = value()
|
|
80
|
+
const c = cursor()
|
|
81
|
+
const w = layout.width || 0
|
|
82
|
+
|
|
83
|
+
if (!v && placeholder && !focused) {
|
|
84
|
+
return jsx('text', { style: { color: 'gray', flexGrow: 1 }, children: placeholder })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!v && placeholder && focused) {
|
|
88
|
+
return jsxs('box', {
|
|
89
|
+
style: BOX,
|
|
90
|
+
children: [
|
|
91
|
+
jsx('text', { style: { inverse: true, color: 'gray' }, children: placeholder[0] }),
|
|
92
|
+
placeholder.length > 1 && jsx('text', { style: { color: 'gray' }, children: placeholder.slice(1) }),
|
|
93
|
+
],
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const contentWidth = v.length + 1
|
|
98
|
+
const needsScroll = w > 0 && contentWidth > w
|
|
99
|
+
|
|
100
|
+
if (!needsScroll) {
|
|
101
|
+
const cursorChar = v[c] || ' '
|
|
102
|
+
return jsxs('box', {
|
|
103
|
+
style: BOX,
|
|
104
|
+
children: [
|
|
105
|
+
v.slice(0, c) && jsx('text', { children: v.slice(0, c) }),
|
|
106
|
+
jsx('text', { style: { inverse: focused }, children: cursorChar }),
|
|
107
|
+
v.slice(c + 1) && jsx('text', { children: v.slice(c + 1) }),
|
|
108
|
+
],
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let scrollStart = 0
|
|
113
|
+
if (c >= w) {
|
|
114
|
+
scrollStart = c - w + 1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const visible = v.slice(scrollStart, scrollStart + w)
|
|
118
|
+
const cursorInView = c - scrollStart
|
|
119
|
+
const before = visible.slice(0, cursorInView)
|
|
120
|
+
const cursorChar = visible[cursorInView] || ' '
|
|
121
|
+
const after = visible.slice(cursorInView + 1)
|
|
122
|
+
|
|
123
|
+
return jsxs('box', {
|
|
124
|
+
style: BOX,
|
|
125
|
+
children: [
|
|
126
|
+
before && jsx('text', { children: before }),
|
|
127
|
+
jsx('text', { style: { inverse: focused }, children: cursorChar }),
|
|
128
|
+
after && jsx('text', { children: after }),
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
}
|
package/src/toast.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx } from '../jsx-runtime.js'
|
|
2
|
+
import { createSignalRaw } from './signal.js'
|
|
3
|
+
import { useInterval } from './hooks.js'
|
|
4
|
+
import { registerHook } from './renderer.js'
|
|
5
|
+
import { registerOverlay } from './renderer.js'
|
|
6
|
+
|
|
7
|
+
const POSITIONS = {
|
|
8
|
+
'top-left': { justifyContent: 'flex-start', alignItems: 'flex-start' },
|
|
9
|
+
'top-center': { justifyContent: 'flex-start', alignItems: 'center' },
|
|
10
|
+
'top-right': { justifyContent: 'flex-start', alignItems: 'flex-end' },
|
|
11
|
+
'center-left': { justifyContent: 'center', alignItems: 'flex-start' },
|
|
12
|
+
'center': { justifyContent: 'center', alignItems: 'center' },
|
|
13
|
+
'center-right': { justifyContent: 'center', alignItems: 'flex-end' },
|
|
14
|
+
'bottom-left': { justifyContent: 'flex-end', alignItems: 'flex-start' },
|
|
15
|
+
'bottom-center': { justifyContent: 'flex-end', alignItems: 'center' },
|
|
16
|
+
'bottom-right': { justifyContent: 'flex-end', alignItems: 'flex-end' },
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let nextId = 0
|
|
20
|
+
|
|
21
|
+
export function useToast({ duration = 2000, position = 'bottom-right', margin = 1 } = {}) {
|
|
22
|
+
const [items, setItems] = registerHook(() => createSignalRaw([]))
|
|
23
|
+
|
|
24
|
+
useInterval(() => {
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
setItems(prev => {
|
|
27
|
+
const next = prev.filter(t => t.expires > now)
|
|
28
|
+
return next.length === prev.length ? prev : next
|
|
29
|
+
})
|
|
30
|
+
}, 200)
|
|
31
|
+
|
|
32
|
+
function toast(message) {
|
|
33
|
+
const id = nextId++
|
|
34
|
+
setItems(prev => [...prev, { id, message, expires: Date.now() + duration }])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const list = items()
|
|
38
|
+
|
|
39
|
+
if (list.length > 0) {
|
|
40
|
+
const pos = POSITIONS[position] || POSITIONS['bottom-right']
|
|
41
|
+
const overlay = jsx('box', {
|
|
42
|
+
style: {
|
|
43
|
+
width: '100%',
|
|
44
|
+
height: '100%',
|
|
45
|
+
flexDirection: 'column',
|
|
46
|
+
padding: margin,
|
|
47
|
+
...pos,
|
|
48
|
+
},
|
|
49
|
+
children: list.map(t =>
|
|
50
|
+
jsx('text', {
|
|
51
|
+
key: t.id,
|
|
52
|
+
style: { inverse: true },
|
|
53
|
+
children: ` ${t.message} `,
|
|
54
|
+
})
|
|
55
|
+
),
|
|
56
|
+
})
|
|
57
|
+
registerOverlay(overlay, { fullscreen: true })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return toast
|
|
61
|
+
}
|
package/src/wrap.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// matches any ANSI escape sequence (CSI + OSC)
|
|
2
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g
|
|
3
|
+
|
|
4
|
+
export function stripAnsi(text) {
|
|
5
|
+
return text.indexOf('\x1b') === -1 ? text : text.replace(ANSI_RE, '')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function measureText(text) {
|
|
9
|
+
const clean = stripAnsi(text)
|
|
10
|
+
let width = 0
|
|
11
|
+
for (let i = 0; i < clean.length; i++) {
|
|
12
|
+
const code = clean.charCodeAt(i)
|
|
13
|
+
if (code >= 0x1100 && isWide(code)) width += 2
|
|
14
|
+
else width += 1
|
|
15
|
+
}
|
|
16
|
+
return width
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// iterates visible characters of a string, bundling any preceding
|
|
20
|
+
// ANSI sequences with the character they precede
|
|
21
|
+
function* visibleChars(str) {
|
|
22
|
+
let i = 0
|
|
23
|
+
let pending = ''
|
|
24
|
+
while (i < str.length) {
|
|
25
|
+
if (str[i] === '\x1b' && str[i + 1] === '[') {
|
|
26
|
+
const end = str.indexOf('m', i + 2)
|
|
27
|
+
if (end !== -1) {
|
|
28
|
+
pending += str.slice(i, end + 1)
|
|
29
|
+
i = end + 1
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
yield { chunk: pending + str[i], width: charWidth(str.charCodeAt(i)) }
|
|
34
|
+
pending = ''
|
|
35
|
+
i++
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function charWidth(code) {
|
|
40
|
+
return (code >= 0x1100 && isWide(code)) ? 2 : 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function sliceVisible(text, maxWidth) {
|
|
44
|
+
let result = ''
|
|
45
|
+
let width = 0
|
|
46
|
+
for (const { chunk, width: w } of visibleChars(text)) {
|
|
47
|
+
if (width + w > maxWidth) break
|
|
48
|
+
result += chunk
|
|
49
|
+
width += w
|
|
50
|
+
}
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function wordWrap(text, maxWidth) {
|
|
55
|
+
if (maxWidth <= 0) return []
|
|
56
|
+
if (!text) return ['']
|
|
57
|
+
|
|
58
|
+
const lines = []
|
|
59
|
+
|
|
60
|
+
for (const rawLine of text.split('\n')) {
|
|
61
|
+
if (rawLine.length === 0) {
|
|
62
|
+
lines.push('')
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (measureText(rawLine) <= maxWidth) {
|
|
67
|
+
lines.push(rawLine)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const words = rawLine.split(/\s+/)
|
|
72
|
+
let line = ''
|
|
73
|
+
let lineWidth = 0
|
|
74
|
+
|
|
75
|
+
for (const word of words) {
|
|
76
|
+
if (!word) continue
|
|
77
|
+
const ww = measureText(word)
|
|
78
|
+
|
|
79
|
+
if (lineWidth === 0 && ww <= maxWidth) {
|
|
80
|
+
line = word
|
|
81
|
+
lineWidth = ww
|
|
82
|
+
} else if (lineWidth === 0 && ww > maxWidth) {
|
|
83
|
+
for (const { chunk, width } of visibleChars(word)) {
|
|
84
|
+
if (lineWidth + width > maxWidth) {
|
|
85
|
+
lines.push(line)
|
|
86
|
+
line = ''
|
|
87
|
+
lineWidth = 0
|
|
88
|
+
}
|
|
89
|
+
line += chunk
|
|
90
|
+
lineWidth += width
|
|
91
|
+
}
|
|
92
|
+
} else if (lineWidth + 1 + ww <= maxWidth) {
|
|
93
|
+
line += ' ' + word
|
|
94
|
+
lineWidth += 1 + ww
|
|
95
|
+
} else if (ww > maxWidth) {
|
|
96
|
+
if (line) lines.push(line)
|
|
97
|
+
line = ''
|
|
98
|
+
lineWidth = 0
|
|
99
|
+
for (const { chunk, width } of visibleChars(word)) {
|
|
100
|
+
if (lineWidth + width > maxWidth) {
|
|
101
|
+
lines.push(line)
|
|
102
|
+
line = ''
|
|
103
|
+
lineWidth = 0
|
|
104
|
+
}
|
|
105
|
+
line += chunk
|
|
106
|
+
lineWidth += width
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
lines.push(line)
|
|
110
|
+
line = word
|
|
111
|
+
lineWidth = ww
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lines.push(line)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return lines
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isWide(code) {
|
|
122
|
+
return (
|
|
123
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
124
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
125
|
+
(code >= 0x3040 && code <= 0x33bf) ||
|
|
126
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
127
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
128
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
129
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
130
|
+
(code >= 0x20000 && code <= 0x2fffd) ||
|
|
131
|
+
(code >= 0x30000 && code <= 0x3fffd)
|
|
132
|
+
)
|
|
133
|
+
}
|