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.
- package/dist/apis/environment.d.ts +1 -0
- package/dist/apis/environment.d.ts.map +1 -1
- package/dist/apis/environment.js +5 -0
- package/dist/apis/environment.js.map +1 -1
- package/dist/app.d.ts +33 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +1130 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.js +80 -0
- package/dist/cli.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -2
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.d.ts +4 -1
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +8 -5
- package/dist/components/actions.js.map +1 -1
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +21 -18
- package/dist/components/detail.js.map +1 -1
- package/dist/components/dropdown.d.ts.map +1 -1
- package/dist/components/dropdown.js +3 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/footer.d.ts +6 -0
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +15 -6
- package/dist/components/footer.js.map +1 -1
- package/dist/components/form/checkbox.d.ts.map +1 -1
- package/dist/components/form/checkbox.js +1 -13
- package/dist/components/form/checkbox.js.map +1 -1
- package/dist/components/form/date-picker.js +2 -2
- package/dist/components/form/date-picker.js.map +1 -1
- package/dist/components/form/description.js +1 -1
- package/dist/components/form/description.js.map +1 -1
- package/dist/components/form/dropdown.d.ts.map +1 -1
- package/dist/components/form/dropdown.js +19 -3
- package/dist/components/form/dropdown.js.map +1 -1
- package/dist/components/form/file-picker.d.ts.map +1 -1
- package/dist/components/form/file-picker.js +22 -4
- package/dist/components/form/file-picker.js.map +1 -1
- package/dist/components/form/index.d.ts +3 -1
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +7 -5
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/password-field.js +3 -3
- package/dist/components/form/password-field.js.map +1 -1
- package/dist/components/form/text-area.d.ts.map +1 -1
- package/dist/components/form/text-area.js +29 -6
- package/dist/components/form/text-area.js.map +1 -1
- package/dist/components/form/text-field.js +3 -3
- package/dist/components/form/text-field.js.map +1 -1
- package/dist/components/graph.d.ts.map +1 -1
- package/dist/components/graph.js +21 -25
- package/dist/components/graph.js.map +1 -1
- package/dist/components/heatmap.d.ts +80 -0
- package/dist/components/heatmap.d.ts.map +1 -0
- package/dist/components/heatmap.js +424 -0
- package/dist/components/heatmap.js.map +1 -0
- package/dist/components/list.d.ts +2 -0
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +91 -58
- package/dist/components/list.js.map +1 -1
- package/dist/components/markdown.d.ts +7 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +19 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/metadata.d.ts.map +1 -1
- package/dist/components/metadata.js +4 -1
- package/dist/components/metadata.js.map +1 -1
- package/dist/components/progress-bar.d.ts +37 -0
- package/dist/components/progress-bar.d.ts.map +1 -0
- package/dist/components/progress-bar.js +34 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/table.d.ts +3 -2
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +78 -63
- package/dist/components/table.js.map +1 -1
- package/dist/diagram-parser.d.ts +17 -3
- package/dist/diagram-parser.d.ts.map +1 -1
- package/dist/diagram-parser.js +17 -3
- package/dist/diagram-parser.js.map +1 -1
- package/dist/examples/list-slot.d.ts +2 -0
- package/dist/examples/list-slot.d.ts.map +1 -0
- package/dist/examples/list-slot.js +14 -0
- package/dist/examples/list-slot.js.map +1 -0
- package/dist/examples/list-with-dropdown.js +2 -4
- package/dist/examples/list-with-dropdown.js.map +1 -1
- package/dist/examples/simple-heatmap.d.ts +2 -0
- package/dist/examples/simple-heatmap.d.ts.map +1 -0
- package/dist/examples/simple-heatmap.js +37 -0
- package/dist/examples/simple-heatmap.js.map +1 -0
- package/dist/examples/simple-progress-bar.d.ts +2 -0
- package/dist/examples/simple-progress-bar.d.ts.map +1 -0
- package/dist/examples/simple-progress-bar.js +36 -0
- package/dist/examples/simple-progress-bar.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/date-picker-widget.d.ts.map +1 -1
- package/dist/internal/date-picker-widget.js +5 -4
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/navigation.d.ts.map +1 -1
- package/dist/internal/navigation.js +7 -2
- package/dist/internal/navigation.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +42 -4
- package/dist/internal/providers.js.map +1 -1
- package/dist/logger.js +6 -1
- package/dist/logger.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +31 -2
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts +1 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +23 -1
- package/dist/theme.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/apis/environment.tsx +6 -0
- package/src/app.tsx +1492 -0
- package/src/assets/default-app-icon.png +0 -0
- package/src/cli.tsx +105 -0
- package/src/compile.tsx +5 -2
- package/src/components/actions.tsx +9 -6
- package/src/components/detail.tsx +33 -23
- package/src/components/dropdown.tsx +3 -2
- package/src/components/footer.tsx +40 -7
- package/src/components/form/checkbox.tsx +2 -17
- package/src/components/form/date-picker.tsx +2 -2
- package/src/components/form/description.tsx +1 -1
- package/src/components/form/dropdown.tsx +22 -3
- package/src/components/form/file-picker.tsx +33 -10
- package/src/components/form/index.tsx +11 -7
- package/src/components/form/password-field.tsx +3 -3
- package/src/components/form/text-area.tsx +31 -6
- package/src/components/form/text-field.tsx +3 -3
- package/src/components/graph.tsx +21 -24
- package/src/components/heatmap.tsx +602 -0
- package/src/components/list.tsx +147 -78
- package/src/components/markdown.tsx +30 -0
- package/src/components/metadata.tsx +9 -2
- package/src/components/progress-bar.tsx +112 -0
- package/src/components/table.tsx +88 -71
- package/src/diagram-parser.tsx +17 -3
- package/src/examples/bar-graph-weekly.vitest.tsx +4 -4
- package/src/examples/detail-metadata-showcase.vitest.tsx +12 -12
- package/src/examples/form-basic.vitest.tsx +117 -16
- package/src/examples/graph-bar-chart.vitest.tsx +7 -7
- package/src/examples/graph-row.vitest.tsx +45 -45
- package/src/examples/graph-styles.vitest.tsx +19 -19
- package/src/examples/internal/descendants-rerender.vitest.tsx +94 -46
- package/src/examples/internal/simple-scrollbox.vitest.tsx +38 -14
- package/src/examples/list-dropdown-default.vitest.tsx +78 -58
- package/src/examples/list-slot.tsx +38 -0
- package/src/examples/list-with-detail.vitest.tsx +8 -8
- package/src/examples/list-with-dropdown.tsx +2 -2
- package/src/examples/list-with-dropdown.vitest.tsx +16 -16
- package/src/examples/list-with-sections.vitest.tsx +45 -32
- package/src/examples/simple-detail-table.vitest.tsx +2 -2
- package/src/examples/simple-file-picker.vitest.tsx +1 -1
- package/src/examples/simple-grid.vitest.tsx +27 -53
- package/src/examples/simple-heatmap.tsx +63 -0
- package/src/examples/simple-heatmap.vitest.tsx +88 -0
- package/src/examples/simple-progress-bar.tsx +82 -0
- package/src/examples/simple-progress-bar.vitest.tsx +72 -0
- package/src/examples/table-edge-cases.vitest.tsx +1 -1
- package/src/index.tsx +19 -0
- package/src/internal/date-picker-widget.tsx +23 -12
- package/src/internal/navigation.tsx +7 -2
- package/src/internal/providers.tsx +48 -3
- package/src/logger.tsx +6 -1
- package/src/state.tsx +38 -2
- package/src/theme.tsx +26 -2
- package/src/utils.tsx +6 -1
package/src/components/list.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BoxRenderable,
|
|
3
|
+
MouseButton,
|
|
3
4
|
ScrollBoxRenderable,
|
|
4
5
|
TextAttributes,
|
|
5
6
|
TextareaRenderable,
|
|
6
7
|
} from '@opentui/core'
|
|
7
|
-
import {
|
|
8
|
+
import type { MouseEvent as OpenTUIMouseEvent } from '@opentui/core'
|
|
9
|
+
import { useKeyboard, flushSync } from '@opentui/react'
|
|
8
10
|
import React, {
|
|
9
11
|
ReactElement,
|
|
10
12
|
ReactNode,
|
|
@@ -21,9 +23,10 @@ import { LoadingBar } from 'termcast/src/components/loading-bar'
|
|
|
21
23
|
import { LoadingText } from 'termcast/src/components/loading-text'
|
|
22
24
|
import { Spinner } from 'termcast/src/components/spinner'
|
|
23
25
|
import { useAnimationTick, TICK_DIVISORS } from 'termcast/src/components/animation-tick'
|
|
24
|
-
import { Footer } from 'termcast/src/components/footer'
|
|
26
|
+
import { Footer, Hoverable } from 'termcast/src/components/footer'
|
|
25
27
|
import { createDescendants } from 'termcast/src/descendants'
|
|
26
28
|
import { useStore } from 'termcast/src/state'
|
|
29
|
+
import { showToast, Toast } from 'termcast/src/apis/toast'
|
|
27
30
|
import { useDialog } from 'termcast/src/internal/dialog'
|
|
28
31
|
import { useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
29
32
|
import { useNavigationPending } from 'termcast/src/internal/navigation'
|
|
@@ -33,8 +36,8 @@ import { ScrollBox } from 'termcast/src/internal/scrollbox'
|
|
|
33
36
|
import { Color, resolveColor } from 'termcast/src/colors'
|
|
34
37
|
import { getIconEmoji, getIconValue } from 'termcast/src/components/icon'
|
|
35
38
|
import { ActionPanel, matchesShortcut } from 'termcast/src/components/actions'
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
39
|
+
import { getInteractiveHoverBackground, useTheme } from 'termcast/src/theme'
|
|
40
|
+
import { Markdown } from 'termcast/src/components/markdown'
|
|
38
41
|
import { CommonProps } from 'termcast/src/utils'
|
|
39
42
|
|
|
40
43
|
export { Color }
|
|
@@ -78,20 +81,32 @@ interface ActionsInterface {
|
|
|
78
81
|
function ListFooter(): any {
|
|
79
82
|
const theme = useTheme()
|
|
80
83
|
const firstActionTitle = useStore((s) => s.firstActionTitle)
|
|
84
|
+
const dropdownFooterLabel = useStore((s) => s.dropdownFooterLabel)
|
|
85
|
+
const dropdownTooltip = useStore((s) => s.dropdownTooltip)
|
|
81
86
|
const hasToast = useStore((s) => s.toast !== null)
|
|
82
87
|
const listContext = useContext(ListContext)
|
|
83
88
|
const isShowingDetail = listContext?.isShowingDetail ?? false
|
|
84
89
|
const hasDropdown = listContext?.hasDropdown ?? false
|
|
90
|
+
const isDropdownOpen = listContext?.isDropdownOpen ?? false
|
|
91
|
+
const openDropdownIfClosed = () => {
|
|
92
|
+
if (!isDropdownOpen) {
|
|
93
|
+
listContext?.openDropdown()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
85
96
|
|
|
86
97
|
const content = hasToast ? null : (
|
|
87
98
|
<box style={{ flexDirection: 'row', gap: 3, flexShrink: 0 }}>
|
|
88
99
|
{firstActionTitle && (
|
|
89
|
-
<
|
|
100
|
+
<Hoverable
|
|
101
|
+
onMouseDown={() => {
|
|
102
|
+
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
90
105
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
91
106
|
↵
|
|
92
107
|
</text>
|
|
93
108
|
<text flexShrink={0} fg={theme.textMuted}>{firstActionTitle.toLowerCase()}</text>
|
|
94
|
-
</
|
|
109
|
+
</Hoverable>
|
|
95
110
|
)}
|
|
96
111
|
<box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
|
|
97
112
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
@@ -99,20 +114,26 @@ function ListFooter(): any {
|
|
|
99
114
|
</text>
|
|
100
115
|
<text flexShrink={0} fg={theme.textMuted}>navigate</text>
|
|
101
116
|
</box>
|
|
117
|
+
<Hoverable
|
|
118
|
+
onMouseDown={() => {
|
|
119
|
+
useStore.setState({ showActionsDialog: true })
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
123
|
+
^k
|
|
124
|
+
</text>
|
|
125
|
+
<text flexShrink={0} fg={theme.textMuted}>actions</text>
|
|
126
|
+
</Hoverable>
|
|
102
127
|
{hasDropdown && (
|
|
103
|
-
<
|
|
128
|
+
<Hoverable onMouseDown={openDropdownIfClosed}>
|
|
104
129
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
105
130
|
^p
|
|
106
131
|
</text>
|
|
107
|
-
<text flexShrink={0} fg={theme.textMuted}>
|
|
108
|
-
|
|
132
|
+
<text flexShrink={0} fg={theme.textMuted}>
|
|
133
|
+
{(dropdownTooltip || dropdownFooterLabel || 'dropdown').toLowerCase()}
|
|
134
|
+
</text>
|
|
135
|
+
</Hoverable>
|
|
109
136
|
)}
|
|
110
|
-
<box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
|
|
111
|
-
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
112
|
-
^k
|
|
113
|
-
</text>
|
|
114
|
-
<text flexShrink={0} fg={theme.textMuted}>actions</text>
|
|
115
|
-
</box>
|
|
116
137
|
</box>
|
|
117
138
|
)
|
|
118
139
|
|
|
@@ -337,6 +358,8 @@ export interface ListProps
|
|
|
337
358
|
children?: ReactNode
|
|
338
359
|
onSelectionChange?: (id: string | null) => void
|
|
339
360
|
searchBarAccessory?: ReactElement<DropdownProps> | null
|
|
361
|
+
/** Custom ReactNode rendered on the right edge of the search bar row, after the dropdown if present. */
|
|
362
|
+
logo?: ReactNode
|
|
340
363
|
searchText?: string
|
|
341
364
|
enableFiltering?: boolean
|
|
342
365
|
searchBarPlaceholder?: string
|
|
@@ -742,7 +765,7 @@ function ListItemRow(props: {
|
|
|
742
765
|
accessories?: ItemAccessory[]
|
|
743
766
|
active?: boolean
|
|
744
767
|
isShowingDetail?: boolean
|
|
745
|
-
onMouseDown?: () => void
|
|
768
|
+
onMouseDown?: (evt: OpenTUIMouseEvent) => void
|
|
746
769
|
index?: number
|
|
747
770
|
ref?: React.Ref<BoxRenderable>
|
|
748
771
|
}) {
|
|
@@ -752,7 +775,23 @@ function ListItemRow(props: {
|
|
|
752
775
|
const accessoryTagWidths = listCtx?.accessoryTagWidths
|
|
753
776
|
const isRelaxed = spacingMode === 'relaxed'
|
|
754
777
|
const { title, subtitle, icon, iconColor, accessories, active, ref } = props
|
|
778
|
+
const hoverBackgroundColor = getInteractiveHoverBackground(theme)
|
|
755
779
|
const [isHovered, setIsHovered] = useState(false)
|
|
780
|
+
const handleMouseMove = () => {
|
|
781
|
+
setIsHovered(true)
|
|
782
|
+
// Select item on hover
|
|
783
|
+
if (!active && props.index !== undefined && props.index !== -1 && listCtx?.setSelectedIndex) {
|
|
784
|
+
listCtx.setSelectedIndex(props.index)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const handleMouseDown = (evt: OpenTUIMouseEvent) => {
|
|
789
|
+
props.onMouseDown?.(evt)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const handleMouseOut = () => {
|
|
793
|
+
setIsHovered(false)
|
|
794
|
+
}
|
|
756
795
|
|
|
757
796
|
const accessoryElements: ReactNode[] = []
|
|
758
797
|
if (accessories) {
|
|
@@ -851,20 +890,16 @@ function ListItemRow(props: {
|
|
|
851
890
|
backgroundColor: active
|
|
852
891
|
? theme.primary
|
|
853
892
|
: isHovered
|
|
854
|
-
?
|
|
893
|
+
? hoverBackgroundColor
|
|
855
894
|
: undefined,
|
|
856
895
|
paddingLeft: 0,
|
|
857
896
|
paddingRight: 1,
|
|
858
897
|
marginBottom: 1,
|
|
859
898
|
}}
|
|
860
899
|
border={false}
|
|
861
|
-
onMouseMove={
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
onMouseOut={() => {
|
|
865
|
-
setIsHovered(false)
|
|
866
|
-
}}
|
|
867
|
-
onMouseDown={props.onMouseDown}
|
|
900
|
+
onMouseMove={handleMouseMove}
|
|
901
|
+
onMouseOut={handleMouseOut}
|
|
902
|
+
onMouseDown={handleMouseDown}
|
|
868
903
|
>
|
|
869
904
|
{/* Line 1: marker + icon + title + accessories */}
|
|
870
905
|
<box style={{ flexDirection: 'row', justifyContent: 'space-between', gap: 1 }}>
|
|
@@ -920,20 +955,16 @@ function ListItemRow(props: {
|
|
|
920
955
|
backgroundColor: active
|
|
921
956
|
? theme.primary
|
|
922
957
|
: isHovered
|
|
923
|
-
?
|
|
958
|
+
? hoverBackgroundColor
|
|
924
959
|
: undefined,
|
|
925
960
|
paddingLeft: 0,
|
|
926
961
|
paddingRight: 1,
|
|
927
962
|
gap: 1,
|
|
928
963
|
}}
|
|
929
964
|
border={false}
|
|
930
|
-
onMouseMove={
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
onMouseOut={() => {
|
|
934
|
-
setIsHovered(false)
|
|
935
|
-
}}
|
|
936
|
-
onMouseDown={props.onMouseDown}
|
|
965
|
+
onMouseMove={handleMouseMove}
|
|
966
|
+
onMouseOut={handleMouseOut}
|
|
967
|
+
onMouseDown={handleMouseDown}
|
|
937
968
|
>
|
|
938
969
|
<box style={{ flexDirection: 'row', flexGrow: 1, flexShrink: 1, overflow: 'hidden', gap: 1 }}>
|
|
939
970
|
<box style={{ flexDirection: 'row', flexShrink: 0 }}>
|
|
@@ -987,6 +1018,7 @@ export const List: ListType = (props) => {
|
|
|
987
1018
|
isShowingDetail,
|
|
988
1019
|
selectedItemId,
|
|
989
1020
|
searchBarAccessory,
|
|
1021
|
+
logo,
|
|
990
1022
|
spacingMode = 'default',
|
|
991
1023
|
accessoryTagsLayout,
|
|
992
1024
|
throttle,
|
|
@@ -1361,9 +1393,9 @@ export const List: ListType = (props) => {
|
|
|
1361
1393
|
.sort((a, b) => a.index - b.index)
|
|
1362
1394
|
const currentItem = items.find((item) => item.index === selectedIndex)
|
|
1363
1395
|
|
|
1364
|
-
// Handle Ctrl+K to show actions dialog via portal
|
|
1396
|
+
// Handle Ctrl+K / Cmd+K to show actions dialog via portal
|
|
1365
1397
|
// Always open — built-in actions (Change Theme, etc.) are always available
|
|
1366
|
-
if (evt.name === 'k' && evt.ctrl) {
|
|
1398
|
+
if (evt.name === 'k' && (evt.ctrl || evt.super)) {
|
|
1367
1399
|
useStore.setState({ showActionsDialog: true })
|
|
1368
1400
|
return
|
|
1369
1401
|
}
|
|
@@ -1410,7 +1442,7 @@ export const List: ListType = (props) => {
|
|
|
1410
1442
|
<box style={{ flexDirection: 'column', flexGrow: 1 }}>
|
|
1411
1443
|
{/* Cannot mount focused actions here - would need to be handled differently */}
|
|
1412
1444
|
|
|
1413
|
-
{navigationTitle && (
|
|
1445
|
+
{(navigationTitle || (logo && searchBarAccessory)) && (
|
|
1414
1446
|
<box
|
|
1415
1447
|
border={false}
|
|
1416
1448
|
style={{
|
|
@@ -1419,12 +1451,25 @@ export const List: ListType = (props) => {
|
|
|
1419
1451
|
paddingLeft: 1,
|
|
1420
1452
|
paddingRight: 1,
|
|
1421
1453
|
overflow: 'hidden',
|
|
1454
|
+
flexDirection: 'row',
|
|
1455
|
+
alignItems: 'center',
|
|
1422
1456
|
}}
|
|
1423
1457
|
>
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1458
|
+
{navigationTitle ? (
|
|
1459
|
+
<box flexGrow={1} flexShrink={1} overflow='hidden'>
|
|
1460
|
+
<LoadingBar
|
|
1461
|
+
title={navigationTitle}
|
|
1462
|
+
isLoading={isLoading || navigationPending}
|
|
1463
|
+
/>
|
|
1464
|
+
</box>
|
|
1465
|
+
) : (
|
|
1466
|
+
<box flexGrow={1} />
|
|
1467
|
+
)}
|
|
1468
|
+
{logo ? (
|
|
1469
|
+
<box flexShrink={0} paddingLeft={1}>
|
|
1470
|
+
{logo}
|
|
1471
|
+
</box>
|
|
1472
|
+
) : null}
|
|
1428
1473
|
</box>
|
|
1429
1474
|
)}
|
|
1430
1475
|
|
|
@@ -1473,6 +1518,11 @@ export const List: ListType = (props) => {
|
|
|
1473
1518
|
/>
|
|
1474
1519
|
</box>
|
|
1475
1520
|
{searchBarAccessory}
|
|
1521
|
+
{!navigationTitle && !searchBarAccessory && logo ? (
|
|
1522
|
+
<box flexShrink={0} paddingLeft={2}>
|
|
1523
|
+
{logo}
|
|
1524
|
+
</box>
|
|
1525
|
+
) : null}
|
|
1476
1526
|
</box>
|
|
1477
1527
|
</box>
|
|
1478
1528
|
|
|
@@ -1649,18 +1699,23 @@ const ListItem: ListItemType = (props) => {
|
|
|
1649
1699
|
// Don't render if not visible
|
|
1650
1700
|
if (!isVisible) return null
|
|
1651
1701
|
|
|
1652
|
-
// Handle mouse click on item
|
|
1653
|
-
|
|
1702
|
+
// Handle mouse click on item — left-click selects and executes first action,
|
|
1703
|
+
// right-click selects and opens the actions dialog.
|
|
1704
|
+
// flushSync ensures React commits the new selectedIndex before Zustand
|
|
1705
|
+
// triggers auto-execute, so ActionPanel picks up the clicked item's actions.
|
|
1706
|
+
const handleMouseDown = (evt: OpenTUIMouseEvent) => {
|
|
1654
1707
|
if (listContext && index !== -1) {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1708
|
+
if (!isActive && listContext.setSelectedIndex) {
|
|
1709
|
+
const setIdx = listContext.setSelectedIndex
|
|
1710
|
+
flushSync(() => {
|
|
1711
|
+
setIdx(index)
|
|
1712
|
+
})
|
|
1713
|
+
}
|
|
1714
|
+
if (evt.button === MouseButton.RIGHT) {
|
|
1715
|
+
// Right-click opens the actions dialog
|
|
1716
|
+
useStore.setState({ showActionsDialog: true })
|
|
1717
|
+
} else if (props.actions) {
|
|
1718
|
+
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
1664
1719
|
}
|
|
1665
1720
|
}
|
|
1666
1721
|
}
|
|
@@ -1706,17 +1761,7 @@ const ListItem: ListItemType = (props) => {
|
|
|
1706
1761
|
)
|
|
1707
1762
|
}
|
|
1708
1763
|
|
|
1709
|
-
// Renders markdown with link URL stripping via renderNode for list detail panel.
|
|
1710
|
-
function ListMarkdownContent({ markdown }: { markdown: string }): any {
|
|
1711
|
-
const renderer = useRenderer()
|
|
1712
|
-
const renderNode = React.useMemo(() => {
|
|
1713
|
-
return createMarkdownRenderNode(renderer)
|
|
1714
|
-
}, [renderer])
|
|
1715
1764
|
|
|
1716
|
-
return (
|
|
1717
|
-
<markdown content={markdown} syntaxStyle={markdownSyntaxStyle} conceal renderNode={renderNode} />
|
|
1718
|
-
)
|
|
1719
|
-
}
|
|
1720
1765
|
|
|
1721
1766
|
const ListItemDetail: ListItemDetailType = (props) => {
|
|
1722
1767
|
const theme = useTheme()
|
|
@@ -1747,7 +1792,7 @@ const ListItemDetail: ListItemDetailType = (props) => {
|
|
|
1747
1792
|
>
|
|
1748
1793
|
<box gap={1} style={{ flexDirection: 'column' }}>
|
|
1749
1794
|
{markdown && markdown.trim().length > 0 && (
|
|
1750
|
-
<
|
|
1795
|
+
<Markdown content={markdown} />
|
|
1751
1796
|
)}
|
|
1752
1797
|
{metadata}
|
|
1753
1798
|
</box>
|
|
@@ -1807,6 +1852,7 @@ ListItem.Detail = ListItemDetail
|
|
|
1807
1852
|
const ListDropdown: ListDropdownType = (props) => {
|
|
1808
1853
|
const theme = useTheme()
|
|
1809
1854
|
const listContext = useContext(ListContext)
|
|
1855
|
+
const hoverBackgroundColor = getInteractiveHoverBackground(theme)
|
|
1810
1856
|
const [isHovered, setIsHovered] = useState(false)
|
|
1811
1857
|
|
|
1812
1858
|
// If not inside a List, just render nothing (for type safety)
|
|
@@ -1815,6 +1861,11 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1815
1861
|
}
|
|
1816
1862
|
|
|
1817
1863
|
const { isDropdownOpen, setIsDropdownOpen } = listContext
|
|
1864
|
+
|
|
1865
|
+
const setDropdownSelection = (props: { value: string; title: string }) => {
|
|
1866
|
+
setDropdownState({ value: props.value, title: props.title })
|
|
1867
|
+
useStore.setState({ dropdownFooterLabel: props.title || 'dropdown' })
|
|
1868
|
+
}
|
|
1818
1869
|
// Store both value and title together
|
|
1819
1870
|
const [dropdownState, setDropdownState] = useState<{
|
|
1820
1871
|
value: string
|
|
@@ -1827,6 +1878,11 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1827
1878
|
const dialog = useDialog()
|
|
1828
1879
|
const inFocus = useIsInFocus()
|
|
1829
1880
|
|
|
1881
|
+
// Store dropdown tooltip in zustand for footer display
|
|
1882
|
+
useLayoutEffect(() => {
|
|
1883
|
+
useStore.setState({ dropdownTooltip: props.tooltip || '' })
|
|
1884
|
+
}, [props.tooltip])
|
|
1885
|
+
|
|
1830
1886
|
// Update value and find its title
|
|
1831
1887
|
useLayoutEffect(() => {
|
|
1832
1888
|
const valueToUse =
|
|
@@ -1840,12 +1896,17 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1840
1896
|
|
|
1841
1897
|
if (items.length > 0) {
|
|
1842
1898
|
const firstItem = items[0].props as DropdownItemDescendant
|
|
1843
|
-
|
|
1899
|
+
setDropdownSelection({ value: firstItem.value, title: firstItem.title })
|
|
1844
1900
|
return
|
|
1845
1901
|
}
|
|
1846
1902
|
}
|
|
1847
1903
|
|
|
1848
|
-
if (!valueToUse)
|
|
1904
|
+
if (!valueToUse) {
|
|
1905
|
+
useStore.setState({
|
|
1906
|
+
dropdownFooterLabel: dropdownState.title || 'dropdown',
|
|
1907
|
+
})
|
|
1908
|
+
return
|
|
1909
|
+
}
|
|
1849
1910
|
|
|
1850
1911
|
// Try to find the title for this value
|
|
1851
1912
|
let title = valueToUse
|
|
@@ -1859,8 +1920,11 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1859
1920
|
|
|
1860
1921
|
// Only update if something changed
|
|
1861
1922
|
if (dropdownState.value !== valueToUse || dropdownState.title !== title) {
|
|
1862
|
-
|
|
1923
|
+
setDropdownSelection({ value: valueToUse, title })
|
|
1924
|
+
return
|
|
1863
1925
|
}
|
|
1926
|
+
|
|
1927
|
+
useStore.setState({ dropdownFooterLabel: title || 'dropdown' })
|
|
1864
1928
|
}, [props.value]) // Run when props.value changes and on mount
|
|
1865
1929
|
|
|
1866
1930
|
const dropdownContextValue = useMemo<DropdownContextValue>(
|
|
@@ -1889,7 +1953,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1889
1953
|
break
|
|
1890
1954
|
}
|
|
1891
1955
|
}
|
|
1892
|
-
|
|
1956
|
+
setDropdownSelection({ value: newValue, title })
|
|
1893
1957
|
setIsDropdownOpen(false)
|
|
1894
1958
|
dialog.clear()
|
|
1895
1959
|
if (props.onChange) {
|
|
@@ -1912,6 +1976,11 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1912
1976
|
|
|
1913
1977
|
// Display the title from our state
|
|
1914
1978
|
const displayValue = dropdownState.title || 'All'
|
|
1979
|
+
const openDropdownIfClosed = () => {
|
|
1980
|
+
if (!isDropdownOpen) {
|
|
1981
|
+
listContext.openDropdown()
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1915
1984
|
|
|
1916
1985
|
return (
|
|
1917
1986
|
<DropdownDescendantsProvider value={descendantsContext}>
|
|
@@ -1926,27 +1995,25 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1926
1995
|
// minWidth: value.length + 4,
|
|
1927
1996
|
flexDirection: 'row',
|
|
1928
1997
|
flexShrink: 0,
|
|
1929
|
-
backgroundColor: isHovered ?
|
|
1998
|
+
backgroundColor: isHovered ? hoverBackgroundColor : undefined,
|
|
1930
1999
|
}}
|
|
1931
2000
|
onMouseMove={() => setIsHovered(true)}
|
|
1932
2001
|
onMouseOut={() => setIsHovered(false)}
|
|
1933
|
-
onMouseDown={
|
|
1934
|
-
// Open dropdown when clicked
|
|
1935
|
-
if (!isDropdownOpen) {
|
|
1936
|
-
listContext.openDropdown()
|
|
1937
|
-
}
|
|
1938
|
-
}}
|
|
2002
|
+
onMouseDown={openDropdownIfClosed}
|
|
1939
2003
|
>
|
|
1940
2004
|
{/*<text >^p </text>*/}
|
|
1941
2005
|
{listContext.isLoading ? (
|
|
1942
|
-
<
|
|
1943
|
-
{
|
|
1944
|
-
|
|
2006
|
+
<box onMouseDown={openDropdownIfClosed}>
|
|
2007
|
+
<LoadingText isLoading color={isHovered ? theme.text : theme.textMuted}>
|
|
2008
|
+
{displayValue || 'Loading...'}
|
|
2009
|
+
</LoadingText>
|
|
2010
|
+
</box>
|
|
1945
2011
|
) : (
|
|
1946
2012
|
<text
|
|
1947
2013
|
flexShrink={0}
|
|
1948
2014
|
fg={isHovered ? theme.text : theme.textMuted}
|
|
1949
2015
|
selectable={false}
|
|
2016
|
+
onMouseDown={openDropdownIfClosed}
|
|
1950
2017
|
>
|
|
1951
2018
|
{displayValue}
|
|
1952
2019
|
</text>
|
|
@@ -1955,6 +2022,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1955
2022
|
flexShrink={0}
|
|
1956
2023
|
fg={isHovered ? theme.text : theme.textMuted}
|
|
1957
2024
|
selectable={false}
|
|
2025
|
+
onMouseDown={openDropdownIfClosed}
|
|
1958
2026
|
>
|
|
1959
2027
|
{' '}
|
|
1960
2028
|
▾
|
|
@@ -1967,6 +2035,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1967
2035
|
|
|
1968
2036
|
ListDropdown.Item = (props) => {
|
|
1969
2037
|
const theme = useTheme()
|
|
2038
|
+
const hoverBackgroundColor = getInteractiveHoverBackground(theme)
|
|
1970
2039
|
const dropdownContext = useContext(DropdownContext)
|
|
1971
2040
|
const [isHovered, setIsHovered] = useState(false)
|
|
1972
2041
|
|
|
@@ -2032,7 +2101,7 @@ ListDropdown.Item = (props) => {
|
|
|
2032
2101
|
backgroundColor: isActive
|
|
2033
2102
|
? theme.primary
|
|
2034
2103
|
: isHovered
|
|
2035
|
-
?
|
|
2104
|
+
? hoverBackgroundColor
|
|
2036
2105
|
: undefined,
|
|
2037
2106
|
paddingLeft: isActive ? 0 : 1,
|
|
2038
2107
|
paddingRight: 1,
|
|
@@ -2177,9 +2246,9 @@ function EmptyViewContent(props: EmptyViewProps): any {
|
|
|
2177
2246
|
useKeyboard((evt) => {
|
|
2178
2247
|
if (!inFocus) return
|
|
2179
2248
|
|
|
2180
|
-
// Handle Ctrl+K to show actions dialog via portal
|
|
2249
|
+
// Handle Ctrl+K / Cmd+K to show actions dialog via portal
|
|
2181
2250
|
// Always open — built-in actions (Change Theme, etc.) are always available
|
|
2182
|
-
if (evt.name === 'k' && evt.ctrl) {
|
|
2251
|
+
if (evt.name === 'k' && (evt.ctrl || evt.super)) {
|
|
2183
2252
|
useStore.setState({ showActionsDialog: true })
|
|
2184
2253
|
return
|
|
2185
2254
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Standalone Markdown component for rendering themed markdown in terminal UI.
|
|
2
|
+
// Wraps opentui's <markdown> element with termcast's custom renderNode hook
|
|
3
|
+
// (link URL stripping, borderless tables, OSC 8 hyperlinks) and automatic
|
|
4
|
+
// theme-aware syntax highlighting. Accepts BoxProps so it can be composed
|
|
5
|
+
// with Row, CalendarHeatmap, Graph, etc.
|
|
6
|
+
|
|
7
|
+
import { useMemo } from 'react'
|
|
8
|
+
import { useRenderer } from '@opentui/react'
|
|
9
|
+
import type { BoxProps } from '@opentui/react'
|
|
10
|
+
import { markdownSyntaxStyle } from 'termcast/src/theme'
|
|
11
|
+
import { createMarkdownRenderNode } from 'termcast/src/markdown-utils'
|
|
12
|
+
|
|
13
|
+
export interface MarkdownProps extends BoxProps {
|
|
14
|
+
content: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Markdown({ content, children, ...boxProps }: MarkdownProps): any {
|
|
18
|
+
const renderer = useRenderer()
|
|
19
|
+
const renderNode = useMemo(() => {
|
|
20
|
+
return createMarkdownRenderNode(renderer)
|
|
21
|
+
}, [renderer])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<box {...boxProps}>
|
|
25
|
+
<markdown content={content} syntaxStyle={markdownSyntaxStyle} conceal renderNode={renderNode} />
|
|
26
|
+
</box>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { Markdown }
|
|
@@ -13,6 +13,7 @@ import { TextAttributes } from '@opentui/core'
|
|
|
13
13
|
import { useTheme } from 'termcast/src/theme'
|
|
14
14
|
import { Color, resolveColor } from 'termcast/src/colors'
|
|
15
15
|
import type { ImageLike } from 'termcast/src/components/image'
|
|
16
|
+
import { openInBrowser } from 'termcast/src/action-utils'
|
|
16
17
|
|
|
17
18
|
interface MetadataConfig {
|
|
18
19
|
/**
|
|
@@ -174,8 +175,14 @@ const MetadataLink = (props: LinkProps): any => {
|
|
|
174
175
|
<text flexShrink={0} fg={theme.textMuted} style={{ minWidth: config.titleMinWidth }}>
|
|
175
176
|
{props.title}:{' '}
|
|
176
177
|
</text>
|
|
177
|
-
<text
|
|
178
|
-
{
|
|
178
|
+
<text
|
|
179
|
+
fg={theme.accent}
|
|
180
|
+
attributes={TextAttributes.UNDERLINE}
|
|
181
|
+
onMouseDown={() => {
|
|
182
|
+
void openInBrowser(props.target)
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<a href={props.target}>{props.text}</a>
|
|
179
186
|
</text>
|
|
180
187
|
</box>
|
|
181
188
|
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBar component for rendering usage/progress rows in terminal UIs.
|
|
3
|
+
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* - title row
|
|
6
|
+
* - bar + percentage row (bar grows to fill available width)
|
|
7
|
+
* - optional muted label row
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import { TextAttributes } from '@opentui/core'
|
|
12
|
+
import type { BoxProps } from '@opentui/react'
|
|
13
|
+
import { useTheme } from 'termcast/src/theme'
|
|
14
|
+
import { Color, resolveColor } from 'termcast/src/colors'
|
|
15
|
+
|
|
16
|
+
export interface ProgressBarProps extends BoxProps {
|
|
17
|
+
/** Main title shown above the bar */
|
|
18
|
+
title: string
|
|
19
|
+
/** Current progress value */
|
|
20
|
+
value: number
|
|
21
|
+
/** Maximum value used to calculate percentage (default: 100) */
|
|
22
|
+
maxValue?: number
|
|
23
|
+
/** Optional muted label below the bar */
|
|
24
|
+
label?: string
|
|
25
|
+
/** Show percentage text at end of bar row (default: true) */
|
|
26
|
+
showPercentage?: boolean
|
|
27
|
+
/** Optional suffix after percentage text (e.g. "used") */
|
|
28
|
+
percentageSuffix?: string
|
|
29
|
+
/** Optional color override for filled segment */
|
|
30
|
+
color?: Color.ColorLike
|
|
31
|
+
/** Optional color override for track segment */
|
|
32
|
+
trackColor?: Color.ColorLike
|
|
33
|
+
/** Optional formatter for percentage value */
|
|
34
|
+
formatPercentage?: (percentage: number) => string
|
|
35
|
+
/** Character used for the filled portion of the bar (default: '█') */
|
|
36
|
+
barCharacter?: string
|
|
37
|
+
/** Character used for the empty/track portion of the bar (default: '░') */
|
|
38
|
+
trackCharacter?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clamp({ value, min, max }: { value: number; min: number; max: number }): number {
|
|
42
|
+
if (value < min) {
|
|
43
|
+
return min
|
|
44
|
+
}
|
|
45
|
+
if (value > max) {
|
|
46
|
+
return max
|
|
47
|
+
}
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ProgressBar(props: ProgressBarProps): any {
|
|
52
|
+
const theme = useTheme()
|
|
53
|
+
const {
|
|
54
|
+
title,
|
|
55
|
+
value,
|
|
56
|
+
maxValue = 100,
|
|
57
|
+
label,
|
|
58
|
+
showPercentage = true,
|
|
59
|
+
percentageSuffix,
|
|
60
|
+
color,
|
|
61
|
+
trackColor,
|
|
62
|
+
formatPercentage,
|
|
63
|
+
barCharacter = '█',
|
|
64
|
+
trackCharacter = '░',
|
|
65
|
+
...rest
|
|
66
|
+
} = props
|
|
67
|
+
|
|
68
|
+
const safeMaxValue = Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 1
|
|
69
|
+
const safeValue = Number.isFinite(value) ? value : 0
|
|
70
|
+
const rawPercentage = (safeValue / safeMaxValue) * 100
|
|
71
|
+
const clampedPercentage = clamp({ value: rawPercentage, min: 0, max: 100 })
|
|
72
|
+
|
|
73
|
+
const filledGrow = clampedPercentage
|
|
74
|
+
const trackGrow = 100 - clampedPercentage
|
|
75
|
+
|
|
76
|
+
const filledColor = resolveColor(color) || theme.accent
|
|
77
|
+
const resolvedTrackColor = resolveColor(trackColor) || theme.conceal
|
|
78
|
+
|
|
79
|
+
const formattedPercentage = formatPercentage
|
|
80
|
+
? formatPercentage(clampedPercentage)
|
|
81
|
+
: `${Math.round(clampedPercentage)}%`
|
|
82
|
+
|
|
83
|
+
const percentageText = percentageSuffix
|
|
84
|
+
? `${formattedPercentage} ${percentageSuffix}`
|
|
85
|
+
: formattedPercentage
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<box flexDirection="column" width="100%" {...rest}>
|
|
89
|
+
<text fg={theme.text} attributes={TextAttributes.BOLD}>{title}</text>
|
|
90
|
+
<box flexDirection="row" alignItems="center" width="100%" gap={1}>
|
|
91
|
+
<box flexDirection="row" flexGrow={1} overflow="hidden">
|
|
92
|
+
{filledGrow > 0 && (
|
|
93
|
+
<box flexGrow={filledGrow} flexBasis={0} flexShrink={1} overflow="hidden">
|
|
94
|
+
<text fg={filledColor} wrapMode="none">{barCharacter.repeat(300)}</text>
|
|
95
|
+
</box>
|
|
96
|
+
)}
|
|
97
|
+
{trackGrow > 0 && (
|
|
98
|
+
<box flexGrow={trackGrow} flexBasis={0} flexShrink={1} overflow="hidden">
|
|
99
|
+
<text fg={resolvedTrackColor} wrapMode="none">{trackCharacter.repeat(300)}</text>
|
|
100
|
+
</box>
|
|
101
|
+
)}
|
|
102
|
+
</box>
|
|
103
|
+
{showPercentage && (
|
|
104
|
+
<text fg={theme.text} wrapMode="none" flexShrink={0}>{percentageText}</text>
|
|
105
|
+
)}
|
|
106
|
+
</box>
|
|
107
|
+
{label && <text fg={theme.textMuted}>{label}</text>}
|
|
108
|
+
</box>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { ProgressBar }
|