npm package: @knurdz/jack-editor-tab
Description: Reusable React editor tabs extracted from Sonar Code Editor with drag-and-drop, close actions, dirty state, and publish-ready styling
Author: RKK Vishva Kumar
Latest version: 0.1.1

# 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

```bash
npm install @knurdz/jack-editor-tab
```

Peer dependencies:

- `react`
- `react-dom`

## Basic usage

```tsx
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 tabs
- `markEditorTabDirty`: pins the tab as soon as Monaco content changes
- `markEditorTabSaved`: clears dirty state and keeps the tab pinned
- `closeEditorTab`: removes a tab and returns the next active path

This is the intended flow when pairing the package with `@knurdz/jack-file-tree`:

```tsx
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-tree` are opened pinned because that package calls `onFileOpened(..., false)`

## Sonar extraction mapping

The package matches the tab-specific fields from Sonar's `OpenTab` shape, so existing data can be passed through directly:

```ts
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

```tsx
<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 layout
- `dedupePaths`: remove duplicate tabs by normalized path before rendering
- `showDirtyIndicator`: toggle the unsaved-changes dot
- `showCloseButtons`: toggle close buttons entirely
- `closeButtonVisibility`: `"hover"`, `"active"`, or `"always"`
- `renderIcon`: replace the default icon mapping
- `getDisplayName`: customize duplicate-name disambiguation
- `renderTab`: replace the full tab markup while keeping built-in handlers
- `renderEmptyState`: 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:

- `DefaultEditorTabIcon`
- `openEditorTab`
- `markEditorTabDirty`
- `markEditorTabSaved`
- `pinEditorTab`
- `closeEditorTab`
- `replacePreviewEditorTabs`
- `normalizeEditorTabPath`
- `dedupeEditorTabs`
- `reorderEditorTabs`
- `countEditorTabNames`
- `getEditorTabDisplayName`

## Publishing checklist

1. Move the `Jack-Editor-Tab/` folder into its own repository.
2. Update the package `name`, `repository`, and `author` fields if needed.
3. Run `npm install`.
4. Run `npm run build`.
5. Run `npm pack --dry-run` to verify the tarball contents.
6. Publish with `npm publish`.