npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rita-workspace

v0.5.58

Published

Multi-drawing workspace feature for Rita (Excalidraw fork)

Readme

Excalidraw/Rita Workspace

npm version npm downloads

npm install rita-workspace

Multi-drawing workspace feature for Excalidraw/Rita (Excalidraw fork based on B310-digital/excalidraw).

Quick Start

Add to your main app file (e.g. App.tsx). For full integration (debounced save, conflict handling, toggle state), see INTEGRATION.md.

import { WorkspaceProvider, useWorkspace, DrawingsDialog } from "rita-workspace";

function App() {
  const [dialogOpen, setDialogOpen] = useState(false);

  return (
    <WorkspaceProvider lang="en">
      <button onClick={() => setDialogOpen(true)}>My Drawings</button>
      <DrawingsDialog
        open={dialogOpen}
        onClose={() => setDialogOpen(false)}
      />
      <ExcalidrawWithWorkspace />
    </WorkspaceProvider>
  );
}

function ExcalidrawWithWorkspace() {
  const { activeDrawing, saveCurrentDrawing } = useWorkspace();

  return (
    <Excalidraw
      initialData={activeDrawing}
      onChange={(elements, appState, files) => {
        saveCurrentDrawing(elements, appState, files);
      }}
    />
  );
}

Features

  • Multiple drawings - Create and manage multiple drawings in one workspace
  • Folders - Organize drawings in folders
  • Auto-save - All drawings saved locally in IndexedDB
  • Multi-tab conflict detection - Prevents data loss when same drawing is open in multiple tabs
  • F5 preserves write ownership - TAB_ID and openedAt persist in sessionStorage across page refresh
  • Cross-tab refresh - Creating/renaming/deleting drawings in one tab auto-refreshes other tabs via BroadcastChannel
  • Workspace toggle - Can be enabled/disabled per browser tab
  • Export/Import - Export workspace as JSON, export all drawings as individual .excalidraw files, import .excalidraw files
  • i18n support - Swedish and English with automatic Excalidraw language sync
  • Optimized loading - DB pre-warming and parallel initialization
  • Smart drawing naming - counts drawings from IndexedDB to avoid duplicate names across tabs

Installation

npm install rita-workspace
# or
yarn add rita-workspace

Integration Guide

📘 For the full host-app integration walkthrough (10 steps including auto-save debounce, switch effect, conflict handling, and replacing Excalidraw's "Open from file"), see docs/INTEGRATION.md.

1. App.tsx - Add Provider

import { WorkspaceProvider, useWorkspace, DrawingsDialog } from "rita-workspace";

const ExcalidrawApp = () => (
  <WorkspaceProvider lang="sv">
    <ExcalidrawWrapper />
  </WorkspaceProvider>
);

2. Use workspace in your component

const ExcalidrawWrapper = () => {
  const {
    activeDrawing,
    saveCurrentDrawing,
    saveDrawingById,
    isDrawingConflict,
  } = useWorkspace();

  // Load drawing into canvas when activeDrawing changes
  useEffect(() => {
    if (!excalidrawAPI || !activeDrawing) return;
    excalidrawAPI.updateScene({
      elements: activeDrawing.elements || [],
      appState: activeDrawing.appState || {},
    });
  }, [activeDrawing?.id]);

  // Auto-save on canvas changes (debounced)
  const onChange = (elements, appState, files) => {
    if (activeDrawing && !isDrawingConflict) {
      saveCurrentDrawing(elements, { viewBackgroundColor: appState.viewBackgroundColor }, files);
    }
  };
};

3. Add DrawingsDialog for management UI

const [showDialog, setShowDialog] = useState(false);

<DrawingsDialog
  open={showDialog}
  onClose={() => setShowDialog(false)}
  onDrawingSelect={() => setShowDialog(false)}
  renderThumbnail={(drawing) => <DrawingThumbnail drawing={drawing} />}
/>

Multi-Tab Conflict Detection

When the same drawing is open in multiple browser tabs, the workspace automatically detects this and makes the later tab read-only to prevent data loss.

How it works

  1. Each tab registers itself with a unique TAB_ID in localStorage
  2. When a drawing is opened, the tab records which drawing it has and when it opened it
  3. If another tab already has the same drawing open (opened earlier), isDrawingConflict becomes true
  4. The conflicted tab is read-only — saveCurrentDrawing and saveDrawingById silently skip saves
  5. When the first tab closes or switches to another drawing, the conflict resolves automatically

External conflict check

import { isDrawingOpenedEarlierInOtherTab } from "rita-workspace";

// Returns true if another tab opened this drawing before the current tab
if (isDrawingOpenedEarlierInOtherTab(drawingId)) {
  // Don't save — another tab owns this drawing
}

Communication between tabs

  • BroadcastChannel (rita-workspace-tabs) — instant notification when tabs open/close/switch drawings
  • localStorage (rita-workspace-tabs) — persistent tab registry, backup for BroadcastChannel
  • Stale tab cleanup — on mount, pings other tabs via BroadcastChannel and removes entries that don't respond

Workspace Toggle

The workspace can be enabled/disabled per browser tab using sessionStorage:

// Each tab reads its own toggle state
const [workspaceEnabled] = useState(() =>
  sessionStorage.getItem("rita-workspace-enabled") === "true"
);
  • Default: off (each new tab starts without workspace)
  • State stored in sessionStorage (not shared between tabs)
  • When disabled: auto-save to workspace skipped, drawing-switch disabled, footer hidden

Auto-start preference

Users can opt into starting every new tab in workspace mode via a checkbox in DrawingsDialog. The preference is stored in localStorage['rita-workspace-auto-start'] ("true" to enable, removed when disabled). The host app reads this flag at init time as a fallback when sessionStorage has no explicit value:

const [workspaceEnabled] = useState(() => {
  const sessionVal = sessionStorage.getItem("rita-workspace-enabled");
  if (sessionVal === "true") return true;
  if (sessionVal === "false") return false;
  return localStorage.getItem("rita-workspace-auto-start") === "true";
});

Cross-tab "last active drawing" memory

The library writes localStorage['rita-workspace-last-active-drawing'] whenever the active drawing changes. On mount in a tab without a session-pinned drawing (e.g. auto-start in a fresh tab), the active-drawing resolution falls back to this id before defaulting to the first drawing in the list. Users see the drawing they last edited rather than an arbitrary one.

Resolution order: sessionStorage['rita-workspace-tab-drawing']localStorage['rita-workspace-last-active-drawing']wsDrawings[0].

API Reference

Components

| Component | Description | |-----------|-------------| | WorkspaceProvider | React context provider. Props: lang, children | | DrawingsDialog | Management dialog. Props: open, onClose, onDrawingSelect (called on both switch and create), renderThumbnail |

Hooks

| Hook | Returns | |------|---------| | useWorkspace() | Full workspace state and actions | | useWorkspaceLang() | { lang, t } — current language and translations |

Exported functions

| Function | Description | |----------|-------------| | isDrawingOpenedEarlierInOtherTab(id) | Check if another tab has this drawing open | | warmDB() | Pre-warm IndexedDB connection (called automatically at import) |

useWorkspace() returns

const {
  // State
  workspace,          // Workspace | null
  drawings,           // Drawing[]
  folders,            // Folder[]
  activeDrawing,      // Drawing | null
  isLoading,          // boolean
  error,              // string | null
  isDrawingConflict,  // boolean — true if read-only (another tab has this drawing)
  lang,               // string
  t,                  // Translations

  // Drawing actions
  createNewDrawing,       // (name?, folderId?, activate=true) => Promise<Drawing | null>
  switchDrawing,          // (id) => Promise<void>
  renameDrawing,          // (id, name) => Promise<void>
  removeDrawing,          // (id) => Promise<void>
  duplicateCurrentDrawing, // () => Promise<Drawing | null>

  // Folder actions
  createFolder,       // (name) => Promise<Folder | null>
  renameFolder,       // (id, name) => Promise<void>
  deleteFolder,       // (id) => Promise<void>
  moveDrawingToFolder, // (drawingId, folderId) => Promise<void>

  // Save (blocked if drawing is in conflict)
  saveCurrentDrawing, // (elements, appState, files?) => Promise<void>
  saveDrawingById,    // (id, elements, appState, files?) => Promise<void>

  // Utilities
  refreshDrawings,    // () => Promise<void>
  exportWorkspace,    // () => Promise<void>
  importWorkspace,    // () => Promise<void>
  exportDrawingAsExcalidraw, // (id) => Promise<void>
  exportAllDrawingsAsExcalidraw, // () => Promise<void> — downloads all as .excalidraw files
  importExcalidrawFile,      // () => Promise<void> — imports .excalidraw files; switches to the last imported drawing
} = useWorkspace();

Data Storage

Drawings are stored in IndexedDB (rita-workspace database, version 2):

interface Drawing {
  id: string;           // nanoid
  name: string;
  folderId: string | null;
  elements: unknown[];  // Excalidraw elements
  appState: Record<string, unknown>;
  files: Record<string, unknown>;  // Image files
  createdAt: number;
  updatedAt: number;
}

Language Support

| Code | Language | |------|----------| | sv, sv-SE | Swedish | | en, en-US | English (default) |

Development

yarn build    # Build with tsup (cjs + esm + dts)
yarn dev      # Watch mode
yarn test     # Run tests with vitest
yarn typecheck # TypeScript check

License

MIT