Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

Full version-by-version history for Pinakes. The README shows only the latest release; everything older lives here.

## [0.8.2]

### Fixed
- **The catalogue filters are a sidebar again, whatever the names in them.** A publisher named in a hundred characters (shown with an ellipsis) still counted at full length in the width of the filters column: on a live library the column took the whole page and the books went under the filters. The column now has a width of its own, a quarter of the page between 250 and 300px.
- **One tap on the phone's tab bar.** For a visitor who had not answered the cookie banner yet, the banner lay over the bar and took the first tap, so "Catalogo" needed a second one. The banner now sits above the bar.
- **The book page on a phone has no uneven rows.** Year, pages, format and ISBN go two by two instead of three and one; the share buttons and the citation actions go one per row, the citation styles stay on one line that scrolls sideways; the card titles keep room above their buttons; the loan and favourite buttons start where "Available" / "Not available today" starts and fill the box.
- The home search no longer draws a second rectangle around the text while typing: the rounded box alone shows the focus.

No migration.

## [0.8.1]

### Fixed
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ Pinakes is a self-hosted, full-featured ILS for schools, municipalities, and pri

Highlights of the latest release are below. The full version-by-version history (v0.7.59 → v0.6.x) lives in **[CHANGELOG.md](CHANGELOG.md)**.

### v0.8.1 — latest
### v0.8.2 — latest

**The catalogue keeps its sidebar.** A publisher with a very long name no longer pushes the books under the filters: the filters column has a width of its own. On phones the tab bar opens the catalogue with one tap even while the cookie banner is waiting for an answer, the book page lines up its facts two by two and its buttons one per row, and the home search draws no extra ring around the text. No migration.

### v0.8.1

**What the CMS says is what the site shows.** Every text of the home, the catalogue header and the new events header is edited in the admin and shown exactly as written: no hidden default replaces an empty field. The image of a CMS page such as About us is saved when Save is pressed, the search suggestions under the home's search box open on top of the page instead of under the next section, the catalogue grid lines its cards up, and the events page title and subtitle are editable per language. No migration.

Expand Down
47 changes: 40 additions & 7 deletions public/assets/pinakes-2026.css
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,10 @@ body.pk .pk-hero__title em { color: var(--pk-accent-ink); }
body.pk .pk-search input { flex: 1; min-width: 0; border: 0; outline: none; font-size: 16px; background: transparent; color: var(--pk-ink); padding: 8px 0; box-shadow: none; }
/* The field has no frame of its own: the whole search box shows the focus. */
.pk-search:focus-within { border-color: var(--pk-focus); box-shadow: 0 0 0 1px var(--pk-focus), 0 12px 32px -18px color-mix(in srgb, var(--pk-accent-strong) 35%, transparent); }
body.pk .pk-search input:focus-visible { outline: none; }
/* The field draws nothing of its own when focused: the whole rounded box
already shows the focus (:focus-within above). A form-reset rule gave the
input a 1px accent ring, a second rectangle around the typed text. */
body.pk .pk-search input:focus, body.pk .pk-search input:focus-visible { outline: none; box-shadow: none !important; border-color: transparent; }
.pk-search button {
min-height: 44px; padding: 12px 22px; border-radius: 11px; border: 0; background: var(--pk-button); color: var(--pk-button-text);
font-weight: 600; font-size: 15px; cursor: pointer; transition: background-color .2s;
Expand Down Expand Up @@ -603,8 +606,12 @@ body.pk .pk-catalog-head .catalog-header-content { text-align: left; max-width:

.pk-catalog { padding: 0 0 88px; }
.pk-catalog__layout { display: flex; flex-wrap: wrap; gap: 40px; align-items: flex-start; }
.pk-filters { flex: 1 1 250px; max-width: 100%; position: sticky; top: 88px; }
.pk-results { flex: 999 1 520px; min-width: 0; display: flex; flex-direction: column; gap: 28px; }
/* The filters column has a width of its own: a quarter of the layout, 250 to
300px. It never grows to its content: a publisher named in a hundred
characters (shown with an ellipsis) still counted at full length in the
column's minimum width, which pushed the books under the filters. */
.pk-filters { flex: 0 0 clamp(250px, 25%, 300px); min-width: 0; max-width: 100%; position: sticky; top: 88px; }
.pk-results { flex: 1 1 520px; min-width: 0; display: flex; flex-direction: column; gap: 28px; }
Comment thread
fabiodalez-dev marked this conversation as resolved.
Outdated
@media (max-width: 860px) { .pk-filters { position: static; flex-basis: 100%; } }

body.pk :is(.pk-filters, .filters-panel) .filters-panel { background: none; border: 0; box-shadow: none; padding: 0; position: static; max-height: none; overflow: visible; }
Expand Down Expand Up @@ -975,7 +982,9 @@ body.pk .pk-availbox #btn-request-loan:disabled { opacity: .55; cursor: not-allo
@media (max-width: 640px) {
body.pk .pk-availbox { gap: 16px; }
body.pk .pk-availbox .action-buttons { flex-direction: column; width: 100%; }
body.pk .pk-availbox .action-buttons .ui-button { width: 100%; justify-content: center; }
/* Full width, so the buttons start where "Disponibile" starts: book-detail.css
caps them at 300px and centres them inside the box. */
body.pk .pk-availbox .action-buttons .ui-button { width: 100%; max-width: none; justify-content: center; }
}
body.pk .pk-availbox .btn-fav-custom, body.pk .pk-availbox .action-buttons .btn-secondary {
background: var(--pk-surface); color: var(--pk-ink); border: 1px solid var(--pk-line-2); font-weight: 500; padding: 13px 18px;
Expand All @@ -987,6 +996,10 @@ body.pk .pk-availbox .btn-fav-custom:hover { border-color: var(--pk-accent); bac
body.pk .pk-availbox .btn-fav-custom.btn-danger { border-color: var(--pk-accent); color: var(--pk-accent-strong); background: var(--pk-accent-soft); }

body.pk .pk-quick { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); border-top: 1px solid var(--pk-line); border-bottom: 1px solid var(--pk-line); margin: 0; }
/* Four facts are never three and one left alone: four in a row when they
fit (480px, four 120px columns), otherwise two by two. The clamp() picks
25% or 50% as the column minimum from the strip's own width. */
body.pk .pk-quick:has(> .pk-quick__item:nth-child(4):last-child) { grid-template-columns: repeat(auto-fit, minmax(clamp(25%, (480px - 100%) * 999, 50%), 1fr)); }
body.pk .pk-quick__item { padding: 16px 0; display: flex; flex-direction: column; gap: 4px; }
body.pk .pk-quick dt { font-size: 12px; color: var(--pk-faint); text-transform: uppercase; letter-spacing: .06em; font-weight: 400; }
body.pk .pk-quick dd { margin: 0; font-size: 15px; font-weight: 600; color: var(--pk-ink); overflow-wrap: anywhere; }
Expand Down Expand Up @@ -1071,7 +1084,9 @@ body.pk .pk-bookbody .keyword-chip:hover { background: var(--pk-accent-line); }
body.pk .pk-bookbody__aside .card {
padding: 22px; border-radius: 20px; background: var(--pk-surface); border: 1px solid var(--pk-line); box-shadow: none; margin: 0; z-index: auto;
}
body.pk .pk-bookbody__aside .card-header { padding: 0; margin: 0 0 8px; border: 0; background: none; }
/* main.css forces `.card-header { margin: 0 !important }`: the space under a
card's title is padding, or the buttons sit right against it. */
body.pk .pk-bookbody__aside .card-header { padding: 0 0 12px; margin: 0; border: 0; background: none; }
body.pk .pk-bookbody__aside .card-header h6 { font: 600 12px/1.2 var(--pk-sans); letter-spacing: .08em; text-transform: uppercase; color: var(--pk-muted); margin: 0; }
body.pk .pk-bookbody__aside .card-header h6 i { color: var(--pk-faint); }
body.pk .pk-bookbody__aside .card-body { padding: 0; }
Expand All @@ -1083,8 +1098,8 @@ body.pk .pk-bookbody__aside .book-status-inline { padding: 0; background: none;

/* Cite section */
.pk-cite { padding: 0 0 80px; }
body.pk .pk-cite #book-cite-card { background: none; border: 0; box-shadow: none; padding: 0; margin: 0; }
body.pk .pk-cite #book-cite-card > .card-header { padding: 0; margin: 0 0 18px; background: none; border: 0; }
body.pk .pk-cite #book-cite-card { background: none; border: 0; box-shadow: none; padding: 0; margin: 0; overflow: visible; }
body.pk .pk-cite #book-cite-card > .card-header { padding: 0 0 18px; margin: 0; background: none; border: 0; }
body.pk .pk-cite #book-cite-card > .card-header h6 { font-family: var(--pk-serif); font-weight: 500; font-size: 32px; letter-spacing: -.015em; color: var(--pk-ink); margin: 0; text-transform: none; }
body.pk .pk-cite #book-cite-card > .card-header h6 i { font-size: 18px; color: var(--pk-faint); }
body.pk .pk-cite #book-cite-card > .card-body { padding: 0; }
Expand All @@ -1095,6 +1110,15 @@ body.pk .pk-cite #book-cite-card > .card-body { padding: 0; }
.pk-citebox__text { padding: 22px 24px; border-radius: 16px; background: var(--pk-surface); border: 1px solid var(--pk-line); font-family: var(--pk-serif); font-size: 18px; max-width: 880px; line-height: 1.5; color: var(--pk-ink-2); }
.pk-citebox__actions { display: flex; gap: 8px; flex-wrap: wrap; }
body.pk .pk-cite .cite-block { margin-top: 18px; }
/* On a phone a row of buttons is never wrapped unevenly: the citation styles
stay on one line that scrolls sideways, the actions go one per row. */
@media (max-width: 640px) {
.pk-citebox__tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin-right: calc(-1 * var(--pk-gutter)); padding-right: var(--pk-gutter); scroll-snap-type: x proximity; }
.pk-citebox__tabs::-webkit-scrollbar { display: none; }
.pk-citebox__tab { flex: 0 0 auto; white-space: nowrap; scroll-snap-align: start; }
.pk-citebox__actions { flex-direction: column; }
.pk-citebox__actions > *, body.pk .pk-citebox__actions #cite-open { width: 100%; justify-content: center; }
}

/* Series and related */
body.pk .book-series-section { padding: 0 0 56px; background: none; }
Expand Down Expand Up @@ -1446,6 +1470,11 @@ body.pk #book-share-card .social-share-btn:hover { border-color: var(--pk-accent
body.pk #book-share-card .social-share-btn:hover i { color: inherit; }
body.pk #book-share-card .social-share-btn__label { display: inline; }
body.pk #book-share-card .card-body > div { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
@media (max-width: 640px) {
/* One share button per row on a phone, never three and two. */
body.pk #book-share-card .card-body > div { flex-direction: column; gap: 8px; }
body.pk #book-share-card .social-share-btn { width: 100%; justify-content: center; padding: 10px 14px; font-size: 14px; }
}
@media (max-width: 1023px) {
/* book-detail.css centres the hero on small screens; the design keeps it left-aligned. */
body.pk .pk-bookhero__info { text-align: left; }
Expand Down Expand Up @@ -1518,6 +1547,10 @@ body.pk.pk-hero-centered .pk-fan { display: none; }
/* The back-to-top button and the cookie icon move above the bar. */
body.pk-tabbar-on #scroll-to-top { bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important; }
body.pk-tabbar-on #silktide-cookie-icon { bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important; transition: bottom .28s var(--pk-ease); }
/* The cookie banner too: at its own bottom: 16px it lay over the bar, and a
visitor who had not answered it yet tapped the banner instead of the
tab, which then needed a second tap. */
body.pk-tabbar-on #silktide-banner { bottom: calc(76px + env(safe-area-inset-bottom, 0px)) !important; max-height: calc(100vh - 92px - env(safe-area-inset-bottom, 0px)); transition: bottom .28s var(--pk-ease); }
}
@media (max-width: 767px) and (prefers-reduced-motion: reduce) {
.pk-tabbar, .pk-tabbar.is-visible { transition: none; }
Expand Down
73 changes: 73 additions & 0 deletions tests/catalog-filters-sidebar.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// @ts-check
/**
* On a desktop the catalogue filters are a sidebar beside the books, whatever
* the names in them. A live library has publishers named in a hundred
* characters ("Cross-National Research Group, European Research Centre,
* Loughborough University of Technology"): shown with an ellipsis, they still
* counted at full length in the column's minimum width, the column grew to
* the whole page and the books went under the filters.
*
* The data is seeded (a publisher with such a name on three books), the check
* is what a visitor sees: the filters on the left, at most 300px, the books
* beside them.
*
* Run: /tmp/run-e2e.sh tests/catalog-filters-sidebar.spec.js --config=tests/playwright.config.js --workers=1
*/
const { test, expect } = require('@playwright/test');
const { execFileSync } = require('child_process');

const BASE = process.env.E2E_BASE_URL || 'http://localhost:8081';
const LONG = 'Cross-National Research Group, European Research Centre, Loughborough University of Technology E2E';

function db(sql) {
const args = ['--default-character-set=utf8mb4', '-N', '-B', '--raw', '-e', sql];
if (process.env.E2E_DB_HOST) args.push('-h', process.env.E2E_DB_HOST, ...(process.env.E2E_DB_PORT ? ['-P', process.env.E2E_DB_PORT] : []));
else if (process.env.E2E_DB_SOCKET) args.push('-S', process.env.E2E_DB_SOCKET);
args.push('-u', process.env.E2E_DB_USER || '', process.env.E2E_DB_NAME || '');
return execFileSync('mysql', args, { encoding: 'utf-8', timeout: 15000, env: { ...process.env, MYSQL_PWD: process.env.E2E_DB_PASS || '' } }).trim();
}

test.skip(!process.env.E2E_DB_USER, 'database credentials required');

test.describe.serial('Catalogue filters sidebar', () => {
let publisherId = '';
/** @type {Array<[string, string]>} */
let books = [];

test.beforeAll(() => {
db(`INSERT INTO editori (nome) VALUES ('${LONG}')`);
publisherId = db(`SELECT id FROM editori WHERE nome = '${LONG}' ORDER BY id DESC LIMIT 1`);
books = db('SELECT id, IFNULL(editore_id, \'NULL\') FROM libri WHERE deleted_at IS NULL ORDER BY id DESC LIMIT 3')
.split('\n').filter(Boolean).map((l) => /** @type {[string, string]} */ (l.split('\t')));
for (const [id] of books) {
db(`UPDATE libri SET editore_id = ${publisherId} WHERE id = ${id}`);
db(`INSERT IGNORE INTO libri_editori (libro_id, editore_id, ordine) VALUES (${id}, ${publisherId}, 99)`);
}
});

test.afterAll(() => {
try {
for (const [id, prev] of books) db(`UPDATE libri SET editore_id = ${prev} WHERE id = ${id}`);
if (publisherId) {
db(`DELETE FROM libri_editori WHERE editore_id = ${publisherId}`);
db(`DELETE FROM editori WHERE id = ${publisherId}`);
}
} catch { /* best effort */ }
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for (const width of [1440, 1024]) {
test(`at ${width}px the filters stay a sidebar beside the books`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await page.goto(`${BASE}/catalogo`, { waitUntil: 'networkidle' });
await expect(page.locator('#publishers-filter')).toContainText('Loughborough');
const r = await page.evaluate(() => {
const f = document.querySelector('.pk-filters').getBoundingClientRect();
const g = document.querySelector('.pk-results').getBoundingClientRect();
return { fw: Math.round(f.width), fRight: Math.round(f.right), gLeft: Math.round(g.left), sameRow: Math.abs(f.top - g.top) < 120 };
});
expect(r.fw, 'the sidebar keeps its width').toBeLessThanOrEqual(300);
expect(r.sameRow, 'the books sit beside the filters, not under them').toBe(true);
expect(r.gLeft, 'the books start right of the filters').toBeGreaterThan(r.fRight);
});
}
});
Loading
Loading