From aa4b81b9a777f6e30d98d0b251c33003c7e4177b Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 22 Sep 2026 16:08:44 +0200 Subject: [PATCH] Clean up backend and web-player leftovers Remove dead scaffold/config cruft and stale references left over from recent refactors: dotnet's WebApi.http template file, the AppSettings PascalCase/Dockerfile workaround (renamed to appsettings.json to match convention), empty directories left behind by the auth-route and folder-structure moves, a stale ESLint ignore, and the now-fully-applied frontend refactor plan doc. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 + backend/Hosts/Musify.Api/WebApi.http | 6 - ...ment.json => appsettings.Development.json} | 0 .../{AppSettings.json => appsettings.json} | 0 ...ment.json => appsettings.Development.json} | 0 .../{AppSettings.json => appsettings.json} | 0 backend/Musify.Api.Dockerfile | 4 - backend/Musify.Worker.Dockerfile | 3 - deploy/README.md | 3 - deploy/compose.yml | 2 +- docs/development.md | 6 +- docs/media-processing.md | 2 +- web-player/CLAUDE.md | 4 +- web-player/docs/frontend-refactor-plan.md | 982 ------------------ web-player/eslint.config.js | 8 +- web-player/scripts/check-tokens.mjs | 2 +- 16 files changed, 12 insertions(+), 1013 deletions(-) delete mode 100644 backend/Hosts/Musify.Api/WebApi.http rename backend/Hosts/Musify.Api/{AppSettings.Development.json => appsettings.Development.json} (100%) rename backend/Hosts/Musify.Api/{AppSettings.json => appsettings.json} (100%) rename backend/Hosts/Musify.Worker/{AppSettings.Development.json => appsettings.Development.json} (100%) rename backend/Hosts/Musify.Worker/{AppSettings.json => appsettings.json} (100%) delete mode 100644 web-player/docs/frontend-refactor-plan.md diff --git a/.gitignore b/.gitignore index 151e9d4..f17bdc8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ obj/ *.userosscache .vs/ artifacts/ +TestResults/ +*.trx +*.coverage # Rider / VS .idea/ diff --git a/backend/Hosts/Musify.Api/WebApi.http b/backend/Hosts/Musify.Api/WebApi.http deleted file mode 100644 index 213749e..0000000 --- a/backend/Hosts/Musify.Api/WebApi.http +++ /dev/null @@ -1,6 +0,0 @@ -@WebApi_HostAddress = http://localhost:5111 - -GET {{WebApi_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/backend/Hosts/Musify.Api/AppSettings.Development.json b/backend/Hosts/Musify.Api/appsettings.Development.json similarity index 100% rename from backend/Hosts/Musify.Api/AppSettings.Development.json rename to backend/Hosts/Musify.Api/appsettings.Development.json diff --git a/backend/Hosts/Musify.Api/AppSettings.json b/backend/Hosts/Musify.Api/appsettings.json similarity index 100% rename from backend/Hosts/Musify.Api/AppSettings.json rename to backend/Hosts/Musify.Api/appsettings.json diff --git a/backend/Hosts/Musify.Worker/AppSettings.Development.json b/backend/Hosts/Musify.Worker/appsettings.Development.json similarity index 100% rename from backend/Hosts/Musify.Worker/AppSettings.Development.json rename to backend/Hosts/Musify.Worker/appsettings.Development.json diff --git a/backend/Hosts/Musify.Worker/AppSettings.json b/backend/Hosts/Musify.Worker/appsettings.json similarity index 100% rename from backend/Hosts/Musify.Worker/AppSettings.json rename to backend/Hosts/Musify.Worker/appsettings.json diff --git a/backend/Musify.Api.Dockerfile b/backend/Musify.Api.Dockerfile index f27651e..2d35b60 100644 --- a/backend/Musify.Api.Dockerfile +++ b/backend/Musify.Api.Dockerfile @@ -12,10 +12,6 @@ RUN dotnet restore Hosts/Musify.Api/Api.csproj COPY . . RUN dotnet publish Hosts/Musify.Api/Api.csproj -c Release -o /app --no-restore -# Config files are AppSettings*.json (PascalCase); on case-sensitive Linux the host -# looks for appsettings*.json. Add lowercase copies so they get loaded. -RUN cp /app/AppSettings.json /app/appsettings.json \ - && cp /app/AppSettings.Development.json /app/appsettings.Development.json FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app diff --git a/backend/Musify.Worker.Dockerfile b/backend/Musify.Worker.Dockerfile index b254969..dad7085 100644 --- a/backend/Musify.Worker.Dockerfile +++ b/backend/Musify.Worker.Dockerfile @@ -11,9 +11,6 @@ RUN dotnet restore Hosts/Musify.Worker/Worker.csproj COPY . . RUN dotnet publish Hosts/Musify.Worker/Worker.csproj -c Release -o /app --no-restore -# AppSettings*.json (PascalCase) -> lowercase copies for case-sensitive Linux. -RUN cp /app/AppSettings.json /app/appsettings.json \ - && cp /app/AppSettings.Development.json /app/appsettings.Development.json FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app diff --git a/deploy/README.md b/deploy/README.md index cf08e32..3bbd1c0 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -180,9 +180,6 @@ and `web-player/Dockerfile`. the comments in `.env.example`). - **Zitadel** runs `start-from-init`, which is idempotent: the same command works on an empty volume and on an existing one. -- **`AppSettings*.json` are PascalCase** in `Musify.Api` and `Musify.Worker`; - on case-sensitive Linux the host looks for `appsettings*.json`, so their - Dockerfiles create lowercase copies at publish time. - **Re-running `zitadel-init`, `seaweedfs-init` or `migrate`** is safe: they reuse the existing project/apps/bucket by name, and `migrate` is a normal `dotnet ef database update`, a no-op once the schema is current. diff --git a/deploy/compose.yml b/deploy/compose.yml index 3a055aa..c36778f 100644 --- a/deploy/compose.yml +++ b/deploy/compose.yml @@ -19,7 +19,7 @@ # file, so the apps start with real values. See deploy/README.md. # Backend settings that are identical for the API and the Worker. ASP.NET Core -# maps `__` to `:`, so these override the matching AppSettings.json sections. +# maps `__` to `:`, so these override the matching appsettings.json sections. # POSTGRES_HOST / RABBITMQ_HOST / JAEGER_HOST differ between environments # (host.docker.internal in dev, the container name over infra-net in prod, # see .env.example / .env.prod.example), same as PUBLIC_AUTH_URL/S3_PUBLIC_URL diff --git a/docs/development.md b/docs/development.md index 3775245..9f9824b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -134,10 +134,10 @@ omits the `` list when there are no errors, unlike AWS S3), and (load → modify → save) because it ran before EF had detected the pending changes. -## Config (AppSettings) +## Config (appsettings) -Convention: `AppSettings.json` is the template, with empty or neutral -values; `AppSettings.Development.json` has the real dev values; in Docker +Convention: `appsettings.json` is the template, with empty or neutral +values; `appsettings.Development.json` has the real dev values; in Docker everything is overridden with environment variables (`Section__Key`) from `deploy/compose.yml`. To run `Musify.Api` from the IDE, the `Authentication` values go into user-secrets (not the repo). Copy them from `deploy/.env` diff --git a/docs/media-processing.md b/docs/media-processing.md index 8de9e30..8ee9836 100644 --- a/docs/media-processing.md +++ b/docs/media-processing.md @@ -36,7 +36,7 @@ Grouped by area: ## What the Worker needs configured -The Worker registers: storage (S3), transcoder (ffmpeg), pictures, the DB, the MassTransit consumers, and the upload-intent jobs. Its `AppSettings` needs the right sections: `Bucket` (= `webapi-storage`, same as the API), `PlayList`, `Track`, `MassTransit`, `AudioTranscoder`, `UploadIntent`, `Workers` (temp directory), `InfrastructureStorage`, `Database`. +The Worker registers: storage (S3), transcoder (ffmpeg), pictures, the DB, the MassTransit consumers, and the upload-intent jobs. Its `appsettings` needs the right sections: `Bucket` (= `webapi-storage`, same as the API), `PlayList`, `Track`, `MassTransit`, `AudioTranscoder`, `UploadIntent`, `Workers` (temp directory), `InfrastructureStorage`, `Database`. ## Background jobs diff --git a/web-player/CLAUDE.md b/web-player/CLAUDE.md index 114b991..448fbcf 100644 --- a/web-player/CLAUDE.md +++ b/web-player/CLAUDE.md @@ -46,14 +46,14 @@ npm run lint:tokens # scripts/check-tokens.mjs, the design system guardrail ## Styles -The design system lives in `src/routes/layout.css` (`@theme` tokens plus `@utility` recipes) and in `src/lib/theme/` (`theme.css` for the static neutrals, `tokens.ts` for the dynamic tokens that depend on the user's hue). `scripts/check-tokens.mjs` (run as `npm run lint:tokens`, part of `lint`) fails if `src/**/*.svelte` brings back what the refactor in `docs/frontend-refactor-plan.md` (parts P0 through P14) removed, so read that doc before adding an arbitrary value or a stock radius or size. +The design system lives in `src/routes/layout.css` (`@theme` tokens plus `@utility` recipes) and in `src/lib/theme/` (`theme.css` for the static neutrals, `tokens.ts` for the dynamic tokens that depend on the user's hue). `scripts/check-tokens.mjs` (run as `npm run lint:tokens`, part of `lint`) fails on a stock radius, an `h-N w-N` pair, fractional quarter spacing, a literal hex/`rgba()`, or an arbitrary `-[…]` value outside its whitelist, so check that script before adding one. - **Radii:** always `rounded-tag/thumb/control/art/art-lg/panel/panel-lg` (or `rounded-full` for pills and circles). Never `rounded-sm/md/lg/xl/2xl/3xl` or a bare `rounded`. - **Square sizes:** use `size-*` instead of repeating `h-N w-N` (icons: `size-icon-xs/sm/md/lg/xl`; covers: `size-cover-xs` through `size-cover-hero`). - **Typography:** use the recipes `text-display-1/2/3/4`, `text-eyebrow`, `text-body`, `text-count`, with tracking through `tracking-display`/`tracking-eyebrow` rather than a loose `tracking-[…]`. - **Spacing:** stick to the Tailwind scale in `.5` steps and avoid quarters like `.25`/`.75`, unless the semantic token calls for it (for example `mb-4.5` in `SectionHeading`). - **Colors:** always use `--mf-*`/`text-*`/`bg-*`/`border-*` from `theme.css`/`tokens.ts`. Never a literal hex or `rgba()` inside a `.svelte` file; those literals only belong in `theme.css`/`tokens.ts` as the source of truth. -- **Inline `style="…"` and arbitrary `-[…]` values:** only the ones whitelisted in section B.4 of `docs/frontend-refactor-plan.md` (the stagger variable `--i`, tile hue, menu position, dynamic widths and heights through `var(--mf-*)`, `text-[clamp(…)]`, `grid-cols-[auto_1fr]`, `max-h-[85dvh]`, and `vh` paddings for vertical centering). Everything else should become a token. +- **Inline `style="…"` and arbitrary `-[…]` values:** only the ones whitelisted in `scripts/check-tokens.mjs` (the stagger variable `--i`, tile hue, menu position, progress width, `text-[clamp(…)]`, `leading-[…]` on login's display type, `grid-cols-[auto_1fr]`, `max-h-[85dvh]`, `my-`/`py-[Ndvh]` for vertical centering, `transition-[filter]` on genre tiles, and `bg-[image:var(--mf-*)]`). Everything else should become a token. - Reach for these base components before creating a new one: `Artwork`, `Avatar`, `ListRow`, `MediaIdentity`, `TrackList`, `MediaCard`, `PageHeader`, `ContextMenu`, `ConfirmDialog`, `PlayButton`, `Slider`, `Logo`, `Chip`, `SegmentedControl`, `CoverForm`. - Known and accepted gap (documented in `scripts/check-tokens.mjs`): icon `strokeWidth={n}` isn't unified yet. There are 13 different values spread across almost every component, and migrating them to `stroke-thin/regular/bold` recipes safely would require visually confirming that `lucide-svelte` actually respects `stroke-width` through CSS, so that was left out of the last pass. diff --git a/web-player/docs/frontend-refactor-plan.md b/web-player/docs/frontend-refactor-plan.md deleted file mode 100644 index f79a551..0000000 --- a/web-player/docs/frontend-refactor-plan.md +++ /dev/null @@ -1,982 +0,0 @@ -# Plan: Frontend refactor and unification (web-player) - -> Work plan for agents/AI with limited context (~128k). Replaces -> `design-system-refactor-plan.md` (whose content is now mostly applied). -> Based on a full read of **every** client `.svelte`, `.css` and `.ts` file -> in `web-player/src` (2026-09-18, branch `design-prot`). - -## How to use this document - -1. **Always read section A (Common context)**, it's short and every part assumes it. -2. Pick **one part** (P0 through P14). Each part lists: objective, prerequisites, files - to read, tasks, the exact API of what gets created, a mapping table, and closing criteria. -3. One part equals one commit/PR. Don't mix parts. -4. If a part needs something from another part (say, a token or a helper) and it - **doesn't exist yet**, create it yourself following the spec in section B. These are - described with exact paths and APIs so any part can create them identically. -5. When you finish a part, check its box in section D (Log). - -Recommended order (the arrows are **soft** dependencies, see point 4): - -``` -P0 → P1 → P2 - ↘ P3 (Artwork/Avatar) ─┐ - ↘ P4 (Buttons) ───────┤ - ↘ P5 (TS helpers) ─────┼→ P6 (Menus) → P7 (Rows) → P8 (Tables) → P9 (Cards) - └→ P10 (Headers) → P11 (Forms) → P12 (Chrome) → P13 (Pages) → P14 (Final sweep) -``` - ---- - -## A. Common context (always read) - -### A.1 Project stack and rules (from `web-player/CLAUDE.md`) - -- SvelteKit 2 + **Svelte 5 runes** (`$props()` with `interface Props`, `$state`, - `$derived`, `$effect`; no `export let` or `$:`; `{#each}` always keyed). -- **Tailwind 4**: there's no `tailwind.config.js`; the config lives in `src/routes/layout.css` - (`@theme`, `@utility`). Raw color tokens in `src/lib/theme/theme.css` (`--mf-*`), - hue-dependent dynamic tokens in `src/lib/theme/tokens.ts` (`buildThemeTokens`). -- No comments in code unless essential. -- Closing out each part: `npm run check` and `npm run lint` must be **clean** (from `web-player/`). -- Icons: `@lucide/svelte/icons/`. - -### A.2 Known gotchas (don't reintroduce them) - -1. **Hue-animated tokens** (`--mf-bg`, `--mf-elevated`, `--mf-panel-bg`, `--mf-bar-bg`, - `--mf-hairline`, `--mf-hero-bg`, `--mf-modal-glow`, `--mf-spotlight-bg`, - `--mf-logo-grad`, `--mf-cover-grad`, `--mf-accent*`, `--mf-ambient`) are applied as - an **inline style** on `` from `+layout.svelte`. An override in - `:root[data-theme='light']` **doesn't win**: the light variant belongs in `buildThemeTokens()`. -2. **Tailwind class conflicts**: never `class="bg-a {cond ? 'bg-b' : ''}"`. Always - mutually exclusive: `class={cond ? 'bg-b' : 'bg-a'}`. Overrides of a shared component's - base class go through a prop/variant, not `!important`. -3. Text without a size class inherits the browser's 16px. All visible text needs a - typography recipe (see B.1.6). -4. `--mf-ink` is "text on CTA" and inverts in light mode; `--mf-tile-shade`, - `--mf-cover-grad`, `--mf-liked-grad` are "artwork" (don't invert). -5. Visual reference: **Home** (`src/routes/+page.svelte`). A refactor change shouldn't - change the look except where the part explicitly says so (the "Δ" column). - -### A.3 Verifying each part - -```sh -cd web-player -npm run check -npm run lint -``` - -Visual (Browser pane, `.claude/launch.json` → `musify-web`; dev login credentials are in the -project's memory): open each touched page **and** Home; test the active state (a track -playing), hover, mobile (375px) and light mode (account menu → "White mode"). - -### A.4 Measured baseline (to know if you're making progress) - -| Metric | Today | -| ------------------------------------------------ | ------------------------------------ | -| `.svelte` components (lib + route `components/`) | 61 | -| Client `.svelte`/`.css`/`.ts` lines | ~8,700 | -| Arbitrary classes `x-[…]` | 44 | -| Fractional spacing (`h-3.75`, `gap-2.75`…) | ~190 occurrences, 60 distinct values | -| Distinct radii used | 13 (6 tokens + 7 Tailwind defaults) | -| Distinct `strokeWidth` values | 13 values | -| `style="…"` attributes | 24 | -| Hand-rolled pluralizations `=== 1 ?` | 15 | -| `eslint` errors | 2 (`liked/+page.svelte`) | - -Overall goal: ~61 → ~45 components, −1,500/−2,000 lines, 0 arbitrary values outside an -allow list, 1 scale per dimension. - ---- - -## B. Shared specs (single source of truth) - -Any part that needs something from here and it doesn't exist yet creates it **exactly like this**. - -### B.1 Tokens (in `src/routes/layout.css`, inside `@theme`) - -Tailwind 4 generates utilities from these namespaces: `--radius-*` → `rounded-*`, -`--spacing-*` → `size-* w-* h-* p-* m-* gap-*`, `--container-*` → `max-w-*`, -`--tracking-*` → `tracking-*`, `--leading-*` → `leading-*`. - -#### B.1.1 Radii (`--radius-*`) - -| Token | Value | What it's for | -| ------------------ | ----------- | ----------------------------------------- | -| `rounded-tag` | `0.25rem` | badges, mini cells (ExplicitBadge, index) | -| `rounded-thumb` | `0.5rem` | covers ≤ 36px, logo | -| `rounded-control` | `0.625rem` | inputs, rows, menu items, buttons | -| `rounded-art` | `0.8125rem` | covers 42 to 88px, grid cards | -| `rounded-art-lg` | `1.125rem` | covers ≥ 136px, dropzones | -| `rounded-panel` | `1.125rem` | containers, menus, modals, tiles | -| `rounded-panel-lg` | `1.375rem` | hero, spotlight, "best match" | -| `rounded-full` | (Tailwind) | pills, avatars, round buttons | - -Remove `--radius-art-round` (0 uses). Mapping of what exists today: - -| Today | px | Uses | Becomes | Δ | -| ----------------------- | --- | ---- | -------------------------------------------------------- | ---- | -| `rounded-sm`, `rounded` | 4 | 2 | `rounded-tag` | 0 | -| `rounded-md` | 6 | 1 | `rounded-thumb` | +2px | -| `rounded-lg` | 8 | 4 | `rounded-thumb` | 0 | -| `rounded-xl` | 12 | 6 | `rounded-control` | −2px | -| `rounded-2xl` | 16 | 8 | `rounded-art-lg` (artwork) / `rounded-panel` (container) | +2px | -| `rounded-3xl` | 24 | 1 | `rounded-panel-lg` | −2px | - -#### B.1.2 Cover/artwork sizes (`--spacing-cover-*`) - -| Token | Value | Current uses it absorbs (Δ) | -| --------------- | ---------------- | ------------------------------------------------------------------------------- | -| `cover-xs` | `2.25rem` (36) | menu thumbs `h-9`; sidebar `h-8.5` (+2) | -| `cover-sm` | `2.625rem` (42) | table rows `h-10.5`; queue and "Popular" `h-10` (+2); mobile player `h-11` (−2) | -| `cover-md` | `3rem` (48) | desktop player `md:h-12` | -| `cover-lg` | `3.5rem` (56) | "Continue" `h-14`; search rows `h-13` (+4); Home playlist card `h-15.5` (−6) | -| `cover-xl` | `5.5rem` (88) | "Best match" `h-22` | -| `cover-2xl` | `8.5rem` (136) | Home spotlight `h-34` | -| `cover-hero` | `9rem` (144) | collection headers, dropzones, profile avatar | -| `cover-hero-sm` | `10.25rem` (164) | collection headers from `sm:` up | - -Usage: `size-cover-sm` (Tailwind 4's `size-*` = `width`+`height`). **Never** pair `h-X w-X`: -always use `size-*`. - -#### B.1.3 Icons (`--spacing-icon-*`) and stroke - -| Token | Value | Absorbs | -| --------- | ---------- | -------------------------------------------- | -| `icon-xs` | `0.875rem` | `h-3.5` (14) | -| `icon-sm` | `1rem` | `h-3.75` (15), `h-4` (16), `h-4.25` (17) | -| `icon-md` | `1.125rem` | `h-4.5` (18) | -| `icon-lg` | `1.25rem` | `h-5` (20), `h-6` (24, mobile login) | -| `icon-xl` | `2.25rem` | `h-9`/`h-10` (empty states, dropzone, Liked) | - -Stroke: in `layout.css`, outside `@theme`: - -```css -@layer base { - svg.lucide { - stroke-width: 1.8; - } -} -@utility stroke-thin { - stroke-width: 1.3; -} -@utility stroke-regular { - stroke-width: 1.8; -} -@utility stroke-bold { - stroke-width: 2.5; -} -``` - -The CSS property `stroke-width` beats the presentation attribute Lucide sets, so the -`strokeWidth` prop stops doing anything: **remove it from every use**. Mapping: -`1 to 1.4` → `stroke-thin`, `1.5 to 2` → (nothing, default), `2.5 to 3` → `stroke-bold`, -`strokeWidth={0}` + `fill` → the `stroke-0` class. Verify in the browser (DevTools → -Computed `stroke-width`) that the default applies before migrating in bulk. - -#### B.1.4 Widths (`--container-*`) and layout - -| Token | Value | Absorbs | -| ---------------- | ------- | -------------------------------------------------------------------------------- | -| `max-w-prose-sm` | `26rem` | `max-w-105` (error), `max-w-108` (hero p), `max-w-110`, `max-w-115` (no results) | -| `max-w-hero` | `34rem` | `max-w-135` (hero, seekbar) | -| `max-w-search` | `30rem` | `max-w-120` (TopBar search box) | -| `max-w-cta` | `20rem` | `max-w-80` (login buttons) | - -Already exist and stay in `theme.css`: `--mf-sidebar-w`, `--mf-queue-w`, `--mf-nav-h`, -`--mf-player-h`, `--mf-safe-b`. Add: - -```css ---mf-player-side-w: 16rem; /* w-64 from PlayerBar/PlayerExtras */ ---mf-menu-w: 15rem; /* w-60 for context menus (= MENU_WIDTH 240) */ ---mf-submenu-w: 16rem; /* w-64 (= SUBMENU_WIDTH 256) */ ---mf-range-h: 3.5px; /* progress bar and volume */ ---mf-range-thumb: 11px; ---mf-volume-w: 84px; -``` - -The TS constants `MENU_WIDTH`/`SUBMENU_WIDTH` (currently duplicated in `TrackContextMenu` -and `AlbumContextMenu`) move to `src/lib/config.ts`. No comment needed, the name speaks for itself. - -#### B.1.5 Layers (`z-index`) - -In `theme.css`'s `:root`: `--z-raised: 10; --z-sticky: 20; --z-backdrop: 30; --z-menu: 40; ---z-submenu: 50; --z-modal: 60; --z-grain: 80;`. Usage: `z-(--z-menu)`. Maps 1:1 to the -current values (`z-10`, `z-20`, `z-30`, `z-40`, `z-50`, `z-[60]`, `body::after` 80). Note: -`MobileHeader` and `PlayerDock` use `z-40` but they're "sticky", not menus, so they get -`--z-sticky` (verify the account menu still sits above the header: menu 40 > sticky 20). - -#### B.1.6 Typography (`@utility` recipes in `layout.css`) - -Type scale stays Tailwind's (`text-xs` through `text-4xl`), **no arbitrary sizes**. -Add to `@theme`: `--tracking-display: -0.02em; --tracking-eyebrow: 0.1em; ---leading-body: 1.65;`. Recipes (color is **not** part of the recipe, it's added separately): - -```css -@utility text-display-1 { - @apply font-display text-3xl font-medium tracking-tight sm:text-4xl; -} -@utility text-display-2 { - @apply font-display text-2xl font-medium tracking-tight; -} -@utility text-display-3 { - @apply font-display text-xl font-medium tracking-display; -} -@utility text-display-4 { - @apply font-display text-lg font-medium tracking-tight; -} -@utility text-eyebrow { - @apply text-xs font-medium tracking-widest uppercase; -} -@utility text-body { - @apply text-sm leading-body; -} -@utility text-count { - @apply text-xs tabular-nums; -} -``` - -> If `@apply` with an `sm:` variant doesn't compile inside `@utility`, use -> `@media (width >= 40rem) { font-size: var(--text-4xl); line-height: var(--text-4xl--line-height); }` -> the way `page-x` does. - -Mapping of what exists today: - -| Current pattern | Recipe | -| -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| `font-display text-3xl … sm:text-4xl` (PageHeader, CollectionHeader, Home hero, search h1 `text-2xl sm:text-3xl font-semibold tracking-[-0.03em]`) | `text-display-1` (Δ search: +1 step) | -| `font-display text-2xl` (Home stats, spotlight title, "Uploaded!") | `text-display-2` | -| `font-display text-xl … tracking-[-0.02em]` (Modal, EmptyState, +error, "No results", delete dialogs) | `text-display-3` (all `font-medium`) | -| `font-display text-lg` (SectionHeading, "Best match", Queue "Up next" `text-base`) | `text-display-4` | -| `text-xs font-medium tracking-widest uppercase` (plus variants `tracking-[0.14em]`, `[0.16em]`, `tracking-wider`, `text-sm font-semibold`) | `text-eyebrow` | -| `text-sm leading-[1.65] text-fg-2` | `text-body text-fg-2` | -| `text-xs text-muted tabular-nums` | `text-count text-muted` | -| `tracking-[-0.01em]` (MixTile) | remove | - -#### B.1.7 Surface and motion recipes (`@utility` in `layout.css`) - -```css -@utility theme-transition { - transition: background 0.5s ease-out; -} /* 8× transition-[background] duration-500 */ -@utility glass-bar { - /* PlayerBar ×2, TabsBar */ - background-image: var(--mf-bar-bg); - border-top: 1px solid var(--mf-hairline); - backdrop-filter: blur(24px); - transition: background 0.5s ease-out; -} -@utility grid-cards { - /* MediaGrid default: 150/165 */ - display: grid; - gap: --spacing(3); - grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr)); - @media (width >= 40rem) { - gap: --spacing(4); - grid-template-columns: repeat(auto-fill, minmax(10.3125rem, 1fr)); - } -} -@utility grid-cards-lg { - /* /playlists and /u/[id]: 180/210 */ - display: grid; - gap: --spacing(3); - grid-template-columns: repeat(auto-fill, minmax(11.25rem, 1fr)); - @media (width >= 40rem) { - gap: --spacing(4); - grid-template-columns: repeat(auto-fill, minmax(13.125rem, 1fr)); - } -} -@utility grid-tiles { - /* Home mixes: 164 */ - display: grid; - gap: --spacing(5); - grid-template-columns: repeat(auto-fill, minmax(10.25rem, 1fr)); -} -@utility grid-wide { - /* Home playlists (258) and genres (268) → 16.5rem, Δ ±6px */ - display: grid; - gap: --spacing(3.5); - grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr)); -} -``` - -`.glass-panel` already exists; `Modal` should use it instead of its duplicated inline `style=`. - -Stagger (replaces the 5 `style="animation-delay:…"` with 40 **or** 45ms): - -```css -.animate-enter { - animation-delay: calc(min(var(--i, 0), 10) * 45ms); -} -``` - -Usage: `style="--i:{i}"`. The only `style=` allowed alongside the ones in B.4. - -#### B.1.8 Colors: cleanup - -- Delete unused utilities: `--color-accent-dim`, `--color-accent-glow`, - `--color-accent-hair`, `--color-accent-line`, `--color-accent-muted` (0 uses as a - class; `--mf-accent-dim` is used in `::selection`, so that variable stays). -- `--color-on-accent` is an alias of `--mf-on-art` (same as `--color-on-art`), delete it and - use `on-art`. `--color-hover` = `--mf-surface-2` (same as `surface-2`), keep - only `hover` as the hover semantic and leave `surface-2` for resting-state backgrounds. -- `--mf-sidebar-bg` isn't used by any component (Sidebar uses `bg-bg`), delete it from - `theme.css` and from `buildThemeTokens`. -- Button: `--mf-btn-bg` = `--mf-surface-2`; `--mf-btn-secondary-bg` only differs in light - mode. Expose `--color-btn`, `--color-btn-hover`, `--color-btn-glass`, - `--color-btn-glass-hover` in `@theme` and remove the `bg-[var(--mf-btn-…)]` from `Button.svelte`. -- Scrollbar: `rgba(255,255,255,.18/.32)` hardcoded in `layout.css` (broken in light mode) → - tokens `--mf-scrollbar`, `--mf-scrollbar-hover` with a variant in `:root[data-theme='light']`. -- `MixTile`: `to-black/42`, `bg-ink/76`, `border-on-art/12` → `to-scrim/40`, `PlayButton` - recipe (B.3). - -### B.2 TypeScript helpers (single source of truth) - -| File (new) | API | Replaces | -| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `src/lib/actions/pressable.ts` | `pressable(node, fn)` → adds `role="button"`, `tabindex=0`, click + Enter/Space | 7 copies of `onkeydown` Enter/Space (Home ×5, Queue, TrackTitleCell) | -| `src/lib/actions/clickOutside.ts` | `clickOutside(node, fn)` | `onDocumentClick` from `AccountMenu` | -| `src/lib/toggle.svelte.ts` | `createToggle(initial=false)` → `{ get open, show(), close(), toggle() }` | `playlists.svelte.ts` and `player/queuePanel.svelte.ts` (identical) | -| `src/lib/format.ts` (extend) | `plural(n, one, many)` → `"1 song"`; `fmtCount(n,'song','songs')`; **always lowercase** | 15 hand-rolled pluralizations, with inconsistent casing ("Song"/"song") | -| `src/lib/navigation.svelte.ts` (extend) | `searchHref(q)`; `appNavLinks(user, counts?)` → `{href,label,icon,count?,primary:boolean}[]` | `buildHref` in TopBar and explore; Sidebar and TabsBar link lists | -| `src/lib/player/player.svelte.ts` (extend) | `isQueueCurrent(items)`, `playAllOrToggle(items)`, `playShuffled(items)`; rename `addToQueue` → `playNextItem` | `playAll`/`isCurrentQueue` in album, playlist, liked, mixes, Home spotlight (5 copies) | -| `src/lib/player/player.svelte.ts` | single `toQueueItem(track)` (normalizes `duration`/`listensCount` to `number`) | hand-rolled mappings in `recentlyPlayed.ts` (×2), `liked.svelte.ts` (×2), `liked/+page`, `tracks.ts#targetForQueueItem` and the scattered `Number(…)` calls | -| `src/lib/menus.svelte.ts` | `createTrackMenu()` → `{ state, open(e, track), close(), playNext() }` | context menu state + handler in Home, Explore, Mix | - -### B.3 New components (API) - -| Component (`$lib/components/ui/`) | Props | Replaces / absorbs | -| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Artwork.svelte` | `src?`, `trackIds?: (string\|number)[]`, `size: 'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'2xl'\|'hero'`, `shape?: 'auto'\|'round'`, `imageSize?: 'small'\|'medium'\|'large'` (auto from `size`), `fallback?: 'music'\|'gradient'\|'none'`, `class?`, `children?` (overlay) | `Cover`, `PlaylistArt`, `MixArt`; radius picked automatically by size (xs→thumb, sm to xl→art/control, 2xl/hero→art-lg). Priority: `src` → mosaic if ≥4 `trackIds` → first cover → fallback. A single `onerror`. | -| `Avatar.svelte` | `name`, `src?`, `size: 'sm'\|'md'\|'lg'\|'hero'`, `class?` | initials/image in TopBar, MobileHeader, Explore (×2), `ArtistAvatar` | -| `ListRow.svelte` | `href?` \| `onclick?`, `oncontextmenu?`, `active?`, `size?: 'sm'\|'md'\|'lg'`, `title`, `subtitle?`, `subtitleHref?`, `explicit?`, `meta?`, `leading?: Snippet`, `art: Snippet<[string]>` \| artwork props, `trailing?: Snippet` | `SearchResultRow` + 7 hand-rolled rows (see P7) | -| `MediaIdentity.svelte` | `title`, `subtitle?`, `subtitleHref?`, `explicit?`, `active?`, `art?: Snippet` \| `artwork` props, `titleClass?` | `TrackTitleCell` (renamed and generalized) and the `player/TrackInfo` block | -| `TrackList.svelte` | `tracks: TrackLike[]`, `columns?: ('album'\|'added'\|'uploaded'\|'plays')[]`, `index?`, `onPlay(i)`, `rowAction?: {action, icon, label}` \| `rowActionSnippet?`, `oncontextmenu?(e, track)` | `TrackTable`+`TrackRow`+`TrackIndexCell`+`TrackMeta` and the 6 tables (P8) | -| `ContextMenu.svelte` | `x`, `y`, `openLeft`, `onClose`, `items: {icon,label,onclick}[]`, `playlistAction?: {action: string, fields: Record, label}`, `playlists` | `TrackContextMenu` + `AlbumContextMenu` (90% identical) | -| `PlayButton.svelte` | `playing?`, `size: 'sm'\|'md'\|'lg'`, `variant: 'strong'\|'glass'`, `label`, `onclick?`, `as?: 'button'\|'span'` | play button in TransportControls, "Best match", MixTile overlay | -| `ConfirmDialog.svelte` | `open`, `onClose`, `title`, `description`, `confirmLabel`, `action` (form POST) | 2 delete modals with different styles | -| `Slider.svelte` | `value`, `max`, `label`, `oninput`, `variant: 'seek'\|'volume'` | `SeekBar` (click-only) + `VolumeControl` (own `` - (serialized with a `tokensToCss(tokens)` helper in `tokens.ts`). -3. Delete from `theme.css` every token that `buildThemeTokens` generates (dark and light). - `theme.css` is left with only fixed neutrals, surfaces, text, layout and z-index. -4. `+layout.svelte`: the two background `div`s with `style=` → `@utility app-backdrop` / - `app-vignette` in `layout.css`. The `main` with an inline `padding-bottom: calc(…)` → - a class using `--mf-dock-h`, activated via `data-has-track` on `main` itself. - -Closing: with light mode saved, a hard reload shows no flash; track changes still animate -the hue; check+lint. - ---- - -### P3: `Artwork` and `Avatar` (unified artwork) - -**Objective:** a single cover component and a single avatar component. - -Findings: - -- `Cover`, `PlaylistArt` and `MixArt` implement the same cascade (image → 2×2 mosaic → - first cover → placeholder). `MixArt` is basically `PlaylistArt` with - `trackIds = items.map(i => i.trackId)`. -- `AlbumCard` passes the **album id** as `trackId` to `Cover` just to be able to use `src` - (a hack). -- "Image or initial" avatars are reimplemented in `TopBar`, `MobileHeader`, Explore (best - match + user row) and `ArtistAvatar` (with a ring). -- Each usage picks its own radius (`rounded-lg`, `rounded-xl`, `rounded-2xl`, - `rounded-control`, `rounded-art-lg`…) and its own size (`h-8.5`, `h-9`, `h-10`, `h-10.5`, - `h-11 md:h-12`, `h-13`, `h-14`, `h-15.5`, `h-22`, `h-34`, `h-36 sm:h-41`). - -Tasks: - -1. Create `Artwork.svelte` and `Avatar.svelte` per B.3 using tokens B.1.1/B.1.2. - `size` drives both dimension **and** radius; `class` is only for layout (`shrink-0`, margins). -2. Migrate **every** usage (search for `Cover`, `PlaylistArt`, `MixArt`, `ArtistAvatar`, - `user.picture`, `profilePictureUrl`), applying the B.1.2 table. -3. `ArtistAvatar`: on `u/[id]/+page.svelte`, use `` (the name is already - shown by the header; the label under the avatar was redundant, **visible Δ**, confirm this). -4. Delete `Cover.svelte`, `PlaylistArt.svelte`, `MixArt.svelte`, `ArtistAvatar.svelte`. - -Closing: `grep -rn "Cover\b\|PlaylistArt\|MixArt\|ArtistAvatar" src` only matches inside -`Artwork`; visual check on Home, Explore (search), Sidebar, Queue, player, headers. - ---- - -### P4: Buttons, `Button`, `IconButton`, `PlayButton` - -**Objective:** no `