@trendr/core 0.4.14 → 0.4.16
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/index.js +2 -1
- package/package.json +1 -1
- package/src/diff-view.js +11 -3
- package/src/focus.js +9 -3
- package/src/horizontal-scroll-box.js +49 -0
- package/src/layout.js +9 -4
- package/src/markdown.js +20 -7
- package/src/renderer.js +21 -12
- package/src/scroll-box.js +16 -1
package/index.js
CHANGED
|
@@ -20,11 +20,12 @@ export { Diff } from './src/diff-view.js'
|
|
|
20
20
|
export { computeDiff } from './src/diff-engine.js'
|
|
21
21
|
export { PickList } from './src/pick-list.js'
|
|
22
22
|
export { ScrollBox } from './src/scroll-box.js'
|
|
23
|
+
export { HorizontalScrollBox } from './src/horizontal-scroll-box.js'
|
|
23
24
|
export { SplitPane } from './src/split-pane.js'
|
|
24
25
|
export { MillerNav } from './src/miller-nav.js'
|
|
25
26
|
export { MenuBar } from './src/menubar.js'
|
|
26
27
|
export { Menu } from './src/menu.js'
|
|
27
|
-
export { Markdown } from './src/markdown.js'
|
|
28
|
+
export { CodeBlock, Markdown } from './src/markdown.js'
|
|
28
29
|
export { Task } from './src/task.js'
|
|
29
30
|
export { Shimmer } from './src/shimmer.js'
|
|
30
31
|
export { useFocus, useFocusTrap } from './src/focus.js'
|
package/package.json
CHANGED
package/src/diff-view.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
3
|
import { useInput, useMouse, useLayout, useHitTest, useScrollDrag } from './hooks.js'
|
|
4
|
-
import { sliceVisibleRange } from './wrap.js'
|
|
4
|
+
import { measureText, sliceVisibleRange } from './wrap.js'
|
|
5
5
|
import { computeDiff } from './diff-engine.js'
|
|
6
|
+
import { HorizontalScrollBox } from './horizontal-scroll-box.js'
|
|
6
7
|
|
|
7
8
|
const PALETTE = {
|
|
8
9
|
addBg: '#10301a', addGutterBg: '#0a2412', addIntraBg: '#1f6e34', addMarker: '#7ee787',
|
|
@@ -120,6 +121,8 @@ export function Diff({
|
|
|
120
121
|
|
|
121
122
|
const oldW = digits(rows, 'oldNo')
|
|
122
123
|
const newW = digits(rows, 'newNo')
|
|
124
|
+
const gutterWidth = lineNumbers ? oldW + newW + 6 : 2
|
|
125
|
+
const contentWidth = gutterWidth + Math.max(0, ...rows.map((row) => measureText(row.text ?? '')))
|
|
123
126
|
|
|
124
127
|
const headerH = filename ? 1 : 0
|
|
125
128
|
const h = Math.max(0, layout.height - headerH)
|
|
@@ -227,6 +230,11 @@ export function Diff({
|
|
|
227
230
|
|
|
228
231
|
const visible = h > 0 ? rows.slice(clamped, clamped + h) : []
|
|
229
232
|
const bodyRows = visible.map((row, i) => renderRow(row, clamped + i))
|
|
233
|
+
const scrollableRows = jsx(HorizontalScrollBox, {
|
|
234
|
+
contentWidth,
|
|
235
|
+
style: { flexDirection: 'column', flexGrow: 1 },
|
|
236
|
+
children: jsx('box', { style: { flexDirection: 'column' }, children: bodyRows }),
|
|
237
|
+
})
|
|
230
238
|
|
|
231
239
|
let body
|
|
232
240
|
if (rows.length === 0) {
|
|
@@ -247,12 +255,12 @@ export function Diff({
|
|
|
247
255
|
body = jsxs('box', {
|
|
248
256
|
style: { flexDirection: 'row', flexGrow: 1 },
|
|
249
257
|
children: [
|
|
250
|
-
|
|
258
|
+
scrollableRows,
|
|
251
259
|
jsx('box', { style: { flexDirection: 'column', width: barW }, children: bar }),
|
|
252
260
|
],
|
|
253
261
|
})
|
|
254
262
|
} else {
|
|
255
|
-
body =
|
|
263
|
+
body = scrollableRows
|
|
256
264
|
}
|
|
257
265
|
|
|
258
266
|
if (!filename) return body
|
package/src/focus.js
CHANGED
|
@@ -79,13 +79,14 @@ export function useFocus({ initial, cycle = 'tab' } = {}) {
|
|
|
79
79
|
return state.items.filter(i => i.gen === state.gen).sort((a, b) => a.seq - b.seq)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function item(name) {
|
|
82
|
+
function item(name, layout) {
|
|
83
83
|
const existing = state.items.find(i => i.name === name)
|
|
84
84
|
if (existing) {
|
|
85
85
|
existing.gen = state.gen
|
|
86
86
|
existing.seq = state.seq++
|
|
87
|
+
existing.layout = layout
|
|
87
88
|
} else {
|
|
88
|
-
state.items.push({ name, type: 'item', gen: state.gen, seq: state.seq
|
|
89
|
+
state.items.push({ name, type: 'item', gen: state.gen, seq: state.seq++, layout })
|
|
89
90
|
}
|
|
90
91
|
if (state.current() == null) state.setCurrent(name)
|
|
91
92
|
}
|
|
@@ -157,6 +158,11 @@ export function useFocus({ initial, cycle = 'tab' } = {}) {
|
|
|
157
158
|
return state.current()
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
function currentLayout() {
|
|
162
|
+
const name = state.current()
|
|
163
|
+
return state.items.find(i => i.name === name)?.layout ?? null
|
|
164
|
+
}
|
|
165
|
+
|
|
160
166
|
function findTopLevel(cur, items) {
|
|
161
167
|
const parent = state.nameToParent.get(cur)
|
|
162
168
|
if (parent) return items.findIndex(i => i.name === parent)
|
|
@@ -220,5 +226,5 @@ export function useFocus({ initial, cycle = 'tab' } = {}) {
|
|
|
220
226
|
}
|
|
221
227
|
})
|
|
222
228
|
|
|
223
|
-
return { item, group, is, focus, push, pop, current }
|
|
229
|
+
return { item, group, is, focus, push, pop, current, currentLayout }
|
|
224
230
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsxs, jsx } from '../jsx-runtime.js'
|
|
2
|
+
import { createSignal } from './signal.js'
|
|
3
|
+
import { useHitTest, useLayout, useMouse, useTheme } from './hooks.js'
|
|
4
|
+
|
|
5
|
+
export function HorizontalScrollBox({ children, contentWidth, step = 3, indicators = true, style: userStyle }) {
|
|
6
|
+
const { muted = 'gray' } = useTheme()
|
|
7
|
+
const [offset, setOffset] = createSignal(0)
|
|
8
|
+
const layout = useLayout()
|
|
9
|
+
const hitTest = useHitTest()
|
|
10
|
+
const measuredWidth = contentWidth ?? layout.contentWidth ?? 0
|
|
11
|
+
const maxOffset = Math.max(0, measuredWidth - layout.width)
|
|
12
|
+
const clamped = Math.max(0, Math.min(maxOffset, offset()))
|
|
13
|
+
|
|
14
|
+
useMouse((event) => {
|
|
15
|
+
if (event.action !== 'scroll' || !hitTest(event.x, event.y)) return
|
|
16
|
+
if (event.direction !== 'left' && event.direction !== 'right') return
|
|
17
|
+
const next = Math.max(0, Math.min(maxOffset, clamped + (event.direction === 'left' ? -step : step)))
|
|
18
|
+
if (next === clamped) return
|
|
19
|
+
setOffset(next)
|
|
20
|
+
event.stopPropagation()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const overlays = []
|
|
24
|
+
if (indicators && clamped > 0) {
|
|
25
|
+
overlays.push(jsx('text', {
|
|
26
|
+
key: 'left',
|
|
27
|
+
style: { position: 'absolute', left: 0, top: 0, color: muted, dim: true, copyIgnore: true },
|
|
28
|
+
children: '‹',
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
31
|
+
if (indicators && clamped < maxOffset) {
|
|
32
|
+
overlays.push(jsx('text', {
|
|
33
|
+
key: 'right',
|
|
34
|
+
style: { position: 'absolute', right: 0, top: 0, color: muted, dim: true, copyIgnore: true },
|
|
35
|
+
children: '›',
|
|
36
|
+
}))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return jsxs('box', {
|
|
40
|
+
style: userStyle,
|
|
41
|
+
children: [
|
|
42
|
+
jsx('box', {
|
|
43
|
+
style: { flexDirection: 'row', overflow: 'scroll', scrollOffsetX: clamped },
|
|
44
|
+
children,
|
|
45
|
+
}),
|
|
46
|
+
...overlays,
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
}
|
package/src/layout.js
CHANGED
|
@@ -98,17 +98,22 @@ export function computeLayout(node, rect) {
|
|
|
98
98
|
|
|
99
99
|
if (isScroll && flowChildren.length > 0) {
|
|
100
100
|
let maxEdge = 0
|
|
101
|
-
const
|
|
101
|
+
const sizes = []
|
|
102
102
|
for (const child of flowChildren) {
|
|
103
103
|
const cl = getLeaf(child)?._layout
|
|
104
104
|
if (cl) {
|
|
105
105
|
const edge = isRow ? (cl.x + cl.width - innerX) : (cl.y + cl.height - innerY)
|
|
106
106
|
if (edge > maxEdge) maxEdge = edge
|
|
107
|
-
|
|
107
|
+
sizes.push(isRow ? cl.width : cl.height)
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
if (isRow) {
|
|
111
|
+
node._contentWidth = maxEdge
|
|
112
|
+
node._childWidths = sizes
|
|
113
|
+
} else {
|
|
114
|
+
node._contentHeight = maxEdge
|
|
115
|
+
node._childHeights = sizes
|
|
116
|
+
}
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
// absolute children anchor to the padding box (inside borders, ignoring
|
package/src/markdown.js
CHANGED
|
@@ -106,7 +106,21 @@ export function renderInline(s, { accent = 'cyan' } = {}) {
|
|
|
106
106
|
return out
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export function
|
|
109
|
+
export function CodeBlock({ value, language, highlight, codeBg = '#1e1e22' }) {
|
|
110
|
+
const shown = highlight ? highlight(value, language) : value
|
|
111
|
+
const rows = shown.split('\n').map((line, key) => jsx('text', {
|
|
112
|
+
key,
|
|
113
|
+
style: { overflow: 'truncate' },
|
|
114
|
+
children: line || ' ',
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
return jsx('box', {
|
|
118
|
+
style: { flexDirection: 'column', bg: codeBg, paddingX: 1 },
|
|
119
|
+
children: rows,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function Markdown({ text, children, highlight, codeBg = '#1e1e22', codeBlock: CodeBlockComponent = CodeBlock, style: userStyle }) {
|
|
110
124
|
const { accent = 'cyan', muted = 'gray' } = useTheme()
|
|
111
125
|
const layout = useLayout()
|
|
112
126
|
const src = text ?? (Array.isArray(children) ? children.join('') : (children ?? ''))
|
|
@@ -126,13 +140,12 @@ export function Markdown({ text, children, highlight, codeBg = '#1e1e22', style:
|
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
if (block.type === 'code') {
|
|
129
|
-
|
|
130
|
-
const shown = highlight ? highlight(raw, block.lang) : raw
|
|
131
|
-
const rows = shown.split('\n').map((line, k) => jsx('text', { key: k, style: { overflow: 'truncate' }, children: line || ' ' }))
|
|
132
|
-
return jsx('box', {
|
|
143
|
+
return jsx(CodeBlockComponent, {
|
|
133
144
|
key,
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
value: block.lines.join('\n'),
|
|
146
|
+
language: block.lang,
|
|
147
|
+
highlight,
|
|
148
|
+
codeBg,
|
|
136
149
|
})
|
|
137
150
|
}
|
|
138
151
|
|
package/src/renderer.js
CHANGED
|
@@ -220,14 +220,14 @@ function clearOverlayRect(overlayTree, buf) {
|
|
|
220
220
|
fillRect(buf, rect.x, rect.y, rect.width, rect.height, ' ', null, null, 0)
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
function
|
|
223
|
+
function findScrollContentSize(node, field) {
|
|
224
224
|
if (!node) return null
|
|
225
|
-
if (node
|
|
226
|
-
if (node._resolved) return
|
|
225
|
+
if (node[field] != null) return node[field]
|
|
226
|
+
if (node._resolved) return findScrollContentSize(node._resolved, field)
|
|
227
227
|
if (node._resolvedChildren) {
|
|
228
228
|
for (const child of node._resolvedChildren) {
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
229
|
+
const size = findScrollContentSize(child, field)
|
|
230
|
+
if (size != null) return size
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
return null
|
|
@@ -251,7 +251,8 @@ function updateOverlayLayouts(node) {
|
|
|
251
251
|
if (node._instance) {
|
|
252
252
|
const rect = node._availableRect ?? node._layout
|
|
253
253
|
if (rect) {
|
|
254
|
-
const ch =
|
|
254
|
+
const ch = findScrollContentSize(node, '_contentHeight')
|
|
255
|
+
const cw = findScrollContentSize(node, '_contentWidth')
|
|
255
256
|
if (!node._instance.layout) node._instance.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
256
257
|
const target = node._instance.layout
|
|
257
258
|
target.x = rect.x
|
|
@@ -259,6 +260,7 @@ function updateOverlayLayouts(node) {
|
|
|
259
260
|
target.width = rect.width
|
|
260
261
|
target.height = rect.height
|
|
261
262
|
target.contentHeight = ch
|
|
263
|
+
target.contentWidth = cw
|
|
262
264
|
target.childHeights = findScrollChildHeights(node)
|
|
263
265
|
}
|
|
264
266
|
}
|
|
@@ -417,8 +419,9 @@ function paintTree(node, buf, clip, offset, prevBuf) {
|
|
|
417
419
|
let childOffset = offset
|
|
418
420
|
let childPrevBuf = prevBuf
|
|
419
421
|
if (style.overflow === 'scroll') {
|
|
422
|
+
const scrollX = style.scrollOffsetX ?? 0
|
|
420
423
|
const scrollY = style.scrollOffset ?? 0
|
|
421
|
-
childOffset = { x: offset?.x ?? 0, y: (offset?.y ?? 0) - scrollY }
|
|
424
|
+
childOffset = { x: (offset?.x ?? 0) - scrollX, y: (offset?.y ?? 0) - scrollY }
|
|
422
425
|
childPrevBuf = null
|
|
423
426
|
}
|
|
424
427
|
|
|
@@ -807,15 +810,17 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
807
810
|
for (const inst of instances.values()) {
|
|
808
811
|
const rect = inst.node?._availableRect ?? inst.node?._layout
|
|
809
812
|
if (!rect) continue
|
|
810
|
-
const ch =
|
|
813
|
+
const ch = findScrollContentSize(inst.node, '_contentHeight')
|
|
814
|
+
const cw = findScrollContentSize(inst.node, '_contentWidth')
|
|
811
815
|
if (!inst.layout) inst.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
812
816
|
const p = inst.layout
|
|
813
|
-
if (p.width !== rect.width || p.height !== rect.height || p.contentHeight !== ch) changed = true
|
|
817
|
+
if (p.width !== rect.width || p.height !== rect.height || p.contentHeight !== ch || p.contentWidth !== cw) changed = true
|
|
814
818
|
p.x = rect.x
|
|
815
819
|
p.y = rect.y
|
|
816
820
|
p.width = rect.width
|
|
817
821
|
p.height = rect.height
|
|
818
822
|
p.contentHeight = ch
|
|
823
|
+
p.contentWidth = cw
|
|
819
824
|
p.childHeights = findScrollChildHeights(inst.node)
|
|
820
825
|
}
|
|
821
826
|
return changed
|
|
@@ -990,10 +995,11 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
990
995
|
for (const inst of instances.values()) {
|
|
991
996
|
const rect = inst.node?._availableRect ?? inst.node?._layout
|
|
992
997
|
if (!rect) continue
|
|
993
|
-
const ch =
|
|
998
|
+
const ch = findScrollContentSize(inst.node, '_contentHeight')
|
|
999
|
+
const cw = findScrollContentSize(inst.node, '_contentWidth')
|
|
994
1000
|
if (!inst.layout) inst.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
995
1001
|
const prev = inst.layout
|
|
996
|
-
if (prev.width !== rect.width || prev.height !== rect.height || prev.contentHeight !== ch) {
|
|
1002
|
+
if (prev.width !== rect.width || prev.height !== rect.height || prev.contentHeight !== ch || prev.contentWidth !== cw) {
|
|
997
1003
|
layoutChanged = true
|
|
998
1004
|
}
|
|
999
1005
|
prev.x = rect.x
|
|
@@ -1001,6 +1007,7 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
1001
1007
|
prev.width = rect.width
|
|
1002
1008
|
prev.height = rect.height
|
|
1003
1009
|
prev.contentHeight = ch
|
|
1010
|
+
prev.contentWidth = cw
|
|
1004
1011
|
prev.childHeights = findScrollChildHeights(inst.node)
|
|
1005
1012
|
}
|
|
1006
1013
|
|
|
@@ -1014,13 +1021,15 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
1014
1021
|
for (const inst of instances.values()) {
|
|
1015
1022
|
const rect = inst.node?._availableRect ?? inst.node?._layout
|
|
1016
1023
|
if (!rect) continue
|
|
1017
|
-
const ch =
|
|
1024
|
+
const ch = findScrollContentSize(inst.node, '_contentHeight')
|
|
1025
|
+
const cw = findScrollContentSize(inst.node, '_contentWidth')
|
|
1018
1026
|
if (!inst.layout) inst.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
1019
1027
|
inst.layout.x = rect.x
|
|
1020
1028
|
inst.layout.y = rect.y
|
|
1021
1029
|
inst.layout.width = rect.width
|
|
1022
1030
|
inst.layout.height = rect.height
|
|
1023
1031
|
inst.layout.contentHeight = ch
|
|
1032
|
+
inst.layout.contentWidth = cw
|
|
1024
1033
|
inst.layout.childHeights = findScrollChildHeights(inst.node)
|
|
1025
1034
|
inst._dirty = true
|
|
1026
1035
|
}
|
package/src/scroll-box.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx, jsxs } from '../jsx-runtime.js'
|
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
3
|
import { useInput, useMouse, useLayout, useTheme, useScrollDrag } from './hooks.js'
|
|
4
4
|
|
|
5
|
-
export function ScrollBox({ children, focused = true, scrollOffset: offsetProp, onScroll, scrollbar = false, gap = 0, thumbChar = '\u2588', trackChar = '\u2502', style: userStyle }) {
|
|
5
|
+
export function ScrollBox({ children, focused = true, followFocus, focusPadding = 0, scrollOffset: offsetProp, onScroll, scrollbar = false, gap = 0, thumbChar = '\u2588', trackChar = '\u2502', style: userStyle }) {
|
|
6
6
|
const { accent = 'cyan', muted = 'gray' } = useTheme()
|
|
7
7
|
const [offsetInternal, setOffsetInternal] = createSignal(0)
|
|
8
8
|
const layout = useLayout()
|
|
@@ -22,6 +22,21 @@ export function ScrollBox({ children, focused = true, scrollOffset: offsetProp,
|
|
|
22
22
|
const clamp = (v) => Math.max(0, Math.min(maxOffset, v))
|
|
23
23
|
const clamped = clamp(offset)
|
|
24
24
|
|
|
25
|
+
if (followFocus) {
|
|
26
|
+
const target = typeof followFocus === 'function' ? followFocus() : followFocus.currentLayout?.()
|
|
27
|
+
if (target && visibleH > 0) {
|
|
28
|
+
const top = target.y - layout.y + clamped
|
|
29
|
+
const bottom = top + Math.max(1, target.height ?? 1)
|
|
30
|
+
const padding = Math.max(0, focusPadding)
|
|
31
|
+
const next = top < clamped + padding
|
|
32
|
+
? top - padding
|
|
33
|
+
: bottom > clamped + visibleH - padding
|
|
34
|
+
? bottom - visibleH + padding
|
|
35
|
+
: clamped
|
|
36
|
+
if (clamp(next) !== clamped) setOffset(next)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
useInput(({ key, ctrl }) => {
|
|
26
41
|
if (!focused) return
|
|
27
42
|
if (contentH <= visibleH) return
|