Skip to content

Commit 0a4c98e

Browse files
committed
feat: add user education to New Workspace flow
1 parent 9a85da3 commit 0a4c98e

File tree

2 files changed

+81
-7
lines changed

2 files changed

+81
-7
lines changed

src/components/NewWorkspaceModal.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useId, useState } from "react";
22
import styled from "@emotion/styled";
33
import { Modal, ModalInfo, ModalActions, CancelButton, PrimaryButton } from "./Modal";
4+
import { TooltipWrapper, Tooltip } from "./Tooltip";
45

56
const FormGroup = styled.div`
67
margin-bottom: 20px;
@@ -54,6 +55,12 @@ const InfoCode = styled.code`
5455
word-break: break-all;
5556
`;
5657

58+
const UnderlinedLabel = styled.span`
59+
text-decoration: underline dotted #666;
60+
text-underline-offset: 2px;
61+
cursor: help;
62+
`;
63+
5764
interface NewWorkspaceModalProps {
5865
isOpen: boolean;
5966
projectName: string;
@@ -152,7 +159,23 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
152159
>
153160
<form onSubmit={(event) => void handleSubmit(event)}>
154161
<FormGroup>
155-
<label htmlFor="branchName">Workspace Branch Name:</label>
162+
<label htmlFor="branchName">
163+
<TooltipWrapper inline>
164+
<UnderlinedLabel>Workspace Branch Name:</UnderlinedLabel>
165+
<Tooltip width="wide" position="bottom" interactive>
166+
<strong>About Workspaces:</strong>
167+
<ul style={{ margin: "4px 0", paddingLeft: "16px" }}>
168+
<li>Uses git worktrees (separate directories sharing .git)</li>
169+
<li>All committed changes visible across all worktrees</li>
170+
<li>Agent can switch branches freely during session</li>
171+
<li>Define branching strategy in AGENTS.md</li>
172+
</ul>
173+
<a href="https://cmux.io/workspaces.html" target="_blank" rel="noopener noreferrer">
174+
Learn more
175+
</a>
176+
</Tooltip>
177+
</TooltipWrapper>
178+
</label>
156179
<input
157180
id="branchName"
158181
type="text"

src/components/Tooltip.tsx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import styled from "@emotion/styled";
55
// Context for passing hover state and trigger ref from wrapper to tooltip
66
interface TooltipContextValue {
77
isHovered: boolean;
8+
setIsHovered: (value: boolean) => void;
89
triggerRef: React.RefObject<HTMLElement> | null;
910
}
1011

1112
const TooltipContext = createContext<TooltipContextValue>({
1213
isHovered: false,
14+
// eslint-disable-next-line @typescript-eslint/no-empty-function
15+
setIsHovered: () => {},
1316
triggerRef: null,
1417
});
1518

@@ -22,14 +25,30 @@ interface TooltipWrapperProps {
2225
export const TooltipWrapper: React.FC<TooltipWrapperProps> = ({ inline = false, children }) => {
2326
const [isHovered, setIsHovered] = useState(false);
2427
const triggerRef = useRef<HTMLSpanElement>(null);
28+
const leaveTimerRef = useRef<NodeJS.Timeout | null>(null);
29+
30+
const handleMouseEnter = () => {
31+
if (leaveTimerRef.current) {
32+
clearTimeout(leaveTimerRef.current);
33+
leaveTimerRef.current = null;
34+
}
35+
setIsHovered(true);
36+
};
37+
38+
const handleMouseLeave = () => {
39+
// Delay hiding to allow moving mouse to tooltip
40+
leaveTimerRef.current = setTimeout(() => {
41+
setIsHovered(false);
42+
}, 100);
43+
};
2544

2645
return (
27-
<TooltipContext.Provider value={{ isHovered, triggerRef }}>
46+
<TooltipContext.Provider value={{ isHovered, setIsHovered, triggerRef }}>
2847
<StyledWrapper
2948
ref={triggerRef}
3049
inline={inline}
31-
onMouseEnter={() => setIsHovered(true)}
32-
onMouseLeave={() => setIsHovered(false)}
50+
onMouseEnter={handleMouseEnter}
51+
onMouseLeave={handleMouseLeave}
3352
>
3453
{children}
3554
</StyledWrapper>
@@ -49,6 +68,7 @@ interface TooltipProps {
4968
position?: "top" | "bottom";
5069
children: React.ReactNode;
5170
className?: string;
71+
interactive?: boolean;
5272
}
5373

5474
export const Tooltip: React.FC<TooltipProps> = ({
@@ -57,9 +77,11 @@ export const Tooltip: React.FC<TooltipProps> = ({
5777
position = "top",
5878
children,
5979
className = "tooltip",
80+
interactive = false,
6081
}) => {
61-
const { isHovered, triggerRef } = useContext(TooltipContext);
82+
const { isHovered, setIsHovered, triggerRef } = useContext(TooltipContext);
6283
const tooltipRef = useRef<HTMLDivElement>(null);
84+
const leaveTimerRef = useRef<NodeJS.Timeout | null>(null);
6385
const [tooltipState, setTooltipState] = useState<{
6486
style: React.CSSProperties;
6587
arrowStyle: React.CSSProperties;
@@ -170,6 +192,22 @@ export const Tooltip: React.FC<TooltipProps> = ({
170192
return () => cancelAnimationFrame(rafId);
171193
}, [isHovered, align, position, triggerRef]);
172194

195+
const handleTooltipMouseEnter = () => {
196+
if (interactive) {
197+
if (leaveTimerRef.current) {
198+
clearTimeout(leaveTimerRef.current);
199+
leaveTimerRef.current = null;
200+
}
201+
setIsHovered(true);
202+
}
203+
};
204+
205+
const handleTooltipMouseLeave = () => {
206+
if (interactive) {
207+
setIsHovered(false);
208+
}
209+
};
210+
173211
if (!isHovered) {
174212
return null;
175213
}
@@ -187,6 +225,9 @@ export const Tooltip: React.FC<TooltipProps> = ({
187225
}}
188226
width={width}
189227
className={className}
228+
interactive={interactive}
229+
onMouseEnter={handleTooltipMouseEnter}
230+
onMouseLeave={handleTooltipMouseLeave}
190231
>
191232
{children}
192233
<Arrow style={tooltipState.arrowStyle} />
@@ -195,7 +236,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
195236
);
196237
};
197238

198-
const StyledTooltip = styled.div<{ width: string }>`
239+
const StyledTooltip = styled.div<{ width: string; interactive: boolean }>`
199240
background-color: #2d2d30;
200241
color: #cccccc;
201242
text-align: left;
@@ -209,8 +250,18 @@ const StyledTooltip = styled.div<{ width: string }>`
209250
font-family: var(--font-primary);
210251
border: 1px solid #464647;
211252
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
212-
pointer-events: none;
253+
pointer-events: ${(props) => (props.interactive ? "auto" : "none")};
213254
/* No default visibility/opacity - controlled via inline styles */
255+
256+
a {
257+
color: #4ec9b0;
258+
text-decoration: underline;
259+
cursor: pointer;
260+
261+
&:hover {
262+
color: #6fd9c0;
263+
}
264+
}
214265
`;
215266

216267
const Arrow = styled.div`

0 commit comments

Comments
 (0)