@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.
@@ -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
+ }