termcast 1.3.50 → 1.3.51
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 +1125 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.js +80 -0
- package/dist/cli.js.map +1 -1
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +20 -17
- 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 +6 -4
- 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/heatmap.d.ts +80 -0
- package/dist/components/heatmap.d.ts.map +1 -0
- package/dist/components/heatmap.js +405 -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 +80 -52
- 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 +1487 -0
- package/src/assets/default-app-icon.png +0 -0
- package/src/cli.tsx +105 -0
- package/src/components/detail.tsx +32 -22
- package/src/components/dropdown.tsx +3 -2
- package/src/components/footer.tsx +37 -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 +10 -6
- 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/heatmap.tsx +584 -0
- package/src/components/list.tsx +135 -72
- 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 +2 -2
- package/src/examples/graph-row.vitest.tsx +10 -10
- 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
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
TextAttributes,
|
|
5
5
|
TextareaRenderable,
|
|
6
6
|
} from '@opentui/core'
|
|
7
|
-
import { useKeyboard, flushSync
|
|
7
|
+
import { useKeyboard, flushSync } from '@opentui/react'
|
|
8
8
|
import React, {
|
|
9
9
|
ReactElement,
|
|
10
10
|
ReactNode,
|
|
@@ -21,9 +21,10 @@ import { LoadingBar } from 'termcast/src/components/loading-bar'
|
|
|
21
21
|
import { LoadingText } from 'termcast/src/components/loading-text'
|
|
22
22
|
import { Spinner } from 'termcast/src/components/spinner'
|
|
23
23
|
import { useAnimationTick, TICK_DIVISORS } from 'termcast/src/components/animation-tick'
|
|
24
|
-
import { Footer } from 'termcast/src/components/footer'
|
|
24
|
+
import { Footer, Hoverable } from 'termcast/src/components/footer'
|
|
25
25
|
import { createDescendants } from 'termcast/src/descendants'
|
|
26
26
|
import { useStore } from 'termcast/src/state'
|
|
27
|
+
import { showToast, Toast } from 'termcast/src/apis/toast'
|
|
27
28
|
import { useDialog } from 'termcast/src/internal/dialog'
|
|
28
29
|
import { useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
29
30
|
import { useNavigationPending } from 'termcast/src/internal/navigation'
|
|
@@ -33,8 +34,8 @@ import { ScrollBox } from 'termcast/src/internal/scrollbox'
|
|
|
33
34
|
import { Color, resolveColor } from 'termcast/src/colors'
|
|
34
35
|
import { getIconEmoji, getIconValue } from 'termcast/src/components/icon'
|
|
35
36
|
import { ActionPanel, matchesShortcut } from 'termcast/src/components/actions'
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
37
|
+
import { getInteractiveHoverBackground, useTheme } from 'termcast/src/theme'
|
|
38
|
+
import { Markdown } from 'termcast/src/components/markdown'
|
|
38
39
|
import { CommonProps } from 'termcast/src/utils'
|
|
39
40
|
|
|
40
41
|
export { Color }
|
|
@@ -78,20 +79,32 @@ interface ActionsInterface {
|
|
|
78
79
|
function ListFooter(): any {
|
|
79
80
|
const theme = useTheme()
|
|
80
81
|
const firstActionTitle = useStore((s) => s.firstActionTitle)
|
|
82
|
+
const dropdownFooterLabel = useStore((s) => s.dropdownFooterLabel)
|
|
83
|
+
const dropdownTooltip = useStore((s) => s.dropdownTooltip)
|
|
81
84
|
const hasToast = useStore((s) => s.toast !== null)
|
|
82
85
|
const listContext = useContext(ListContext)
|
|
83
86
|
const isShowingDetail = listContext?.isShowingDetail ?? false
|
|
84
87
|
const hasDropdown = listContext?.hasDropdown ?? false
|
|
88
|
+
const isDropdownOpen = listContext?.isDropdownOpen ?? false
|
|
89
|
+
const openDropdownIfClosed = () => {
|
|
90
|
+
if (!isDropdownOpen) {
|
|
91
|
+
listContext?.openDropdown()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
85
94
|
|
|
86
95
|
const content = hasToast ? null : (
|
|
87
96
|
<box style={{ flexDirection: 'row', gap: 3, flexShrink: 0 }}>
|
|
88
97
|
{firstActionTitle && (
|
|
89
|
-
<
|
|
98
|
+
<Hoverable
|
|
99
|
+
onMouseDown={() => {
|
|
100
|
+
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
90
103
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
91
104
|
↵
|
|
92
105
|
</text>
|
|
93
106
|
<text flexShrink={0} fg={theme.textMuted}>{firstActionTitle.toLowerCase()}</text>
|
|
94
|
-
</
|
|
107
|
+
</Hoverable>
|
|
95
108
|
)}
|
|
96
109
|
<box style={{ flexDirection: 'row', gap: 1, flexShrink: 0 }}>
|
|
97
110
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
@@ -99,20 +112,26 @@ function ListFooter(): any {
|
|
|
99
112
|
</text>
|
|
100
113
|
<text flexShrink={0} fg={theme.textMuted}>navigate</text>
|
|
101
114
|
</box>
|
|
115
|
+
<Hoverable
|
|
116
|
+
onMouseDown={() => {
|
|
117
|
+
useStore.setState({ showActionsDialog: true })
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
121
|
+
^k
|
|
122
|
+
</text>
|
|
123
|
+
<text flexShrink={0} fg={theme.textMuted}>actions</text>
|
|
124
|
+
</Hoverable>
|
|
102
125
|
{hasDropdown && (
|
|
103
|
-
<
|
|
126
|
+
<Hoverable onMouseDown={openDropdownIfClosed}>
|
|
104
127
|
<text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
|
|
105
128
|
^p
|
|
106
129
|
</text>
|
|
107
|
-
<text flexShrink={0} fg={theme.textMuted}>
|
|
108
|
-
|
|
130
|
+
<text flexShrink={0} fg={theme.textMuted}>
|
|
131
|
+
{(dropdownTooltip || dropdownFooterLabel || 'dropdown').toLowerCase()}
|
|
132
|
+
</text>
|
|
133
|
+
</Hoverable>
|
|
109
134
|
)}
|
|
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
135
|
</box>
|
|
117
136
|
)
|
|
118
137
|
|
|
@@ -337,6 +356,8 @@ export interface ListProps
|
|
|
337
356
|
children?: ReactNode
|
|
338
357
|
onSelectionChange?: (id: string | null) => void
|
|
339
358
|
searchBarAccessory?: ReactElement<DropdownProps> | null
|
|
359
|
+
/** Custom ReactNode rendered on the right edge of the search bar row, after the dropdown if present. */
|
|
360
|
+
logo?: ReactNode
|
|
340
361
|
searchText?: string
|
|
341
362
|
enableFiltering?: boolean
|
|
342
363
|
searchBarPlaceholder?: string
|
|
@@ -752,7 +773,23 @@ function ListItemRow(props: {
|
|
|
752
773
|
const accessoryTagWidths = listCtx?.accessoryTagWidths
|
|
753
774
|
const isRelaxed = spacingMode === 'relaxed'
|
|
754
775
|
const { title, subtitle, icon, iconColor, accessories, active, ref } = props
|
|
776
|
+
const hoverBackgroundColor = getInteractiveHoverBackground(theme)
|
|
755
777
|
const [isHovered, setIsHovered] = useState(false)
|
|
778
|
+
const handleMouseMove = () => {
|
|
779
|
+
setIsHovered(true)
|
|
780
|
+
// Select item on hover
|
|
781
|
+
if (!active && props.index !== undefined && props.index !== -1 && listCtx?.setSelectedIndex) {
|
|
782
|
+
listCtx.setSelectedIndex(props.index)
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const handleMouseDown = () => {
|
|
787
|
+
props.onMouseDown?.()
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const handleMouseOut = () => {
|
|
791
|
+
setIsHovered(false)
|
|
792
|
+
}
|
|
756
793
|
|
|
757
794
|
const accessoryElements: ReactNode[] = []
|
|
758
795
|
if (accessories) {
|
|
@@ -851,20 +888,16 @@ function ListItemRow(props: {
|
|
|
851
888
|
backgroundColor: active
|
|
852
889
|
? theme.primary
|
|
853
890
|
: isHovered
|
|
854
|
-
?
|
|
891
|
+
? hoverBackgroundColor
|
|
855
892
|
: undefined,
|
|
856
893
|
paddingLeft: 0,
|
|
857
894
|
paddingRight: 1,
|
|
858
895
|
marginBottom: 1,
|
|
859
896
|
}}
|
|
860
897
|
border={false}
|
|
861
|
-
onMouseMove={
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
onMouseOut={() => {
|
|
865
|
-
setIsHovered(false)
|
|
866
|
-
}}
|
|
867
|
-
onMouseDown={props.onMouseDown}
|
|
898
|
+
onMouseMove={handleMouseMove}
|
|
899
|
+
onMouseOut={handleMouseOut}
|
|
900
|
+
onMouseDown={handleMouseDown}
|
|
868
901
|
>
|
|
869
902
|
{/* Line 1: marker + icon + title + accessories */}
|
|
870
903
|
<box style={{ flexDirection: 'row', justifyContent: 'space-between', gap: 1 }}>
|
|
@@ -920,20 +953,16 @@ function ListItemRow(props: {
|
|
|
920
953
|
backgroundColor: active
|
|
921
954
|
? theme.primary
|
|
922
955
|
: isHovered
|
|
923
|
-
?
|
|
956
|
+
? hoverBackgroundColor
|
|
924
957
|
: undefined,
|
|
925
958
|
paddingLeft: 0,
|
|
926
959
|
paddingRight: 1,
|
|
927
960
|
gap: 1,
|
|
928
961
|
}}
|
|
929
962
|
border={false}
|
|
930
|
-
onMouseMove={
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
onMouseOut={() => {
|
|
934
|
-
setIsHovered(false)
|
|
935
|
-
}}
|
|
936
|
-
onMouseDown={props.onMouseDown}
|
|
963
|
+
onMouseMove={handleMouseMove}
|
|
964
|
+
onMouseOut={handleMouseOut}
|
|
965
|
+
onMouseDown={handleMouseDown}
|
|
937
966
|
>
|
|
938
967
|
<box style={{ flexDirection: 'row', flexGrow: 1, flexShrink: 1, overflow: 'hidden', gap: 1 }}>
|
|
939
968
|
<box style={{ flexDirection: 'row', flexShrink: 0 }}>
|
|
@@ -987,6 +1016,7 @@ export const List: ListType = (props) => {
|
|
|
987
1016
|
isShowingDetail,
|
|
988
1017
|
selectedItemId,
|
|
989
1018
|
searchBarAccessory,
|
|
1019
|
+
logo,
|
|
990
1020
|
spacingMode = 'default',
|
|
991
1021
|
accessoryTagsLayout,
|
|
992
1022
|
throttle,
|
|
@@ -1410,7 +1440,7 @@ export const List: ListType = (props) => {
|
|
|
1410
1440
|
<box style={{ flexDirection: 'column', flexGrow: 1 }}>
|
|
1411
1441
|
{/* Cannot mount focused actions here - would need to be handled differently */}
|
|
1412
1442
|
|
|
1413
|
-
{navigationTitle && (
|
|
1443
|
+
{(navigationTitle || (logo && searchBarAccessory)) && (
|
|
1414
1444
|
<box
|
|
1415
1445
|
border={false}
|
|
1416
1446
|
style={{
|
|
@@ -1419,12 +1449,25 @@ export const List: ListType = (props) => {
|
|
|
1419
1449
|
paddingLeft: 1,
|
|
1420
1450
|
paddingRight: 1,
|
|
1421
1451
|
overflow: 'hidden',
|
|
1452
|
+
flexDirection: 'row',
|
|
1453
|
+
alignItems: 'center',
|
|
1422
1454
|
}}
|
|
1423
1455
|
>
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1456
|
+
{navigationTitle ? (
|
|
1457
|
+
<box flexGrow={1} flexShrink={1} overflow='hidden'>
|
|
1458
|
+
<LoadingBar
|
|
1459
|
+
title={navigationTitle}
|
|
1460
|
+
isLoading={isLoading || navigationPending}
|
|
1461
|
+
/>
|
|
1462
|
+
</box>
|
|
1463
|
+
) : (
|
|
1464
|
+
<box flexGrow={1} />
|
|
1465
|
+
)}
|
|
1466
|
+
{logo ? (
|
|
1467
|
+
<box flexShrink={0} paddingLeft={1}>
|
|
1468
|
+
{logo}
|
|
1469
|
+
</box>
|
|
1470
|
+
) : null}
|
|
1428
1471
|
</box>
|
|
1429
1472
|
)}
|
|
1430
1473
|
|
|
@@ -1473,6 +1516,11 @@ export const List: ListType = (props) => {
|
|
|
1473
1516
|
/>
|
|
1474
1517
|
</box>
|
|
1475
1518
|
{searchBarAccessory}
|
|
1519
|
+
{!navigationTitle && !searchBarAccessory && logo ? (
|
|
1520
|
+
<box flexShrink={0} paddingLeft={2}>
|
|
1521
|
+
{logo}
|
|
1522
|
+
</box>
|
|
1523
|
+
) : null}
|
|
1476
1524
|
</box>
|
|
1477
1525
|
</box>
|
|
1478
1526
|
|
|
@@ -1649,18 +1697,19 @@ const ListItem: ListItemType = (props) => {
|
|
|
1649
1697
|
// Don't render if not visible
|
|
1650
1698
|
if (!isVisible) return null
|
|
1651
1699
|
|
|
1652
|
-
// Handle mouse click on item
|
|
1700
|
+
// Handle mouse click on item — always select and execute first action.
|
|
1701
|
+
// flushSync ensures React commits the new selectedIndex before Zustand
|
|
1702
|
+
// triggers auto-execute, so ActionPanel picks up the clicked item's actions.
|
|
1653
1703
|
const handleMouseDown = () => {
|
|
1654
1704
|
if (listContext && index !== -1) {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
listContext.setSelectedIndex(index)
|
|
1705
|
+
if (!isActive && listContext.setSelectedIndex) {
|
|
1706
|
+
const setIdx = listContext.setSelectedIndex
|
|
1707
|
+
flushSync(() => {
|
|
1708
|
+
setIdx(index)
|
|
1709
|
+
})
|
|
1710
|
+
}
|
|
1711
|
+
if (props.actions) {
|
|
1712
|
+
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
1664
1713
|
}
|
|
1665
1714
|
}
|
|
1666
1715
|
}
|
|
@@ -1706,17 +1755,7 @@ const ListItem: ListItemType = (props) => {
|
|
|
1706
1755
|
)
|
|
1707
1756
|
}
|
|
1708
1757
|
|
|
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
1758
|
|
|
1716
|
-
return (
|
|
1717
|
-
<markdown content={markdown} syntaxStyle={markdownSyntaxStyle} conceal renderNode={renderNode} />
|
|
1718
|
-
)
|
|
1719
|
-
}
|
|
1720
1759
|
|
|
1721
1760
|
const ListItemDetail: ListItemDetailType = (props) => {
|
|
1722
1761
|
const theme = useTheme()
|
|
@@ -1747,7 +1786,7 @@ const ListItemDetail: ListItemDetailType = (props) => {
|
|
|
1747
1786
|
>
|
|
1748
1787
|
<box gap={1} style={{ flexDirection: 'column' }}>
|
|
1749
1788
|
{markdown && markdown.trim().length > 0 && (
|
|
1750
|
-
<
|
|
1789
|
+
<Markdown content={markdown} />
|
|
1751
1790
|
)}
|
|
1752
1791
|
{metadata}
|
|
1753
1792
|
</box>
|
|
@@ -1807,6 +1846,7 @@ ListItem.Detail = ListItemDetail
|
|
|
1807
1846
|
const ListDropdown: ListDropdownType = (props) => {
|
|
1808
1847
|
const theme = useTheme()
|
|
1809
1848
|
const listContext = useContext(ListContext)
|
|
1849
|
+
const hoverBackgroundColor = getInteractiveHoverBackground(theme)
|
|
1810
1850
|
const [isHovered, setIsHovered] = useState(false)
|
|
1811
1851
|
|
|
1812
1852
|
// If not inside a List, just render nothing (for type safety)
|
|
@@ -1815,6 +1855,11 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1815
1855
|
}
|
|
1816
1856
|
|
|
1817
1857
|
const { isDropdownOpen, setIsDropdownOpen } = listContext
|
|
1858
|
+
|
|
1859
|
+
const setDropdownSelection = (props: { value: string; title: string }) => {
|
|
1860
|
+
setDropdownState({ value: props.value, title: props.title })
|
|
1861
|
+
useStore.setState({ dropdownFooterLabel: props.title || 'dropdown' })
|
|
1862
|
+
}
|
|
1818
1863
|
// Store both value and title together
|
|
1819
1864
|
const [dropdownState, setDropdownState] = useState<{
|
|
1820
1865
|
value: string
|
|
@@ -1827,6 +1872,11 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1827
1872
|
const dialog = useDialog()
|
|
1828
1873
|
const inFocus = useIsInFocus()
|
|
1829
1874
|
|
|
1875
|
+
// Store dropdown tooltip in zustand for footer display
|
|
1876
|
+
useLayoutEffect(() => {
|
|
1877
|
+
useStore.setState({ dropdownTooltip: props.tooltip || '' })
|
|
1878
|
+
}, [props.tooltip])
|
|
1879
|
+
|
|
1830
1880
|
// Update value and find its title
|
|
1831
1881
|
useLayoutEffect(() => {
|
|
1832
1882
|
const valueToUse =
|
|
@@ -1840,12 +1890,17 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1840
1890
|
|
|
1841
1891
|
if (items.length > 0) {
|
|
1842
1892
|
const firstItem = items[0].props as DropdownItemDescendant
|
|
1843
|
-
|
|
1893
|
+
setDropdownSelection({ value: firstItem.value, title: firstItem.title })
|
|
1844
1894
|
return
|
|
1845
1895
|
}
|
|
1846
1896
|
}
|
|
1847
1897
|
|
|
1848
|
-
if (!valueToUse)
|
|
1898
|
+
if (!valueToUse) {
|
|
1899
|
+
useStore.setState({
|
|
1900
|
+
dropdownFooterLabel: dropdownState.title || 'dropdown',
|
|
1901
|
+
})
|
|
1902
|
+
return
|
|
1903
|
+
}
|
|
1849
1904
|
|
|
1850
1905
|
// Try to find the title for this value
|
|
1851
1906
|
let title = valueToUse
|
|
@@ -1859,8 +1914,11 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1859
1914
|
|
|
1860
1915
|
// Only update if something changed
|
|
1861
1916
|
if (dropdownState.value !== valueToUse || dropdownState.title !== title) {
|
|
1862
|
-
|
|
1917
|
+
setDropdownSelection({ value: valueToUse, title })
|
|
1918
|
+
return
|
|
1863
1919
|
}
|
|
1920
|
+
|
|
1921
|
+
useStore.setState({ dropdownFooterLabel: title || 'dropdown' })
|
|
1864
1922
|
}, [props.value]) // Run when props.value changes and on mount
|
|
1865
1923
|
|
|
1866
1924
|
const dropdownContextValue = useMemo<DropdownContextValue>(
|
|
@@ -1889,7 +1947,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1889
1947
|
break
|
|
1890
1948
|
}
|
|
1891
1949
|
}
|
|
1892
|
-
|
|
1950
|
+
setDropdownSelection({ value: newValue, title })
|
|
1893
1951
|
setIsDropdownOpen(false)
|
|
1894
1952
|
dialog.clear()
|
|
1895
1953
|
if (props.onChange) {
|
|
@@ -1912,6 +1970,11 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1912
1970
|
|
|
1913
1971
|
// Display the title from our state
|
|
1914
1972
|
const displayValue = dropdownState.title || 'All'
|
|
1973
|
+
const openDropdownIfClosed = () => {
|
|
1974
|
+
if (!isDropdownOpen) {
|
|
1975
|
+
listContext.openDropdown()
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1915
1978
|
|
|
1916
1979
|
return (
|
|
1917
1980
|
<DropdownDescendantsProvider value={descendantsContext}>
|
|
@@ -1926,27 +1989,25 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1926
1989
|
// minWidth: value.length + 4,
|
|
1927
1990
|
flexDirection: 'row',
|
|
1928
1991
|
flexShrink: 0,
|
|
1929
|
-
backgroundColor: isHovered ?
|
|
1992
|
+
backgroundColor: isHovered ? hoverBackgroundColor : undefined,
|
|
1930
1993
|
}}
|
|
1931
1994
|
onMouseMove={() => setIsHovered(true)}
|
|
1932
1995
|
onMouseOut={() => setIsHovered(false)}
|
|
1933
|
-
onMouseDown={
|
|
1934
|
-
// Open dropdown when clicked
|
|
1935
|
-
if (!isDropdownOpen) {
|
|
1936
|
-
listContext.openDropdown()
|
|
1937
|
-
}
|
|
1938
|
-
}}
|
|
1996
|
+
onMouseDown={openDropdownIfClosed}
|
|
1939
1997
|
>
|
|
1940
1998
|
{/*<text >^p </text>*/}
|
|
1941
1999
|
{listContext.isLoading ? (
|
|
1942
|
-
<
|
|
1943
|
-
{
|
|
1944
|
-
|
|
2000
|
+
<box onMouseDown={openDropdownIfClosed}>
|
|
2001
|
+
<LoadingText isLoading color={isHovered ? theme.text : theme.textMuted}>
|
|
2002
|
+
{displayValue || 'Loading...'}
|
|
2003
|
+
</LoadingText>
|
|
2004
|
+
</box>
|
|
1945
2005
|
) : (
|
|
1946
2006
|
<text
|
|
1947
2007
|
flexShrink={0}
|
|
1948
2008
|
fg={isHovered ? theme.text : theme.textMuted}
|
|
1949
2009
|
selectable={false}
|
|
2010
|
+
onMouseDown={openDropdownIfClosed}
|
|
1950
2011
|
>
|
|
1951
2012
|
{displayValue}
|
|
1952
2013
|
</text>
|
|
@@ -1955,6 +2016,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1955
2016
|
flexShrink={0}
|
|
1956
2017
|
fg={isHovered ? theme.text : theme.textMuted}
|
|
1957
2018
|
selectable={false}
|
|
2019
|
+
onMouseDown={openDropdownIfClosed}
|
|
1958
2020
|
>
|
|
1959
2021
|
{' '}
|
|
1960
2022
|
▾
|
|
@@ -1967,6 +2029,7 @@ const ListDropdown: ListDropdownType = (props) => {
|
|
|
1967
2029
|
|
|
1968
2030
|
ListDropdown.Item = (props) => {
|
|
1969
2031
|
const theme = useTheme()
|
|
2032
|
+
const hoverBackgroundColor = getInteractiveHoverBackground(theme)
|
|
1970
2033
|
const dropdownContext = useContext(DropdownContext)
|
|
1971
2034
|
const [isHovered, setIsHovered] = useState(false)
|
|
1972
2035
|
|
|
@@ -2032,7 +2095,7 @@ ListDropdown.Item = (props) => {
|
|
|
2032
2095
|
backgroundColor: isActive
|
|
2033
2096
|
? theme.primary
|
|
2034
2097
|
: isHovered
|
|
2035
|
-
?
|
|
2098
|
+
? hoverBackgroundColor
|
|
2036
2099
|
: undefined,
|
|
2037
2100
|
paddingLeft: isActive ? 0 : 1,
|
|
2038
2101
|
paddingRight: 1,
|
|
@@ -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 }
|