From 459f7d4895c33deb247c6d5a52f8ff1a0f8fbf37 Mon Sep 17 00:00:00 2001 From: Ethan Graf Date: Thu, 21 May 2026 21:07:26 -0400 Subject: [PATCH] Lint and format --- .opencode/skills/textile-design/SKILL.md | 16 +-- .opencode/skills/textile-design/a11y.md | 14 +-- .opencode/skills/textile-design/components.md | 12 +- .opencode/skills/textile-design/motion.md | 10 +- .opencode/skills/textile-design/tokens.md | 58 ++++----- src/App.tsx | 116 +++++++++--------- src/components/AppTitleBar.tsx | 5 +- src/documents/documentSlotOpen.ts | 57 +++++++++ .../habitatDocumentsProvider.test.ts | 3 +- .../providers/habitatDocumentsProvider.ts | 3 +- src/editors/DocumentPane.tsx | 5 +- src/editors/DocumentSlotView.tsx | 3 +- src/editors/yjs/yjsDocumentEditor.tsx | 4 +- .../providers/habitatFilesystemProvider.ts | 3 +- src/habitat/connectionProvider.ts | 5 +- src/habitat/docSession.ts | 16 ++- src/habitat/habitatDoc.test.ts | 3 +- src/habitat/habitatDoc.ts | 8 +- src/habitat/types.ts | 5 +- src/habitat/xrpc.ts | 18 +-- src/main.ts | 13 +- src/workspaces/workspace.tsx | 4 +- 22 files changed, 231 insertions(+), 150 deletions(-) create mode 100644 src/documents/documentSlotOpen.ts diff --git a/.opencode/skills/textile-design/SKILL.md b/.opencode/skills/textile-design/SKILL.md index 358574b..6be84f2 100644 --- a/.opencode/skills/textile-design/SKILL.md +++ b/.opencode/skills/textile-design/SKILL.md @@ -34,14 +34,14 @@ Planned token extensions (`--radius-*`, `--motion-*`, `--font-*`, surface/muted) ## Token cheat sheet -| Area | Rule | -|------------|------| -| Page shell | `bg-background text-foreground` | -| Hairlines | `border-border` (1px borders) | -| Primary CTA | `bg-primary text-primary-foreground` | -| Links / key accents in prose | `text-accent` (per `EditPad`) | -| Danger | `text-destructive` for copy; full destructive buttons TBD | -| Muted copy | `text-foreground/60`–`/80` until `--semantic-muted-fg` lands | +| Area | Rule | +| ---------------------------- | ------------------------------------------------------------ | +| Page shell | `bg-background text-foreground` | +| Hairlines | `border-border` (1px borders) | +| Primary CTA | `bg-primary text-primary-foreground` | +| Links / key accents in prose | `text-accent` (per `EditPad`) | +| Danger | `text-destructive` for copy; full destructive buttons TBD | +| Muted copy | `text-foreground/60`–`/80` until `--semantic-muted-fg` lands | Full tables and proposed CSS variables: [tokens.md](tokens.md). diff --git a/.opencode/skills/textile-design/a11y.md b/.opencode/skills/textile-design/a11y.md index 739ee5c..9974521 100644 --- a/.opencode/skills/textile-design/a11y.md +++ b/.opencode/skills/textile-design/a11y.md @@ -19,13 +19,13 @@ Screen reader testing: **VoiceOver on macOS** (primary platform today). ## Patterns in the codebase -| Pattern | Expectation | -|---------|-------------| -| **Dialog** | `role="dialog"`, `aria-modal="true"`, `aria-label` or labelled-by; Esc to close when appropriate. | -| **Icon-only button** | `aria-label` on `
- +
); diff --git a/src/documents/documentSlotOpen.ts b/src/documents/documentSlotOpen.ts new file mode 100644 index 0000000..87a26a0 --- /dev/null +++ b/src/documents/documentSlotOpen.ts @@ -0,0 +1,57 @@ +import type { FileSystemProvider } from '../filesystem/types'; +import type { OpenDocRequest } from '../workspaces/workspace'; +import type { DocumentHandle } from './types'; + +export type DocumentOpenCallbacks = { + onLoading: () => void; + onOpen: (handle: DocumentHandle, request: OpenDocRequest) => void; + onError: (message: string, request: OpenDocRequest) => void; + /** Remove and return the previous handle; released in `finally` after open settles. */ + takePreviousHandle: () => DocumentHandle | null; + /** After open, return true if this handle is no longer the active one. */ + isStaleHandle?: (handle: DocumentHandle) => boolean; +}; + +/** + * Starts opening a document; returns a cancel function for effect cleanup. + */ +export function startDocumentOpen( + request: OpenDocRequest, + resolveProvider: (providerId: string) => FileSystemProvider | undefined, + callbacks: DocumentOpenCallbacks, +): () => void { + const fs = resolveProvider(request.providerId); + if (!fs) return () => undefined; + + callbacks.onLoading(); + const prevHandle = callbacks.takePreviousHandle(); + + let cancelled = false; + fs.documents + .openDocument(request.entryId) + .then((handle) => { + if (cancelled) { + handle.release(); + return; + } + callbacks.onOpen(handle, request); + handle.loadPromise.catch((err: unknown) => { + if (cancelled || callbacks.isStaleHandle?.(handle)) return; + const message = err instanceof Error ? err.message : 'Unknown error'; + handle.release(); + callbacks.onError(message, request); + }); + }) + .catch((err: unknown) => { + if (cancelled) return; + const message = err instanceof Error ? err.message : 'Unknown error'; + callbacks.onError(message, request); + }) + .finally(() => { + prevHandle?.release(); + }); + + return () => { + cancelled = true; + }; +} diff --git a/src/documents/providers/habitatDocumentsProvider.test.ts b/src/documents/providers/habitatDocumentsProvider.test.ts index 214a2ad..0b9fe13 100644 --- a/src/documents/providers/habitatDocumentsProvider.test.ts +++ b/src/documents/providers/habitatDocumentsProvider.test.ts @@ -9,7 +9,8 @@ const URI = 'habitat://did:plc:rtzf5y356funa3tgp6fzmkjn/network.habitat.docs/test-rkey'; vi.mock('../../habitat/docSession', async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, acquireDocSession: vi.fn((uri: string) => { diff --git a/src/documents/providers/habitatDocumentsProvider.ts b/src/documents/providers/habitatDocumentsProvider.ts index 45bfde8..cd6d7de 100644 --- a/src/documents/providers/habitatDocumentsProvider.ts +++ b/src/documents/providers/habitatDocumentsProvider.ts @@ -45,8 +45,7 @@ export class HabitatDocumentsProvider implements DocumentProvider { released = true; ydoc.destroy(); }, - getEditorBinding: () => - createYjsBinding({ ydoc, collabProvider: null }), + getEditorBinding: () => createYjsBinding({ ydoc, collabProvider: null }), }; } } diff --git a/src/editors/DocumentPane.tsx b/src/editors/DocumentPane.tsx index 2f61dc8..0e6a95f 100644 --- a/src/editors/DocumentPane.tsx +++ b/src/editors/DocumentPane.tsx @@ -20,7 +20,10 @@ export function DocumentPane({ handle, ...editorProps }: DocumentPaneProps) { ? `Unknown editor binding "${String(binding.kind)}" (not registered in editors/registry.ts).` : 'This document cannot be opened in the editor.'; return ( - + ); } diff --git a/src/editors/DocumentSlotView.tsx b/src/editors/DocumentSlotView.tsx index 037b6ca..8ecccf7 100644 --- a/src/editors/DocumentSlotView.tsx +++ b/src/editors/DocumentSlotView.tsx @@ -1,8 +1,7 @@ import type { DocumentSlotState } from '../documents/useDocumentSlot'; import { DocumentPane } from './DocumentPane'; -const DEFAULT_EMPTY_HINT = - 'Select a file from the sidebar to open it here.'; +const DEFAULT_EMPTY_HINT = 'Select a file from the sidebar to open it here.'; type DocumentSlotViewProps = { state: DocumentSlotState; diff --git a/src/editors/yjs/yjsDocumentEditor.tsx b/src/editors/yjs/yjsDocumentEditor.tsx index 3256db9..fa870e2 100644 --- a/src/editors/yjs/yjsDocumentEditor.tsx +++ b/src/editors/yjs/yjsDocumentEditor.tsx @@ -43,9 +43,7 @@ export function YjsDocumentEditor({ }), ] : []), - ...(placeholder - ? [Placeholder.configure({ placeholder })] - : []), + ...(placeholder ? [Placeholder.configure({ placeholder })] : []), ], [ydoc, collabProvider, user?.name, user?.color, placeholder], ); diff --git a/src/filesystem/providers/habitatFilesystemProvider.ts b/src/filesystem/providers/habitatFilesystemProvider.ts index 78e961f..7d11410 100644 --- a/src/filesystem/providers/habitatFilesystemProvider.ts +++ b/src/filesystem/providers/habitatFilesystemProvider.ts @@ -46,7 +46,8 @@ export class HabitatFilesystemProvider implements FileSystemProvider { ); return records.map((record) => { - const fallback = record.uri.split('/').filter(Boolean).pop() ?? record.uri; + const fallback = + record.uri.split('/').filter(Boolean).pop() ?? record.uri; const name = record.value.name?.trim() ? record.value.name : fallback; return { id: record.uri, name }; }); diff --git a/src/habitat/connectionProvider.ts b/src/habitat/connectionProvider.ts index 2f7f50e..b3b74e3 100644 --- a/src/habitat/connectionProvider.ts +++ b/src/habitat/connectionProvider.ts @@ -42,7 +42,10 @@ export class Libp2pConnectionProvider status: (event: { status: 'connected' | 'disconnected' | 'connecting'; }) => void; - 'connection-error': (event: Event, provider: Libp2pConnectionProvider) => void; + 'connection-error': ( + event: Event, + provider: Libp2pConnectionProvider, + ) => void; sync: (state: boolean) => void; }> implements CollabCaretProvider diff --git a/src/habitat/docSession.ts b/src/habitat/docSession.ts index 01b9d6f..cafa770 100644 --- a/src/habitat/docSession.ts +++ b/src/habitat/docSession.ts @@ -1,7 +1,10 @@ import * as Y from 'yjs'; import { habitatAuth } from '../auth/habitatAuth'; -import { Libp2pConnectionProvider, HabitatLibp2pNode } from './connectionProvider'; +import { + Libp2pConnectionProvider, + HabitatLibp2pNode, +} from './connectionProvider'; import { getHeadingFromYdoc, loadHabitatDoc, @@ -254,7 +257,10 @@ async function ensureLiveEditing(entry: RegistryEntry) { entry.provider = new Libp2pConnectionProvider(node, entry.ydoc, entry.uri); await dialRelayAndStartPeerDiscovery(entry.uri, node); } catch (e) { - console.error('[habitat] could not start libp2p; doc is read-write but offline', e); + console.error( + '[habitat] could not start libp2p; doc is read-write but offline', + e, + ); } } @@ -281,7 +287,11 @@ function makeHandle(entry: RegistryEntry): DocSession { }, loadPromise, getTitle() { - return getHeadingFromYdoc(entry.ydoc) ?? entry.ownerRecord?.value.name ?? 'Untitled'; + return ( + getHeadingFromYdoc(entry.ydoc) ?? + entry.ownerRecord?.value.name ?? + 'Untitled' + ); }, resync: () => resyncEntry(entry), release: () => { diff --git a/src/habitat/habitatDoc.test.ts b/src/habitat/habitatDoc.test.ts index 89ff4ac..e88c618 100644 --- a/src/habitat/habitatDoc.test.ts +++ b/src/habitat/habitatDoc.test.ts @@ -12,7 +12,8 @@ import { function encode(ydoc: Y.Doc): string { const bytes = Y.encodeStateAsUpdateV2(ydoc); let binary = ''; - for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!); + for (let i = 0; i < bytes.length; i++) + binary += String.fromCharCode(bytes[i]!); return btoa(binary); } diff --git a/src/habitat/habitatDoc.ts b/src/habitat/habitatDoc.ts index 891fa73..2b4dd28 100644 --- a/src/habitat/habitatDoc.ts +++ b/src/habitat/habitatDoc.ts @@ -50,7 +50,8 @@ export function applyHabitatBlob(ydoc: Y.Doc, b64: string | null | undefined) { export function encodeBlob(ydoc: Y.Doc): string { const bytes = Y.encodeStateAsUpdateV2(ydoc); let binary = ''; - for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!); + for (let i = 0; i < bytes.length; i++) + binary += String.fromCharCode(bytes[i]!); return btoa(binary); } @@ -163,10 +164,7 @@ export type SaveTarget = { * Pure routing: if I own the doc, write back to the owner record; otherwise * write a per-collaborator mirror in my repo. */ -export function chooseSaveTarget( - ownerUri: string, - myDid: string, -): SaveTarget { +export function chooseSaveTarget(ownerUri: string, myDid: string): SaveTarget { const { ownerDid, rkey } = parseDocUri(ownerUri); if (ownerDid === myDid) { return { repo: myDid, collection: HABITAT_DOCS_COLLECTION, rkey }; diff --git a/src/habitat/types.ts b/src/habitat/types.ts index e305e36..a689970 100644 --- a/src/habitat/types.ts +++ b/src/habitat/types.ts @@ -31,13 +31,15 @@ export class XRPCError extends Error { error: string; /** Endpoint NSID (or full path) that failed; helps locate the caller. */ endpoint?: string; + /** Request path passed to `habitatAuth.fetch`. */ + requestPath?: string; /** Raw response body if it wasn't a parseable JSON error envelope. */ rawBody?: string; constructor( status: number, payload: { error?: string; message?: string }, - context?: { endpoint?: string; rawBody?: string }, + context?: { endpoint?: string; requestPath?: string; rawBody?: string }, ) { const detail = payload.message || payload.error || `XRPC error ${status}`; const prefix = context?.endpoint ? `[${context.endpoint}] ` : ''; @@ -46,6 +48,7 @@ export class XRPCError extends Error { this.status = status; this.error = payload.error ?? 'XRPCError'; this.endpoint = context?.endpoint; + this.requestPath = context?.requestPath; this.rawBody = context?.rawBody; } } diff --git a/src/habitat/xrpc.ts b/src/habitat/xrpc.ts index cc37a69..7e57e5b 100644 --- a/src/habitat/xrpc.ts +++ b/src/habitat/xrpc.ts @@ -3,11 +3,7 @@ import { HABITAT_DOCS_COLLECTION, HABITAT_DOCS_EDIT_COLLECTION, } from './config'; -import { - ListRecordsResponse, - TypedRecord, - XRPCError, -} from './types'; +import { ListRecordsResponse, TypedRecord, XRPCError } from './types'; /** * Typed XRPC over `habitatAuth.fetch`. We hand-roll a tiny subset of the @@ -125,7 +121,11 @@ export async function query( } const requestPath = `/xrpc/${endpoint}?${search.toString()}`; const response = await habitatAuth.fetch(requestPath, { method: 'GET' }); - return parseResponse(response, endpoint, requestPath); + return parseResponse( + response, + endpoint, + requestPath, + ); } export async function procedure( @@ -138,7 +138,11 @@ export async function procedure( body: JSON.stringify(body), headers: { 'content-type': 'application/json' }, }); - return parseResponse(response, endpoint, requestPath); + return parseResponse( + response, + endpoint, + requestPath, + ); } /** Typed helpers for the most-used "private" record paths. */ diff --git a/src/main.ts b/src/main.ts index 404eea4..cadba62 100644 --- a/src/main.ts +++ b/src/main.ts @@ -143,11 +143,14 @@ function startHabitatLogin(authUrl: string, redirectUri: string) { } return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - pendingAuthCallback = undefined; - closeAuthWindow(); - reject(new Error('Authentication timed out. Please try again.')); - }, 5 * 60 * 1000); + const timeoutId = setTimeout( + () => { + pendingAuthCallback = undefined; + closeAuthWindow(); + reject(new Error('Authentication timed out. Please try again.')); + }, + 5 * 60 * 1000, + ); pendingAuthCallback = { redirectUri, diff --git a/src/workspaces/workspace.tsx b/src/workspaces/workspace.tsx index 231ffe6..6e1e53a 100644 --- a/src/workspaces/workspace.tsx +++ b/src/workspaces/workspace.tsx @@ -76,7 +76,9 @@ 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 { +export function getWorkspaceDescriptor( + kind: WorkspaceKind, +): WorkspaceDescriptor { const d = BY_KIND.get(kind); if (!d) throw new Error(`Unknown workspace kind: ${kind}`); return d; -- 2.51.2