+
+
Try moving the slider to see the purple glow effect intensify:
-
+
• Off: No thinking (gray)
• Low: Minimal thinking (light purple)
• Medium: Moderate thinking (purple)
• High: Maximum thinking (bright purple)
-
+
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -126,32 +95,20 @@ export const InteractiveDemo: Story = {
export const LockedThinking: Story = {
args: { modelString: "openai:gpt-5-pro" },
render: (args) => (
-
-
+
+
Some models have locked thinking levels based on their capabilities:
-
+
+ GPT-5-Pro (locked to “high”)
+
-
+
Hover over the locked indicator to see why it's fixed.
-
+
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
diff --git a/src/components/ThinkingSlider.tsx b/src/components/ThinkingSlider.tsx
index 0374d89d9..01f6d3286 100644
--- a/src/components/ThinkingSlider.tsx
+++ b/src/components/ThinkingSlider.tsx
@@ -1,5 +1,4 @@
import React, { useEffect, useId } from "react";
-import styled from "@emotion/styled";
import type { ThinkingLevel, ThinkingLevelOn } from "@/types/thinking";
import { useThinkingLevel } from "@/hooks/useThinkingLevel";
import { TooltipWrapper, Tooltip } from "./Tooltip";
@@ -8,19 +7,6 @@ import { getThinkingPolicyForModel } from "@/utils/thinking/policy";
import { updatePersistedState } from "@/hooks/usePersistedState";
import { getLastThinkingByModelKey } from "@/constants/storage";
-const ThinkingSliderContainer = styled.div`
- display: flex;
- align-items: center;
- gap: 8px;
- margin-left: 20px;
-`;
-
-const ThinkingLabel = styled.label`
- font-size: 10px;
- color: #606060;
- user-select: none;
-`;
-
// Subtle consistent glow for active levels
const GLOW = {
track: "0 0 6px 1px hsl(271 76% 53% / 0.3)",
@@ -60,87 +46,16 @@ const getTextStyle = (n: number) => {
};
};
-const ThinkingSlider = styled.input<{ value: number }>`
- width: 80px;
- height: 4px;
- -webkit-appearance: none;
- appearance: none;
- background: #3e3e42;
- outline: none;
- border-radius: 2px;
- transition: box-shadow 0.2s ease;
-
- /* Purple glow that intensifies with level */
- box-shadow: ${(props) => GLOW_INTENSITIES[props.value].track};
-
- &::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 12px;
- height: 12px;
- border-radius: 50%;
- background: ${(props) =>
- props.value === 0
- ? "#606060"
- : `hsl(271 76% ${53 + props.value * 5}%)`}; /* Lighter purple as value increases */
- cursor: pointer;
- transition:
- background 0.2s ease,
- box-shadow 0.2s ease;
- box-shadow: ${(props) => GLOW_INTENSITIES[props.value].thumb};
- }
-
- &::-moz-range-thumb {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- background: ${(props) =>
- props.value === 0 ? "#606060" : `hsl(271 76% ${53 + props.value * 5}%)`};
- cursor: pointer;
- border: none;
- transition:
- background 0.2s ease,
- box-shadow 0.2s ease;
- box-shadow: ${(props) => GLOW_INTENSITIES[props.value].thumb};
- }
-
- &:hover {
- box-shadow: ${(props) => {
- const nextValue = Math.min(props.value + 1, 3);
- return GLOW_INTENSITIES[nextValue].track;
- }};
-
- &::-webkit-slider-thumb {
- box-shadow: ${(props) => {
- const nextValue = Math.min(props.value + 1, 3);
- return GLOW_INTENSITIES[nextValue].thumb;
- }};
- }
+const getSliderStyles = (value: number, isHover = false) => {
+ const effectiveValue = isHover ? Math.min(value + 1, 3) : value;
+ const thumbBg = value === 0 ? "#606060" : `hsl(271 76% ${53 + value * 5}%)`;
- &::-moz-range-thumb {
- box-shadow: ${(props) => {
- const nextValue = Math.min(props.value + 1, 3);
- return GLOW_INTENSITIES[nextValue].thumb;
- }};
- }
- }
-`;
-
-const ThinkingLevelText = styled.span<{ value: number }>`
- min-width: 45px;
- text-transform: uppercase;
- user-select: none;
- transition: all 0.2s ease;
- ${(props) => {
- const style = getTextStyle(props.value);
- return `
- color: ${style.color};
- font-weight: ${style.fontWeight};
- text-shadow: ${style.textShadow};
- font-size: ${style.fontSize};
- `;
- }}
-`;
+ return {
+ trackShadow: GLOW_INTENSITIES[effectiveValue].track,
+ thumbShadow: GLOW_INTENSITIES[effectiveValue].thumb,
+ thumbBg,
+ };
+};
// Helper functions to map between slider value and ThinkingLevel
const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
@@ -159,6 +74,7 @@ interface ThinkingControlProps {
export const ThinkingSliderComponent: React.FC
= ({ modelString }) => {
const [thinkingLevel, setThinkingLevel] = useThinkingLevel();
+ const [isHovering, setIsHovering] = React.useState(false);
const sliderId = useId();
const allowed = getThinkingPolicyForModel(modelString);
@@ -175,25 +91,29 @@ export const ThinkingSliderComponent: React.FC = ({ modelS
const value = thinkingLevelToValue(fixedLevel);
const formattedLevel = fixedLevel === "off" ? "Off" : fixedLevel;
const tooltipMessage = `Model ${modelString} locks thinking at ${formattedLevel.toUpperCase()} to match its capabilities.`;
+ const textStyle = getTextStyle(value);
return (
-
- Thinking:
-
+
+
{fixedLevel}
-
-
+
+
{tooltipMessage}
);
}
const value = thinkingLevelToValue(thinkingLevel);
+ const sliderStyles = getSliderStyles(value, isHovering);
+ const textStyle = getTextStyle(value);
const handleThinkingLevelChange = (newLevel: ThinkingLevel) => {
setThinkingLevel(newLevel);
@@ -207,9 +127,11 @@ export const ThinkingSliderComponent: React.FC
= ({ modelS
return (
-
- Thinking:
-
+
+ = ({ modelS
onChange={(e) =>
handleThinkingLevelChange(valueToThinkingLevel(parseInt(e.target.value)))
}
+ onMouseEnter={() => setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
id={sliderId}
role="slider"
aria-valuemin={0}
aria-valuemax={3}
aria-valuenow={value}
aria-valuetext={thinkingLevel}
+ className="thinking-slider"
+ style={
+ {
+ "--track-shadow": sliderStyles.trackShadow,
+ "--thumb-shadow": sliderStyles.thumbShadow,
+ "--thumb-bg": sliderStyles.thumbBg,
+ } as React.CSSProperties
+ }
/>
-
+
{thinkingLevel}
-
-
+
+
{formatKeybind(KEYBINDS.TOGGLE_THINKING)} to toggle
);
diff --git a/src/components/TipsCarousel.stories.tsx b/src/components/TipsCarousel.stories.tsx
index fa1988f31..148133b60 100644
--- a/src/components/TipsCarousel.stories.tsx
+++ b/src/components/TipsCarousel.stories.tsx
@@ -1,15 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { TipsCarousel } from "./TipsCarousel";
-import styled from "@emotion/styled";
-
-const DemoContainer = styled.div`
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 20px;
- background: #1e1e1e;
- min-width: 500px;
-`;
const meta = {
title: "Components/TipsCarousel",
@@ -34,85 +24,40 @@ export const Default: Story = {
export const WithExplanation: Story = {
render: () => (
-
-
+
+
Tips rotate automatically based on time. Hover to see the gradient effect:
-
+
Tips change every hour to provide variety and convey UX information.
-
+
),
};
export const DebugControls: Story = {
render: () => (
-
-
- For debugging, you can use:
-
+
+
For debugging, you can use:
-
+
window.setTip(0) // Show first tip
window.setTip(1) // Show second tip
window.clearTip() // Return to auto-rotation
-
+
),
};
export const InContext: Story = {
render: () => {
- const ToolbarMock = styled.div`
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 8px 12px;
- background: #252526;
- border-bottom: 1px solid #3e3e42;
- font-family: var(--font-primary);
- `;
-
- const ToolbarSection = styled.div`
- display: flex;
- align-items: center;
- gap: 8px;
- `;
-
return (
-
-
- Workspace:
- main
-
+
+
+ Workspace:
+ main
+
-
- Mode: Plan
-
-
+
+ Mode: Plan
+
+
);
},
};
diff --git a/src/components/TipsCarousel.tsx b/src/components/TipsCarousel.tsx
index 21392fc0d..8979c551a 100644
--- a/src/components/TipsCarousel.tsx
+++ b/src/components/TipsCarousel.tsx
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from "react";
-import styled from "@emotion/styled";
import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
// Extend window with tip debugging functions
@@ -10,78 +9,29 @@ declare global {
}
}
-const TipsContainer = styled.div`
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 11px;
- color: var(--color-text);
- font-family: var(--font-primary);
- line-height: 20px;
- margin: 3px 0 0 0;
- padding: 4px 8px;
- border-radius: 4px;
- transition: all 0.3s ease;
- cursor: default;
-
- &:hover {
- background: linear-gradient(
- 135deg,
- color-mix(in srgb, var(--color-plan-mode), transparent 85%) 0%,
- color-mix(in srgb, var(--color-exec-mode), transparent 85%) 50%,
- color-mix(in srgb, var(--color-thinking-mode), transparent 85%) 100%
- );
-
- .tip-label,
- .tip-content {
- color: var(--color-text);
- }
-
- .keybind,
- .command {
- color: #fff;
- }
- }
-`;
-
-const TipLabel = styled.span`
- font-weight: 500;
- color: color-mix(in srgb, var(--color-text-secondary), transparent 20%);
- transition: color 0.3s ease;
-`;
-
-const TipContent = styled.span`
- color: var(--color-text-secondary);
- transition: color 0.3s ease;
-`;
-
-const Keybind = styled.span`
- font-family: var(--font-primary);
- color: color-mix(in srgb, var(--color-text), transparent 30%);
- transition: color 0.3s ease;
-`;
-
-const Command = styled.code`
- font-family: var(--font-monospace);
- color: color-mix(in srgb, var(--color-text), transparent 30%);
- transition: color 0.3s ease;
-`;
-
const TIPS = [
{
content: (
<>
Navigate workspaces with{" "}
- {formatKeybind(KEYBINDS.PREV_WORKSPACE)} and{" "}
- {formatKeybind(KEYBINDS.NEXT_WORKSPACE)}
+
+ {formatKeybind(KEYBINDS.PREV_WORKSPACE)}
+ {" "}
+ and{" "}
+
+ {formatKeybind(KEYBINDS.NEXT_WORKSPACE)}
+
>
),
},
{
content: (
<>
- Use /truncate 50 to trim the first 50% of the chat
- from context
+ Use{" "}
+
+ /truncate 50
+ {" "}
+ to trim the first 50% of the chat from context
>
),
},
@@ -121,9 +71,18 @@ export const TipsCarousel: React.FC = () => {
}, []);
return (
-
- Tip:
- {TIPS[currentTipIndex]?.content}
-
+
+
+ Tip:
+
+
+ {TIPS[currentTipIndex]?.content}
+
+
);
};
diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx
index 8435fc23c..5900ba654 100644
--- a/src/components/TitleBar.tsx
+++ b/src/components/TitleBar.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
-import styled from "@emotion/styled";
+import { cn } from "@/lib/utils";
import { VERSION } from "@/version";
import { TooltipWrapper, Tooltip } from "./Tooltip";
import type { UpdateStatus } from "@/types/ipc";
@@ -9,75 +9,13 @@ import { isTelemetryEnabled } from "@/telemetry";
const UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
const UPDATE_CHECK_HOVER_COOLDOWN_MS = 60 * 1000; // 1 minute
-const TitleBarContainer = styled.div`
- padding: 8px 16px;
- background: #1e1e1e;
- border-bottom: 1px solid #3c3c3c;
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-family: var(--font-primary);
- font-size: 11px;
- color: #858585;
- user-select: none;
- flex-shrink: 0;
-`;
-
-const LeftSection = styled.div`
- display: flex;
- align-items: center;
- gap: 8px;
- min-width: 0; /* Allow flex items to shrink */
- margin-right: 16px; /* Ensure space between title and date */
-`;
-
-const TitleText = styled.div`
- font-weight: normal;
- letter-spacing: 0.5px;
- user-select: text;
- cursor: text;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0; /* Allow ellipsis to work in flex container */
-`;
-
-const UpdateIndicator = styled.div<{
- status: "available" | "downloading" | "downloaded" | "disabled";
-}>`
- width: 16px;
- height: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: ${(props) => (props.status === "disabled" ? "default" : "pointer")};
- color: ${(props) => {
- switch (props.status) {
- case "available":
- return "#4CAF50"; // Green for available
- case "downloading":
- return "#2196F3"; // Blue for downloading
- case "downloaded":
- return "#FF9800"; // Orange for ready to install
- case "disabled":
- return "#666666"; // Gray for disabled
- }
- }};
-
- &:hover {
- opacity: ${(props) => (props.status === "disabled" ? "1" : "0.7")};
- }
-`;
-
-const UpdateIcon = styled.span`
- font-size: 14px;
-`;
-
-const BuildInfo = styled.div`
- font-size: 10px;
- opacity: 0.7;
- cursor: default;
-`;
+const updateStatusColors: Record<"available" | "downloading" | "downloaded" | "disabled", string> =
+ {
+ available: "#4CAF50", // Green for available
+ downloading: "#2196F3", // Blue for downloading
+ downloaded: "#FF9800", // Orange for ready to install
+ disabled: "#666666", // Gray for disabled
+ };
interface VersionMetadata {
buildTime: string;
@@ -276,34 +214,42 @@ export function TitleBar() {
const showUpdateIndicator = true;
return (
-
-
+
+
{showUpdateIndicator && (
-
-
+
{indicatorStatus === "disabled"
? "⊘"
: indicatorStatus === "downloading"
? "⟳"
: "↓"}
-
-
+
+
{getUpdateTooltip()}
)}
-
cmux {gitDescribe ?? "(dev)"}
-
+
+ cmux {gitDescribe ?? "(dev)"}
+
+
- {buildDate}
+ {buildDate}
Built at {extendedTimestamp}
-
+
);
}
diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx
index 941fe4f89..479670170 100644
--- a/src/components/TodoList.tsx
+++ b/src/components/TodoList.tsx
@@ -1,60 +1,24 @@
import React from "react";
-import styled from "@emotion/styled";
+import { cn } from "@/lib/utils";
import type { TodoItem } from "@/types/tools";
-const TodoListContainer = styled.div`
- display: flex;
- flex-direction: column;
- gap: 3px;
- padding: 6px 8px;
-`;
-
-const TodoItemContainer = styled.div<{ status: TodoItem["status"] }>`
- display: flex;
- align-items: flex-start;
- gap: 6px;
- padding: 4px 8px;
- background: ${(props) => {
- switch (props.status) {
- case "completed":
- return "color-mix(in srgb, #4caf50, transparent 92%)";
- case "in_progress":
- return "color-mix(in srgb, #2196f3, transparent 92%)";
- case "pending":
- default:
- return "color-mix(in srgb, #888, transparent 96%)";
- }
- }};
- border-left: 2px solid
- ${(props) => {
- switch (props.status) {
- case "completed":
- return "#4caf50";
- case "in_progress":
- return "#2196f3";
- case "pending":
- default:
- return "#666";
- }
- }};
- border-radius: 3px;
- font-family: var(--font-monospace);
- font-size: 11px;
- line-height: 1.35;
- color: var(--color-text);
-`;
+const statusBgColors: Record = {
+ completed: "color-mix(in srgb, #4caf50, transparent 92%)",
+ in_progress: "color-mix(in srgb, #2196f3, transparent 92%)",
+ pending: "color-mix(in srgb, #888, transparent 96%)",
+};
-const TodoIcon = styled.div`
- font-size: 12px;
- flex-shrink: 0;
- margin-top: 1px;
- opacity: 0.8;
-`;
+const statusBorderColors: Record = {
+ completed: "#4caf50",
+ in_progress: "#2196f3",
+ pending: "#666",
+};
-const TodoContent = styled.div`
- flex: 1;
- min-width: 0;
-`;
+const statusTextColors: Record = {
+ completed: "#888",
+ in_progress: "#2196f3",
+ pending: "var(--color-text)",
+};
/**
* Calculate opacity fade for items distant from the center (exponential decay).
@@ -66,81 +30,40 @@ function calculateFadeOpacity(distance: number, minOpacity: number): number {
return Math.max(minOpacity, 1 - distance * 0.15);
}
-const TodoText = styled.div<{
- status: TodoItem["status"];
- completedIndex?: number;
- totalCompleted?: number;
- pendingIndex?: number;
- totalPending?: number;
-}>`
- color: ${(props) => {
- switch (props.status) {
- case "completed":
- return "#888";
- case "in_progress":
- return "#2196f3";
- default:
- return "var(--color-text)";
- }
- }};
- text-decoration: ${(props) => (props.status === "completed" ? "line-through" : "none")};
- opacity: ${(props) => {
- if (props.status === "completed") {
- // Apply gradient fade for old completed items (distant past)
- if (
- props.completedIndex !== undefined &&
- props.totalCompleted !== undefined &&
- props.totalCompleted > 2 &&
- props.completedIndex < props.totalCompleted - 2
- ) {
- const distance = props.totalCompleted - props.completedIndex;
- return calculateFadeOpacity(distance, 0.35);
- }
- return "0.7";
+function calculateTextOpacity(
+ status: TodoItem["status"],
+ completedIndex?: number,
+ totalCompleted?: number,
+ pendingIndex?: number,
+ totalPending?: number
+): number {
+ if (status === "completed") {
+ // Apply gradient fade for old completed items (distant past)
+ if (
+ completedIndex !== undefined &&
+ totalCompleted !== undefined &&
+ totalCompleted > 2 &&
+ completedIndex < totalCompleted - 2
+ ) {
+ const distance = totalCompleted - completedIndex;
+ return calculateFadeOpacity(distance, 0.35);
}
- if (props.status === "pending") {
- // Apply gradient fade for far future pending items (distant future)
- if (
- props.pendingIndex !== undefined &&
- props.totalPending !== undefined &&
- props.totalPending > 2 &&
- props.pendingIndex > 1
- ) {
- const distance = props.pendingIndex - 1;
- return calculateFadeOpacity(distance, 0.5);
- }
- }
- return "1";
- }};
- font-weight: ${(props) => (props.status === "in_progress" ? "500" : "normal")};
- white-space: nowrap;
-
- ${(props) =>
- props.status === "in_progress" &&
- `
- &::after {
- content: "...";
- display: inline;
- overflow: hidden;
- animation: ellipsis 1.5s steps(4, end) infinite;
- }
-
- @keyframes ellipsis {
- 0% {
- content: "";
- }
- 25% {
- content: ".";
- }
- 50% {
- content: "..";
- }
- 75% {
- content: "...";
- }
+ return 0.7;
+ }
+ if (status === "pending") {
+ // Apply gradient fade for far future pending items (distant future)
+ if (
+ pendingIndex !== undefined &&
+ totalPending !== undefined &&
+ totalPending > 2 &&
+ pendingIndex > 1
+ ) {
+ const distance = pendingIndex - 1;
+ return calculateFadeOpacity(distance, 0.5);
}
- `}
-`;
+ }
+ return 1;
+}
interface TodoListProps {
todos: TodoItem[];
@@ -171,28 +94,51 @@ export const TodoList: React.FC = ({ todos }) => {
let pendingIndex = 0;
return (
-
+
{todos.map((todo, index) => {
const currentCompletedIndex = todo.status === "completed" ? completedIndex++ : undefined;
const currentPendingIndex = todo.status === "pending" ? pendingIndex++ : undefined;
+ const textOpacity = calculateTextOpacity(
+ todo.status,
+ currentCompletedIndex,
+ completedCount,
+ currentPendingIndex,
+ pendingCount
+ );
+
return (
-
- {getStatusIcon(todo.status)}
-
-
+
+ {getStatusIcon(todo.status)}
+
+
+
{todo.content}
-
-
-
+
+
+
);
})}
-
+
);
};
diff --git a/src/components/ToggleGroup.stories.tsx b/src/components/ToggleGroup.stories.tsx
index 350b832c9..b55c170b1 100644
--- a/src/components/ToggleGroup.stories.tsx
+++ b/src/components/ToggleGroup.stories.tsx
@@ -4,14 +4,7 @@ import { expect, userEvent, within, waitFor } from "@storybook/test";
import { useArgs } from "@storybook/preview-api";
import { ToggleGroup, type ToggleOption } from "./ToggleGroup";
import { useState } from "react";
-import styled from "@emotion/styled";
-
-const DemoContainer = styled.div`
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 20px;
-`;
+import { cn } from "@/lib/utils";
const meta = {
title: "Components/ToggleGroup",
@@ -172,40 +165,25 @@ export const ManyOptions: Story = {
},
};
-const StyledModeToggle = styled.div<{ mode: "exec" | "plan" }>`
- display: flex;
- gap: 0;
- background: var(--color-toggle-bg);
- border-radius: 4px;
-
- button {
- &:first-of-type {
- ${(props) =>
- props.mode === "exec" &&
- `
- background: var(--color-exec-mode);
- color: white;
-
- &:hover {
- background: var(--color-exec-mode-hover);
- }
- `}
- }
-
- &:last-of-type {
- ${(props) =>
- props.mode === "plan" &&
- `
- background: var(--color-plan-mode);
- color: white;
-
- &:hover {
- background: var(--color-plan-mode-hover);
- }
- `}
- }
- }
-`;
+const StyledModeToggle = ({
+ mode,
+ children,
+}: {
+ mode: "exec" | "plan";
+ children: React.ReactNode;
+}) => (
+
+ {children}
+
+);
export const PermissionModes: Story = {
args: {
@@ -276,22 +254,16 @@ export const WithStateDisplay: Story = {
const [{ value }, updateArgs] = useArgs
();
return (
-
+
updateArgs({ value: newValue })}
/>
-
- Current selection:
{value}
+
+ Current selection: {value}
-
+
);
},
};
@@ -342,49 +314,22 @@ export const MultipleGroups: Story = {
};
return (
-
+
);
},
};
diff --git a/src/components/ToggleGroup.tsx b/src/components/ToggleGroup.tsx
index 36bd15b85..0cebe8476 100644
--- a/src/components/ToggleGroup.tsx
+++ b/src/components/ToggleGroup.tsx
@@ -1,36 +1,9 @@
-import styled from "@emotion/styled";
-
-const ToggleContainer = styled.div`
- display: flex;
- gap: 0;
- background: var(--color-toggle-bg);
- border-radius: 4px;
-`;
-
-const ToggleButton = styled.button<{ active: boolean }>`
- padding: 4px 8px;
- font-size: 11px;
- font-family: var(--font-primary);
- color: ${(props) =>
- props.active ? "var(--color-toggle-text-active)" : "var(--color-toggle-text)"};
- background: ${(props) => (props.active ? "var(--color-toggle-active)" : "transparent")};
- border: none;
- border-radius: 3px;
- cursor: pointer;
- transition: all 0.15s ease;
- font-weight: ${(props) => (props.active ? "500" : "400")};
-
- &:hover {
- color: ${(props) =>
- props.active ? "var(--color-toggle-text-active)" : "var(--color-toggle-text-hover)"};
- background: ${(props) =>
- props.active ? "var(--color-toggle-active)" : "var(--color-toggle-hover)"};
- }
-`;
+import { cn } from "@/lib/utils";
export interface ToggleOption {
value: T;
label: string;
+ activeClassName?: string;
}
interface ToggleGroupProps {
@@ -41,18 +14,27 @@ interface ToggleGroupProps {
export function ToggleGroup({ options, value, onChange }: ToggleGroupProps) {
return (
-
- {options.map((option) => (
- onChange(option.value)}
- aria-pressed={value === option.value}
- type="button"
- >
- {option.label}
-
- ))}
-
+
+ {options.map((option) => {
+ const isActive = value === option.value;
+ return (
+
+ );
+ })}
+
);
}
diff --git a/src/components/Tooltip.stories.tsx b/src/components/Tooltip.stories.tsx
index 1f34336ba..e7dd732a4 100644
--- a/src/components/Tooltip.stories.tsx
+++ b/src/components/Tooltip.stories.tsx
@@ -1,29 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within, waitFor } from "@storybook/test";
import { TooltipWrapper, Tooltip, HelpIndicator } from "./Tooltip";
-import styled from "@emotion/styled";
-
-const DemoButton = styled.button`
- padding: 8px 16px;
- background: #0e639c;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-family: var(--font-primary);
- font-size: 13px;
-
- &:hover {
- background: #1177bb;
- }
-`;
-
-const DemoContainer = styled.div`
- display: flex;
- gap: 20px;
- padding: 40px;
- flex-wrap: wrap;
-`;
const meta = {
title: "Components/Tooltip",
@@ -41,7 +18,9 @@ export const BasicTooltip: Story = {
args: { children: "This is a helpful tooltip" },
render: () => (
- Hover me
+
This is a helpful tooltip
),
@@ -85,39 +64,49 @@ export const BasicTooltip: Story = {
export const TooltipPositions: Story = {
args: { children: "Tooltip content" },
render: () => (
-
+
- Top (default)
+
Tooltip appears above
- Bottom
+
Tooltip appears below
-
+
),
};
export const TooltipAlignments: Story = {
args: { children: "Tooltip content" },
render: () => (
-
+
- Left Aligned
+
Left-aligned tooltip
- Center Aligned
+
Center-aligned tooltip
- Right Aligned
+
Right-aligned tooltip
-
+
),
};
@@ -125,7 +114,9 @@ export const WideTooltip: Story = {
args: { children: "Tooltip content" },
render: () => (
- Hover for detailed info
+
This is a wider tooltip that can contain more detailed information. It will wrap text
automatically and has a maximum width of 300px.
@@ -170,7 +161,9 @@ export const KeyboardShortcut: Story = {
args: { children: "Tooltip content" },
render: () => (
- Save File
+
Save File ⌘S
@@ -182,7 +175,9 @@ export const LongContent: Story = {
args: { children: "Tooltip content" },
render: () => (
- Documentation
+
Getting Started:
diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx
index e3e15b64c..5a64ee8fa 100644
--- a/src/components/Tooltip.tsx
+++ b/src/components/Tooltip.tsx
@@ -1,6 +1,6 @@
import React, { useState, useRef, useLayoutEffect, createContext, useContext } from "react";
import { createPortal } from "react-dom";
-import styled from "@emotion/styled";
+import { cn } from "@/lib/utils";
// Context for passing hover state and trigger ref from wrapper to tooltip
interface TooltipContextValue {
@@ -44,23 +44,18 @@ export const TooltipWrapper: React.FC = ({ inline = false,
return (
-
{children}
-
+
);
};
-const StyledWrapper = styled.span<{ inline: boolean }>`
- position: relative;
- display: ${(props) => (props.inline ? "inline-block" : "block")};
-`;
-
// Tooltip - Portal-based component with collision detection
interface TooltipProps {
align?: "left" | "center" | "right";
@@ -213,7 +208,7 @@ export const Tooltip: React.FC = ({
}
return createPortal(
- = ({
visibility: tooltipState.isPositioned ? "visible" : "hidden",
opacity: tooltipState.isPositioned ? 1 : 0,
}}
- width={width}
- className={className}
- interactive={interactive}
+ className={cn(
+ "bg-[#2d2d30] text-[#cccccc] text-left rounded px-[10px] py-[6px] z-[9999]",
+ "text-[11px] font-normal font-sans border border-[#464647] shadow-[0_2px_8px_rgba(0,0,0,0.4)]",
+ width === "wide" ? "whitespace-normal max-w-[300px] w-max" : "whitespace-nowrap",
+ interactive ? "pointer-events-auto" : "pointer-events-none",
+ className
+ )}
onMouseEnter={handleTooltipMouseEnter}
onMouseLeave={handleTooltipMouseLeave}
>
{children}
-
- ,
+
+ ,
document.body
);
};
-const StyledTooltip = styled.div<{ width: string; interactive: boolean }>`
- background-color: #2d2d30;
- color: #cccccc;
- text-align: left;
- border-radius: 4px;
- padding: 6px 10px;
- z-index: 9999;
- white-space: ${(props) => (props.width === "wide" ? "normal" : "nowrap")};
- ${(props) =>
- props.width === "wide" && "max-width: min(300px, calc(100vw - 40px)); width: max-content;"}
- font-size: 11px;
- font-weight: normal;
- font-family: var(--font-primary);
- border: 1px solid #464647;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
- pointer-events: ${(props) => (props.interactive ? "auto" : "none")};
- /* No default visibility/opacity - controlled via inline styles */
-
- a {
- color: #4ec9b0;
- text-decoration: underline;
- cursor: pointer;
-
- &:hover {
- color: #6fd9c0;
- }
- }
-`;
-
-const Arrow = styled.div`
- content: "";
- position: absolute;
- border-width: 5px;
- border-style: solid;
- transform: translateX(-50%);
-`;
-
-export const HelpIndicator = styled.span`
- color: #666666;
- font-size: 7px;
- cursor: help;
- display: inline-block;
- vertical-align: baseline;
- border: 1px solid #666666;
- border-radius: 50%;
- width: 10px;
- height: 10px;
- line-height: 8px;
- text-align: center;
- font-weight: bold;
- margin-bottom: 2px;
-`;
+export const HelpIndicator: React.FC<{ className?: string; children?: React.ReactNode }> = ({
+ className,
+ children,
+}) => (
+
+ {children}
+
+);
diff --git a/src/components/VimTextArea.tsx b/src/components/VimTextArea.tsx
index 582567fc4..1dfd9246b 100644
--- a/src/components/VimTextArea.tsx
+++ b/src/components/VimTextArea.tsx
@@ -1,9 +1,9 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
-import styled from "@emotion/styled";
import type { UIMode } from "@/types/mode";
import * as vim from "@/utils/vim";
import { TooltipWrapper, Tooltip, HelpIndicator } from "./Tooltip";
import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
+import { cn } from "@/lib/utils";
/**
* VimTextArea – minimal Vim-like editing for a textarea.
@@ -31,87 +31,6 @@ export interface VimTextAreaProps
suppressKeys?: string[]; // keys for which Vim should not interfere (e.g. ["Tab","ArrowUp","ArrowDown","Escape"]) when popovers are open
}
-const StyledTextArea = styled.textarea<{
- isEditing?: boolean;
- mode: UIMode;
- vimMode: VimMode;
-}>`
- width: 100%;
- background: ${(props) => (props.isEditing ? "var(--color-editing-mode-alpha)" : "#1e1e1e")};
- border: 1px solid ${(props) => (props.isEditing ? "var(--color-editing-mode)" : "#3e3e42")};
- color: #d4d4d4;
- padding: 6px 8px;
- border-radius: 4px;
- font-family: inherit;
- font-size: 13px;
- resize: none;
- min-height: 32px;
- max-height: 50vh;
- overflow-y: auto;
- caret-color: ${(props) => (props.vimMode === "normal" ? "transparent" : "#ffffff")};
-
- &:focus {
- outline: none;
- border-color: ${(props) =>
- props.isEditing
- ? "var(--color-editing-mode)"
- : props.mode === "plan"
- ? "var(--color-plan-mode)"
- : "var(--color-exec-mode)"};
- }
-
- &::placeholder {
- color: #6b6b6b;
- }
-
- /* Solid block cursor in normal mode (no blinking) */
- &::selection {
- background-color: ${(props) =>
- props.vimMode === "normal" ? "rgba(255, 255, 255, 0.5)" : "rgba(51, 153, 255, 0.5)"};
- }
-`;
-
-const ModeIndicator = styled.div`
- font-size: 9px;
- color: rgba(212, 212, 212, 0.6);
- letter-spacing: 0.8px;
- user-select: none;
- height: 11px; /* Fixed height to prevent border bump */
- line-height: 11px;
- margin-bottom: 1px; /* Minimal spacing between indicator and textarea */
- display: flex;
- align-items: center;
- justify-content: space-between; /* Space between left (vim mode) and right (focus hint) */
- gap: 4px;
-`;
-
-const ModeLeftSection = styled.div`
- display: flex;
- align-items: center;
- gap: 4px;
-`;
-
-const ModeRightSection = styled.div`
- display: flex;
- align-items: center;
- gap: 4px;
- margin-left: auto;
-`;
-
-const ModeText = styled.span`
- text-transform: uppercase; /* Only uppercase the mode name, not commands */
-`;
-
-const EmptyCursor = styled.div`
- position: absolute;
- width: 8px;
- height: 16px;
- background-color: rgba(255, 255, 255, 0.5);
- pointer-events: none;
- left: 8px;
- top: 6px;
-`;
-
type VimMode = vim.VimMode;
export const VimTextArea = React.forwardRef(
@@ -235,8 +154,11 @@ export const VimTextArea = React.forwardRef
-
-
+
+
{showVimMode && (
<>
@@ -262,35 +184,46 @@ export const VimTextArea = React.forwardRef
- normal
+ normal
{pendingCommand && {pendingCommand}}
>
)}
-
+
{showFocusHint && (
-
+
{formatKeybind(KEYBINDS.FOCUS_CHAT)} to focus
-
+
)}
-
+
-
onChange(e.target.value)}
onKeyDown={handleKeyDownInternal}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
- isEditing={isEditing}
- mode={mode}
- vimMode={vimMode}
spellCheck={false}
autoCorrect="off"
autoCapitalize="none"
autoComplete="off"
{...rest}
+ className={cn(
+ "w-full border text-[#d4d4d4] py-1.5 px-2 rounded font-mono text-[13px] resize-none min-h-8 max-h-[50vh] overflow-y-auto",
+ "placeholder:text-[#6b6b6b]",
+ "focus:outline-none",
+ isEditing
+ ? "bg-[var(--color-editing-mode-alpha)] border-[var(--color-editing-mode)] focus:border-[var(--color-editing-mode)]"
+ : "bg-[#1e1e1e] border-[#3e3e42]",
+ !isEditing && (mode === "plan" ? "focus:border-plan-mode" : "focus:border-exec-mode"),
+ vimMode === "normal"
+ ? "caret-transparent selection:bg-white/50"
+ : "caret-white selection:bg-[rgba(51,153,255,0.5)]"
+ )}
/>
- {vimMode === "normal" && value.length === 0 && }
+ {vimMode === "normal" && value.length === 0 && (
+
+ )}
);
diff --git a/src/components/WorkspaceListItem.tsx b/src/components/WorkspaceListItem.tsx
index 53e9b753e..87abdf12c 100644
--- a/src/components/WorkspaceListItem.tsx
+++ b/src/components/WorkspaceListItem.tsx
@@ -1,6 +1,4 @@
import React, { useState, useCallback, useMemo } from "react";
-import styled from "@emotion/styled";
-import { css } from "@emotion/react";
import type { FrontendWorkspaceMetadata } from "@/types/workspace";
import { useWorkspaceSidebarState } from "@/stores/WorkspaceStore";
import { useGitStatus } from "@/stores/GitStatusStore";
@@ -10,115 +8,7 @@ import { GitStatusIndicator } from "./GitStatusIndicator";
import { ModelDisplay } from "./Messages/ModelDisplay";
import { StatusIndicator } from "./StatusIndicator";
import { useRename } from "@/contexts/WorkspaceRenameContext";
-
-// Styled Components
-const WorkspaceStatusIndicator = styled(StatusIndicator)`
- margin-left: 8px;
-`;
-
-const WorkspaceItem = styled.div<{ selected?: boolean }>`
- padding: 6px 12px 6px 28px;
- cursor: pointer;
- display: grid;
- grid-template-columns: auto auto 1fr auto;
- gap: 8px;
- align-items: center;
- border-left: 3px solid transparent;
- transition: all 0.15s;
- font-size: 13px;
- position: relative;
-
- ${(props) =>
- props.selected &&
- css`
- background: #2a2a2b;
- border-left-color: #569cd6;
- `}
-
- &:hover {
- background: #2a2a2b;
-
- button {
- opacity: 1;
- }
- }
-`;
-
-const WorkspaceName = styled.span`
- color: #ccc;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- cursor: pointer;
- padding: 2px 4px;
- border-radius: 3px;
- transition: background 0.2s;
- min-width: 0; /* Allow grid item to shrink below content size */
- text-align: right;
-
- &:hover {
- background: rgba(255, 255, 255, 0.05);
- }
-`;
-
-const WorkspaceNameInput = styled.input`
- background: var(--color-input-bg);
- color: var(--color-input-text);
- border: 1px solid var(--color-input-border);
- border-radius: 3px;
- padding: 2px 4px;
- font-size: 13px;
- font-family: inherit;
- outline: none;
- min-width: 0; /* Allow grid item to shrink */
- text-align: right;
-
- &:focus {
- border-color: var(--color-input-border-focus);
- }
-`;
-
-const WorkspaceErrorContainer = styled.div`
- position: absolute;
- top: 100%;
- left: 28px;
- right: 32px;
- margin-top: 4px;
- padding: 6px 8px;
- background: var(--color-error-bg);
- border: 1px solid var(--color-error);
- border-radius: 3px;
- color: var(--color-error);
- font-size: 12px;
- z-index: 10;
-`;
-
-const RemoveBtn = styled.button`
- opacity: 0;
- background: transparent;
- color: #888;
- border: none;
- cursor: pointer;
- font-size: 16px;
- padding: 0;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- flex-shrink: 0;
-
- &:hover {
- color: #ccc;
- background: rgba(255, 255, 255, 0.1);
- border-radius: 3px;
- }
-`;
-
-const WorkspaceRemoveBtn = styled(RemoveBtn)`
- grid-column: 1;
-`;
+import { cn } from "@/lib/utils";
export interface WorkspaceSelection {
projectPath: string;
@@ -240,8 +130,11 @@ const WorkspaceListItemInner: React.FC = ({
return (
-
onSelectWorkspace({
projectPath,
@@ -268,7 +161,8 @@ const WorkspaceListItemInner: React.FC = ({
data-workspace-id={workspaceId}
>
- {
e.stopPropagation();
void onRemoveWorkspace(workspaceId, e.currentTarget);
@@ -277,7 +171,7 @@ const WorkspaceListItemInner: React.FC = ({
data-workspace-id={workspaceId}
>
×
-
+
Remove workspace
@@ -288,7 +182,8 @@ const WorkspaceListItemInner: React.FC = ({
tooltipPosition="right"
/>
{isEditing ? (
- setEditingName(e.target.value)}
onKeyDown={handleRenameKeyDown}
@@ -299,7 +194,8 @@ const WorkspaceListItemInner: React.FC = ({
data-workspace-id={workspaceId}
/>
) : (
- {
e.stopPropagation();
startRenaming();
@@ -307,16 +203,21 @@ const WorkspaceListItemInner: React.FC = ({
title="Double-click to rename"
>
{displayName}
-
+
)}
-
-
- {renameError && isEditing && {renameError}}
+
+ {renameError && isEditing && (
+
+ {renameError}
+
+ )}
);
};
diff --git a/src/components/shared/DiffRenderer.tsx b/src/components/shared/DiffRenderer.tsx
index 864f710f8..48cb7e0c1 100644
--- a/src/components/shared/DiffRenderer.tsx
+++ b/src/components/shared/DiffRenderer.tsx
@@ -5,7 +5,7 @@
*/
import React, { useEffect, useState } from "react";
-import styled from "@emotion/styled";
+import { cn } from "@/lib/utils";
import { getLanguageFromPath } from "@/utils/git/languageDetector";
import { Tooltip, TooltipWrapper } from "../Tooltip";
import { groupDiffLines } from "@/utils/highlighting/diffChunking";
@@ -18,125 +18,77 @@ import {
// Shared type for diff line types
export type DiffLineType = "add" | "remove" | "context" | "header";
+// Helper function for getting diff line background color
+const getDiffLineBackground = (type: DiffLineType): string => {
+ switch (type) {
+ case "add":
+ return "rgba(46, 160, 67, 0.15)";
+ case "remove":
+ return "rgba(248, 81, 73, 0.15)";
+ default:
+ return "transparent";
+ }
+};
+
+// Helper function for getting diff line text color
+const getDiffLineColor = (type: DiffLineType): string => {
+ switch (type) {
+ case "add":
+ return "#4caf50";
+ case "remove":
+ return "#f44336";
+ case "header":
+ return "#2196f3";
+ case "context":
+ default:
+ return "var(--color-text)";
+ }
+};
+
+// Helper function for getting line content color
+const getLineContentColor = (type: DiffLineType): string => {
+ switch (type) {
+ case "header":
+ return "#2196f3";
+ case "context":
+ return "var(--color-text-secondary)";
+ case "add":
+ case "remove":
+ return "var(--color-text)";
+ }
+};
+
// Helper function for computing contrast color for add/remove indicators
-const getContrastColor = (type: DiffLineType) => {
+const getContrastColor = (type: DiffLineType): string => {
return type === "add" || type === "remove"
? "color-mix(in srgb, var(--color-text-secondary), white 50%)"
: "var(--color-text-secondary)";
};
/**
- * Wrapper for diff lines - works with CSS Grid parent to ensure uniform widths
- *
- * Problem: Lines of varying length created jagged backgrounds during horizontal scroll
- * because each wrapper was only as wide as its content.
- *
- * Solution: Parent container uses CSS Grid, which automatically makes all grid items
- * (these wrappers) the same width as the widest item. This ensures backgrounds span
- * the full scrollable area without creating infinite scroll.
- *
- * Key insight: width: 100% makes each wrapper span the full grid column width,
- * which CSS Grid automatically sets to the widest line's content.
+ * Container component for diff rendering - exported for custom diff displays
+ * Used by FileEditToolCall for wrapping custom diff content
*/
-export const DiffLineWrapper = styled.div<{ type: DiffLineType }>`
- display: block;
- width: 100%; /* Span full grid column (width of longest line) */
-
- background: ${({ type }) => {
- switch (type) {
- case "add":
- return "rgba(46, 160, 67, 0.15)";
- case "remove":
- return "rgba(248, 81, 73, 0.15)";
- default:
- return "transparent";
- }
- }};
-`;
-
-export const DiffLine = styled.div<{ type: DiffLineType }>`
- font-family: var(--font-monospace);
- white-space: pre;
- display: flex;
- padding: ${({ type }) => (type === "header" ? "4px 8px" : "0 8px")};
- color: ${({ type }) => {
- switch (type) {
- case "add":
- return "#4caf50";
- case "remove":
- return "#f44336";
- case "header":
- return "#2196f3";
- case "context":
- default:
- return "var(--color-text)";
- }
- }};
-`;
-
-export const LineNumber = styled.span<{ type: DiffLineType }>`
- display: flex;
- align-items: center;
- justify-content: flex-end;
- min-width: 35px;
- padding-right: 4px;
- font-weight: ${({ type }) => (type === "header" ? "bold" : "normal")};
- color: ${({ type }) => getContrastColor(type)};
- opacity: ${({ type }) => (type === "add" || type === "remove" ? 0.9 : 0.6)};
- user-select: none;
- flex-shrink: 0;
-`;
-
-export const LineContent = styled.span<{ type: DiffLineType }>`
- padding-left: 8px;
- color: ${({ type }) => {
- switch (type) {
- case "header":
- return "#2196f3";
- case "context":
- return "var(--color-text-secondary)";
- case "add":
- case "remove":
- return "var(--color-text)";
- }
- }};
-
- /* Ensure Shiki spans don't interfere with diff backgrounds */
- /* Exclude search-highlight to allow search marking to show */
- span:not(.search-highlight) {
- background: transparent !important;
- }
-`;
-
-export const DiffIndicator = styled.span<{ type: DiffLineType }>`
- display: inline-block;
- width: 4px;
- text-align: center;
- color: ${({ type }) => getContrastColor(type)};
- opacity: ${({ type }) => (type === "add" || type === "remove" ? 0.9 : 0.6)};
- flex-shrink: 0;
-`;
-
-export const DiffContainer = styled.div<{ fontSize?: string; maxHeight?: string }>`
- margin: 0;
- padding: 6px 0;
- background: var(--color-code-bg);
- border-radius: 3px;
- font-size: ${({ fontSize }) => fontSize ?? "12px"};
- line-height: 1.4;
- max-height: ${({ maxHeight }) => maxHeight ?? "400px"};
- overflow-y: auto;
- overflow-x: auto;
-
- /* CSS Grid ensures all lines span the same width (width of longest line) */
- display: grid;
- grid-template-columns: minmax(min-content, 1fr);
-
- /* Ensure all child elements inherit font size from container */
- * {
- font-size: inherit;
- }
-`;
+export const DiffContainer: React.FC<
+ React.PropsWithChildren<{ fontSize?: string; maxHeight?: string; className?: string }>
+> = ({ children, fontSize, maxHeight, className }) => {
+ return (
+
+ {children}
+
+ );
+};
interface DiffRendererProps {
/** Raw diff content with +/- prefixes */
@@ -226,31 +178,75 @@ export const DiffRenderer: React.FC
= ({
// Show loading state while highlighting
if (!highlightedChunks) {
return (
-
+
);
}
return (
-
+
{highlightedChunks.flatMap((chunk) =>
chunk.lines.map((line) => {
const indicator = chunk.type === "add" ? "+" : chunk.type === "remove" ? "-" : " ";
return (
-
-
- {indicator}
- {showLineNumbers && {line.lineNumber}}
-
+
+
+
+ {indicator}
+
+ {showLineNumbers && (
+
+ {line.lineNumber}
+
+ )}
+
-
-
-
+
+
+
);
})
)}
-
+
);
};
@@ -273,91 +269,8 @@ interface LineSelection {
endLineNum: number;
}
-const SelectableDiffLineWrapper = styled(DiffLineWrapper)<{
- type: DiffLineType;
- isSelected: boolean;
-}>`
- position: relative;
- cursor: text; /* Allow text selection by default */
-
- ${({ isSelected }) =>
- isSelected &&
- `
- background: hsl(from var(--color-review-accent) h s l / 0.2) !important;
- `}
-`;
-
-// Wrapper for CommentButton tooltip - doesn't interfere with absolute positioning
-const CommentButtonWrapper = styled.span`
- position: absolute;
- left: 4px;
- top: 50%;
- transform: translateY(-50%);
- z-index: 1;
-`;
-
-const CommentButton = styled.button`
- opacity: 0; /* Hidden by default */
- background: var(--color-review-accent);
- border: none;
- border-radius: 2px;
- width: 14px;
- height: 14px;
- padding: 0;
- cursor: pointer;
- transition: opacity 0.15s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- font-weight: bold;
- flex-shrink: 0;
-
- /* Show button on line hover */
- ${SelectableDiffLineWrapper}:hover & {
- opacity: 0.7;
- }
-
- &:hover {
- opacity: 1 !important;
- background: hsl(from var(--color-review-accent) h s calc(l * 1.2));
- }
-
- &:active {
- transform: scale(0.9);
- }
-`;
-
-const InlineNoteContainer = styled.div`
- padding: 6px 8px;
- background: #1e1e1e;
- border-top: 1px solid hsl(from var(--color-review-accent) h s l / 0.3);
- margin: 0;
-`;
-
-const NoteTextarea = styled.textarea`
- width: 100%;
- min-height: calc(12px * 1.4 * 3 + 12px); /* 3 lines + padding */
- padding: 6px 8px;
- font-family: var(--font-monospace);
- font-size: 12px;
- line-height: 1.4;
- background: #1e1e1e;
- border: 1px solid hsl(from var(--color-review-accent) h s l / 0.4);
- border-radius: 2px;
- color: var(--color-text);
- resize: none; /* Disable manual resize since we auto-grow */
- overflow-y: hidden; /* Hide scrollbar during auto-grow */
-
- &:focus {
- outline: none;
- border-color: hsl(from var(--color-review-accent) h s l / 0.6);
- }
-
- &::placeholder {
- color: #888;
- }
-`;
+// CSS class for diff line wrapper - used by arbitrary selector in CommentButton
+const SELECTABLE_DIFF_LINE_CLASS = "selectable-diff-line";
// Separate component to prevent re-rendering diff lines on every keystroke
interface ReviewNoteInputProps {
@@ -422,9 +335,17 @@ const ReviewNoteInput: React.FC = React.memo(
};
return (
-
-
+
+
);
}
);
@@ -558,9 +479,17 @@ export const SelectableDiffRenderer = React.memo