Skip to content

Commit c68561e

Browse files
committed
feat(editor): add tab bar system components
Create 5 new components for the editor tab bar system: - ConfirmDialog (src/components/ui/ConfirmDialog.tsx): Modal dialog using CortexModal for dirty file close confirmation with Save, Don't Save, and Cancel buttons. - EditorTab (src/components/editor/EditorTab.tsx): Individual tab component with file icon, name (with parent dir disambiguation), dirty indicator, close button, drag-and-drop support, and preview tab styling (italic text). - EditorTabs (src/components/editor/EditorTabs.tsx): Tab strip container with horizontal scroll on overflow, double-click empty area for new file, drag-and-drop reordering with visual insertion indicator, context menu via ContextMenuPresets.tabItems, duplicate filename detection, and new tab button. - WelcomeTab (src/components/editor/WelcomeTab.tsx): Welcome screen shown when no files are open with Cortex IDE branding and keyboard shortcut hints (Ctrl+P, Ctrl+N, Ctrl+O, Ctrl+Shift+P). - EditorArea (src/components/editor/EditorArea.tsx): Container that renders EditorTabs at top, shows WelcomeTab when empty, renders children for active file content, and integrates ConfirmDialog for dirty file close confirmation. All components integrate with the existing useEditor() context and use CortexTokens for styling. Exports added to editor/index.ts and ui/index.ts. Components follow existing conventions: SolidJS functional components, inline styles with JSX.CSSProperties, CortexTokens CSS variable references, and the project's import patterns (@/context/EditorContext, @/components/ui, @/design-system/tokens/cortex-tokens). TypeScript compilation passes and existing TabBar tests (49) pass.
1 parent dc22123 commit c68561e

File tree

7 files changed

+926
-0
lines changed

7 files changed

+926
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* EditorArea - Main container for the editor tab bar + content area
3+
*
4+
* Renders EditorTabs at the top and the active file's editor below.
5+
* Shows WelcomeTab when no files are open. Integrates ConfirmDialog
6+
* for dirty file close confirmation.
7+
*
8+
* This component wraps the tab system and content display into a
9+
* single cohesive editor area that can be placed inside EditorPanel
10+
* or used standalone.
11+
*/
12+
13+
import { Show, createSignal, createMemo, type JSX } from "solid-js";
14+
import { useEditor } from "@/context/EditorContext";
15+
import { CortexTokens } from "@/design-system/tokens/cortex-tokens";
16+
import { EditorTabs } from "./EditorTabs";
17+
import { WelcomeTab } from "./WelcomeTab";
18+
import { ConfirmDialog } from "../ui/ConfirmDialog";
19+
20+
export interface EditorAreaProps {
21+
groupId?: string;
22+
children?: JSX.Element;
23+
class?: string;
24+
style?: JSX.CSSProperties;
25+
}
26+
27+
export function EditorArea(props: EditorAreaProps) {
28+
const editor = useEditor();
29+
30+
const [confirmState, setConfirmState] = createSignal<{
31+
open: boolean;
32+
fileId: string;
33+
fileName: string;
34+
}>({ open: false, fileId: "", fileName: "" });
35+
36+
const hasOpenFiles = createMemo(() => editor.state.openFiles.length > 0);
37+
38+
const handleFileClose = (fileId: string) => {
39+
const file = editor.state.openFiles.find((f) => f.id === fileId);
40+
if (!file) return;
41+
42+
if (file.modified) {
43+
setConfirmState({ open: true, fileId: file.id, fileName: file.name });
44+
} else {
45+
editor.closeFile(fileId);
46+
}
47+
};
48+
49+
const handleSave = async () => {
50+
const { fileId } = confirmState();
51+
await editor.saveFile(fileId);
52+
editor.closeFile(fileId);
53+
setConfirmState({ open: false, fileId: "", fileName: "" });
54+
};
55+
56+
const handleDontSave = () => {
57+
const { fileId } = confirmState();
58+
editor.closeFile(fileId);
59+
setConfirmState({ open: false, fileId: "", fileName: "" });
60+
};
61+
62+
const handleCancel = () => {
63+
setConfirmState({ open: false, fileId: "", fileName: "" });
64+
};
65+
66+
const containerStyle = (): JSX.CSSProperties => ({
67+
display: "flex",
68+
"flex-direction": "column",
69+
flex: "1",
70+
"min-height": "0",
71+
overflow: "hidden",
72+
background: CortexTokens.colors.bg.primary,
73+
...props.style,
74+
});
75+
76+
const contentStyle: JSX.CSSProperties = {
77+
display: "flex",
78+
flex: "1",
79+
"min-height": "0",
80+
overflow: "hidden",
81+
};
82+
83+
return (
84+
<div class={props.class} style={containerStyle()}>
85+
<Show when={hasOpenFiles()}>
86+
<EditorTabs
87+
onFileClose={handleFileClose}
88+
groupId={props.groupId}
89+
/>
90+
</Show>
91+
92+
<div style={contentStyle}>
93+
<Show when={hasOpenFiles()} fallback={<WelcomeTab />}>
94+
{props.children}
95+
</Show>
96+
</div>
97+
98+
<ConfirmDialog
99+
open={confirmState().open}
100+
fileName={confirmState().fileName}
101+
onSave={handleSave}
102+
onDontSave={handleDontSave}
103+
onCancel={handleCancel}
104+
/>
105+
</div>
106+
);
107+
}
108+
109+
export default EditorArea;
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* EditorTab - Individual editor tab component
3+
*
4+
* Renders a single tab in the editor tab strip with:
5+
* - File icon (via FileIcon component)
6+
* - File name with optional parent directory for disambiguation
7+
* - Dirty indicator (dot) / close button
8+
* - Drag-and-drop support (HTML5 Drag API)
9+
* - Preview tab styling (italic text)
10+
* - Active/inactive visual states
11+
*
12+
* Styled with CortexTokens to match the Cortex IDE dark theme.
13+
*/
14+
15+
import { createSignal, Show, type JSX } from "solid-js";
16+
import { FileIcon } from "../ui/FileIcon";
17+
import { Icon } from "../ui/Icon";
18+
import { CortexTokens } from "@/design-system/tokens/cortex-tokens";
19+
20+
export interface EditorTabProps {
21+
fileId: string;
22+
fileName: string;
23+
filePath: string;
24+
isActive: boolean;
25+
isDirty: boolean;
26+
isPreview?: boolean;
27+
isPinned?: boolean;
28+
showParentDir?: boolean;
29+
dropPosition?: "left" | "right" | null;
30+
onSelect: () => void;
31+
onClose: (e: MouseEvent) => void;
32+
onMiddleClick: () => void;
33+
onDoubleClick?: () => void;
34+
onContextMenu: (e: MouseEvent) => void;
35+
onDragStart: (e: DragEvent) => void;
36+
onDragEnd: (e: DragEvent) => void;
37+
}
38+
39+
export function EditorTab(props: EditorTabProps) {
40+
const [isHovered, setIsHovered] = createSignal(false);
41+
42+
const parentDir = () => {
43+
if (!props.showParentDir) return null;
44+
const parts = props.filePath.split(/[/\\]/);
45+
return parts.length >= 2 ? parts[parts.length - 2] : null;
46+
};
47+
48+
const handleMouseDown = (e: MouseEvent) => {
49+
if (e.button === 1) {
50+
e.preventDefault();
51+
props.onMiddleClick();
52+
}
53+
};
54+
55+
const tabStyle = (): JSX.CSSProperties => {
56+
const base: JSX.CSSProperties = {
57+
display: "flex",
58+
"align-items": "center",
59+
gap: "6px",
60+
padding: "0 12px",
61+
height: "100%",
62+
cursor: "pointer",
63+
"user-select": "none",
64+
"white-space": "nowrap",
65+
"flex-shrink": "0",
66+
"font-size": "13px",
67+
"font-family": "var(--cortex-font-sans, Inter, system-ui, sans-serif)",
68+
transition: "background 100ms ease, color 100ms ease",
69+
position: "relative",
70+
"border-left": props.dropPosition === "left"
71+
? "2px solid var(--cortex-accent-primary, #B2FF22)"
72+
: "none",
73+
"border-right": props.dropPosition === "right"
74+
? "2px solid var(--cortex-accent-primary, #B2FF22)"
75+
: "none",
76+
};
77+
78+
if (props.isActive) {
79+
base.background = CortexTokens.colors.bg.secondary;
80+
base.color = CortexTokens.colors.text.primary;
81+
base["border-bottom"] = "2px solid var(--cortex-accent-primary, #B2FF22)";
82+
} else {
83+
base.background = isHovered() ? CortexTokens.colors.bg.hover : "transparent";
84+
base.color = CortexTokens.colors.text.secondary;
85+
base["border-bottom"] = "2px solid transparent";
86+
}
87+
88+
return base;
89+
};
90+
91+
const labelStyle = (): JSX.CSSProperties => ({
92+
overflow: "hidden",
93+
"text-overflow": "ellipsis",
94+
"font-style": props.isPreview ? "italic" : "normal",
95+
});
96+
97+
const parentDirStyle: JSX.CSSProperties = {
98+
color: CortexTokens.colors.text.muted,
99+
"font-size": "12px",
100+
"margin-right": "2px",
101+
};
102+
103+
const closeButtonStyle = (): JSX.CSSProperties => ({
104+
display: "flex",
105+
"align-items": "center",
106+
"justify-content": "center",
107+
width: "18px",
108+
height: "18px",
109+
"border-radius": "var(--cortex-radius-xs, 4px)",
110+
border: "none",
111+
background: "transparent",
112+
color: CortexTokens.colors.text.muted,
113+
cursor: "pointer",
114+
padding: "0",
115+
"flex-shrink": "0",
116+
opacity: isHovered() || props.isActive ? "1" : "0",
117+
transition: "opacity 100ms ease, background 100ms ease",
118+
});
119+
120+
const dirtyDotStyle: JSX.CSSProperties = {
121+
width: "8px",
122+
height: "8px",
123+
"border-radius": "var(--cortex-radius-full, 9999px)",
124+
background: CortexTokens.colors.text.secondary,
125+
"flex-shrink": "0",
126+
};
127+
128+
const showDirtyDot = () => props.isDirty && !isHovered() && !props.isActive;
129+
const showCloseBtn = () => !showDirtyDot();
130+
131+
return (
132+
<div
133+
style={tabStyle()}
134+
onClick={props.onSelect}
135+
onDblClick={props.onDoubleClick}
136+
onMouseDown={handleMouseDown}
137+
onContextMenu={props.onContextMenu}
138+
onMouseEnter={() => setIsHovered(true)}
139+
onMouseLeave={() => setIsHovered(false)}
140+
draggable={true}
141+
onDragStart={props.onDragStart}
142+
onDragEnd={props.onDragEnd}
143+
data-tab-id={props.fileId}
144+
role="tab"
145+
aria-selected={props.isActive}
146+
>
147+
<FileIcon filename={props.fileName} size={16} />
148+
149+
<span style={labelStyle()}>
150+
<Show when={parentDir()}>
151+
<span style={parentDirStyle}>{parentDir()}/</span>
152+
</Show>
153+
{props.fileName}
154+
</span>
155+
156+
<Show when={props.isPinned}>
157+
<Icon
158+
name="thumbtack"
159+
size={10}
160+
style={{
161+
color: CortexTokens.colors.text.muted,
162+
"flex-shrink": "0",
163+
}}
164+
/>
165+
</Show>
166+
167+
<Show when={!props.isPinned}>
168+
<Show when={showDirtyDot()}>
169+
<span style={dirtyDotStyle} title="Unsaved changes" />
170+
</Show>
171+
<Show when={showCloseBtn()}>
172+
<button
173+
style={closeButtonStyle()}
174+
onClick={(e) => {
175+
e.stopPropagation();
176+
props.onClose(e);
177+
}}
178+
onMouseEnter={(e) => {
179+
e.currentTarget.style.background = "var(--cortex-interactive-hover, rgba(255,255,255,0.1))";
180+
}}
181+
onMouseLeave={(e) => {
182+
e.currentTarget.style.background = "transparent";
183+
}}
184+
title="Close"
185+
aria-label={`Close ${props.fileName}`}
186+
>
187+
<Show when={props.isDirty && (isHovered() || props.isActive)}>
188+
<span style={dirtyDotStyle} />
189+
</Show>
190+
<Show when={!props.isDirty || (!isHovered() && !props.isActive)}>
191+
<Icon name="xmark" size={12} />
192+
</Show>
193+
</button>
194+
</Show>
195+
</Show>
196+
</div>
197+
);
198+
}
199+
200+
export default EditorTab;

0 commit comments

Comments
 (0)