Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3edde697 | ||
|
|
14e3ab574c | ||
|
|
4b2eec5703 | ||
|
|
3871d37fdb | ||
|
|
09b005557c |
No files matched your search
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
description: Take on the next actionable backlog issue end to end, and stop
|
||||||
|
---
|
||||||
|
Take on exactly one issue from the YellowJacket backlog, end to end, and stop.
|
||||||
|
|
||||||
|
Repo: yonlu/yellowjacket at https://git.ljones.me — API base
|
||||||
|
https://git.ljones.me/api/v1/repos/yonlu/yellowjacket, auth with
|
||||||
|
`-H "Authorization: token $GITEA_TOKEN"`. Default branch is `main`.
|
||||||
|
|
||||||
|
## 1. Orient before you pick
|
||||||
|
|
||||||
|
Read, in this order: `CLAUDE.md` (the architecture and the reasons behind
|
||||||
|
it), `.pi/journal.md` (what happened last), `.planning/NOTES.md` (what was
|
||||||
|
already considered and rejected), and `.planning/plans/active/`. Do not skip
|
||||||
|
this because the issue looks small — most of this codebase's traps are
|
||||||
|
written down in exactly one of those four places, and the ones that bite are
|
||||||
|
the ones you didn't read.
|
||||||
|
|
||||||
|
## 2. Pick the issue
|
||||||
|
|
||||||
|
List open issues. Choose the single highest-value one that is *actionable
|
||||||
|
right now*:
|
||||||
|
|
||||||
|
- Order by `Priority/Critical` → `High` → `Medium` → `Low`. Within a tier,
|
||||||
|
prefer `Reviewed/Confirmed`, then `Kind/Bug` over `Kind/Enhancement` over
|
||||||
|
`Kind/Feature`.
|
||||||
|
- Consult issue #73 (the roadmap) — if it sequences the candidates, that
|
||||||
|
ordering wins over the label ordering.
|
||||||
|
- **Skip** anything labelled `Status/Blocked`, `Status/In Progress`,
|
||||||
|
`Status/Abandoned`, `Reviewed/Won't Fix`, `Reviewed/Duplicate`,
|
||||||
|
`Reviewed/Invalid`, or already carrying an open PR.
|
||||||
|
- **Skip anything someone else is already on.** The label is not the only
|
||||||
|
claim, because a concurrent session may not have applied it — several pi
|
||||||
|
sessions run against this repo from separate worktrees under
|
||||||
|
`~/.paseo/worktrees/`. Run `git ls-remote --heads origin` and skip any
|
||||||
|
issue whose number or slug matches an existing branch (`60-…`,
|
||||||
|
`fix/<slug>`). A duplicated fix costs more than a skipped issue.
|
||||||
|
- **Skip** anything that cannot be verified without hardware you do not
|
||||||
|
have: physical-device Android behaviour (audio output, on-device file
|
||||||
|
writes, real gesture input). A browser at 424px is not a phone — see the
|
||||||
|
Chrome 113 section of `CLAUDE.md`.
|
||||||
|
- **Skip** intermittent-failure issues unless you can reproduce the failure
|
||||||
|
on demand within a few minutes. Chasing a 1-in-3 flake is an unbounded
|
||||||
|
task and does not belong in a scheduled run.
|
||||||
|
- If nothing qualifies, say so, do nothing, and stop. An empty run is a
|
||||||
|
correct outcome.
|
||||||
|
|
||||||
|
## 3. Claim it
|
||||||
|
|
||||||
|
Add `Status/In Progress` to the issue and comment that you are picking it
|
||||||
|
up. Then branch:
|
||||||
|
|
||||||
|
```
|
||||||
|
git fetch origin && git checkout -b <type>/<short-slug> origin/main
|
||||||
|
```
|
||||||
|
|
||||||
|
`<type>` matches the issue's `Kind` (`fix/`, `feat/`, `refactor/`, `test/`,
|
||||||
|
`docs/`, `ci/`). Branch from `origin/main`, never by checking out `main`
|
||||||
|
itself — this repo is worked from several git worktrees at once and `main`
|
||||||
|
is checked out in one of them, so `git checkout main` fails outright.
|
||||||
|
|
||||||
|
## 4. Do the work
|
||||||
|
|
||||||
|
Fix the issue that was reported and nothing else. Match the surrounding
|
||||||
|
code's style. Follow the constraints in `CLAUDE.md` rather than reasoning
|
||||||
|
from first principles — where it explains why something is shaped the way it
|
||||||
|
is, that shape is load-bearing and there is usually a test pinning it.
|
||||||
|
|
||||||
|
**Anything else you discover becomes a new issue, not a bigger diff.** File
|
||||||
|
it with the right `Area/`, `Kind/`, `Priority/` labels, describe the
|
||||||
|
symptom before the theory, and link it from your PR. Scope creep is the
|
||||||
|
failure mode this instruction exists to prevent.
|
||||||
|
|
||||||
|
If the work turns out to be materially larger than the issue implied, stop:
|
||||||
|
comment on the issue with what you found and what it would actually take,
|
||||||
|
remove `Status/In Progress`, push nothing, and end the run.
|
||||||
|
|
||||||
|
## 5. Verify — the right tier, not the cheapest one
|
||||||
|
|
||||||
|
Run `make generate` if you touched `.sql` or `.templ`, and `make bindings`
|
||||||
|
if you changed a bound Go signature. Then run what the change actually
|
||||||
|
demands:
|
||||||
|
|
||||||
|
- Go change → `make lint` and `make test` (both cover all three build
|
||||||
|
configurations).
|
||||||
|
- Frontend component or store → `make ui-test`.
|
||||||
|
- User-visible flow → `make e2e` against `make dev-headless`. **Check the
|
||||||
|
port first**: `ss -ltn | grep 34115`. If it is occupied, another worktree
|
||||||
|
is already running the app — do not start a second one and do not run
|
||||||
|
`make e2e`. Attaching to someone else's build produces a green result
|
||||||
|
about code that is not yours, which is worse than no result. Either
|
||||||
|
choose an issue that does not need this tier, or stop and say why.
|
||||||
|
- Anything cosmetic or layout-related → look at a screenshot. Several bugs
|
||||||
|
in this repo's history were invisible to every assertion and obvious in an
|
||||||
|
image.
|
||||||
|
|
||||||
|
A tier you skipped is a claim you did not check. If a tier fails for reasons
|
||||||
|
unrelated to your change, say so explicitly rather than quietly moving on.
|
||||||
|
|
||||||
|
## 6. Keep the documentation true
|
||||||
|
|
||||||
|
If you changed structure, behaviour, or a constraint, update `CLAUDE.md` in
|
||||||
|
the same commit. That file is this project's memory; a change that leaves it
|
||||||
|
describing the old shape is worse than no change. Append a short entry to
|
||||||
|
`.pi/journal.md` covering what you did, what you verified, and what you left
|
||||||
|
open.
|
||||||
|
|
||||||
|
## 7. Commit and open the PR
|
||||||
|
|
||||||
|
Conventional Commits, imperative subject, ≤72 chars, scope optional. The
|
||||||
|
body explains *why*. Push the branch — never push to `main`, never
|
||||||
|
force-push.
|
||||||
|
|
||||||
|
Open the PR:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
https://git.ljones.me/api/v1/repos/yonlu/yellowjacket/pulls \
|
||||||
|
-d '{"head":"<branch>","base":"main","title":"<subject>","body":"<body>"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The body states: what the issue was, what you changed and why, **which
|
||||||
|
verification tiers you ran and their results**, anything you deliberately
|
||||||
|
did not do, and `Closes #<n>`.
|
||||||
|
|
||||||
|
Then wait for CI (`ci.yml`, jobs `check` and `e2e`) and report the result on
|
||||||
|
the PR. If it fails, read the log — `gitea_ci`'s `job_logs` 404s on this
|
||||||
|
Gitea build, so use
|
||||||
|
`GET /api/v1/repos/yonlu/yellowjacket/actions/runs/<run>/jobs` for per-step
|
||||||
|
status and `GET /api/v1/repos/yonlu/yellowjacket/actions/jobs/<id>/logs` for
|
||||||
|
the log — and fix it. Two consecutive failed CI runs on the same cause: stop,
|
||||||
|
comment what you know on the PR, and leave it for a human.
|
||||||
|
|
||||||
|
**Do not merge.** Comment on the issue linking the PR, leave
|
||||||
|
`Status/In Progress` on, and end the run.
|
||||||
|
|
||||||
|
## Finally
|
||||||
|
|
||||||
|
Report in three lines: which issue you took, what state it is in
|
||||||
|
(PR open / CI green / stopped and why), and any issues you filed.
|
||||||
@@ -2489,6 +2489,31 @@ missing half; `catalogFailed` is the only route to `unavailable` now,
|
|||||||
and the timer is a 60 s backstop for a genuine hang rather than the
|
and the timer is a 60 s backstop for a genuine hang rather than the
|
||||||
verdict.
|
verdict.
|
||||||
|
|
||||||
|
**On a phone that page is one scroll container, and the header is in
|
||||||
|
it** (#66). It was built as a fixed header over a scrolling tracklist,
|
||||||
|
which is the desktop arrangement: at the reference device's 424×439 the
|
||||||
|
header owned **253 of the panel's 318px** and the list scrolled inside
|
||||||
|
the 64 that were left. Below 600px the *host* is the scroller and
|
||||||
|
`.content` stops being one, so the whole page moves together — which is
|
||||||
|
only available because this tracklist is plain DOM rather than a
|
||||||
|
virtualizer, and because `.main-panel > *` already gives the host a
|
||||||
|
definite height.
|
||||||
|
|
||||||
|
Three things about it are load-bearing. **Another `min-width: 0` was
|
||||||
|
not the fix**: `.album-info` carries one and was shrinking exactly as
|
||||||
|
asked, to 112px beside a 200px cover — so the title drew as `G…` and
|
||||||
|
"Shuffle album" ended at x=443 inside a 424px box, clipped by the
|
||||||
|
component's own `overflow: hidden` and reachable by no gesture. A row
|
||||||
|
with a fixed-size sibling has to **stack** at that width, or the column
|
||||||
|
that must shrink has nothing to be wide with. **`layout-overflow.spec.ts`
|
||||||
|
cannot see any of this** — `body.scrollWidth` equalled the viewport
|
||||||
|
throughout, because the overflow was *inside* a component; the spec
|
||||||
|
measures each header control against the host's own box, which is
|
||||||
|
`top-bar-fit.spec.ts`'s shape for the same reason. And **the phone block
|
||||||
|
is last in the stylesheet**, on `index.css`'s rule: a media query adds
|
||||||
|
no specificity, so written above the plain rules it overrides every
|
||||||
|
declaration in it is silently dead.
|
||||||
|
|
||||||
**Activating a row plays the list the row is in, from that row.** A
|
**Activating a row plays the list the row is in, from that row.** A
|
||||||
double-click — and Play on a single row's context menu — queues the
|
double-click — and Play on a single row's context menu — queues the
|
||||||
list as *displayed* with `startIndex` on that row, not a queue of one
|
list as *displayed* with `startIndex` on that row, not a queue of one
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { test, expect } from '../support/fixtures.js';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The album page on a phone (#66).
|
||||||
|
*
|
||||||
|
* Two faults, and neither was visible to `layout-overflow.spec.ts`:
|
||||||
|
* that spec asserts the *shell* needs no sideways scrolling, and the
|
||||||
|
* shell was correct throughout — `body.scrollWidth === clientWidth`
|
||||||
|
* while `explore-album-details` itself measured 443 inside a 424px box
|
||||||
|
* and clipped two of the album's three primary actions with its own
|
||||||
|
* `overflow: hidden`. So the measurement here is **per control against
|
||||||
|
* the component's box**, which is the same shape `top-bar-fit.spec.ts`
|
||||||
|
* needed for the same reason.
|
||||||
|
*
|
||||||
|
* The other half is the scroll: the page was a fixed header over a
|
||||||
|
* scrolling tracklist, so at the reference device's 424x439 the header
|
||||||
|
* owned 253 of the panel's 318px and the list scrolled in the 64 that
|
||||||
|
* were left. It is one scroll container below 600px, which is a
|
||||||
|
* property of the *host* rather than of `.content`.
|
||||||
|
*
|
||||||
|
* The engine is the caveat this tier cannot close: the reference device
|
||||||
|
* renders in Chrome 113 and this is Chromium/WebKit. A flex direction
|
||||||
|
* and a scroll container are nowhere near that engine's documented gaps
|
||||||
|
* (relaxed nesting, the Popover API, `light-dark()`), but "it renders
|
||||||
|
* at that size in Chromium" is not evidence about the phone.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** The phone this was measured on, in CSS pixels. */
|
||||||
|
const DEVICE = { width: 424, height: 439 };
|
||||||
|
|
||||||
|
const details = (page: Page) => page.locator('explore-album-details');
|
||||||
|
|
||||||
|
/** The page's own boxes, read from inside its shadow root. */
|
||||||
|
const geometry = (page: Page) =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const host = document.querySelector('explore-album-details');
|
||||||
|
const sr = host?.shadowRoot;
|
||||||
|
|
||||||
|
if (!host || !sr) return null;
|
||||||
|
|
||||||
|
const box = (sel: string) => {
|
||||||
|
const el = sr.querySelector(sel);
|
||||||
|
|
||||||
|
if (!el) return null;
|
||||||
|
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
return { width: Math.round(r.width), right: Math.round(r.right) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = sr.querySelector('.content');
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostWidth: host.clientWidth,
|
||||||
|
hostScrollWidth: host.scrollWidth,
|
||||||
|
// The host is the scroller below 600px, so the page is taller
|
||||||
|
// than its box rather than the tracklist being a window inside it.
|
||||||
|
hostScrolls: host.scrollHeight > host.clientHeight,
|
||||||
|
contentScrolls: content
|
||||||
|
? content.scrollHeight > content.clientHeight
|
||||||
|
: null,
|
||||||
|
header: box('.album-header'),
|
||||||
|
play: box('[data-testid="album-play"]'),
|
||||||
|
shuffle: box('[data-testid="album-shuffle"]'),
|
||||||
|
queue: box('[data-testid="album-queue"]'),
|
||||||
|
title: (() => {
|
||||||
|
const el = sr.querySelector('.album-title-text');
|
||||||
|
|
||||||
|
return el ? el.scrollWidth <= el.clientWidth + 1 : null;
|
||||||
|
})(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('the album page on a phone', () => {
|
||||||
|
test.beforeEach(async ({ app }) => {
|
||||||
|
await app.setViewportSize(DEVICE);
|
||||||
|
await openFirstAlbum(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ app }) => {
|
||||||
|
await app.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
await app.getByTestId('nav-tracks').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps every action inside its own box', async ({ app }) => {
|
||||||
|
const geo = await geometry(app);
|
||||||
|
|
||||||
|
expect(geo).not.toBeNull();
|
||||||
|
// "Shuffle album" ended at x=443 in a 424px component and could not
|
||||||
|
// be reached by any gesture; "Add to queue" at 440.
|
||||||
|
for (const action of ['play', 'shuffle', 'queue'] as const) {
|
||||||
|
expect(
|
||||||
|
geo?.[action],
|
||||||
|
`${action} is rendered`,
|
||||||
|
).not.toBeNull();
|
||||||
|
expect(
|
||||||
|
geo?.[action]?.right ?? 0,
|
||||||
|
`${action} ends inside the page`,
|
||||||
|
).toBeLessThanOrEqual(geo?.hostWidth ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(geo?.hostScrollWidth).toBe(geo?.hostWidth);
|
||||||
|
expect(geo?.header?.width).toBe(geo?.hostWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gives the title the row rather than one glyph of it', async ({
|
||||||
|
app,
|
||||||
|
}) => {
|
||||||
|
// `.album-info` was squeezed to 112px beside the art, so an album
|
||||||
|
// called *Glass Harbour* drew as `G…`. It carries `min-width: 0`
|
||||||
|
// and was shrinking as asked — the row had to stack.
|
||||||
|
expect(await geometry(app).then((g) => g?.title)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scrolls as one page, with the header scrolling away', async ({
|
||||||
|
app,
|
||||||
|
}) => {
|
||||||
|
const before = await geometry(app);
|
||||||
|
|
||||||
|
expect(before?.hostScrolls).toBe(true);
|
||||||
|
expect(before?.contentScrolls).toBe(false);
|
||||||
|
|
||||||
|
const headerTop = () =>
|
||||||
|
app.evaluate(
|
||||||
|
() =>
|
||||||
|
document
|
||||||
|
.querySelector('explore-album-details')
|
||||||
|
?.shadowRoot?.querySelector('.album-header')
|
||||||
|
?.getBoundingClientRect().top ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await headerTop()).toBeGreaterThanOrEqual(0);
|
||||||
|
|
||||||
|
// A wheel gesture, not `scrollTop`: `overflow: hidden` still permits
|
||||||
|
// programmatic scrolling, so a probe that assigns it passes on the
|
||||||
|
// build this exists to fail.
|
||||||
|
await details(app).hover();
|
||||||
|
await app.mouse.wheel(0, 250);
|
||||||
|
|
||||||
|
await expect.poll(headerTop).toBeLessThan(-100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is the desktop arrangement again above the breakpoint', async ({
|
||||||
|
app,
|
||||||
|
}) => {
|
||||||
|
await app.setViewportSize({ width: 1024, height: 800 });
|
||||||
|
|
||||||
|
// The same element, re-laid-out: one component with two
|
||||||
|
// arrangements, not a phone-only copy.
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await geometry(app))?.hostScrolls)
|
||||||
|
.toBe(false);
|
||||||
|
|
||||||
|
const arrangement = await app.evaluate(() => {
|
||||||
|
const sr = document.querySelector('explore-album-details')?.shadowRoot;
|
||||||
|
const header = sr?.querySelector('.album-header');
|
||||||
|
const content = sr?.querySelector('.content');
|
||||||
|
|
||||||
|
return {
|
||||||
|
direction: header ? getComputedStyle(header).flexDirection : null,
|
||||||
|
contentOverflow: content ? getComputedStyle(content).overflowY : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(arrangement.direction).toBe('row');
|
||||||
|
expect(arrangement.contentOverflow).toBe('auto');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Albums → the second card, which navigates to the album page. */
|
||||||
|
async function openFirstAlbum(app: Page): Promise<void> {
|
||||||
|
// Below 600px the sidebar is gone; the tab bar is the navigation.
|
||||||
|
await app.getByTestId('tab-albums').click();
|
||||||
|
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
||||||
|
'data-active-view',
|
||||||
|
'albums',
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect.poll(() => cardCount(app)).toBeGreaterThan(1);
|
||||||
|
|
||||||
|
// Dispatched rather than clicked: the card lives in a virtualizer
|
||||||
|
// inside a shadow root, and Enter expands the dropdown instead.
|
||||||
|
await app.evaluate(() => {
|
||||||
|
document
|
||||||
|
.querySelector('cover-grid')
|
||||||
|
?.shadowRoot?.querySelectorAll('.album-card')[1]
|
||||||
|
?.dispatchEvent(
|
||||||
|
new MouseEvent('click', { bubbles: true, composed: true }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
||||||
|
'data-active-view',
|
||||||
|
'explore-album-details',
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
details(app).locator('[data-testid="album-play"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cardCount(app: Page): Promise<number> {
|
||||||
|
return app.evaluate(
|
||||||
|
() =>
|
||||||
|
document
|
||||||
|
.querySelector('cover-grid')
|
||||||
|
?.shadowRoot?.querySelectorAll('.album-card').length ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -892,6 +892,67 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
|||||||
.track-row .track-request {
|
.track-row .track-request {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── The phone (#66) ──
|
||||||
|
*
|
||||||
|
* **This block is last on purpose**, for index.css's
|
||||||
|
* reason: a media query adds no specificity, so a rule
|
||||||
|
* written above the plain one it overrides loses to it and
|
||||||
|
* every declaration here is silently dead.
|
||||||
|
*
|
||||||
|
* Two faults, one shape. The page is a fixed header over a
|
||||||
|
* scrolling tracklist — the desktop arrangement — so at the
|
||||||
|
* reference device's 424x439 the header owned 253 of the
|
||||||
|
* panel's 318px and the tracklist scrolled inside the 64px
|
||||||
|
* that were left. And the header's flex row squeezed
|
||||||
|
* .album-info to 112px, so the title drew as one ellipsised
|
||||||
|
* glyph and two of the album's three primary actions were
|
||||||
|
* clipped by the host's own overflow: Shuffle album ended
|
||||||
|
* at x=443 in a 424px box, unreachable by any gesture.
|
||||||
|
*
|
||||||
|
* .album-info carries min-width: 0 and was shrinking as
|
||||||
|
* asked, so another one is not the fix — the row has to
|
||||||
|
* stack, or the info column has nothing to be wide with.
|
||||||
|
*
|
||||||
|
* The scroller moves to the host and .content stops being
|
||||||
|
* one, which is what makes the header scroll away; the
|
||||||
|
* tracklist is plain DOM rather than a virtualizer, so
|
||||||
|
* nothing inside wants a scroll window of its own. */
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
:host {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stacked, the art is the whole of the header's width
|
||||||
|
* budget and its 200px square is 45% of the reference
|
||||||
|
* device's height. It is still what identifies the
|
||||||
|
* album, so it shrinks rather than going. */
|
||||||
|
.cover-art-container {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A column flex item takes its content's width from
|
||||||
|
* align-items: flex-start above, which would leave the
|
||||||
|
* actions wrapping inside a box narrower than the row
|
||||||
|
* they now have to themselves. */
|
||||||
|
.album-info {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding: 16px 16px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user