termcast 1.3.50 → 1.3.52

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.
Files changed (178) hide show
  1. package/dist/apis/environment.d.ts +1 -0
  2. package/dist/apis/environment.d.ts.map +1 -1
  3. package/dist/apis/environment.js +5 -0
  4. package/dist/apis/environment.js.map +1 -1
  5. package/dist/app.d.ts +33 -0
  6. package/dist/app.d.ts.map +1 -0
  7. package/dist/app.js +1130 -0
  8. package/dist/app.js.map +1 -0
  9. package/dist/cli.js +80 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/compile.d.ts.map +1 -1
  12. package/dist/compile.js +5 -2
  13. package/dist/compile.js.map +1 -1
  14. package/dist/components/actions.d.ts +4 -1
  15. package/dist/components/actions.d.ts.map +1 -1
  16. package/dist/components/actions.js +8 -5
  17. package/dist/components/actions.js.map +1 -1
  18. package/dist/components/detail.d.ts.map +1 -1
  19. package/dist/components/detail.js +21 -18
  20. package/dist/components/detail.js.map +1 -1
  21. package/dist/components/dropdown.d.ts.map +1 -1
  22. package/dist/components/dropdown.js +3 -2
  23. package/dist/components/dropdown.js.map +1 -1
  24. package/dist/components/footer.d.ts +6 -0
  25. package/dist/components/footer.d.ts.map +1 -1
  26. package/dist/components/footer.js +15 -6
  27. package/dist/components/footer.js.map +1 -1
  28. package/dist/components/form/checkbox.d.ts.map +1 -1
  29. package/dist/components/form/checkbox.js +1 -13
  30. package/dist/components/form/checkbox.js.map +1 -1
  31. package/dist/components/form/date-picker.js +2 -2
  32. package/dist/components/form/date-picker.js.map +1 -1
  33. package/dist/components/form/description.js +1 -1
  34. package/dist/components/form/description.js.map +1 -1
  35. package/dist/components/form/dropdown.d.ts.map +1 -1
  36. package/dist/components/form/dropdown.js +19 -3
  37. package/dist/components/form/dropdown.js.map +1 -1
  38. package/dist/components/form/file-picker.d.ts.map +1 -1
  39. package/dist/components/form/file-picker.js +22 -4
  40. package/dist/components/form/file-picker.js.map +1 -1
  41. package/dist/components/form/index.d.ts +3 -1
  42. package/dist/components/form/index.d.ts.map +1 -1
  43. package/dist/components/form/index.js +7 -5
  44. package/dist/components/form/index.js.map +1 -1
  45. package/dist/components/form/password-field.js +3 -3
  46. package/dist/components/form/password-field.js.map +1 -1
  47. package/dist/components/form/text-area.d.ts.map +1 -1
  48. package/dist/components/form/text-area.js +29 -6
  49. package/dist/components/form/text-area.js.map +1 -1
  50. package/dist/components/form/text-field.js +3 -3
  51. package/dist/components/form/text-field.js.map +1 -1
  52. package/dist/components/graph.d.ts.map +1 -1
  53. package/dist/components/graph.js +21 -25
  54. package/dist/components/graph.js.map +1 -1
  55. package/dist/components/heatmap.d.ts +80 -0
  56. package/dist/components/heatmap.d.ts.map +1 -0
  57. package/dist/components/heatmap.js +424 -0
  58. package/dist/components/heatmap.js.map +1 -0
  59. package/dist/components/list.d.ts +2 -0
  60. package/dist/components/list.d.ts.map +1 -1
  61. package/dist/components/list.js +91 -58
  62. package/dist/components/list.js.map +1 -1
  63. package/dist/components/markdown.d.ts +7 -0
  64. package/dist/components/markdown.d.ts.map +1 -0
  65. package/dist/components/markdown.js +19 -0
  66. package/dist/components/markdown.js.map +1 -0
  67. package/dist/components/metadata.d.ts.map +1 -1
  68. package/dist/components/metadata.js +4 -1
  69. package/dist/components/metadata.js.map +1 -1
  70. package/dist/components/progress-bar.d.ts +37 -0
  71. package/dist/components/progress-bar.d.ts.map +1 -0
  72. package/dist/components/progress-bar.js +34 -0
  73. package/dist/components/progress-bar.js.map +1 -0
  74. package/dist/components/table.d.ts +3 -2
  75. package/dist/components/table.d.ts.map +1 -1
  76. package/dist/components/table.js +78 -63
  77. package/dist/components/table.js.map +1 -1
  78. package/dist/diagram-parser.d.ts +17 -3
  79. package/dist/diagram-parser.d.ts.map +1 -1
  80. package/dist/diagram-parser.js +17 -3
  81. package/dist/diagram-parser.js.map +1 -1
  82. package/dist/examples/list-slot.d.ts +2 -0
  83. package/dist/examples/list-slot.d.ts.map +1 -0
  84. package/dist/examples/list-slot.js +14 -0
  85. package/dist/examples/list-slot.js.map +1 -0
  86. package/dist/examples/list-with-dropdown.js +2 -4
  87. package/dist/examples/list-with-dropdown.js.map +1 -1
  88. package/dist/examples/simple-heatmap.d.ts +2 -0
  89. package/dist/examples/simple-heatmap.d.ts.map +1 -0
  90. package/dist/examples/simple-heatmap.js +37 -0
  91. package/dist/examples/simple-heatmap.js.map +1 -0
  92. package/dist/examples/simple-progress-bar.d.ts +2 -0
  93. package/dist/examples/simple-progress-bar.d.ts.map +1 -0
  94. package/dist/examples/simple-progress-bar.js +36 -0
  95. package/dist/examples/simple-progress-bar.js.map +1 -0
  96. package/dist/index.d.ts +6 -0
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +6 -0
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  101. package/dist/internal/date-picker-widget.js +5 -4
  102. package/dist/internal/date-picker-widget.js.map +1 -1
  103. package/dist/internal/navigation.d.ts.map +1 -1
  104. package/dist/internal/navigation.js +7 -2
  105. package/dist/internal/navigation.js.map +1 -1
  106. package/dist/internal/providers.d.ts.map +1 -1
  107. package/dist/internal/providers.js +42 -4
  108. package/dist/internal/providers.js.map +1 -1
  109. package/dist/logger.js +6 -1
  110. package/dist/logger.js.map +1 -1
  111. package/dist/state.d.ts +2 -0
  112. package/dist/state.d.ts.map +1 -1
  113. package/dist/state.js +31 -2
  114. package/dist/state.js.map +1 -1
  115. package/dist/theme.d.ts +1 -0
  116. package/dist/theme.d.ts.map +1 -1
  117. package/dist/theme.js +23 -1
  118. package/dist/theme.js.map +1 -1
  119. package/dist/utils.d.ts.map +1 -1
  120. package/dist/utils.js +6 -1
  121. package/dist/utils.js.map +1 -1
  122. package/package.json +3 -3
  123. package/src/apis/environment.tsx +6 -0
  124. package/src/app.tsx +1492 -0
  125. package/src/assets/default-app-icon.png +0 -0
  126. package/src/cli.tsx +105 -0
  127. package/src/compile.tsx +5 -2
  128. package/src/components/actions.tsx +9 -6
  129. package/src/components/detail.tsx +33 -23
  130. package/src/components/dropdown.tsx +3 -2
  131. package/src/components/footer.tsx +40 -7
  132. package/src/components/form/checkbox.tsx +2 -17
  133. package/src/components/form/date-picker.tsx +2 -2
  134. package/src/components/form/description.tsx +1 -1
  135. package/src/components/form/dropdown.tsx +22 -3
  136. package/src/components/form/file-picker.tsx +33 -10
  137. package/src/components/form/index.tsx +11 -7
  138. package/src/components/form/password-field.tsx +3 -3
  139. package/src/components/form/text-area.tsx +31 -6
  140. package/src/components/form/text-field.tsx +3 -3
  141. package/src/components/graph.tsx +21 -24
  142. package/src/components/heatmap.tsx +602 -0
  143. package/src/components/list.tsx +147 -78
  144. package/src/components/markdown.tsx +30 -0
  145. package/src/components/metadata.tsx +9 -2
  146. package/src/components/progress-bar.tsx +112 -0
  147. package/src/components/table.tsx +88 -71
  148. package/src/diagram-parser.tsx +17 -3
  149. package/src/examples/bar-graph-weekly.vitest.tsx +4 -4
  150. package/src/examples/detail-metadata-showcase.vitest.tsx +12 -12
  151. package/src/examples/form-basic.vitest.tsx +117 -16
  152. package/src/examples/graph-bar-chart.vitest.tsx +7 -7
  153. package/src/examples/graph-row.vitest.tsx +45 -45
  154. package/src/examples/graph-styles.vitest.tsx +19 -19
  155. package/src/examples/internal/descendants-rerender.vitest.tsx +94 -46
  156. package/src/examples/internal/simple-scrollbox.vitest.tsx +38 -14
  157. package/src/examples/list-dropdown-default.vitest.tsx +78 -58
  158. package/src/examples/list-slot.tsx +38 -0
  159. package/src/examples/list-with-detail.vitest.tsx +8 -8
  160. package/src/examples/list-with-dropdown.tsx +2 -2
  161. package/src/examples/list-with-dropdown.vitest.tsx +16 -16
  162. package/src/examples/list-with-sections.vitest.tsx +45 -32
  163. package/src/examples/simple-detail-table.vitest.tsx +2 -2
  164. package/src/examples/simple-file-picker.vitest.tsx +1 -1
  165. package/src/examples/simple-grid.vitest.tsx +27 -53
  166. package/src/examples/simple-heatmap.tsx +63 -0
  167. package/src/examples/simple-heatmap.vitest.tsx +88 -0
  168. package/src/examples/simple-progress-bar.tsx +82 -0
  169. package/src/examples/simple-progress-bar.vitest.tsx +72 -0
  170. package/src/examples/table-edge-cases.vitest.tsx +1 -1
  171. package/src/index.tsx +19 -0
  172. package/src/internal/date-picker-widget.tsx +23 -12
  173. package/src/internal/navigation.tsx +7 -2
  174. package/src/internal/providers.tsx +48 -3
  175. package/src/logger.tsx +6 -1
  176. package/src/state.tsx +38 -2
  177. package/src/theme.tsx +26 -2
  178. package/src/utils.tsx +6 -1
@@ -15,7 +15,7 @@ import { useDialog } from 'termcast/src/internal/dialog'
15
15
  import { Offscreen } from 'termcast/src/internal/offscreen'
16
16
  import { useTheme } from 'termcast/src/theme'
17
17
  import { useStore } from 'termcast/src/state'
18
- import { Footer } from 'termcast/src/components/footer'
18
+ import { Footer, Hoverable } from 'termcast/src/components/footer'
19
19
  import {
20
20
  TextAttributes,
21
21
  ScrollBoxRenderable,
@@ -77,7 +77,7 @@ export const useFormScrollContext = () => {
77
77
  // Context for managing focused field and loading state
78
78
  interface FocusContextValue {
79
79
  focusedField: string | null
80
- setFocusedField: (id: string | null) => void
80
+ setFocusedField: (id: string | null, opts?: { skipScroll?: boolean }) => void
81
81
  isLoading: boolean
82
82
  }
83
83
 
@@ -121,12 +121,16 @@ function FormFooter(): any {
121
121
  </text>
122
122
  <text flexShrink={0} fg={theme.textMuted}>navigate</text>
123
123
  </box>
124
- <box style={{ flexDirection: 'row', gap: 1 }}>
124
+ <Hoverable
125
+ onMouseDown={() => {
126
+ useStore.setState({ showActionsDialog: true })
127
+ }}
128
+ >
125
129
  <text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
126
130
  ^k
127
131
  </text>
128
132
  <text flexShrink={0} fg={theme.textMuted}>actions</text>
129
- </box>
133
+ </Hoverable>
130
134
  </box>
131
135
  )
132
136
 
@@ -221,11 +225,11 @@ export const Form: FormType = ((props) => {
221
225
  scrollBox.scrollTo(Math.max(0, targetScrollTop))
222
226
  }
223
227
 
224
- const setFocusedField = (id: string | null) => {
228
+ const setFocusedField = (id: string | null, opts?: { skipScroll?: boolean }) => {
225
229
  flushSync(() => {
226
230
  setFocusedFieldRaw(id)
227
231
  })
228
- if (id) {
232
+ if (id && !opts?.skipScroll) {
229
233
  scrollToField(id)
230
234
  }
231
235
  }
@@ -330,7 +334,7 @@ export const Form: FormType = ((props) => {
330
334
  return
331
335
  }
332
336
 
333
- if (evt.name === 'k' && evt.ctrl) {
337
+ if (evt.name === 'k' && (evt.ctrl || evt.super)) {
334
338
  // Always open — built-in actions (Change Theme, etc.) are always available
335
339
  useStore.setState({ showActionsDialog: true })
336
340
  } else if ((evt.name === 'return' && evt.ctrl) || (evt.name === 'return' && evt.meta)) {
@@ -40,12 +40,12 @@ export const PasswordField = (props: PasswordFieldProps): any => {
40
40
  const displayValue = '*'.repeat(displayLength)
41
41
 
42
42
  return (
43
- <box ref={elementRef} flexDirection="column">
43
+ <box ref={elementRef} flexDirection="column" onMouseDown={() => { setFocusedField(props.id, { skipScroll: true }) }}>
44
44
  <WithLeftBorder isFocused={isFocused} paddingBottom={1}>
45
45
  <TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
46
46
  <box
47
47
  onMouseDown={() => {
48
- setFocusedField(props.id)
48
+ setFocusedField(props.id, { skipScroll: true })
49
49
  }}
50
50
  >
51
51
  <LoadingText
@@ -83,7 +83,7 @@ export const PasswordField = (props: PasswordFieldProps): any => {
83
83
  placeholder={props.placeholder}
84
84
  focused={isFocused}
85
85
  onMouseDown={() => {
86
- setFocusedField(props.id)
86
+ setFocusedField(props.id, { skipScroll: true })
87
87
  }}
88
88
  />
89
89
  {(fieldState.error || props.error || props.info) && <box height={1} />}
@@ -1,12 +1,14 @@
1
1
  import React, { useRef, useCallback } from 'react'
2
2
  import { BoxRenderable, TextareaRenderable } from '@opentui/core'
3
+ import { useKeyboard } from '@opentui/react'
3
4
  import { useFormContext } from 'react-hook-form'
4
5
  import { useFocusContext, useFormFieldDescendant } from './index'
5
6
  import { FormItemProps, FormItemRef } from './types'
6
7
  import { useTheme } from 'termcast/src/theme'
7
8
  import { WithLeftBorder, TitleIndicator } from './with-left-border'
8
- import { useFormNavigation } from './use-form-navigation'
9
+ import { useFormNavigation, useFormNavigationHelpers } from './use-form-navigation'
9
10
  import { createTextareaFormRef } from './form-ref'
11
+ import { useIsInFocus } from 'termcast/src/internal/focus-context'
10
12
  import { LoadingText } from 'termcast/src/components/loading-text'
11
13
 
12
14
  export interface TextAreaProps extends FormItemProps<string> {
@@ -21,7 +23,9 @@ export const TextArea = (props: TextAreaProps): any => {
21
23
  const { register, formState } = useFormContext()
22
24
  const focusContext = useFocusContext()
23
25
  const { focusedField, setFocusedField } = focusContext
26
+ const isInFocus = useIsInFocus()
24
27
  const isFocused = focusedField === props.id
28
+ const { navigateToPrevious, navigateToNext } = useFormNavigationHelpers(props.id)
25
29
 
26
30
  const elementRef = useRef<BoxRenderable>(null)
27
31
  const textareaRef = useRef<TextareaRenderable>(null)
@@ -32,8 +36,29 @@ export const TextArea = (props: TextAreaProps): any => {
32
36
  elementRef: elementRef.current,
33
37
  })
34
38
 
35
- // TODO in textarea arrows should probably go to lines instead of other forms
36
- useFormNavigation(props.id)
39
+ useFormNavigation(props.id, { handleArrows: false })
40
+
41
+ useKeyboard((evt) => {
42
+ if (!isFocused || !isInFocus) return
43
+ if (evt.name !== 'up' && evt.name !== 'down') return
44
+
45
+ const textarea = textareaRef.current
46
+ if (!textarea) return
47
+
48
+ const cursorLine = textarea.logicalCursor.row
49
+ const lastLine = textarea.lineCount - 1
50
+
51
+ if (evt.name === 'up' && cursorLine <= 0) {
52
+ navigateToPrevious()
53
+ evt.stopPropagation()
54
+ return
55
+ }
56
+
57
+ if (evt.name === 'down' && cursorLine >= lastLine) {
58
+ navigateToNext()
59
+ evt.stopPropagation()
60
+ }
61
+ })
37
62
 
38
63
  // Get register props
39
64
  const registration = register(props.id)
@@ -60,12 +85,12 @@ export const TextArea = (props: TextAreaProps): any => {
60
85
  const fieldError = formState.errors[props.id]
61
86
 
62
87
  return (
63
- <box ref={elementRef} flexDirection="column">
88
+ <box ref={elementRef} flexDirection="column" onMouseDown={() => { setFocusedField(props.id, { skipScroll: true }) }}>
64
89
  <WithLeftBorder isFocused={isFocused} paddingBottom={1}>
65
90
  <TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
66
91
  <box
67
92
  onMouseDown={() => {
68
- setFocusedField(props.id)
93
+ setFocusedField(props.id, { skipScroll: true })
69
94
  }}
70
95
  >
71
96
  <LoadingText
@@ -86,7 +111,7 @@ export const TextArea = (props: TextAreaProps): any => {
86
111
  placeholder={props.placeholder}
87
112
  focused={isFocused}
88
113
  onMouseDown={() => {
89
- setFocusedField(props.id)
114
+ setFocusedField(props.id, { skipScroll: true })
90
115
  }}
91
116
  />
92
117
  </box>
@@ -61,12 +61,12 @@ export const TextField = (props: TextFieldProps): any => {
61
61
  const fieldError = formState.errors[props.id]
62
62
 
63
63
  return (
64
- <box ref={elementRef} flexDirection="column">
64
+ <box ref={elementRef} flexDirection="column" onMouseDown={() => { setFocusedField(props.id, { skipScroll: true }) }}>
65
65
  <WithLeftBorder isFocused={isFocused} paddingBottom={1}>
66
66
  <TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
67
67
  <box
68
68
  onMouseDown={() => {
69
- setFocusedField(props.id)
69
+ setFocusedField(props.id, { skipScroll: true })
70
70
  }}
71
71
  >
72
72
  <LoadingText
@@ -90,7 +90,7 @@ export const TextField = (props: TextFieldProps): any => {
90
90
  placeholder={props.placeholder}
91
91
  focused={isFocused}
92
92
  onMouseDown={() => {
93
- setFocusedField(props.id)
93
+ setFocusedField(props.id, { skipScroll: true })
94
94
  }}
95
95
  />
96
96
  {(fieldError || props.error || props.info) && <box height={1} />}
@@ -38,14 +38,15 @@ import { Color, resolveColor } from 'termcast/src/colors'
38
38
  export type GraphVariant = 'area' | 'filled' | 'striped'
39
39
 
40
40
  // ── Block characters for Filled/Striped modes ───────────────────────
41
- // We use ▀/▄ with fg+bg color encoding to eliminate the tiny gaps
42
- // some terminals show between adjacent █ rows.
43
- // = top half drawn with fg, bottom half shows bg
44
- // = bottom half drawn with fg, top half shows bg
45
- // = lower 1/8 block, used as a thin baseline for zero/minimum values
46
- const UPPER_HALF = '▀' // U+2580
47
- const LOWER_HALF = '' // U+2584
48
- const LOWER_EIGHTH = '' // U+2581
41
+ // We use left-half and quadrant characters so each bar column is 50%
42
+ // cell width, creating visible vertical gaps between adjacent bars.
43
+ // = left half block (full height, left 50%)
44
+ // = quadrant upper left (top half height, left 50%)
45
+ // = quadrant lower left (bottom half height, left 50%)
46
+ // All three align at left 50%, maintaining 2x vertical sub-row resolution.
47
+ const LEFT_HALF = '' // U+258C
48
+ const QUAD_UL = '' // U+2598
49
+ const QUAD_LL = '▖' // U+2596
49
50
 
50
51
  // ── Braille bit map ──────────────────────────────────────────────────
51
52
  // Maps (subCol, subRow) to the braille bit for that dot position.
@@ -287,8 +288,8 @@ export class GraphPlotRenderable extends Renderable {
287
288
  }
288
289
 
289
290
  // ── Style: Filled / Striped (block characters) ───────────────
290
- // Always uses ▀ (upper-half block) with fg=top color, bg=bottom color.
291
- // This eliminates the tiny gaps some terminals show between adjacent rows.
291
+ // Uses left-half and quadrant characters for thin bars with visible gaps.
292
+ // (left half) for full rows, ▘/▖ (quadrants) for top/bottom edge rows.
292
293
  // Filled: every column uses the series color.
293
294
  // Striped: all columns filled, even cols = stripeColor1, odd = stripeColor2.
294
295
  // Pass a transparent color to skip those columns (gap-style bars).
@@ -323,10 +324,10 @@ export class GraphPlotRenderable extends Renderable {
323
324
  const topVRow = lineY[col]!
324
325
  if (topVRow >= virtualH) continue
325
326
 
326
- // Fill from topVRow down to virtualH-1 using ▀/▄ with fg+bg encoding.
327
- // ▀: fg paints top half, bg paints bottom half.
328
- // ▄: fg paints bottom half, bg paints top half.
329
- // We never set fg=transparent on a visible glyph part (would show as black).
327
+ // Fill from topVRow down using left-half/quadrant chars.
328
+ // ▌: full height left-half (both sub-rows filled)
329
+ // ▘: upper-left quadrant (top sub-row only)
330
+ // ▖: lower-left quadrant (bottom sub-row only)
330
331
  for (let row = 0; row < plotH; row++) {
331
332
  const vTop = row * 2 // virtual row for top half
332
333
  const vBot = row * 2 + 1 // virtual row for bottom half
@@ -336,18 +337,14 @@ export class GraphPlotRenderable extends Renderable {
336
337
  if (!topFilled && !botFilled) continue
337
338
 
338
339
  if (topFilled && botFilled) {
339
- // Both halves: with fg=color, bg=color → seamless full block
340
- buffer.setCell(plotX + col, plotY + row, UPPER_HALF, fillColor, fillColor)
340
+ // Both halves: left half block
341
+ buffer.setCell(plotX + col, plotY + row, LEFT_HALF, fillColor, transparent)
341
342
  } else if (topFilled) {
342
- // Top only: with fg=color, bg=transparent
343
- buffer.setCell(plotX + col, plotY + row, UPPER_HALF, fillColor, transparent)
344
- } else if (topVRow >= virtualH - 1) {
345
- // Minimum fill: only the very last virtual row is filled (zero/min value).
346
- // Use ▁ (lower 1/8 block) for a thin baseline indicator.
347
- buffer.setCell(plotX + col, plotY + row, LOWER_EIGHTH, fillColor, transparent)
343
+ // Top only: quadrant upper left
344
+ buffer.setCell(plotX + col, plotY + row, QUAD_UL, fillColor, transparent)
348
345
  } else {
349
- // Bottom only: with fg=color, bg=transparent
350
- buffer.setCell(plotX + col, plotY + row, LOWER_HALF, fillColor, transparent)
346
+ // Bottom only: quadrant lower left
347
+ buffer.setCell(plotX + col, plotY + row, QUAD_LL, fillColor, transparent)
351
348
  }
352
349
  }
353
350
  }