@trendr/core 0.4.17 → 0.4.19
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/README.md +1 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/field-list.js +56 -0
- package/src/focus.js +2 -1
- package/src/number-input.js +71 -0
- package/src/text-area.js +8 -1
- package/src/text-input.js +8 -2
package/README.md
CHANGED
|
@@ -547,6 +547,7 @@ Multi-line text input. Auto-grows up to `maxHeight`, then scrolls.
|
|
|
547
547
|
value={draft()} // controlled value (optional)
|
|
548
548
|
submitOnEnter={false} // default false: Alt+Enter submits, Enter inserts newline.
|
|
549
549
|
// true flips it: Enter submits, Shift/Alt+Enter inserts newline
|
|
550
|
+
newlineOnBackslashEnter={false} // replace a backslash immediately before Enter with a newline
|
|
550
551
|
clearOnSubmit={true} // reset to empty on submit (default true)
|
|
551
552
|
cursor={{ blink: true }} // per-component cursor config (overrides theme)
|
|
552
553
|
onChange={(v, prev) => {}} // every edit, receives new and previous value
|
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { createEffect, createMemo, batch, untrack, onCleanup } from './src/signa
|
|
|
4
4
|
export { useInput, useMouse, useResize, useInterval, useTimeout, useLayout, useHitTest, useStdout, useRepaint, useTitle, useTheme, useCursor, useFrameStats, useAsync } from './src/hooks.js'
|
|
5
5
|
export { Box, Text, Spacer } from './src/components.js'
|
|
6
6
|
export { TextInput } from './src/text-input.js'
|
|
7
|
+
export { NumberInput } from './src/number-input.js'
|
|
7
8
|
export { TextArea } from './src/text-area.js'
|
|
8
9
|
export { List } from './src/list.js'
|
|
9
10
|
export { Table } from './src/table.js'
|
|
@@ -20,6 +21,7 @@ export { Diff } from './src/diff-view.js'
|
|
|
20
21
|
export { computeDiff } from './src/diff-engine.js'
|
|
21
22
|
export { PickList } from './src/pick-list.js'
|
|
22
23
|
export { ScrollBox } from './src/scroll-box.js'
|
|
24
|
+
export { FieldList, Field } from './src/field-list.js'
|
|
23
25
|
export { HorizontalScrollBox } from './src/horizontal-scroll-box.js'
|
|
24
26
|
export { SplitPane } from './src/split-pane.js'
|
|
25
27
|
export { MillerNav } from './src/miller-nav.js'
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx } from '../jsx-runtime.js'
|
|
2
|
+
import { useFocus } from './focus.js'
|
|
3
|
+
import { useLayout } from './hooks.js'
|
|
4
|
+
import { ScrollBox } from './scroll-box.js'
|
|
5
|
+
|
|
6
|
+
function toArray(children) {
|
|
7
|
+
if (children == null) return []
|
|
8
|
+
if (!Array.isArray(children)) return [children]
|
|
9
|
+
return children.flatMap(toArray)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FieldList({
|
|
13
|
+
children,
|
|
14
|
+
focused = true,
|
|
15
|
+
initialFocus,
|
|
16
|
+
focusPadding = 1,
|
|
17
|
+
scrollbar = false,
|
|
18
|
+
gap = 0,
|
|
19
|
+
style,
|
|
20
|
+
}) {
|
|
21
|
+
const focus = useFocus({ initial: initialFocus, active: focused })
|
|
22
|
+
const fieldContext = { focus, active: focused }
|
|
23
|
+
const fields = toArray(children).map((child) => {
|
|
24
|
+
if (child == null || typeof child !== 'object') return child
|
|
25
|
+
return {
|
|
26
|
+
...child,
|
|
27
|
+
props: { ...child.props, fieldContext },
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return jsx(ScrollBox, {
|
|
32
|
+
focused,
|
|
33
|
+
followFocus: focus,
|
|
34
|
+
focusPadding,
|
|
35
|
+
scrollbar,
|
|
36
|
+
gap,
|
|
37
|
+
style,
|
|
38
|
+
children: fields,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Field({ name, disabled = false, children, style, fieldContext }) {
|
|
43
|
+
if (!fieldContext) throw new Error('Field must be a direct child of FieldList')
|
|
44
|
+
if (name == null) throw new Error('Field requires a name')
|
|
45
|
+
|
|
46
|
+
const { focus, active } = fieldContext
|
|
47
|
+
const layout = useLayout()
|
|
48
|
+
|
|
49
|
+
if (!disabled) focus.item(name, layout)
|
|
50
|
+
const focused = active && !disabled && focus.is(name)
|
|
51
|
+
|
|
52
|
+
return jsx('box', {
|
|
53
|
+
style: { flexDirection: 'column', flexShrink: 0, ...style },
|
|
54
|
+
children: typeof children === 'function' ? children({ focused, disabled }) : children,
|
|
55
|
+
})
|
|
56
|
+
}
|
package/src/focus.js
CHANGED
|
@@ -17,7 +17,7 @@ export function useFocusTrap(active) {
|
|
|
17
17
|
})
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function useFocus({ initial, cycle = 'tab' } = {}) {
|
|
20
|
+
export function useFocus({ initial, cycle = 'tab', active = true } = {}) {
|
|
21
21
|
const state = registerHook(() => {
|
|
22
22
|
const [current, setCurrent] = createSignalRaw(initial ?? null)
|
|
23
23
|
return {
|
|
@@ -170,6 +170,7 @@ export function useFocus({ initial, cycle = 'tab' } = {}) {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
useInput((event) => {
|
|
173
|
+
if (!active) return
|
|
173
174
|
const items = liveItems()
|
|
174
175
|
if (items.length === 0) return
|
|
175
176
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx } from '../jsx-runtime.js'
|
|
2
|
+
import { createSignal } from './signal.js'
|
|
3
|
+
import { useInput } from './hooks.js'
|
|
4
|
+
import { TextInput } from './text-input.js'
|
|
5
|
+
|
|
6
|
+
function clamp(value, min, max) {
|
|
7
|
+
return Math.min(max, Math.max(min, value))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stepPrecision(step) {
|
|
11
|
+
const decimal = String(step).split('.')[1]
|
|
12
|
+
return decimal?.length ?? 0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NumberInput({
|
|
16
|
+
value = 0,
|
|
17
|
+
onChange,
|
|
18
|
+
focused = true,
|
|
19
|
+
min = -Infinity,
|
|
20
|
+
max = Infinity,
|
|
21
|
+
step = 1,
|
|
22
|
+
width = 8,
|
|
23
|
+
placeholder,
|
|
24
|
+
}) {
|
|
25
|
+
const [draft, setDraft] = createSignal(String(value))
|
|
26
|
+
const [syncedValue, setSyncedValue] = createSignal(value)
|
|
27
|
+
|
|
28
|
+
if (value !== syncedValue()) {
|
|
29
|
+
setSyncedValue(value)
|
|
30
|
+
setDraft(String(value))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function update(next) {
|
|
34
|
+
const bounded = clamp(next, min, max)
|
|
35
|
+
const precision = stepPrecision(step)
|
|
36
|
+
const normalized = precision > 0 ? Number(bounded.toFixed(precision)) : bounded
|
|
37
|
+
setDraft(String(normalized))
|
|
38
|
+
setSyncedValue(normalized)
|
|
39
|
+
onChange?.(normalized)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
useInput((event) => {
|
|
43
|
+
if (!focused || (event.key !== 'up' && event.key !== 'down')) return
|
|
44
|
+
const parsed = Number(draft())
|
|
45
|
+
const current = Number.isFinite(parsed) ? parsed : Number(value) || 0
|
|
46
|
+
update(current + (event.key === 'up' ? step : -step))
|
|
47
|
+
event.stopPropagation()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
function updateDraft(next) {
|
|
51
|
+
if (!/^-?(?:\d+\.?\d*|\.\d*)?$/.test(next)) return
|
|
52
|
+
setDraft(next)
|
|
53
|
+
const parsed = Number(next)
|
|
54
|
+
if (next !== '' && next !== '-' && next !== '.' && next !== '-.' && Number.isFinite(parsed)) {
|
|
55
|
+
const bounded = clamp(parsed, min, max)
|
|
56
|
+
if (bounded !== parsed) setDraft(String(bounded))
|
|
57
|
+
setSyncedValue(bounded)
|
|
58
|
+
onChange?.(bounded)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return jsx('box', {
|
|
63
|
+
style: { width, minWidth: width, height: 1 },
|
|
64
|
+
children: jsx(TextInput, {
|
|
65
|
+
value: draft(),
|
|
66
|
+
focused,
|
|
67
|
+
placeholder,
|
|
68
|
+
onChange: updateDraft,
|
|
69
|
+
}),
|
|
70
|
+
})
|
|
71
|
+
}
|
package/src/text-area.js
CHANGED
|
@@ -124,7 +124,7 @@ function ensureVisible(cursorRow, scroll, height, totalLines) {
|
|
|
124
124
|
return scroll
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder, focused = true, maxHeight = 10, clearOnSubmit = true, cursor: cursorProp, value: valueProp, submitOnEnter = false, color, lineCounter = false }) {
|
|
127
|
+
export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder, focused = true, maxHeight = 10, clearOnSubmit = true, cursor: cursorProp, value: valueProp, submitOnEnter = false, newlineOnBackslashEnter = false, color, lineCounter = false }) {
|
|
128
128
|
const [value, setValue] = createSignal('')
|
|
129
129
|
const [cursor, setCursor] = createSignal(0)
|
|
130
130
|
if (valueProp !== undefined && valueProp !== value()) {
|
|
@@ -169,6 +169,7 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
169
169
|
return
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
const isBackslashNewlineKey = newlineOnBackslashEnter && key === 'return' && !meta && c > 0 && v[c - 1] === '\\'
|
|
172
173
|
const isSubmitKey = submitOnEnter
|
|
173
174
|
? (key === 'return' && !meta && !shift)
|
|
174
175
|
: (meta && key === 'return')
|
|
@@ -176,6 +177,12 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
176
177
|
? ((shift && key === 'return') || (meta && key === 'return'))
|
|
177
178
|
: (key === 'return')
|
|
178
179
|
|
|
180
|
+
if (isBackslashNewlineKey) {
|
|
181
|
+
update(v.slice(0, c - 1) + '\n' + v.slice(c), c)
|
|
182
|
+
event.stopPropagation()
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
179
186
|
if (isSubmitKey) {
|
|
180
187
|
if (onSubmit) onSubmit(v)
|
|
181
188
|
if (clearOnSubmit) update('', 0)
|
package/src/text-input.js
CHANGED
|
@@ -37,10 +37,16 @@ function firstCodePoint(s) {
|
|
|
37
37
|
return s.slice(0, nextBoundary(s, 0))
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused = true, initialValue, clearOnSubmit = false, cursor: cursorProp }) {
|
|
41
|
-
const init = initialValue ?? ''
|
|
40
|
+
export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused = true, initialValue, value: valueProp, clearOnSubmit = false, cursor: cursorProp }) {
|
|
41
|
+
const init = valueProp ?? initialValue ?? ''
|
|
42
42
|
const [value, setValue] = createSignal(init)
|
|
43
43
|
const [cursor, setCursor] = createSignal(init.length)
|
|
44
|
+
const [controlledValue, setControlledValue] = createSignal(valueProp)
|
|
45
|
+
if (valueProp !== undefined && valueProp !== controlledValue()) {
|
|
46
|
+
setControlledValue(valueProp)
|
|
47
|
+
setValue(valueProp)
|
|
48
|
+
setCursor(valueProp.length)
|
|
49
|
+
}
|
|
44
50
|
const { muted = 'gray' } = useTheme()
|
|
45
51
|
const layout = useLayout()
|
|
46
52
|
const { cursorStyle, reset: resetBlink } = useCursor(cursorProp, focused)
|