@trendr/core 0.4.0 → 0.4.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendr/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "direct-mode TUI renderer with JSX, signals, and per-cell diffing",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Pyers",
package/src/focus.js CHANGED
@@ -40,6 +40,7 @@ export function useFocus({ initial, cycle = 'tab' } = {}) {
40
40
  function sweep() {
41
41
  const prevGen = state.gen
42
42
  state.gen = prevGen + 1
43
+ state.seq = 0
43
44
  if (prevGen === 0) return
44
45
 
45
46
  const stale = state.items.filter(i => i.gen < prevGen)
@@ -71,21 +72,32 @@ export function useFocus({ initial, cycle = 'tab' } = {}) {
71
72
  return name
72
73
  }
73
74
 
75
+ // cycle order follows this frame's registration order, not first-ever
76
+ // registration: a conditionally rendered field slots in where it is
77
+ // declared instead of appending to the end of the tab order
74
78
  function liveItems() {
75
- return state.items.filter(i => i.gen === state.gen)
79
+ return state.items.filter(i => i.gen === state.gen).sort((a, b) => a.seq - b.seq)
76
80
  }
77
81
 
78
82
  function item(name) {
79
83
  const existing = state.items.find(i => i.name === name)
80
- if (existing) existing.gen = state.gen
81
- else state.items.push({ name, type: 'item', gen: state.gen })
84
+ if (existing) {
85
+ existing.gen = state.gen
86
+ existing.seq = state.seq++
87
+ } else {
88
+ state.items.push({ name, type: 'item', gen: state.gen, seq: state.seq++ })
89
+ }
82
90
  if (state.current() == null) state.setCurrent(name)
83
91
  }
84
92
 
85
93
  function group(name, { items: subItems = [], navigate = 'both', wrap = false } = {}) {
86
94
  const existing = state.items.find(i => i.name === name)
87
- if (existing) existing.gen = state.gen
88
- else state.items.push({ name, type: 'group', gen: state.gen })
95
+ if (existing) {
96
+ existing.gen = state.gen
97
+ existing.seq = state.seq++
98
+ } else {
99
+ state.items.push({ name, type: 'group', gen: state.gen, seq: state.seq++ })
100
+ }
89
101
 
90
102
  let g = state.groups.get(name)
91
103
  if (!g) {
package/src/menu.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx } from '../jsx-runtime.js'
1
+ import { jsx, jsxs } from '../jsx-runtime.js'
2
2
  import { createSignal } from './signal.js'
3
3
  import { useInput, useTheme } from './hooks.js'
4
4
  import { List } from './list.js'
@@ -21,6 +21,7 @@ export function Menu({
21
21
  renderItem,
22
22
  arrow = '›',
23
23
  vimKeys = false,
24
+ counter = false,
24
25
  }) {
25
26
  const { accent = 'cyan', muted = 'gray' } = useTheme()
26
27
  const [internal, setInternal] = createSignal(0)
@@ -77,7 +78,7 @@ export function Menu({
77
78
 
78
79
  const render = renderItem ?? defaultRender
79
80
 
80
- return jsx('box', {
81
+ const list = jsx('box', {
81
82
  style: { height, minHeight: height },
82
83
  children: jsx(List, {
83
84
  items,
@@ -91,4 +92,19 @@ export function Menu({
91
92
  renderItem: (item, ctx) => render(item, { active: ctx.selected, index: ctx.index }),
92
93
  }),
93
94
  })
95
+
96
+ // counter renders only when the list overflows its viewport, so callers
97
+ // can enable it unconditionally without adding noise to short lists
98
+ if (!counter || len <= maxVisible) return list
99
+
100
+ return jsxs('box', {
101
+ style: { flexDirection: 'column' },
102
+ children: [
103
+ list,
104
+ jsx('text', {
105
+ style: { color: muted, dim: true },
106
+ children: `${(selected + 1).toLocaleString()}/${len.toLocaleString()}`,
107
+ }),
108
+ ],
109
+ })
94
110
  }
package/src/pick-list.js CHANGED
@@ -4,7 +4,7 @@ import { useInput, useLayout, useTheme, useCursor } from './hooks.js'
4
4
  import { registerHook } from './renderer.js'
5
5
  import { List } from './list.js'
6
6
 
7
- export function PickList({ items, onSubmit, onCancel, onChange, onCursorChange, focused = true, placeholder = 'search...', filter: filterFn, renderItem, maxVisible = 10, scrollbar = false, scrolloff = 0, itemHeight = 1, itemGap = 0, gap = 0, clearOnSubmit = false, style: userStyle, cursor: cursorProp }) {
7
+ export function PickList({ items, onSubmit, onCancel, onChange, onCursorChange, focused = true, placeholder = 'search...', filter: filterFn, renderItem, maxVisible = 10, scrollbar = false, scrolloff = 0, itemHeight = 1, itemGap = 0, gap = 0, clearOnSubmit = false, counter = false, style: userStyle, cursor: cursorProp }) {
8
8
  const { accent = 'cyan', accentText = 'black', muted = 'gray' } = useTheme()
9
9
  const defaults = {
10
10
  borderColor: accent,
@@ -205,8 +205,17 @@ export function PickList({ items, onSubmit, onCancel, onChange, onCursorChange,
205
205
  renderItem: listRenderItem,
206
206
  })
207
207
 
208
+ // counter renders only when the filtered list overflows its viewport, so
209
+ // callers can enable it unconditionally without adding noise to short lists
210
+ const counterEl = counter && filtered.length > maxVisible
211
+ ? jsx('text', {
212
+ style: { color: muted, dim: true },
213
+ children: `${(filtered.length === 0 ? 0 : cursor + 1).toLocaleString()}/${filtered.length.toLocaleString()}`,
214
+ })
215
+ : null
216
+
208
217
  return jsxs('box', {
209
218
  style: { flexDirection: 'column', flexGrow: 1, gap },
210
- children: [inputEl, list],
219
+ children: counterEl ? [inputEl, list, counterEl] : [inputEl, list],
211
220
  })
212
221
  }
package/src/scroll-box.js CHANGED
@@ -8,12 +8,17 @@ export function ScrollBox({ children, focused = true, scrollOffset: offsetProp,
8
8
  const layout = useLayout()
9
9
 
10
10
  const offset = offsetProp ?? offsetInternal()
11
- const setOffset = onScroll ?? setOffsetInternal
12
11
 
13
12
  const visibleH = layout.height
14
13
  const contentH = layout.contentHeight ?? 0
15
14
  const maxOffset = Math.max(0, contentH - visibleH)
16
15
 
16
+ const setOffset = (v) => {
17
+ const next = Math.max(0, Math.min(maxOffset, v))
18
+ if (onScroll) onScroll(next, { atBottom: next >= maxOffset, maxOffset })
19
+ else setOffsetInternal(next)
20
+ }
21
+
17
22
  const clamp = (v) => Math.max(0, Math.min(maxOffset, v))
18
23
  const clamped = clamp(offset)
19
24