···11+import type { FileSystemProvider } from '../filesystem/types';
22+import type { OpenDocRequest } from '../workspaces/workspace';
33+import type { DocumentHandle } from './types';
44+55+export type DocumentOpenCallbacks = {
66+ onLoading: () => void;
77+ onOpen: (handle: DocumentHandle, request: OpenDocRequest) => void;
88+ onError: (message: string, request: OpenDocRequest) => void;
99+ /** Remove and return the previous handle; released in `finally` after open settles. */
1010+ takePreviousHandle: () => DocumentHandle | null;
1111+ /** After open, return true if this handle is no longer the active one. */
1212+ isStaleHandle?: (handle: DocumentHandle) => boolean;
1313+};
1414+1515+/**
1616+ * Starts opening a document; returns a cancel function for effect cleanup.
1717+ */
1818+export function startDocumentOpen(
1919+ request: OpenDocRequest,
2020+ resolveProvider: (providerId: string) => FileSystemProvider | undefined,
2121+ callbacks: DocumentOpenCallbacks,
2222+): () => void {
2323+ const fs = resolveProvider(request.providerId);
2424+ if (!fs) return () => undefined;
2525+2626+ callbacks.onLoading();
2727+ const prevHandle = callbacks.takePreviousHandle();
2828+2929+ let cancelled = false;
3030+ fs.documents
3131+ .openDocument(request.entryId)
3232+ .then((handle) => {
3333+ if (cancelled) {
3434+ handle.release();
3535+ return;
3636+ }
3737+ callbacks.onOpen(handle, request);
3838+ handle.loadPromise.catch((err: unknown) => {
3939+ if (cancelled || callbacks.isStaleHandle?.(handle)) return;
4040+ const message = err instanceof Error ? err.message : 'Unknown error';
4141+ handle.release();
4242+ callbacks.onError(message, request);
4343+ });
4444+ })
4545+ .catch((err: unknown) => {
4646+ if (cancelled) return;
4747+ const message = err instanceof Error ? err.message : 'Unknown error';
4848+ callbacks.onError(message, request);
4949+ })
5050+ .finally(() => {
5151+ prevHandle?.release();
5252+ });
5353+5454+ return () => {
5555+ cancelled = true;
5656+ };
5757+}
+4-1
src/editors/DocumentPane.tsx
···2020 ? `Unknown editor binding "${String(binding.kind)}" (not registered in editors/registry.ts).`
2121 : 'This document cannot be opened in the editor.';
2222 return (
2323- <DocumentEditorError className={editorProps.className} message={message} />
2323+ <DocumentEditorError
2424+ className={editorProps.className}
2525+ message={message}
2626+ />
2427 );
2528 }
2629
+1-2
src/editors/DocumentSlotView.tsx
···11import type { DocumentSlotState } from '../documents/useDocumentSlot';
22import { DocumentPane } from './DocumentPane';
3344-const DEFAULT_EMPTY_HINT =
55- 'Select a file from the sidebar to open it here.';
44+const DEFAULT_EMPTY_HINT = 'Select a file from the sidebar to open it here.';
6576type DocumentSlotViewProps = {
87 state: DocumentSlotState;
···19192020## Patterns in the codebase
21212222-| Pattern | Expectation |
2323-|---------|-------------|
2424-| **Dialog** | `role="dialog"`, `aria-modal="true"`, `aria-label` or labelled-by; Esc to close when appropriate. |
2525-| **Icon-only button** | `aria-label` on `<button>`; `aria-hidden` on decorative `<svg>`. |
2626-| **Toggle / switch** | `role="switch"`, `aria-checked`, `aria-label` (see theme control in `SettingsModal`). |
2727-| **Sidebar toggle** | `aria-controls` pointing at sidebar `id`, `aria-pressed` for open state. |
2828-| **Decorative icons** | `aria-hidden` on SVG when adjacent text conveys meaning. |
2222+| Pattern | Expectation |
2323+| -------------------- | ------------------------------------------------------------------------------------------------------------------ |
2424+| **Dialog** | `role="dialog"`, `aria-modal="true"`, `aria-label` or labelled-by; Esc to close when appropriate. |
2525+| **Icon-only button** | `aria-label` on `<button>`; `aria-hidden` on decorative `<svg>`. |
2626+| **Toggle / switch** | `role="switch"`, `aria-checked`, `aria-label` (see theme control in `SettingsModal`). |
2727+| **Sidebar toggle** | `aria-controls` pointing at sidebar `id`, `aria-pressed` for open state. |
2828+| **Decorative icons** | `aria-hidden` on SVG when adjacent text conveys meaning. |
2929| **Lists of actions** | Prefer native list semantics (`role="list"` / `listitem`) or ensure roving tabindex if building composite widgets. |
30303131## Focus rings
+6-6
.opencode/skills/textile-design/components.md
···62626363## Migration / consistency backlog
64646565-| Item | Suggestion |
6666-|------|------------|
6767-| Settings category rows (`rounded` vs `rounded-md`) | Use `rounded-md` to match `AppSidebar` rows |
6868-| “Sign in” sizing | Align with `text-sm` + `rounded-md` where it is a primary action |
6969-| Settings category selected state | Add `aria-current="true"` on active category button |
7070-| Modal focus trap | Add roving focus / focus trap library or manual implementation |
6565+| Item | Suggestion |
6666+| -------------------------------------------------- | ---------------------------------------------------------------- |
6767+| Settings category rows (`rounded` vs `rounded-md`) | Use `rounded-md` to match `AppSidebar` rows |
6868+| “Sign in” sizing | Align with `text-sm` + `rounded-md` where it is a primary action |
6969+| Settings category selected state | Add `aria-current="true"` on active category button |
7070+| Modal focus trap | Add roving focus / focus trap library or manual implementation |
71717272Return to [SKILL.md](SKILL.md).