+ );
+}
diff --git a/client/src/components/BookCard/BookCard.module.css b/client/src/components/BookCard/BookCard.module.css
index 51b0e68..f67485c 100644
--- a/client/src/components/BookCard/BookCard.module.css
+++ b/client/src/components/BookCard/BookCard.module.css
@@ -1,18 +1,20 @@
/* ===== List item ===== */
.item {
- min-width: 0; /* lets long titles shrink instead of stretching the grid */
+ min-width: 0;
+ /* lets long titles shrink instead of stretching the grid */
}
/* ===== Card container ===== */
.card {
- position: relative; /* anchor for the stretched link and the ribbon */
+ position: relative;
+ /* anchor for the stretched link and the ribbon */
display: flex;
flex-direction: column;
gap: var(--space-3);
}
-/* ===== Cover sits above the shelf ===== */
-/* So the cover's shadow falls onto the shelf instead of being painted over
+/* ===== Cover sits above the plank ===== */
+/* So the cover's shadow falls onto the shelf plank instead of being painted over
by a neighbouring card's plank */
.coverWrap {
position: relative;
@@ -29,6 +31,7 @@
z-index: 0;
height: 10px;
}
+
.plank::before {
content: '';
position: absolute;
@@ -36,9 +39,11 @@
bottom: 0;
left: -100vw;
right: -100vw;
- background: linear-gradient(var(--color-shelf-edge) 0 2px, var(--color-shelf) 2px);
- border-bottom: 2px solid rgb(0 0 0 / 0.25); /* opaque "shadow": a real box-shadow would stack across cards */
- pointer-events: none; /* otherwise it blocks clicks on neighbouring cards */
+ background: linear-gradient(var(--color-shelf-plank-edge) 0 2px, var(--color-shelf-plank) 2px);
+ border-bottom: 2px solid rgb(0 0 0 / 0.25);
+ /* opaque "shadow": a real box-shadow would stack across cards */
+ pointer-events: none;
+ /* otherwise it blocks clicks on neighbouring cards */
}
/* ===== Text ===== */
@@ -47,13 +52,16 @@
flex-direction: column;
gap: 3px;
}
+
.title {
margin: 0;
font-family: var(--font-heading);
font-weight: 600;
- font-size: var(--text-lg); /* mockup uses 18px; adjust if you like */
+ font-size: var(--text-lg);
+ /* mockup uses 18px; adjust if you like */
line-height: 1.2;
}
+
.meta {
font-size: var(--text-sm);
color: var(--color-text-soft);
@@ -64,12 +72,14 @@
color: inherit;
text-decoration: none;
}
+
.link::after {
/* invisible layer over the whole card */
content: '';
position: absolute;
inset: 0;
}
+
.card:hover .link {
text-decoration: underline;
}
@@ -78,6 +88,7 @@
.link:focus-visible {
outline: none;
}
+
.card:has(.link:focus-visible) {
outline: 2px solid var(--color-focus);
outline-offset: 4px;
@@ -90,43 +101,20 @@
gap: 2px;
align-items: center;
}
+
.starOn {
fill: var(--color-star);
}
+
.starOff {
fill: var(--color-line);
}
-/* ===== Ribbon ===== */
+/* ===== Ribbon position (the look lives in ShelfRibbon.module.css) ===== */
.ribbon {
position: absolute;
top: 0;
right: 4px;
- z-index: 2; /* above the cover and the stretched link, so it stays clickable */
- width: 44px; /* 44px-wide tap area; the drawn ribbon is only 24px */
- height: 48px;
- display: flex;
- justify-content: center;
- align-items: flex-start;
- padding: 0;
- border: 0;
- background: none;
- cursor: pointer;
-}
-.ribbon:focus-visible {
- outline: 2px solid var(--color-focus);
- outline-offset: -2px;
-}
-
-/* Not saved: pale outline ribbon */
-.ribbon path {
- fill: rgb(255 253 248 / 0.9);
- stroke: var(--color-ribbon);
- stroke-width: 1.8;
-}
-/* Saved: solid ribbon. Styled from aria-pressed, so no extra class is needed */
-.ribbon[aria-pressed='true'] path {
- fill: var(--color-ribbon);
- stroke: rgb(255 255 255 / 0.8);
- stroke-width: 1;
+ z-index: 2;
+ /* above the cover and the stretched link, so it stays clickable */
}
diff --git a/client/src/components/BookCard/BookCard.tsx b/client/src/components/BookCard/BookCard.tsx
index decd191..7080b99 100644
--- a/client/src/components/BookCard/BookCard.tsx
+++ b/client/src/components/BookCard/BookCard.tsx
@@ -3,7 +3,7 @@ import type { Book } from '../../types/book';
import { BookCover } from '../BookCover/BookCover';
import styles from './BookCard.module.css';
import { StarIcon } from '../../icons/StarIcon';
-import { RibbonIcon } from '../../icons/RibbonIcon';
+import { ShelfRibbon } from '../ShelfRibbon/ShelfRibbon';
/* ---------- Types: the contract with BookGrid and the pages ---------- */
type BookCardProps = {
@@ -11,7 +11,7 @@ type BookCardProps = {
isSaved: boolean; // is this book on My shelf?
myRating?: number; // 1–5; undefined = not rated (no stars shown)
onToggleSaved: () => void;
- showShelf?: boolean; // draw the decorative shelf plank under the cover
+ showShelfPlank?: boolean; // draw the decorative shelf plank under the cover
};
/* ---------- BookCard: one book ---------- */
@@ -21,7 +21,7 @@ export function BookCard({
isSaved,
myRating,
onToggleSaved,
- showShelf = false,
+ showShelfPlank = false,
}: BookCardProps) {
// Meta line: "Jane Austen · 1813". Year is optional, so build it defensively
const meta = [book.authors.join(', '), book.publishedYear].filter(Boolean).join(' · ');
@@ -37,10 +37,10 @@ export function BookCard({
id={book.id}
title={book.title}
author={book.authors[0]}
- coverUrl={book.coverURL}
+ coverURL={book.coverURL}
/>
- {showShelf && }
+ {showShelfPlank && }
{/* Text block */}
@@ -56,7 +56,12 @@ export function BookCard({
{/* Ribbon: sibling of the link, NOT inside it */}
-
+
);
@@ -73,26 +78,3 @@ function StarRow({ value }: { value: number }) {
);
}
-
-/* ---------- FavoriteRibbon: bookmark toggle ---------- */
-function FavoriteRibbon({
- title,
- isSaved,
- onToggle,
-}: {
- title: string;
- isSaved: boolean;
- onToggle: () => void;
-}) {
- return (
-
- );
-}
diff --git a/client/src/components/BookCover/BookCover.tsx b/client/src/components/BookCover/BookCover.tsx
index dda4490..03b5b1f 100644
--- a/client/src/components/BookCover/BookCover.tsx
+++ b/client/src/components/BookCover/BookCover.tsx
@@ -10,22 +10,22 @@ function toneFor(key: string) {
return hash % TONE_COUNT;
}
-type Props = {
+type BookCoverProps = {
id: string;
title: string;
author?: string;
- coverUrl?: string;
+ coverURL?: string;
eager?: boolean; // true for the hero on the detail page
};
-export function BookCover({ id, title, author, coverUrl, eager = false }: Props) {
+export function BookCover({ id, title, author, coverURL, eager = false }: BookCoverProps) {
const [failed, setFailed] = useState(false);
- if (coverUrl && !failed) {
+ if (coverURL && !failed) {
return (
setFailed(true)}
diff --git a/client/src/components/BookGrid/BookGrid.module.css b/client/src/components/BookGrid/BookGrid.module.css
index a206665..d0a30b9 100644
--- a/client/src/components/BookGrid/BookGrid.module.css
+++ b/client/src/components/BookGrid/BookGrid.module.css
@@ -10,10 +10,10 @@
gap: 2.75rem 1.5rem; /* row gap, column gap */
}
-/* ===== Only when the shelf is shown ===== */
+/* ===== Only when the shelf plank is shown ===== */
/* Each card's .plank::before (in BookCard.module.css) is stretched far to both
sides; this clips it at the edge of the grid */
-.withShelf {
+.withShelfPlank {
overflow-x: clip;
padding-inline: var(--space-2); /* room so focus rings and shadows aren't clipped */
}
diff --git a/client/src/components/BookGrid/BookGrid.tsx b/client/src/components/BookGrid/BookGrid.tsx
index c345139..91d1b6f 100644
--- a/client/src/components/BookGrid/BookGrid.tsx
+++ b/client/src/components/BookGrid/BookGrid.tsx
@@ -8,21 +8,21 @@ type BookGridProps = {
savedIds: Set; // which books are on My shelf
ratings: Record; // bookId → the user's own rating (missing = unrated)
onToggleSaved: (bookId: string) => void;
- showShelf?: boolean; // decorative shelf under each row; off on Browse, on for My shelf
+ showShelfPlank?: boolean; // decorative shelf plank under each row; off on Browse, on for My shelf
};
/* ---------- BookGrid: one list, one card per book ---------- */
-// Doesn't know which page it's on. The page decides which books to pass and whether to show the shelf.
+// Doesn't know which page it's on. The page decides which books to pass and whether to show the shelf plank.
export function BookGrid({
books,
savedIds,
ratings,
onToggleSaved,
- showShelf = false,
+ showShelfPlank = false,
}: BookGridProps) {
return (
// The modifier class holds the CSS that only the shelf needs (clipping)
-
+
{books.map((book) => (
onToggleSaved(book.id)} // the grid binds the id, so a card doesn't need to know it
- showShelf={showShelf}
+ showShelfPlank={showShelfPlank}
/>
))}