@trendr/core 0.4.1 → 0.4.4

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.1",
3
+ "version": "0.4.4",
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/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 }) {
127
+ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder, focused = true, maxHeight = 10, clearOnSubmit = true, cursor: cursorProp, value: valueProp, submitOnEnter = false, color }) {
128
128
  const [value, setValue] = createSignal('')
129
129
  const [cursor, setCursor] = createSignal(0)
130
130
  if (valueProp !== undefined && valueProp !== value()) {
@@ -345,7 +345,7 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
345
345
  const hasCursor = focused && row === displayPos.row
346
346
 
347
347
  if (!hasCursor) {
348
- return jsx('text', { key: row, children: content || ' ' })
348
+ return jsx('text', { key: row, style: color ? { color } : {}, children: content || ' ' })
349
349
  }
350
350
 
351
351
  const cursorIdx = Math.max(line.start, Math.min(c, line.end))
@@ -358,9 +358,9 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
358
358
  key: row,
359
359
  style: { flexDirection: 'row', height: 1 },
360
360
  children: [
361
- before && jsx('text', { children: before }),
361
+ before && jsx('text', { style: color ? { color } : {}, children: before }),
362
362
  jsx('text', { style: cs ?? {}, children: cursorChar }),
363
- after && jsx('text', { children: after }),
363
+ after && jsx('text', { style: color ? { color } : {}, children: after }),
364
364
  ],
365
365
  })
366
366
  })