@trendr/core 0.2.5 → 0.2.7
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 +1 -1
- package/package.json +1 -1
- package/src/focus.js +31 -1
- package/src/input.js +16 -0
- package/src/layout.js +1 -1
- package/src/list.js +8 -1
- package/src/modal.js +3 -0
- package/src/pick-list.js +10 -1
- package/src/renderer.js +2 -1
- package/src/text-area.js +20 -8
package/index.js
CHANGED
|
@@ -23,7 +23,7 @@ export { MillerNav } from './src/miller-nav.js'
|
|
|
23
23
|
export { MenuBar } from './src/menubar.js'
|
|
24
24
|
export { Task } from './src/task.js'
|
|
25
25
|
export { Shimmer } from './src/shimmer.js'
|
|
26
|
-
export { useFocus } from './src/focus.js'
|
|
26
|
+
export { useFocus, useFocusTrap } from './src/focus.js'
|
|
27
27
|
export { useHotkey } from './src/hotkey.js'
|
|
28
28
|
export { useToast } from './src/toast.js'
|
|
29
29
|
export { Fragment } from './src/element.js'
|
package/package.json
CHANGED
package/src/focus.js
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
|
-
import { createSignalRaw } from './signal.js'
|
|
1
|
+
import { createSignalRaw, onCleanup } from './signal.js'
|
|
2
2
|
import { registerHook } from './renderer.js'
|
|
3
3
|
import { useInput } from './hooks.js'
|
|
4
4
|
|
|
5
|
+
let focusTrapStack = []
|
|
6
|
+
|
|
7
|
+
export function useFocusTrap(active) {
|
|
8
|
+
const state = registerHook(() => {
|
|
9
|
+
const id = Symbol()
|
|
10
|
+
onCleanup(() => {
|
|
11
|
+
const idx = focusTrapStack.indexOf(id)
|
|
12
|
+
if (idx >= 0) focusTrapStack.splice(idx, 1)
|
|
13
|
+
})
|
|
14
|
+
return { id, wasActive: false }
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
if (active && !state.wasActive) {
|
|
18
|
+
focusTrapStack.push(state.id)
|
|
19
|
+
state.wasActive = true
|
|
20
|
+
} else if (!active && state.wasActive) {
|
|
21
|
+
const idx = focusTrapStack.indexOf(state.id)
|
|
22
|
+
if (idx >= 0) focusTrapStack.splice(idx, 1)
|
|
23
|
+
state.wasActive = false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
useInput((event) => {
|
|
27
|
+
if (!active) return
|
|
28
|
+
if (event.key === 'tab' || event.key === 'shift-tab') {
|
|
29
|
+
event.stopPropagation()
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
5
34
|
export function useFocus({ initial, cycle = 'tab' } = {}) {
|
|
6
35
|
const state = registerHook(() => {
|
|
7
36
|
const [current, setCurrent] = createSignalRaw(initial ?? null)
|
|
@@ -93,6 +122,7 @@ export function useFocus({ initial, cycle = 'tab' } = {}) {
|
|
|
93
122
|
const cur = state.current()
|
|
94
123
|
|
|
95
124
|
if (state.stack.length > 0) return
|
|
125
|
+
if (focusTrapStack.length > 0) return
|
|
96
126
|
|
|
97
127
|
if (cycle === 'tab' && (key === 'tab' || key === 'shift-tab')) {
|
|
98
128
|
const idx = findTopLevel(cur)
|
package/src/input.js
CHANGED
|
@@ -153,7 +153,23 @@ export function createInputHandler(stream) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
function isPaste(data) {
|
|
157
|
+
const s = typeof data === 'string' ? data : data.toString()
|
|
158
|
+
return s.length > 1 && !s.startsWith('\x1b') && (s.includes('\n') || s.includes('\r'))
|
|
159
|
+
}
|
|
160
|
+
|
|
156
161
|
function onData(data) {
|
|
162
|
+
if (isPaste(data)) {
|
|
163
|
+
const text = (typeof data === 'string' ? data : data.toString()).replace(/\r\n?/g, '\n')
|
|
164
|
+
const event = { key: 'paste', text, ctrl: false, meta: false, shift: false, raw: text }
|
|
165
|
+
event.stopPropagation = () => { event._stopped = true }
|
|
166
|
+
const snapshot = [...keyListeners].reverse()
|
|
167
|
+
for (const fn of snapshot) {
|
|
168
|
+
fn(event)
|
|
169
|
+
if (event._stopped) break
|
|
170
|
+
}
|
|
171
|
+
return
|
|
172
|
+
}
|
|
157
173
|
for (const key of splitKeys(data)) {
|
|
158
174
|
dispatch(key)
|
|
159
175
|
}
|
package/src/layout.js
CHANGED
|
@@ -259,7 +259,7 @@ function measureChild(child, cs, isRow, availW, availH) {
|
|
|
259
259
|
const text = extractText(leaf)
|
|
260
260
|
const overflow = cs.overflow
|
|
261
261
|
if (!text) { w = explicitW ?? 0; h = explicitH ?? 1 }
|
|
262
|
-
else if (overflow === 'nowrap' || overflow === 'truncate') {
|
|
262
|
+
else if (overflow === 'nowrap' || overflow === 'truncate' || overflow === 'clip') {
|
|
263
263
|
w = explicitW ?? Math.min(availW, measureText(text))
|
|
264
264
|
h = explicitH ?? 1
|
|
265
265
|
} else {
|
package/src/list.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
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
|
+
import { registerHook } from './renderer.js'
|
|
4
5
|
|
|
5
|
-
export function List({ items, selected: selectedProp, onSelect, renderItem, header, headerHeight = 1, focused = true, interactive = focused, itemHeight = 1, scrollbar = false, stickyHeader = false, gap = 0, scrolloff = 2 }) {
|
|
6
|
+
export function List({ items, selected: selectedProp, onSelect, onCursorChange, renderItem, header, headerHeight = 1, focused = true, interactive = focused, itemHeight = 1, scrollbar = false, stickyHeader = false, gap = 0, scrolloff = 2 }) {
|
|
6
7
|
const { accent = 'cyan' } = useTheme()
|
|
7
8
|
const [selectedInternal, setSelectedInternal] = createSignal(0)
|
|
8
9
|
const [scrollState, setScrollState] = createSignal(0)
|
|
9
10
|
const layout = useLayout()
|
|
11
|
+
const prevCursor = registerHook(() => ({ index: -1 }))
|
|
10
12
|
|
|
11
13
|
const selected = selectedProp ?? selectedInternal()
|
|
12
14
|
const setSelected = onSelect ?? setSelectedInternal
|
|
13
15
|
|
|
16
|
+
if (onCursorChange && selected !== prevCursor.index && items.length > 0) {
|
|
17
|
+
prevCursor.index = selected
|
|
18
|
+
onCursorChange(items[selected], selected)
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
const viewH = layout.height
|
|
15
22
|
const contentH = layout.contentHeight ?? 0
|
|
16
23
|
const headerH = header ? headerHeight : 0
|
package/src/modal.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx } from '../jsx-runtime.js'
|
|
2
2
|
import { useInput, useTheme } from './hooks.js'
|
|
3
3
|
import { registerOverlay } from './renderer.js'
|
|
4
|
+
import { useFocusTrap } from './focus.js'
|
|
4
5
|
|
|
5
6
|
export function Modal({ open, onClose, title, children, width: w = 40, border = 'round' }) {
|
|
6
7
|
const { accent = 'cyan' } = useTheme()
|
|
7
8
|
|
|
9
|
+
useFocusTrap(open)
|
|
10
|
+
|
|
8
11
|
useInput((event) => {
|
|
9
12
|
if (!open) return
|
|
10
13
|
if (event.key === 'escape') {
|
package/src/pick-list.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
3
|
import { useInput, useLayout, useTheme, useCursor } from './hooks.js'
|
|
4
|
+
import { registerHook } from './renderer.js'
|
|
4
5
|
import { List } from './list.js'
|
|
5
6
|
|
|
6
|
-
export function PickList({ items, onSelect, onCancel, onChange, focused = true, placeholder = 'search...', filter: filterFn, renderItem, maxVisible = 10, scrollbar = false, scrolloff = 0, itemHeight = 1, itemGap = 0, gap = 0, clearOnSelect = false, style: userStyle, cursor: cursorProp }) {
|
|
7
|
+
export function PickList({ items, onSelect, onCancel, onChange, onCursorChange, focused = true, placeholder = 'search...', filter: filterFn, renderItem, maxVisible = 10, scrollbar = false, scrolloff = 0, itemHeight = 1, itemGap = 0, gap = 0, clearOnSelect = false, style: userStyle, cursor: cursorProp }) {
|
|
7
8
|
const { accent = 'cyan' } = useTheme()
|
|
8
9
|
const defaults = {
|
|
9
10
|
borderColor: accent,
|
|
@@ -33,6 +34,14 @@ export function PickList({ items, onSelect, onCancel, onChange, focused = true,
|
|
|
33
34
|
if (cursor >= filtered.length) cursor = Math.max(0, filtered.length - 1)
|
|
34
35
|
if (cursor !== listCursor()) setListCursor(cursor)
|
|
35
36
|
|
|
37
|
+
const prevCursor = registerHook(() => ({ index: -1, item: undefined }))
|
|
38
|
+
const cursorItem = filtered.length > 0 ? filtered[cursor] : undefined
|
|
39
|
+
if (onCursorChange && (cursor !== prevCursor.index || cursorItem !== prevCursor.item)) {
|
|
40
|
+
prevCursor.index = cursor
|
|
41
|
+
prevCursor.item = cursorItem
|
|
42
|
+
if (cursorItem !== undefined) onCursorChange(cursorItem, cursor)
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function updateText(v, c) {
|
|
37
46
|
setQuery(v)
|
|
38
47
|
setTextCursor(c)
|
package/src/renderer.js
CHANGED
|
@@ -321,7 +321,8 @@ function paintTree(node, buf, clip, offset, prevBuf) {
|
|
|
321
321
|
if (!text) return
|
|
322
322
|
|
|
323
323
|
const truncate = style.overflow === 'truncate'
|
|
324
|
-
const
|
|
324
|
+
const clip = style.overflow === 'clip'
|
|
325
|
+
const wrap = style.overflow !== 'nowrap' && !truncate && !clip
|
|
325
326
|
|
|
326
327
|
if (wrap) {
|
|
327
328
|
const lines = wordWrap(text, layout.width)
|
package/src/text-area.js
CHANGED
|
@@ -87,34 +87,46 @@ 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, clearOnSubmit = true, cursor: cursorProp, value: valueProp, submitOnEnter = false }) {
|
|
90
|
+
export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder, focused = true, maxHeight = 10, clearOnSubmit = true, cursor: cursorProp, value: valueProp, submitOnEnter = false }) {
|
|
91
91
|
const [value, setValue] = createSignal('')
|
|
92
92
|
const [cursor, setCursor] = createSignal(0)
|
|
93
|
-
|
|
94
|
-
if (valueProp !== undefined && valueProp !== _prev.value) {
|
|
95
|
-
_prev.value = valueProp
|
|
93
|
+
if (valueProp !== undefined && valueProp !== value()) {
|
|
96
94
|
setValue(valueProp)
|
|
97
|
-
setCursor(valueProp.length)
|
|
95
|
+
setCursor(c => Math.min(c, valueProp.length))
|
|
98
96
|
}
|
|
99
97
|
const ref = registerHook(() => ({ scroll: 0, goalCol: null }))
|
|
100
98
|
const layout = useLayout()
|
|
101
99
|
const { cursorStyle, reset: resetBlink } = useCursor(cursorProp, focused)
|
|
102
100
|
|
|
103
|
-
function update(
|
|
104
|
-
|
|
101
|
+
function update(next, c) {
|
|
102
|
+
const prev = value()
|
|
103
|
+
setValue(next)
|
|
105
104
|
setCursor(c)
|
|
106
105
|
ref.goalCol = null
|
|
107
|
-
if (onChange) onChange(
|
|
106
|
+
if (onChange) onChange(next, prev)
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
useInput((event) => {
|
|
111
110
|
if (!focused) return
|
|
111
|
+
|
|
112
|
+
if (onKeyDown && onKeyDown(event)) {
|
|
113
|
+
event.stopPropagation()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
112
117
|
resetBlink()
|
|
113
118
|
|
|
114
119
|
const { key, raw, ctrl, meta } = event
|
|
115
120
|
const v = value()
|
|
116
121
|
const c = cursor()
|
|
117
122
|
|
|
123
|
+
if (key === 'paste') {
|
|
124
|
+
const pasted = event.text || ''
|
|
125
|
+
update(v.slice(0, c) + pasted + v.slice(c), c + pasted.length)
|
|
126
|
+
event.stopPropagation()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
const isSubmitKey = submitOnEnter
|
|
119
131
|
? (key === 'return' && !meta)
|
|
120
132
|
: (meta && key === '\r')
|