| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 60 | 10 | 0% |
| Commands | 0 | 1 | 7 | 0% |
| Section tags | 2 | 5 | 4 | 18% |
What each file covers
Sections
0 shared · 60 only in A · 10 only in B- − Spacedrive Interface Development Rules
- − Core Principles
- − Package Architecture
- − What Lives Where
- − Current Structure
- − Architecture Layers
- − Code Style Rules
- − React 19 Standards
- − Critical: You Might Not Need an Effect
- − Examples
- − Function components only:
- − Color System Rules
- − CRITICAL: Always Use Semantic Tailwind Classes
- − Color Categories
- − Opacity Modifiers
- − Component Rules
- − Primitive vs Feature Components
- − Component Structure
- − Naming Conventions
- − Styling Rules
- − CRITICAL: Never Use Style Tags
- − Tailwind Class Order
- − Rounding (V2 Style)
- − Animation
- − Data Fetching Rules
- − Use Type-Safe Hooks
- − Never Fetch Manually
- − Performance Rules
- − Virtual Scrolling
- − Code Splitting
- − Memoization
- − Component Composition Rules
- − Dropdown Example (Current Implementation)
- − Type Safety Rules
- − Use Generated Types
- − Query Type Safety
- − File Organization Rules
- − Component Co-location
- − Exports
- − macOS-Specific Rules
- − Native Traffic Lights
- − Window Styling
- − Blur Effects
- − Current Architectural Decisions
- − 1. Expanding Dropdowns (Not Overlays)
- − 2. Library Switcher Logic
- − 3. Color System
- − 4. Rounded Style (V2)
- − Development Workflow
- − Before Writing Code
- − When Adding Features
- − When Styling
- − Common Patterns
- − Shell Entry Point Pattern
- − TopBar Portal Pattern
- − Library Switcher Pattern
- − Sidebar Item Pattern
- − Dropdown Pattern
- − Context Menu Pattern
- − Type-Safe Query Pattern
- + AGENTS.md - Spacedrive Core v2
- + Build/Test Commands
- + Code Style
- + Daemon Architecture
- + **The `Wire` Trait**
- + **Registration Macros**
- + **Registry System**
- + Logging Standards
- + Documentation
- + Debug Instructions
Commands
0 shared · 1 only in A · 7 only in B- − cargo run --bin generate_typescript_types
- + cargo build
- + cargo test
- + cargo test <test_name>
- + cargo test library_test
- + cargo clippy
- + cargo fmt
- + cargo run --bin sd-cli -- <command>
Section tags
2 shared · 5 only in A · 4 only in B- − architecture
- − types
- − ui
- − performance
- − do-not
- + build
- + test
- + lint-format
- + docs
- code-style
- agent-behaviour
Line diff
spacedriveapp/spacedrive · packages/interface/CLAUDE.md
@@ −1 @@
1# Spacedrive Interface Development Rules
2
3**Status:** Living Document - Update as architectural decisions are made
4**Purpose:** Ensure consistent, clean, and maintainable code across the interface package
5**Audience:** AI assistants and developers working on @sd/interface
6
7---
8
9## Core Principles
10
111. **Platform Agnostic** - This package works on Tauri, Web, and React Native
122. **Clean Separation** - UI components here, state in @sd/ts-client, primitives in @sd/ui
133. **Type Safety First** - Use auto-generated types, no `any`, strict TypeScript
144. **Performance Matters** - Virtual scrolling, code splitting, memoization when needed
155. **Accessible** - Radix primitives, proper ARIA labels, keyboard navigation
166. **Consistent Styling** - Semantic color system, no arbitrary values
17
18---
19
20## Package Architecture
21
22### What Lives Where
23
24**@sd/interface** (this package):
25- Route components and layouts
26- Feature components (Explorer, Settings, etc.)
27- React Query hook wrappers
28- UI composition and interactivity
29- NO state management (use @sd/ts-client)
30- NO primitive components (use @sd/ui)
31- NO platform APIs (use platform prop)
32
33**@sd/ts-client**:
34- Client implementation
35- Transport layer
36- Auto-generated types from Rust
37- State stores (if needed)
38
39**@sd/ui**:
40- Primitive components (Button, Input, DropdownMenu, etc.)
41- Reusable, unstyled or minimally styled
42- No business logic
43- No API calls
44
45### Current Structure
46
47```
48packages/interface/
49├── src/
50│ ├── Shell.tsx # App entry point (providers, daemon check)
51│ ├── ShellLayout.tsx # Layout shell (sidebar, inspector, TopBar)
52│ ├── router.tsx # Route configuration
53│ ├── DemoWindow.tsx # Demo/testing window
54│ ├── FloatingControls.tsx # Floating controls UI
55│ ├── components/
56│ │ ├── DndProvider.tsx # Drag-and-drop coordinator
57│ │ ├── Explorer/
58│ │ │ ├── ExplorerView.tsx # File browser view
59│ │ │ ├── context.tsx # Explorer state/context
60│ │ │ └── ...
61│ │ ├── QuickPreview/
62│ │ │ ├── Controller.tsx # Preview navigation
63│ │ │ ├── Syncer.tsx # Selection sync
64│ │ │ └── ...
65│ │ └── ...
66│ ├── TopBar/ # TopBar portal system
67│ │ ├── TopBar.tsx
68│ │ ├── Context.tsx
69│ │ └── Portal.tsx
70│ ├── routes/ # Route components
71│ │ └── overview/
72│ ├── hooks/ # React hooks
73│ ├── context.tsx # Client context and hooks
74│ ├── styles.css # Global CSS variables
75│ └── index.tsx # Public exports
76```
77
78### Architecture Layers
79
80The interface is organized into clear separation of concerns:
81
82**Shell Layer** (`Shell.tsx`):
83- Root entry point
84- Provider setup (Spacedrive, Server, TabManager, Platform)
85- Daemon connection management (Tauri-specific)
86
87**Layout Layer** (`ShellLayout.tsx`):
88- Chrome/frame (sidebar, inspector, TopBar containers)
89- Provider setup (TopBar, Selection, Explorer)
90- Tab bar positioning
91- QuickPreview coordination
92
93**View Layer** (routes like `ExplorerView.tsx`):
94- Actual content rendering
95- TopBar button registration (via portal)
96- Feature-specific logic
97
98**Coordination Layer**:
99- `DndProvider.tsx` - Global drag-and-drop
100- `QuickPreview/Controller.tsx` - Preview navigation
101- `QuickPreview/Syncer.tsx` - Selection-to-preview sync
102
103**Visual Hierarchy:**
104```
105Shell (providers) → DndProvider → Router
106 ↓
107 ShellLayout (chrome)
108 ↓
109 <Outlet> (routes)
110 ↓
111 Overview | ExplorerView | Settings | etc.
112```
113
114---
115
116## Code Style Rules
117
118### React 19 Standards
119
120### Critical: You Might Not Need an Effect
121
122**Effects are an escape hatch** - only use them to sync with external systems (network, DOM, browser APIs).
123
124**DON'T use Effects for:**
125- Transforming data for rendering (calculate during render instead)
126- Handling user events (use event handlers)
127- Updating state based on props (calculate during render or use `key`)
128- Chains of state updates (do in event handler)
129- Initializing app (use module-level code)
130- Notifying parent of changes (pass callback, call in event handler)
131
132**DO use Effects for:**
133- Subscribing to external systems (WebSocket, browser events)
134- Syncing with non-React widgets
135- Network requests with proper cleanup
136
137### Examples
138
139**Wrong - Don't use Effect to transform data:**
140```tsx
141function TodoList({ todos, filter }) {
142 const [visibleTodos, setVisibleTodos] = useState([]);
143 useEffect(() => {
144 setVisibleTodos(getFilteredTodos(todos, filter));
145 }, [todos, filter]);
146 // Extra render pass!
147}
148```
149
150**Correct - Calculate during render:**
151```tsx
152function TodoList({ todos, filter }) {
153 const visibleTodos = getFilteredTodos(todos, filter);
154 // Or use useMemo if expensive:
155 const visibleTodos = useMemo(
156 () => getFilteredTodos(todos, filter),
157 [todos, filter]
158 );
159}
160```
161
162**Wrong - Don't use Effect for user events:**
163```tsx
164function ProductPage({ product, addToCart }) {
165 useEffect(() => {
166 if (product.isInCart) {
167 showNotification('Added to cart!');
168 }
169 }, [product]);
170}
171```
172
173**Correct - Use event handler:**
174```tsx
175function ProductPage({ product, addToCart }) {
176 function buyProduct() {
177 addToCart(product);
178 showNotification('Added to cart!');
179 }
180}
181```
182
183**Wrong - Don't use Effect to update parent:**
184```tsx
185function Toggle({ onChange }) {
186 const [isOn, setIsOn] = useState(false);
187 useEffect(() => {
188 onChange(isOn); // Too late! Extra render.
189 }, [isOn, onChange]);
190}
191```
192
193**Correct - Call in event handler:**
194```tsx
195function Toggle({ onChange }) {
196 const [isOn, setIsOn] = useState(false);
197 function updateToggle(nextIsOn) {
198 setIsOn(nextIsOn);
199 onChange(nextIsOn); // Same render pass!
200 }
201}
202```
203
204**Wrong - Don't chain Effects:**
205```tsx
206useEffect(() => {
207 if (card.gold) setGoldCardCount(c => c + 1);
208}, [card]);
209
210useEffect(() => {
211 if (goldCardCount > 3) setRound(r => r + 1);
212}, [goldCardCount]);
213// Multiple render passes!
214```
215
216**Correct - Calculate in event handler:**
217```tsx
218function handlePlaceCard(nextCard) {
219 setCard(nextCard);
220 if (nextCard.gold) {
221 if (goldCardCount < 3) {
222 setGoldCardCount(goldCardCount + 1);
223 } else {
224 setGoldCardCount(0);
225 setRound(round + 1);
226 }
227 }
228 // Single render pass!
229}
230```
231
232### Function components only:
233```tsx
234// Correct
235function Component({ name }: { name: string }) {
236 return <div>{name}</div>;
237}
238
239// Wrong
240const Component: React.FC<{ name: string }> = ({ name }) => {
241 return <div>{name}</div>;
242};
243```
244
245**Hooks must follow rules:**
246```tsx
247// Correct - proper cleanup
248useEffect(() => {
249 const subscription = subscribe();
250 return () => subscription.unsubscribe();
251}, [dependency]);
252
253// Wrong - missing cleanup
254useEffect(() => {
255 subscribe();
256}, []);
257```
258
259**Use TypeScript strictly:**
260```tsx
261// Correct - explicit types
262interface ButtonProps {
263 label: string;
264 onClick: () => void;
265}
266
267function Button({ label, onClick }: ButtonProps) { }
268
269// Wrong - implicit any
270function Button(props) { }
271```
272
273---
274
275## Color System Rules
276
277### CRITICAL: Always Use Semantic Tailwind Classes
278
279Never use `var()` syntax directly. Always use Tailwind's semantic color classes.
280
281**WRONG:**
282```tsx
283className="bg-[var(--color-sidebar)]"
284className="text-[var(--color-sidebar-ink)]"
285className="border-[var(--color-accent)]"
286```
287
288**CORRECT:**
289```tsx
290className="bg-sidebar"
291className="text-sidebar-ink"
292className="border-accent"
293```
294
295**IMPORTANT:** CSS variables must be defined as comma-separated HSL values (not wrapped in `hsl()`):
296```css
297/* CORRECT - bare values for Tailwind */
298--color-sidebar: 235, 15%, 7%;
299
300/* WRONG - wrapped in hsl() */
301--color-sidebar: hsl(235, 15%, 7%);
302```
303
304This is because Tailwind uses `hsla(var(--color-sidebar), <alpha-value>)` which becomes `hsla(235, 15%, 7%, 0.5)` for opacity support.
305
306### Color Categories
307
308**Accent:** `accent`, `accent-faint`, `accent-deep`
309- Use for: Primary actions, selections, focus states
310
311**Text (Ink):** `ink`, `ink-dull`, `ink-faint`
312- Use for: Text hierarchy (primary, secondary, tertiary)
313
314**Sidebar:** `sidebar`, `sidebar-box`, `sidebar-line`, `sidebar-ink`, `sidebar-selected`, etc.
315- Use for: Sidebar-specific elements
316
317**App:** `app`, `app-box`, `app-line`, `app-hover`, `app-selected`, etc.
318- Use for: Main content area elements
319
320**Menu:** `menu`, `menu-line`, `menu-hover`, `menu-ink`, etc.
321- Use for: Dropdowns, context menus
322
323### Opacity Modifiers
324
325```tsx
326// Use Tailwind opacity
327className="bg-accent/10"
328className="bg-sidebar/65"
329
330// Don't use manual alpha
331className="bg-[var(--color-accent)]/10"
332```
333
334---
335
336## Component Rules
337
338### Primitive vs Feature Components
339
340**Primitives** (@sd/ui):
341- Generic, reusable
342- Minimal styling (or unstyled)
343- No business logic
344- Example: `DropdownMenu`, `Button`, `Input`
345
346**Feature Components** (@sd/interface):
347- Specific to Spacedrive features
348- Uses primitives
349- Can have business logic
350- Example: `Explorer`, `Sidebar`, `LibrariesDropdown`
351
352### Component Structure
353
354```tsx
355// Correct structure
356import { Primitive } from '@sd/ui';
357import { useSomeQuery } from '../context';
358
359interface ComponentProps {
360 // Props interface
361}
362
363function Component({ prop }: ComponentProps) {
364 // Hooks first
365 const data = useSomeQuery();
366
367 // Logic
368 const derived = useMemo(() => transform(data), [data]);
369
370 // Render
371 return (
372 <Primitive className="semantic-colors">
373 {/* Content */}
374 </Primitive>
375 );
376}
377
378export { Component };
379```
380
381### Naming Conventions
382
383- **Files:** `PascalCase.tsx` for components, `camelCase.ts` for utilities
384- **Components:** `PascalCase` functions
385- **Hooks:** `useCamelCase` pattern
386- **Constants:** `SCREAMING_SNAKE_CASE`
387- **CSS classes:** Semantic names only (`bg-sidebar`, not `bg-gray-900`)
388
389---
390
391## Styling Rules
392
393### CRITICAL: Never Use Style Tags
394
395**NEVER** use `<style>`, `<style jsx>`, or any inline style tags. Always use Tailwind utility classes.
396
397**WRONG:**
398```tsx
399<style jsx>{`
400 .slider::-webkit-slider-thumb {
401 background: var(--color-accent);
402 }
403`}</style>
404```
405
406**CORRECT:**
407```tsx
408className="[&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:rounded-full"
409```
410
411Use Tailwind's arbitrary variant syntax for pseudo-elements and other edge cases.
412
413### Tailwind Class Order
414
415Follow this order for readability:
4161. Layout (`flex`, `grid`, `w-full`, `h-screen`)
4172. Spacing (`p-4`, `m-2`, `gap-2`)
4183. Typography (`text-sm`, `font-medium`)
4194. Colors (`bg-sidebar`, `text-ink`)
4205. Borders (`border`, `border-sidebar-line`, `rounded-lg`)
4216. Effects (`shadow-sm`, `backdrop-blur`)
4227. States (`hover:bg-app-hover`, `focus:ring-accent`)
4238. Transitions (`transition-colors`)
424
425### Rounding (V2 Style)
426
427V2 is more rounded than V1. Use:
428- `rounded-lg` for most containers (8px)
429- `rounded-md` for smaller elements (6px)
430- `rounded-full` for pills/badges
431- `rounded-[10px]` for window frame
432
433### Animation
434
435Use framer-motion for complex animations:
436```tsx
437import { motion, AnimatePresence } from 'framer-motion';
438
439<AnimatePresence>
440 {isOpen && (
441 <motion.div
442 initial={{ height: 0, opacity: 0 }}
443 animate={{ height: 'auto', opacity: 1 }}
444 exit={{ height: 0, opacity: 0 }}
445 transition={{ duration: 0.15, ease: [0.25, 1, 0.5, 1] }}
446 >
447 {content}
448 </motion.div>
449 )}
450</AnimatePresence>
451```
452
453---
454
455## Data Fetching Rules
456
457### Use Type-Safe Hooks
458
459**Core queries** (no library required):
460```tsx
461import { useCoreQuery } from '../context';
462
463const { data: libraries } = useCoreQuery({
464 type: 'libraries.list',
465 input: { include_stats: false },
466});
467```
468
469**Library queries** (requires library context):
470```tsx
471import { useLibraryQuery } from '../context';
472
473const { data: files } = useLibraryQuery({
474 type: 'files.directory_listing',
475 input: { path: '/' },
476});
477```
478
479**Mutations:**
480```tsx
481import { useCoreMutation, useLibraryMutation } from '../context';
482
483const createLib = useCoreMutation('libraries.create');
484const applyTags = useLibraryMutation('tags.apply');
485const copyFiles = useLibraryMutation('files.copy');
486const deleteFiles = useLibraryMutation('files.delete');
487
488// Use mutations, not client.execute()
489createLib.mutate({ name: 'New Library', path: null });
490await copyFiles.mutateAsync({
491 sources: { paths: [path1, path2] },
492 destination: destPath,
493 overwrite: false,
494 verify_checksum: false,
495 preserve_timestamps: true,
496 move_files: false,
497 copy_method: "Auto"
498});
499```
500
501### Never Fetch Manually
502
503**Wrong:**
504```tsx
505const [data, setData] = useState();
506useEffect(() => {
507 fetchData().then(setData);
508}, []);
509```
510
511**Correct:**
512```tsx
513const { data } = useCoreQuery({ type: 'operation', input: {} });
514```
515
516---
517
518## Performance Rules
519
520### Virtual Scrolling
521
522Use for lists > 100 items:
523```tsx
524import { useVirtualizer } from '@tanstack/react-virtual';
525
526const virtualizer = useVirtualizer({
527 count: items.length,
528 getScrollElement: () => parentRef.current,
529 estimateSize: () => 50,
530});
531```
532
533### Code Splitting
534
535Lazy load routes:
536```tsx
537const SettingsPage = lazy(() => import('./Settings'));
538
539<Suspense fallback={<Spinner />}>
540 <SettingsPage />
541</Suspense>
542```
543
544### Memoization
545
546Only when actually needed:
547```tsx
548// Expensive computation
549const sorted = useMemo(
550 () => items.sort(expensiveCompare),
551 [items]
552);
553
554// Premature optimization
555const greeting = useMemo(() => `Hello ${name}`, [name]);
556```
557
558---
559
560## Component Composition Rules
561
562### Dropdown Example (Current Implementation)
563
564The `DropdownMenu` primitive provides minimal base functionality. Explorer customizes it:
565
566```tsx
567// Primitive (in @sd/ui/DropdownMenu.tsx)
568export const DropdownMenu = {
569 Root: ({ trigger, children, className }) => (
570 // Minimal expanding container with motion
571 ),
572 Item: ({ children, onClick, className }) => (
573 // Basic button with flex layout
574 ),
575 Separator: ({ className }) => (
576 // Simple divider
577 ),
578};
579
580// Usage (in Explorer.tsx)
581<DropdownMenu.Root
582 trigger={
583 <button className="w-full bg-sidebar-box border-sidebar-line rounded-lg">
584 {currentLibrary?.name}
585 </button>
586 }
587 className="bg-sidebar-box border-sidebar-line rounded-lg"
588>
589 <DropdownMenu.Item
590 className="px-2 py-1 rounded-md hover:bg-sidebar-selected"
591 onClick={() => switchLibrary(lib.id)}
592 >
593 {lib.name}
594 </DropdownMenu.Item>
595</DropdownMenu.Root>
596```
597
598**Key principles:**
5991. Primitive has minimal/no styling
6002. All visual styling applied via className prop
6013. Business logic (filtering, selecting) in parent component
6024. Semantic color classes only
603
604---
605
606## Type Safety Rules
607
608### Use Generated Types
609
610All types are auto-generated from Rust:
611```tsx
612import type { LibraryInfo, CoreQuery, LibraryAction } from '@sd/ts-client';
613```
614
615**Never:**
616- Define manual type interfaces that duplicate Rust types
617- Use `any` (use `unknown` with type guards if needed)
618- Ignore TypeScript errors
619
620### Query Type Safety
621
622The hooks automatically infer types:
623```tsx
624// TypeScript knows data is LibraryInfo[]
625const { data } = useCoreQuery({
626 type: 'libraries.list',
627 input: { include_stats: false },
628});
629
630// data is automatically typed based on the operation!
631```
632
633---
634
635## File Organization Rules
636
637### Component Co-location
638
639```
640Explorer/
641├── index.tsx # Main component
642├── Sidebar.tsx # Sub-component
643├── TopBar.tsx # Sub-component
644└── hooks/
645 └── useExplorer.ts # Feature-specific hooks
646```
647
648### Exports
649
650Only export what's needed:
651```tsx
652// index.tsx
653export { Shell } from './Shell';
654export { DemoWindow } from './DemoWindow';
655// Don't export everything
656```
657
658---
659
660## macOS-Specific Rules
661
662### Native Traffic Lights
663
664The window uses **native** macOS traffic lights positioned by Swift code:
665- Traffic lights are real, functional native controls
666- Content must have `pt-[52px]` to avoid overlap
667- No fake CSS traffic lights
668- Transparent titlebar + invisible toolbar trick (see sd-desktop-macos crate)
669
670### Window Styling
671
672```tsx
673// Correct - accounts for native traffic lights
674<nav className="pt-[52px] ...">
675 {/* Content starts below traffic lights */}
676</nav>
677
678// Correct - window frame with rounded corners
679<div className="rounded-[10px] border-transparent frame">
680 {/* App content */}
681</div>
682```
683
684### Blur Effects
685
686Use backdrop blur for macOS native feel:
687```tsx
688className="backdrop-blur-lg bg-sidebar/65"
689```
690
691---
692
693## Current Architectural Decisions
694
695### 1. Expanding Dropdowns (Not Overlays)
696
697Decision: Dropdowns should expand inline and push content down, not overlay it.
698
699Implementation:
700- Use `framer-motion` for smooth height animation
701- No Radix Portal (renders inline in DOM)
702- Pushes surrounding content naturally
703
704### 2. Library Switcher Logic
705
706Decision: Show/hide current library based on count.
707
708Rules:
709- **1 library:** Hide current from dropdown (no point showing it)
710- **2+ libraries:** Show all including current (with highlight)
711- Always show "New Library" and "Library Settings"
712
713### 3. Color System
714
715Decision: Use Tailwind semantic classes, never `var()` directly.
716
717```tsx
718// Correct
719className="bg-sidebar-box text-sidebar-ink border-sidebar-line"
720
721// Wrong
722className="bg-[var(--color-sidebar-box)]"
723```
724
725### 4. Rounded Style (V2)
726
727Decision: V2 is more rounded than V1.
728
729- Containers: `rounded-lg` (8px)
730- Small elements: `rounded-md` (6px)
731- Window: `rounded-[10px]`
732- Pills/badges: `rounded-full`
733
734---
735
736## Development Workflow
737
738### Before Writing Code
739
7401. Check if primitive exists in @sd/ui
7412. Check if types are auto-generated (they probably are)
7423. Plan component composition (primitive + styling)
7434. Use semantic color classes
744
745### When Adding Features
746
7471. Create minimal primitive in @sd/ui if needed
7482. Use primitive in @sd/interface with styling
7493. Use type-safe queries/mutations
7504. Add to this document if architectural decision made
751
752### When Styling
753
7541. Use semantic colors (`bg-sidebar`, not `bg-gray-900`)
7552. Follow V2 rounded style
7563. Use opacity modifiers (`bg-accent/10`)
7574. Maintain color context (sidebar colors in sidebar, app colors in main area)
758
759---
760
761## Common Patterns
762
763### Shell Entry Point Pattern
764
765The app entry point follows a clean provider hierarchy:
766
767```tsx
768// Shell.tsx
769export function Shell({ client }: { client: SpacedriveClient }) {
770 const platform = usePlatform();
771
772 return (
773 <SpacedriveProvider client={client}>
774 <ServerProvider>
775 <TabManagerProvider routes={explorerRoutes}>
776 <TabKeyboardHandler />
777 <DndProvider>
778 <RouterProvider router={router} />
779 </DndProvider>
780 </TabManagerProvider>
781 </ServerProvider>
782 </SpacedriveProvider>
783 );
784}
785
786// ShellLayout renders inside router, provides layout chrome
787// Routes (ExplorerView, Overview, etc.) render inside <Outlet />
788```
789
790### TopBar Portal Pattern
791
792Views register their TopBar buttons via portal:
793
794```tsx
795// ExplorerView.tsx
796import { TopBarPortal } from '../../TopBar';
797
798function ExplorerView() {
799 return (
800 <>
801 <TopBarPortal
802 left={<BackButton />}
803 center={<PathBar />}
804 right={<ViewControls />}
805 />
806 <div>{/* View content */}</div>
807 </>
808 );
809}
810```
811
812### Library Switcher Pattern
813
814```tsx
815const client = useSpacedriveClient();
816const { data: libraries } = useLibraries();
817const [currentLibraryId, setCurrentLibraryId] = useState<string | null>(null);
818
819// Auto-select first library
820useEffect(() => {
821 if (libraries && libraries.length > 0 && !currentLibraryId) {
822 client.setCurrentLibrary(libraries[0].id);
823 setCurrentLibraryId(libraries[0].id);
824 }
825}, [libraries, currentLibraryId, client]);
826
827// Switch library
828const handleSwitch = (id: string) => {
829 client.setCurrentLibrary(id);
830 setCurrentLibraryId(id);
831};
832```
833
834### Sidebar Item Pattern
835
836```tsx
837function SidebarItem({ icon: Icon, label, active }: Props) {
838 return (
839 <button
840 className={clsx(
841 "flex items-center gap-2 px-2 py-1 rounded-md text-sm font-medium",
842 active
843 ? "bg-sidebar-selected text-sidebar-ink"
844 : "text-sidebar-inkDull hover:text-sidebar-ink"
845 )}
846 >
847 <Icon className="size-4" weight={active ? "fill" : "bold"} />
848 <span className="truncate">{label}</span>
849 </button>
850 );
851}
852```
853
854### Dropdown Pattern
855
856```tsx
857<DropdownMenu.Root
858 trigger={<button className="...">Trigger</button>}
859 className="bg-sidebar-box border-sidebar-line rounded-lg"
860>
861 <DropdownMenu.Item
862 className="px-2 py-1 hover:bg-sidebar-selected"
863 onClick={() => action()}
864 >
865 Item content
866 </DropdownMenu.Item>
867 <DropdownMenu.Separator className="border-sidebar-line" />
868</DropdownMenu.Root>
869```
870
871### Context Menu Pattern
872
873Use `useContextMenu` hook for platform-agnostic context menus:
874
875```tsx
876import { useContextMenu } from '../hooks/useContextMenu';
877import { Copy, Trash } from '@phosphor-icons/react';
878
879const { selectedFiles } = useExplorer();
880const copyFiles = useLibraryMutation('files.copy');
881const deleteFiles = useLibraryMutation('files.delete');
882
883const contextMenu = useContextMenu({
884 items: [
885 {
886 icon: Copy,
887 label: selectedFiles.length > 1 ? `Copy ${selectedFiles.length} items` : "Copy",
888 onClick: async () => {
889 await copyFiles.mutateAsync({
890 sources: { paths: selectedFiles.map(f => f.sd_path) },
891 destination: currentPath,
892 overwrite: false,
893 verify_checksum: false,
894 preserve_timestamps: true,
895 move_files: false,
896 copy_method: "Auto"
897 });
898 },
899 keybind: "⌘C",
900 condition: () => selectedFiles.length > 0, // Only show if files selected
901 },
902 { type: "separator" },
903 {
904 icon: Trash,
905 label: "Delete",
906 onClick: async () => {
907 await deleteFiles.mutateAsync({
908 targets: { paths: selectedFiles.map(f => f.sd_path) },
909 permanent: false,
910 recursive: true
911 });
912 },
913 keybind: "⌘⌫",
914 variant: "danger"
915 }
916 ]
917});
918
919return <div onContextMenu={contextMenu.show}>Content</div>;
920```
921
922**Key features:**
923- Platform-agnostic (native on Tauri, Radix on web)
924- Conditional items via `condition` callback
925- Smart labels that update based on state
926- Supports icons, keybinds, variants, submenus, separators
927- Use `useLibraryMutation` for actions, not `client.execute()`
928
929---
930
931## Type-Safe Query Pattern
932
933### Query Keys
934
935Use descriptive, hierarchical keys:
936```tsx
937// Good
938queryKey: ['libraries', 'list']
939queryKey: ['files', 'directory', libraryId, path]
940
941// Bad
942queryKey: ['getLibraries']
943queryKey: ['data']
944```
945
946### Using Queries
947
948```tsx
949const { data, isLoading, error } = useCoreQuery({
950 type: 'libraries.list',
951 input: { include_stats: true },
952});
953
954// data is automatically typed as LibraryInfo[]!
955```
956
957---
958
959## Testing Requirements
960
961### Critical Paths Must Be Tested
962
963- Explorer file operations
964- Library switching
965- Settings mutations
966- Search functionality
967
968### Test Pattern
969
970```tsx
971import { render, screen } from '@testing-library/react';
972import { Shell } from './Shell';
973
974test('switches libraries', async () => {
975 const user = userEvent.setup();
976 render(<Shell client={mockClient} />);
977
978 await user.click(screen.getByText('Switch Library'));
979 // ...
980});
981```
982
983---
984
985## Migration from V1
986
987When porting V1 components:
988
9891. **Update colors:** `bg-gray-900` → `bg-app`, `text-gray-400` → `text-ink-dull`
9902. **Use primitives:** Extract reusable parts to @sd/ui
9913. **Remove state:** Move to @sd/ts-client if global, use local state if component-specific
9924. **Update queries:** Use new type-safe hooks
9935. **Add rounding:** V1 used `rounded-md`, V2 uses `rounded-lg`
994
995---
996
997## Checklist Before PR
998
999- [ ] All colors use semantic classes (no `var()` directly)
1000- [ ] Component uses primitives from @sd/ui where applicable
1001- [ ] Type-safe queries/mutations (no manual fetch)
1002- [ ] Follows V2 rounded style
1003- [ ] No `any` types
1004- [ ] Proper cleanup in useEffect
1005- [ ] Accessible (keyboard nav, ARIA labels)
1006- [ ] Tested critical paths
1007
1008---
1009
1010## Quick Reference
1011
1012### Import Order
1013
1014```tsx
1015// 1. External libraries
1016import { useState } from 'react';
1017import { motion } from 'framer-motion';
1018
1019// 2. @sd packages
1020import { Button, DropdownMenu } from '@sd/ui';
1021import { useCoreQuery } from '@sd/ts-client';
1022
1023// 3. Local imports
1024import { useLibraries } from './hooks/useLibraries';
1025import clsx from 'clsx';
1026```
1027
1028### Common Mistakes
1029
1030`<style>` or `<style jsx>` tags → Use Tailwind arbitrary variants
1031`className="bg-[var(--color-sidebar)]"` → `className="bg-sidebar"`
1032`bg-gray-900` → `bg-app`
1033`rounded-md` everywhere → `rounded-lg` for V2
1034Manual fetch → Use type-safe hooks
1035State in component → Use @sd/ts-client or local state
1036
1037---
1038
1039## Questions to Ask
1040
1041Before writing code:
1042
10431. **Is this a primitive?** → Should it be in @sd/ui?
10442. **Is this state global?** → Should it be in @sd/ts-client?
10453. **Are the types auto-generated?** → Don't duplicate them!
10464. **Can I use a semantic color?** → Yes, always!
10475. **Is this accessible?** → Keyboard nav? ARIA labels?
1048
1049---
1050
1051## Resources
1052
1053- **Type Generation:** `cargo run --bin generate_typescript_types`
1054- **Color System:** `/docs/react/ui/colors.mdx`
1055- **Workbench Docs:** `/workbench/interface/`
1056- **V1 Reference:** `/Users/jamespine/Projects/spacedrive_v1`
1057
1058---
1059
1060## Status: Current Implementation
1061
1062**Complete:**
1063- Type-safe client with auto-generated types
1064- Native macOS traffic lights
1065- V1 color system as CSS variables
1066- Expanding dropdown (DropdownMenu primitive)
1067- Explorer with sidebar and library switcher
1068- TanStack Query integration
1069- Clean architecture refactor (Shell → ShellLayout → Views)
1070- Extracted DndProvider for drag-and-drop coordination
1071- QuickPreview components (Controller + Syncer)
1072- TopBar portal system for view-specific controls
1073
1074**In Progress:**
1075- Port remaining V1 components
1076- Build complete Explorer (file grid/list views)
1077- Settings pages
1078- Multi-window system
1079
1080---
1081
1082**Remember:** This is a living document. Update it when architectural decisions are made. This is our rulebook for building a world-class file manager interface!
spacedriveapp/spacedrive · core/AGENTS.md
@@ +1 @@
1# AGENTS.md - Spacedrive Core v2
2
3## Build/Test Commands
4
5- `cargo build` - Build the project
6- `cargo test` - Run all tests
7- `cargo test <test_name>` - Run specific test (e.g., `cargo test library_test`)
8- `cargo clippy` - Lint code
9- `cargo fmt` - Format code
10- `cargo run --bin sd-cli -- <command>` - Run CLI (note: binary is `sd-cli`, not `spacedrive`)
11
12## Code Style
13
14- **Imports**: Group std, external crates, then local modules with blank lines between
15- **Formatting**: Use `cargo fmt` - tabs for indentation, snake_case for variables/functions. DO NOT use emojis at all.
16- **Types**: Explicit types preferred, use `Result<T, E>` for error handling with `thiserror`
17- **Naming**: snake_case for functions/variables, PascalCase for types, SCREAMING_SNAKE_CASE for constants
18- **Error Handling**: Use `Result` types, `thiserror` for custom errors, `anyhow` for application errors
19- **Async**: Use `async/await`, prefer `tokio` primitives, avoid blocking operations
20- **Resumable Jobs**: For long-running jobs that need to be resumable, store the job's state within the job's struct itself. Use `#[serde(skip)]` for fields that should not be persisted. For example, in a file copy job, the list of already copied files can be stored to allow the job to resume from where it left off.
21- **Documentation**: Use `//!` for module docs, `///` for public items, include examples
22- **Architecture**: Follow a Command Query Responsibility Segregation (CQRS) and Domain-Driven Design (DDD) pattern.
23 - **Domain**: Core data structures and business logic are located in `src/domain/`. These are the "nouns" of your system.
24 - **Operations**: State-changing commands (actions) and data-retrieving queries are located in `src/ops/`. These are the "verbs" of your system.
25 - **Actions**: Operations that modify the state of the application. They should be self-contained and transactional.
26 - **Queries**: Operations that retrieve data without modifying state. They should be efficient and optimized for reading.
27- **Feature Modules**: Each new feature should be implemented in its own module within the `src/ops/` directory. For example, a new "share" feature would live in `src/ops/files/share`. Each feature module should contain the following files where applicable:
28 - `action.rs`: The main logic for the state-changing operation.
29 - `input.rs`: Data structures for the action's input.
30 - `output.rs`: Data structures for the action's output.
31 - `job.rs`: If the action is long-running, the job implementation.
32- **Database**: Use SeaORM entities, async queries, proper error propagation
33- **Comments**: Minimal inline comments, focus on why not what, no TODO comments in production code
34
35## Daemon Architecture
36
37Spacedrive uses a **daemon-client architecture** where a single daemon process manages the core functionality and multiple client applications (CLI, GraphQL server, desktop app) connect to it via Unix domain sockets.
38
39> **For detailed daemon architecture documentation, see [/docs/core/daemon.md](/docs/core/daemon.md)**
40
41### **The `Wire` Trait**
42
43All actions and queries must implement the `Wire` trait to enable type-safe client-daemon communication:
44
45```rust
46pub trait Wire {
47 const METHOD: &'static str;
48}
49```
50
51### **Registration Macros**
52
53Instead of manually implementing `Wire`, use these registration macros that automatically:
54
551. Implement the `Wire` trait with the correct method string
562. Register the operation in the global registry using the `inventory` crate
57
58**For Queries:**
59
60```rust
61crate::register_query!(NetworkStatusQuery, "network.status");
62// Generates method: "query:network.status"
63```
64
65**For Library Actions:**
66
67```rust
68crate::register_library_action!(FileCopyAction, "files.copy");
69// Generates method: "action:files.copy.input"
70```
71
72**For Core Actions:**
73
74```rust
75crate::register_core_action!(LibraryCreateAction, "libraries.create");
76// Generates method: "action:libraries.create.input"
77```
78
79### **Registry System**
80
81- **Location**: `core/src/ops/registry.rs`
82- **Mechanism**: Uses the `inventory` crate for compile-time registration
83- **Global Maps**: `QUERIES` and `ACTIONS` hashmaps populated at startup
84- **Handler Functions**: Generic handlers that decode payloads, execute operations, and encode results
85
86## Logging Standards
87
88- **Setup**: Use `tracing_subscriber::fmt()` with env filter for structured logging
89- **Macros**: Use `info!`, `warn!`, `error!`, `debug!` from `tracing` crate, not `println!`
90- **Job Context**: Use `ctx.log()` in jobs for job-specific logging with automatic job_id tagging
91- **Structured**: Include relevant context fields: `debug!(job_id = %self.id, "message")`
92- **Levels**: debug for detailed flow, info for user-relevant events, warn for recoverable issues, error for failures
93- **Format**: `tracing_subscriber::fmt().with_env_filter(env_filter).init()` in main/examples
94- **Environment**: Respect `RUST_LOG` env var, fallback to module-specific filters like `sd_core=info`
95
96## Documentation
97
98- **Core level docs**: Live in `/docs/core` - comprehensive architecture and implementation guides
99- **Core design docs**: Live in `/docs/core/design` - planning documents, RFCs, and design decisions
100- **Application level docs**: Live in `/docs`
101- **Code docs**: Use `///` for public APIs, `//!` for module overviews, include examples
102
103## Debug Instructions
104
105- You can view the logs of a job in the job_logs directory in the root of the data folder
106- When testing the CLI, after compiling you must first use the `restart` command to ensure the Spacedrive daemon is using the latest build.
107
@@ −1 +1 @@
1−# Spacedrive Interface Development Rules
1+# AGENTS.md - Spacedrive Core v2
22
3−**Status:** Living Document - Update as architectural decisions are made
4−**Purpose:** Ensure consistent, clean, and maintainable code across the interface package
5−**Audience:** AI assistants and developers working on @sd/interface
3+## Build/Test Commands
64
7−---
5+- `cargo build` - Build the project
6+- `cargo test` - Run all tests
7+- `cargo test <test_name>` - Run specific test (e.g., `cargo test library_test`)
8+- `cargo clippy` - Lint code
9+- `cargo fmt` - Format code
10+- `cargo run --bin sd-cli -- <command>` - Run CLI (note: binary is `sd-cli`, not `spacedrive`)
811
9−## Core Principles
12+## Code Style
1013
11−1. **Platform Agnostic** - This package works on Tauri, Web, and React Native
12−2. **Clean Separation** - UI components here, state in @sd/ts-client, primitives in @sd/ui
13−3. **Type Safety First** - Use auto-generated types, no `any`, strict TypeScript
14−4. **Performance Matters** - Virtual scrolling, code splitting, memoization when needed
15−5. **Accessible** - Radix primitives, proper ARIA labels, keyboard navigation
16−6. **Consistent Styling** - Semantic color system, no arbitrary values
14+- **Imports**: Group std, external crates, then local modules with blank lines between
15+- **Formatting**: Use `cargo fmt` - tabs for indentation, snake_case for variables/functions. DO NOT use emojis at all.
16+- **Types**: Explicit types preferred, use `Result<T, E>` for error handling with `thiserror`
17+- **Naming**: snake_case for functions/variables, PascalCase for types, SCREAMING_SNAKE_CASE for constants
18+- **Error Handling**: Use `Result` types, `thiserror` for custom errors, `anyhow` for application errors
19+- **Async**: Use `async/await`, prefer `tokio` primitives, avoid blocking operations
20+- **Resumable Jobs**: For long-running jobs that need to be resumable, store the job's state within the job's struct itself. Use `#[serde(skip)]` for fields that should not be persisted. For example, in a file copy job, the list of already copied files can be stored to allow the job to resume from where it left off.
21+- **Documentation**: Use `//!` for module docs, `///` for public items, include examples
22+- **Architecture**: Follow a Command Query Responsibility Segregation (CQRS) and Domain-Driven Design (DDD) pattern.
23+ - **Domain**: Core data structures and business logic are located in `src/domain/`. These are the "nouns" of your system.
24+ - **Operations**: State-changing commands (actions) and data-retrieving queries are located in `src/ops/`. These are the "verbs" of your system.
25+ - **Actions**: Operations that modify the state of the application. They should be self-contained and transactional.
26+ - **Queries**: Operations that retrieve data without modifying state. They should be efficient and optimized for reading.
27+- **Feature Modules**: Each new feature should be implemented in its own module within the `src/ops/` directory. For example, a new "share" feature would live in `src/ops/files/share`. Each feature module should contain the following files where applicable:
28+ - `action.rs`: The main logic for the state-changing operation.
29+ - `input.rs`: Data structures for the action's input.
30+ - `output.rs`: Data structures for the action's output.
31+ - `job.rs`: If the action is long-running, the job implementation.
32+- **Database**: Use SeaORM entities, async queries, proper error propagation
33+- **Comments**: Minimal inline comments, focus on why not what, no TODO comments in production code
1734
18−---
35+## Daemon Architecture
1936
20−## Package Architecture
37+Spacedrive uses a **daemon-client architecture** where a single daemon process manages the core functionality and multiple client applications (CLI, GraphQL server, desktop app) connect to it via Unix domain sockets.
2138
22−### What Lives Where
39+> **For detailed daemon architecture documentation, see [/docs/core/daemon.md](/docs/core/daemon.md)**
2340
24−**@sd/interface** (this package):
25−- Route components and layouts
26−- Feature components (Explorer, Settings, etc.)
27−- React Query hook wrappers
28−- UI composition and interactivity
29−- NO state management (use @sd/ts-client)
30−- NO primitive components (use @sd/ui)
31−- NO platform APIs (use platform prop)
41+### **The `Wire` Trait**
3242
33−**@sd/ts-client**:
34−- Client implementation
35−- Transport layer
36−- Auto-generated types from Rust
37−- State stores (if needed)
43+All actions and queries must implement the `Wire` trait to enable type-safe client-daemon communication:
3844
39−**@sd/ui**:
40−- Primitive components (Button, Input, DropdownMenu, etc.)
41−- Reusable, unstyled or minimally styled
42−- No business logic
43−- No API calls
44−
45−### Current Structure
46−
47−```
48−packages/interface/
49−├── src/
50−│ ├── Shell.tsx # App entry point (providers, daemon check)
51−│ ├── ShellLayout.tsx # Layout shell (sidebar, inspector, TopBar)
52−│ ├── router.tsx # Route configuration
53−│ ├── DemoWindow.tsx # Demo/testing window
54−│ ├── FloatingControls.tsx # Floating controls UI
55−│ ├── components/
56−│ │ ├── DndProvider.tsx # Drag-and-drop coordinator
57−│ │ ├── Explorer/
58−│ │ │ ├── ExplorerView.tsx # File browser view
59−│ │ │ ├── context.tsx # Explorer state/context
60−│ │ │ └── ...
61−│ │ ├── QuickPreview/
62−│ │ │ ├── Controller.tsx # Preview navigation
63−│ │ │ ├── Syncer.tsx # Selection sync
64−│ │ │ └── ...
65−│ │ └── ...
66−│ ├── TopBar/ # TopBar portal system
67−│ │ ├── TopBar.tsx
68−│ │ ├── Context.tsx
69−│ │ └── Portal.tsx
70−│ ├── routes/ # Route components
71−│ │ └── overview/
72−│ ├── hooks/ # React hooks
73−│ ├── context.tsx # Client context and hooks
74−│ ├── styles.css # Global CSS variables
75−│ └── index.tsx # Public exports
76−```
77−
78−### Architecture Layers
79−
80−The interface is organized into clear separation of concerns:
81−
82−**Shell Layer** (`Shell.tsx`):
83−- Root entry point
84−- Provider setup (Spacedrive, Server, TabManager, Platform)
85−- Daemon connection management (Tauri-specific)
86−
87−**Layout Layer** (`ShellLayout.tsx`):
88−- Chrome/frame (sidebar, inspector, TopBar containers)
89−- Provider setup (TopBar, Selection, Explorer)
90−- Tab bar positioning
91−- QuickPreview coordination
92−
93−**View Layer** (routes like `ExplorerView.tsx`):
94−- Actual content rendering
95−- TopBar button registration (via portal)
96−- Feature-specific logic
97−
98−**Coordination Layer**:
99−- `DndProvider.tsx` - Global drag-and-drop
100−- `QuickPreview/Controller.tsx` - Preview navigation
101−- `QuickPreview/Syncer.tsx` - Selection-to-preview sync
102−
103−**Visual Hierarchy:**
104−```
105−Shell (providers) → DndProvider → Router
106− ↓
107− ShellLayout (chrome)
108− ↓
109− <Outlet> (routes)
110− ↓
111− Overview | ExplorerView | Settings | etc.
112−```
113−
114−---
115−
116−## Code Style Rules
117−
118−### React 19 Standards
119−
120−### Critical: You Might Not Need an Effect
121−
122−**Effects are an escape hatch** - only use them to sync with external systems (network, DOM, browser APIs).
123−
124−**DON'T use Effects for:**
125−- Transforming data for rendering (calculate during render instead)
126−- Handling user events (use event handlers)
127−- Updating state based on props (calculate during render or use `key`)
128−- Chains of state updates (do in event handler)
129−- Initializing app (use module-level code)
130−- Notifying parent of changes (pass callback, call in event handler)
131−
132−**DO use Effects for:**
133−- Subscribing to external systems (WebSocket, browser events)
134−- Syncing with non-React widgets
135−- Network requests with proper cleanup
136−
137−### Examples
138−
139−**Wrong - Don't use Effect to transform data:**
140−```tsx
141−function TodoList({ todos, filter }) {
142− const [visibleTodos, setVisibleTodos] = useState([]);
143− useEffect(() => {
144− setVisibleTodos(getFilteredTodos(todos, filter));
145− }, [todos, filter]);
146− // Extra render pass!
45+```rust
46+pub trait Wire {
47+ const METHOD: &'static str;
14748 }
14849 ```
14950
150−**Correct - Calculate during render:**
151−```tsx
152−function TodoList({ todos, filter }) {
153− const visibleTodos = getFilteredTodos(todos, filter);
154− // Or use useMemo if expensive:
155− const visibleTodos = useMemo(
156− () => getFilteredTodos(todos, filter),
157− [todos, filter]
158− );
159−}
160−```
51+### **Registration Macros**
16152
162−**Wrong - Don't use Effect for user events:**
163−```tsx
164−function ProductPage({ product, addToCart }) {
165− useEffect(() => {
166− if (product.isInCart) {
167− showNotification('Added to cart!');
168− }
169− }, [product]);
170−}
171−```
53+Instead of manually implementing `Wire`, use these registration macros that automatically:
17254
173−**Correct - Use event handler:**
174−```tsx
175−function ProductPage({ product, addToCart }) {
176− function buyProduct() {
177− addToCart(product);
178− showNotification('Added to cart!');
179− }
180−}
181−```
55+1. Implement the `Wire` trait with the correct method string
56+2. Register the operation in the global registry using the `inventory` crate
18257
183−**Wrong - Don't use Effect to update parent:**
184−```tsx
185−function Toggle({ onChange }) {
186− const [isOn, setIsOn] = useState(false);
187− useEffect(() => {
188− onChange(isOn); // Too late! Extra render.
189− }, [isOn, onChange]);
190−}
191−```
58+**For Queries:**
19259
193−**Correct - Call in event handler:**
194−```tsx
195−function Toggle({ onChange }) {
196− const [isOn, setIsOn] = useState(false);
197− function updateToggle(nextIsOn) {
198− setIsOn(nextIsOn);
199− onChange(nextIsOn); // Same render pass!
200− }
201−}
60+```rust
61+crate::register_query!(NetworkStatusQuery, "network.status");
62+// Generates method: "query:network.status"
20263 ```
20364
204−**Wrong - Don't chain Effects:**
205−```tsx
206−useEffect(() => {
207− if (card.gold) setGoldCardCount(c => c + 1);
208−}, [card]);
65+**For Library Actions:**
20966
210−useEffect(() => {
211− if (goldCardCount > 3) setRound(r => r + 1);
212−}, [goldCardCount]);
213−// Multiple render passes!
67+```rust
68+crate::register_library_action!(FileCopyAction, "files.copy");
69+// Generates method: "action:files.copy.input"
21470 ```
21571
216−**Correct - Calculate in event handler:**
217−```tsx
218−function handlePlaceCard(nextCard) {
219− setCard(nextCard);
220− if (nextCard.gold) {
221− if (goldCardCount < 3) {
222− setGoldCardCount(goldCardCount + 1);
223− } else {
224− setGoldCardCount(0);
225− setRound(round + 1);
226− }
227− }
228− // Single render pass!
229−}
230−```
72+**For Core Actions:**
23173
232−### Function components only:
233−```tsx
234−// Correct
235−function Component({ name }: { name: string }) {
236− return <div>{name}</div>;
237−}
238−
239−// Wrong
240−const Component: React.FC<{ name: string }> = ({ name }) => {
241− return <div>{name}</div>;
242−};
74+```rust
75+crate::register_core_action!(LibraryCreateAction, "libraries.create");
76+// Generates method: "action:libraries.create.input"
24377 ```
24478
245−**Hooks must follow rules:**
246−```tsx
247−// Correct - proper cleanup
248−useEffect(() => {
249− const subscription = subscribe();
250− return () => subscription.unsubscribe();
251−}, [dependency]);
79+### **Registry System**
25280
253−// Wrong - missing cleanup
254−useEffect(() => {
255− subscribe();
256−}, []);
257−```
81+- **Location**: `core/src/ops/registry.rs`
82+- **Mechanism**: Uses the `inventory` crate for compile-time registration
83+- **Global Maps**: `QUERIES` and `ACTIONS` hashmaps populated at startup
84+- **Handler Functions**: Generic handlers that decode payloads, execute operations, and encode results
25885
259−**Use TypeScript strictly:**
260−```tsx
261−// Correct - explicit types
262−interface ButtonProps {
263− label: string;
264− onClick: () => void;
265−}
86+## Logging Standards
26687
267−function Button({ label, onClick }: ButtonProps) { }
88+- **Setup**: Use `tracing_subscriber::fmt()` with env filter for structured logging
89+- **Macros**: Use `info!`, `warn!`, `error!`, `debug!` from `tracing` crate, not `println!`
90+- **Job Context**: Use `ctx.log()` in jobs for job-specific logging with automatic job_id tagging
91+- **Structured**: Include relevant context fields: `debug!(job_id = %self.id, "message")`
92+- **Levels**: debug for detailed flow, info for user-relevant events, warn for recoverable issues, error for failures
93+- **Format**: `tracing_subscriber::fmt().with_env_filter(env_filter).init()` in main/examples
94+- **Environment**: Respect `RUST_LOG` env var, fallback to module-specific filters like `sd_core=info`
26895
269−// Wrong - implicit any
270−function Button(props) { }
271−```
96+## Documentation
27297
273−---
98+- **Core level docs**: Live in `/docs/core` - comprehensive architecture and implementation guides
99+- **Core design docs**: Live in `/docs/core/design` - planning documents, RFCs, and design decisions
100+- **Application level docs**: Live in `/docs`
101+- **Code docs**: Use `///` for public APIs, `//!` for module overviews, include examples
274102
275−## Color System Rules
103+## Debug Instructions
276104
277−### CRITICAL: Always Use Semantic Tailwind Classes
105+- You can view the logs of a job in the job_logs directory in the root of the data folder
106+- When testing the CLI, after compiling you must first use the `restart` command to ensure the Spacedrive daemon is using the latest build.
278107
279−Never use `var()` syntax directly. Always use Tailwind's semantic color classes.
280−
281−**WRONG:**
282−```tsx
283−className="bg-[var(--color-sidebar)]"
284−className="text-[var(--color-sidebar-ink)]"
285−className="border-[var(--color-accent)]"
286−```
287−
288−**CORRECT:**
289−```tsx
290−className="bg-sidebar"
291−className="text-sidebar-ink"
292−className="border-accent"
293−```
294−
295−**IMPORTANT:** CSS variables must be defined as comma-separated HSL values (not wrapped in `hsl()`):
296−```css
297−/* CORRECT - bare values for Tailwind */
298−--color-sidebar: 235, 15%, 7%;
299−
300−/* WRONG - wrapped in hsl() */
301−--color-sidebar: hsl(235, 15%, 7%);
302−```
303−
304−This is because Tailwind uses `hsla(var(--color-sidebar), <alpha-value>)` which becomes `hsla(235, 15%, 7%, 0.5)` for opacity support.
305−
306−### Color Categories
307−
308−**Accent:** `accent`, `accent-faint`, `accent-deep`
309−- Use for: Primary actions, selections, focus states
310−
311−**Text (Ink):** `ink`, `ink-dull`, `ink-faint`
312−- Use for: Text hierarchy (primary, secondary, tertiary)
313−
314−**Sidebar:** `sidebar`, `sidebar-box`, `sidebar-line`, `sidebar-ink`, `sidebar-selected`, etc.
315−- Use for: Sidebar-specific elements
316−
317−**App:** `app`, `app-box`, `app-line`, `app-hover`, `app-selected`, etc.
318−- Use for: Main content area elements
319−
320−**Menu:** `menu`, `menu-line`, `menu-hover`, `menu-ink`, etc.
321−- Use for: Dropdowns, context menus
322−
323−### Opacity Modifiers
324−
325−```tsx
326−// Use Tailwind opacity
327−className="bg-accent/10"
328−className="bg-sidebar/65"
329−
330−// Don't use manual alpha
331−className="bg-[var(--color-accent)]/10"
332−```
333−
334−---
335−
336−## Component Rules
337−
338−### Primitive vs Feature Components
339−
340−**Primitives** (@sd/ui):
341−- Generic, reusable
342−- Minimal styling (or unstyled)
343−- No business logic
344−- Example: `DropdownMenu`, `Button`, `Input`
345−
346−**Feature Components** (@sd/interface):
347−- Specific to Spacedrive features
348−- Uses primitives
349−- Can have business logic
350−- Example: `Explorer`, `Sidebar`, `LibrariesDropdown`
351−
352−### Component Structure
353−
354−```tsx
355−// Correct structure
356−import { Primitive } from '@sd/ui';
357−import { useSomeQuery } from '../context';
358−
359−interface ComponentProps {
360− // Props interface
361−}
362−
363−function Component({ prop }: ComponentProps) {
364− // Hooks first
365− const data = useSomeQuery();
366−
367− // Logic
368− const derived = useMemo(() => transform(data), [data]);
369−
370− // Render
371− return (
372− <Primitive className="semantic-colors">
373− {/* Content */}
374− </Primitive>
375− );
376−}
377−
378−export { Component };
379−```
380−
381−### Naming Conventions
382−
383−- **Files:** `PascalCase.tsx` for components, `camelCase.ts` for utilities
384−- **Components:** `PascalCase` functions
385−- **Hooks:** `useCamelCase` pattern
386−- **Constants:** `SCREAMING_SNAKE_CASE`
387−- **CSS classes:** Semantic names only (`bg-sidebar`, not `bg-gray-900`)
388−
389−---
390−
391−## Styling Rules
392−
393−### CRITICAL: Never Use Style Tags
394−
395−**NEVER** use `<style>`, `<style jsx>`, or any inline style tags. Always use Tailwind utility classes.
396−
397−**WRONG:**
398−```tsx
399−<style jsx>{`
400− .slider::-webkit-slider-thumb {
401− background: var(--color-accent);
402− }
403−`}</style>
404−```
405−
406−**CORRECT:**
407−```tsx
408−className="[&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:rounded-full"
409−```
410−
411−Use Tailwind's arbitrary variant syntax for pseudo-elements and other edge cases.
412−
413−### Tailwind Class Order
414−
415−Follow this order for readability:
416−1. Layout (`flex`, `grid`, `w-full`, `h-screen`)
417−2. Spacing (`p-4`, `m-2`, `gap-2`)
418−3. Typography (`text-sm`, `font-medium`)
419−4. Colors (`bg-sidebar`, `text-ink`)
420−5. Borders (`border`, `border-sidebar-line`, `rounded-lg`)
421−6. Effects (`shadow-sm`, `backdrop-blur`)
422−7. States (`hover:bg-app-hover`, `focus:ring-accent`)
423−8. Transitions (`transition-colors`)
424−
425−### Rounding (V2 Style)
426−
427−V2 is more rounded than V1. Use:
428−- `rounded-lg` for most containers (8px)
429−- `rounded-md` for smaller elements (6px)
430−- `rounded-full` for pills/badges
431−- `rounded-[10px]` for window frame
432−
433−### Animation
434−
435−Use framer-motion for complex animations:
436−```tsx
437−import { motion, AnimatePresence } from 'framer-motion';
438−
439−<AnimatePresence>
440− {isOpen && (
441− <motion.div
442− initial={{ height: 0, opacity: 0 }}
443− animate={{ height: 'auto', opacity: 1 }}
444− exit={{ height: 0, opacity: 0 }}
445− transition={{ duration: 0.15, ease: [0.25, 1, 0.5, 1] }}
446− >
447− {content}
448− </motion.div>
449− )}
450−</AnimatePresence>
451−```
452−
453−---
454−
455−## Data Fetching Rules
456−
457−### Use Type-Safe Hooks
458−
459−**Core queries** (no library required):
460−```tsx
461−import { useCoreQuery } from '../context';
462−
463−const { data: libraries } = useCoreQuery({
464− type: 'libraries.list',
465− input: { include_stats: false },
466−});
467−```
468−
469−**Library queries** (requires library context):
470−```tsx
471−import { useLibraryQuery } from '../context';
472−
473−const { data: files } = useLibraryQuery({
474− type: 'files.directory_listing',
475− input: { path: '/' },
476−});
477−```
478−
479−**Mutations:**
480−```tsx
481−import { useCoreMutation, useLibraryMutation } from '../context';
482−
483−const createLib = useCoreMutation('libraries.create');
484−const applyTags = useLibraryMutation('tags.apply');
485−const copyFiles = useLibraryMutation('files.copy');
486−const deleteFiles = useLibraryMutation('files.delete');
487−
488−// Use mutations, not client.execute()
489−createLib.mutate({ name: 'New Library', path: null });
490−await copyFiles.mutateAsync({
491− sources: { paths: [path1, path2] },
492− destination: destPath,
493− overwrite: false,
494− verify_checksum: false,
495− preserve_timestamps: true,
496− move_files: false,
497− copy_method: "Auto"
498−});
499−```
500−
501−### Never Fetch Manually
502−
503−**Wrong:**
504−```tsx
505−const [data, setData] = useState();
506−useEffect(() => {
507− fetchData().then(setData);
508−}, []);
509−```
510−
511−**Correct:**
512−```tsx
513−const { data } = useCoreQuery({ type: 'operation', input: {} });
514−```
515−
516−---
517−
518−## Performance Rules
519−
520−### Virtual Scrolling
521−
522−Use for lists > 100 items:
523−```tsx
524−import { useVirtualizer } from '@tanstack/react-virtual';
525−
526−const virtualizer = useVirtualizer({
527− count: items.length,
528− getScrollElement: () => parentRef.current,
529− estimateSize: () => 50,
530−});
531−```
532−
533−### Code Splitting
534−
535−Lazy load routes:
536−```tsx
537−const SettingsPage = lazy(() => import('./Settings'));
538−
539−<Suspense fallback={<Spinner />}>
540− <SettingsPage />
541−</Suspense>
542−```
543−
544−### Memoization
545−
546−Only when actually needed:
547−```tsx
548−// Expensive computation
549−const sorted = useMemo(
550− () => items.sort(expensiveCompare),
551− [items]
552−);
553−
554−// Premature optimization
555−const greeting = useMemo(() => `Hello ${name}`, [name]);
556−```
557−
558−---
559−
560−## Component Composition Rules
561−
562−### Dropdown Example (Current Implementation)
563−
564−The `DropdownMenu` primitive provides minimal base functionality. Explorer customizes it:
565−
566−```tsx
567−// Primitive (in @sd/ui/DropdownMenu.tsx)
568−export const DropdownMenu = {
569− Root: ({ trigger, children, className }) => (
570− // Minimal expanding container with motion
571− ),
572− Item: ({ children, onClick, className }) => (
573− // Basic button with flex layout
574− ),
575− Separator: ({ className }) => (
576− // Simple divider
577− ),
578−};
579−
580−// Usage (in Explorer.tsx)
581−<DropdownMenu.Root
582− trigger={
583− <button className="w-full bg-sidebar-box border-sidebar-line rounded-lg">
584− {currentLibrary?.name}
585− </button>
586− }
587− className="bg-sidebar-box border-sidebar-line rounded-lg"
588−>
589− <DropdownMenu.Item
590− className="px-2 py-1 rounded-md hover:bg-sidebar-selected"
591− onClick={() => switchLibrary(lib.id)}
592− >
593− {lib.name}
594− </DropdownMenu.Item>
595−</DropdownMenu.Root>
596−```
597−
598−**Key principles:**
599−1. Primitive has minimal/no styling
600−2. All visual styling applied via className prop
601−3. Business logic (filtering, selecting) in parent component
602−4. Semantic color classes only
603−
604−---
605−
606−## Type Safety Rules
607−
608−### Use Generated Types
609−
610−All types are auto-generated from Rust:
611−```tsx
612−import type { LibraryInfo, CoreQuery, LibraryAction } from '@sd/ts-client';
613−```
614−
615−**Never:**
616−- Define manual type interfaces that duplicate Rust types
617−- Use `any` (use `unknown` with type guards if needed)
618−- Ignore TypeScript errors
619−
620−### Query Type Safety
621−
622−The hooks automatically infer types:
623−```tsx
624−// TypeScript knows data is LibraryInfo[]
625−const { data } = useCoreQuery({
626− type: 'libraries.list',
627− input: { include_stats: false },
628−});
629−
630−// data is automatically typed based on the operation!
631−```
632−
633−---
634−
635−## File Organization Rules
636−
637−### Component Co-location
638−
639−```
640−Explorer/
641−├── index.tsx # Main component
642−├── Sidebar.tsx # Sub-component
643−├── TopBar.tsx # Sub-component
644−└── hooks/
645− └── useExplorer.ts # Feature-specific hooks
646−```
647−
648−### Exports
649−
650−Only export what's needed:
651−```tsx
652−// index.tsx
653−export { Shell } from './Shell';
654−export { DemoWindow } from './DemoWindow';
655−// Don't export everything
656−```
657−
658−---
659−
660−## macOS-Specific Rules
661−
662−### Native Traffic Lights
663−
664−The window uses **native** macOS traffic lights positioned by Swift code:
665−- Traffic lights are real, functional native controls
666−- Content must have `pt-[52px]` to avoid overlap
667−- No fake CSS traffic lights
668−- Transparent titlebar + invisible toolbar trick (see sd-desktop-macos crate)
669−
670−### Window Styling
671−
672−```tsx
673−// Correct - accounts for native traffic lights
674−<nav className="pt-[52px] ...">
675− {/* Content starts below traffic lights */}
676−</nav>
677−
678−// Correct - window frame with rounded corners
679−<div className="rounded-[10px] border-transparent frame">
680− {/* App content */}
681−</div>
682−```
683−
684−### Blur Effects
685−
686−Use backdrop blur for macOS native feel:
687−```tsx
688−className="backdrop-blur-lg bg-sidebar/65"
689−```
690−
691−---
692−
693−## Current Architectural Decisions
694−
695−### 1. Expanding Dropdowns (Not Overlays)
696−
697−Decision: Dropdowns should expand inline and push content down, not overlay it.
698−
699−Implementation:
700−- Use `framer-motion` for smooth height animation
701−- No Radix Portal (renders inline in DOM)
702−- Pushes surrounding content naturally
703−
704−### 2. Library Switcher Logic
705−
706−Decision: Show/hide current library based on count.
707−
708−Rules:
709−- **1 library:** Hide current from dropdown (no point showing it)
710−- **2+ libraries:** Show all including current (with highlight)
711−- Always show "New Library" and "Library Settings"
712−
713−### 3. Color System
714−
715−Decision: Use Tailwind semantic classes, never `var()` directly.
716−
717−```tsx
718−// Correct
719−className="bg-sidebar-box text-sidebar-ink border-sidebar-line"
720−
721−// Wrong
722−className="bg-[var(--color-sidebar-box)]"
723−```
724−
725−### 4. Rounded Style (V2)
726−
727−Decision: V2 is more rounded than V1.
728−
729−- Containers: `rounded-lg` (8px)
730−- Small elements: `rounded-md` (6px)
731−- Window: `rounded-[10px]`
732−- Pills/badges: `rounded-full`
733−
734−---
735−
736−## Development Workflow
737−
738−### Before Writing Code
739−
740−1. Check if primitive exists in @sd/ui
741−2. Check if types are auto-generated (they probably are)
742−3. Plan component composition (primitive + styling)
743−4. Use semantic color classes
744−
745−### When Adding Features
746−
747−1. Create minimal primitive in @sd/ui if needed
748−2. Use primitive in @sd/interface with styling
749−3. Use type-safe queries/mutations
750−4. Add to this document if architectural decision made
751−
752−### When Styling
753−
754−1. Use semantic colors (`bg-sidebar`, not `bg-gray-900`)
755−2. Follow V2 rounded style
756−3. Use opacity modifiers (`bg-accent/10`)
757−4. Maintain color context (sidebar colors in sidebar, app colors in main area)
758−
759−---
760−
761−## Common Patterns
762−
763−### Shell Entry Point Pattern
764−
765−The app entry point follows a clean provider hierarchy:
766−
767−```tsx
768−// Shell.tsx
769−export function Shell({ client }: { client: SpacedriveClient }) {
770− const platform = usePlatform();
771−
772− return (
773− <SpacedriveProvider client={client}>
774− <ServerProvider>
775− <TabManagerProvider routes={explorerRoutes}>
776− <TabKeyboardHandler />
777− <DndProvider>
778− <RouterProvider router={router} />
779− </DndProvider>
780− </TabManagerProvider>
781− </ServerProvider>
782− </SpacedriveProvider>
783− );
784−}
785−
786−// ShellLayout renders inside router, provides layout chrome
787−// Routes (ExplorerView, Overview, etc.) render inside <Outlet />
788−```
789−
790−### TopBar Portal Pattern
791−
792−Views register their TopBar buttons via portal:
793−
794−```tsx
795−// ExplorerView.tsx
796−import { TopBarPortal } from '../../TopBar';
797−
798−function ExplorerView() {
799− return (
800− <>
801− <TopBarPortal
802− left={<BackButton />}
803− center={<PathBar />}
804− right={<ViewControls />}
805− />
806− <div>{/* View content */}</div>
807− </>
808− );
809−}
810−```
811−
812−### Library Switcher Pattern
813−
814−```tsx
815−const client = useSpacedriveClient();
816−const { data: libraries } = useLibraries();
817−const [currentLibraryId, setCurrentLibraryId] = useState<string | null>(null);
818−
819−// Auto-select first library
820−useEffect(() => {
821− if (libraries && libraries.length > 0 && !currentLibraryId) {
822− client.setCurrentLibrary(libraries[0].id);
823− setCurrentLibraryId(libraries[0].id);
824− }
825−}, [libraries, currentLibraryId, client]);
826−
827−// Switch library
828−const handleSwitch = (id: string) => {
829− client.setCurrentLibrary(id);
830− setCurrentLibraryId(id);
831−};
832−```
833−
834−### Sidebar Item Pattern
835−
836−```tsx
837−function SidebarItem({ icon: Icon, label, active }: Props) {
838− return (
839− <button
840− className={clsx(
841− "flex items-center gap-2 px-2 py-1 rounded-md text-sm font-medium",
842− active
843− ? "bg-sidebar-selected text-sidebar-ink"
844− : "text-sidebar-inkDull hover:text-sidebar-ink"
845− )}
846− >
847− <Icon className="size-4" weight={active ? "fill" : "bold"} />
848− <span className="truncate">{label}</span>
849− </button>
850− );
851−}
852−```
853−
854−### Dropdown Pattern
855−
856−```tsx
857−<DropdownMenu.Root
858− trigger={<button className="...">Trigger</button>}
859− className="bg-sidebar-box border-sidebar-line rounded-lg"
860−>
861− <DropdownMenu.Item
862− className="px-2 py-1 hover:bg-sidebar-selected"
863− onClick={() => action()}
864− >
865− Item content
866− </DropdownMenu.Item>
867− <DropdownMenu.Separator className="border-sidebar-line" />
868−</DropdownMenu.Root>
869−```
870−
871−### Context Menu Pattern
872−
873−Use `useContextMenu` hook for platform-agnostic context menus:
874−
875−```tsx
876−import { useContextMenu } from '../hooks/useContextMenu';
877−import { Copy, Trash } from '@phosphor-icons/react';
878−
879−const { selectedFiles } = useExplorer();
880−const copyFiles = useLibraryMutation('files.copy');
881−const deleteFiles = useLibraryMutation('files.delete');
882−
883−const contextMenu = useContextMenu({
884− items: [
885− {
886− icon: Copy,
887− label: selectedFiles.length > 1 ? `Copy ${selectedFiles.length} items` : "Copy",
888− onClick: async () => {
889− await copyFiles.mutateAsync({
890− sources: { paths: selectedFiles.map(f => f.sd_path) },
891− destination: currentPath,
892− overwrite: false,
893− verify_checksum: false,
894− preserve_timestamps: true,
895− move_files: false,
896− copy_method: "Auto"
897− });
898− },
899− keybind: "⌘C",
900− condition: () => selectedFiles.length > 0, // Only show if files selected
901− },
902− { type: "separator" },
903− {
904− icon: Trash,
905− label: "Delete",
906− onClick: async () => {
907− await deleteFiles.mutateAsync({
908− targets: { paths: selectedFiles.map(f => f.sd_path) },
909− permanent: false,
910− recursive: true
911− });
912− },
913− keybind: "⌘⌫",
914− variant: "danger"
915− }
916− ]
917−});
918−
919−return <div onContextMenu={contextMenu.show}>Content</div>;
920−```
921−
922−**Key features:**
923−- Platform-agnostic (native on Tauri, Radix on web)
924−- Conditional items via `condition` callback
925−- Smart labels that update based on state
926−- Supports icons, keybinds, variants, submenus, separators
927−- Use `useLibraryMutation` for actions, not `client.execute()`
928−
929−---
930−
931−## Type-Safe Query Pattern
932−
933−### Query Keys
934−
935−Use descriptive, hierarchical keys:
936−```tsx
937−// Good
938−queryKey: ['libraries', 'list']
939−queryKey: ['files', 'directory', libraryId, path]
940−
941−// Bad
942−queryKey: ['getLibraries']
943−queryKey: ['data']
944−```
945−
946−### Using Queries
947−
948−```tsx
949−const { data, isLoading, error } = useCoreQuery({
950− type: 'libraries.list',
951− input: { include_stats: true },
952−});
953−
954−// data is automatically typed as LibraryInfo[]!
955−```
956−
957−---
958−
959−## Testing Requirements
960−
961−### Critical Paths Must Be Tested
962−
963−- Explorer file operations
964−- Library switching
965−- Settings mutations
966−- Search functionality
967−
968−### Test Pattern
969−
970−```tsx
971−import { render, screen } from '@testing-library/react';
972−import { Shell } from './Shell';
973−
974−test('switches libraries', async () => {
975− const user = userEvent.setup();
976− render(<Shell client={mockClient} />);
977−
978− await user.click(screen.getByText('Switch Library'));
979− // ...
980−});
981−```
982−
983−---
984−
985−## Migration from V1
986−
987−When porting V1 components:
988−
989−1. **Update colors:** `bg-gray-900` → `bg-app`, `text-gray-400` → `text-ink-dull`
990−2. **Use primitives:** Extract reusable parts to @sd/ui
991−3. **Remove state:** Move to @sd/ts-client if global, use local state if component-specific
992−4. **Update queries:** Use new type-safe hooks
993−5. **Add rounding:** V1 used `rounded-md`, V2 uses `rounded-lg`
994−
995−---
996−
997−## Checklist Before PR
998−
999−- [ ] All colors use semantic classes (no `var()` directly)
1000−- [ ] Component uses primitives from @sd/ui where applicable
1001−- [ ] Type-safe queries/mutations (no manual fetch)
1002−- [ ] Follows V2 rounded style
1003−- [ ] No `any` types
1004−- [ ] Proper cleanup in useEffect
1005−- [ ] Accessible (keyboard nav, ARIA labels)
1006−- [ ] Tested critical paths
1007−
1008−---
1009−
1010−## Quick Reference
1011−
1012−### Import Order
1013−
1014−```tsx
1015−// 1. External libraries
1016−import { useState } from 'react';
1017−import { motion } from 'framer-motion';
1018−
1019−// 2. @sd packages
1020−import { Button, DropdownMenu } from '@sd/ui';
1021−import { useCoreQuery } from '@sd/ts-client';
1022−
1023−// 3. Local imports
1024−import { useLibraries } from './hooks/useLibraries';
1025−import clsx from 'clsx';
1026−```
1027−
1028−### Common Mistakes
1029−
1030−`<style>` or `<style jsx>` tags → Use Tailwind arbitrary variants
1031−`className="bg-[var(--color-sidebar)]"` → `className="bg-sidebar"`
1032−`bg-gray-900` → `bg-app`
1033−`rounded-md` everywhere → `rounded-lg` for V2
1034−Manual fetch → Use type-safe hooks
1035−State in component → Use @sd/ts-client or local state
1036−
1037−---
1038−
1039−## Questions to Ask
1040−
1041−Before writing code:
1042−
1043−1. **Is this a primitive?** → Should it be in @sd/ui?
1044−2. **Is this state global?** → Should it be in @sd/ts-client?
1045−3. **Are the types auto-generated?** → Don't duplicate them!
1046−4. **Can I use a semantic color?** → Yes, always!
1047−5. **Is this accessible?** → Keyboard nav? ARIA labels?
1048−
1049−---
1050−
1051−## Resources
1052−
1053−- **Type Generation:** `cargo run --bin generate_typescript_types`
1054−- **Color System:** `/docs/react/ui/colors.mdx`
1055−- **Workbench Docs:** `/workbench/interface/`
1056−- **V1 Reference:** `/Users/jamespine/Projects/spacedrive_v1`
1057−
1058−---
1059−
1060−## Status: Current Implementation
1061−
1062−**Complete:**
1063−- Type-safe client with auto-generated types
1064−- Native macOS traffic lights
1065−- V1 color system as CSS variables
1066−- Expanding dropdown (DropdownMenu primitive)
1067−- Explorer with sidebar and library switcher
1068−- TanStack Query integration
1069−- Clean architecture refactor (Shell → ShellLayout → Views)
1070−- Extracted DndProvider for drag-and-drop coordination
1071−- QuickPreview components (Controller + Syncer)
1072−- TopBar portal system for view-specific controls
1073−
1074−**In Progress:**
1075−- Port remaining V1 components
1076−- Build complete Explorer (file grid/list views)
1077−- Settings pages
1078−- Multi-window system
1079−
1080−---
1081−
1082−**Remember:** This is a living document. Update it when architectural decisions are made. This is our rulebook for building a world-class file manager interface!
