@trendr/core 0.1.0 → 0.2.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/src/table.js CHANGED
@@ -2,10 +2,12 @@ import { jsx, jsxs } from '../jsx-runtime.js'
2
2
  import { useTheme } from './hooks.js'
3
3
  import { List } from './list.js'
4
4
 
5
- export function Table({ columns, data, selected, onSelect, focused = true }) {
5
+ const DEFAULT_SEP = { left: '', fill: '\u2500', right: '' }
6
+
7
+ export function Table({ columns, data, selected, onSelect, focused = true, separator = false, separatorChars, renderItem, scrollbar, stickyHeader = false }) {
6
8
  const { accent = 'cyan' } = useTheme()
7
9
 
8
- const header = jsxs('box', {
10
+ const headerRow = jsxs('box', {
9
11
  style: { flexDirection: 'row' },
10
12
  children: columns.map((col, i) =>
11
13
  jsx('text', {
@@ -24,28 +26,53 @@ export function Table({ columns, data, selected, onSelect, focused = true }) {
24
26
  ),
25
27
  })
26
28
 
29
+ const header = separator
30
+ ? jsxs('box', {
31
+ style: { flexDirection: 'column' },
32
+ children: [
33
+ headerRow,
34
+ (() => {
35
+ const s = { ...DEFAULT_SEP, ...separatorChars }
36
+ return jsxs('box', {
37
+ style: { flexDirection: 'row' },
38
+ children: [
39
+ jsx('text', { style: { color: 'gray', dim: true }, children: s.left }),
40
+ jsx('text', { style: { color: 'gray', dim: true, flexGrow: 1, overflow: 'nowrap' }, children: s.fill.repeat(200) }),
41
+ jsx('text', { style: { color: 'gray', dim: true }, children: s.right }),
42
+ ],
43
+ })
44
+ })(),
45
+ ],
46
+ })
47
+ : headerRow
48
+
49
+ const defaultRenderItem = (row, { selected: isSel }) =>
50
+ jsxs('box', {
51
+ style: { flexDirection: 'row', bg: isSel ? (focused ? accent : 'gray') : null },
52
+ children: columns.map((col, i) =>
53
+ jsx('text', {
54
+ key: i,
55
+ style: {
56
+ width: col.width,
57
+ flexGrow: col.flexGrow,
58
+ overflow: 'truncate',
59
+ paddingX: col.paddingX ?? 1,
60
+ color: isSel && focused ? 'black' : (col.color ?? null),
61
+ },
62
+ children: col.render ? col.render(row, isSel) : String(row[col.key] ?? ''),
63
+ })
64
+ ),
65
+ })
66
+
27
67
  return jsx(List, {
28
68
  items: data,
29
69
  selected,
30
70
  onSelect,
31
71
  focused,
32
72
  header,
33
- renderItem: (row, { selected: isSel }) =>
34
- jsxs('box', {
35
- style: { flexDirection: 'row', bg: isSel ? (focused ? accent : 'gray') : null },
36
- children: columns.map((col, i) =>
37
- jsx('text', {
38
- key: i,
39
- style: {
40
- width: col.width,
41
- flexGrow: col.flexGrow,
42
- overflow: 'truncate',
43
- paddingX: col.paddingX ?? 1,
44
- color: isSel && focused ? 'black' : (col.color ?? null),
45
- },
46
- children: col.render ? col.render(row, isSel) : String(row[col.key] ?? ''),
47
- })
48
- ),
49
- }),
73
+ headerHeight: separator ? 2 : 1,
74
+ scrollbar,
75
+ stickyHeader,
76
+ renderItem: renderItem || defaultRenderItem,
50
77
  })
51
78
  }
package/src/task.js ADDED
@@ -0,0 +1,43 @@
1
+ import { jsx, jsxs } from '../jsx-runtime.js'
2
+ import { useAsync } from './hooks.js'
3
+ import { Spinner } from './spinner.js'
4
+ import { useTheme } from './hooks.js'
5
+
6
+ const DEFAULT_ICONS = {
7
+ success: '\u2714',
8
+ error: '\u2716',
9
+ idle: '\u25CB',
10
+ }
11
+
12
+ export function Task({ run, label, successLabel, errorLabel, icon, color, immediate = true }) {
13
+ const { accent = 'cyan' } = useTheme()
14
+ const { status, data, error } = useAsync(run, { immediate })
15
+
16
+ const s = status()
17
+
18
+ if (s === 'loading') {
19
+ return jsx(Spinner, { label, color: color ?? accent })
20
+ }
21
+
22
+ const icons = { ...DEFAULT_ICONS, ...icon }
23
+
24
+ if (s === 'success') {
25
+ return jsxs('box', { style: { flexDirection: 'row' }, children: [
26
+ jsx('text', { style: { color: color ?? 'green' }, children: icons.success }),
27
+ jsx('text', { children: ` ${successLabel ?? label}` }),
28
+ ]})
29
+ }
30
+
31
+ if (s === 'error') {
32
+ const msg = errorLabel ?? error()?.message ?? 'failed'
33
+ return jsxs('box', { style: { flexDirection: 'row' }, children: [
34
+ jsx('text', { style: { color: color ?? 'red' }, children: icons.error }),
35
+ jsx('text', { children: ` ${msg}` }),
36
+ ]})
37
+ }
38
+
39
+ return jsxs('box', { style: { flexDirection: 'row' }, children: [
40
+ jsx('text', { style: { color: 'gray' }, children: icons.idle }),
41
+ jsx('text', { style: { color: 'gray' }, children: ` ${label}` }),
42
+ ]})
43
+ }
package/src/text-area.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs } from '../jsx-runtime.js'
2
2
  import { createSignal } from './signal.js'
3
- import { useInput, useLayout } from './hooks.js'
3
+ import { useInput, useLayout, useCursor } from './hooks.js'
4
4
  import { registerHook } from './renderer.js'
5
5
 
6
6
  export function wrapForEditor(text, width) {
@@ -87,11 +87,12 @@ function ensureVisible(cursorRow, scroll, height, totalLines) {
87
87
  return scroll
88
88
  }
89
89
 
90
- export function TextArea({ onSubmit, onCancel, onChange, placeholder, focused = true, maxHeight = 10 }) {
90
+ export function TextArea({ onSubmit, onCancel, onChange, placeholder, focused = true, maxHeight = 10, clearOnSubmit = true, cursor: cursorProp }) {
91
91
  const [value, setValue] = createSignal('')
92
92
  const [cursor, setCursor] = createSignal(0)
93
93
  const ref = registerHook(() => ({ scroll: 0, goalCol: null }))
94
94
  const layout = useLayout()
95
+ const { cursorStyle, reset: resetBlink } = useCursor(cursorProp, focused)
95
96
 
96
97
  function update(v, c) {
97
98
  setValue(v)
@@ -102,6 +103,7 @@ export function TextArea({ onSubmit, onCancel, onChange, placeholder, focused =
102
103
 
103
104
  useInput((event) => {
104
105
  if (!focused) return
106
+ resetBlink()
105
107
 
106
108
  const { key, raw, ctrl, meta } = event
107
109
  const v = value()
@@ -109,7 +111,7 @@ export function TextArea({ onSubmit, onCancel, onChange, placeholder, focused =
109
111
 
110
112
  if (meta && key === '\r') {
111
113
  if (onSubmit) onSubmit(v)
112
- update('', 0)
114
+ if (clearOnSubmit) update('', 0)
113
115
  ref.scroll = 0
114
116
  event.stopPropagation()
115
117
  return
@@ -226,6 +228,7 @@ export function TextArea({ onSubmit, onCancel, onChange, placeholder, focused =
226
228
  const v = value()
227
229
  const c = cursor()
228
230
  const w = layout.width || 0
231
+ const cs = cursorStyle()
229
232
 
230
233
  if (!v && placeholder && !focused) {
231
234
  return jsx('text', { style: { color: 'gray', flexGrow: 1 }, children: placeholder })
@@ -247,7 +250,7 @@ export function TextArea({ onSubmit, onCancel, onChange, placeholder, focused =
247
250
  children: jsxs('box', {
248
251
  style: { flexDirection: 'row', height: 1 },
249
252
  children: [
250
- jsx('text', { style: { inverse: true, color: 'gray' }, children: placeholder[0] }),
253
+ jsx('text', { style: cs ? { ...cs, color: cs.color ?? 'gray' } : { inverse: true, color: 'gray' }, children: placeholder[0] }),
251
254
  placeholder.length > 1 && jsx('text', { style: { color: 'gray' }, children: placeholder.slice(1) }),
252
255
  ],
253
256
  }),
@@ -273,7 +276,7 @@ export function TextArea({ onSubmit, onCancel, onChange, placeholder, focused =
273
276
  style: { flexDirection: 'row', height: 1 },
274
277
  children: [
275
278
  before && jsx('text', { children: before }),
276
- jsx('text', { style: { inverse: true }, children: cursorChar }),
279
+ jsx('text', { style: cs ?? {}, children: cursorChar }),
277
280
  after && jsx('text', { children: after }),
278
281
  ],
279
282
  })
package/src/text-input.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { jsx, jsxs } from '../jsx-runtime.js'
2
2
  import { createSignal } from './signal.js'
3
- import { useInput, useLayout } from './hooks.js'
3
+ import { useInput, useLayout, useCursor } from './hooks.js'
4
4
 
5
5
  const BOX = { flexDirection: 'row', height: 1, minHeight: 1, flexGrow: 1 }
6
6
 
7
- export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused = true, initialValue }) {
7
+ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused = true, initialValue, clearOnSubmit = false, cursor: cursorProp }) {
8
8
  const init = initialValue ?? ''
9
9
  const [value, setValue] = createSignal(init)
10
10
  const [cursor, setCursor] = createSignal(init.length)
11
11
  const layout = useLayout()
12
+ const { cursorStyle, reset: resetBlink } = useCursor(cursorProp, focused)
12
13
 
13
14
  function update(v, c) {
14
15
  setValue(v)
@@ -18,6 +19,7 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
18
19
 
19
20
  useInput((event) => {
20
21
  if (!focused) return
22
+ resetBlink()
21
23
 
22
24
  const { key, raw, ctrl } = event
23
25
  const v = value()
@@ -26,7 +28,7 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
26
28
  if (key === 'return') {
27
29
  if (onSubmit) {
28
30
  onSubmit(v)
29
- update('', 0)
31
+ if (clearOnSubmit) update('', 0)
30
32
  event.stopPropagation()
31
33
  }
32
34
  return
@@ -79,6 +81,7 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
79
81
  const v = value()
80
82
  const c = cursor()
81
83
  const w = layout.width || 0
84
+ const cs = cursorStyle()
82
85
 
83
86
  if (!v && placeholder && !focused) {
84
87
  return jsx('text', { style: { color: 'gray', flexGrow: 1 }, children: placeholder })
@@ -88,7 +91,7 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
88
91
  return jsxs('box', {
89
92
  style: BOX,
90
93
  children: [
91
- jsx('text', { style: { inverse: true, color: 'gray' }, children: placeholder[0] }),
94
+ jsx('text', { style: cs ? { ...cs, color: cs.color ?? 'gray' } : { inverse: true, color: 'gray' }, children: placeholder[0] }),
92
95
  placeholder.length > 1 && jsx('text', { style: { color: 'gray' }, children: placeholder.slice(1) }),
93
96
  ],
94
97
  })
@@ -103,7 +106,7 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
103
106
  style: BOX,
104
107
  children: [
105
108
  v.slice(0, c) && jsx('text', { children: v.slice(0, c) }),
106
- jsx('text', { style: { inverse: focused }, children: cursorChar }),
109
+ jsx('text', { style: cs ?? {}, children: cursorChar }),
107
110
  v.slice(c + 1) && jsx('text', { children: v.slice(c + 1) }),
108
111
  ],
109
112
  })
@@ -124,7 +127,7 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
124
127
  style: BOX,
125
128
  children: [
126
129
  before && jsx('text', { children: before }),
127
- jsx('text', { style: { inverse: focused }, children: cursorChar }),
130
+ jsx('text', { style: cs ?? {}, children: cursorChar }),
128
131
  after && jsx('text', { children: after }),
129
132
  ],
130
133
  })
package/src/toast.js CHANGED
@@ -18,7 +18,7 @@ const POSITIONS = {
18
18
 
19
19
  let nextId = 0
20
20
 
21
- export function useToast({ duration = 2000, position = 'bottom-right', margin = 1 } = {}) {
21
+ export function useToast({ duration = 2000, position = 'bottom-right', margin = 1, render } = {}) {
22
22
  const [items, setItems] = registerHook(() => createSignalRaw([]))
23
23
 
24
24
  useInterval(() => {
@@ -47,11 +47,9 @@ export function useToast({ duration = 2000, position = 'bottom-right', margin =
47
47
  ...pos,
48
48
  },
49
49
  children: list.map(t =>
50
- jsx('text', {
51
- key: t.id,
52
- style: { inverse: true },
53
- children: ` ${t.message} `,
54
- })
50
+ render
51
+ ? jsx('box', { key: t.id, children: render(t.message) })
52
+ : jsx('text', { key: t.id, style: { inverse: true }, children: ` ${t.message} ` })
55
53
  ),
56
54
  })
57
55
  registerOverlay(overlay, { fullscreen: true })
package/src/wrap.js CHANGED
@@ -9,9 +9,9 @@ export function measureText(text) {
9
9
  const clean = stripAnsi(text)
10
10
  let width = 0
11
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
12
+ const code = clean.codePointAt(i)
13
+ if (code > 0xffff) i++
14
+ width += (code >= 0x1100 && isWide(code)) ? 2 : 1
15
15
  }
16
16
  return width
17
17
  }
@@ -30,13 +30,15 @@ function* visibleChars(str) {
30
30
  continue
31
31
  }
32
32
  }
33
- yield { chunk: pending + str[i], width: charWidth(str.charCodeAt(i)) }
33
+ const code = str.codePointAt(i)
34
+ const len = code > 0xffff ? 2 : 1
35
+ yield { chunk: pending + str.slice(i, i + len), width: charWidth(code) }
34
36
  pending = ''
35
- i++
37
+ i += len
36
38
  }
37
39
  }
38
40
 
39
- function charWidth(code) {
41
+ export function charWidth(code) {
40
42
  return (code >= 0x1100 && isWide(code)) ? 2 : 1
41
43
  }
42
44
 
@@ -118,16 +120,113 @@ export function wordWrap(text, maxWidth) {
118
120
  return lines
119
121
  }
120
122
 
123
+ // generated from Unicode 15.1 EastAsianWidth=W/F + Emoji_Presentation=Yes
121
124
  function isWide(code) {
122
125
  return (
123
126
  (code >= 0x1100 && code <= 0x115f) ||
124
- (code >= 0x2e80 && code <= 0x303e) ||
125
- (code >= 0x3040 && code <= 0x33bf) ||
126
- (code >= 0xf900 && code <= 0xfaff) ||
127
+ (code >= 0x231a && code <= 0x231b) ||
128
+ (code >= 0x2329 && code <= 0x232a) ||
129
+ (code >= 0x23e9 && code <= 0x23ec) ||
130
+ code === 0x23f0 ||
131
+ code === 0x23f3 ||
132
+ (code >= 0x25fd && code <= 0x25fe) ||
133
+ (code >= 0x2614 && code <= 0x2615) ||
134
+ (code >= 0x2648 && code <= 0x2653) ||
135
+ code === 0x267f ||
136
+ code === 0x2693 ||
137
+ code === 0x26a1 ||
138
+ (code >= 0x26aa && code <= 0x26ab) ||
139
+ (code >= 0x26bd && code <= 0x26be) ||
140
+ (code >= 0x26c4 && code <= 0x26c5) ||
141
+ code === 0x26ce ||
142
+ code === 0x26d4 ||
143
+ code === 0x26ea ||
144
+ (code >= 0x26f2 && code <= 0x26f3) ||
145
+ code === 0x26f5 ||
146
+ code === 0x26fa ||
147
+ code === 0x26fd ||
148
+ code === 0x2705 ||
149
+ (code >= 0x270a && code <= 0x270b) ||
150
+ code === 0x2728 ||
151
+ code === 0x274c ||
152
+ code === 0x274e ||
153
+ (code >= 0x2753 && code <= 0x2755) ||
154
+ code === 0x2757 ||
155
+ (code >= 0x2795 && code <= 0x2797) ||
156
+ code === 0x27b0 ||
157
+ code === 0x27bf ||
158
+ (code >= 0x2b1b && code <= 0x2b1c) ||
159
+ code === 0x2b50 ||
160
+ code === 0x2b55 ||
161
+ (code >= 0x2e80 && code <= 0x2fd5) ||
162
+ (code >= 0x2ff0 && code <= 0x2ffb) ||
163
+ (code >= 0x3000 && code <= 0x3096) ||
164
+ (code >= 0x3099 && code <= 0x30ff) ||
165
+ (code >= 0x3105 && code <= 0x312f) ||
166
+ (code >= 0x3131 && code <= 0x318e) ||
167
+ (code >= 0x3190 && code <= 0x31ba) ||
168
+ (code >= 0x31c0 && code <= 0x31e3) ||
169
+ (code >= 0x31f0 && code <= 0x321e) ||
170
+ (code >= 0x3220 && code <= 0x32fe) ||
171
+ (code >= 0x3300 && code <= 0x4db5) ||
172
+ (code >= 0x4e00 && code <= 0x9fef) ||
173
+ (code >= 0xa000 && code <= 0xa4c6) ||
174
+ (code >= 0xac00 && code <= 0xd7a3) ||
175
+ (code >= 0xf900 && code <= 0xfa6d) ||
176
+ (code >= 0xfa70 && code <= 0xfad9) ||
177
+ (code >= 0xfe10 && code <= 0xfe19) ||
127
178
  (code >= 0xfe30 && code <= 0xfe6f) ||
128
179
  (code >= 0xff01 && code <= 0xff60) ||
129
180
  (code >= 0xffe0 && code <= 0xffe6) ||
130
- (code >= 0x20000 && code <= 0x2fffd) ||
131
- (code >= 0x30000 && code <= 0x3fffd)
181
+ code === 0x1f004 ||
182
+ code === 0x1f0cf ||
183
+ code === 0x1f18e ||
184
+ (code >= 0x1f191 && code <= 0x1f19a) ||
185
+ (code >= 0x1f1e6 && code <= 0x1f202) ||
186
+ (code >= 0x1f210 && code <= 0x1f23b) ||
187
+ (code >= 0x1f240 && code <= 0x1f248) ||
188
+ (code >= 0x1f250 && code <= 0x1f251) ||
189
+ (code >= 0x1f300 && code <= 0x1f320) ||
190
+ (code >= 0x1f32d && code <= 0x1f335) ||
191
+ (code >= 0x1f337 && code <= 0x1f37c) ||
192
+ (code >= 0x1f37e && code <= 0x1f393) ||
193
+ (code >= 0x1f3a0 && code <= 0x1f3ca) ||
194
+ (code >= 0x1f3cf && code <= 0x1f3d3) ||
195
+ (code >= 0x1f3e0 && code <= 0x1f3f0) ||
196
+ code === 0x1f3f4 ||
197
+ (code >= 0x1f3f8 && code <= 0x1f43e) ||
198
+ code === 0x1f440 ||
199
+ (code >= 0x1f442 && code <= 0x1f4fc) ||
200
+ (code >= 0x1f4ff && code <= 0x1f53d) ||
201
+ (code >= 0x1f54b && code <= 0x1f54e) ||
202
+ (code >= 0x1f550 && code <= 0x1f567) ||
203
+ code === 0x1f57a ||
204
+ (code >= 0x1f595 && code <= 0x1f596) ||
205
+ code === 0x1f5a4 ||
206
+ (code >= 0x1f5fb && code <= 0x1f64f) ||
207
+ (code >= 0x1f680 && code <= 0x1f6c5) ||
208
+ code === 0x1f6cc ||
209
+ (code >= 0x1f6d0 && code <= 0x1f6d2) ||
210
+ (code >= 0x1f6d5 && code <= 0x1f6d7) ||
211
+ (code >= 0x1f6dc && code <= 0x1f6df) ||
212
+ (code >= 0x1f6eb && code <= 0x1f6ec) ||
213
+ (code >= 0x1f6f4 && code <= 0x1f6fc) ||
214
+ (code >= 0x1f7e0 && code <= 0x1f7eb) ||
215
+ code === 0x1f7f0 ||
216
+ (code >= 0x1f90c && code <= 0x1f93a) ||
217
+ (code >= 0x1f93c && code <= 0x1f945) ||
218
+ (code >= 0x1f947 && code <= 0x1f9ff) ||
219
+ (code >= 0x1fa70 && code <= 0x1fa7c) ||
220
+ (code >= 0x1fa80 && code <= 0x1fa88) ||
221
+ (code >= 0x1fa90 && code <= 0x1fabd) ||
222
+ (code >= 0x1fabf && code <= 0x1fac5) ||
223
+ (code >= 0x1face && code <= 0x1fadb) ||
224
+ (code >= 0x1fae0 && code <= 0x1fae8) ||
225
+ (code >= 0x1faf0 && code <= 0x1faf8) ||
226
+ (code >= 0x20000 && code <= 0x2a6d6) ||
227
+ (code >= 0x2a700 && code <= 0x2b734) ||
228
+ (code >= 0x2b740 && code <= 0x2b81d) ||
229
+ (code >= 0x2b820 && code <= 0x2ebe0) ||
230
+ (code >= 0x30000 && code <= 0x3134a)
132
231
  )
133
232
  }