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.
This commit is contained in:
@@ -7,6 +7,9 @@ obj/
|
|||||||
*.userosscache
|
*.userosscache
|
||||||
.vs/
|
.vs/
|
||||||
artifacts/
|
artifacts/
|
||||||
|
TestResults/
|
||||||
|
*.trx
|
||||||
|
*.coverage
|
||||||
|
|
||||||
# Rider / VS
|
# Rider / VS
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
@WebApi_HostAddress = http://localhost:5111
|
|
||||||
|
|
||||||
GET {{WebApi_HostAddress}}/weatherforecast/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
||||||
@@ -12,10 +12,6 @@ RUN dotnet restore Hosts/Musify.Api/Api.csproj
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN dotnet publish Hosts/Musify.Api/Api.csproj -c Release -o /app --no-restore
|
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
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ RUN dotnet restore Hosts/Musify.Worker/Worker.csproj
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN dotnet publish Hosts/Musify.Worker/Worker.csproj -c Release -o /app --no-restore
|
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
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -180,9 +180,6 @@ and `web-player/Dockerfile`.
|
|||||||
the comments in `.env.example`).
|
the comments in `.env.example`).
|
||||||
- **Zitadel** runs `start-from-init`, which is idempotent: the same command
|
- **Zitadel** runs `start-from-init`, which is idempotent: the same command
|
||||||
works on an empty volume and on an existing one.
|
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
|
- **Re-running `zitadel-init`, `seaweedfs-init` or `migrate`** is safe: they
|
||||||
reuse the existing project/apps/bucket by name, and `migrate` is a normal
|
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.
|
`dotnet ef database update`, a no-op once the schema is current.
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
# file, so the apps start with real values. See deploy/README.md.
|
# 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
|
# 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
|
# POSTGRES_HOST / RABBITMQ_HOST / JAEGER_HOST differ between environments
|
||||||
# (host.docker.internal in dev, the container name over infra-net in prod,
|
# (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
|
# see .env.example / .env.prod.example), same as PUBLIC_AUTH_URL/S3_PUBLIC_URL
|
||||||
|
|||||||
+3
-3
@@ -134,10 +134,10 @@ omits the `<Error>` list when there are no errors, unlike AWS S3), and
|
|||||||
(load → modify → save) because it ran before EF had detected the pending
|
(load → modify → save) because it ran before EF had detected the pending
|
||||||
changes.
|
changes.
|
||||||
|
|
||||||
## Config (AppSettings)
|
## Config (appsettings)
|
||||||
|
|
||||||
Convention: `AppSettings.json` is the template, with empty or neutral
|
Convention: `appsettings.json` is the template, with empty or neutral
|
||||||
values; `AppSettings.Development.json` has the real dev values; in Docker
|
values; `appsettings.Development.json` has the real dev values; in Docker
|
||||||
everything is overridden with environment variables (`Section__Key`) from
|
everything is overridden with environment variables (`Section__Key`) from
|
||||||
`deploy/compose.yml`. To run `Musify.Api` from the IDE, the `Authentication`
|
`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`
|
values go into user-secrets (not the repo). Copy them from `deploy/.env`
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Grouped by area:
|
|||||||
|
|
||||||
## What the Worker needs configured
|
## 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
|
## Background jobs
|
||||||
|
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ npm run lint:tokens # scripts/check-tokens.mjs, the design system guardrail
|
|||||||
|
|
||||||
## Styles
|
## 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`.
|
- **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`).
|
- **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-[…]`.
|
- **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`).
|
- **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.
|
- **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`.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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/<name>`.
|
|
||||||
|
|
||||||
### 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 `<html>` 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<string,string>, 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 `<style>`) |
|
|
||||||
| `Logo.svelte` | `size: 'sm'\|'lg'` | logo in Sidebar (text only), MobileHeader, login |
|
|
||||||
| `Chip.svelte` | `selected?`, `count?`, `href?`\|`onclick?` | filter chips and suggested genre chips in Explore |
|
|
||||||
|
|
||||||
### B.4 Allow list for `style=` and arbitrary values (after the refactor)
|
|
||||||
|
|
||||||
Allowed: `style="--i:{i}"` (stagger), per-hue tile gradients in Explore
|
|
||||||
(`--tile-hue`), progress width (`width:{pct}%`), menu position (`left/top`).
|
|
||||||
Allowed arbitrary values: `text-[clamp(…)]` on login, `grid-cols-[auto_1fr]`,
|
|
||||||
`max-h-[85dvh]`. Everything else becomes a token.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## C. Parts
|
|
||||||
|
|
||||||
### P0: Baseline and loose bugs (independent, ~30 min)
|
|
||||||
|
|
||||||
**Objective:** get `lint` green and fix detected bugs that don't require a refactor.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. `src/routes/(app)/liked/+page.svelte`: remove the `Play` and `Pause` imports (eslint errors).
|
|
||||||
2. `src/lib/components/ui/MediaCard.svelte`: the title has no size class (inherits 16px) →
|
|
||||||
`text-sm` (same as `MixTile`).
|
|
||||||
3. `src/routes/+page.svelte`, spotlight rows: `hover:bg-hover … {cond ? 'bg-accent-tint' : ''}`
|
|
||||||
→ make it mutually exclusive (`{cond ? 'bg-accent-tint' : 'hover:bg-hover'}`), same as the other rows.
|
|
||||||
4. `src/routes/(app)/albums/[id]/+page.svelte` delete modal: `text-base text-fg-3` →
|
|
||||||
`text-sm leading-[1.65] text-fg-2` and a title matching the playlist one (will be unified in P10).
|
|
||||||
5. `Sidebar.svelte` (`+` button) and `Queue.svelte` (close): remove the useless `text-base`.
|
|
||||||
6. Delete `src/lib/components/ui/Badge.svelte` (0 importers).
|
|
||||||
7. `Button.svelte`: `primary` and `secondary` are the **same** class. Don't change the look;
|
|
||||||
just leave `const variants = { primary: subtle, secondary: subtle, … }` with a single
|
|
||||||
`subtle`, and note in D that P4 will decide whether `primary` should be differentiated
|
|
||||||
(**product decision, ask the user**).
|
|
||||||
|
|
||||||
Closing: `npm run lint` 0 errors.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P1: Base tokens (prerequisite for P3 through P14)
|
|
||||||
|
|
||||||
**Objective:** create all of B.1 **additively** (without migrating components yet, except
|
|
||||||
where noted). Nothing should change visually after this part.
|
|
||||||
|
|
||||||
Read: `src/routes/layout.css`, `src/lib/theme/theme.css`, `src/lib/theme/tokens.ts`.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Add to `@theme`: radii B.1.1 (and delete `--radius-art-round`), `--spacing-cover-*`
|
|
||||||
B.1.2, `--spacing-icon-*` B.1.3, `--container-*` B.1.4, `--tracking-*`/`--leading-*`
|
|
||||||
B.1.6, `--color-btn*` B.1.8.
|
|
||||||
2. Add to `theme.css`'s `:root`: layout variables from B.1.4 and z-index from B.1.5.
|
|
||||||
3. Add the `@utility` recipes from B.1.3 (stroke), B.1.6 (typography), B.1.7 (surfaces, grids);
|
|
||||||
stagger in `.animate-enter`.
|
|
||||||
4. Move the scrollbar to tokens with a light variant (B.1.8).
|
|
||||||
5. Delete unused color utilities and `--mf-sidebar-bg` (B.1.8). Before deleting each
|
|
||||||
one: `grep -rn "<name>" src` = 0.
|
|
||||||
6. Create a temporary, **uncommitted** test page, or use DevTools, to confirm
|
|
||||||
`size-cover-sm`, `rounded-thumb`, `max-w-hero`, `text-display-1`, `z-(--z-menu)`
|
|
||||||
and `stroke-thin` generate CSS (Tailwind 4 only generates what it sees used: test with a
|
|
||||||
class on a component and revert).
|
|
||||||
|
|
||||||
Closing: check+lint; before/after screenshot of Home identical.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P2: Theme, single source of truth + no flash in light mode
|
|
||||||
|
|
||||||
**Objective:** eliminate the `theme.css` ↔ `tokens.ts` duplication and the dark flash when
|
|
||||||
loading with light mode saved.
|
|
||||||
|
|
||||||
Findings:
|
|
||||||
|
|
||||||
- The default values of the ~15 dynamic tokens are written **twice**: in
|
|
||||||
`theme.css` (`:root`, hue 145) and in `buildThemeTokens()`. The light block in
|
|
||||||
`theme.css` repeats `--mf-bg`, `--mf-elevated`, `--mf-panel-bg`, `--mf-bar-bg`,
|
|
||||||
`--mf-hairline`, `--mf-hero-bg`, `--mf-spotlight-bg`, `--mf-modal-glow`, which
|
|
||||||
`tokens.ts` then overrides inline anyway.
|
|
||||||
- `mode.svelte.ts` only applies `data-theme` after hydration, causing a dark flash.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. `src/app.html`: add an inline `<head>` script that reads `localStorage['musify.theme']`
|
|
||||||
and sets `document.documentElement.dataset.theme` before the first paint (try/catch).
|
|
||||||
2. `+layout.svelte`: in `<svelte:head>`, emit
|
|
||||||
`<style>:root{…buildThemeTokens(ACCENT_HUE,'dark')}:root[data-theme=light]{…buildThemeTokens(ACCENT_HUE,'light')}</style>`
|
|
||||||
(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 `<Avatar size="hero">` (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 `<button>` left hand-styled.
|
|
||||||
|
|
||||||
Findings (hand-made buttons): TopBar back/forward (circles `h-7.5`), clear
|
|
||||||
search, login (`bg-accent-soft rounded-full`), MobileHeader login, Sidebar `+` ×2, Queue
|
|
||||||
close/"Clear", PlayerExtras queue, TransportControls (shuffle, prev, play, next, repeat),
|
|
||||||
TrackInfo heart, "Best match" (circle `h-11.5`), MixTile overlay (`h-9.5`),
|
|
||||||
`BackLink`. Overrides with `!`: Home `!bg-cta-strong !text-ink hover:!brightness-95` and
|
|
||||||
`!font-normal`.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. `Button.svelte`: variants `subtle` (today primary/secondary), `glass` (today
|
|
||||||
`secondaryOnGlow`), `accent`, `strong` (cta-strong/ink, replaces the `!` overrides), `danger`.
|
|
||||||
Keep the `primary`/`secondary` aliases only if P0 didn't land a decision. Radius →
|
|
||||||
`rounded-control`. Remove the need for `!font-normal` (normal weight in `glass`, or a
|
|
||||||
`weight` prop).
|
|
||||||
2. `IconButton.svelte`: add `shape: 'square'|'round'`, `size: 'xs'|'sm'|'md'|'lg'`,
|
|
||||||
`tone: 'muted'|'subtle'|'plain'`, `pressed?: boolean` (→ `aria-pressed` + `text-accent`),
|
|
||||||
`href?`, `surface?: boolean` (`surface-2` background). Icons with `size-icon-*`.
|
|
||||||
3. Create `PlayButton.svelte` (B.3); play/pause SVG icons from `TransportControls` move
|
|
||||||
into it (one single place for those `<svg>`s).
|
|
||||||
4. Migrate all listed buttons. `TopBar` "forward" is a disabled decorative `<span>` and
|
|
||||||
`PlayerExtras` has a decorative `AlignJustify` icon with no function:
|
|
||||||
**ask** whether to remove them (visually dead code) or keep them for fidelity to
|
|
||||||
the prototype.
|
|
||||||
|
|
||||||
Closing: `grep -rn "<button" src | grep -v "ui/\(Button\|IconButton\|PlayButton\|Chip\|ListRow\)"`
|
|
||||||
→ only justified cases remain (dropzones, segmented control).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P5: TypeScript helpers
|
|
||||||
|
|
||||||
**Objective:** create B.2 and migrate its usages (logic, not styles).
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Create `actions/pressable.ts`, `actions/clickOutside.ts`, `toggle.svelte.ts`.
|
|
||||||
`createPlaylistModal` and `queuePanel` → `createToggle()`; delete the two old files
|
|
||||||
or reduce them to `export const queuePanel = createToggle()`.
|
|
||||||
2. `format.ts`: `plural()`; migrate the 15 hand-rolled pluralizations (`grep -rn "=== 1 ?" src`).
|
|
||||||
Unify to lowercase ("3 songs"), **visible Δ** on Playlists/Home/liked.
|
|
||||||
3. `player.svelte.ts`: `toQueueItem`, `isQueueCurrent`, `playAllOrToggle`,
|
|
||||||
`playShuffled`; rename `addToQueue` → `playNextItem` (the current name is confusing:
|
|
||||||
it inserts next, not at the end). Migrate album, playlist, liked, mixes, Home.
|
|
||||||
4. `recentlyPlayed.ts` and `liked.svelte.ts`: use `toQueueItem`.
|
|
||||||
5. `tracks.ts`: the `TrackTarget = { track }` wrapper plus 8 accessors (`targetTitle`,
|
|
||||||
`targetArtist`, …) doesn't add anything. Replace with the `ApiTrackLike` object directly
|
|
||||||
and delete the accessors; keep only `isCurrent(track)` if needed. (This touches Home,
|
|
||||||
Explore, Mix, menus: do it in this part so there's no mix of styles left behind.)
|
|
||||||
6. `navigation.svelte.ts`: `searchHref`, `appNavLinks`.
|
|
||||||
7. `menus.svelte.ts`: `createTrackMenu()`; migrate Home, Explore, Mix.
|
|
||||||
|
|
||||||
Closing: check+lint; reproduce from album, playlist, liked, mix, spotlight; the
|
|
||||||
"Play next" context menu action still works.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P6: Menus, `ContextMenu` + `AccountMenu` + `GlassMenu`
|
|
||||||
|
|
||||||
**Objective:** a single context menu.
|
|
||||||
|
|
||||||
Findings: `TrackContextMenu` (109 lines) and `AlbumContextMenu` (119) are copies:
|
|
||||||
same positioning function with the same constants, same backdrop, same Escape handling,
|
|
||||||
same "Add to playlist" submenu with `<form use:enhance>`; only the items and hidden
|
|
||||||
fields differ. Their items hand-reimplement `MenuItem`'s classes (with
|
|
||||||
`strokeWidth={2}` instead of `1.8`). The playlist item reimplements `ListRow`.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Create `ContextMenu.svelte` (B.3) using `GlassMenu` + `MenuItem` + `Artwork size="xs"`
|
|
||||||
and widths `--mf-menu-w`/`--mf-submenu-w`; export `contextMenuPosition(event)` from
|
|
||||||
the module using `MENU_WIDTH`/`SUBMENU_WIDTH` from `config.ts`.
|
|
||||||
2. Replace usages in Home, Explore (track and album), Mix. Delete the two old menus.
|
|
||||||
3. `AccountMenu`: use `clickOutside`; remove the duplicated `div.relative` wrapper that
|
|
||||||
`TopBar`/`MobileHeader` put around it; `panelClass` → `width: 'sm'|'md'` prop.
|
|
||||||
4. Add Escape support to `AccountMenu` (currently it doesn't close on Escape, an a11y improvement).
|
|
||||||
|
|
||||||
Closing: track and album menus in Explore, submenu opens to the left near the edge,
|
|
||||||
mobile (submenu below), Escape closes it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P7: Rows, `ListRow` + `MediaIdentity` + `EqBars`
|
|
||||||
|
|
||||||
**Objective:** a single compact row for every list that isn't a table.
|
|
||||||
|
|
||||||
Rows reimplemented today (all of them: art + title + subtitle + meta + active state):
|
|
||||||
|
|
||||||
| Where | Size |
|
|
||||||
| ------------------------------------- | -------- |
|
|
||||||
| `SearchResultRow` (Explore ×4 groups) | lg |
|
|
||||||
| Home "Continue listening" (card) | lg, card |
|
|
||||||
| Home "Popular" (index/EQ + meta) | sm |
|
|
||||||
| Home spotlight (index, no art) | sm |
|
|
||||||
| Home "Your playlists" (card) | lg, card |
|
|
||||||
| `Queue` "Now playing" + "Up next" | sm |
|
|
||||||
| `Sidebar` playlists | xs |
|
|
||||||
| `ContextMenu` playlist items | xs |
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Rename/generalize `TrackTitleCell` → `MediaIdentity` (B.3). Use it in
|
|
||||||
`player/TrackInfo` (replacing its cover+title+artist block).
|
|
||||||
2. Create `ListRow` (B.3) on top of `MediaIdentity` + `pressable`; `card` variant = padding
|
|
||||||
`p-2.25 pr-3.5 rounded-art` (Continue/Your playlists).
|
|
||||||
3. `NowPlaying` is basically `EqBars` with an overlay: add an `overlay` prop to `EqBars`
|
|
||||||
(background `scrim/45`, `on-art` color) and delete `NowPlaying`.
|
|
||||||
4. Migrate every row in the table above. Delete `SearchResultRow`.
|
|
||||||
|
|
||||||
Closing: Home, Explore, Queue, Sidebar unchanged visually except for the B.1.2 Δ; keyboard
|
|
||||||
(Tab + Enter) plays from every row.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P8: Track tables, `TrackList`
|
|
||||||
|
|
||||||
**Objective:** declarative tables; delete 3 cells and 3 route components.
|
|
||||||
|
|
||||||
Findings:
|
|
||||||
|
|
||||||
- 6 tables (`library`, `liked`, `mixes/[id]`, `AlbumTrackTable`, `PlaylistTrackTable`,
|
|
||||||
`LibraryPicker`) repeat: `{@const active}`, `TrackIndexCell`, `TrackTitleCell`, N
|
|
||||||
`TrackMeta`s, and a `<form method=POST use:enhance><input hidden trackId><IconButton revealOnHover>`.
|
|
||||||
- Column widths passed down from each page are **inconsistent**: "Plays" is `106px` in
|
|
||||||
some places and `100px` in others.
|
|
||||||
- The "Album" column **always shows a placeholder dash**: it's a stand-in with no real data.
|
|
||||||
**Decision needed**: remove it (recommended) or leave it in the preset.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Create `TrackList.svelte` (B.3) with column presets in the module itself:
|
|
||||||
`{ album: {label:'Album', width:'76px'}, added: {'Added','124px'}, uploaded: {'Uploaded','124px'}, plays: {'Plays','104px'} }`
|
|
||||||
and fixed columns (`index 32px/28px`, title `minmax(180px,1fr)/minmax(120px,1fr)`,
|
|
||||||
duration `104px/52px`, action `36px/32px`) as module constants.
|
|
||||||
2. `rowAction={{ action: '?/removeTrack', icon: X, label: 'Remove from playlist' }}`
|
|
||||||
generates the form+IconButton; for liked (`onclick`), accept `rowAction.onclick`.
|
|
||||||
3. Migrate the 6 tables; inline `AlbumTrackTable`, `PlaylistTrackTable`, `LibraryPicker`
|
|
||||||
into their pages and delete them. Delete `TrackRow`, `TrackIndexCell`, `TrackMeta`,
|
|
||||||
`TrackTable`.
|
|
||||||
4. `LibraryPicker` uses the `NowPlaying` overlay instead of an index: `TrackList` with
|
|
||||||
`index={false}` shows the EQ over the art (via `Artwork` children).
|
|
||||||
|
|
||||||
Closing: all 6 pages; hover reveals the action; the active track shows the EQ; mobile hides meta.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P9: Cards and grids
|
|
||||||
|
|
||||||
**Objective:** one card, zero wrappers.
|
|
||||||
|
|
||||||
Findings: `AlbumCard` and `PlaylistCard` only build the subtitle and pick the art;
|
|
||||||
`MixTile` is a `MediaCard` with a play overlay and different typography; `MediaGrid` is a
|
|
||||||
`<div>` with inline variables; Home (mixes 164px, playlists 258px) and Explore (genres
|
|
||||||
268px) each write their own `grid-cols-[repeat(auto-fill,minmax(…))]`.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. `MediaCard`: props `href`, `title`, `subtitle?`, `onPlay?` (`PlayButton variant="glass"`
|
|
||||||
overlay), `art` snippet **or** `Artwork` props, `oncontextmenu?`, `index`.
|
|
||||||
Fixed typography (`text-sm text-fg` / `text-xs text-fg-3`).
|
|
||||||
2. Helpers in `format.ts`: `albumMeta(album)`, `playlistMeta(playlist)`.
|
|
||||||
3. Replace `AlbumCard`, `PlaylistCard`, `MixTile` with `MediaCard`; replace `MediaGrid`
|
|
||||||
with `grid-cards`/`grid-cards-lg`/`grid-tiles`/`grid-wide` (B.1.7). Delete the 4 files.
|
|
||||||
4. Explore's genre tile: the `style` with `--tile-hue` and its gradient move into a
|
|
||||||
`@utility genre-tile` that reads `var(--tile-hue)`.
|
|
||||||
|
|
||||||
Closing: /albums, /playlists, /u/[id], Home (mixes), Explore (genres).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P10: Headers, dialogs and typography
|
|
||||||
|
|
||||||
**Objective:** 1 page header, 1 section header, 1 confirmation dialog.
|
|
||||||
|
|
||||||
Findings:
|
|
||||||
|
|
||||||
- `PageHeader` is basically `CollectionHeader` without the cover/eyebrow. Explore's
|
|
||||||
search header reimplements both (eyebrow `text-sm font-semibold tracking-[0.16em]`).
|
|
||||||
- `AlbumHeader`/`PlaylistHeader` (routes) just wire up the meta and the buttons.
|
|
||||||
- Each page decides its own margin after the header (`mt-7 sm:mt-8`, `mt-6 sm:mt-8`, `mt-9`,
|
|
||||||
`mt-10 sm:mt-12`…).
|
|
||||||
- 2 delete dialogs with different typography.
|
|
||||||
- `EmptyState` literally duplicated in Home ("You haven't listened to anything yet" ×2).
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Merge into `PageHeader`: `title`, `eyebrow?`, `description?`, `meta?`, `cover?`,
|
|
||||||
`actions?`, `align?`. **The header owns its own bottom margin** (`mb-7 sm:mb-8`);
|
|
||||||
remove the `mt-*` from whatever follows it. Delete `CollectionHeader`.
|
|
||||||
2. Inline `AlbumHeader`/`PlaylistHeader` into their pages (with `plural()` and the `BackLink`)
|
|
||||||
and delete them. Button icons: unify (album has icons, playlist doesn't), so
|
|
||||||
**decide**: icons on both (recommended) or on neither.
|
|
||||||
3. `SectionHeading`: add a `count?` prop that renders the line + "N results" (currently
|
|
||||||
repeated 4× in Explore) and `href?`/`linkLabel?` for "View all".
|
|
||||||
4. `ConfirmDialog` (B.3); migrate album and playlist. `Modal` uses `.glass-panel` and
|
|
||||||
`z-(--z-modal)`; its eyebrow and title use the recipes.
|
|
||||||
5. Apply B.1.6 recipes in: `EmptyState`, `+error.svelte`, "No results", the Home
|
|
||||||
hero, stats, `Queue`, `Sidebar` ("Your playlists"), `PlaylistForm` (visibility).
|
|
||||||
6. Home: replace the repeated empty state with a local snippet.
|
|
||||||
|
|
||||||
Closing: every page has a header; delete dialogs look identical.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P11: Forms
|
|
||||||
|
|
||||||
**Objective:** a single form skeleton with a cover.
|
|
||||||
|
|
||||||
Findings:
|
|
||||||
|
|
||||||
- `AlbumForm` and `PlaylistForm` share: `use:enhance` with `submitting`,
|
|
||||||
`ImageDropzone gradient class="h-36 w-36 rounded-2xl"`, `Field`/`Input`/`Textarea`,
|
|
||||||
`Alert`, and a Cancel/Submit footer.
|
|
||||||
- `upload/+page.svelte` reimplements the cover picker (label + input + preview +
|
|
||||||
gradient) that `ImageDropzone` already does.
|
|
||||||
- `Input` and `Textarea` repeat the same class string.
|
|
||||||
- The Private/Public toggle (`w-[calc(50%-4px)]`) and Explore's chips are both single-option
|
|
||||||
selectors with their own styles.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. `CoverForm.svelte` (skeleton): props `action`, `coverFallbackUrl?`, `coverIcon`,
|
|
||||||
`requireCover?`, `formMessage?`, `submitLabel`, `submittingLabel`, `canSubmit`,
|
|
||||||
`helperText?`, `onCancel`, `onSuccess?`, `fields` snippet. `AlbumForm` and
|
|
||||||
`PlaylistForm` end up as ~30 lines of fields each (or get inlined into their
|
|
||||||
pages if only used in one place: `AlbumForm` is used in 2, `PlaylistForm` in 2, so
|
|
||||||
keep them as components).
|
|
||||||
2. `ImageDropzone`: `size="hero"` using `size-cover-hero` + `rounded-art-lg`; upload
|
|
||||||
uses it (Δ: 64px icon → `icon-xl`).
|
|
||||||
3. `@utility field-control` in `layout.css` with the shared `Input`/`Textarea` class string.
|
|
||||||
4. `SegmentedControl.svelte` (`options`, bindable `value`) for Private/Public; `Chip`
|
|
||||||
(B.3) for Explore.
|
|
||||||
|
|
||||||
Closing: create/edit album and playlist, upload a song (including drag & drop).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P12: App chrome (layout, navigation, player)
|
|
||||||
|
|
||||||
**Objective:** a single navigation definition and a smaller player.
|
|
||||||
|
|
||||||
Findings:
|
|
||||||
|
|
||||||
- `Sidebar` and `TabsBar` each define their own link list + `isActive`.
|
|
||||||
- `MobileHeader` compensates for links missing from `TabsBar` with extra menu items.
|
|
||||||
- Logo: gradient + text in MobileHeader and login; text-only in Sidebar.
|
|
||||||
- `PlayerBar` repeats the "glass bar" string twice (same as `TabsBar`).
|
|
||||||
- `PlayerExtras` (22 lines) only composes volume + queue button.
|
|
||||||
- `SeekBar` is a click-only `<button>` (no keyboard, no dragging) with `h-[3.5px]`;
|
|
||||||
`VolumeControl` is a range `<input>` with its own `<style>` (`84px`, `11px`, `3.5px`).
|
|
||||||
- `TrackInfo` duplicates the track identity block (→ `MediaIdentity`, P7).
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. `appNavLinks()` (B.2) in Sidebar and TabsBar (`primary` = the 4 items from TabsBar).
|
|
||||||
2. `Logo.svelte`; use it in Sidebar, MobileHeader and login (Δ: Sidebar gains the gradient
|
|
||||||
square, **confirm this**).
|
|
||||||
3. `glass-bar` in PlayerBar ×2 and TabsBar; `theme-transition` in Sidebar, Queue, etc.
|
|
||||||
4. Merge `PlayerExtras` into `PlayerBar`; side widths via `--mf-player-side-w`.
|
|
||||||
5. `Slider.svelte` (B.3) with an `<input type="range">` styled through the
|
|
||||||
`--mf-range-h/--mf-range-thumb/--mf-volume-w` tokens; replaces `SeekBar` and `VolumeControl`
|
|
||||||
(bonus: dragging and keyboard support on the progress bar). Delete both.
|
|
||||||
6. `TopBar`: use `searchHref`, `IconButton`, `Avatar`; `max-w-search`.
|
|
||||||
7. Header/dock/menu z-index via B.1.5.
|
|
||||||
|
|
||||||
Closing: desktop/tablet/mobile navigation, queue opens/closes, seek via click, drag and
|
|
||||||
arrow keys, volume.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P13: Big pages, Explore (602 lines) and Home (466)
|
|
||||||
|
|
||||||
**Objective:** make these pages composition, not logic or repeated markup. Do this
|
|
||||||
**after** P7 through P10 (uses `ListRow`, `SectionHeading count`, `PageHeader`).
|
|
||||||
|
|
||||||
Explore:
|
|
||||||
|
|
||||||
1. `GENRE_INFO` + `genreTiles` → `src/lib/genres.ts`.
|
|
||||||
2. Search logic (`topResult`, `searchCounts`, `searchChips`, `capped`,
|
|
||||||
`showGroup`, `playlistMatches`) → `src/lib/search.ts` (pure functions).
|
|
||||||
3. The 4 group blocks (Songs/Albums/Users/Playlists) → a single `{#each groups}`
|
|
||||||
with `SectionHeading count` + `ListRow`.
|
|
||||||
4. "Best match": a single markup block with `Artwork`/`Avatar` depending on type + `PlayButton
|
|
||||||
as="span"`; eliminate the duplicated button/a snippet with a `ListRow`-like element or
|
|
||||||
`<svelte:element this={href ? 'a' : 'button'}>`.
|
|
||||||
5. `loadMore` pagination: reusable if another page needs it; if not, leave as is.
|
|
||||||
|
|
||||||
Home:
|
|
||||||
|
|
||||||
1. Use `Page` instead of the hand-written `page-x pt-3 sm:pt-4 …`; `gap-11.5` → a section
|
|
||||||
token (`--spacing-section: 2.875rem`, `gap-section`).
|
|
||||||
2. The 3 identical stats blocks → `{#each stats}`.
|
|
||||||
3. Hero/spotlight: background `style=` → `@utility hero-surface` / `spotlight-surface`.
|
|
||||||
4. Rows → `ListRow` (if P7 didn't already do it).
|
|
||||||
|
|
||||||
Closing: Explore with no query, with a query, filters, no results, infinite scroll; Home
|
|
||||||
complete with and without data.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### P14: Final sweep and guardrails
|
|
||||||
|
|
||||||
**Objective:** make sure what's been cleaned up doesn't come back.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
|
|
||||||
1. Script `web-player/scripts/check-tokens.mjs` + `npm run lint:tokens` (add it to
|
|
||||||
`lint`). Fails if it finds any of the following in `src/**/*.svelte`:
|
|
||||||
- `-\[` arbitrary values outside the B.4 allow list;
|
|
||||||
- `rounded-(sm|md|lg|xl|2xl|3xl)\b` or a bare `rounded\b`;
|
|
||||||
- `\b[hw]-\d` paired with its counterpart on the same element (should be `size-*`);
|
|
||||||
- fractional spacing `-\d+\.\d+` in `h-`/`w-`/`size-` (sizes should be tokens);
|
|
||||||
- `#[0-9a-f]{3,8}` in classes, `rgba(`, `strokeWidth=`, `text-base` with no recipe;
|
|
||||||
- `style="` outside the allow list.
|
|
||||||
2. Remaining fractional spacing (`gap-2.75`, `py-1.75`, `mt-3.25`, `p-2.25`…):
|
|
||||||
round to the nearest 0.5 step **or** to the whole-number scale when the Δ is ≤ 1px.
|
|
||||||
Guide table: `.25/.75` → nearest `.5`; `mt-0.75` → `mt-1`; `gap-1.75` → `gap-2`;
|
|
||||||
`p-4.25` → `p-4`; `mb-4.5` (SectionHeading) stays as is (Δ 2px, visible across all
|
|
||||||
sections).
|
|
||||||
3. Orphan detector (run it repeatedly and delete whatever it flags):
|
|
||||||
```bash
|
|
||||||
for f in $(find src/lib/components -name "*.svelte"); do n=$(basename "$f" .svelte); c=$(grep -rlw "$n" --include=*.svelte --include=*.ts src | grep -v "/$n.svelte$" | wc -l); [ "$c" -eq 0 ] && echo "$f"; done
|
|
||||||
```
|
|
||||||
4. Update `web-player/CLAUDE.md`, the "Styles" section, with: B.4, "use `size-cover-*`,
|
|
||||||
`size-icon-*`, the `text-display-*`/`text-eyebrow`/`text-body`/`text-count` recipes,
|
|
||||||
semantic radii; components: `Artwork`, `Avatar`, `ListRow`, `TrackList`,
|
|
||||||
`MediaCard`, `PageHeader`, `ContextMenu`…". Delete `docs/design-system-refactor-plan.md`.
|
|
||||||
5. Recompute the A.4 table and log it in D.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## D. Log
|
|
||||||
|
|
||||||
| Part | Status | Commit | Notes / decisions made |
|
|
||||||
| ---- | ------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| P0 | ✅ | (pending) | Button primary/secondary stay identical (user's decision) |
|
|
||||||
| P1 | ✅ | (pending) | Verified with `build` (Tailwind compiles `@apply sm:` inside `@utility`); no authenticated visual QA (Docker not available in this session) |
|
|
||||||
| P2 | ✅ | (pending) | `<style>{expr}</style>` doesn't interpolate in Svelte (it's treated as the component's own CSS); solution: `{@html \`<style>${css}</style>\`}` |
|
|
||||||
| P3 | ✅ | (pending) | Removed the name under the profile avatar (decision made: redundant with CollectionHeader's title). Added `size="fill"` to Artwork (not in the spec) for the fluid grid cards (AlbumCard/PlaylistCard/MixTile), since P9 (MediaCard) doesn't exist yet. SearchResultRow lost its `kind`/radius-by-type prop (radius is now decided by Artwork/Avatar). No authenticated visual QA (Docker not available) |
|
|
||||||
| P4 | ✅ | (pending) | Decision made without blocking: removed the decorative "forward" button (TopBar) and the decorative `AlignJustify` icon (PlayerExtras) since they were visually dead code. `TransportControls` (shuffle/prev/next/repeat) is intentionally left unmigrated to IconButton: those are chromeless icons (no background/hit box), migrating them would have added a hover pill that doesn't exist today, a visual risk that couldn't be verified without Docker |
|
|
||||||
| P5 | 🟡 | (pending) | Done: pressable/clickOutside, toggle.svelte.ts, plural() (+ lowercase, visible Δ), player.svelte.ts helpers (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks, appNavLinks created but its migration into Sidebar/TabsBar is left for P12 as that part specifies), menus.svelte.ts (createTrackMenu, migrated in Home/Explore/Mix). **Deliberately pending**: task 5 (removing the `TrackTarget` wrapper from tracks.ts and its 8 accessors) is deferred, it's the riskiest change in the plan (touches types across Home/Explore/Mix/menus) and couldn't be verified visually without Docker; tackle it together with P13 when those pages get rewritten |
|
|
||||||
| P6 | ✅ | (pending) | AccountMenu's z-index unified to z-(--z-menu) (was z-20, now at the same level as ContextMenu, a minor Δ that improves consistency); no authenticated visual QA (Docker not available) |
|
|
||||||
| P7 | ✅ | (pending) | MediaIdentity stays "pure" (no onClick) per spec; the 6 track tables (P8) wrap their own `use:pressable` until P8 replaces them with TrackList. Documented deltas: Sidebar's active row now uses bg-accent-tint/text-accent-soft (was neutral gray, now consistent with other active rows); Home's "Your playlists" card loses the "by {your name}" line (redundant, it's always the current user) and its radius changes from rounded-panel to rounded-art (consistent with the "Continue" card); Queue's "Up next" rows lose the art's opacity-80 (ListRow doesn't expose that customization). No authenticated visual QA (Docker not available) |
|
|
||||||
| P8 | ✅ | (pending) | Decision made (recommended by the plan itself, non-blocking): removed the "Album" column that always showed a placeholder dash. TrackList unifies TrackTable+TrackRow+TrackIndexCell+TrackMeta; AlbumTrackTable/PlaylistTrackTable/LibraryPicker are inlined into their pages and deleted. No authenticated visual QA (Docker not available) |
|
|
||||||
| P9 | ✅ | (pending) | MediaCard gains the art's hover-zoom and scrim+PlayButton, conditioned on `onPlay` (previously exclusive to MixTile); this will now apply to any future card with onPlay, an intentional consistency choice. No authenticated visual QA (Docker not available) |
|
|
||||||
| P10 | ✅ | (pending) | Decision made (recommended, non-blocking): icons on playlist header buttons (previously only album had them). PlaylistHeader loses its unused `id` prop after being inlined. Explore: the "Results for…" header now uses PageHeader's `mb-7/8` instead of a hand-tuned `mt-5` before the filter chips, slightly larger gap between the summary and the chips (minor, documented Δ). No authenticated visual QA (Docker not available) |
|
|
||||||
| P11 | ✅ | (pending) | CoverForm adds an `extra` snippet (not in the literal spec, only `fields`) for full-width content below the cover+fields row (album description, playlist visibility selector) that didn't fit in a single column. Upload uses ImageDropzone with `onselect` (new prop) and wraps it in `{#key}` to be able to reset it after publishing, Δ: the cover container goes from 144/176px (mobile/desktop) to a fixed 144px (size-cover-hero, no larger desktop variant), and the empty-state icon goes from 64px to 36px (icon-xl), as the plan recommends. No authenticated visual QA (Docker not available) |
|
|
||||||
| P12 | ✅ | (pending) | PlayerExtras merges into PlayerBar (volume/queue inline); Slider replaces SeekBar+VolumeControl (confirmed via grep that SeekBar's `compact` mode was dead code). Sidebar and TabsBar migrate to `appNavLinks()` (P5). Decision made (non-blocking): Sidebar gains the gradient-branded `Logo` it didn't have before, for consistency with MobileHeader/login. PlayerDock/MobileHeader z-index fixed to `z-(--z-sticky)` (they're fixed bars, not menus) and SegmentedControl to `z-(--z-raised)`, as B.1.5 specifies. `glass-bar`/`theme-transition` applied on PlayerBar (both variants), TabsBar, Queue and Home (hero/spotlight), replacing duplicated styles. No authenticated visual QA (Docker not available); `/login` smoke test with no console errors |
|
|
||||||
| P13 | ✅ | (pending) | This part also tackles the task deferred in P5: removes the `TrackTarget` wrapper from `tracks.ts` and its 8 accessors (`targetId`/`targetTitle`/`targetArtist`/`targetOwnerUserId`/`targetExplicit`/`targetListensCount`/`isTargetCurrent`/`queueItemForTarget`/`targetForQueueItem`); Explore, Home, Mix, `menus.svelte.ts` and `mixes.ts` now use track objects (`ApiTrackLike`/`LocalTrack`) directly, with `toQueueItem`/`trackFromQueueItem` (new, in `player.svelte.ts`) as the only conversion functions. TypeScript revealed the `isExplicit` field doesn't exist in the real `/tracks` schema, Explore's "explicit" badge was already a no-op before the refactor (always `undefined ?? false`); the binding was removed instead of faking support for it. Explore: `GENRE_INFO`/`genreTiles` → `src/lib/genres.ts`; search logic (`showGroup`/`capped`/`matchPlaylists`/`searchCounts`/`searchChips`/`findTopResult`) → `src/lib/search.ts` (pure functions, parameterized by `sfilter`/`query` instead of closing over component state); "Best match" unifies the duplicated button/link with `<svelte:element this={...}>`. Decision made without blocking: the 4 group blocks (Songs/Albums/Users/Playlists) are **not** collapsed into a single `{#each groups}`, each group has genuinely different snippets, context menus and `ListRow` props (track menu vs. album menu, overlay only on songs, differently formatted trailing content), and forcing a generic configuration would have added more indirection than it removed. Home: uses `Page` (previously a hand-written `page-x pt-3 sm:pt-4`); the 3 stats blocks → `{#each}`; hero/spotlight backgrounds (`style=` with `var(--mf-hero-bg)`/`var(--mf-spotlight-bg)`) → new `hero-surface`/`spotlight-surface` utilities (in `layout.css`) and the `--spacing-section: 2.875rem` token → `gap-section` replaces `gap-11.5`. No authenticated visual QA (Docker not available); `/explore` without a session redirects to `/login` because `+layout.server.ts` requires `allowAnonymousListening` from the backend (unreachable without Docker), a pre-existing behavior, not a regression; `/login` smoke test with no console errors |
|
|
||||||
| P14 | ✅ | (pending) | `scripts/check-tokens.mjs` (`npm run lint:tokens`, added to `lint`) scans `src/**/*.svelte` and fails on: `-[…]` arbitrary values outside the B.4 allow list, bare `rounded-(sm/md/lg/xl/2xl/3xl)`/`rounded`, `h-N w-N` pairs (should be `size-N`), fractional quarter spacing (`.25`/`.75`), literal hex/`rgba()` values. Cleaned the repo down to 0 violations: ~40 `h-N w-N` pairs → `size-N` (mostly icons, via a one-off script), ~26 quarter-spacing values rounded to the nearest whole number (Δ≤1px, e.g. `py-2.75`→`py-3`, `gap-1.75`→`gap-2`), 3 `rounded-sm/lg` + 1 bare `rounded` → tokens (`rounded-tag`/`rounded-thumb`/`rounded-control`), 2 `tracking-[-0.02em]` → `tracking-display` (matched the existing token exactly and hadn't been migrated), 1 `tracking-[0.14em]` in Explore → the `text-eyebrow` recipe, Home hero's `rounded-3xl` → `rounded-panel-lg` (consistent with the featured playlist card). Removed the `stroke-thin/regular/bold` recipes (created in P1, 0 consumers), **decision made, non-blocking**: `strokeWidth={n}` (13 values, across almost every component) isn't migrated to those CSS recipes because there's no way to verify without Docker that `lucide-svelte` respects `stroke-width` via class instead of the prop, and a regression there would be silent across the whole app; the script documents this gap explicitly and doesn't enforce it. Also tackled the task deferred from P5 (see P13). Orphan detector: 0 components with no importers (all 47 in `src/lib/components/` are in use). `docs/design-system-refactor-plan.md` (previous plan, superseded) deleted. `web-player/CLAUDE.md` gains a "Styles" section with the B.4 allow list, the base components to reuse, and the documented `strokeWidth` gap. Baseline recalculated (see table below) |
|
|
||||||
|
|
||||||
Pending product decisions (ask the user before the listed part):
|
|
||||||
|
|
||||||
- P0/P4: should `Button` primary look different from secondary?
|
|
||||||
- P3: remove the name under the profile avatar?
|
|
||||||
- P4: remove the decorative "forward" button and the `AlignJustify` icon from the player?
|
|
||||||
- P5: lowercase pluralizations across the whole app.
|
|
||||||
- P8: remove the "Album" column that always shows a placeholder dash?
|
|
||||||
- P10: icons on album **and** playlist header buttons?
|
|
||||||
- P12: gradient-square logo in the Sidebar too?
|
|
||||||
|
|
||||||
### D.1 Recalculated baseline (after P14, closing out the refactor)
|
|
||||||
|
|
||||||
Recalculated with the same criteria as A.4 (grep over `src/`), in the same session that closed out P14.
|
|
||||||
|
|
||||||
| Metric | A.4 (before) | After P14 |
|
|
||||||
| --------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `.svelte` components in `src/lib/components/` | 61 | 47 |
|
|
||||||
| Client `.svelte`/`.css`/`.ts` lines | ~8,700 | ~8,310 (−~390; the −1,500/−2,000 goal had already been reached in P3 through P11, before P13 added `genres.ts`/`search.ts` as new, well-separated code) |
|
|
||||||
| Arbitrary classes `x-[…]` | 44 | 11 (the remaining 11 are all in the B.4 allow list) |
|
|
||||||
| Fractional quarter spacing (`.25`/`.75`) | ~190 occurrences | 0 (rounded to the nearest whole number, Δ≤1px) |
|
|
||||||
| Distinct radii used | 13 | 8 (7 B.1.1 tokens + `rounded-full`) |
|
|
||||||
| Distinct `strokeWidth` values | 13 | 12 (out of scope, see the P14 log entry and CLAUDE.md) |
|
|
||||||
| `style="…"` attributes | 24 | 16 (across 12 files; all dynamic per instance, stagger, menu position, `var(--mf-*)`, animation, none with a literal color/measurement) |
|
|
||||||
| Hand-rolled pluralizations `=== 1 ?` | 15 | 2 (1 is `plural()` itself in `format.ts`; 1 is a past-participle agreement in `library` that `plural()` can't cover) |
|
|
||||||
| `eslint` errors | 2 | 0 |
|
|
||||||
| `npm run lint:tokens` (new, P14) | n/a | 0 violations |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## E. Full inventory (verdict per file)
|
|
||||||
|
|
||||||
Legend: **=** stays as is · **~** gets modified · **→X** merges into X · **✕** gets deleted.
|
|
||||||
|
|
||||||
### `src/lib/components/ui/`
|
|
||||||
|
|
||||||
| File | Lines | Verdict | Part | Reason |
|
|
||||||
| ---------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------- |
|
|
||||||
| AccountMenu | 73 | ~ | P6 | `clickOutside`, Escape, width via prop |
|
|
||||||
| AlbumCard | 62 | ✕ →MediaCard | P9 | meta wrapper + the `trackId=albumId` hack |
|
|
||||||
| AlbumForm | 123 | ~ (CoverForm) | P11 | shared skeleton |
|
|
||||||
| Alert | 20 | = | | |
|
|
||||||
| ArtistAvatar | 36 | ✕ →Avatar | P3 | |
|
|
||||||
| BackLink | 33 | ~ | P4 | `IconButton`/tokens; remove duplicated `hover:text-accent-soft` |
|
|
||||||
| Badge | 23 | ✕ | P0 | 0 uses |
|
|
||||||
| Button | 66 | ~ | P0/P4 | duplicated variants, `!` overrides |
|
|
||||||
| Checkbox | 29 | ~ | P14 | `size-*`, tokens |
|
|
||||||
| CollectionHeader | 45 | ✕ →PageHeader | P10 | |
|
|
||||||
| Cover | 47 | ✕ →Artwork | P3 | |
|
|
||||||
| EmptyState | 31 | ~ | P10 | recipes |
|
|
||||||
| EqBars | 23 | ~ | P7 | absorbs `NowPlaying` |
|
|
||||||
| ExplicitBadge | 13 | ~ | P1/P14 | `rounded-tag`, `size-4` |
|
|
||||||
| Field | 27 | = | | |
|
|
||||||
| GlassMenu | 15 | = | | menu base |
|
|
||||||
| IconButton | 39 | ~ | P4 | shape/size/tone/pressed/href |
|
|
||||||
| ImageDropzone | 62 | ~ | P11 | `size="hero"`; used by upload |
|
|
||||||
| InfiniteScroll | 39 | = | | |
|
|
||||||
| Input | 59 | ~ | P11 | `field-control` |
|
|
||||||
| MediaCard | 32 | ~ | P0/P9 | 16px bug; absorbs MixTile |
|
|
||||||
| MediaGrid | 27 | ✕ →`grid-*` | P9 | |
|
|
||||||
| MenuItem | 48 | ~ | P6 | `size-icon-md` icons |
|
|
||||||
| MixArt | 25 | ✕ →Artwork | P3 | |
|
|
||||||
| MixTile | 51 | ✕ →MediaCard | P9 | |
|
|
||||||
| Modal | 106 | ~ | P10 | `.glass-panel`, z-token, recipes |
|
|
||||||
| NowPlaying | 20 | ✕ →EqBars | P7 | |
|
|
||||||
| Page | 14 | = | | Home should use it (P13) |
|
|
||||||
| PageHeader | 26 | ~ | P10 | absorbs CollectionHeader |
|
|
||||||
| PlaylistArt | 61 | ✕ →Artwork | P3 | |
|
|
||||||
| PlaylistCard | 23 | ✕ →MediaCard | P9 | |
|
|
||||||
| PlaylistForm | 161 | ~ (CoverForm) | P11 | + SegmentedControl |
|
|
||||||
| SearchResultRow | 78 | ✕ →ListRow | P7 | |
|
|
||||||
| SectionHeading | 32 | ~ | P10 | `count`, "view all" |
|
|
||||||
| Surface | 17 | = | | (upload only; acceptable) |
|
|
||||||
| Textarea | 28 | ~ | P11 | `field-control` |
|
|
||||||
| TrackIndexCell | 27 | ✕ →TrackList | P8 | |
|
|
||||||
| TrackMeta | 14 | ✕ →TrackList | P8 | |
|
|
||||||
| TrackRow | 27 | ✕ →TrackList | P8 | |
|
|
||||||
| TrackTable | 79 | ✕ →TrackList | P8 | |
|
|
||||||
| TrackTitleCell | 77 | → MediaIdentity | P7 | rename + generalize |
|
|
||||||
| **New** | | Artwork, Avatar, ListRow, MediaIdentity, TrackList, ContextMenu, PlayButton, ConfirmDialog, Slider, Logo, Chip, SegmentedControl, CoverForm | | |
|
|
||||||
|
|
||||||
### `src/lib/components/` and `player/`
|
|
||||||
|
|
||||||
| File | Lines | Verdict | Part | Reason |
|
|
||||||
| ------------------------ | ----- | -------------- | ------ | ----------------------------------- |
|
|
||||||
| AlbumContextMenu | 119 | ✕ →ContextMenu | P6 | copy of TrackContextMenu |
|
|
||||||
| TrackContextMenu | 109 | ✕ →ContextMenu | P6 | |
|
|
||||||
| MobileHeader | 70 | ~ | P6/P12 | Avatar, Logo, no duplicated wrapper |
|
|
||||||
| Sidebar | 125 | ~ | P7/P12 | `appNavLinks`, `ListRow` |
|
|
||||||
| TabsBar | 50 | ~ | P12 | `appNavLinks`, `glass-bar` |
|
|
||||||
| TopBar | 140 | ~ | P4/P12 | IconButton, Avatar, `searchHref` |
|
|
||||||
| player/PlayerBar | 28 | ~ | P12 | `glass-bar`, absorbs PlayerExtras |
|
|
||||||
| player/PlayerDock | 29 | ~ | P12 | z-token |
|
|
||||||
| player/PlayerExtras | 22 | ✕ →PlayerBar | P12 | |
|
|
||||||
| player/Queue | 111 | ~ | P7 | `ListRow`, recipes, `pressable` |
|
|
||||||
| player/SeekBar | 48 | ✕ →Slider | P12 | |
|
|
||||||
| player/TrackInfo | 65 | ~ | P7 | `MediaIdentity`, IconButton pressed |
|
|
||||||
| player/TransportControls | 75 | ~ | P4 | `PlayButton`, `IconButton pressed` |
|
|
||||||
| player/VolumeControl | 52 | ✕ →Slider | P12 | |
|
|
||||||
|
|
||||||
### Routes
|
|
||||||
|
|
||||||
| File | Lines | Verdict | Part | Reason |
|
|
||||||
| ---------------------------------------------- | ----- | ---------- | ------------ | ------------------------------------------------------ |
|
|
||||||
| `+layout.svelte` | 142 | ~ | P2 | SSR tokens, backgrounds to utilities, dock padding |
|
|
||||||
| `+page.svelte` (Home) | 466 | ~ | P0/P5/P7/P13 | 4 hand-rolled rows, stats ×3, empty state ×2, `style=` |
|
|
||||||
| `+error.svelte` | 16 | ~ | P10 | recipes, `max-w-prose-sm` |
|
|
||||||
| `login/+page.svelte` | 50 | ~ | P12 | `Logo`, `max-w-cta` |
|
|
||||||
| `(app)/explore/+page.svelte` | 602 | ~ | P13 | split data/logic; 4 groups → each |
|
|
||||||
| `(app)/upload/+page.svelte` | 288 | ~ | P11 | `ImageDropzone`, recipes |
|
|
||||||
| `(app)/library/+page.svelte` | 108 | ~ | P8 | `TrackList` |
|
|
||||||
| `(app)/liked/+page.svelte` | 123 | ~ | P0/P5/P8/P10 | dead imports, `playAllOrToggle`, `TrackList` |
|
|
||||||
| `(app)/mixes/[id]/+page.svelte` | 134 | ~ | P5/P8/P10 | menu, `TrackList`, `PageHeader` |
|
|
||||||
| `(app)/albums/+page.svelte` | 74 | ~ | P9 | `MediaCard` + `grid-cards` |
|
|
||||||
| `(app)/albums/[id]/+page.svelte` | 91 | ~ | P8/P10 | inline header/tables, `ConfirmDialog` |
|
|
||||||
| `albums/[id]/components/AlbumHeader` | 82 | ✕ (inline) | P10 | |
|
|
||||||
| `albums/[id]/components/AlbumTrackTable` | 64 | ✕ (inline) | P8 | |
|
|
||||||
| `albums/[id]/components/LibraryPicker` | 65 | ✕ (inline) | P8 | |
|
|
||||||
| `(app)/playlists/+page.svelte` | 62 | ~ | P9 | |
|
|
||||||
| `(app)/playlists/[id]/+page.svelte` | 95 | ~ | P8/P10 | |
|
|
||||||
| `playlists/[id]/components/PlaylistHeader` | 62 | ✕ (inline) | P10 | |
|
|
||||||
| `playlists/[id]/components/PlaylistTrackTable` | 71 | ✕ (inline) | P8 | |
|
|
||||||
| `(app)/u/[id]/+page.svelte` | 97 | ~ | P3/P9/P10 | Avatar, grid |
|
|
||||||
| `routes/layout.css` | 238 | ~ | P1 | all tokens and recipes |
|
|
||||||
|
|
||||||
### `src/lib/` (client)
|
|
||||||
|
|
||||||
| File | Verdict | Part | Reason |
|
|
||||||
| --------------------------------------------------------------------------------- | -------------------- | ----- | ------------------------------------------------- |
|
|
||||||
| `theme/theme.css` | ~ | P1/P2 | remove `tokens.ts` duplicates, add layout/z |
|
|
||||||
| `theme/tokens.ts` | ~ | P2 | `tokensToCss`, no `--mf-sidebar-bg` |
|
|
||||||
| `theme/palette.ts`, `theme/color.ts`, `theme/mode.svelte.ts` | = | | (`mode` loses the flash with P2) |
|
|
||||||
| `tracks.ts` | ~ | P5 | remove the `TrackTarget` wrapper + 8 accessors |
|
|
||||||
| `recentlyPlayed.ts` | ~ | P5 | `toQueueItem` |
|
|
||||||
| `player/player.svelte.ts` | ~ | P5 | queue helpers, `toQueueItem`, rename `addToQueue` |
|
|
||||||
| `player/liked.svelte.ts` | ~ | P5 | `toQueueItem` |
|
|
||||||
| `player/queuePanel.svelte.ts`, `playlists.svelte.ts` | → `toggle.svelte.ts` | P5 | identical |
|
|
||||||
| `format.ts` | ~ | P5/P9 | `plural`, `albumMeta`, `playlistMeta` |
|
|
||||||
| `navigation.svelte.ts` | ~ | P5 | `searchHref`, `appNavLinks` |
|
|
||||||
| `config.ts` | ~ | P6 | `MENU_WIDTH`, `SUBMENU_WIDTH` |
|
|
||||||
| `mixes.ts`, `albums.ts`, `collections.ts`, `types.ts` | = | | |
|
|
||||||
| `lib/server/**`, `+page.server.ts`, `routes/api/**`, `auth/**`, `hooks.server.ts` | out of scope | | no UI or styles |
|
|
||||||
@@ -36,12 +36,6 @@ export default ts.config(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: ['build/', '.svelte-kit/', 'static/', 'src/lib/api/schema.d.ts']
|
||||||
'build/',
|
|
||||||
'.svelte-kit/',
|
|
||||||
'static/',
|
|
||||||
'src/lib/api/schema.d.ts',
|
|
||||||
'design_handoff_musify_player/'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ for (const file of files) {
|
|||||||
if (violations.length > 0) {
|
if (violations.length > 0) {
|
||||||
console.error(`\n${violations.length} violación(es) del sistema de diseño:\n`);
|
console.error(`\n${violations.length} violación(es) del sistema de diseño:\n`);
|
||||||
for (const v of violations) console.error(` ${v}`);
|
for (const v of violations) console.error(` ${v}`);
|
||||||
console.error('\nVer docs/frontend-refactor-plan.md, sección B.4.\n');
|
console.error('\nVer la lista blanca (ARBITRARY_WHITELIST) en este script, o CLAUDE.md.\n');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user