Simplify design
This commit is contained in:
@@ -9,3 +9,5 @@ package
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
src/lib/api/schema.d.ts
|
src/lib/api/schema.d.ts
|
||||||
static
|
static
|
||||||
|
.agents
|
||||||
|
skills-lock.json
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
|
"endOfLine": "auto",
|
||||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
"tailwindStylesheet": "./src/routes/layout.css",
|
"tailwindStylesheet": "./src/routes/layout.css",
|
||||||
"overrides": [
|
"overrides": [
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ The design system lives in `src/routes/layout.css` (`@theme` tokens plus `@utili
|
|||||||
- 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.
|
||||||
|
|
||||||
|
## Motion
|
||||||
|
|
||||||
|
Every animation follows one scale of three durations and one easing curve, all defined in `theme.css`. Fast (`--mf-motion-fast`, 150ms) is for hover, focus and press feedback: color, opacity and small scale changes. Plain `transition` or `transition-colors` already uses it because it is the Tailwind default, so never write `duration-150`. Base (`--mf-motion-base`, 200ms) is for things that appear on top of the page or move inside a control: modals, menus, the queue panel, `animate-pop`, `animate-fade` and the segmented control thumb. Slow (`--mf-motion-slow`, 300ms) is for content entering the page and larger movement: `animate-enter`, cover zoom on hover and the player dock padding.
|
||||||
|
|
||||||
|
Entrances and movement always use `--mf-ease` (`ease-snappy` in Tailwind, `expoOut` in Svelte transitions), which feels immediate because most of the motion happens at the start. Color transitions keep the Tailwind default curve. Overlays must feel instant: the modal backdrop uses `transition:fade={{ duration: 200 }}` and the panel uses a local `pop` transition (opacity + scale + translateY, `expoOut`, 200ms) mirroring `animate-pop`'s keyframes — both directives so the modal fades and pops on close too, not just on open, and the whole thing settles in 200ms. Staggered lists use `--i` with 40ms steps capped at ten items.
|
||||||
|
|
||||||
|
The only exceptions are looping indicators (equalizer bars, pulse, spin, sheen) and the accent hue crossfade in `+layout.svelte` (600ms), which is ambient and meant to be slow. Do not add new durations or curves; if something needs a different feel, pick the closest step of the scale. `prefers-reduced-motion` disables everything globally in `layout.css`.
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
`hooks.server.ts` validates the encrypted session cookie (`mf_session`), refreshes the `access_token`, and fills `locals.user` and `locals.accessToken`. To protect a route, check `locals.user` in its `load` and redirect to `/auth?returnTo=…`. When talking to the backend, send `locals.accessToken` as a Bearer token to `API_BASE_URL`.
|
`hooks.server.ts` validates the encrypted session cookie (`mf_session`), refreshes the `access_token`, and fills `locals.user` and `locals.accessToken`. To protect a route, check `locals.user` in its `load` and redirect to `/auth?returnTo=…`. When talking to the backend, send `locals.accessToken` as a Bearer token to `API_BASE_URL`.
|
||||||
|
|||||||
@@ -119,10 +119,10 @@ A fixed bottom bar, visible while something is playing. Shows track info, transp
|
|||||||
|
|
||||||
## Available backend (summary)
|
## Available backend (summary)
|
||||||
|
|
||||||
| Resource | Endpoints |
|
| Resource | Endpoints |
|
||||||
| --------- | --------------------------------------------------------------------- |
|
| --------- | ----------------------------------------------------------------- |
|
||||||
| Tracks | list/search, detail, by user, stream, create, delete, upload-urls |
|
| Tracks | list/search, detail, by user, stream, create, delete, upload-urls |
|
||||||
| Playlists | CRUD, playlist tracks, add/remove track, upload-picture |
|
| Playlists | CRUD, playlist tracks, add/remove track, upload-picture |
|
||||||
| Users | list/search, detail, create |
|
| Users | list/search, detail, create |
|
||||||
|
|
||||||
Typed client in `$lib/server/api.ts` (`openapi-fetch`, ErrorOr style). Use it inside `load`/server actions, never from the client. Types live in `src/lib/api/schema.d.ts`.
|
Typed client in `$lib/server/api.ts` (`openapi-fetch`, ErrorOr style). Use it inside `load`/server actions, never from the client. Types live in `src/lib/api/schema.d.ts`.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import type { SessionUser } from '$lib/types';
|
import type { SessionUser } from '$lib/types';
|
||||||
import ListRow from '../ui/media/ListRow.svelte';
|
import ListRow from '../ui/media/ListRow.svelte';
|
||||||
import IconButton from '../ui/primitives/IconButton.svelte';
|
import IconButton from '../ui/primitives/IconButton.svelte';
|
||||||
import Logo from '../ui/primitives/Logo.svelte';
|
|
||||||
import Plus from '@lucide/svelte/icons/plus';
|
import Plus from '@lucide/svelte/icons/plus';
|
||||||
|
|
||||||
interface SidebarPlaylist {
|
interface SidebarPlaylist {
|
||||||
@@ -40,14 +39,14 @@
|
|||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
class="flex items-center gap-3 rounded-control px-3 py-2 text-sm transition-colors duration-150 {active
|
class="flex items-center gap-3 rounded-control px-3 py-2 text-sm transition-colors {active
|
||||||
? 'bg-surface-2 text-fg'
|
? 'bg-surface-2 text-fg'
|
||||||
: 'text-fg-2 hover:bg-hover hover:text-fg'}"
|
: 'text-fg-2 hover:bg-hover hover:text-fg'}"
|
||||||
>
|
>
|
||||||
<link.icon class="size-icon-md shrink-0" strokeWidth={1.5} />
|
<link.icon class="size-icon-md shrink-0" strokeWidth={1.5} />
|
||||||
<span class="flex-1 truncate">{link.label}</span>
|
<span class="flex-1 truncate">{link.label}</span>
|
||||||
{#if link.count}
|
{#if link.count}
|
||||||
<span class="text-xs text-muted tabular-nums">{link.count}</span>
|
<span class="text-xs text-fg-2 tabular-nums">{link.count}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
{#if user}
|
{#if user}
|
||||||
<div class="mx-3 mt-6 mb-3.5 h-px bg-line"></div>
|
<div class="mx-3 mt-6 mb-3.5 h-px bg-line"></div>
|
||||||
<div class="flex items-center justify-between px-3 pb-1.5">
|
<div class="flex items-center justify-between px-3 pb-1.5">
|
||||||
<span class="text-eyebrow text-muted">Tus playlists</span>
|
<span class="text-eyebrow text-fg-2">Tus playlists</span>
|
||||||
{#if playlists.length > 0}
|
{#if playlists.length > 0}
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Crear playlist"
|
label="Crear playlist"
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
size="xs"
|
size="xs"
|
||||||
onclick={() => createPlaylistModal.show()}
|
onclick={() => createPlaylistModal.show()}
|
||||||
>
|
>
|
||||||
<Plus class="size-icon-sm text-muted" strokeWidth={1.25} />
|
<Plus class="size-icon-sm text-fg-2" strokeWidth={1.25} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +86,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => createPlaylistModal.show()}
|
onclick={() => createPlaylistModal.show()}
|
||||||
class="flex items-center gap-2 rounded-control px-3 py-2 text-xs text-muted transition-colors hover:bg-hover hover:text-fg-2"
|
class="flex items-center gap-2 rounded-control px-3 py-2 text-xs text-fg-2 transition-colors hover:bg-hover hover:text-fg-2"
|
||||||
>
|
>
|
||||||
<Plus class="size-3.5" strokeWidth={1.8} />
|
<Plus class="size-3.5" strokeWidth={1.8} />
|
||||||
Crear tu primera lista
|
Crear tu primera lista
|
||||||
|
|||||||
@@ -69,21 +69,21 @@
|
|||||||
<label
|
<label
|
||||||
class="flex h-9.5 w-full items-center gap-2.5 rounded-control bg-surface-2 px-3.5 transition focus-within:bg-surface-hover"
|
class="flex h-9.5 w-full items-center gap-2.5 rounded-control bg-surface-2 px-3.5 transition focus-within:bg-surface-hover"
|
||||||
>
|
>
|
||||||
<Search class="size-4 shrink-0 text-fg-3" strokeWidth={1.8} />
|
<Search class="size-4 shrink-0 text-fg-2" strokeWidth={1.8} />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:this={searchInput}
|
bind:this={searchInput}
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
oninput={onInput}
|
oninput={onInput}
|
||||||
placeholder="Canciones, artistas, álbumes o playlists"
|
placeholder="Canciones, artistas, álbumes o playlists"
|
||||||
class="w-full min-w-0 bg-transparent text-sm text-fg placeholder:text-muted focus:outline-none"
|
class="w-full min-w-0 bg-transparent text-sm text-fg placeholder:text-fg-2 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
{#if query}
|
{#if query}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={clearQuery}
|
onclick={clearQuery}
|
||||||
aria-label="Borrar búsqueda"
|
aria-label="Borrar búsqueda"
|
||||||
class="grid size-5 shrink-0 place-items-center rounded-full text-muted transition hover:text-fg"
|
class="grid size-5 shrink-0 place-items-center rounded-full text-fg-2 transition hover:text-fg"
|
||||||
>
|
>
|
||||||
<X class="size-3.5" />
|
<X class="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="flex min-w-0 flex-1 flex-col items-center gap-2">
|
<div class="flex min-w-0 flex-1 flex-col items-center gap-2">
|
||||||
<TransportControls />
|
<TransportControls />
|
||||||
<div class="flex w-full max-w-hero items-center gap-3">
|
<div class="flex w-full max-w-hero items-center gap-3">
|
||||||
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
|
<span class="min-w-8 text-right text-xs text-fg-2 tabular-nums">
|
||||||
{fmtTime(player.progress)}
|
{fmtTime(player.progress)}
|
||||||
</span>
|
</span>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
oninput={seek}
|
oninput={seek}
|
||||||
variant="seek"
|
variant="seek"
|
||||||
/>
|
/>
|
||||||
<span class="min-w-8 text-xs text-fg-3 tabular-nums">
|
<span class="min-w-8 text-xs text-fg-2 tabular-nums">
|
||||||
{fmtTime(player.current.duration)}
|
{fmtTime(player.current.duration)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
|
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
|
||||||
<Volume2 class="size-icon-sm shrink-0 text-fg-3" strokeWidth={1.8} />
|
<Volume2 class="size-icon-sm shrink-0 text-fg-2" strokeWidth={1.8} />
|
||||||
<Slider
|
<Slider
|
||||||
value={player.volume}
|
value={player.volume}
|
||||||
max={100}
|
max={100}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PlayerBar from './PlayerBar.svelte';
|
import PlayerBar from './PlayerBar.svelte';
|
||||||
import TabsBar from '../layout/TabsBar.svelte';
|
import TabsBar from '../layout/TabsBar.svelte';
|
||||||
import Alert from '$lib/components/ui/primitives/Alert.svelte';
|
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import type { SessionUser } from '$lib/types';
|
import type { SessionUser } from '$lib/types';
|
||||||
|
|
||||||
@@ -17,11 +16,6 @@
|
|||||||
<div
|
<div
|
||||||
class="pointer-events-none fixed inset-x-0 bottom-0 z-(--z-sticky) flex flex-col lg:left-(--mf-sidebar-w)"
|
class="pointer-events-none fixed inset-x-0 bottom-0 z-(--z-sticky) flex flex-col lg:left-(--mf-sidebar-w)"
|
||||||
>
|
>
|
||||||
{#if player.error}
|
|
||||||
<div class="pointer-events-auto px-4 pb-2.5">
|
|
||||||
<Alert tone="danger">{player.error}</Alert>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if hasTrack}
|
{#if hasTrack}
|
||||||
<PlayerBar />
|
<PlayerBar />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { fmtTime } from '$lib/utils/format';
|
import { fmtTime } from '$lib/utils/format';
|
||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { expoOut } from 'svelte/easing';
|
||||||
|
|
||||||
const upcoming = $derived.by(() => {
|
const upcoming = $derived.by(() => {
|
||||||
const idx = player.tracks.findIndex((t) => t.id === player.currentId);
|
const idx = player.tracks.findIndex((t) => t.id === player.currentId);
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
{#if queuePanel.open}
|
{#if queuePanel.open}
|
||||||
<aside
|
<aside
|
||||||
transition:fly={{ x: 24, duration: 220, easing: cubicOut }}
|
transition:fly={{ x: 24, duration: 200, easing: expoOut }}
|
||||||
class="hidden shrink-0 flex-col border-l border-hairline px-4 pt-5 sm:flex"
|
class="hidden shrink-0 flex-col border-l border-hairline px-4 pt-5 sm:flex"
|
||||||
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
|
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
|
||||||
>
|
>
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if player.current.id}
|
{#if player.current.id}
|
||||||
<p class="mb-3 text-eyebrow text-fg-3">Reproduciendo</p>
|
<p class="mb-3 text-eyebrow text-fg-2">Reproduciendo</p>
|
||||||
<div class="mb-6 rounded-control bg-accent-tint p-2.5">
|
<div class="mb-6 rounded-control surface-active p-2.5">
|
||||||
<MediaIdentity
|
<MediaIdentity
|
||||||
trackId={player.current.id}
|
trackId={player.current.id}
|
||||||
title={player.current.title}
|
title={player.current.title}
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mb-3 flex items-center justify-between gap-3">
|
<div class="mb-3 flex items-center justify-between gap-3">
|
||||||
<span class="min-w-0 flex-1 truncate text-eyebrow text-fg-3"> A continuación </span>
|
<span class="min-w-0 flex-1 truncate text-eyebrow text-fg-2"> A continuación </span>
|
||||||
{#if upcoming.length > 0}
|
{#if upcoming.length > 0}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => player.clearUpcoming()}
|
onclick={() => player.clearUpcoming()}
|
||||||
class="shrink-0 text-xs text-fg-3 transition-colors hover:text-fg"
|
class="shrink-0 text-xs text-fg-2 transition-colors hover:text-fg"
|
||||||
>
|
>
|
||||||
Vaciar
|
Vaciar
|
||||||
</button>
|
</button>
|
||||||
@@ -68,13 +68,12 @@
|
|||||||
>
|
>
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
{#if track.duration}
|
{#if track.duration}
|
||||||
<span class="shrink-0 text-xs text-muted tabular-nums">{fmtTime(track.duration)}</span
|
<span class="shrink-0 text-xs text-fg-2 tabular-nums">{fmtTime(track.duration)}</span>
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ListRow>
|
</ListRow>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="px-2 py-6 text-center text-xs text-fg-3">No hay más canciones en la cola.</p>
|
<p class="px-2 py-6 text-center text-xs text-fg-2">No hay más canciones en la cola.</p>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<ArtistLink
|
<ArtistLink
|
||||||
name={player.current.artist || '—'}
|
name={player.current.artist || '—'}
|
||||||
ownerUserId={player.current.ownerUserId}
|
ownerUserId={player.current.ownerUserId}
|
||||||
class="mt-0.5 text-xs text-fg-3"
|
class="mt-0.5 text-xs text-fg-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if !compact}
|
{#if !compact}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
const sideClass = $derived(
|
const sideClass = $derived(
|
||||||
compact
|
compact
|
||||||
? 'grid size-10 place-items-center text-fg opacity-70 transition active:scale-90'
|
? 'grid size-10 place-items-center text-fg opacity-70 transition active:scale-90'
|
||||||
: 'flex text-fg-2 transition-colors hover:text-fg active:scale-90'
|
: 'flex text-fg-2 transition hover:text-fg active:scale-90'
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
onclick={() => player.toggleShuffle()}
|
onclick={() => player.toggleShuffle()}
|
||||||
aria-pressed={player.shuffle}
|
aria-pressed={player.shuffle}
|
||||||
aria-label="Aleatorio"
|
aria-label="Aleatorio"
|
||||||
class="transition-colors hover:text-fg {player.shuffle ? 'text-accent' : 'text-fg-3'}"
|
class="transition-colors hover:text-fg {player.shuffle ? 'text-accent' : 'text-fg-2'}"
|
||||||
>
|
>
|
||||||
<Shuffle class="size-4" strokeWidth={1.8} />
|
<Shuffle class="size-4" strokeWidth={1.8} />
|
||||||
</button>
|
</button>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
onclick={() => player.toggleRepeat()}
|
onclick={() => player.toggleRepeat()}
|
||||||
aria-pressed={player.repeat}
|
aria-pressed={player.repeat}
|
||||||
aria-label="Repetir"
|
aria-label="Repetir"
|
||||||
class="transition-colors hover:text-fg {player.repeat ? 'text-accent' : 'text-fg-3'}"
|
class="transition-colors hover:text-fg {player.repeat ? 'text-accent' : 'text-fg-2'}"
|
||||||
>
|
>
|
||||||
<Repeat class="size-4" strokeWidth={1.8} />
|
<Repeat class="size-4" strokeWidth={1.8} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
{#if helperText}
|
{#if helperText}
|
||||||
<p class="text-sm text-muted">{helperText}</p>
|
<p class="text-sm text-fg-2">{helperText}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<span></span>
|
<span></span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -68,6 +68,6 @@
|
|||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon class="size-icon-xl {gradient ? 'text-on-art/70' : 'text-muted'}" strokeWidth={1.3} />
|
<Icon class="size-icon-xl {gradient ? 'text-on-art/70' : 'text-fg-2'}" strokeWidth={1.3} />
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<div class="flex items-center justify-between gap-4 rounded-art bg-surface px-4 py-3.5">
|
<div class="flex items-center justify-between gap-4 rounded-art bg-surface px-4 py-3.5">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-sm font-medium text-fg-2">Visibilidad</div>
|
<div class="text-sm font-medium text-fg-2">Visibilidad</div>
|
||||||
<div class="mt-0.5 text-xs text-muted">
|
<div class="mt-0.5 text-xs text-fg-2">
|
||||||
{visibility === 'private'
|
{visibility === 'private'
|
||||||
? 'Solo tú la ves en tu biblioteca'
|
? 'Solo tú la ves en tu biblioteca'
|
||||||
: 'Cualquiera con el enlace puede verla'}
|
: 'Cualquiera con el enlace puede verla'}
|
||||||
|
|||||||
@@ -30,21 +30,21 @@
|
|||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
{#if subtitle}
|
{#if subtitle}
|
||||||
<p class="mt-1 text-xs text-fg-3">{subtitle}</p>
|
<p class="mt-1 text-xs text-fg-2">{subtitle}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet autoActions()}
|
{#snippet autoActions()}
|
||||||
{#if count !== undefined}
|
{#if count !== undefined}
|
||||||
<div class="h-px flex-1 self-center bg-line"></div>
|
<div class="h-px flex-1 self-center bg-line"></div>
|
||||||
<span class="shrink-0 text-xs text-muted tabular-nums">
|
<span class="shrink-0 text-xs text-fg-2 tabular-nums">
|
||||||
{plural(count, 'resultado', 'resultados')}
|
{plural(count, 'resultado', 'resultados')}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if href}
|
{#if href}
|
||||||
<a
|
<a
|
||||||
{href}
|
{href}
|
||||||
class="shrink-0 text-xs font-medium tracking-wider text-fg-3 uppercase transition-colors hover:text-fg"
|
class="shrink-0 text-xs font-medium tracking-wider text-fg-2 uppercase transition-colors hover:text-fg"
|
||||||
>
|
>
|
||||||
{linkLabel}
|
{linkLabel}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
/>
|
/>
|
||||||
{:else if fallback === 'music'}
|
{:else if fallback === 'music'}
|
||||||
<div class="grid h-full w-full place-items-center bg-surface">
|
<div class="grid h-full w-full place-items-center bg-surface">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" class="h-2/5 w-2/5 text-fg-3">
|
<svg viewBox="0 0 24 24" fill="currentColor" class="h-2/5 w-2/5 text-fg-2">
|
||||||
<path
|
<path
|
||||||
d="M19.952 1.651a.75.75 0 0 1 .298.599V16.303a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.401-4.909l2.311-.66a1.5 1.5 0 0 0 1.088-1.442V6.994l-9 2.572v9.737a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.402-4.909l2.312-.66a1.5 1.5 0 0 0 1.088-1.442V5.25a.75.75 0 0 1 .544-.721l10.5-3a.75.75 0 0 1 .754.122Z"
|
d="M19.952 1.651a.75.75 0 0 1 .298.599V16.303a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.401-4.909l2.311-.66a1.5 1.5 0 0 0 1.088-1.442V6.994l-9 2.572v9.737a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.402-4.909l2.312-.66a1.5 1.5 0 0 0 1.088-1.442V5.25a.75.75 0 0 1 .544-.721l10.5-3a.75.75 0 0 1 .754.122Z"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="grid size-4 shrink-0 place-items-center rounded-tag bg-fg-3 text-xs leading-4 font-bold text-bg {klass}"
|
class="grid size-4 shrink-0 place-items-center rounded-tag bg-fg-2 text-xs leading-4 font-bold text-bg {klass}"
|
||||||
title="Contenido explícito"
|
title="Contenido explícito"
|
||||||
aria-label="Contenido explícito">E</span
|
aria-label="Contenido explícito">E</span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
const classes = $derived(
|
const classes = $derived(
|
||||||
`flex items-center gap-3 text-left transition-colors ${
|
`flex items-center gap-3 text-left transition-colors ${
|
||||||
variant === 'card' ? 'rounded-art p-2 pr-3.5' : 'rounded-control p-2'
|
variant === 'card' ? 'rounded-art p-2 pr-3.5' : 'rounded-control p-2'
|
||||||
} ${active ? 'bg-accent-tint' : 'hover:bg-hover'} ${klass}`
|
} ${active ? 'surface-active' : 'hover:bg-hover'} ${klass}`
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
{trackIds}
|
{trackIds}
|
||||||
size="fill"
|
size="fill"
|
||||||
alt={title}
|
alt={title}
|
||||||
class="rounded-art transition duration-300 group-hover:scale-105"
|
class="rounded-art transition duration-300 ease-snappy group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if onPlay}
|
{#if onPlay}
|
||||||
@@ -50,12 +50,12 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
onclick={onPlay}
|
onclick={onPlay}
|
||||||
label="Reproducir {title}"
|
label="Reproducir {title}"
|
||||||
class="absolute right-2.5 bottom-2.5 opacity-0 transition-opacity duration-150 group-hover:opacity-100"
|
class="absolute right-2.5 bottom-2.5 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2.5 truncate text-sm text-fg">{title}</div>
|
<div class="mt-2.5 truncate text-sm text-fg">{title}</div>
|
||||||
{#if subtitle}
|
{#if subtitle}
|
||||||
<div class="mt-0.5 truncate text-xs text-fg-3">{subtitle}</div>
|
<div class="mt-0.5 truncate text-xs text-fg-2">{subtitle}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -63,6 +63,6 @@
|
|||||||
>
|
>
|
||||||
{@render badge?.()}
|
{@render badge?.()}
|
||||||
</div>
|
</div>
|
||||||
<ArtistLink name={artist} {ownerUserId} class="mt-0.5 text-xs text-fg-3" />
|
<ArtistLink name={artist} {ownerUserId} class="mt-0.5 text-xs text-fg-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const VARIANT: Record<'strong' | 'glass', string> = {
|
const VARIANT: Record<'strong' | 'glass', string> = {
|
||||||
strong: 'bg-cta-strong text-ink hover:brightness-95',
|
strong: 'bg-cta-strong text-ink hover:bg-cta-strong-hover',
|
||||||
glass: 'border border-on-art/12 bg-ink/76 text-fg backdrop-blur'
|
glass: 'border border-on-art/12 bg-ink/76 text-fg backdrop-blur'
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = $derived(
|
const classes = $derived(
|
||||||
`grid shrink-0 place-items-center rounded-full transition duration-150 ${SIZE[size]} ${VARIANT[variant]} ${klass}`
|
`grid shrink-0 place-items-center rounded-full transition ${SIZE[size]} ${VARIANT[variant]} ${klass}`
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
<div class="overflow-x-auto {klass}">
|
<div class="overflow-x-auto {klass}">
|
||||||
<div
|
<div
|
||||||
class="grid track-grid items-center gap-3 border-b border-line pr-4 pb-3 pl-2 text-sm text-fg-3 sm:gap-8 sm:pr-6 sm:pl-3"
|
class="grid track-grid items-center gap-3 border-b border-line pr-4 pb-3 pl-2 text-sm text-fg-2 sm:gap-8 sm:pr-6 sm:pl-3"
|
||||||
style="--mf-track-cols:{gridColumns}; --mf-track-cols-mobile:{gridColumnsMobile}"
|
style="--mf-track-cols:{gridColumns}; --mf-track-cols-mobile:{gridColumnsMobile}"
|
||||||
>
|
>
|
||||||
{#if index}
|
{#if index}
|
||||||
@@ -106,13 +106,14 @@
|
|||||||
<span></span>
|
<span></span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 flex flex-col gap-0.5">
|
<div class="mt-1 flex flex-col gap-0.5" role="list">
|
||||||
{#each tracks as track, i (track.id)}
|
{#each tracks as track, i (track.id)}
|
||||||
{@const active = player.current.id === track.id}
|
{@const active = player.current.id === track.id}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
role="listitem"
|
||||||
class="group grid track-grid items-center gap-3 rounded-control py-3 pr-4 pl-2 transition-colors sm:gap-8 sm:pr-6 sm:pl-3 {active
|
class="group grid track-grid items-center gap-3 rounded-control py-3 pr-4 pl-2 transition-colors sm:gap-8 sm:pr-6 sm:pl-3 {active
|
||||||
? 'bg-accent-tint'
|
? 'surface-active'
|
||||||
: 'hover:bg-hover'}"
|
: 'hover:bg-hover'}"
|
||||||
style="--mf-track-cols:{gridColumns}; --mf-track-cols-mobile:{gridColumnsMobile}"
|
style="--mf-track-cols:{gridColumns}; --mf-track-cols-mobile:{gridColumnsMobile}"
|
||||||
oncontextmenu={oncontextmenu ? (event) => oncontextmenu(event, track, i) : undefined}
|
oncontextmenu={oncontextmenu ? (event) => oncontextmenu(event, track, i) : undefined}
|
||||||
@@ -122,7 +123,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
onclick={() => onPlay(i)}
|
onclick={() => onPlay(i)}
|
||||||
aria-label={active && player.playing ? 'Pausar' : 'Reproducir'}
|
aria-label={active && player.playing ? 'Pausar' : 'Reproducir'}
|
||||||
class="relative grid size-8 place-items-center rounded-tag text-xs text-muted tabular-nums"
|
class="relative grid size-8 place-items-center rounded-tag text-xs text-fg-2 tabular-nums"
|
||||||
>
|
>
|
||||||
{#if active}
|
{#if active}
|
||||||
<EqBars paused={!player.playing} />
|
<EqBars paused={!player.playing} />
|
||||||
@@ -138,6 +139,7 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
use:pressable={() => onPlay(i)}
|
use:pressable={() => onPlay(i)}
|
||||||
onclick={(event) => event.stopPropagation()}
|
onclick={(event) => event.stopPropagation()}
|
||||||
|
onkeydown={(event) => event.stopPropagation()}
|
||||||
class="flex min-w-0 flex-1 text-left"
|
class="flex min-w-0 flex-1 text-left"
|
||||||
>
|
>
|
||||||
<MediaIdentity
|
<MediaIdentity
|
||||||
@@ -156,7 +158,7 @@
|
|||||||
</MediaIdentity>
|
</MediaIdentity>
|
||||||
</div>
|
</div>
|
||||||
{#each columns as column (column)}
|
{#each columns as column (column)}
|
||||||
<span class="hidden truncate text-center text-xs text-muted tabular-nums sm:block">
|
<span class="hidden truncate text-center text-xs text-fg-2 tabular-nums sm:block">
|
||||||
{#if column === 'plays'}
|
{#if column === 'plays'}
|
||||||
{Number(track.listensCount ?? 0)}
|
{Number(track.listensCount ?? 0)}
|
||||||
{:else if column === 'added' && track.addedAt}
|
{:else if column === 'added' && track.addedAt}
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
<span class="truncate text-center text-xs text-muted tabular-nums">
|
<span class="truncate text-center text-xs text-fg-2 tabular-nums">
|
||||||
{fmtTime(Number(track.duration))}
|
{fmtTime(Number(track.duration))}
|
||||||
</span>
|
</span>
|
||||||
{#if rowAction}
|
{#if rowAction}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Modal from './Modal.svelte';
|
import Dialog from './Dialog.svelte';
|
||||||
import Button from '../primitives/Button.svelte';
|
import Button from '../primitives/Button.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,13 +14,11 @@
|
|||||||
let { open, onClose, title, description, confirmLabel, action }: Props = $props();
|
let { open, onClose, title, description, confirmLabel, action }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {open} {onClose} maxWidth="max-w-sm">
|
<Dialog {open} {onClose} {title} {description}>
|
||||||
<h2 class="text-display-3 text-fg">{title}</h2>
|
{#snippet actions()}
|
||||||
<p class="mt-2.5 text-body text-fg-2">{description}</p>
|
|
||||||
<div class="mt-6 flex items-center justify-end gap-3">
|
|
||||||
<Button variant="secondary" onclick={onClose}>Cancelar</Button>
|
<Button variant="secondary" onclick={onClose}>Cancelar</Button>
|
||||||
<form method="POST" {action}>
|
<form method="POST" {action}>
|
||||||
<Button type="submit" variant="danger">{confirmLabel}</Button>
|
<Button type="submit" variant="danger">{confirmLabel}</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
{/snippet}
|
||||||
</Modal>
|
</Dialog>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-sm font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
|
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-sm font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
|
||||||
>
|
>
|
||||||
<span class="flex-1 cursor-default">{playlistAction.label}</span>
|
<span class="flex-1 cursor-default">{playlistAction.label}</span>
|
||||||
<ChevronRight class="size-icon-sm shrink-0 text-fg-3" />
|
<ChevronRight class="size-icon-sm shrink-0 text-fg-2" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="invisible absolute top-0 z-(--z-submenu) opacity-0 transition group-hover/addmenu:visible group-hover/addmenu:opacity-100 max-sm:top-full max-sm:right-0 max-sm:left-0 max-sm:px-0 max-sm:pt-1.5 {openLeft
|
class="invisible absolute top-0 z-(--z-submenu) opacity-0 transition group-hover/addmenu:visible group-hover/addmenu:opacity-100 max-sm:top-full max-sm:right-0 max-sm:left-0 max-sm:px-0 max-sm:pt-1.5 {openLeft
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
>
|
>
|
||||||
<div class="max-h-80 overflow-y-auto">
|
<div class="max-h-80 overflow-y-auto">
|
||||||
{#if playlists.length === 0}
|
{#if playlists.length === 0}
|
||||||
<p class="px-2 py-2.5 text-sm text-fg-3">Todavía no tienes playlists.</p>
|
<p class="px-2 py-2.5 text-sm text-fg-2">Todavía no tienes playlists.</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#each playlists as playlist (playlist.id)}
|
{#each playlists as playlist (playlist.id)}
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import CircleAlert from '@lucide/svelte/icons/circle-alert';
|
||||||
|
import Modal from './Modal.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
tone?: 'neutral' | 'danger';
|
||||||
|
children?: Snippet;
|
||||||
|
actions?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open, onClose, title, description, tone = 'neutral', children, actions }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
{open}
|
||||||
|
{onClose}
|
||||||
|
maxWidth="max-w-sm"
|
||||||
|
panelClass={tone === 'danger' ? 'glass-panel-danger' : ''}
|
||||||
|
>
|
||||||
|
{#if tone === 'danger'}
|
||||||
|
<span class="mb-4 grid size-10 place-items-center rounded-full bg-danger/12 text-danger-fg">
|
||||||
|
<CircleAlert class="size-icon-md" strokeWidth={2} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<h2 class="text-display-3 text-fg">{title}</h2>
|
||||||
|
{#if description}
|
||||||
|
<p class="mt-2.5 text-body text-fg-2">{description}</p>
|
||||||
|
{/if}
|
||||||
|
{@render children?.()}
|
||||||
|
{#if actions}
|
||||||
|
<div class="mt-6 flex flex-wrap items-center justify-end gap-3">
|
||||||
|
{@render actions()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Dialog from './Dialog.svelte';
|
||||||
|
import Button from '../primitives/Button.svelte';
|
||||||
|
import { dialog, type DialogAction, type DialogOptions } from '$lib/state/dialog.svelte';
|
||||||
|
|
||||||
|
// Keep the last dialog's content so the exit transition doesn't render an empty panel.
|
||||||
|
let last = $state<DialogOptions | null>(null);
|
||||||
|
$effect.pre(() => {
|
||||||
|
if (dialog.current) last = dialog.current;
|
||||||
|
});
|
||||||
|
const current = $derived(dialog.current ?? last);
|
||||||
|
const buttons: DialogAction[] = $derived(
|
||||||
|
current?.actions?.length ? current.actions : [{ label: 'Entendido' }]
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={dialog.current !== null}
|
||||||
|
onClose={() => dialog.close()}
|
||||||
|
title={current?.title ?? ''}
|
||||||
|
description={current?.description}
|
||||||
|
tone={current?.tone}
|
||||||
|
>
|
||||||
|
{#snippet actions()}
|
||||||
|
{#each buttons as action, i (action.label)}
|
||||||
|
<Button
|
||||||
|
variant={action.variant ?? (i === buttons.length - 1 ? 'primary' : 'secondary')}
|
||||||
|
onclick={() => dialog.run(action)}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
{/snippet}
|
||||||
|
</Dialog>
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="glass-panel animate-pop rounded-panel p-1.5 {border ? 'border border-hairline' : ''} {klass}"
|
class="glass-panel animate-pop rounded-panel p-1.5 {border
|
||||||
|
? 'border border-hairline'
|
||||||
|
: ''} {klass}"
|
||||||
{style}
|
{style}
|
||||||
>
|
>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import IconButton from '../primitives/IconButton.svelte';
|
import IconButton from '../primitives/IconButton.svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { expoOut } from 'svelte/easing';
|
||||||
|
import type { TransitionConfig } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -45,6 +48,15 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pop(node: Element, { duration = 200 }: { duration?: number } = {}): TransitionConfig {
|
||||||
|
return {
|
||||||
|
duration,
|
||||||
|
easing: expoOut,
|
||||||
|
css: (t) =>
|
||||||
|
`opacity: ${t}; transform: scale(${0.98 + 0.02 * t}) translateY(${0.25 * (1 - t)}rem);`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function onWindowKeydown(event: KeyboardEvent) {
|
function onWindowKeydown(event: KeyboardEvent) {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
@@ -70,13 +82,15 @@
|
|||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<div
|
||||||
class="animate-fade fixed inset-0 z-(--z-modal) grid place-items-center bg-scrim/70 p-4 backdrop-blur-sm"
|
transition:fade={{ duration: 200 }}
|
||||||
|
class="fixed inset-0 z-(--z-modal) grid place-items-center bg-scrim/70 p-4 backdrop-blur-sm"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
onclick={onBackdropClick}
|
onclick={onBackdropClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
bind:this={panel}
|
bind:this={panel}
|
||||||
class="glass-panel animate-pop max-h-[85dvh] w-full overflow-y-auto {maxWidth} rounded-panel p-5 outline-none sm:p-6 {panelClass}"
|
transition:pop={{ duration: 200 }}
|
||||||
|
class="glass-panel max-h-[85dvh] w-full overflow-y-auto {maxWidth} rounded-panel p-5 outline-none sm:p-6 {panelClass}"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
@@ -86,7 +100,7 @@
|
|||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
{#if eyebrow}
|
{#if eyebrow}
|
||||||
<p class="mb-2.5 text-eyebrow text-muted">
|
<p class="mb-2.5 text-eyebrow text-fg-2">
|
||||||
{eyebrow}
|
{eyebrow}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<a
|
<a
|
||||||
href={target.url}
|
href={target.url}
|
||||||
{onclick}
|
{onclick}
|
||||||
class="inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
|
class="inline-flex items-center text-fg-2 transition-colors hover:text-accent-soft"
|
||||||
>
|
>
|
||||||
<span class="grid size-8 place-items-center">
|
<span class="grid size-8 place-items-center">
|
||||||
<ArrowLeft class="size-icon-sm" />
|
<ArrowLeft class="size-icon-sm" />
|
||||||
|
|||||||
@@ -29,20 +29,20 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
sm: 'px-4 py-2.5 text-xs',
|
sm: 'rounded-control px-4 py-2 text-xs',
|
||||||
md: 'px-5 py-3 text-sm',
|
md: 'rounded-btn-sm px-4.5 py-2.5 text-control-sm',
|
||||||
lg: 'px-6 py-3.5 text-sm'
|
lg: 'rounded-btn px-6 py-3.5 text-sm'
|
||||||
};
|
};
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary: 'bg-cta-strong text-ink hover:brightness-95',
|
primary: 'bg-cta-strong text-ink hover:bg-cta-strong-hover',
|
||||||
secondary: 'bg-btn text-fg hover:bg-btn-hover',
|
secondary: 'bg-btn text-fg hover:bg-btn-hover',
|
||||||
accent: 'bg-accent-tint text-accent-soft hover:bg-accent-btn-hover',
|
accent: 'surface-active text-accent-soft hover:surface-active-hover',
|
||||||
danger: 'bg-danger text-on-art hover:brightness-110'
|
danger: 'bg-danger text-on-art hover:brightness-110'
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = $derived(
|
const classes = $derived(
|
||||||
`inline-flex items-center justify-center gap-2 rounded-control btn-transition focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none disabled:opacity-40 ${sizes[size]} ${variants[variant]} ${klass}`
|
`inline-flex items-center justify-center gap-2 font-medium btn-transition focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none disabled:opacity-40 ${sizes[size]} ${variants[variant]} ${klass}`
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,7 @@
|
|||||||
href
|
href
|
||||||
? 'rounded-full border border-line-strong px-3.5 py-2 text-xs text-fg-2 transition-colors hover:bg-hover hover:text-fg'
|
? 'rounded-full border border-line-strong px-3.5 py-2 text-xs text-fg-2 transition-colors hover:bg-hover hover:text-fg'
|
||||||
: `flex items-center gap-2 rounded-full px-3.5 py-2 text-xs font-medium transition ${
|
: `flex items-center gap-2 rounded-full px-3.5 py-2 text-xs font-medium transition ${
|
||||||
selected
|
selected ? 'bg-cta-strong text-ink' : 'bg-btn text-fg-2 hover:bg-btn-hover hover:text-fg'
|
||||||
? 'bg-cta-strong text-ink'
|
|
||||||
: 'bg-surface-2 text-fg-2 hover:bg-surface-hover hover:text-fg'
|
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
@@ -25,7 +23,7 @@
|
|||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{#if count !== undefined}
|
{#if count !== undefined}
|
||||||
<span class="tabular-nums {selected ? 'opacity-55' : 'text-muted'}">{count}</span>
|
<span class="tabular-nums {selected ? 'opacity-55' : 'text-fg-2'}">{count}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{#if hint}
|
{#if hint}
|
||||||
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-muted">
|
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-fg-2">
|
||||||
{@render hint()}
|
{@render hint()}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TEXT: Record<'muted' | 'subtle' | 'plain', string> = {
|
const TEXT: Record<'muted' | 'subtle' | 'plain', string> = {
|
||||||
muted: 'text-muted hover:text-fg',
|
muted: 'text-fg-2 hover:text-fg',
|
||||||
subtle: 'text-fg-2 hover:text-fg',
|
subtle: 'text-fg-2 hover:text-fg',
|
||||||
plain: 'text-fg-2 hover:text-fg'
|
plain: 'text-fg-2 hover:text-fg'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,6 @@
|
|||||||
|
|
||||||
<div bind:this={sentinel} class="flex min-h-12 items-center justify-center py-8">
|
<div bind:this={sentinel} class="flex min-h-12 items-center justify-center py-8">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<LoaderCircle class="size-5 animate-spin text-muted" />
|
<LoaderCircle class="size-5 animate-spin text-fg-2" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
{#if Icon}
|
{#if Icon}
|
||||||
<span class="pointer-events-none absolute inset-y-0 left-4 grid place-items-center text-muted">
|
<span class="pointer-events-none absolute inset-y-0 left-4 grid place-items-center text-fg-2">
|
||||||
<Icon class="size-icon-lg" />
|
<Icon class="size-icon-lg" />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<div class="relative flex shrink-0 rounded-full bg-surface-2 p-1">
|
<div class="relative flex shrink-0 rounded-full bg-surface-2 p-1">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-y-1 left-1 rounded-full bg-fg transition-transform duration-300 ease-out"
|
class="absolute inset-y-1 left-1 rounded-full bg-fg transition-transform duration-200 ease-snappy"
|
||||||
style="width:calc((100% - 0.5rem) / {options.length}); transform:translateX(calc({index} * 100%))"
|
style="width:calc((100% - 0.5rem) / {options.length}); transform:translateX(calc({index} * 100%))"
|
||||||
></div>
|
></div>
|
||||||
{#each options as option (option.value)}
|
{#each options as option (option.value)}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
class="relative z-(--z-raised) flex-1 px-4 py-1.5 text-center text-xs font-medium transition-colors {value ===
|
class="relative z-(--z-raised) flex-1 px-4 py-1.5 text-center text-xs font-medium transition-colors {value ===
|
||||||
option.value
|
option.value
|
||||||
? 'text-ink'
|
? 'text-ink'
|
||||||
: 'text-muted hover:text-fg-2'}"
|
: 'text-fg-2 hover:text-fg-2'}"
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { browser } from '$app/environment';
|
|||||||
import { DEFAULT_ACCENT } from '$lib/theme/color';
|
import { DEFAULT_ACCENT } from '$lib/theme/color';
|
||||||
import { extractAccent, type Accent } from '$lib/theme/palette';
|
import { extractAccent, type Accent } from '$lib/theme/palette';
|
||||||
import { shuffle } from '$lib/data/collections';
|
import { shuffle } from '$lib/data/collections';
|
||||||
|
import { dialog } from '$lib/state/dialog.svelte';
|
||||||
|
|
||||||
export interface PlayerTrack {
|
export interface PlayerTrack {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
@@ -119,7 +120,6 @@ class PlayerState {
|
|||||||
progress = $state(0);
|
progress = $state(0);
|
||||||
volume = $state(browser ? Number(localStorage.getItem('player.volume') ?? 100) : 100);
|
volume = $state(browser ? Number(localStorage.getItem('player.volume') ?? 100) : 100);
|
||||||
loading = $state(false);
|
loading = $state(false);
|
||||||
error = $state('');
|
|
||||||
recentlyPlayed = $state<PlayerTrack[]>([]);
|
recentlyPlayed = $state<PlayerTrack[]>([]);
|
||||||
playlists = $state<Playlist[]>([]);
|
playlists = $state<Playlist[]>([]);
|
||||||
shuffle = $state(false);
|
shuffle = $state(false);
|
||||||
@@ -173,7 +173,7 @@ class PlayerState {
|
|||||||
this.#stopProgressLoop();
|
this.#stopProgressLoop();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
this.error = 'No se pudo reproducir la pista.';
|
this.#fail();
|
||||||
});
|
});
|
||||||
this.#audio = audio;
|
this.#audio = audio;
|
||||||
this.#setupMediaSession(audio);
|
this.#setupMediaSession(audio);
|
||||||
@@ -268,7 +268,6 @@ class PlayerState {
|
|||||||
if (track) this.#syncMediaSessionMetadata(track);
|
if (track) this.#syncMediaSessionMetadata(track);
|
||||||
const token = ++this.#loadToken;
|
const token = ++this.#loadToken;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = '';
|
|
||||||
this.progress = 0;
|
this.progress = 0;
|
||||||
try {
|
try {
|
||||||
const src = await this.#resolveLocalSrc(String(id));
|
const src = await this.#resolveLocalSrc(String(id));
|
||||||
@@ -283,13 +282,23 @@ class PlayerState {
|
|||||||
if (token !== this.#loadToken) return;
|
if (token !== this.#loadToken) return;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
this.error =
|
if (exception instanceof DOMException && exception.name === 'AbortError') return;
|
||||||
exception instanceof Error && exception.message
|
this.#fail(
|
||||||
|
exception instanceof Error && !(exception instanceof DOMException)
|
||||||
? exception.message
|
? exception.message
|
||||||
: 'No se pudo reproducir la pista.';
|
: undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#fail(detail?: string) {
|
||||||
|
dialog.error(
|
||||||
|
'No se pudo reproducir la pista',
|
||||||
|
detail || 'El archivo no está disponible o su formato no es compatible.',
|
||||||
|
[{ label: 'Cerrar' }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async #resolveLocalSrc(id: string): Promise<string> {
|
async #resolveLocalSrc(id: string): Promise<string> {
|
||||||
const res = await fetch(`/api/tracks/${id}/stream`);
|
const res = await fetch(`/api/tracks/${id}/stream`);
|
||||||
if (!res.ok) throw new Error(await readErrorMessage(res));
|
if (!res.ok) throw new Error(await readErrorMessage(res));
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export interface DialogAction {
|
||||||
|
label: string;
|
||||||
|
variant?: 'primary' | 'secondary' | 'accent' | 'danger';
|
||||||
|
onclick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DialogOptions {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
tone?: 'neutral' | 'danger';
|
||||||
|
actions?: DialogAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class DialogStore {
|
||||||
|
current = $state<DialogOptions | null>(null);
|
||||||
|
|
||||||
|
show(options: DialogOptions) {
|
||||||
|
this.current = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
error(title: string, description?: string, actions?: DialogAction[]) {
|
||||||
|
this.show({ title, description, tone: 'danger', actions });
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
run(action: DialogAction) {
|
||||||
|
this.close();
|
||||||
|
action.onclick?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dialog = new DialogStore();
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { contextMenuPosition, type MenuPosition } from '$lib/components/ui/overlay/ContextMenu.svelte';
|
import {
|
||||||
|
contextMenuPosition,
|
||||||
|
type MenuPosition
|
||||||
|
} from '$lib/components/ui/overlay/ContextMenu.svelte';
|
||||||
import { player, toQueueItem, type ApiTrackLike } from '../player/player.svelte';
|
import { player, toQueueItem, type ApiTrackLike } from '../player/player.svelte';
|
||||||
|
|
||||||
export type TrackMenuState = { track: ApiTrackLike } & MenuPosition;
|
export type TrackMenuState = { track: ApiTrackLike } & MenuPosition;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const ACCENT_LIGHTNESS = 74;
|
export const ACCENT_LIGHTNESS = 74;
|
||||||
export const ACCENT_CHROMA = 0.145;
|
export const ACCENT_CHROMA = 0.06;
|
||||||
export const ACCENT_HUE = 145;
|
export const ACCENT_HUE = 30;
|
||||||
|
|
||||||
export const DEFAULT_ACCENT = `oklch(${ACCENT_LIGHTNESS}% ${ACCENT_CHROMA} ${ACCENT_HUE})`;
|
export const DEFAULT_ACCENT = `oklch(${ACCENT_LIGHTNESS}% ${ACCENT_CHROMA} ${ACCENT_HUE})`;
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
--mf-tile-shade-c: 0.035;
|
--mf-tile-shade-c: 0.035;
|
||||||
|
|
||||||
--mf-text: #d9d8dc;
|
--mf-text: #d9d8dc;
|
||||||
--mf-text-2: #8a8a8e;
|
--mf-text-2: #b1b0b5;
|
||||||
--mf-text-3: #7d7d81;
|
|
||||||
--mf-text-4: #646468;
|
|
||||||
|
|
||||||
--mf-surface: rgba(255, 255, 255, 0.022);
|
--mf-surface: rgba(255, 255, 255, 0.022);
|
||||||
--mf-surface-2: rgba(255, 255, 255, 0.045);
|
--mf-surface-2: rgba(255, 255, 255, 0.055);
|
||||||
--mf-surface-hover: rgba(255, 255, 255, 0.06);
|
--mf-surface-hover: rgba(255, 255, 255, 0.06);
|
||||||
--mf-btn-bg-hover: var(--mf-surface-hover);
|
--mf-row-hover: rgba(255, 255, 255, 0.035);
|
||||||
|
--mf-btn-bg: rgba(255, 255, 255, 0.055);
|
||||||
|
--mf-btn-bg-hover: rgba(255, 255, 255, 0.09);
|
||||||
|
|
||||||
--mf-border: rgba(255, 255, 255, 0.04);
|
--mf-border: rgba(255, 255, 255, 0.04);
|
||||||
--mf-border-strong: rgba(255, 255, 255, 0.09);
|
--mf-border-strong: rgba(255, 255, 255, 0.09);
|
||||||
@@ -24,7 +24,14 @@
|
|||||||
--mf-on-art: #fefdfb;
|
--mf-on-art: #fefdfb;
|
||||||
--mf-on-art-2: #cac8c3;
|
--mf-on-art-2: #cac8c3;
|
||||||
--mf-ink: #0a0a0d;
|
--mf-ink: #0a0a0d;
|
||||||
--mf-cta-strong-bg: #c4c4c8;
|
--mf-cta-strong-bg: #c6c4cb;
|
||||||
|
--mf-cta-strong-hover: #d8d7dc;
|
||||||
|
--mf-danger-fg: oklch(0.885 0.062 18.334);
|
||||||
|
|
||||||
|
--mf-motion-fast: 150ms;
|
||||||
|
--mf-motion-base: 200ms;
|
||||||
|
--mf-motion-slow: 300ms;
|
||||||
|
--mf-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|
||||||
--mf-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
--mf-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||||
|
|
||||||
@@ -71,15 +78,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme='light'] {
|
:root[data-theme='light'] {
|
||||||
--mf-text: #17171a;
|
--mf-text: #17161c;
|
||||||
--mf-text-2: #4a4a4e;
|
--mf-text-2: #5d5c66;
|
||||||
--mf-text-3: #6b6b6f;
|
|
||||||
--mf-text-4: #8c8c90;
|
|
||||||
|
|
||||||
--mf-surface: rgba(0, 0, 0, 0.035);
|
--mf-surface: rgba(0, 0, 0, 0.035);
|
||||||
--mf-surface-2: rgba(0, 0, 0, 0.06);
|
--mf-surface-2: rgba(0, 0, 0, 0.06);
|
||||||
--mf-surface-hover: #e8eceb;
|
--mf-surface-hover: rgba(26, 24, 38, 0.06);
|
||||||
--mf-btn-bg-hover: var(--mf-surface-hover);
|
--mf-row-hover: rgba(26, 24, 38, 0.04);
|
||||||
|
--mf-btn-bg: rgba(255, 255, 255, 0.7);
|
||||||
|
--mf-btn-bg-hover: #ffffff;
|
||||||
|
|
||||||
--mf-scrollbar: rgba(0, 0, 0, 0.18);
|
--mf-scrollbar: rgba(0, 0, 0, 0.18);
|
||||||
--mf-scrollbar-hover: rgba(0, 0, 0, 0.32);
|
--mf-scrollbar-hover: rgba(0, 0, 0, 0.32);
|
||||||
@@ -89,8 +96,10 @@
|
|||||||
--mf-track: rgba(0, 0, 0, 0.12);
|
--mf-track: rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
--mf-scrim: #ffffff;
|
--mf-scrim: #ffffff;
|
||||||
--mf-ink: #ffffff;
|
--mf-ink: #f7f6f9;
|
||||||
--mf-cta-strong-bg: #17171a;
|
--mf-cta-strong-bg: #1b1a20;
|
||||||
|
--mf-cta-strong-hover: #2e2d36;
|
||||||
|
--mf-danger-fg: oklch(0.5 0.16 25);
|
||||||
|
|
||||||
--mf-tile-l: 0.58;
|
--mf-tile-l: 0.58;
|
||||||
--mf-tile-c: 0.085;
|
--mf-tile-c: 0.085;
|
||||||
|
|||||||
@@ -2,70 +2,71 @@ function r(value: number, digits = 4): string {
|
|||||||
return value.toFixed(digits);
|
return value.toFixed(digits);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tc(alpha: number, l: number, hue: number): string {
|
|
||||||
const lightness = 0.37 - l / 380;
|
|
||||||
const chroma = Math.max(0, 0.15 - l / 1600);
|
|
||||||
return `oklch(${r(lightness)} ${r(chroma)} ${r(hue, 1)} / ${alpha})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tcLight(alpha: number, l: number, hue: number): string {
|
|
||||||
const depth = (88 - l) / 58;
|
|
||||||
const lightness = 0.9 - 0.14 * depth;
|
|
||||||
const chroma = 0.05 + 0.13 * depth;
|
|
||||||
return `oklch(${r(lightness)} ${r(chroma)} ${r(hue, 1)} / ${alpha})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThemeTokens {
|
export interface ThemeTokens {
|
||||||
[cssVar: string]: string;
|
[cssVar: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildThemeTokens(hue: number, mode: 'dark' | 'light' = 'dark'): ThemeTokens {
|
export function buildThemeTokens(hue: number, mode: 'dark' | 'light' = 'dark'): ThemeTokens {
|
||||||
const h = ((hue % 360) + 360) % 360;
|
const h = ((hue % 360) + 360) % 360;
|
||||||
const h2 = (h + 40) % 360;
|
|
||||||
const H = r(h, 1);
|
const H = r(h, 1);
|
||||||
const H2 = r(h2, 1);
|
|
||||||
const light = mode === 'light';
|
const light = mode === 'light';
|
||||||
|
|
||||||
const accentL = light ? 0.8 : 0.8;
|
const accentL = light ? 0.5 : 0.72;
|
||||||
const accentC = light ? 0.4 : 0.2;
|
const accentC = light ? 0.07 : 0.06;
|
||||||
const titleL = light ? 0.5 : 0.8;
|
const titleL = light ? 0.36 : 0.86;
|
||||||
const titleC = light ? 0.1 : 0.05;
|
const titleC = light ? 0.06 : 0.03;
|
||||||
const mutedL = light ? 0.48 : 0.79;
|
const mutedL = light ? 0.42 : 0.8;
|
||||||
const mutedC = light ? 0.06 : 0.035;
|
const mutedC = light ? 0.05 : 0.02;
|
||||||
|
const accent = `oklch(${r(accentL)} ${r(accentC)} ${H})`;
|
||||||
|
|
||||||
|
const g = (l1: number, c1: number, a1: number, l2: number, c2: number, a2: number) =>
|
||||||
|
`linear-gradient(90deg, oklch(${l1} ${c1} ${H} / ${a1}), oklch(${l2} ${c2} ${H} / ${a2}))`;
|
||||||
|
const gActive = light
|
||||||
|
? g(0.82, 0.05, 0.55, 0.87, 0.035, 0.36)
|
||||||
|
: g(0.46, 0.06, 0.34, 0.4, 0.045, 0.22);
|
||||||
|
const gHover = light
|
||||||
|
? g(0.86, 0.035, 0.38, 0.9, 0.025, 0.24)
|
||||||
|
: g(0.42, 0.045, 0.26, 0.38, 0.035, 0.17);
|
||||||
|
const glow = (l: number, c: number, a: number) => `oklch(${l} ${c} ${H} / ${a})`;
|
||||||
|
// Fixed danger hue so error dialogs read red whatever the theme hue is.
|
||||||
|
const dangerGlow = (l: number, c: number, a: number) => `oklch(${l} ${c} 25 / ${a})`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'--mf-accent': `oklch(${r(accentL)} ${r(accentC)} ${H})`,
|
'--mf-accent': accent,
|
||||||
'--mf-accent-title': `oklch(${r(titleL)} ${r(titleC)} ${H})`,
|
'--mf-accent-title': `oklch(${r(titleL)} ${r(titleC)} ${H})`,
|
||||||
'--mf-accent-muted': `oklch(${r(mutedL)} ${r(mutedC)} ${H})`,
|
'--mf-accent-muted': `oklch(${r(mutedL)} ${r(mutedC)} ${H})`,
|
||||||
'--mf-accent-soft-bg': `color-mix(in srgb, oklch(${r(accentL)} ${r(accentC)} ${H}) 8%, var(--mf-bg))`,
|
'--mf-accent-soft-bg': `color-mix(in srgb, ${accent} 12%, var(--mf-bg))`,
|
||||||
'--mf-accent-line': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.2)`,
|
'--mf-accent-line': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.2)`,
|
||||||
'--mf-accent-hair': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.09)`,
|
'--mf-accent-hair': `oklch(${r(accentL)} ${r(accentC)} ${H} / 0.09)`,
|
||||||
'--mf-accent-btn-bg': `color-mix(in srgb, oklch(${r(accentL)} ${r(accentC)} ${H}) 15%, var(--mf-bg))`,
|
'--mf-accent-btn-bg': `color-mix(in srgb, ${accent} 18%, var(--mf-bg))`,
|
||||||
'--mf-accent-btn-bg-hover': `color-mix(in srgb, oklch(${r(accentL)} ${r(accentC)} ${H}) 24%, var(--mf-bg))`,
|
'--mf-accent-btn-bg-hover': `color-mix(in srgb, ${accent} 26%, var(--mf-bg))`,
|
||||||
'--mf-bg': light ? `oklch(0.97 0.014 ${H})` : `oklch(0.075 0.02 ${H})`,
|
'--mf-g-active': gActive,
|
||||||
'--mf-elevated': light ? `oklch(0.99 0.008 ${H})` : `oklch(0.12 0.025 ${H})`,
|
'--mf-g-hover': gHover,
|
||||||
'--mf-panel-bg': light ? `oklch(0.97 0.014 ${H} / 0.7)` : `oklch(0.1 0.022 ${H} / 0.28)`,
|
'--mf-bg': light ? `oklch(0.975 0.012 ${H})` : `oklch(0.068 0.008 ${H})`,
|
||||||
'--mf-bar-bg': light ? `oklch(0.97 0.014 ${H} / 0.9)` : `oklch(0.115 0.025 ${H} / 0.9)`,
|
'--mf-elevated': light ? `oklch(0.99 0.008 ${H})` : `oklch(0.12 0.012 ${H})`,
|
||||||
'--mf-hairline': light ? `oklch(0.3 0.035 ${H} / 0.1)` : `oklch(0.62 0.05 ${H} / 0.08)`,
|
'--mf-panel-bg': light ? 'rgba(255, 255, 255, 0.28)' : 'rgba(0, 0, 0, 0.16)',
|
||||||
|
'--mf-bar-bg': light ? `oklch(0.99 0.01 ${H} / 0.86)` : `oklch(0.105 0.012 ${H} / 0.9)`,
|
||||||
|
'--mf-hairline': light ? `oklch(0.3 0.02 ${H} / 0.06)` : `oklch(0.62 0.02 ${H} / 0.05)`,
|
||||||
'--mf-ambient': light
|
'--mf-ambient': light
|
||||||
? 'none'
|
? `radial-gradient(80% 70% at 12% 0%, ${glow(0.9, 0.05, 0.55)}, transparent 75%), radial-gradient(70% 60% at 88% 8%, ${glow(0.92, 0.035, 0.4)}, transparent 75%), radial-gradient(90% 70% at 50% 110%, ${glow(0.93, 0.03, 0.35)}, transparent 80%), linear-gradient(180deg, ${glow(0.96, 0.02, 0.4)}, transparent 60%)`
|
||||||
: `radial-gradient(95% 85% at 20% 0%, ${tc(0.12, 30, h)}, transparent 88%), radial-gradient(90% 80% at 80% 0%, ${tc(0.09, 40, h)}, transparent 88%)`,
|
: `radial-gradient(75% 65% at 12% 0%, ${glow(0.3, 0.045, 0.5)}, transparent 75%), radial-gradient(65% 55% at 85% 5%, ${glow(0.24, 0.035, 0.35)}, transparent 75%), radial-gradient(90% 70% at 50% 105%, ${glow(0.18, 0.025, 0.3)}, transparent 80%), linear-gradient(180deg, ${glow(0.14, 0.02, 0.35)}, transparent 55%)`,
|
||||||
'--mf-hero-tint': light ? tcLight(1, 55, h) : tc(1, 42, h),
|
'--mf-hero-tint': light ? `oklch(0.8 0.05 ${H})` : `oklch(0.3 0.04 ${H})`,
|
||||||
'--mf-hero-bg': light
|
'--mf-hero-bg': light
|
||||||
? `linear-gradient(${tcLight(0.1, 55, h)}, ${tcLight(0.1, 55, h)})`
|
? `linear-gradient(120deg, ${glow(0.9, 0.04, 0.6)}, ${glow(0.96, 0.02, 0.4)} 60%)`
|
||||||
: `linear-gradient(${tc(0.1, 38, h)}, ${tc(0.1, 38, h)})`,
|
: `linear-gradient(120deg, ${glow(0.24, 0.04, 0.55)}, ${glow(0.14, 0.02, 0.35)} 60%, ${glow(0.12, 0.015, 0.25)})`,
|
||||||
'--mf-modal-glow': light
|
'--mf-modal-glow': light
|
||||||
? `linear-gradient(135deg, ${tcLight(0.34, 42, h)} 0%, ${tcLight(0.2, 55, h)} 30%, ${tcLight(0.1, 70, h)} 55%, ${tcLight(0.04, 85, h)} 80%, ${tcLight(0.02, 88, h)} 100%)`
|
? `linear-gradient(135deg, ${glow(0.88, 0.05, 0.5)} 0%, ${glow(0.93, 0.03, 0.2)} 40%, transparent 75%)`
|
||||||
: `linear-gradient(135deg, ${tc(0.34, 42, h)} 0%, ${tc(0.11, 70, h)} 28%, ${tc(0.03, 88, h)} 50%, rgba(6,6,9,0) 70%)`,
|
: `linear-gradient(135deg, ${glow(0.3, 0.045, 0.4)} 0%, ${glow(0.2, 0.03, 0.15)} 35%, transparent 70%)`,
|
||||||
|
'--mf-modal-glow-danger': light
|
||||||
|
? `linear-gradient(135deg, ${dangerGlow(0.86, 0.07, 0.55)} 0%, ${dangerGlow(0.92, 0.04, 0.22)} 40%, transparent 75%)`
|
||||||
|
: `linear-gradient(135deg, ${dangerGlow(0.34, 0.1, 0.45)} 0%, ${dangerGlow(0.22, 0.06, 0.18)} 35%, transparent 70%)`,
|
||||||
'--mf-spotlight-bg': light
|
'--mf-spotlight-bg': light
|
||||||
? `linear-gradient(${tcLight(0.1, 55, h)}, ${tcLight(0.1, 55, h)})`
|
? `linear-gradient(120deg, ${glow(0.9, 0.04, 0.55)}, ${glow(0.96, 0.02, 0.35)} 55%)`
|
||||||
: `linear-gradient(${tc(0.1, 38, h)}, ${tc(0.1, 38, h)})`,
|
: `linear-gradient(120deg, ${glow(0.22, 0.04, 0.5)}, ${glow(0.13, 0.02, 0.3)} 55%, ${glow(0.11, 0.015, 0.2)})`,
|
||||||
'--mf-logo-grad': light
|
'--mf-logo-grad': `linear-gradient(${accent}, ${accent})`,
|
||||||
? `linear-gradient(140deg, oklch(0.74 0.18 ${H}), oklch(0.58 0.16 ${H2}) 92%)`
|
|
||||||
: `linear-gradient(140deg, oklch(0.78 0.15 ${H}), oklch(0.62 0.13 ${H2}) 92%)`,
|
|
||||||
'--mf-cover-grad': light
|
'--mf-cover-grad': light
|
||||||
? `linear-gradient(150deg, oklch(0.7 0.16 ${H}), oklch(0.54 0.11 ${H}))`
|
? `linear-gradient(150deg, oklch(0.84 0.03 ${H}), oklch(0.74 0.02 ${H}))`
|
||||||
: `linear-gradient(150deg, oklch(0.5 0.13 ${H}), oklch(0.16 0.035 ${H}))`
|
: `linear-gradient(150deg, oklch(0.34 0.03 ${H}), oklch(0.18 0.015 ${H}))`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>
|
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>Editar</Button>
|
||||||
Editar
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
||||||
Eliminar
|
Eliminar
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -249,7 +249,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
|
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
|
||||||
<div class="text-eyebrow leading-none font-semibold text-fg-3">
|
<div class="text-eyebrow leading-none font-semibold text-fg-2">
|
||||||
Mejor resultado · {TOP_RESULT_KIND_LABEL[topResult.kind]}
|
Mejor resultado · {TOP_RESULT_KIND_LABEL[topResult.kind]}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
: topResult.user.name}
|
: topResult.user.name}
|
||||||
</div>
|
</div>
|
||||||
{#if topResult.kind === 'track' && topResult.track.artist}
|
{#if topResult.kind === 'track' && topResult.track.artist}
|
||||||
<div class="truncate text-sm leading-none text-fg-3">
|
<div class="truncate text-sm leading-none text-fg-2">
|
||||||
{topResult.track.artist}
|
{topResult.track.artist}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -276,6 +276,7 @@
|
|||||||
<svelte:element
|
<svelte:element
|
||||||
this={topResult.kind === 'track' ? 'button' : 'a'}
|
this={topResult.kind === 'track' ? 'button' : 'a'}
|
||||||
type={topResult.kind === 'track' ? 'button' : undefined}
|
type={topResult.kind === 'track' ? 'button' : undefined}
|
||||||
|
role={topResult.kind === 'track' ? 'button' : 'link'}
|
||||||
href={topResult.kind === 'track' ? undefined : topResultHref}
|
href={topResult.kind === 'track' ? undefined : topResultHref}
|
||||||
onclick={topResult.kind === 'track' ? playTopResult : undefined}
|
onclick={topResult.kind === 'track' ? playTopResult : undefined}
|
||||||
class="group/top animate-pop mt-6.5 flex w-full items-center gap-5 rounded-panel-lg bg-surface p-4.5 text-left transition hover:bg-surface-hover"
|
class="group/top animate-pop mt-6.5 flex w-full items-center gap-5 rounded-panel-lg bg-surface p-4.5 text-left transition hover:bg-surface-hover"
|
||||||
@@ -348,7 +349,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block">
|
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
||||||
{fmtTime(seconds)} · {fmtPlays(track.listensCount)}
|
{fmtTime(seconds)} · {fmtPlays(track.listensCount)}
|
||||||
</span>
|
</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -383,7 +384,7 @@
|
|||||||
oncontextmenu={(e) => openAlbumMenu(e, album.id)}
|
oncontextmenu={(e) => openAlbumMenu(e, album.id)}
|
||||||
>
|
>
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block">
|
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
||||||
{Number(album.trackCount)} canciones
|
{Number(album.trackCount)} canciones
|
||||||
</span>
|
</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -67,9 +67,7 @@
|
|||||||
Reproducir
|
Reproducir
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>
|
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>Editar</Button>
|
||||||
Editar
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
||||||
Eliminar
|
Eliminar
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-medium text-fg">{step.label}</span>
|
<span class="text-sm font-medium text-fg">{step.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2.5 text-xs text-muted">{step.hint}</p>
|
<p class="mt-2.5 text-xs text-fg-2">{step.hint}</p>
|
||||||
</Surface>
|
</Surface>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
aria-label="Seleccionar archivo de audio"
|
aria-label="Seleccionar archivo de audio"
|
||||||
/>
|
/>
|
||||||
{#if audioName === ''}
|
{#if audioName === ''}
|
||||||
<Headphones class="size-9 text-muted sm:size-10" strokeWidth={1.4} />
|
<Headphones class="size-9 text-fg-2 sm:size-10" strokeWidth={1.4} />
|
||||||
{:else}
|
{:else}
|
||||||
<Music class="size-9 text-accent-soft sm:size-10" strokeWidth={1.4} />
|
<Music class="size-9 text-accent-soft sm:size-10" strokeWidth={1.4} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -199,12 +199,12 @@
|
|||||||
<p class="mt-4 text-sm font-medium text-fg">
|
<p class="mt-4 text-sm font-medium text-fg">
|
||||||
Arrastra tu audio aquí o haz clic para elegir
|
Arrastra tu audio aquí o haz clic para elegir
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1.5 text-xs text-muted">MP3, FLAC, WAV…</p>
|
<p class="mt-1.5 text-xs text-fg-2">MP3, FLAC, WAV…</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mt-4 max-w-full truncate text-sm font-medium text-fg">
|
<p class="mt-4 max-w-full truncate text-sm font-medium text-fg">
|
||||||
{audioName}
|
{audioName}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1.5 text-xs text-muted">{formatSize(audioSize)} · Listo para subir</p>
|
<p class="mt-1.5 text-xs text-fg-2">{formatSize(audioSize)} · Listo para subir</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@
|
|||||||
<Surface padding="sm">
|
<Surface padding="sm">
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="font-medium text-fg">Subiendo y creando la pista…</span>
|
<span class="font-medium text-fg">Subiendo y creando la pista…</span>
|
||||||
<span class="text-xs text-muted">un momento</span>
|
<span class="text-xs text-fg-2">un momento</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 h-2 overflow-hidden rounded-full bg-surface-2">
|
<div class="mt-3 h-2 overflow-hidden rounded-full bg-surface-2">
|
||||||
<div class="h-full w-full animate-pulse rounded-full bg-accent-soft"></div>
|
<div class="h-full w-full animate-pulse rounded-full bg-accent-soft"></div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import TopBar from '$lib/components/layout/TopBar.svelte';
|
import TopBar from '$lib/components/layout/TopBar.svelte';
|
||||||
import MobileHeader from '$lib/components/layout/MobileHeader.svelte';
|
import MobileHeader from '$lib/components/layout/MobileHeader.svelte';
|
||||||
import Modal from '$lib/components/ui/overlay/Modal.svelte';
|
import Modal from '$lib/components/ui/overlay/Modal.svelte';
|
||||||
|
import DialogHost from '$lib/components/ui/overlay/DialogHost.svelte';
|
||||||
import PlaylistForm from '$lib/components/ui/forms/PlaylistForm.svelte';
|
import PlaylistForm from '$lib/components/ui/forms/PlaylistForm.svelte';
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import { queuePanel } from '$lib/player/queuePanel.svelte';
|
import { queuePanel } from '$lib/player/queuePanel.svelte';
|
||||||
@@ -87,6 +88,7 @@
|
|||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- CSS generado a partir de números, sin entrada de usuario -->
|
||||||
{@html `<style>${initialThemeCss}</style>`}
|
{@html `<style>${initialThemeCss}</style>`}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
@@ -134,3 +136,5 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<DialogHost />
|
||||||
|
|||||||
@@ -134,9 +134,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<div
|
<div class="relative overflow-hidden rounded-panel-lg hero-surface px-6 py-8 sm:px-9 sm:py-8.5">
|
||||||
class="relative overflow-hidden rounded-panel-lg hero-surface px-6 py-8 sm:px-9 sm:py-8.5"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-col items-start gap-7 lg:flex-row lg:flex-wrap lg:items-end lg:justify-between"
|
class="relative flex flex-col items-start gap-7 lg:flex-row lg:flex-wrap lg:items-end lg:justify-between"
|
||||||
>
|
>
|
||||||
@@ -173,7 +171,7 @@
|
|||||||
{#each statsList as stat (stat.label)}
|
{#each statsList as stat (stat.label)}
|
||||||
<div>
|
<div>
|
||||||
<div class="text-display-2 text-fg">{stat.value}</div>
|
<div class="text-display-2 text-fg">{stat.value}</div>
|
||||||
<div class="mt-1 text-xs text-fg-3">{stat.label}</div>
|
<div class="mt-1 text-xs text-fg-2">{stat.label}</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -245,7 +243,7 @@
|
|||||||
<Button variant="secondary" onclick={shuffleSpotlight}>Aleatorio</Button>
|
<Button variant="secondary" onclick={shuffleSpotlight}>Aleatorio</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-0.5 p-5.5 sm:p-6">
|
<div class="flex flex-col gap-2 p-5.5 sm:p-6">
|
||||||
{#each spotlightRows as { track, seconds }, i (track.id)}
|
{#each spotlightRows as { track, seconds }, i (track.id)}
|
||||||
<ListRow
|
<ListRow
|
||||||
onclick={() => playSpotlightTrack(i)}
|
onclick={() => playSpotlightTrack(i)}
|
||||||
@@ -265,7 +263,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</ListRow>
|
</ListRow>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="p-2 text-sm text-fg-3">Esta playlist todavía no tiene canciones.</p>
|
<p class="p-2 text-sm text-fg-2">Esta playlist todavía no tiene canciones.</p>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,12 +316,12 @@
|
|||||||
{#if isCurrent}
|
{#if isCurrent}
|
||||||
<EqBars size={13} paused={!player.playing} />
|
<EqBars size={13} paused={!player.playing} />
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-xs text-muted tabular-nums">{i + 1}</span>
|
<span class="text-xs text-fg-2 tabular-nums">{i + 1}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<span class="hidden shrink-0 text-xs text-muted tabular-nums sm:block">
|
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
||||||
{fmtPlays(track.listensCount)}
|
{fmtPlays(track.listensCount)}
|
||||||
</span>
|
</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -9,15 +9,13 @@
|
|||||||
--color-surface: var(--mf-surface);
|
--color-surface: var(--mf-surface);
|
||||||
--color-surface-2: var(--mf-surface-2);
|
--color-surface-2: var(--mf-surface-2);
|
||||||
--color-surface-hover: var(--mf-surface-hover);
|
--color-surface-hover: var(--mf-surface-hover);
|
||||||
--color-hover: var(--mf-surface-2);
|
--color-hover: var(--mf-row-hover);
|
||||||
--color-line: var(--mf-border);
|
--color-line: var(--mf-border);
|
||||||
--color-line-strong: var(--mf-border-strong);
|
--color-line-strong: var(--mf-border-strong);
|
||||||
--color-hairline: var(--mf-hairline);
|
--color-hairline: var(--mf-hairline);
|
||||||
--color-track: var(--mf-track);
|
--color-track: var(--mf-track);
|
||||||
--color-fg: var(--mf-text);
|
--color-fg: var(--mf-text);
|
||||||
--color-fg-2: var(--mf-text-2);
|
--color-fg-2: var(--mf-text-2);
|
||||||
--color-fg-3: var(--mf-text-3);
|
|
||||||
--color-muted: var(--mf-text-4);
|
|
||||||
--color-accent: var(--mf-accent);
|
--color-accent: var(--mf-accent);
|
||||||
--color-accent-soft: var(--mf-accent-title);
|
--color-accent-soft: var(--mf-accent-title);
|
||||||
--color-accent-tint: var(--mf-accent-soft-bg);
|
--color-accent-tint: var(--mf-accent-soft-bg);
|
||||||
@@ -29,17 +27,17 @@
|
|||||||
--color-on-art: var(--mf-on-art);
|
--color-on-art: var(--mf-on-art);
|
||||||
--color-on-art-2: var(--mf-on-art-2);
|
--color-on-art-2: var(--mf-on-art-2);
|
||||||
--color-danger: oklch(63.7% 0.15 25.331);
|
--color-danger: oklch(63.7% 0.15 25.331);
|
||||||
--color-danger-fg: oklch(88.5% 0.062 18.334);
|
--color-danger-fg: var(--mf-danger-fg);
|
||||||
--color-btn: color-mix(
|
--color-btn: var(--mf-btn-bg);
|
||||||
in srgb,
|
|
||||||
var(--mf-elevated) 45%,
|
|
||||||
color-mix(in srgb, var(--mf-hero-tint) 8%, var(--mf-bg))
|
|
||||||
);
|
|
||||||
--color-btn-hover: var(--mf-btn-bg-hover);
|
--color-btn-hover: var(--mf-btn-bg-hover);
|
||||||
|
--color-cta-strong-hover: var(--mf-cta-strong-hover);
|
||||||
|
--color-row-hover: var(--mf-row-hover);
|
||||||
|
|
||||||
--radius-tag: 0.25rem;
|
--radius-tag: 0.25rem;
|
||||||
--radius-thumb: 0.5rem;
|
--radius-thumb: 0.5rem;
|
||||||
--radius-control: 0.625rem;
|
--radius-control: 0.625rem;
|
||||||
|
--radius-btn-sm: 0.6875rem;
|
||||||
|
--radius-btn: 0.75rem;
|
||||||
--radius-art: 0.8125rem;
|
--radius-art: 0.8125rem;
|
||||||
--radius-art-lg: 1.125rem;
|
--radius-art-lg: 1.125rem;
|
||||||
--radius-panel: 1.125rem;
|
--radius-panel: 1.125rem;
|
||||||
@@ -66,6 +64,11 @@
|
|||||||
--container-search: 30rem;
|
--container-search: 30rem;
|
||||||
--container-cta: 20rem;
|
--container-cta: 20rem;
|
||||||
|
|
||||||
|
--text-control-sm: 0.78125rem;
|
||||||
|
|
||||||
|
--ease-snappy: var(--mf-ease);
|
||||||
|
--default-transition-duration: var(--mf-motion-fast);
|
||||||
|
|
||||||
--tracking-display: -0.02em;
|
--tracking-display: -0.02em;
|
||||||
--tracking-eyebrow: 0.1em;
|
--tracking-eyebrow: 0.1em;
|
||||||
--leading-body: 1.65;
|
--leading-body: 1.65;
|
||||||
@@ -122,7 +125,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility btn-transition {
|
@utility btn-transition {
|
||||||
transition-property: color, background-color, border-color, outline-color, opacity, box-shadow, filter;
|
transition-property:
|
||||||
|
color, background-color, border-color, outline-color, opacity, box-shadow, filter;
|
||||||
transition-timing-function: var(--default-transition-timing-function);
|
transition-timing-function: var(--default-transition-timing-function);
|
||||||
transition-duration: var(--default-transition-duration);
|
transition-duration: var(--default-transition-duration);
|
||||||
}
|
}
|
||||||
@@ -184,7 +188,7 @@
|
|||||||
@utility app-main {
|
@utility app-main {
|
||||||
--mf-dock-h: 0px;
|
--mf-dock-h: 0px;
|
||||||
padding-bottom: calc(var(--mf-nav-h) + var(--mf-safe-b) + var(--mf-dock-h));
|
padding-bottom: calc(var(--mf-nav-h) + var(--mf-safe-b) + var(--mf-dock-h));
|
||||||
transition: padding 0.3s;
|
transition: padding var(--mf-motion-slow) var(--mf-ease);
|
||||||
|
|
||||||
&[data-has-track] {
|
&[data-has-track] {
|
||||||
--mf-dock-h: var(--mf-player-h);
|
--mf-dock-h: var(--mf-player-h);
|
||||||
@@ -197,6 +201,12 @@
|
|||||||
oklch(var(--mf-tile-shade-l) var(--mf-tile-shade-c) var(--tile-hue))
|
oklch(var(--mf-tile-shade-l) var(--mf-tile-shade-c) var(--tile-hue))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@utility surface-active {
|
||||||
|
background-image: var(--mf-g-active);
|
||||||
|
}
|
||||||
|
@utility surface-active-hover {
|
||||||
|
background-image: var(--mf-g-hover);
|
||||||
|
}
|
||||||
@utility hero-surface {
|
@utility hero-surface {
|
||||||
background-image: var(--mf-hero-bg);
|
background-image: var(--mf-hero-bg);
|
||||||
}
|
}
|
||||||
@@ -211,7 +221,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
@utility field-control {
|
@utility field-control {
|
||||||
@apply w-full rounded-control bg-surface py-3 text-sm text-fg transition placeholder:text-muted focus:bg-surface-2 focus:outline-none;
|
@apply w-full rounded-control bg-surface py-3 text-sm text-fg transition placeholder:text-fg-2 focus:bg-surface-2 focus:outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -258,7 +268,7 @@ button:disabled {
|
|||||||
@keyframes pop-in {
|
@keyframes pop-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.96) translateY(0.5rem);
|
transform: scale(0.98) translateY(0.25rem);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -307,16 +317,16 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.animate-enter {
|
.animate-enter {
|
||||||
animation: fade-up 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: fade-up var(--mf-motion-slow) var(--mf-ease) both;
|
||||||
animation-delay: calc(min(var(--i, 0), 10) * 45ms);
|
animation-delay: calc(min(var(--i, 0), 10) * 40ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-pop {
|
.animate-pop {
|
||||||
animation: pop-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: pop-in var(--mf-motion-base) var(--mf-ease) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade {
|
.animate-fade {
|
||||||
animation: fade-in 0.4s ease both;
|
animation: fade-in var(--mf-motion-base) var(--mf-ease) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
@@ -324,7 +334,11 @@ button:disabled {
|
|||||||
background-image: var(--mf-modal-glow);
|
background-image: var(--mf-modal-glow);
|
||||||
backdrop-filter: blur(22px);
|
backdrop-filter: blur(22px);
|
||||||
-webkit-backdrop-filter: blur(22px);
|
-webkit-backdrop-filter: blur(22px);
|
||||||
transition: background 0.6s;
|
transition: background var(--mf-motion-slow) var(--mf-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel.glass-panel-danger {
|
||||||
|
background-image: var(--mf-modal-glow-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
Reference in New Issue
Block a user