Files
tanabata/docs/FRONTEND_STRUCTURE.md
T
H1K0 28f5b5d150 docs(project): sync docs with code and bump to 3.0.0
Prepare the 3.0.0 release:
- Bump the version in openapi.yaml and frontend/package.json to 3.0.0.
- Document the existing GET /health endpoint in openapi.yaml (served at the
  root, outside /api/v1) and refine the auth note.
- Add docs/REQUIREMENTS.md (product requirements, in English) and
  docs/ARCHITECTURE.md (system overview); remove the old Russian
  docs/Описание.md.
- Rewrite GO_PROJECT_STRUCTURE.md and FRONTEND_STRUCTURE.md to match the
  current code (dedup CLI, imagehash, real components/stores, Tailwind v4).
- Fix stale counts and references in CLAUDE.md and link the new docs from
  README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:35:16 +03:00

277 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tanabata File Manager — Frontend Structure
> Frontend counterpart of [ARCHITECTURE.md](ARCHITECTURE.md). This document
> details the SvelteKit layout, the CSS approach, and the API client.
## Stack
- **Framework**: SvelteKit in **SPA mode** (`adapter-static`, `ssr = false`),
Svelte 5 (runes)
- **Language**: TypeScript (strict)
- **Build**: Vite 7
- **CSS**: Tailwind CSS **v4** via `@tailwindcss/vite` + CSS custom properties
(`@theme` in `app.css`) — no `tailwind.config.*` / `postcss.config.*` file
- **API types**: auto-generated via `openapi-typescript` (`src/lib/api/schema.ts`)
- **PWA**: service worker + web manifest
- **Font**: Epilogue (variable weight)
- **Dev**: `vite-mock-plugin.ts` serves a mock API so the UI can run without the
Go backend
- **Package manager**: npm
## SPA mode — why SvelteKit without the server
This frontend runs as a **pure client-side SPA**: `adapter-static` with
`fallback: 'index.html'` and `ssr = false` globally (see
`src/routes/+layout.ts`). There is no Node server in production — the build is
static assets, and the only backend is the Go API. SvelteKit is used here
purely as an SPA framework: file-based routing, the client router, and build
tooling.
**SvelteKit features we _do_ use:**
- File-based routing with nested layouts (`admin/` has its own guard) and
dynamic segments (`[id]`).
- The client router: `goto`, the `page` store/state, `afterNavigate`,
`navigating`.
- **Shallow routing** — `pushState`/`replaceState` + `page.state`. The
Immich-style file viewer in `files/` and `pools/[id]/` opens as an overlay
over the still-mounted list via shallow routing, so the browser back button
dismisses it without reloading the grid. This is the single biggest reason we
stay on SvelteKit rather than a plain router.
- `load` functions, used _only_ as client-side route guards (auth redirect,
admin redirect, `/``/files`).
- `$lib` alias, generated `./$types`, Vite/HMR integration.
**SvelteKit features we deliberately do _not_ use** (the "server half"):
- SSR / hydration.
- `+page.server.ts`, `+server.ts` endpoints, form actions — all data goes
through the Go API via the `$lib/api` client.
- `hooks.*`, prerendering, server-only modules — no `hooks.server.ts` /
`hooks.client.ts` files exist.
**Decision: stay on SvelteKit, do not migrate to a bare Svelte + router SPA.**
The project already _is_ an SPA, so there is no runtime gain from switching
(adapter-static tree-shakes the unused server bits; the client-runtime size
difference is negligible). A migration would mean re-implementing nested
layouts, guards, dynamic params, and — most painfully — shallow routing /
history-state overlays by hand, for zero benefit. New contributors should not
expect SSR, endpoints, or hooks to do anything here; that is intentional.
## Monorepo Layout
```
tanabata/
├── backend/ ← Go project (go.mod in here)
├── frontend/ ← SvelteKit project (package.json in here)
├── openapi.yaml ← Shared API contract (root level)
├── docker-compose.yml
├── Dockerfile
├── .env.example
└── README.md
```
`openapi.yaml` lives at repository root — both backend and frontend reference
it. The frontend generates types from it; the backend implements it.
## Frontend Directory Layout
```
frontend/
├── package.json
├── svelte.config.js # adapter-static, fallback: index.html
├── vite.config.ts # plugins: tailwindcss(), sveltekit(), mockApiPlugin()
├── vite-mock-plugin.ts # dev-only mock API (run the UI without the Go backend)
├── tsconfig.json
├── static/ # Copied verbatim into the build
│ ├── manifest.webmanifest # PWA manifest
│ ├── browserconfig.xml
│ ├── robots.txt
│ ├── favicon.ico
│ ├── fonts/
│ │ └── Epilogue-VariableFont_wght.ttf
│ └── images/ # PWA icons, section icons (svg), login decorations
└── src/
├── app.html # Shell HTML (PWA meta, font preload)
├── app.css # `@import 'tailwindcss'` + `@theme` custom properties
├── app.d.ts # Ambient types
├── service-worker.ts # PWA: app-shell + pinned-file offline cache
├── lib/ # Shared code ($lib alias)
│ ├── index.ts
│ │
│ ├── api/ # API client layer
│ │ ├── client.ts # fetch wrapper: bearer auth, 401 refresh+retry, error parsing,
│ │ │ # upload-with-progress (XHR), NDJSON streaming; exports `api`
│ │ ├── auth.ts # login, refresh, logout, sessions
│ │ ├── tags.ts # tag + tag-rule calls
│ │ ├── categories.ts # category calls
│ │ ├── duplicates.ts # duplicate list / dismiss / resolve
│ │ ├── schema.ts # AUTO-GENERATED from openapi.yaml (gitignored; do not edit)
│ │ └── types.ts # Friendly aliases: components['schemas'][...]
│ │
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Header.svelte # Section header with sorting controls
│ │ │ ├── SelectionBar.svelte # Floating bar for multi-select actions
│ │ │ └── KeyboardHelp.svelte # Keyboard-shortcut overlay
│ │ │
│ │ ├── file/
│ │ │ ├── FileCard.svelte # Single thumbnail (160×160, selectable)
│ │ │ ├── Thumb.svelte # Lazy-loaded thumbnail (IntersectionObserver)
│ │ │ ├── FileViewer.svelte # Full-screen viewer with prev/next
│ │ │ ├── FileUpload.svelte # Upload form + drag-and-drop
│ │ │ ├── FilterBar.svelte # DSL filter builder UI
│ │ │ ├── MetadataEditor.svelte # Notes / datetime / metadata (nested) editor
│ │ │ ├── TagPicker.svelte # Searchable tag selector (add/remove)
│ │ │ ├── PoolPicker.svelte # Add-to-pool dialog
│ │ │ ├── BulkTagEditor.svelte # Multi-select tag add/remove
│ │ │ └── DuplicateMergeDialog.svelte # Field-by-field duplicate resolution
│ │ │
│ │ ├── tag/
│ │ │ ├── TagBadge.svelte # Colored pill
│ │ │ └── TagRuleEditor.svelte # Auto-tag rule management
│ │ │
│ │ └── common/
│ │ ├── ConfirmDialog.svelte
│ │ └── InfiniteScroll.svelte # Below-the-fold lazy loading on scroll
│ │
│ ├── stores/ # Svelte stores (global state)
│ │ ├── auth.ts # Current user + JWT tokens (persisted, cross-tab sync)
│ │ ├── selection.ts # Selected item IDs, selection mode
│ │ ├── sorting.ts # Per-section sort key + order (persisted)
│ │ ├── theme.ts # Dark/light theme (persisted)
│ │ ├── appSettings.ts # Misc client-side settings
│ │ ├── listScroll.ts # Restore list scroll position after overlay/back
│ │ └── sectionCache.ts # Cached list snapshots, invalidated on mutation
│ │
│ └── utils/ # Pure helpers
│ ├── dsl.ts # Filter DSL builder: UI state → query string
│ ├── metadata.ts # Nested metadata <-> editor rows
│ ├── pwa.ts # PWA reset / update prompt
│ └── rovingGrid.svelte.ts # Roving-tabindex keyboard grid navigation
└── routes/ # SvelteKit file-based routing (guards only in load)
├── +layout.svelte # Root layout: nav, theme
├── +layout.ts # ssr=false; root auth guard
├── +page.svelte / +page.ts # / → redirect to /files
├── login/+page.svelte
├── files/
│ ├── +page.svelte / +page.ts # Grid: filter, sort, multi-select, upload
│ ├── [id]/+page.svelte # File view (also opened as shallow-routing overlay)
│ ├── duplicates/+page.svelte # Duplicate clusters
│ └── trash/+page.svelte # Trash: restore / permanent delete
├── tags/ { +page.svelte, new/, [id]/ }
├── categories/ { +page.svelte, new/, [id]/ }
├── pools/ { +page.svelte, new/, [id]/ }
├── settings/+page.svelte # Profile: name, password, sessions, import path
└── admin/
├── +layout.svelte / +layout.ts # Restrict to admins
├── users/{ +page.svelte, [id]/ }
└── audit/+page.svelte
```
## Key Architecture Decisions
### CSS: Tailwind v4 + Custom Properties
Tailwind v4 is configured **in CSS**, not in a JS config file. `app.css` imports
Tailwind and declares the theme tokens as CSS custom properties inside `@theme`;
Tailwind then generates utilities (`bg-bg-primary`, `text-text-primary`,
`font-sans`, …) from those tokens automatically.
```css
/* src/app.css */
@import "tailwindcss";
@theme {
--color-bg-primary: #312f45;
--color-bg-secondary: #181721;
--color-bg-elevated: #111118;
--color-accent: #9592b5;
--color-accent-hover: #7d7aa4;
--color-text-primary: #f0f0f0;
--color-tag-default: #444455;
/* … info / danger / warning / success / nav tokens … */
--font-sans: "Epilogue", sans-serif;
}
```
Dark theme is primary; the light theme overrides the same custom properties.
Usage in components: `<div class="bg-bg-primary text-text-primary rounded-xl p-4">`.
Complex cases use scoped `<style>` inside `.svelte` files.
### API Client Pattern
`src/lib/api/client.ts` is a thin fetch wrapper exporting a generic `api`
object. It attaches the bearer token, transparently handles a single `401`
refresh-and-retry (deduplicating concurrent refreshes and syncing rotated
tokens across tabs), parses `{ code, message, details }` errors into `ApiError`,
and invalidates cached list snapshots on mutation. It also provides
`uploadWithProgress` (XHR, for upload progress) and `postStream` (NDJSON, for
the live import progress).
```ts
// $lib/api/client.ts (shape)
export const api = {
get: <T>(path) => request<T>(path),
post: <T>(path, body?) =>
request<T>(path, { method: "POST", body: JSON.stringify(body) }),
patch: <T>(path, body?) =>
request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
put: <T>(path, body?) =>
request<T>(path, { method: "PUT", body: JSON.stringify(body) }),
delete: <T>(path) => request<T>(path, { method: "DELETE" }),
upload: <T>(path, fd) => request<T>(path, { method: "POST", body: fd }),
};
```
Resource modules (`auth.ts`, `tags.ts`, `categories.ts`, `duplicates.ts`) wrap
`api` with typed helpers. Endpoints without a dedicated module (files, pools,
users, acl, audit) are called through `api.*` directly from their route
components.
### Type Generation
```json
// package.json
{
"scripts": {
"generate:types": "openapi-typescript ../openapi.yaml -o src/lib/api/schema.ts",
"dev": "npm run generate:types && vite dev",
"build": "npm run generate:types && vite build"
}
}
```
`schema.ts` is generated (and gitignored) — never edit it by hand. `types.ts`
re-exports friendly aliases:
```ts
import type { components } from "./schema";
export type File = components["schemas"]["File"];
export type Tag = components["schemas"]["Tag"];
// …
```
### SPA Mode (build)
```js
// svelte.config.js
import adapter from "@sveltejs/adapter-static";
export default { kit: { adapter: adapter({ fallback: "index.html" }) } };
```
The Go backend serves `index.html` for all non-API routes (SPA fallback, see
`handler/static.go`). In development the Vite dev server serves the UI and the
mock plugin (or a proxied Go backend) answers `/api`.
### PWA
`service-worker.ts` handles app-shell caching (HTML/CSS/JS/fonts) and optional
user-pinned file caching for offline viewing; `utils/pwa.ts` exposes the reset /
update flow (clear caches and reload from the server, keeping pinned files).