CLAUDE.md
packages/interface/CLAUDE.mdCLAUDE.md
Quality
65/100
Scores the file, not the repository.Length
3,279 words
73 headings · 47 code blocksRepository
39k
— · pushed 6 days agoLast changed
3 days ago
First indexed 3 days ago.1# Spacedrive Interface Development Rules23**Status:** Living Document - Update as architectural decisions are made4**Purpose:** Ensure consistent, clean, and maintainable code across the interface package5**Audience:** AI assistants and developers working on @sd/interface67---89## Core Principles10111. **Platform Agnostic** - This package works on Tauri, Web, and React Native122. **Clean Separation** - UI components here, state in @sd/ts-client, primitives in @sd/ui133. **Type Safety First** - Use auto-generated types, no `any`, strict TypeScript144. **Performance Matters** - Virtual scrolling, code splitting, memoization when needed155. **Accessible** - Radix primitives, proper ARIA labels, keyboard navigation166. **Consistent Styling** - Semantic color system, no arbitrary values1718---1920## Package Architecture2122### What Lives Where2324**@sd/interface** (this package):25- Route components and layouts26- Feature components (Explorer, Settings, etc.)27- React Query hook wrappers28- UI composition and interactivity29- NO state management (use @sd/ts-client)30- NO primitive components (use @sd/ui)31- NO platform APIs (use platform prop)3233**@sd/ts-client**:34- Client implementation35- Transport layer36- Auto-generated types from Rust37- State stores (if needed)3839**@sd/ui**:40- Primitive components (Button, Input, DropdownMenu, etc.)41- Reusable, unstyled or minimally styled42- No business logic43- No API calls4445### Current Structure4647```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 configuration53│ ├── DemoWindow.tsx # Demo/testing window54│ ├── FloatingControls.tsx # Floating controls UI55│ ├── components/56│ │ ├── DndProvider.tsx # Drag-and-drop coordinator57│ │ ├── Explorer/58│ │ │ ├── ExplorerView.tsx # File browser view59│ │ │ ├── context.tsx # Explorer state/context60│ │ │ └── ...61│ │ ├── QuickPreview/62│ │ │ ├── Controller.tsx # Preview navigation63│ │ │ ├── Syncer.tsx # Selection sync64│ │ │ └── ...65│ │ └── ...66│ ├── TopBar/ # TopBar portal system67│ │ ├── TopBar.tsx68│ │ ├── Context.tsx69│ │ └── Portal.tsx70│ ├── routes/ # Route components71│ │ └── overview/72│ ├── hooks/ # React hooks73│ ├── context.tsx # Client context and hooks74│ ├── styles.css # Global CSS variables75│ └── index.tsx # Public exports76```7778### Architecture Layers7980The interface is organized into clear separation of concerns:8182**Shell Layer** (`Shell.tsx`):83- Root entry point84- Provider setup (Spacedrive, Server, TabManager, Platform)85- Daemon connection management (Tauri-specific)8687**Layout Layer** (`ShellLayout.tsx`):88- Chrome/frame (sidebar, inspector, TopBar containers)89- Provider setup (TopBar, Selection, Explorer)90- Tab bar positioning91- QuickPreview coordination9293**View Layer** (routes like `ExplorerView.tsx`):94- Actual content rendering95- TopBar button registration (via portal)96- Feature-specific logic9798**Coordination Layer**:99- `DndProvider.tsx` - Global drag-and-drop100- `QuickPreview/Controller.tsx` - Preview navigation101- `QuickPreview/Syncer.tsx` - Selection-to-preview sync102103**Visual Hierarchy:**104```105Shell (providers) → DndProvider → Router106 ↓107 ShellLayout (chrome)108 ↓109 <Outlet> (routes)110 ↓111 Overview | ExplorerView | Settings | etc.112```113114---115116## Code Style Rules117118### React 19 Standards119120### Critical: You Might Not Need an Effect121122**Effects are an escape hatch** - only use them to sync with external systems (network, DOM, browser APIs).123124**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)131132**DO use Effects for:**133- Subscribing to external systems (WebSocket, browser events)134- Syncing with non-React widgets135- Network requests with proper cleanup136137### Examples138139**Wrong - Don't use Effect to transform data:**140```tsx141function TodoList({ todos, filter }) {142 const [visibleTodos, setVisibleTodos] = useState([]);143 useEffect(() => {144 setVisibleTodos(getFilteredTodos(todos, filter));145 }, [todos, filter]);146 // Extra render pass!147}148```149150**Correct - Calculate during render:**151```tsx152function 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```161162**Wrong - Don't use Effect for user events:**163```tsx164function ProductPage({ product, addToCart }) {165 useEffect(() => {166 if (product.isInCart) {167 showNotification('Added to cart!');168 }169 }, [product]);170}171```172173**Correct - Use event handler:**174```tsx175function ProductPage({ product, addToCart }) {176 function buyProduct() {177 addToCart(product);178 showNotification('Added to cart!');179 }180}181```182183**Wrong - Don't use Effect to update parent:**184```tsx185function Toggle({ onChange }) {186 const [isOn, setIsOn] = useState(false);187 useEffect(() => {188 onChange(isOn); // Too late! Extra render.189 }, [isOn, onChange]);190}191```192193**Correct - Call in event handler:**194```tsx195function Toggle({ onChange }) {196 const [isOn, setIsOn] = useState(false);197 function updateToggle(nextIsOn) {198 setIsOn(nextIsOn);199 onChange(nextIsOn); // Same render pass!200 }201}202```203204**Wrong - Don't chain Effects:**205```tsx206useEffect(() => {207 if (card.gold) setGoldCardCount(c => c + 1);208}, [card]);209210useEffect(() => {211 if (goldCardCount > 3) setRound(r => r + 1);212}, [goldCardCount]);213// Multiple render passes!214```215216**Correct - Calculate in event handler:**217```tsx218function 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```231232### Function components only:233```tsx234// Correct235function Component({ name }: { name: string }) {236 return <div>{name}</div>;237}238239// Wrong240const Component: React.FC<{ name: string }> = ({ name }) => {241 return <div>{name}</div>;242};243```244245**Hooks must follow rules:**246```tsx247// Correct - proper cleanup248useEffect(() => {249 const subscription = subscribe();250 return () => subscription.unsubscribe();251}, [dependency]);252253// Wrong - missing cleanup254useEffect(() => {255 subscribe();256}, []);257```258259**Use TypeScript strictly:**260```tsx261// Correct - explicit types262interface ButtonProps {263 label: string;264 onClick: () => void;265}266267function Button({ label, onClick }: ButtonProps) { }268269// Wrong - implicit any270function Button(props) { }271```272273---274275## Color System Rules276277### CRITICAL: Always Use Semantic Tailwind Classes278279Never use `var()` syntax directly. Always use Tailwind's semantic color classes.280281**WRONG:**282```tsx283className="bg-[var(--color-sidebar)]"284className="text-[var(--color-sidebar-ink)]"285className="border-[var(--color-accent)]"286```287288**CORRECT:**289```tsx290className="bg-sidebar"291className="text-sidebar-ink"292className="border-accent"293```294295**IMPORTANT:** CSS variables must be defined as comma-separated HSL values (not wrapped in `hsl()`):296```css297/* CORRECT - bare values for Tailwind */298--color-sidebar: 235, 15%, 7%;299300/* WRONG - wrapped in hsl() */301--color-sidebar: hsl(235, 15%, 7%);302```303304This is because Tailwind uses `hsla(var(--color-sidebar), <alpha-value>)` which becomes `hsla(235, 15%, 7%, 0.5)` for opacity support.305306### Color Categories307308**Accent:** `accent`, `accent-faint`, `accent-deep`309- Use for: Primary actions, selections, focus states310311**Text (Ink):** `ink`, `ink-dull`, `ink-faint`312- Use for: Text hierarchy (primary, secondary, tertiary)313314**Sidebar:** `sidebar`, `sidebar-box`, `sidebar-line`, `sidebar-ink`, `sidebar-selected`, etc.315- Use for: Sidebar-specific elements316317**App:** `app`, `app-box`, `app-line`, `app-hover`, `app-selected`, etc.318- Use for: Main content area elements319320**Menu:** `menu`, `menu-line`, `menu-hover`, `menu-ink`, etc.321- Use for: Dropdowns, context menus322323### Opacity Modifiers324325```tsx326// Use Tailwind opacity327className="bg-accent/10"328className="bg-sidebar/65"329330// Don't use manual alpha331className="bg-[var(--color-accent)]/10"332```333334---335336## Component Rules337338### Primitive vs Feature Components339340**Primitives** (@sd/ui):341- Generic, reusable342- Minimal styling (or unstyled)343- No business logic344- Example: `DropdownMenu`, `Button`, `Input`345346**Feature Components** (@sd/interface):347- Specific to Spacedrive features348- Uses primitives349- Can have business logic350- Example: `Explorer`, `Sidebar`, `LibrariesDropdown`351352### Component Structure353354```tsx355// Correct structure356import { Primitive } from '@sd/ui';357import { useSomeQuery } from '../context';358359interface ComponentProps {360 // Props interface361}362363function Component({ prop }: ComponentProps) {364 // Hooks first365 const data = useSomeQuery();366367 // Logic368 const derived = useMemo(() => transform(data), [data]);369370 // Render371 return (372 <Primitive className="semantic-colors">373 {/* Content */}374 </Primitive>375 );376}377378export { Component };379```380381### Naming Conventions382383- **Files:** `PascalCase.tsx` for components, `camelCase.ts` for utilities384- **Components:** `PascalCase` functions385- **Hooks:** `useCamelCase` pattern386- **Constants:** `SCREAMING_SNAKE_CASE`387- **CSS classes:** Semantic names only (`bg-sidebar`, not `bg-gray-900`)388389---390391## Styling Rules392393### CRITICAL: Never Use Style Tags394395**NEVER** use `<style>`, `<style jsx>`, or any inline style tags. Always use Tailwind utility classes.396397**WRONG:**398```tsx399<style jsx>{`400 .slider::-webkit-slider-thumb {401 background: var(--color-accent);402 }403`}</style>404```405406**CORRECT:**407```tsx408className="[&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:rounded-full"409```410411Use Tailwind's arbitrary variant syntax for pseudo-elements and other edge cases.412413### Tailwind Class Order414415Follow 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`)424425### Rounding (V2 Style)426427V2 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/badges431- `rounded-[10px]` for window frame432433### Animation434435Use framer-motion for complex animations:436```tsx437import { motion, AnimatePresence } from 'framer-motion';438439<AnimatePresence>440 {isOpen && (441 <motion.div442 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```452453---454455## Data Fetching Rules456457### Use Type-Safe Hooks458459**Core queries** (no library required):460```tsx461import { useCoreQuery } from '../context';462463const { data: libraries } = useCoreQuery({464 type: 'libraries.list',465 input: { include_stats: false },466});467```468469**Library queries** (requires library context):470```tsx471import { useLibraryQuery } from '../context';472473const { data: files } = useLibraryQuery({474 type: 'files.directory_listing',475 input: { path: '/' },476});477```478479**Mutations:**480```tsx481import { useCoreMutation, useLibraryMutation } from '../context';482483const createLib = useCoreMutation('libraries.create');484const applyTags = useLibraryMutation('tags.apply');485const copyFiles = useLibraryMutation('files.copy');486const deleteFiles = useLibraryMutation('files.delete');487488// 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```500501### Never Fetch Manually502503**Wrong:**504```tsx505const [data, setData] = useState();506useEffect(() => {507 fetchData().then(setData);508}, []);509```510511**Correct:**512```tsx513const { data } = useCoreQuery({ type: 'operation', input: {} });514```515516---517518## Performance Rules519520### Virtual Scrolling521522Use for lists > 100 items:523```tsx524import { useVirtualizer } from '@tanstack/react-virtual';525526const virtualizer = useVirtualizer({527 count: items.length,528 getScrollElement: () => parentRef.current,529 estimateSize: () => 50,530});531```532533### Code Splitting534535Lazy load routes:536```tsx537const SettingsPage = lazy(() => import('./Settings'));538539<Suspense fallback={<Spinner />}>540 <SettingsPage />541</Suspense>542```543544### Memoization545546Only when actually needed:547```tsx548// Expensive computation549const sorted = useMemo(550 () => items.sort(expensiveCompare),551 [items]552);553554// Premature optimization555const greeting = useMemo(() => `Hello ${name}`, [name]);556```557558---559560## Component Composition Rules561562### Dropdown Example (Current Implementation)563564The `DropdownMenu` primitive provides minimal base functionality. Explorer customizes it:565566```tsx567// Primitive (in @sd/ui/DropdownMenu.tsx)568export const DropdownMenu = {569 Root: ({ trigger, children, className }) => (570 // Minimal expanding container with motion571 ),572 Item: ({ children, onClick, className }) => (573 // Basic button with flex layout574 ),575 Separator: ({ className }) => (576 // Simple divider577 ),578};579580// Usage (in Explorer.tsx)581<DropdownMenu.Root582 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.Item590 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```597598**Key principles:**5991. Primitive has minimal/no styling6002. All visual styling applied via className prop6013. Business logic (filtering, selecting) in parent component6024. Semantic color classes only603604---605606## Type Safety Rules607608### Use Generated Types609610All types are auto-generated from Rust:611```tsx612import type { LibraryInfo, CoreQuery, LibraryAction } from '@sd/ts-client';613```614615**Never:**616- Define manual type interfaces that duplicate Rust types617- Use `any` (use `unknown` with type guards if needed)618- Ignore TypeScript errors619620### Query Type Safety621622The hooks automatically infer types:623```tsx624// TypeScript knows data is LibraryInfo[]625const { data } = useCoreQuery({626 type: 'libraries.list',627 input: { include_stats: false },628});629630// data is automatically typed based on the operation!631```632633---634635## File Organization Rules636637### Component Co-location638639```640Explorer/641├── index.tsx # Main component642├── Sidebar.tsx # Sub-component643├── TopBar.tsx # Sub-component644└── hooks/645 └── useExplorer.ts # Feature-specific hooks646```647648### Exports649650Only export what's needed:651```tsx652// index.tsx653export { Shell } from './Shell';654export { DemoWindow } from './DemoWindow';655// Don't export everything656```657658---659660## macOS-Specific Rules661662### Native Traffic Lights663664The window uses **native** macOS traffic lights positioned by Swift code:665- Traffic lights are real, functional native controls666- Content must have `pt-[52px]` to avoid overlap667- No fake CSS traffic lights668- Transparent titlebar + invisible toolbar trick (see sd-desktop-macos crate)669670### Window Styling671672```tsx673// Correct - accounts for native traffic lights674<nav className="pt-[52px] ...">675 {/* Content starts below traffic lights */}676</nav>677678// Correct - window frame with rounded corners679<div className="rounded-[10px] border-transparent frame">680 {/* App content */}681</div>682```683684### Blur Effects685686Use backdrop blur for macOS native feel:687```tsx688className="backdrop-blur-lg bg-sidebar/65"689```690691---692693## Current Architectural Decisions694695### 1. Expanding Dropdowns (Not Overlays)696697Decision: Dropdowns should expand inline and push content down, not overlay it.698699Implementation:700- Use `framer-motion` for smooth height animation701- No Radix Portal (renders inline in DOM)702- Pushes surrounding content naturally703704### 2. Library Switcher Logic705706Decision: Show/hide current library based on count.707708Rules: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"712713### 3. Color System714715Decision: Use Tailwind semantic classes, never `var()` directly.716717```tsx718// Correct719className="bg-sidebar-box text-sidebar-ink border-sidebar-line"720721// Wrong722className="bg-[var(--color-sidebar-box)]"723```724725### 4. Rounded Style (V2)726727Decision: V2 is more rounded than V1.728729- Containers: `rounded-lg` (8px)730- Small elements: `rounded-md` (6px)731- Window: `rounded-[10px]`732- Pills/badges: `rounded-full`733734---735736## Development Workflow737738### Before Writing Code7397401. Check if primitive exists in @sd/ui7412. Check if types are auto-generated (they probably are)7423. Plan component composition (primitive + styling)7434. Use semantic color classes744745### When Adding Features7467471. Create minimal primitive in @sd/ui if needed7482. Use primitive in @sd/interface with styling7493. Use type-safe queries/mutations7504. Add to this document if architectural decision made751752### When Styling7537541. Use semantic colors (`bg-sidebar`, not `bg-gray-900`)7552. Follow V2 rounded style7563. Use opacity modifiers (`bg-accent/10`)7574. Maintain color context (sidebar colors in sidebar, app colors in main area)758759---760761## Common Patterns762763### Shell Entry Point Pattern764765The app entry point follows a clean provider hierarchy:766767```tsx768// Shell.tsx769export function Shell({ client }: { client: SpacedriveClient }) {770 const platform = usePlatform();771772 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}785786// ShellLayout renders inside router, provides layout chrome787// Routes (ExplorerView, Overview, etc.) render inside <Outlet />788```789790### TopBar Portal Pattern791792Views register their TopBar buttons via portal:793794```tsx795// ExplorerView.tsx796import { TopBarPortal } from '../../TopBar';797798function ExplorerView() {799 return (800 <>801 <TopBarPortal802 left={<BackButton />}803 center={<PathBar />}804 right={<ViewControls />}805 />806 <div>{/* View content */}</div>807 </>808 );809}810```811812### Library Switcher Pattern813814```tsx815const client = useSpacedriveClient();816const { data: libraries } = useLibraries();817const [currentLibraryId, setCurrentLibraryId] = useState<string | null>(null);818819// Auto-select first library820useEffect(() => {821 if (libraries && libraries.length > 0 && !currentLibraryId) {822 client.setCurrentLibrary(libraries[0].id);823 setCurrentLibraryId(libraries[0].id);824 }825}, [libraries, currentLibraryId, client]);826827// Switch library828const handleSwitch = (id: string) => {829 client.setCurrentLibrary(id);830 setCurrentLibraryId(id);831};832```833834### Sidebar Item Pattern835836```tsx837function SidebarItem({ icon: Icon, label, active }: Props) {838 return (839 <button840 className={clsx(841 "flex items-center gap-2 px-2 py-1 rounded-md text-sm font-medium",842 active843 ? "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```853854### Dropdown Pattern855856```tsx857<DropdownMenu.Root858 trigger={<button className="...">Trigger</button>}859 className="bg-sidebar-box border-sidebar-line rounded-lg"860>861 <DropdownMenu.Item862 className="px-2 py-1 hover:bg-sidebar-selected"863 onClick={() => action()}864 >865 Item content866 </DropdownMenu.Item>867 <DropdownMenu.Separator className="border-sidebar-line" />868</DropdownMenu.Root>869```870871### Context Menu Pattern872873Use `useContextMenu` hook for platform-agnostic context menus:874875```tsx876import { useContextMenu } from '../hooks/useContextMenu';877import { Copy, Trash } from '@phosphor-icons/react';878879const { selectedFiles } = useExplorer();880const copyFiles = useLibraryMutation('files.copy');881const deleteFiles = useLibraryMutation('files.delete');882883const 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 selected901 },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: true911 });912 },913 keybind: "⌘⌫",914 variant: "danger"915 }916 ]917});918919return <div onContextMenu={contextMenu.show}>Content</div>;920```921922**Key features:**923- Platform-agnostic (native on Tauri, Radix on web)924- Conditional items via `condition` callback925- Smart labels that update based on state926- Supports icons, keybinds, variants, submenus, separators927- Use `useLibraryMutation` for actions, not `client.execute()`928929---930931## Type-Safe Query Pattern932933### Query Keys934935Use descriptive, hierarchical keys:936```tsx937// Good938queryKey: ['libraries', 'list']939queryKey: ['files', 'directory', libraryId, path]940941// Bad942queryKey: ['getLibraries']943queryKey: ['data']944```945946### Using Queries947948```tsx949const { data, isLoading, error } = useCoreQuery({950 type: 'libraries.list',951 input: { include_stats: true },952});953954// data is automatically typed as LibraryInfo[]!955```956957---958959## Testing Requirements960961### Critical Paths Must Be Tested962963- Explorer file operations964- Library switching965- Settings mutations966- Search functionality967968### Test Pattern969970```tsx971import { render, screen } from '@testing-library/react';972import { Shell } from './Shell';973974test('switches libraries', async () => {975 const user = userEvent.setup();976 render(<Shell client={mockClient} />);977978 await user.click(screen.getByText('Switch Library'));979 // ...980});981```982983---984985## Migration from V1986987When porting V1 components:9889891. **Update colors:** `bg-gray-900` → `bg-app`, `text-gray-400` → `text-ink-dull`9902. **Use primitives:** Extract reusable parts to @sd/ui9913. **Remove state:** Move to @sd/ts-client if global, use local state if component-specific9924. **Update queries:** Use new type-safe hooks9935. **Add rounding:** V1 used `rounded-md`, V2 uses `rounded-lg`994995---996997## Checklist Before PR998999- [ ] All colors use semantic classes (no `var()` directly)1000- [ ] Component uses primitives from @sd/ui where applicable1001- [ ] Type-safe queries/mutations (no manual fetch)1002- [ ] Follows V2 rounded style1003- [ ] No `any` types1004- [ ] Proper cleanup in useEffect1005- [ ] Accessible (keyboard nav, ARIA labels)1006- [ ] Tested critical paths10071008---10091010## Quick Reference10111012### Import Order10131014```tsx1015// 1. External libraries1016import { useState } from 'react';1017import { motion } from 'framer-motion';10181019// 2. @sd packages1020import { Button, DropdownMenu } from '@sd/ui';1021import { useCoreQuery } from '@sd/ts-client';10221023// 3. Local imports1024import { useLibraries } from './hooks/useLibraries';1025import clsx from 'clsx';1026```10271028### Common Mistakes10291030`<style>` or `<style jsx>` tags → Use Tailwind arbitrary variants1031`className="bg-[var(--color-sidebar)]"` → `className="bg-sidebar"`1032`bg-gray-900` → `bg-app`1033`rounded-md` everywhere → `rounded-lg` for V21034Manual fetch → Use type-safe hooks1035State in component → Use @sd/ts-client or local state10361037---10381039## Questions to Ask10401041Before writing code:104210431. **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?10481049---10501051## Resources10521053- **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`10571058---10591060## Status: Current Implementation10611062**Complete:**1063- Type-safe client with auto-generated types1064- Native macOS traffic lights1065- V1 color system as CSS variables1066- Expanding dropdown (DropdownMenu primitive)1067- Explorer with sidebar and library switcher1068- TanStack Query integration1069- Clean architecture refactor (Shell → ShellLayout → Views)1070- Extracted DndProvider for drag-and-drop coordination1071- QuickPreview components (Controller + Syncer)1072- TopBar portal system for view-specific controls10731074**In Progress:**1075- Port remaining V1 components1076- Build complete Explorer (file grid/list views)1077- Settings pages1078- Multi-window system10791080---10811082**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!
Also in spacedriveapp/spacedrive
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| spacedriveapp/spacedriveAGENTS.md · 39k | AGENTS.md | setupbuildtestlint-format+9 | 84/100 | 3 days ago | |
| spacedriveapp/spacedrivecore/AGENTS.md · 39k | AGENTS.md | buildtestlint-formatstyle+2 | 89/100 | 3 days ago |
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| Adit-Jain-srm/NightmareNetCLAUDE.md · 45 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 3 days ago | |
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| microsoft/playwrightCLAUDE.md · 94k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 3 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.4k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 3 days ago | |
| filamentphp/filamentCLAUDE.md · 32k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today | |
| lollipopkit/flutter_server_boxCLAUDE.md · 8.3k | CLAUDE.md | buildteststylearch+2 | 98/100 | 3 days ago |
