@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendr/core",
3
- "version": "0.4.17",
3
+ "version": "0.4.19",
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/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)