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

12 KiB
Raw Blame History

Tanabata File Manager — Frontend Structure

Frontend counterpart of 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 routingpushState/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.

/* 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).

// $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

// 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:

import type { components } from "./schema";
export type File = components["schemas"]["File"];
export type Tag = components["schemas"]["Tag"];
// …

SPA Mode (build)

// 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).