Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
6.5 kB · 189 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190import type { ComponentType, MutableRefObject } from 'react';import type { FileSystemProvider } from '../filesystem/types';import { TilingWorkspace } from './tiling/tiling';import { ZenWorkspace } from './zen/zen';import { WorkspaceActiveProvider } from './barSlot';import type { TilingHandle } from '../actions';
export type WorkspaceKind = 'tiling' | 'zen';
/** * Intent from the shell (e.g. sidebar) asking the active workspace to open a * document. * * TODO(naming): `providerId` / `entryId` come from the FileSystemProvider * abstraction in `src/filesystem/types.ts`. As we add non-filesystem sources * (search results, recent items, deep links) these names start to lie. Options * we may want to workshop: `sourceId` + `documentId`, a single opaque `uri` * (e.g. `habitat-docs://<entry>`), or a tagged union per source kind. Leaving * the FS-flavoured names for now since the only source today *is* the * filesystem sidebar. */export type OpenDocRequest = { /** * Identifies the source that owns the document. Today this matches both * `FileSystemProvider.id` and `DocumentProvider.id` (e.g. `"habitat-docs"`). */ providerId: string; /** * Identifies the specific document within that source. For * `habitat-docs` this is an AT-protocol URI; for future providers it could * be a file path, blob CID, etc. Opaque to the workspace. */ entryId: string; /** Human-readable label used for tab/title chrome. */ title: string;};
export type WorkspaceProps = { /** * Whether this workspace is the one currently shown. Inactive workspaces stay * mounted so their tile layout and tabs survive a switch, but they ignore * open intents and do not paint the bottom bar. */ active: boolean; /** * Stable per-vault key (the vault's provider id). Used as the persistence * namespace so each vault restores its own layout. */ instanceKey: string; /** * Monotonically bumped each time the shell issues a new open intent. * Workspaces detect "a new open arrived" by comparing against their * last-seen id, which avoids re-opening on unrelated re-renders. */ openRequestId: number; openRequest: OpenDocRequest | null; /** * Look up a FileSystemProvider by `OpenDocRequest.providerId`. Workspaces * open documents via `provider.documents` when present. */ resolveProvider: (providerId: string) => FileSystemProvider | undefined; /** * Mutable ref for TilingWorkspace to expose its split handle. * Only meaningful for the tiling workspace; other kinds ignore it. */ tilingHandleRef?: MutableRefObject<TilingHandle | null>; /** * Create a new file in the selected source and open it in the active tile. * Surfaced by a tile's empty-state "Open a new file" action. Absent when no * source is selected. */ onNewFile?: () => void;};
export type WorkspaceComponent = ComponentType<WorkspaceProps>;
export type WorkspaceDescriptor = { kind: WorkspaceKind; label: string; description: string; Component: WorkspaceComponent;};
export const WORKSPACE_DESCRIPTORS: readonly WorkspaceDescriptor[] = [ { kind: 'tiling', label: 'Tiling', description: 'Split-pane editor with resizable tiles and split controls.', Component: TilingWorkspace, }, { kind: 'zen', label: 'Zen', description: 'Single editor focused on one document at a time.', Component: ZenWorkspace, },];
const BY_KIND = new Map(WORKSPACE_DESCRIPTORS.map((d) => [d.kind, d]));
export const DEFAULT_WORKSPACE_KIND: WorkspaceKind = 'tiling';
export function getWorkspaceDescriptor( kind: WorkspaceKind,): WorkspaceDescriptor { const d = BY_KIND.get(kind); if (!d) throw new Error(`Unknown workspace kind: ${kind}`); return d;}
export function isWorkspaceKind(value: unknown): value is WorkspaceKind { return typeof value === 'string' && BY_KIND.has(value as WorkspaceKind);}
type WorkspaceHostProps = { kind: WorkspaceKind; /** Provider ids of the mounted vaults, one workspace instance per vault. */ vaultIds: readonly string[]; /** The currently selected vault's provider id. */ selectedVaultId: string; openRequestId: number; openRequest: OpenDocRequest | null; resolveProvider: (providerId: string) => FileSystemProvider | undefined; tilingHandleRef?: MutableRefObject<TilingHandle | null>; onNewFile?: () => void;};
/** * Mounts one workspace instance per (kind × vault) and shows only the one * matching the current kind and selected vault. Keeping every instance mounted * preserves each vault's tile layout and tabs (and its open documents), so * switching vaults — or modes — restores that vault's setup exactly. Inactive * instances are hidden, ignore open intents (`active=false`), and suppress * their bottom-bar portal. */export function WorkspaceHost({ kind, vaultIds, selectedVaultId, openRequestId, openRequest, resolveProvider, tilingHandleRef, onNewFile,}: WorkspaceHostProps) { // Always render at least one slot — even before any vault is mounted — so the // empty start page is visible. Guarantee exactly one active vault key. const vaultKeys = vaultIds.length > 0 ? vaultIds : ['']; const activeVaultKey = vaultKeys.includes(selectedVaultId) ? selectedVaultId : vaultKeys[0];
return ( <> {vaultKeys.flatMap((vaultKey) => WORKSPACE_DESCRIPTORS.map(({ kind: descriptorKind, Component }) => { const isActive = descriptorKind === kind && vaultKey === activeVaultKey; return ( <div key={`${descriptorKind}:${vaultKey}`} className={ isActive ? 'flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden' : 'hidden' } > <WorkspaceActiveProvider value={isActive}> <Component active={isActive} instanceKey={vaultKey} openRequestId={openRequestId} openRequest={openRequest} resolveProvider={resolveProvider} tilingHandleRef={ descriptorKind === 'tiling' && isActive ? tilingHandleRef : undefined } onNewFile={onNewFile} /> </WorkspaceActiveProvider> </div> ); }), )} </> );}