diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index eb7f667..fc5e270 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -44,7 +44,7 @@ jobs: - name: Assert the publish actually produced a site run: | W="$GITHUB_WORKSPACE/publish/wwwroot" - for f in index.html appsettings.json build.json _framework/blazor.webassembly.js css/style.min.css; do + for f in index.html appsettings.json build.json _framework/blazor.webassembly.js css/app.css css/app.css.gz js/theme.js; do [ -e "$W/$f" ] || { echo "::error::publish output is missing $f"; exit 1; } done ls "$W/_framework" | grep -q '\.wasm$' || { echo "::error::no .wasm in _framework"; exit 1; } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a1720cc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,163 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +decePubClient is a Blazor WebAssembly PWA (.NET 10). It is meant as a Pleroma-FE-like client for **PrivaPub**, the +owner's ActivityPub server, whose repo is `thepra/SocialPub`. It is deployed as a static site at +https://decepub.thepra.dev, and its API base address is `https://privapub.thepra.dev` +(`AppConfiguration:ApiBaseAddress` in `wwwroot/appsettings.json`). + +**It is still a prototype and is not connected to the server:** +- The feed comes from `Helpers/Faker.cs`. `Storage.GetMessages()` (`Services/IStorage.cs`) and `Pages/ExpandMessage.razor` + create mock posts. +- Every method in `Services/MessagesService.cs` is a `//TODO` stub. +- `HttpService` (`Services/IHttpService.cs`) is written but not registered in DI. +- The nginx vhost sends `X-Robots-Tag: noindex` while the site shows mock posts. + +`Docs/Insomnia_APIs.json` is an old API collection for a localhost backend. Don't treat it as PrivaPub's API. + +## Sibling repo dependency + +The project references `../SocialPub/PrivaPub.ClientModels/PrivaPub.ClientModels.csproj`, so SocialPub must be +checked out next to this repo. CI clones it there. That project supplies: +- the shared DTOs; +- `Policies` (IsAdmin, IsUser, IsModerator); +- the `FieldsNameResource` and `ErrorsResource` localization resources. + +Before wiring anything to the API, read `../SocialPub/CLAUDE.md`. PrivaPub's route names (`/peasants/...`, `/mouth`, +`/anus`, ...) are deliberate. Never "correct" them. The intended API surface is the Mastodon client API plus PrivaPub's +private `/clientapi`. + +## Commands + +```sh +dotnet build decePubClient.csproj # what CI runs (-c Release) on every push to master and on PRs +dotnet run # dev server: https://localhost:7046 / http://localhost:5046 +dotnet publish decePubClient.csproj -c Release -o publish # static site lands in publish/wwwroot +``` + +`global.json` pins SDK 10.0.100 with `rollForward: latestFeature`. There are no tests and no linter. + +## CSS: Tailwind 4 + daisyUI 5, no Node + +`Styles/app.css` is compiled into `wwwroot/css/app.css` by the Tailwind CSS standalone binary with the daisyUI +plugin. `app.css` is gitignored and regenerated on every build. + +- `tools/tailwind.sh` downloads Tailwind 4.3.3 and daisyUI 5.7.47 into the gitignored `.tools/tailwind/`, checking each + against a pinned sha256. To upgrade, change the versions and hashes there. +- The `TailwindCompile` target in the csproj runs it, so `dotnet build` / `publish` are enough, in CI as well. Debug + builds use `--optimize`, Release builds `--minify`. Pass `-p:SkipTailwind=true` to skip it. +- `IncludeTailwindOutput` adds the generated file to `Content` before static web assets are resolved. Without that, a + clean-clone publish would ship `app.css` without its `.gz`/`.br` copies and outside `service-worker-assets.js`. + The deploy workflow checks for `css/app.css.gz`. +- `bash tools/tailwind.sh watch` rebuilds the CSS live next to `dotnet watch`. `bash tools/tailwind.sh docs` fetches + daisyUI's `llms.txt` (its component reference) into `.tools/tailwind/`. +- Tailwind scans every file `.gitignore` doesn't exclude, and only generates the classes it finds written out + **whole**. Never build a class name by concatenation or interpolation (`"btn-" + size`, `$"btn-{size}"`). Write + every variant as a literal, e.g. in a switch arm. + +Ionicons 4.5 (`wwwroot/vendor/ionicons.min.css`, ``) is loaded as its own stylesheet. + +## Theme and neumorphism + +- `Styles/theme.css` holds two daisyUI themes, `neo-light` (default) and `neo-dark` (`prefersdark`). Every colour + is an `hsl()` of `--neo-hue-light` / `--neo-hue-dark` / `--neo-chroma` (0 for the grey themes). +- `wwwroot/js/theme.js` sets those variables and `data-theme` on `` from `PageSettings`. It runs in `` + from localStorage, so there is no flash before Blazor boots. After that, CascadingState calls it through + `AppStatusService.ApplyTheme` whenever the settings change. With `PreferSystemTheming` it leaves `data-theme` off, + and the OS decides. +- `Styles/neo.css` holds the neumorphic layer: + - `neo-raised` / `neo-inset` / `neo-flat`, with sizes `neo-2xs` / `neo-xs` / `neo-sm` / `neo-md` (these replace the + old `neomorph is-nxsmall` classes); + - `neo-gradient(-pressed)` and `neo-hue-track`; + - `@utility ` overrides that give btn, input, select, card, … the neo look. daisyUI puts its rules + in sublayers of `utilities`, so these overrides beat **every** daisyUI rule. Each override must exclude the + daisyUI variants it shouldn't touch, e.g. `:not(:where(.btn-ghost, .btn-link, ...))`, and call sites drop a neo + shadow with `shadow-none!`. +- Never write `@utility collapse`. It brings back Tailwind's own `collapse` utility (`visibility: collapse`), and + every collapse disappears. + +## daisyUI Blazor components (`Components/Daisy/`) + +Every daisyUI component has a `D`-prefixed wrapper (`DButton`, `DModal`, `DDropdown`, `DInput`, ...), grouped by +daisyUI's categories into folders whose namespaces are imported in `_Imports.razor`. `/styleguide` (not linked) renders +all of them in every theme. When you add or change one: +- Map each enum to its classes in a switch with whole literal class names. +- Value-holding fields derive from `DInputBase`, Blazor's `InputBase`, so `@bind-Value` and EditForm + validation work. Everything else derives from `DComponentBase` (`Class`, pass-through attributes, `Cx(...)`). +- Open/active state lives in C# (`@bind-IsOpen`, `@bind-Active`) and is rendered with daisyUI's `*-open` modifier + classes. No JS interop. `DCalendar` is the one exception: it wraps the vendored Cally web component. +- Don't name a parameter after an HTML attribute (`Title`, `Style`, `Id`, `Label`, ...). Blazor matches attributes + to parameters without regard to case, so the parameter would swallow the attribute. +- Components don't use `Localizer` or `CascadingState`; their user-visible strings are parameters. + +## Architecture + +**Startup (`Program.cs`).** +- DI registration happens here. +- The named `HttpClient` "default" uses `ApiBaseAddress` as its base address. It falls back to the host's base + address when that setting is empty. +- `SetDefaultCulture()` (`Extensions/ExtensionMethods.cs`) runs before the app starts. It picks the culture from the + saved `PublicCacheData` or from the browser language, and maps any `en*` to `en-GB`. + +**`LayerComponents/CascadingState.razor`** wraps the router in `App.razor`. It is the app-wide state object, and pages +take it as `[CascadingParameter] CascadingState`. It handles: +- `PublicCacheData` (page settings: theme colours, dark/light/system theming, language), persisted in localStorage; +- the theme, applied through `AppStatusService.ApplyTheme` (`wwwroot/js/theme.js`); +- an online check every 10 seconds; +- `ProcessError` / `ProcessWarning`, which components should call to report errors and warnings; +- `LogFromJs`, which JS calls back into through a `DotNetObjectReference`. + +**Components and pages** inherit `LocalizableComponentBase`, which supplies `Localizer`, an `IsLoading` flag and +`AfterRenderAsyncJobs`. `PagesBase` is an empty subclass of it, and only `Authentication.razor` uses it. + +**Localization (`Services/CoalescingStringLocalizer.cs`).** +- Lookup order: this repo's `Resources/AllStrings.resx`, then `FieldsNameResource`, then `ErrorsResource`. +- A missing key renders as the key itself, so a typo fails silently. +- The class lives in namespace `collAnon.Client.Services`, a leftover from another project. + +**Client storage** comes in two layers: +- **localStorage** (Blazored.LocalStorage, camelCase JSON) holds `AuthData` and `PublicCacheData`, keyed by + `nameof(Type)`. +- **IndexedDB** (DnetIndexedDb, database `data`) is reached through `IStorage`. Its schema is declared in + `GenericExtensions.AddIndexedDb()`, and you must bump `WithVersion` when you change it. +- Known gap: `IStorage` reads and writes a `ClientLogs` store that the schema never declares, so the error log writes + fail silently (they are wrapped in try/catch). + +**Auth.** +- `TokenAuthStateProvider` builds the auth state from the `AuthData` token in localStorage. +- `SetToken` is mostly commented out, so there is no real login flow yet. +- `AddApiAuthorization` and `Pages/Authentication.razor` are leftovers from the Blazor template. + +**JS interop.** +- `wwwroot/main.js` defines `window.*` helpers. `Services/AppStatusService.cs` calls them, mostly through the + synchronous `IJSInProcessRuntime`. +- Each helper reports its errors back through `window.logFromJs`. +- The other scripts (IndexedDB, HeadElement, auth, rxjs) are loaded by `