11# Xhare — CLAUDE.md
22
3- App desktop de compartilhamento de arquivos em tempo real entre PCs na mesma rede local (LAN).
4- Código em ` ~/js/xhare/app/ ` .
3+ App desktop de compartilhamento de arquivos em tempo real entre Macs e PCs na
4+ mesma LAN. Código em ` ~/js/xhare/app/ ` . Tauri 2 (Rust + WebView) + React 19 +
5+ TypeScript + Tailwind v4.
56
67---
78
89## Como trabalhar neste projeto
910
1011### Modo de execução
11- Sempre trabalhe em ** versões pequenas e validáveis** . Cada versão tem escopo fechado, testes passando e app rodando antes de avançar.
12-
13- ### Subagentes
14- Use subagentes em paralelo sempre que possível:
15- - ** Frontend agent** — implementa componentes React/UI
16- - ** Rust agent** — implementa lógica Tauri (discovery, transfer, cache, tray)
17- - ** Test agent** — escreve testes automatizados pra cada módulo
18- - ** Review agent** — revisa código gerado por outros agentes (qualidade, padrões, edge cases)
19- - ** QA agent** — testa a UI, fluxos, estados de erro, edge cases visuais
12+ Sempre trabalhe em ** versões pequenas e validáveis** . Cada mudança tem escopo
13+ fechado, tests passando e app rodando antes de avançar pra próxima.
2014
2115### Regras de qualidade
22- - ** Nunca declare uma versão pronta sem:** testes passando + TypeScript sem erros + app rodando
23- - ** Zero ` any ` no TypeScript** — tipe tudo corretamente
24- - ** Zero code smells** — sem duplicação, sem funções gigantes, sem lógica misturada com UI
25- - ** Componente puro = sem efeito colateral** — hooks e stores ficam fora do JSX
26- - ** Cada arquivo faz uma coisa só**
27- - Rode ` pnpm build ` e ` pnpm typecheck ` antes de marcar qualquer coisa como concluída
16+ - ** Nunca declare uma mudança pronta sem:** tests passando + TypeScript sem
17+ erros + ` cargo check ` limpo + app rodando.
18+ - ** Zero ` any ` no TypeScript** — tipe tudo corretamente.
19+ - ** Zero code smells** — sem duplicação, sem funções gigantes, sem lógica
20+ misturada com UI.
21+ - ** Cada arquivo faz uma coisa só.**
22+ - Rode ` pnpm typecheck ` , ` pnpm lint ` , ` pnpm test ` antes de marcar como concluído.
23+
24+ ### Package manager
25+ ** SEMPRE ` pnpm ` .** Nunca ` bun ` , ` npm ` ou ` yarn ` — o lockfile é ` pnpm-lock.yaml `
26+ e o CI assume isso.
2827
2928---
3029
3130## Compatibilidade obrigatória
3231
33- O app ** deve rodar identicamente em macOS e Windows** . Nunca use APIs exclusivas de um SO sem o equivalente no outro.
32+ O app ** deve rodar identicamente em macOS e Windows** . Nunca use APIs
33+ exclusivas de um SO sem o equivalente no outro.
3434
3535| Concern | macOS | Windows |
3636| ---| ---| ---|
37- | Cache | ` ~/Library/Caches/Xhare/ ` | ` %APPDATA%\Xhare\cache\ ` |
37+ | Cache | ` ~/Library/Caches/Xhare/ ` | ` %LOCALAPPDATA%\Xhare\ ` |
38+ | Logs | ` ~/Library/Application Support/com.felipe.xhare/logs/ ` | ` %APPDATA%\com.felipe.xhare\logs\ ` |
39+ | Settings | ` ~/Library/Application Support/com.felipe.xhare/settings.json ` | ` %APPDATA%\com.felipe.xhare\settings.json ` |
3840| Tray | Menu bar (top) | System tray (bottom right) |
39- | Abrir arquivo | ` open ` | ` explorer ` / ` start ` |
40- | Separador de path | ` / ` | use ` path::join ` no Rust, nunca hardcode |
41- | Fonte | SF Pro (sistema) | Segoe UI (sistema) |
41+ | Abrir path | ` open ` | ` explorer ` / ` start ` |
42+ | Reveal in folder | ` open -R ` | ` explorer /select, ` |
43+ | Clipboard files (read) | ` osascript ` + ` NSPasteboard ` via AppleScriptObjC | ` Get-Clipboard -Format FileDropList ` (PowerShell) |
44+ | Clipboard files (write) | ` NSPasteboard.writeObjects:[NSURL] ` | ` System.Windows.Forms.Clipboard.SetFileDropList ` |
4245| Notificação | macOS Notification Center | Windows Toast |
46+ | Separador de path | ` / ` | use ` path::join ` no Rust, nunca hardcode |
4347
44- No Rust: use ` dirs ` crate pra caminhos de sistema. No frontend: Tauri abstrai tudo — nunca acesse paths diretamente no TypeScript.
48+ No Rust: use ` dirs ` crate pra caminhos de sistema. No frontend: Tauri abstrai
49+ tudo — nunca acesse paths diretamente no TypeScript.
4550
4651---
4752
4853## Stack
4954
50- - ** Shell:** Tauri 2 (Rust + WebView)
51- - ** Frontend:** React 19 + TypeScript + Tailwind CSS v4
52- - ** Package manager:** pnpm
53- - ** State:** Zustand (global) · Context API (local/subtree) · React Query (async/Tauri invokes)
54- - ** UI primitivos:** Radix UI + Lucide React
55- - ** DB local:** SQLite via ` rusqlite ` (metadata, peers, settings)
56- - ** Plataformas MVP:** macOS + Windows
55+ - ** Shell:** Tauri 2 (Rust + WebView). Plugins: ` dialog ` , ` opener ` , ` notification ` .
56+ - ** Frontend:** React 19 + TypeScript + Tailwind CSS v4.
57+ - ** State:** Zustand. Nada de React Query (removido — não havia uso).
58+ - ** Forms:** react-hook-form + ` zod/mini ` (não a ` zod ` cheia).
59+ - ** UI:** Radix UI primitives + Lucide icons + Sonner (toasts).
60+ - ** Persistência local:** arquivos JSON em config dir (settings) e logs em
61+ arquivos diários. Sem SQLite — overkill pra escopo atual.
62+ - ** Plataformas:** macOS + Windows.
5763
5864---
5965
@@ -62,114 +68,127 @@ No Rust: use `dirs` crate pra caminhos de sistema. No frontend: Tauri abstrai tu
6268```
6369app/
6470├── src/
65- │ ├── components/ ← primitivos de UI reutilizáveis (Button, Input, Dialog, etc.)
71+ │ ├── components/ ← primitivos: Button, Dialog, Toast, Tooltip, AlertDialog, …
6672│ ├── features/
67- │ │ ├── devices/ ← DeviceList, useDevices, devicesStore
68- │ │ ├── file-feed/ ← FileFeed, FileRow, FileRowActions, DragOverlay, useFiles, filesStore
69- │ │ └── settings/ ← SettingsForm, settingsStore
70- │ ├── hooks/ ← hooks compartilhados (useHotkey, useDragDrop, useClipboard)
71- │ ├── services/ ← wrappers de Tauri commands (mock em V1, real em V2+)
72- │ ├── stores/ ← stores Zustand que cruzam features
73- │ ├── types/ ← tipos compartilhados
74- │ ├── mock/ ← dados mockados para V1
75- │ └── utils/ ← helpers (cn, format, etc.)
73+ │ │ ├── devices/ ← DeviceList, DeviceItem, useDevices, NewDeviceDialog
74+ │ │ ├── file-feed/ ← FileFeed, FileRow, BulkActionBar, DragOverlay, useTransferSubscription
75+ │ │ ├── logs/ ← LogViewer
76+ │ │ └── settings/ ← SettingsForm, settingsSchema
77+ │ ├── hooks/ ← useClipboardPaste, useDragDrop, useNativeDragDrop,
78+ │ │ useCopySelectedShortcut, useSelectAllShortcut,
79+ │ │ useUnreadBadge, useWindowDrag, useWindowControls,
80+ │ │ usePlatform, useLocalIp
81+ │ ├── services/ ← wrappers de Tauri commands (files, transfer, devices,
82+ │ │ network, settings, logs, pickFolder)
83+ │ ├── stores/ ← Zustand (filesStore, devicesStore, settingsStore,
84+ │ │ connectionStore)
85+ │ ├── types/ ← Device, SharedFile, Settings
86+ │ ├── utils/ ← cn, formatSize, fileType, timeAgo, uniqueName
87+ │ ├── test/ ← setup.ts (mocks de Tauri), fixtures, renderWithTooltip
88+ │ └── App.tsx
7689├── src-tauri/
7790│ └── src/
78- │ ├── main.rs
79- │ ├── discovery.rs ← mDNS + IP manual
80- │ ├── transfer.rs ← TCP chunks + progress + cancel
81- │ ├── cache.rs ← cache em disco + TTL
82- │ ├── tray.rs ← ícone, menu, drop target, badge
83- │ └── hotkeys.rs ← atalhos globais
91+ │ ├── lib.rs ← entrypoint, invoke_handler, window event handling
92+ │ ├── main.rs ← thin wrapper
93+ │ ├── discovery.rs ← mDNS + ARP + heartbeat reconciliation
94+ │ ├── transfer.rs ← TCP protocol, folder zip, clipboard ops, cache cleanup
95+ │ ├── lan_scan.rs ← cross-platform `arp -a` parser
96+ │ ├── settings.rs ← JSON settings (atomic writes)
97+ │ ├── tray.rs ← native tray icon + unread badge
98+ │ └── logger.rs ← file + terminal logger with daily rotation
99+ └── scripts/
100+ └── release.mjs ← `pnpm release:patch|minor|major`
84101```
85102
86103---
87104
88- ## Roadmap de versões
89-
90- ### V1 — UI completa com mocks ← ATUAL
91- ** Objetivo:** app visualmente completo, todos os estados funcionando, sem Rust real.
92-
93- Escopo:
94- - [ ] Feed de arquivos (` FileFeed ` + ` FileRow ` ) com mock de dados
95- - [ ] Estados: vazio, novo/não-lido, lido, enviando (progress), recebendo (progress), erro
96- - [ ] Hover actions por linha: Salvar · Abrir · Copiar · Mostrar · Descartar (recebido) / Reenviar · Cancelar (enviado)
97- - [ ] ` DragOverlay ` — overlay fullscreen ao arrastar arquivo pra janela
98- - [ ] ` SettingsDialog ` completo — pasta destino + TTL do cache
99- - [ ] ` NewDeviceDialog ` com validação de IP (regex)
100- - [ ] Sidebar com estados: conectando, sem wifi, sem dispositivos, lista de dispositivos
101- - [ ] Zustand stores: ` filesStore ` , ` devicesStore ` , ` settingsStore `
102- - [ ] ` services/ ` com mock que simula eventos Tauri (receber arquivo, progresso, etc.)
103- - [ ] Testes: cada componente, cada store, cada hook
104-
105- ** Done when:** ` pnpm build ` passa, zero erros TypeScript, todos os estados visíveis no app.
106-
107- ---
108-
109- ### V2 — Descoberta real (Rust + mDNS)
110- - mDNS via crate ` mdns-sd `
111- - Tauri commands: ` get_devices ` , ` add_device_by_ip ` , ` remove_device `
112- - Tauri events: ` device-discovered ` , ` device-lost ` , ` device-status-changed `
113- - Substituir mock de devices por dados reais
114- - Testes de integração Rust
115-
116- ** Done when:** dois PCs na mesma rede se descobrem automaticamente.
105+ ## Status
106+
107+ Tudo abaixo está ** implementado e funcionando** (V0.1.0 — primeira release):
108+
109+ - ** Discovery:** mDNS (` _xhare._tcp.local. ` ) via ` mdns-sd ` + ARP fallback +
110+ TCP heartbeat. Host claim usa ` xhare-<hostname>.local. ` pra evitar conflito
111+ com o LocalHostName do macOS (que bumpa pra ` -1 ` , ` -2 ` , … quando colide).
112+ IP picker prefere o /24 da própria máquina (Windows multi-NIC).
113+ - ** Transferência:** TCP chunks de 256KB + CRC32 + JSON header. Frontend gera
114+ o UUID ** antes** do invoke pra evitar race com eventos. Folder → auto-zip
115+ com progresso real (pré-scan + emit por bytes lidos).
116+ - ** UI completa:** drag-drop, paste (clipboard files via Rust + inline blobs),
117+ multi-select com bottom action bar, Cmd/Ctrl+A toggle, Cmd/Ctrl+C copia
118+ arquivo real pro clipboard, browser-style name dedupe (` notes (1).txt ` ),
119+ thumbnails reservados pra futuro.
120+ - ** Tray + notifications:** menu bar (macOS) / system tray (Windows). Fechar a
121+ janela esconde + flipa pra ` ActivationPolicy::Accessory ` no macOS (some do
122+ dock). Notificação OS suprimida quando janela focada. Batch debounce 1.5s.
123+ - ** Logs:** file logger com rotação diária, viewer in-app, limpeza automática
124+ > 2 dias.
125+
126+ ** Roadmap futuro** (não-prioridade):
127+ - Thumbnails de imagem/vídeo no feed
128+ - Pause / resume / cancel mid-transfer
129+ - Hotkey global Cmd/Ctrl+Shift+X (pulado a pedido do usuário)
130+ - Drop no ícone do tray (hard cross-platform — pulado)
131+ - Code signing + notarização
117132
118133---
119134
120- ### V3 — Transferência real (Rust TCP)
121- - Protocolo TCP com chunks de 256KB + checksum + resume
122- - Cache em ` ~/Library/Caches/Xhare/ ` (macOS) / ` %APPDATA%\Xhare\cache\ ` (Windows)
123- - TTL: limpeza automática baseada em ` settingsStore.ttl `
124- - Tauri events: ` transfer-progress ` , ` transfer-complete ` , ` transfer-error `
125- - Drag-drop dispara envio real
126- - Paste de clipboard (imagem/arquivo)
127-
128- ** Done when:** arquivo vai de um PC pro outro com progress real.
129-
130- ---
135+ ## Decisões de UX
131136
132- ### V4 — Tray + notificações + hotkeys
133- - Tray nativo (macOS menu bar / Windows system tray)
134- - Drop target no ícone do tray
135- - Badge quando há arquivos não lidos
136- - Notificação nativa ao receber arquivo
137- - Hotkey global ` Cmd/Ctrl+Shift+X ` pra abrir janela
137+ - ** Broadcast automático** — arquivo enviado vai pra todos os peers online.
138+ - ** Fire and forget** — quem está offline não recebe; lista mostra com bolinha
139+ cinza.
140+ - ** Cache, não disco** — arquivo recebido fica em cache até o usuário clicar
141+ "Salvar". Cache é wipe no exit do app.
142+ - ** Não lido** — fundo levemente destacado + borda esquerda azul 2px.
143+ - ** Hover actions** — botões de ação aparecem alinhados à direita no hover da
144+ linha; em modo de seleção (>=1 checkbox marcado), action bar flutuante
145+ aparece no rodapé com Salvar/Apagar/Limpar.
146+ - ** Notificação suprimida quando focado** — se janela está visível+focada,
147+ Rust skipa o OS notification. Em-app toast também só aparece se desfocado.
148+ - ** Batch de 1.5s** — receber 10 arquivos seguidos não gera 10 notifs; agrupa
149+ em "Felipe enviou 10 arquivos".
138150
139151---
140152
141- ## Design
142-
143- Arquivo Figma: ** "Design Xhare desktop app"** (acessível via Figma MCP ` figma-desktop ` ).
153+ ## Comandos
144154
145- Ao implementar UI:
146- 1 . Use a skill ` figma:figma-implement-design ` ANTES de codificar qualquer tela
147- 2 . Leia variáveis de design, estrutura do frame e tire screenshot via MCP
148- 3 . Implemente pixel-perfect, depois peça ao Review agent validar contra o Figma
155+ ``` bash
156+ # Dev
157+ pnpm tauri dev # app completo com hot reload
158+ pnpm dev # só Vite (sem Tauri)
159+ pnpm build # build de produção (frontend)
160+ pnpm tauri build # gera .dmg / .msi locais
161+
162+ # Verificação
163+ pnpm typecheck # tsc --noEmit
164+ pnpm lint # eslint .
165+ pnpm lint:fix # eslint . --fix
166+ pnpm test # vitest run
167+ pnpm test:watch # vitest em watch
168+
169+ # Release (script automatizado)
170+ pnpm release:patch # 0.1.0 → 0.1.1
171+ pnpm release:minor # 0.1.5 → 0.2.0
172+ pnpm release:major # 0.4.2 → 1.0.0
173+ ```
149174
150175---
151176
152- ## Decisões de UX
153-
154- - ** Broadcast automático** — arquivo enviado vai pra todos os peers online
155- - ** Fire and forget** — quem estava offline não recebe; mostra ` ⏸ offline ` na linha
156- - ** Cache, não disco** — arquivo recebido fica em cache até o usuário escolher Salvar
157- - ** Não lido** — linha com fundo levemente destacado + borda esquerda 2px azul
158- - ** Lido** — linha escura, texto cinza
159- - ** Hover actions** — ações aparecem alinhadas à direita no hover da linha
177+ ## Release process
160178
161- ---
179+ ` pnpm release:<kind> ` em ` scripts/release.mjs ` :
162180
163- ## Comandos
181+ 1 . Valida ambiente — recusa se não está em ` main ` , working tree suja, ou local
182+ desincado com ` origin/main ` .
183+ 2 . Bumpa versão em ` package.json ` , ` tauri.conf.json ` , ` Cargo.toml ` , refresca
184+ ` Cargo.lock ` .
185+ 3 . Commit ` release: vX.Y.Z ` + tag anotada + push.
186+ 4 . Tag push dispara ` .github/workflows/release.yml ` que builda ` .dmg `
187+ universal (macOS) + ` .msi ` (Windows) em paralelo e publica GitHub Release.
164188
165- ``` bash
166- pnpm dev # inicia app em modo dev
167- pnpm build # build de produção
168- pnpm tauri dev # alias pro dev com hot reload
169- pnpm typecheck # type check sem build (tsc --noEmit)
170- pnpm test # roda testes unitários (vitest)
171- pnpm test:watch # vitest em modo watch
172- ```
189+ CI separado em ` .github/workflows/ci.yml ` roda em todo push/PR pra ` main ` :
190+ typecheck + lint + tests no frontend, ` cargo check ` + `cargo clippy -D
191+ warnings` no Rust.
173192
174193---
175194
@@ -178,30 +197,54 @@ pnpm test:watch # vitest em modo watch
178197``` typescript
179198// src/types/Device.ts
180199type Device = {
200+ id: string // self:<fullname> | mdns:<fullname> | lan:<ip> | manual:<ip>
181201 name: string
182202 address: string
183203 status: ' ONLINE' | ' OFFLINE'
204+ isSelf: boolean
184205}
185206
186207// src/types/SharedFile.ts
187- type FileStatus = ' sending' | ' receiving' | ' sent' | ' received' | ' error'
208+ type FileStatus = ' zipping' | ' sending' | ' receiving' | ' sent' | ' received' | ' error'
209+ type FileKind = ' file' | ' folder' | ' image' | ' video'
188210
189211type SharedFile = {
190212 id: string
191- name: string
192- size: number // bytes
193- type : ' file ' | ' folder ' | ' image ' | ' video '
213+ name: string // display name (pode ter "(1)" pra dedupe)
214+ size: number // bytes
215+ kind : FileKind
194216 extension? : string
195- thumbnailUrl? : string // só pra imagem/vídeo
196- from: string // device name ou 'você'
217+ thumbnailUrl? : string
218+ from: string // device name ou 'você'
219+ fromAddress? : string // IP capturado no receive (fallback de match no FromCell)
197220 sentAt: Date
198221 status: FileStatus
199- progress? : number // 0-100, só quando sending/receiving
200- speedBps? : number // bytes/s, só quando sending/receiving
201- deliveredTo? : string [] // devices que confirmaram recebimento
202- failedTo? : string [] // devices offline no momento
222+ progress? : number // 0-100 durante zipping/sending/receiving
223+ speedBps? : number // só durante transfer
203224 isRead: boolean
204225 isPinned: boolean
205- cachedPath? : string // caminho no cache local
226+ cachedPath? : string // cache OS, set on received
227+ savedPath? : string // após "Salvar"
228+ sourcePath? : string // source do envio (usado pra resend)
206229}
207230` ` `
231+
232+ ---
233+
234+ ## Gotchas conhecidos
235+
236+ - **mDNS hostname bumping** — registre como ` xhare -<host >.local .` , nunca ` <host >.local .` . macOS Bonjour fica numa briga eterna.
237+ - **Race UUID frontend↔Rust** — gere o id no JS antes do invoke; senão eventos
238+ podem chegar antes do ` addFile ` e ficam órfãos (linha trava em 0%).
239+ - **WebKit drag** — ` getCurrentWindow ().startDragging ()` precisa ser chamada
240+ **sincronamente** do mousedown handler; nada de ` await ` antes (microtask
241+ break mata o gesto). Static import only.
242+ - **Tauri v2 tray** — não expõe drop target. Drop no ícone do tray não é
243+ viável sem código nativo pesado (objc2 + win32 hacks).
244+ - **Notificação OS em dev** — vem com nome/ícone do Terminal (macOS) ou
245+ PowerShell (Windows) porque o processo herda do parent. Em build instalado
246+ (.dmg/.msi) vem como "Xhare" corretamente.
247+ - **Tailwind v4 sintaxe** — ` p -0 !` (sufixo) e não ` !p -0 ` (prefixo).
248+ - **Tailwind v4 + .gitignore** — content auto-detection respeita .gitignore;
249+ se houver entrada conflitando com pasta real (ex: ` logs /` ), classes podem
250+ sumir. Solução: ` @source " ./**/*.{ts,tsx,html}" ` no CSS.
0 commit comments