@knurdz/jack-editor-tab
v0.1.1
Published
Reusable React editor tabs extracted from Sonar Code Editor with drag-and-drop, close actions, dirty state, and publish-ready styling
Maintainers
Readme
Jack Editor Tab
Reusable React editor tabs extracted from Sonar Code Editor. The package ships a tab-strip UI plus state helpers for preview tabs, pinning, dirty tracking, and close behavior so it pairs cleanly with Monaco and @knurdz/jack-file-tree.
Features
- Reusable tab strip with Sonar-inspired defaults
- Preview-tab helpers for Monaco + file-tree workflows
- Drag-and-drop tab reordering
- Close button handling with hover, active-only, or always-visible modes
- Dirty-state indicators and deleted-tab styling
- Duplicate filename disambiguation (
index.ts - components) - Default file-type icon mapping with
lucide-react - Theme tokens and CSS overrides for host applications
- Optional custom render hooks for icons, labels, titles, and full tab rows
- Publish-ready TypeScript + Vite library build with auto-injected styles
Install
npm install @knurdz/jack-editor-tabPeer dependencies:
reactreact-dom
Basic usage
import { EditorTabs, type EditorTabItem } from "@knurdz/jack-editor-tab";
import { useState } from "react";
const tabs: EditorTabItem[] = [
{
path: "/workspace/src/App.tsx",
name: "App.tsx",
isDirty: true,
type: "file",
},
{
path: "/workspace/src/routes/App.tsx",
name: "App.tsx",
type: "file",
},
];
export function TabBar() {
const [activeTabPath, setActiveTabPath] = useState<string | null>(tabs[0].path);
return (
<EditorTabs
tabs={tabs}
activeTabPath={activeTabPath}
onTabClick={(path) => setActiveTabPath(path)}
onTabClose={(path) => {
console.log("close", path);
}}
onTabReorder={(fromIndex, toIndex) => {
console.log("reorder", fromIndex, toIndex);
}}
/>
);
}@knurdz/jack-editor-tab auto-injects its built-in styles when you import the package entry. If you want explicit stylesheet ordering, you can also import @knurdz/jack-editor-tab/styles.css manually.
Preview tabs with Monaco and Jack File Tree
The Sonar-style "italic preview tab" behavior is implemented through the exported state helpers:
openEditorTab: opens a file and replaces older clean preview tabsmarkEditorTabDirty: pins the tab as soon as Monaco content changesmarkEditorTabSaved: clears dirty state and keeps the tab pinnedcloseEditorTab: removes a tab and returns the next active path
This is the intended flow when pairing the package with @knurdz/jack-file-tree:
import { useCallback, useState } from "react";
import { FileTree } from "@knurdz/jack-file-tree";
import {
EditorTabs,
closeEditorTab,
markEditorTabDirty,
markEditorTabSaved,
openEditorTab,
type EditorTabItem,
} from "@knurdz/jack-editor-tab";
interface WorkspaceTab extends EditorTabItem {
content: string;
language: string;
}
function Workspace() {
const [tabs, setTabs] = useState<WorkspaceTab[]>([]);
const [activeTabPath, setActiveTabPath] = useState<string | null>(null);
const openFile = useCallback(async (path: string, name: string, isPreview = true) => {
const content = await window.electronAPI.fs.readFile(path);
setTabs((prev) =>
openEditorTab(
prev,
{
path,
name,
content,
language: "typescript",
isDirty: false,
},
{ isPreview },
),
);
setActiveTabPath(path);
}, []);
return (
<>
<FileTree
fs={window.electronAPI.fs}
workspaceRoot={workspaceRoot}
onOpenFolder={openFolder}
onFileClick={(path, name) => void openFile(path, name, true)}
onFileOpened={(path, name, isPreview) =>
void openFile(path, name, isPreview ?? true)
}
/>
<EditorTabs
tabs={tabs}
activeTabPath={activeTabPath}
onTabClick={(path) => setActiveTabPath(path)}
onTabClose={(path) => {
setTabs((prev) => {
const result = closeEditorTab(prev, path, activeTabPath);
setActiveTabPath(result.activeTabPath);
return result.tabs;
});
}}
/>
<MonacoEditor
path={activeTabPath ?? undefined}
value={tabs.find((tab) => tab.path === activeTabPath)?.content ?? ""}
onChange={(value) => {
if (!activeTabPath) return;
setTabs((prev) =>
markEditorTabDirty(prev, activeTabPath, {
content: value ?? "",
}),
);
}}
/>
</>
);
}Expected behavior:
- Single-click file opens as a preview tab and renders in italics
- Opening another clean preview file replaces the old preview tab
- Typing in Monaco pins the tab immediately and removes the italic style
- Files created from
jack-file-treeare opened pinned because that package callsonFileOpened(..., false)
Sonar extraction mapping
The package matches the tab-specific fields from Sonar's OpenTab shape, so existing data can be passed through directly:
interface OpenTab {
path: string;
name: string;
isDirty: boolean;
type?: "file" | "preview" | "image";
isPreviewFile?: boolean;
isDeleted?: boolean;
}Any extra fields on your host tab objects are ignored by the library.
Customization
<EditorTabs
tabs={tabs}
activeTabPath={activeTabPath}
onTabClick={(path) => setActiveTabPath(path)}
onTabClose={(path) => closeTab(path)}
closeButtonVisibility="always"
minTabWidth={140}
maxTabWidth={240}
theme={{
background: "#050816",
backgroundSecondary: "#0f172a",
backgroundHover: "rgba(56, 189, 248, 0.12)",
accent: "#38bdf8",
borderColor: "rgba(148, 163, 184, 0.18)",
dirtyIndicator: "#f8fafc",
fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif",
}}
renderIcon={(tab) => <MyTabIcon tab={tab} />}
getTabTitle={(tab, displayName) => `${displayName} (${tab.path})`}
/>Useful props:
allowDragReorder: disable built-in drag behavior while keeping the same layoutdedupePaths: remove duplicate tabs by normalized path before renderingshowDirtyIndicator: toggle the unsaved-changes dotshowCloseButtons: toggle close buttons entirelycloseButtonVisibility:"hover","active", or"always"renderIcon: replace the default icon mappinggetDisplayName: customize duplicate-name disambiguationrenderTab: replace the full tab markup while keeping built-in handlersrenderEmptyState: render a host-defined empty placeholder when no tabs exist
Useful CSS variables:
--jet-bg-primary--jet-bg-secondary--jet-bg-hover--jet-bg-active--jet-text-primary--jet-text-secondary--jet-text-muted--jet-accent--jet-border--jet-dirty-indicator--jet-deleted-text--jet-font-family--jet-tab-bar-min-height--jet-tab-height--jet-tab-min-width--jet-tab-max-width--jet-tab-padding-x
Exported helpers
The package also exports a few host-level helpers:
DefaultEditorTabIconopenEditorTabmarkEditorTabDirtymarkEditorTabSavedpinEditorTabcloseEditorTabreplacePreviewEditorTabsnormalizeEditorTabPathdedupeEditorTabsreorderEditorTabscountEditorTabNamesgetEditorTabDisplayName
Publishing checklist
- Move the
Jack-Editor-Tab/folder into its own repository. - Update the package
name,repository, andauthorfields if needed. - Run
npm install. - Run
npm run build. - Run
npm pack --dry-runto verify the tarball contents. - Publish with
npm publish.
