Skip to content

Commit 0562378

Browse files
committed
Extract tabs into a dedicated component and update interaction logic
- tabs should be placed on the right side (together with close and token count) - clicking on active tab should have the same effect as clicking on 'clear button' - remove 'clear' button (as we already can clear by clicking on active tab bullet) - once mouse is over active tab, replace its filled dot with 'close-small' icon - do this replacement [Pasted text] with js - we want to requre user to first move out, and in to show the close icon. without the out-in they should see filled dot - when changing active tab, we can see for a split second 'close' icon, we should not until mouse re-enters the active tab - when closing second tab, it dissapears and cursor is now over first tab. the issue is that this edge case scenario triggers 'close' icon state - extract tabs component to `packages/ui/src/components/editor/common/prompts/PromptField/components/Tabs` - show token count on the left to tabs └── packages └── ui └── src └── components └── editor └── common └── prompts └── PromptField ├── PromptField.module.scss ├── PromptField.stories.tsx ├── PromptField.tsx ├── components │ └── Tabs │ ├── Tabs.tsx │ └── index.ts ├── hooks │ ├── use-drag-drop.ts │ ├── use-dropdown.ts │ ├── use-ghost-text.ts │ ├── use-handlers │ │ ├── handle-copy.ts │ │ ├── handle-cut.ts │ │ ├── handle-input-change.ts │ │ ├── handle-input-click.ts │ │ ├── handle-key-down.ts │ │ ├── handle-paste.ts │ │ ├── handle-submit.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── use-handlers.ts │ │ └── utils.ts │ └── use-keyboard-shortcuts.ts └── index.ts
1 parent b2e15c6 commit 0562378

5 files changed

Lines changed: 200 additions & 144 deletions

File tree

‎packages/ui/src/components/editor/common/prompts/PromptField/PromptField.module.scss‎

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@
6363
float: right;
6464
display: flex;
6565
align-items: center;
66-
margin: 2px 2px 0 4px;
66+
margin: 3px 3px 0 4px;
6767
position: relative;
6868
z-index: 2;
69+
gap: 4px;
6970

7071
&__prompt-token-count {
7172
font-size: 10px;
@@ -75,27 +76,6 @@
7576
font-variant-numeric: tabular-nums;
7677
transform: translateY(0.5px);
7778
}
78-
79-
&__clear-button {
80-
position: relative;
81-
width: 18px;
82-
cursor: pointer;
83-
color: var(--vscode-icon-foreground);
84-
opacity: var(--dimmed-opacity);
85-
display: flex;
86-
align-items: center;
87-
justify-content: center;
88-
padding-bottom: 2px;
89-
90-
&::before {
91-
font-size: 13px !important;
92-
line-height: 1;
93-
}
94-
95-
&:hover {
96-
opacity: 1;
97-
}
98-
}
9979
}
10080

10181
.input {
@@ -345,14 +325,12 @@
345325
}
346326

347327
.tabs {
348-
float: left;
349328
display: flex;
350329
flex-wrap: wrap;
351330
user-select: none;
352-
margin: 2px 1px 0 -2px;
353331
position: relative;
354332
z-index: 2;
355-
margin: 7px 1px 0 5px;
333+
margin-top: 1px;
356334

357335
&__tab {
358336
height: 15px;
@@ -386,6 +364,14 @@
386364
content: '\ea71';
387365
}
388366
}
367+
368+
&.tabs__tab--show-close {
369+
.tabs__tab-icon {
370+
&::before {
371+
content: '\ea76';
372+
}
373+
}
374+
}
389375
}
390376

391377
&:hover {

‎packages/ui/src/components/editor/common/prompts/PromptField/PromptField.tsx‎

Lines changed: 20 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useRef, useEffect, useMemo, useState, useCallback } from 'react'
2-
import { ReactSortable } from 'react-sortablejs'
32
import styles from './PromptField.module.scss'
43
import cn from 'classnames'
54
import { Icon } from '../../Icon'
@@ -20,6 +19,7 @@ import {
2019
map_raw_pos_to_display_pos,
2120
get_highlighted_text
2221
} from '../shared/symbols'
22+
import { Tabs, TabsRef } from './components/Tabs'
2323

2424
export type EditFormat = 'whole' | 'search-replace' | 'diff' | 'truncated'
2525

@@ -131,24 +131,8 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
131131
'at' | 'hash' | 'slash' | null
132132
>(null)
133133

134-
const [tab_items, set_tab_items] = useState<{ id: string }[]>([])
135-
136134
const container_inner_ref = useRef<HTMLDivElement>(null)
137-
138-
useEffect(() => {
139-
set_tab_items((prev) => {
140-
if (prev.length === props.tabs_count) return prev
141-
if (prev.length < props.tabs_count) {
142-
return [
143-
...prev,
144-
...Array.from({ length: props.tabs_count - prev.length }).map(() => ({
145-
id: Math.random().toString(36).substring(7)
146-
}))
147-
]
148-
}
149-
return prev.slice(0, props.tabs_count)
150-
})
151-
}, [props.tabs_count])
135+
const tabs_ref = useRef<TabsRef>(null)
152136

153137
const chevron_button_ref = useRef<HTMLButtonElement>(null)
154138
const disconnected_chevron_button_ref = useRef<HTMLButtonElement>(null)
@@ -943,105 +927,34 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
943927
{display_token_count(props.prompt_token_count)}
944928
</div>
945929
)}
946-
{props.tabs_count > 1 && (
947-
<div
948-
className={cn(
949-
styles['top-right__clear-button'],
950-
'codicon',
951-
'codicon-close-small'
952-
)}
953-
/>
954-
)}
930+
<Tabs
931+
is_hidden
932+
tabs_count={props.tabs_count}
933+
active_tab_index={props.active_tab_index}
934+
/>
955935
</div>
956-
{props.tabs_count > 1 ? (
957-
<div className={styles.tabs} style={{ visibility: 'hidden' }}>
958-
{tab_items.map((item) => (
959-
<div key={item.id} className={styles.tabs__tab} />
960-
))}
961-
<div
962-
className={cn(styles.tabs__tab, styles['tabs__tab--new'])}
963-
/>
964-
</div>
965-
) : props.tabs_count === 1 ? (
966-
<div className={styles.tabs} style={{ visibility: 'hidden' }}>
967-
<div
968-
className={cn(styles.tabs__tab, styles['tabs__tab--new'])}
969-
/>
970-
</div>
971-
) : null}
972936
<div className={styles['placeholder-text']}>{placeholder}</div>
973937
</div>
974938
)}
975-
<div className={styles['top-right']}>
939+
<div
940+
className={styles['top-right']}
941+
onMouseLeave={() => {
942+
tabs_ref.current?.handle_mouse_leave()
943+
}}
944+
>
976945
{has_content && props.prompt_token_count > 250 && (
977946
<div className={styles['top-right__prompt-token-count']}>
978947
{display_token_count(props.prompt_token_count)}
979948
</div>
980949
)}
981-
{(!!props.value || props.tabs_count > 1) && (
982-
<div
983-
className={cn(
984-
styles['top-right__clear-button'],
985-
'codicon',
986-
'codicon-close-small'
987-
)}
988-
data-role="clear-button"
989-
onClick={handle_input_click}
990-
/>
991-
)}
950+
<Tabs
951+
ref={tabs_ref}
952+
tabs_count={props.tabs_count}
953+
active_tab_index={props.active_tab_index}
954+
on_tabs_reorder={props.on_tabs_reorder}
955+
handle_input_click={handle_input_click}
956+
/>
992957
</div>
993-
{props.tabs_count > 1 ? (
994-
<ReactSortable
995-
list={tab_items}
996-
setList={(new_list) => {
997-
const has_changed = new_list.some(
998-
(item, i) => item.id !== tab_items[i]?.id
999-
)
1000-
if (has_changed) {
1001-
const new_order = new_list.map((item) =>
1002-
tab_items.findIndex((t) => t.id === item.id)
1003-
)
1004-
set_tab_items(new_list)
1005-
props.on_tabs_reorder?.(new_order)
1006-
}
1007-
}}
1008-
className={styles.tabs}
1009-
animation={150}
1010-
filter={`.${styles['tabs__tab--new']}`}
1011-
>
1012-
{tab_items.map((item, i) => (
1013-
<div
1014-
key={item.id}
1015-
className={cn(styles.tabs__tab, {
1016-
[styles['tabs__tab--active']]: i === props.active_tab_index
1017-
})}
1018-
onClick={(e) => {
1019-
e.stopPropagation()
1020-
props.on_tab_change(i)
1021-
}}
1022-
>
1023-
<div className={styles['tabs__tab-icon']} />
1024-
</div>
1025-
))}
1026-
<div
1027-
className={cn(styles.tabs__tab, styles['tabs__tab--new'])}
1028-
onClick={(e) => {
1029-
e.stopPropagation()
1030-
props.on_new_tab()
1031-
}}
1032-
/>
1033-
</ReactSortable>
1034-
) : props.tabs_count === 1 ? (
1035-
<div className={styles.tabs}>
1036-
<div
1037-
className={cn(styles.tabs__tab, styles['tabs__tab--new'])}
1038-
onClick={(e) => {
1039-
e.stopPropagation()
1040-
props.on_new_tab()
1041-
}}
1042-
/>
1043-
</div>
1044-
) : null}
1045958
<div
1046959
ref={input_ref}
1047960
contentEditable={true}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'
2+
import { ReactSortable } from 'react-sortablejs'
3+
import cn from 'classnames'
4+
import styles from '../../PromptField.module.scss'
5+
6+
export type TabsProps = {
7+
tabs_count: number
8+
active_tab_index: number
9+
on_tabs_reorder?: (new_order: number[]) => void
10+
handle_input_click?: (e: React.MouseEvent<HTMLDivElement>) => void
11+
is_hidden?: boolean
12+
}
13+
14+
export type TabsRef = {
15+
handle_mouse_leave: () => void
16+
}
17+
18+
export const Tabs = forwardRef<TabsRef, TabsProps>((props, ref) => {
19+
const [tab_items, set_tab_items] = useState<{ id: string }[]>([])
20+
const [has_left_active_tab, set_has_left_active_tab] = useState(true)
21+
const [hovered_tab_index, set_hovered_tab_index] = useState<number | null>(
22+
null
23+
)
24+
const [prev_active_tab_index_state, set_prev_active_tab_index_state] =
25+
useState(props.active_tab_index)
26+
const [prev_tabs_count_state, set_prev_tabs_count_state] = useState(
27+
props.tabs_count
28+
)
29+
30+
useImperativeHandle(ref, () => ({
31+
handle_mouse_leave: () => {
32+
set_has_left_active_tab(true)
33+
set_hovered_tab_index(null)
34+
}
35+
}))
36+
37+
let effective_has_left_active_tab = has_left_active_tab
38+
if (
39+
prev_active_tab_index_state !== props.active_tab_index ||
40+
prev_tabs_count_state !== props.tabs_count
41+
) {
42+
if (prev_tabs_count_state > props.tabs_count) {
43+
effective_has_left_active_tab = false
44+
} else {
45+
effective_has_left_active_tab =
46+
hovered_tab_index !== props.active_tab_index
47+
}
48+
set_has_left_active_tab(effective_has_left_active_tab)
49+
set_prev_active_tab_index_state(props.active_tab_index)
50+
set_prev_tabs_count_state(props.tabs_count)
51+
}
52+
53+
useEffect(() => {
54+
set_tab_items((prev) => {
55+
if (prev.length === props.tabs_count) return prev
56+
if (prev.length < props.tabs_count) {
57+
return [
58+
...prev,
59+
...Array.from({ length: props.tabs_count - prev.length }).map(() => ({
60+
id: Math.random().toString(36).substring(7)
61+
}))
62+
]
63+
}
64+
return prev.slice(0, props.tabs_count)
65+
})
66+
}, [props.tabs_count])
67+
68+
if (props.is_hidden) {
69+
if (props.tabs_count > 1) {
70+
return (
71+
<div className={styles.tabs} style={{ visibility: 'hidden' }}>
72+
{tab_items.map((item) => (
73+
<div key={item.id} className={styles.tabs__tab} />
74+
))}
75+
<div className={cn(styles.tabs__tab, styles['tabs__tab--new'])} />
76+
</div>
77+
)
78+
} else if (props.tabs_count === 1) {
79+
return (
80+
<div className={styles.tabs} style={{ visibility: 'hidden' }}>
81+
<div className={cn(styles.tabs__tab, styles['tabs__tab--new'])} />
82+
</div>
83+
)
84+
}
85+
return null
86+
}
87+
88+
if (props.tabs_count > 1) {
89+
return (
90+
<ReactSortable
91+
list={tab_items}
92+
setList={(new_list) => {
93+
const has_changed = new_list.some(
94+
(item, i) => item.id !== tab_items[i]?.id
95+
)
96+
if (has_changed) {
97+
const new_order = new_list.map((item) =>
98+
tab_items.findIndex((t) => t.id === item.id)
99+
)
100+
set_tab_items(new_list)
101+
props.on_tabs_reorder?.(new_order)
102+
}
103+
}}
104+
className={styles.tabs}
105+
animation={150}
106+
filter={`.${styles['tabs__tab--new']}`}
107+
>
108+
{tab_items.map((item, i) => {
109+
const is_active = i === props.active_tab_index
110+
const is_hovered = hovered_tab_index === i
111+
const show_close =
112+
is_active && is_hovered && effective_has_left_active_tab
113+
return (
114+
<div
115+
key={item.id}
116+
className={cn(styles.tabs__tab, {
117+
[styles['tabs__tab--active']]: is_active,
118+
[styles['tabs__tab--show-close']]: show_close
119+
})}
120+
data-role="tab-item"
121+
data-index={i}
122+
onClick={props.handle_input_click}
123+
onMouseEnter={() => set_hovered_tab_index(i)}
124+
onMouseLeave={() => {
125+
set_hovered_tab_index(null)
126+
if (is_active) {
127+
set_has_left_active_tab(true)
128+
}
129+
}}
130+
>
131+
<div className={styles['tabs__tab-icon']} />
132+
</div>
133+
)
134+
})}
135+
<div
136+
className={cn(styles.tabs__tab, styles['tabs__tab--new'])}
137+
data-role="tab-new"
138+
onClick={props.handle_input_click}
139+
/>
140+
</ReactSortable>
141+
)
142+
}
143+
144+
if (props.tabs_count === 1) {
145+
return (
146+
<div className={styles.tabs}>
147+
<div
148+
className={cn(styles.tabs__tab, styles['tabs__tab--new'])}
149+
data-role="tab-new"
150+
onClick={props.handle_input_click}
151+
/>
152+
</div>
153+
)
154+
}
155+
156+
return null
157+
})
158+
159+
Tabs.displayName = 'Tabs'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Tabs'

0 commit comments

Comments
 (0)