@trendr/core 0.4.16 → 0.4.18

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendr/core",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "direct-mode TUI renderer with JSX, signals, and per-cell diffing",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Pyers",
@@ -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/scroll-box.js CHANGED
@@ -25,7 +25,7 @@ export function ScrollBox({ children, focused = true, followFocus, focusPadding
25
25
  if (followFocus) {
26
26
  const target = typeof followFocus === 'function' ? followFocus() : followFocus.currentLayout?.()
27
27
  if (target && visibleH > 0) {
28
- const top = target.y - layout.y + clamped
28
+ const top = target.y - layout.y
29
29
  const bottom = top + Math.max(1, target.height ?? 1)
30
30
  const padding = Math.max(0, focusPadding)
31
31
  const next = top < clamped + padding
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)