Files
decePubClient/CLAUDE.md
T
thepraandClaude Opus 5.5 e075c4040b Delete the SCSS/Bulma pipeline and document the new one
Nothing loads the old bundle any more, so it goes, along with everything that
built it and the csproj entries that pointed at it:
- SCSS/, including the vendored Bulma 0.9.4 sass;
- compilerconfig.json and bundleconfig.json;
- tailwind.config.js and terminal-script.txt;
- wwwroot/css/style.min.css, main.css, main.min.css and tailwind-override.css;
- wwwroot/vendor's bulma, tailwind and toggle-dark-light-mode CSS.
The unused Font Awesome and Open Iconic assets stay.

The deploy workflow now requires css/app.css and its .gz in the publish output.
The .gz only exists when the generated file was registered as a static web
asset, so the check catches a silent regression of that. It also checks for
js/theme.js.

CLAUDE.md, new on this branch, documents the repo:
- the no-Node Tailwind/daisyUI build;
- the theme runtime;
- the neo layer, including the cascade-layer rule its overrides must follow;
- the rules for the D components.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-03 12:08:52 +02:00

9.9 KiB

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

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, <i class="ion-md-...">) 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 <html> from PageSettings. It runs in <head> 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 <daisy-component> 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<TValue>, 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 <script> tags in wwwroot/index.html.

PWA. Published builds use wwwroot/service-worker.published.js, and service-worker.js is the dev version.

Deployment

.gitea/workflows/deploy.yml runs on a v* tag or a manual dispatch, on a self-hosted runner that is also the web host. The steps:

  1. Publish the site.
  2. Write build.json (commit, ref, time).
  3. Snapshot the live directory to /var/backups/decepub.thepra.dev.
  4. rsync --delete into /var/www/decepub.thepra.dev.
  5. Check that build.json serves the new commit and that a deep client route returns 200 (the SPA fallback). Otherwise, roll back to the snapshot.

The nginx vhost and headers are in deploy/nginx/, and deploy/max/setup.sh is the one-time root setup on the server. If you add a file that must never be cached, add it to the expires -1 locations in the vhost.

Conventions

  • Indent with tabs.
  • Nullable is disabled and ImplicitUsings is enabled.
  • Global Razor usings are in _Imports.razor.
  • Styling uses the D components, Tailwind utilities and the neo-* utilities. Use gap-* rather than space-x/y-* on flex containers; the feeds use flex-col-reverse, which space-* gets wrong.
  • SUtility.IfTrueThen(cond, "class") is the helper for conditional classes.