@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.
@@ -0,0 +1,105 @@
1
+ import { jsx, jsxs } from '../jsx-runtime.js'
2
+ import { createSignal } from './signal.js'
3
+ import { useInput, useMouse, useLayout, useTheme, useScrollDrag } from './hooks.js'
4
+
5
+ export function ScrollBox({ children, focused = true, scrollOffset: offsetProp, onScroll, scrollbar = false, thumbChar = '\u2588', trackChar = '\u2502', style: userStyle }) {
6
+ const { accent = 'cyan' } = useTheme()
7
+ const [offsetInternal, setOffsetInternal] = createSignal(0)
8
+ const layout = useLayout()
9
+
10
+ const offset = offsetProp ?? offsetInternal()
11
+ const setOffset = onScroll ?? setOffsetInternal
12
+
13
+ const visibleH = layout.height
14
+ const contentH = layout.contentHeight ?? 0
15
+ const maxOffset = Math.max(0, contentH - visibleH)
16
+
17
+ const clamp = (v) => Math.max(0, Math.min(maxOffset, v))
18
+
19
+ useInput(({ key, ctrl }) => {
20
+ if (!focused) return
21
+ if (contentH <= visibleH) return
22
+
23
+ const pageH = visibleH || 10
24
+ const half = Math.max(1, Math.floor(pageH / 2))
25
+
26
+ if (key === 'up' || key === 'k') setOffset(clamp(offset - 1))
27
+ else if (key === 'down' || key === 'j') setOffset(clamp(offset + 1))
28
+ else if (key === 'pageup' || (ctrl && key === 'b')) setOffset(clamp(offset - pageH))
29
+ else if (key === 'pagedown' || (ctrl && key === 'f')) setOffset(clamp(offset + pageH))
30
+ else if (ctrl && key === 'u') setOffset(clamp(offset - half))
31
+ else if (ctrl && key === 'd') setOffset(clamp(offset + half))
32
+ else if (key === 'home' || key === 'g') setOffset(0)
33
+ else if (key === 'end' || key === 'G') setOffset(maxOffset)
34
+ })
35
+
36
+ useMouse((event) => {
37
+ if (!focused) return
38
+ if (event.action !== 'scroll') return
39
+ if (contentH <= visibleH) return
40
+ const { x, y } = event
41
+ if (x < layout.x || x >= layout.x + layout.width || y < layout.y || y >= layout.y + layout.height) return
42
+ if (event.direction === 'up') setOffset(clamp(offset - 3))
43
+ else setOffset(clamp(offset + 3))
44
+ event.stopPropagation()
45
+ })
46
+
47
+ const hasBar = scrollbar && visibleH > 0 && contentH > visibleH
48
+ const barThumbH = hasBar ? Math.max(1, Math.round((visibleH / contentH) * visibleH)) : 0
49
+ const barThumbStart = hasBar && maxOffset > 0 ? Math.round((offset / maxOffset) * (visibleH - barThumbH)) : 0
50
+
51
+ useScrollDrag({
52
+ barX: hasBar ? layout.x + layout.width - 1 : null,
53
+ barY: layout.y + barThumbStart,
54
+ thumbHeight: barThumbH,
55
+ trackHeight: visibleH,
56
+ maxOffset,
57
+ scrollOffset: offset,
58
+ onScroll: (v) => setOffset(clamp(v)),
59
+ })
60
+
61
+ const scrollBox = jsx('box', {
62
+ style: {
63
+ flexDirection: 'column',
64
+ flexGrow: 1,
65
+ ...userStyle,
66
+ overflow: 'scroll',
67
+ scrollOffset: offset,
68
+ },
69
+ children,
70
+ })
71
+
72
+ if (!scrollbar) return scrollBox
73
+
74
+ const hasOverflow = visibleH > 0 && contentH > visibleH
75
+ const barChildren = []
76
+
77
+ if (hasOverflow) {
78
+ const thumbH = Math.max(1, Math.round((visibleH / contentH) * visibleH))
79
+ const thumbStart = maxOffset > 0
80
+ ? Math.round((offset / maxOffset) * (visibleH - thumbH))
81
+ : 0
82
+
83
+ for (let i = 0; i < visibleH; i++) {
84
+ const isThumb = i >= thumbStart && i < thumbStart + thumbH
85
+ barChildren.push(
86
+ jsx('text', {
87
+ key: i,
88
+ style: { color: isThumb ? (focused ? accent : 'gray') : 'gray', dim: !isThumb },
89
+ children: isThumb ? thumbChar : trackChar,
90
+ })
91
+ )
92
+ }
93
+ }
94
+
95
+ return jsxs('box', {
96
+ style: { flexDirection: 'row', flexGrow: userStyle?.flexGrow, gap: 1 },
97
+ children: [
98
+ scrollBox,
99
+ jsx('box', {
100
+ style: { width: 1, flexDirection: 'column' },
101
+ children: barChildren,
102
+ }),
103
+ ],
104
+ })
105
+ }
@@ -1,12 +1,9 @@
1
1
  import { jsx } from '../jsx-runtime.js'
2
2
  import { createSignal } from './signal.js'
3
- import { useInput, useLayout, useTheme } from './hooks.js'
3
+ import { useInput, useMouse, useLayout, useTheme, useScrollDrag } from './hooks.js'
4
4
  import { wordWrap } from './wrap.js'
5
5
 
6
- const TRACK = '\u2502'
7
- const THUMB = '\u2588'
8
-
9
- export function ScrollableText({ content = '', focused = true, scrollOffset: offsetProp, onScroll, width: widthProp, scrollbar = false, wrap = true }) {
6
+ export function ScrollableText({ content = '', focused = true, scrollOffset: offsetProp, onScroll, width: widthProp, scrollbar = false, wrap = true, thumbChar = '\u2588', trackChar = '\u2502' }) {
10
7
  const { accent = 'cyan' } = useTheme()
11
8
  const [offsetInternal, setOffsetInternal] = createSignal(0)
12
9
  const layout = useLayout()
@@ -40,6 +37,31 @@ export function ScrollableText({ content = '', focused = true, scrollOffset: off
40
37
  else if (key === 'end' || key === 'G') setOffset(maxOffset)
41
38
  })
42
39
 
40
+ useMouse((event) => {
41
+ if (!focused) return
42
+ if (event.action !== 'scroll') return
43
+ if (lines.length <= (h || 1)) return
44
+ const { x, y } = event
45
+ if (x < layout.x || x >= layout.x + layout.width || y < layout.y || y >= layout.y + layout.height) return
46
+ if (event.direction === 'up') setOffset(clamp(offset - 3))
47
+ else setOffset(clamp(offset + 3))
48
+ event.stopPropagation()
49
+ })
50
+
51
+ const hasBar = scrollbar && h > 0 && lines.length > h
52
+ const barThumbH = hasBar ? Math.max(1, Math.round((h / lines.length) * h)) : 0
53
+ const barThumbStart = hasBar && maxOffset > 0 ? Math.round((offset / maxOffset) * (h - barThumbH)) : 0
54
+
55
+ useScrollDrag({
56
+ barX: hasBar ? layout.x + layout.width - 1 : null,
57
+ barY: layout.y + barThumbStart,
58
+ thumbHeight: barThumbH,
59
+ trackHeight: h,
60
+ maxOffset,
61
+ scrollOffset: offset,
62
+ onScroll: (v) => setOffset(clamp(v)),
63
+ })
64
+
43
65
  const visible = lines.slice(offset, h > 0 ? offset + h : undefined)
44
66
 
45
67
  const textStyle = wrap ? undefined : { overflow: 'truncate' }
@@ -58,7 +80,7 @@ export function ScrollableText({ content = '', focused = true, scrollOffset: off
58
80
 
59
81
  const children = visible.map((line, i) => {
60
82
  const isThumb = i >= thumbStart && i < thumbStart + thumbH
61
- const barChar = isThumb ? THUMB : TRACK
83
+ const barChar = isThumb ? thumbChar : trackChar
62
84
  const barColor = isThumb ? (focused ? accent : 'gray') : 'gray'
63
85
 
64
86
  return jsx('box', {
package/src/select.js CHANGED
@@ -1,9 +1,143 @@
1
1
  import { jsx } from '../jsx-runtime.js'
2
2
  import { createSignal } from './signal.js'
3
- import { useInput, useTheme } from './hooks.js'
4
- import { registerOverlay, getInstanceLayout, getContext } from './renderer.js'
3
+ import { useInput, useMouse, useLayout, useTheme } from './hooks.js'
4
+ import { registerOverlay, getInstanceLayout, getContext, registerHook } from './renderer.js'
5
5
 
6
- export function Select({ items, selected, onSelect, focused = false, overlay = false, maxVisible = 10, placeholder = 'select...', renderItem, style: userStyle }) {
6
+ function SelectDropdown({ items, cursor, scroll, visibleCount, onSelect, onClose, onCursorChange, renderItem, style: s, accent, dropWidth }) {
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
+ }
15
+
16
+ if (event.action === 'drag' && drag.active) {
17
+ const thumbH = Math.max(1, Math.round((visibleCount / items.length) * visibleCount))
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' }) {
7
141
  const { accent = 'cyan' } = useTheme()
8
142
  const defaults = {
9
143
  border: 'single',
@@ -42,6 +176,34 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
42
176
  else if (key === 'escape') { setOpen(false); event.stopPropagation() }
43
177
  })
44
178
 
179
+ const layout = useLayout()
180
+
181
+ useMouse((event) => {
182
+ if (event.action === 'scroll') {
183
+ if (!focused || !open()) return
184
+ const len = items.length
185
+ if (event.direction === 'up') setCursor(c => Math.max(0, c - 1))
186
+ else setCursor(c => Math.min(len - 1, c + 1))
187
+ event.stopPropagation()
188
+ return
189
+ }
190
+
191
+ if (event.action !== 'press' || event.button !== 'left') return
192
+ const { x, y } = event
193
+ const onCollapsed = x >= layout.x && x < layout.x + layout.width && y === layout.y
194
+
195
+ if (onCollapsed) {
196
+ if (open()) {
197
+ setOpen(false)
198
+ } else {
199
+ const idx = items.indexOf(selected)
200
+ setCursor(idx >= 0 ? idx : 0)
201
+ setOpen(true)
202
+ }
203
+ event.stopPropagation()
204
+ }
205
+ })
206
+
45
207
  const display = selected ?? placeholder
46
208
  const collapsed = jsx('text', {
47
209
  style: {
@@ -49,25 +211,22 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
49
211
  color: focused ? s.focusedColor : (selected ? s.color : 'gray'),
50
212
  bold: focused,
51
213
  },
52
- children: `\u25be ${display}`,
214
+ children: `${open() ? openIcon : closedIcon} ${display}`,
53
215
  })
54
216
 
55
217
  if (!open()) return collapsed
56
218
 
57
- const maxLen = items.reduce((m, v) => Math.max(m, v.length), 0)
58
-
59
219
  let maxRows = Math.min(items.length, maxVisible)
60
220
 
61
221
  if (overlay) {
62
- const layout = getInstanceLayout()
222
+ const instLayout = getInstanceLayout()
63
223
  const ctx = getContext()
64
224
  const termH = ctx?.stream?.rows ?? 24
65
- const anchorY = layout.y + 1
225
+ const anchorY = instLayout.y + 1
66
226
  const available = termH - anchorY - 2
67
227
  if (available > 0) maxRows = Math.min(maxRows, available)
68
228
  }
69
229
 
70
- const scrollable = items.length > maxRows
71
230
  const visibleCount = maxRows
72
231
 
73
232
  const cur = cursor()
@@ -78,68 +237,29 @@ export function Select({ items, selected, onSelect, focused = false, overlay = f
78
237
  newScroll = Math.max(0, Math.min(newScroll, items.length - visibleCount))
79
238
  if (newScroll !== sc) setScroll(newScroll)
80
239
 
81
- const visible = items.slice(newScroll, newScroll + visibleCount)
82
-
83
- const thumbH = scrollable ? Math.max(1, Math.round((visibleCount / items.length) * visibleCount)) : 0
84
- const maxSc = items.length - visibleCount
85
- const thumbStart = scrollable && maxSc > 0 ? Math.round((newScroll / maxSc) * (visibleCount - thumbH)) : 0
86
-
87
- const dropdownChildren = visible.map((item, vi) => {
88
- const i = vi + newScroll
89
- const isCursor = i === cur
90
-
91
- const barChar = scrollable
92
- ? (vi >= thumbStart && vi < thumbStart + thumbH ? '\u2588' : '\u2502')
93
- : null
94
-
95
- const row = (content) => {
96
- if (!scrollable) return content
97
- const barIsThumb = vi >= thumbStart && vi < thumbStart + thumbH
98
- return jsx('box', {
99
- key: i,
100
- style: { flexDirection: 'row' },
101
- children: [
102
- content,
103
- jsx('text', {
104
- style: { color: barIsThumb ? accent : 'gray', dim: !barIsThumb },
105
- children: ' ' + barChar,
106
- }),
107
- ],
108
- })
109
- }
110
-
111
- if (renderItem) {
112
- const content = jsx('box', {
113
- key: scrollable ? undefined : i,
114
- style: { bg: isCursor ? s.cursorBg : s.bg, flexGrow: 1 },
115
- children: renderItem(item, { selected: isCursor, index: i }),
116
- })
117
- return row(content)
118
- }
240
+ const handleSelect = (item) => {
241
+ onSelect?.(item)
242
+ setOpen(false)
243
+ }
119
244
 
120
- const content = jsx('box', {
121
- key: scrollable ? undefined : i,
122
- style: { bg: isCursor ? s.cursorBg : s.bg, paddingX: 1, flexGrow: 1 },
123
- children: jsx('text', {
124
- style: { color: isCursor ? s.cursorTextColor : s.color },
125
- children: item,
126
- }),
127
- })
128
- return row(content)
129
- })
245
+ const handleClose = () => setOpen(false)
130
246
 
131
- const dropdownH = visibleCount + 2
247
+ const maxLen = items.reduce((m, v) => Math.max(m, v.length), 0)
248
+ const scrollable = items.length > visibleCount
249
+ const dropWidth = maxLen + 4 + (scrollable ? 2 : 0)
132
250
 
133
- const dropdown = jsx('box', {
134
- style: {
135
- flexDirection: 'column',
136
- border: s.border,
137
- borderColor: s.borderColor,
138
- height: dropdownH,
139
- width: maxLen + 4 + (scrollable ? 2 : 0),
140
- bg: s.bg,
141
- },
142
- children: dropdownChildren,
251
+ const dropdown = jsx(SelectDropdown, {
252
+ items,
253
+ cursor: cur,
254
+ scroll: newScroll,
255
+ visibleCount,
256
+ onSelect: handleSelect,
257
+ onClose: handleClose,
258
+ onCursorChange: (idx) => setCursor(idx),
259
+ renderItem,
260
+ style: s,
261
+ accent,
262
+ dropWidth,
143
263
  })
144
264
 
145
265
  if (overlay) {
package/src/shimmer.js ADDED
@@ -0,0 +1,95 @@
1
+ import { jsx, jsxs } from '../jsx-runtime.js'
2
+ import { createSignalRaw } from './signal.js'
3
+ import { useInterval, useTheme } from './hooks.js'
4
+ import { registerHook } from './renderer.js'
5
+
6
+ const NAMED_RGB = {
7
+ black: [0, 0, 0], red: [205, 0, 0], green: [0, 205, 0], yellow: [205, 205, 0],
8
+ blue: [0, 0, 238], magenta: [205, 0, 205], cyan: [0, 205, 205], white: [229, 229, 229],
9
+ gray: [127, 127, 127], grey: [127, 127, 127],
10
+ brightRed: [255, 0, 0], brightGreen: [0, 255, 0], brightYellow: [255, 255, 0],
11
+ brightBlue: [92, 92, 255], brightMagenta: [255, 0, 255], brightCyan: [0, 255, 255],
12
+ brightWhite: [255, 255, 255],
13
+ }
14
+
15
+ function toRgb(color) {
16
+ if (NAMED_RGB[color]) return NAMED_RGB[color]
17
+ if (color?.startsWith('#') && color.length === 7) {
18
+ return [parseInt(color.slice(1, 3), 16), parseInt(color.slice(3, 5), 16), parseInt(color.slice(5, 7), 16)]
19
+ }
20
+ return [127, 127, 127]
21
+ }
22
+
23
+ function lerpColor(a, b, t) {
24
+ const r = Math.round(a[0] + (b[0] - a[0]) * t)
25
+ const g = Math.round(a[1] + (b[1] - a[1]) * t)
26
+ const bl = Math.round(a[2] + (b[2] - a[2]) * t)
27
+ return '#' + ((1 << 24) + (r << 16) + (g << 8) + bl).toString(16).slice(1)
28
+ }
29
+
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 ?? 'gray'
33
+ const hlColor = highlight ?? accent
34
+
35
+ const text = typeof children === 'string' ? children : String(children ?? '')
36
+ const len = text.length
37
+ const windowSize = size + gradient * 2
38
+ const totalFrames = len + windowSize
39
+ const msPerFrame = duration / totalFrames
40
+ const delayFrames = Math.ceil(delay / msPerFrame)
41
+ const cycleLength = totalFrames + delayFrames
42
+
43
+ const [pos, setPos] = registerHook(() => {
44
+ const [get, set] = createSignalRaw(0)
45
+ return [get, set]
46
+ })
47
+
48
+ useInterval(() => setPos(p => (p + 1) % cycleLength), msPerFrame)
49
+
50
+ const p = pos()
51
+
52
+ if (p >= totalFrames) {
53
+ return jsx('text', { style: { color: baseColor }, children: text })
54
+ }
55
+
56
+ const baseRgb = toRgb(baseColor)
57
+ const hlRgb = toRgb(hlColor)
58
+ const center = reverse ? (len + windowSize / 2) - p : p - windowSize / 2
59
+ const halfSize = (size - 1) / 2
60
+ const parts = []
61
+ let run = { color: null, bold: false, chars: '' }
62
+
63
+ function flushRun() {
64
+ if (run.chars) {
65
+ parts.push(jsx('text', { style: { color: run.color, bold: run.bold }, children: run.chars }))
66
+ run = { color: null, bold: false, chars: '' }
67
+ }
68
+ }
69
+
70
+ for (let i = 0; i < len; i++) {
71
+ const dist = Math.abs(i - center)
72
+ let charColor
73
+ let bold = false
74
+
75
+ if (dist <= halfSize) {
76
+ charColor = lerpColor(baseRgb, hlRgb, 1)
77
+ bold = true
78
+ } else if (gradient > 0 && dist <= halfSize + gradient) {
79
+ const t = 1 - (dist - halfSize) / gradient
80
+ charColor = lerpColor(baseRgb, hlRgb, t)
81
+ } else {
82
+ charColor = baseColor
83
+ }
84
+
85
+ if (charColor !== run.color || bold !== run.bold) {
86
+ flushRun()
87
+ run.color = charColor
88
+ run.bold = bold
89
+ }
90
+ run.chars += text[i]
91
+ }
92
+ flushRun()
93
+
94
+ return jsxs('box', { style: { flexDirection: 'row' }, children: parts })
95
+ }
package/src/signal.js CHANGED
@@ -4,6 +4,17 @@ let pendingEffects = null
4
4
  let batchDepth = 0
5
5
  let schedulerHook = null
6
6
  let hookRegistrar = null
7
+ let renderTracker = null
8
+
9
+ export function startRenderTracking() {
10
+ renderTracker = []
11
+ }
12
+
13
+ export function stopRenderTracking() {
14
+ const tracked = renderTracker
15
+ renderTracker = null
16
+ return tracked
17
+ }
7
18
 
8
19
  export function setSchedulerHook(fn) {
9
20
  schedulerHook = fn
@@ -18,6 +29,7 @@ export function createSignalRaw(value) {
18
29
 
19
30
  function get() {
20
31
  if (currentEffect) subs.add(currentEffect)
32
+ if (renderTracker) renderTracker.push(get)
21
33
  return value
22
34
  }
23
35
 
package/src/spinner.js CHANGED
@@ -3,15 +3,24 @@ import { createSignal } from './signal.js'
3
3
  import { useInterval, useTheme } from './hooks.js'
4
4
  import { registerHook } from './renderer.js'
5
5
 
6
- const FRAMES = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F']
6
+ const VARIANTS = {
7
+ dots: ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'],
8
+ line: ['|', '/', '-', '\\'],
9
+ circle: ['\u25D0', '\u25D3', '\u25D1', '\u25D2'],
10
+ bounce: ['\u2801', '\u2802', '\u2804', '\u2802'],
11
+ arrow: ['\u2190', '\u2196', '\u2191', '\u2197', '\u2192', '\u2198', '\u2193', '\u2199'],
12
+ square: ['\u25F0', '\u25F3', '\u25F2', '\u25F1'],
13
+ star: ['\u2736', '\u2738', '\u2739', '\u273A', '\u2739', '\u2738'],
14
+ }
7
15
 
8
- export function Spinner({ label, color, interval = 80 }) {
16
+ export function Spinner({ label, color, interval = 80, variant = 'dots', frames }) {
9
17
  const { accent = 'cyan' } = useTheme()
10
18
  const c = color ?? accent
19
+ const f = frames ?? VARIANTS[variant] ?? VARIANTS.dots
11
20
  const [frame, setFrame] = createSignal(0)
12
- useInterval(() => setFrame(f => (f + 1) % FRAMES.length), interval)
21
+ useInterval(() => setFrame(i => (i + 1) % f.length), interval)
13
22
 
14
- const children = [jsx('text', { style: { color: c }, children: FRAMES[frame()] })]
23
+ const children = [jsx('text', { style: { color: c }, children: f[frame()] })]
15
24
 
16
25
  if (label != null) {
17
26
  children.push(jsx('text', { children: ` ${label}` }))
@@ -0,0 +1,66 @@
1
+ import { jsx } from '../jsx-runtime.js'
2
+
3
+ const DIVIDER_CHARS = {
4
+ single: { h: '\u2500', v: '\u2502' },
5
+ double: { h: '\u2550', v: '\u2551' },
6
+ round: { h: '\u2500', v: '\u2502' },
7
+ bold: { h: '\u2501', v: '\u2503' },
8
+ }
9
+
10
+ function parseSize(s) {
11
+ if (typeof s === 'number') return { type: 'fixed', value: s }
12
+ const m = String(s).match(/^(\d*\.?\d+)fr$/)
13
+ return m ? { type: 'fr', value: parseFloat(m[1]) } : { type: 'fixed', value: parseInt(s) || 0 }
14
+ }
15
+
16
+ function sizeToStyle(size, isRow) {
17
+ const parsed = parseSize(size)
18
+ if (parsed.type === 'fixed') return { [isRow ? 'width' : 'height']: parsed.value }
19
+ return { flexGrow: parsed.value }
20
+ }
21
+
22
+ export function SplitPane({ children, direction = 'row', sizes: sizesProp, border = 'single', borderColor, borderEdges, style }) {
23
+ const items = Array.isArray(children) ? children.filter(c => c != null && c !== true && c !== false) : children ? [children] : []
24
+ const n = items.length
25
+ if (n === 0) return null
26
+
27
+ const isRow = direction === 'row'
28
+ const chars = DIVIDER_CHARS[border] ?? DIVIDER_CHARS.single
29
+ const sizes = sizesProp ?? items.map(() => '1fr')
30
+
31
+ const elements = []
32
+ for (let i = 0; i < n; i++) {
33
+ elements.push(
34
+ jsx('box', {
35
+ key: `p${i}`,
36
+ style: { ...sizeToStyle(sizes[i] ?? '1fr', isRow), flexDirection: 'column' },
37
+ children: items[i],
38
+ })
39
+ )
40
+
41
+ if (i < n - 1) {
42
+ elements.push(
43
+ jsx('box', {
44
+ key: `d${i}`,
45
+ style: {
46
+ [isRow ? 'width' : 'height']: 1,
47
+ texture: isRow ? chars.v : chars.h,
48
+ textureColor: borderColor,
49
+ _divider: isRow ? 'vertical' : 'horizontal',
50
+ },
51
+ })
52
+ )
53
+ }
54
+ }
55
+
56
+ return jsx('box', {
57
+ style: {
58
+ ...style,
59
+ border: border || undefined,
60
+ borderColor,
61
+ borderEdges,
62
+ flexDirection: isRow ? 'row' : 'column',
63
+ },
64
+ children: elements,
65
+ })
66
+ }