A local-first note taking app
0

Configure Feed

Select the types of activity you want to include in your feed.

Lint and format

Ethan Graf (May 21, 2026, 9:07 PM EDT) 459f7d48 81941692

+231 -150
+58 -58
src/App.tsx
··· 92 92 return ( 93 93 <UserIdentityProvider> 94 94 <MemoryRouter initialEntries={['/']}> 95 - {/* TODO: Replace this unsupported screen with full non-macOS UI path. */} 96 - {!isMac ? ( 97 - <main className="bg-background text-foreground flex h-svh w-full items-center justify-center p-6"> 98 - <div className="border-border bg-background max-w-md rounded-md border p-4 text-sm"> 99 - textile currently supports macOS title bar integration only. 100 - </div> 101 - </main> 102 - ) : ( 103 - <div className="bg-background text-foreground flex h-svh w-full flex-col overflow-hidden"> 104 - <AppTitleBar 105 - sidebarOpen={sidebarOpen} 106 - onToggleSidebar={() => setSidebarOpen((o) => !o)} 107 - workspaceKind={workspaceKind} 108 - onWorkspaceKindChange={setWorkspaceKind} 109 - /> 110 - <AppBottomBar> 111 - <div 112 - className="flex min-h-0 flex-1 pb-12" 113 - style={{ 114 - // Reserve the window’s title bar row; custom chrome is `position: fixed` in that region. 115 - marginTop: 116 - 'calc(env(titlebar-area-y, 0px) + env(titlebar-area-height, 40px))', 117 - }} 118 - > 119 - <AppSidebar 120 - open={sidebarOpen} 121 - providers={fileSystemProviders} 122 - onOpenEntry={handleOpenEntry} 123 - /> 124 - <main className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden"> 125 - <Routes> 126 - <Route 127 - path="/" 128 - element={ 129 - <WorkspaceHost 130 - kind={workspaceKind} 131 - openRequestId={openRequestId} 132 - openRequest={openRequest} 133 - resolveProvider={resolveProvider} 134 - /> 135 - } 136 - /> 137 - <Route path="/about" element={<AboutPage />} /> 138 - </Routes> 139 - </main> 95 + {/* TODO: Replace this unsupported screen with full non-macOS UI path. */} 96 + {!isMac ? ( 97 + <main className="bg-background text-foreground flex h-svh w-full items-center justify-center p-6"> 98 + <div className="border-border bg-background max-w-md rounded-md border p-4 text-sm"> 99 + textile currently supports macOS title bar integration only. 140 100 </div> 141 - </AppBottomBar> 142 - <SettingsModal 143 - open={settingsOpen} 144 - onClose={() => setSettingsOpen(false)} 145 - theme={theme} 146 - onToggleTheme={() => 147 - setTheme((t) => (t === 'light' ? 'dark' : 'light')) 148 - } 149 - workspaceKind={workspaceKind} 150 - onWorkspaceKindChange={setWorkspaceKind} 151 - /> 152 - </div> 153 - )} 101 + </main> 102 + ) : ( 103 + <div className="bg-background text-foreground flex h-svh w-full flex-col overflow-hidden"> 104 + <AppTitleBar 105 + sidebarOpen={sidebarOpen} 106 + onToggleSidebar={() => setSidebarOpen((o) => !o)} 107 + workspaceKind={workspaceKind} 108 + onWorkspaceKindChange={setWorkspaceKind} 109 + /> 110 + <AppBottomBar> 111 + <div 112 + className="flex min-h-0 flex-1 pb-12" 113 + style={{ 114 + // Reserve the window’s title bar row; custom chrome is `position: fixed` in that region. 115 + marginTop: 116 + 'calc(env(titlebar-area-y, 0px) + env(titlebar-area-height, 40px))', 117 + }} 118 + > 119 + <AppSidebar 120 + open={sidebarOpen} 121 + providers={fileSystemProviders} 122 + onOpenEntry={handleOpenEntry} 123 + /> 124 + <main className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden"> 125 + <Routes> 126 + <Route 127 + path="/" 128 + element={ 129 + <WorkspaceHost 130 + kind={workspaceKind} 131 + openRequestId={openRequestId} 132 + openRequest={openRequest} 133 + resolveProvider={resolveProvider} 134 + /> 135 + } 136 + /> 137 + <Route path="/about" element={<AboutPage />} /> 138 + </Routes> 139 + </main> 140 + </div> 141 + </AppBottomBar> 142 + <SettingsModal 143 + open={settingsOpen} 144 + onClose={() => setSettingsOpen(false)} 145 + theme={theme} 146 + onToggleTheme={() => 147 + setTheme((t) => (t === 'light' ? 'dark' : 'light')) 148 + } 149 + workspaceKind={workspaceKind} 150 + onWorkspaceKindChange={setWorkspaceKind} 151 + /> 152 + </div> 153 + )} 154 154 </MemoryRouter> 155 155 </UserIdentityProvider> 156 156 );
+8 -5
src/main.ts
··· 143 143 } 144 144 145 145 return new Promise<string>((resolve, reject) => { 146 - const timeoutId = setTimeout(() => { 147 - pendingAuthCallback = undefined; 148 - closeAuthWindow(); 149 - reject(new Error('Authentication timed out. Please try again.')); 150 - }, 5 * 60 * 1000); 146 + const timeoutId = setTimeout( 147 + () => { 148 + pendingAuthCallback = undefined; 149 + closeAuthWindow(); 150 + reject(new Error('Authentication timed out. Please try again.')); 151 + }, 152 + 5 * 60 * 1000, 153 + ); 151 154 152 155 pendingAuthCallback = { 153 156 redirectUri,
+1 -4
src/components/AppTitleBar.tsx
··· 66 66 </button> 67 67 </div> 68 68 <div className="flex h-full shrink-0 items-center gap-1 pr-1"> 69 - <WorkspaceMenu 70 - value={workspaceKind} 71 - onChange={onWorkspaceKindChange} 72 - /> 69 + <WorkspaceMenu value={workspaceKind} onChange={onWorkspaceKindChange} /> 73 70 </div> 74 71 </header> 75 72 );
+57
src/documents/documentSlotOpen.ts
··· 1 + import type { FileSystemProvider } from '../filesystem/types'; 2 + import type { OpenDocRequest } from '../workspaces/workspace'; 3 + import type { DocumentHandle } from './types'; 4 + 5 + export type DocumentOpenCallbacks = { 6 + onLoading: () => void; 7 + onOpen: (handle: DocumentHandle, request: OpenDocRequest) => void; 8 + onError: (message: string, request: OpenDocRequest) => void; 9 + /** Remove and return the previous handle; released in `finally` after open settles. */ 10 + takePreviousHandle: () => DocumentHandle | null; 11 + /** After open, return true if this handle is no longer the active one. */ 12 + isStaleHandle?: (handle: DocumentHandle) => boolean; 13 + }; 14 + 15 + /** 16 + * Starts opening a document; returns a cancel function for effect cleanup. 17 + */ 18 + export function startDocumentOpen( 19 + request: OpenDocRequest, 20 + resolveProvider: (providerId: string) => FileSystemProvider | undefined, 21 + callbacks: DocumentOpenCallbacks, 22 + ): () => void { 23 + const fs = resolveProvider(request.providerId); 24 + if (!fs) return () => undefined; 25 + 26 + callbacks.onLoading(); 27 + const prevHandle = callbacks.takePreviousHandle(); 28 + 29 + let cancelled = false; 30 + fs.documents 31 + .openDocument(request.entryId) 32 + .then((handle) => { 33 + if (cancelled) { 34 + handle.release(); 35 + return; 36 + } 37 + callbacks.onOpen(handle, request); 38 + handle.loadPromise.catch((err: unknown) => { 39 + if (cancelled || callbacks.isStaleHandle?.(handle)) return; 40 + const message = err instanceof Error ? err.message : 'Unknown error'; 41 + handle.release(); 42 + callbacks.onError(message, request); 43 + }); 44 + }) 45 + .catch((err: unknown) => { 46 + if (cancelled) return; 47 + const message = err instanceof Error ? err.message : 'Unknown error'; 48 + callbacks.onError(message, request); 49 + }) 50 + .finally(() => { 51 + prevHandle?.release(); 52 + }); 53 + 54 + return () => { 55 + cancelled = true; 56 + }; 57 + }
+4 -1
src/editors/DocumentPane.tsx
··· 20 20 ? `Unknown editor binding "${String(binding.kind)}" (not registered in editors/registry.ts).` 21 21 : 'This document cannot be opened in the editor.'; 22 22 return ( 23 - <DocumentEditorError className={editorProps.className} message={message} /> 23 + <DocumentEditorError 24 + className={editorProps.className} 25 + message={message} 26 + /> 24 27 ); 25 28 } 26 29
+1 -2
src/editors/DocumentSlotView.tsx
··· 1 1 import type { DocumentSlotState } from '../documents/useDocumentSlot'; 2 2 import { DocumentPane } from './DocumentPane'; 3 3 4 - const DEFAULT_EMPTY_HINT = 5 - 'Select a file from the sidebar to open it here.'; 4 + const DEFAULT_EMPTY_HINT = 'Select a file from the sidebar to open it here.'; 6 5 7 6 type DocumentSlotViewProps = { 8 7 state: DocumentSlotState;
+4 -1
src/habitat/connectionProvider.ts
··· 42 42 status: (event: { 43 43 status: 'connected' | 'disconnected' | 'connecting'; 44 44 }) => void; 45 - 'connection-error': (event: Event, provider: Libp2pConnectionProvider) => void; 45 + 'connection-error': ( 46 + event: Event, 47 + provider: Libp2pConnectionProvider, 48 + ) => void; 46 49 sync: (state: boolean) => void; 47 50 }> 48 51 implements CollabCaretProvider
+13 -3
src/habitat/docSession.ts
··· 1 1 import * as Y from 'yjs'; 2 2 3 3 import { habitatAuth } from '../auth/habitatAuth'; 4 - import { Libp2pConnectionProvider, HabitatLibp2pNode } from './connectionProvider'; 4 + import { 5 + Libp2pConnectionProvider, 6 + HabitatLibp2pNode, 7 + } from './connectionProvider'; 5 8 import { 6 9 getHeadingFromYdoc, 7 10 loadHabitatDoc, ··· 254 257 entry.provider = new Libp2pConnectionProvider(node, entry.ydoc, entry.uri); 255 258 await dialRelayAndStartPeerDiscovery(entry.uri, node); 256 259 } catch (e) { 257 - console.error('[habitat] could not start libp2p; doc is read-write but offline', e); 260 + console.error( 261 + '[habitat] could not start libp2p; doc is read-write but offline', 262 + e, 263 + ); 258 264 } 259 265 } 260 266 ··· 281 287 }, 282 288 loadPromise, 283 289 getTitle() { 284 - return getHeadingFromYdoc(entry.ydoc) ?? entry.ownerRecord?.value.name ?? 'Untitled'; 290 + return ( 291 + getHeadingFromYdoc(entry.ydoc) ?? 292 + entry.ownerRecord?.value.name ?? 293 + 'Untitled' 294 + ); 285 295 }, 286 296 resync: () => resyncEntry(entry), 287 297 release: () => {
+2 -1
src/habitat/habitatDoc.test.ts
··· 12 12 function encode(ydoc: Y.Doc): string { 13 13 const bytes = Y.encodeStateAsUpdateV2(ydoc); 14 14 let binary = ''; 15 - for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!); 15 + for (let i = 0; i < bytes.length; i++) 16 + binary += String.fromCharCode(bytes[i]!); 16 17 return btoa(binary); 17 18 } 18 19
+3 -5
src/habitat/habitatDoc.ts
··· 50 50 export function encodeBlob(ydoc: Y.Doc): string { 51 51 const bytes = Y.encodeStateAsUpdateV2(ydoc); 52 52 let binary = ''; 53 - for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!); 53 + for (let i = 0; i < bytes.length; i++) 54 + binary += String.fromCharCode(bytes[i]!); 54 55 return btoa(binary); 55 56 } 56 57 ··· 163 164 * Pure routing: if I own the doc, write back to the owner record; otherwise 164 165 * write a per-collaborator mirror in my repo. 165 166 */ 166 - export function chooseSaveTarget( 167 - ownerUri: string, 168 - myDid: string, 169 - ): SaveTarget { 167 + export function chooseSaveTarget(ownerUri: string, myDid: string): SaveTarget { 170 168 const { ownerDid, rkey } = parseDocUri(ownerUri); 171 169 if (ownerDid === myDid) { 172 170 return { repo: myDid, collection: HABITAT_DOCS_COLLECTION, rkey };
+4 -1
src/habitat/types.ts
··· 31 31 error: string; 32 32 /** Endpoint NSID (or full path) that failed; helps locate the caller. */ 33 33 endpoint?: string; 34 + /** Request path passed to `habitatAuth.fetch`. */ 35 + requestPath?: string; 34 36 /** Raw response body if it wasn't a parseable JSON error envelope. */ 35 37 rawBody?: string; 36 38 37 39 constructor( 38 40 status: number, 39 41 payload: { error?: string; message?: string }, 40 - context?: { endpoint?: string; rawBody?: string }, 42 + context?: { endpoint?: string; requestPath?: string; rawBody?: string }, 41 43 ) { 42 44 const detail = payload.message || payload.error || `XRPC error ${status}`; 43 45 const prefix = context?.endpoint ? `[${context.endpoint}] ` : ''; ··· 46 48 this.status = status; 47 49 this.error = payload.error ?? 'XRPCError'; 48 50 this.endpoint = context?.endpoint; 51 + this.requestPath = context?.requestPath; 49 52 this.rawBody = context?.rawBody; 50 53 } 51 54 }
+11 -7
src/habitat/xrpc.ts
··· 3 3 HABITAT_DOCS_COLLECTION, 4 4 HABITAT_DOCS_EDIT_COLLECTION, 5 5 } from './config'; 6 - import { 7 - ListRecordsResponse, 8 - TypedRecord, 9 - XRPCError, 10 - } from './types'; 6 + import { ListRecordsResponse, TypedRecord, XRPCError } from './types'; 11 7 12 8 /** 13 9 * Typed XRPC over `habitatAuth.fetch`. We hand-roll a tiny subset of the ··· 125 121 } 126 122 const requestPath = `/xrpc/${endpoint}?${search.toString()}`; 127 123 const response = await habitatAuth.fetch(requestPath, { method: 'GET' }); 128 - return parseResponse<QueryEndpoints[K]['output']>(response, endpoint, requestPath); 124 + return parseResponse<QueryEndpoints[K]['output']>( 125 + response, 126 + endpoint, 127 + requestPath, 128 + ); 129 129 } 130 130 131 131 export async function procedure<K extends keyof ProcedureEndpoints>( ··· 138 138 body: JSON.stringify(body), 139 139 headers: { 'content-type': 'application/json' }, 140 140 }); 141 - return parseResponse<ProcedureEndpoints[K]['output']>(response, endpoint, requestPath); 141 + return parseResponse<ProcedureEndpoints[K]['output']>( 142 + response, 143 + endpoint, 144 + requestPath, 145 + ); 142 146 } 143 147 144 148 /** Typed helpers for the most-used "private" record paths. */
+3 -1
src/workspaces/workspace.tsx
··· 76 76 77 77 export const DEFAULT_WORKSPACE_KIND: WorkspaceKind = 'tiling'; 78 78 79 - export function getWorkspaceDescriptor(kind: WorkspaceKind): WorkspaceDescriptor { 79 + export function getWorkspaceDescriptor( 80 + kind: WorkspaceKind, 81 + ): WorkspaceDescriptor { 80 82 const d = BY_KIND.get(kind); 81 83 if (!d) throw new Error(`Unknown workspace kind: ${kind}`); 82 84 return d;
+8 -8
.opencode/skills/textile-design/SKILL.md
··· 34 34 35 35 ## Token cheat sheet 36 36 37 - | Area | Rule | 38 - |------------|------| 39 - | Page shell | `bg-background text-foreground` | 40 - | Hairlines | `border-border` (1px borders) | 41 - | Primary CTA | `bg-primary text-primary-foreground` | 42 - | Links / key accents in prose | `text-accent` (per `EditPad`) | 43 - | Danger | `text-destructive` for copy; full destructive buttons TBD | 44 - | Muted copy | `text-foreground/60`–`/80` until `--semantic-muted-fg` lands | 37 + | Area | Rule | 38 + | ---------------------------- | ------------------------------------------------------------ | 39 + | Page shell | `bg-background text-foreground` | 40 + | Hairlines | `border-border` (1px borders) | 41 + | Primary CTA | `bg-primary text-primary-foreground` | 42 + | Links / key accents in prose | `text-accent` (per `EditPad`) | 43 + | Danger | `text-destructive` for copy; full destructive buttons TBD | 44 + | Muted copy | `text-foreground/60`–`/80` until `--semantic-muted-fg` lands | 45 45 46 46 Full tables and proposed CSS variables: [tokens.md](tokens.md). 47 47
+7 -7
.opencode/skills/textile-design/a11y.md
··· 19 19 20 20 ## Patterns in the codebase 21 21 22 - | Pattern | Expectation | 23 - |---------|-------------| 24 - | **Dialog** | `role="dialog"`, `aria-modal="true"`, `aria-label` or labelled-by; Esc to close when appropriate. | 25 - | **Icon-only button** | `aria-label` on `<button>`; `aria-hidden` on decorative `<svg>`. | 26 - | **Toggle / switch** | `role="switch"`, `aria-checked`, `aria-label` (see theme control in `SettingsModal`). | 27 - | **Sidebar toggle** | `aria-controls` pointing at sidebar `id`, `aria-pressed` for open state. | 28 - | **Decorative icons** | `aria-hidden` on SVG when adjacent text conveys meaning. | 22 + | Pattern | Expectation | 23 + | -------------------- | ------------------------------------------------------------------------------------------------------------------ | 24 + | **Dialog** | `role="dialog"`, `aria-modal="true"`, `aria-label` or labelled-by; Esc to close when appropriate. | 25 + | **Icon-only button** | `aria-label` on `<button>`; `aria-hidden` on decorative `<svg>`. | 26 + | **Toggle / switch** | `role="switch"`, `aria-checked`, `aria-label` (see theme control in `SettingsModal`). | 27 + | **Sidebar toggle** | `aria-controls` pointing at sidebar `id`, `aria-pressed` for open state. | 28 + | **Decorative icons** | `aria-hidden` on SVG when adjacent text conveys meaning. | 29 29 | **Lists of actions** | Prefer native list semantics (`role="list"` / `listitem`) or ensure roving tabindex if building composite widgets. | 30 30 31 31 ## Focus rings
+6 -6
.opencode/skills/textile-design/components.md
··· 62 62 63 63 ## Migration / consistency backlog 64 64 65 - | Item | Suggestion | 66 - |------|------------| 67 - | Settings category rows (`rounded` vs `rounded-md`) | Use `rounded-md` to match `AppSidebar` rows | 68 - | “Sign in” sizing | Align with `text-sm` + `rounded-md` where it is a primary action | 69 - | Settings category selected state | Add `aria-current="true"` on active category button | 70 - | Modal focus trap | Add roving focus / focus trap library or manual implementation | 65 + | Item | Suggestion | 66 + | -------------------------------------------------- | ---------------------------------------------------------------- | 67 + | Settings category rows (`rounded` vs `rounded-md`) | Use `rounded-md` to match `AppSidebar` rows | 68 + | “Sign in” sizing | Align with `text-sm` + `rounded-md` where it is a primary action | 69 + | Settings category selected state | Add `aria-current="true"` on active category button | 70 + | Modal focus trap | Add roving focus / focus trap library or manual implementation | 71 71 72 72 Return to [SKILL.md](SKILL.md).
+5 -5
.opencode/skills/textile-design/motion.md
··· 19 19 20 20 ## Durations in use today 21 21 22 - | Surface | Duration | Easing | Location | 23 - |---------|----------|--------|----------| 24 - | Theme colors / borders | 320ms | ease-in-out | Global `@layer base` | 25 - | Sidebar width | 200ms (width), 320ms (colors) | ease-out / ease-in-out | `AppSidebar` inline `style` | 26 - | Theme switch knob | 500ms | ease-in-out | `SettingsModal` `transition-transform` | 22 + | Surface | Duration | Easing | Location | 23 + | ---------------------- | ----------------------------- | ---------------------- | -------------------------------------- | 24 + | Theme colors / borders | 320ms | ease-in-out | Global `@layer base` | 25 + | Sidebar width | 200ms (width), 320ms (colors) | ease-out / ease-in-out | `AppSidebar` inline `style` | 26 + | Theme switch knob | 500ms | ease-in-out | `SettingsModal` `transition-transform` | 27 27 28 28 Proposed standard names: `--motion-fast` (160ms), `--motion-base` (320ms), `--motion-slow` (500ms)—see [tokens.md](tokens.md). 29 29
+30 -28
.opencode/skills/textile-design/tokens.md
··· 4 4 5 5 Defined on `:root` and overridden on `:root[data-theme='dark']`, then exposed via `@theme` as Tailwind colors: 6 6 7 - | Variable | Tailwind utilities | 8 - |----------|---------------------| 9 - | `--semantic-bg` | `bg-background` | 10 - | `--semantic-fg` | `text-foreground` | 11 - | `--semantic-primary` | `bg-primary`, `text-primary`, ring usage as needed | 12 - | `--semantic-primary-fg` | `text-primary-foreground` | 13 - | `--semantic-accent` | `text-accent`, `bg-accent` (if used) | 14 - | `--semantic-border` | `border-border`, hairline dividers | 15 - | `--semantic-destructive` | `text-destructive`, `bg-destructive` (if used) | 7 + | Variable | Tailwind utilities | 8 + | ------------------------ | -------------------------------------------------- | 9 + | `--semantic-bg` | `bg-background` | 10 + | `--semantic-fg` | `text-foreground` | 11 + | `--semantic-primary` | `bg-primary`, `text-primary`, ring usage as needed | 12 + | `--semantic-primary-fg` | `text-primary-foreground` | 13 + | `--semantic-accent` | `text-accent`, `bg-accent` (if used) | 14 + | `--semantic-border` | `border-border`, hairline dividers | 15 + | `--semantic-destructive` | `text-destructive`, `bg-destructive` (if used) | 16 16 17 17 **Rule:** components reference **Tailwind utilities**, not `--semantic-*` directly, except inside global CSS or when defining new `@theme` entries. 18 18 ··· 37 37 38 38 ### Radii (align with existing Tailwind usage) 39 39 40 - | Token (proposed) | Value | Tailwind mapping | Use for | 41 - |------------------|-------|------------------|---------| 42 - | `--radius-sm` | 4px | `rounded` | Compact icon hit areas, small chips | 43 - | `--radius-md` | 6px | `rounded-md` | Inputs, list rows, `EditPad` container | 44 - | `--radius-lg` | 10px | `rounded-lg` | Modal panels, large cards | 45 - | — | pill | `rounded-full` | Switches, avatars | 40 + | Token (proposed) | Value | Tailwind mapping | Use for | 41 + | ---------------- | ----- | ---------------- | -------------------------------------- | 42 + | `--radius-sm` | 4px | `rounded` | Compact icon hit areas, small chips | 43 + | `--radius-md` | 6px | `rounded-md` | Inputs, list rows, `EditPad` container | 44 + | `--radius-lg` | 10px | `rounded-lg` | Modal panels, large cards | 45 + | — | pill | `rounded-full` | Switches, avatars | 46 46 47 47 **Consistency rule:** within a single scrollable list or form, use **one** radius tier for sibling controls. 48 48 49 49 ### Motion (proposed CSS variables) 50 50 51 - | Token | ms | Notes | 52 - |-------|-----|------| 53 - | `--motion-fast` | 160 | Sidebar width, small positional tweens | 51 + | Token | ms | Notes | 52 + | --------------- | --- | ------------------------------------------------ | 53 + | `--motion-fast` | 160 | Sidebar width, small positional tweens | 54 54 | `--motion-base` | 320 | Matches global transition in `@layer base` today | 55 - | `--motion-slow` | 500 | Theme toggle thumb (`SettingsModal`) | 55 + | `--motion-slow` | 500 | Theme toggle thumb (`SettingsModal`) | 56 56 57 57 Wire to Tailwind in a later PR (e.g. `@theme { --duration-base: var(--motion-base); }`) so components use `duration-base` instead of magic numbers. 58 58 ··· 60 60 61 61 ```css 62 62 :root { 63 - --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 63 + --font-sans: 64 + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 65 + Roboto, Helvetica, Arial, sans-serif; 64 66 --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; 65 67 --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; 66 68 } ··· 75 77 76 78 Use these defaults unless a layout clearly needs an exception: 77 79 78 - | Context | Classes | 79 - |---------|---------| 80 - | Icon size (inline, lists) | `size-4` | 81 - | Icon ↔ label | `gap-2` | 82 - | List / nav row | `px-2 py-1.5` | 80 + | Context | Classes | 81 + | ------------------------------------ | ------------------------------- | 82 + | Icon size (inline, lists) | `size-4` | 83 + | Icon ↔ label | `gap-2` | 84 + | List / nav row | `px-2 py-1.5` | 83 85 | Icon button (title bar, modal close) | `p-1` or `p-1.5` with `rounded` | 84 - | Sidebar / tight panels | `p-3`, section gaps `gap-4` | 85 - | Settings main column | `p-5` (`SettingsPaneLayout`) | 86 - | Dense vertical stacks | `space-y-1`–`space-y-4` | 86 + | Sidebar / tight panels | `p-3`, section gaps `gap-4` | 87 + | Settings main column | `p-5` (`SettingsPaneLayout`) | 88 + | Dense vertical stacks | `space-y-1`–`space-y-4` | 87 89 88 90 ## Migration notes (current codebase) 89 91
+2 -1
src/documents/providers/habitatDocumentsProvider.test.ts
··· 9 9 'habitat://did:plc:rtzf5y356funa3tgp6fzmkjn/network.habitat.docs/test-rkey'; 10 10 11 11 vi.mock('../../habitat/docSession', async (importOriginal) => { 12 - const actual = await importOriginal<typeof import('../../habitat/docSession')>(); 12 + const actual = 13 + await importOriginal<typeof import('../../habitat/docSession')>(); 13 14 return { 14 15 ...actual, 15 16 acquireDocSession: vi.fn((uri: string) => {
+1 -2
src/documents/providers/habitatDocumentsProvider.ts
··· 45 45 released = true; 46 46 ydoc.destroy(); 47 47 }, 48 - getEditorBinding: () => 49 - createYjsBinding({ ydoc, collabProvider: null }), 48 + getEditorBinding: () => createYjsBinding({ ydoc, collabProvider: null }), 50 49 }; 51 50 } 52 51 }
+1 -3
src/editors/yjs/yjsDocumentEditor.tsx
··· 43 43 }), 44 44 ] 45 45 : []), 46 - ...(placeholder 47 - ? [Placeholder.configure({ placeholder })] 48 - : []), 46 + ...(placeholder ? [Placeholder.configure({ placeholder })] : []), 49 47 ], 50 48 [ydoc, collabProvider, user?.name, user?.color, placeholder], 51 49 );
+2 -1
src/filesystem/providers/habitatFilesystemProvider.ts
··· 46 46 ); 47 47 48 48 return records.map((record) => { 49 - const fallback = record.uri.split('/').filter(Boolean).pop() ?? record.uri; 49 + const fallback = 50 + record.uri.split('/').filter(Boolean).pop() ?? record.uri; 50 51 const name = record.value.name?.trim() ? record.value.name : fallback; 51 52 return { id: record.uri, name }; 52 53 });