diff --git a/next.config.js b/next.config.js
index e4ca3913..0721bfec 100644
--- a/next.config.js
+++ b/next.config.js
@@ -139,6 +139,15 @@ module.exports = withBundleAnalyzer({
name: 'preset-default',
params: { overrides: { removeViewBox: false } },
},
+ // preset-default's cleanupIds minifies internal ids to short
+ // strings (a, b, c…) per file. SVGR inlines every icon into the
+ // same DOM, so icons that reference their own clipPath/filter/
+ // gradient via url(#id) (book, video, their shadows, delete,
+ // edit) end up with colliding ids — url(#a) resolves to whichever
+ // #a renders first, pointing at the wrong def and rendering blank.
+ // prefixIds namespaces each file's ids by filename so they stay
+ // unique across icons.
+ 'prefixIds',
],
},
},
diff --git a/public/assets/library/audio-cover.png b/public/assets/library/audio-cover.png
new file mode 100644
index 00000000..959af9b8
Binary files /dev/null and b/public/assets/library/audio-cover.png differ
diff --git a/public/assets/library/book-cover.png b/public/assets/library/book-cover.png
new file mode 100644
index 00000000..6c6830d4
Binary files /dev/null and b/public/assets/library/book-cover.png differ
diff --git a/src/components/library/molecules/AudioCard/AudioCard.module.scss b/src/components/library/molecules/AudioCard/AudioCard.module.scss
index 0be3bc2e..53a148d6 100644
--- a/src/components/library/molecules/AudioCard/AudioCard.module.scss
+++ b/src/components/library/molecules/AudioCard/AudioCard.module.scss
@@ -52,10 +52,29 @@
opacity: 0.55;
}
+// The vinyl mockup body (sleeve + disc edge baked in). Always rendered behind
+// the cover so an uncovered object reads as a blank sleeve and a covered one
+// keeps the disc peeking out on the right.
+.placeholder {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 230px;
+ height: 194px;
+ z-index: 0;
+ background: url('/assets/library/audio-cover.png') center / cover no-repeat;
+}
+
+// The uploaded album art sits on the square sleeve only — 190×190, flush to the
+// right edge so the placeholder's disc edge stays exposed on the left.
.cover {
position: absolute;
- inset: 0;
+ top: 0;
+ right: 0;
+ width: 190px;
+ height: 190px;
overflow: hidden;
+ z-index: 1;
transition: opacity 0.15s ease;
}
@@ -66,12 +85,6 @@
object-fit: cover;
}
-.coverPlaceholder {
- width: 100%;
- height: 100%;
- background: var(--white);
-}
-
.tags {
display: flex;
flex-direction: column;
@@ -94,6 +107,15 @@
height: 138px;
gap: 0;
+ .placeholder,
+ .cover {
+ inset: 0;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+
.tags {
display: none;
}
diff --git a/src/components/library/molecules/AudioCard/AudioCard.tsx b/src/components/library/molecules/AudioCard/AudioCard.tsx
index ec9b5904..00df7106 100644
--- a/src/components/library/molecules/AudioCard/AudioCard.tsx
+++ b/src/components/library/molecules/AudioCard/AudioCard.tsx
@@ -57,8 +57,9 @@ export function AudioCard({
/>
)}
+
- {coverUrl ? (
+ {coverUrl && (
- ) : (
-
)}
diff --git a/src/components/library/molecules/BookCard/BookCard.module.scss b/src/components/library/molecules/BookCard/BookCard.module.scss
index b492e601..cdaeb16e 100644
--- a/src/components/library/molecules/BookCard/BookCard.module.scss
+++ b/src/components/library/molecules/BookCard/BookCard.module.scss
@@ -30,8 +30,8 @@
.select {
position: absolute;
top: 8px;
- left: 33px;
- width: 146px;
+ left: 0;
+ width: 100%;
display: flex;
justify-content: center;
z-index: 2;
@@ -51,20 +51,30 @@
opacity: 0.55;
}
+// The book body: a complete 3D mockup (spine + binding + drop shadow baked in).
+// Always rendered behind the cover so an uncovered book reads as a blank book
+// and a covered one keeps its spine and shadow. Spans the full 180×208 card.
+.placeholder {
+ position: absolute;
+ bottom: -6px;
+ left: -5px;
+ width: 180px;
+ height: 208px;
+ z-index: 0;
+ background: url('/assets/library/book-cover.png') center / cover no-repeat;
+}
+
+// The uploaded cover art sits on the book's front face only — 146×206, flush to
+// the right edge so the placeholder's 34px spine stays exposed on the left.
.cover {
position: absolute;
- top: 0;
- left: 33px;
+ top: 1px;
+ right: 0;
width: 146px;
height: 206px;
- border-radius: 1.44px;
overflow: hidden;
+ z-index: 1;
transition: opacity 0.15s ease;
- // Paint the gradient straight onto the cover box so the card shows a filled
- // spine immediately — both for cover-less objects and while next/image is
- // still fetching/optimizing a freshly uploaded cover (which otherwise left a
- // blank gap on add).
- background: var(--gradient-book-placeholder);
}
.coverImage {
@@ -74,28 +84,6 @@
object-fit: cover;
}
-.coverPlaceholder {
- width: 100%;
- height: 100%;
- background: var(--gradient-book-placeholder);
-}
-
-.alpha {
- position: absolute;
- inset: 0;
- background: var(--surface-overlay);
- pointer-events: none;
-}
-
-.shadow {
- position: absolute;
- top: 0;
- left: 0;
- width: 180px;
- height: 208px;
- pointer-events: none;
-}
-
.tags {
display: flex;
flex-direction: column;
@@ -123,19 +111,15 @@
}
.cover {
- left: 22px;
+ top: 1px;
+ right: 0;
width: 97px;
- height: 138px;
- }
-
- .shadow {
- width: 119px;
- height: 138px;
+ height: 137px;
}
.select {
- left: 22px;
- width: 97px;
+ left: 0;
+ width: 100%;
}
.tags {
diff --git a/src/components/library/molecules/BookCard/BookCard.tsx b/src/components/library/molecules/BookCard/BookCard.tsx
index f4006ba4..205cd1aa 100644
--- a/src/components/library/molecules/BookCard/BookCard.tsx
+++ b/src/components/library/molecules/BookCard/BookCard.tsx
@@ -3,8 +3,6 @@ import classNames from 'classnames';
import Image from 'next/image';
import React, { JSX } from 'react';
-import { BookShadowIcon } from '@icons/library/svg';
-
import { SelectToggle } from '@components/library/molecules/SelectToggle';
import type { BookCardProps } from './BookCard.types';
@@ -59,8 +57,9 @@ export function BookCard({
/>
)}
+
- {coverUrl ? (
+ {coverUrl && (
- ) : (
-
)}
-
-
{/* Always render the tag column (even when empty) so the card keeps a
diff --git a/src/components/library/molecules/StepIndicator/StepIndicator.module.scss b/src/components/library/molecules/StepIndicator/StepIndicator.module.scss
index 7812d461..b0029cbf 100644
--- a/src/components/library/molecules/StepIndicator/StepIndicator.module.scss
+++ b/src/components/library/molecules/StepIndicator/StepIndicator.module.scss
@@ -1,4 +1,5 @@
.wrapper {
+ position: relative;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: start;
@@ -23,8 +24,9 @@
}
.circle {
- width: 28px;
- height: 28px;
+ box-sizing: border-box;
+ width: 36px;
+ height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
@@ -35,6 +37,7 @@
transition:
background 0.3s ease,
border-color 0.3s ease,
+ box-shadow 0.3s ease,
color 0.3s ease;
}
@@ -59,10 +62,13 @@
transition: color 0.3s ease;
}
+ // Active/completed: a green disc with a white inner gap and a green outer
+ // ring — the white 3px border carves the gap, the box-shadow draws the ring.
&.active {
.circle {
background: var(--green-200);
- border-color: var(--green-200);
+ border: 3px solid var(--white);
+ box-shadow: 0 0 0 2px var(--green-200);
color: var(--white);
}
@@ -74,7 +80,8 @@
&.completed {
.circle {
background: var(--green-200);
- border-color: var(--green-200);
+ border: 3px solid var(--white);
+ box-shadow: 0 0 0 2px var(--green-200);
color: var(--white);
svg path {
@@ -88,12 +95,17 @@
}
}
+// Span the line circle-to-circle, not column-to-column: anchored to the
+// wrapper so the wide step labels can't squeeze it. 46px = 36px circle + a
+// 10px gap on each side; top 18px centers it on the circle (radius 18px).
.connector {
+ position: absolute;
+ top: 18px;
+ left: 46px;
+ right: 46px;
height: 1px;
background: var(--gray-100);
- position: relative;
overflow: hidden;
- margin-top: 14px;
}
.connectorFill {
diff --git a/src/components/library/molecules/TagMultiSelect/TagMultiSelect.module.scss b/src/components/library/molecules/TagMultiSelect/TagMultiSelect.module.scss
index 14f58739..fa2651d9 100644
--- a/src/components/library/molecules/TagMultiSelect/TagMultiSelect.module.scss
+++ b/src/components/library/molecules/TagMultiSelect/TagMultiSelect.module.scss
@@ -1,8 +1,13 @@
.wrapper {
- position: relative;
width: 100%;
}
+// Anchors the dropdown menu to the trigger alone — kept separate from the
+// selected chips below so the chips' height never pushes the menu off the box.
+.field {
+ position: relative;
+}
+
.trigger {
width: 100%;
display: flex;
diff --git a/src/components/library/molecules/TagMultiSelect/TagMultiSelect.tsx b/src/components/library/molecules/TagMultiSelect/TagMultiSelect.tsx
index 926de056..04aa4c24 100644
--- a/src/components/library/molecules/TagMultiSelect/TagMultiSelect.tsx
+++ b/src/components/library/molecules/TagMultiSelect/TagMultiSelect.tsx
@@ -51,92 +51,94 @@ export function TagMultiSelect(props: TagMultiSelectProps): JSX.Element {
return (
-
!disabled && setIsOpen(prev => !prev)}
- disabled={disabled}
- aria-label={ariaLabel}
- aria-expanded={isOpen}
- >
-
+ !disabled && setIsOpen(prev => !prev)}
+ disabled={disabled}
+ aria-label={ariaLabel}
+ aria-expanded={isOpen}
>
- {value.length > 0 ? `${value.length} selected` : placeholder}
-
-
-
- {isOpen &&
- (!portal || menuPos) &&
- (() => {
- const menuContent = (
-
e.stopPropagation() : undefined}
- >
- {options.length === 0 ? (
-
-
- {emptyState}
-
-
- ) : (
- options.map(option => {
- const selected = isSelected(option);
- return (
-
toggle(option)}
- >
-
- {selected && (
-
- ✓
-
- )}
-
- );
- })
- )}
-
- );
+
+ {value.length > 0 ? `${value.length} selected` : placeholder}
+
+
+
+ {isOpen &&
+ (!portal || menuPos) &&
+ (() => {
+ const menuContent = (
+
e.stopPropagation() : undefined}
+ >
+ {options.length === 0 ? (
+
+
+ {emptyState}
+
+
+ ) : (
+ options.map(option => {
+ const selected = isSelected(option);
+ return (
+
toggle(option)}
+ >
+
+ {selected && (
+
+ ✓
+
+ )}
+
+ );
+ })
+ )}
+
+ );
- return portal && typeof document !== 'undefined'
- ? createPortal(
-
{menuContent}
,
- document.body,
- )
- : menuContent;
- })()}
+ return portal && typeof document !== 'undefined'
+ ? createPortal(
+
{menuContent}
,
+ document.body,
+ )
+ : menuContent;
+ })()}
+
{value.length > 0 && (
diff --git a/src/components/library/molecules/VideoCard/VideoCard.module.scss b/src/components/library/molecules/VideoCard/VideoCard.module.scss
index b1e763e7..c7594f3a 100644
--- a/src/components/library/molecules/VideoCard/VideoCard.module.scss
+++ b/src/components/library/molecules/VideoCard/VideoCard.module.scss
@@ -3,12 +3,14 @@
box-sizing: border-box;
display: flex;
flex-direction: column;
- width: 318px;
+ width: 255px;
height: 183px;
padding: 12px;
background: var(--off-white);
border-radius: 0 0 20px 20px;
- box-shadow: var(--card-shadow);
+ box-shadow:
+ 0px 6px 16px 0px #0000001a,
+ 0px 16px 40px 0px #00000014;
cursor: pointer;
border: none;
outline: none;
@@ -17,7 +19,8 @@
&:focus-visible {
box-shadow:
0 0 0 2px var(--brown),
- var(--card-shadow);
+ 0px 6px 16px 0px #0000001a,
+ 0px 16px 40px 0px #00000014;
}
}
@@ -47,9 +50,10 @@
.thumbWrap {
position: relative;
+ // 231×125 cover: full inner width (255 − 24 padding) at a fixed thumbnail height.
width: 100%;
- flex: 1 1 auto;
- min-height: 0;
+ height: 125px;
+ flex: 0 0 auto;
}
.thumb {
@@ -105,6 +109,10 @@
padding: 0;
border-radius: 0;
+ .thumbWrap {
+ height: 100%;
+ }
+
.bar,
.title {
display: none;
diff --git a/src/components/library/organisms/AddObjectModal/AddObjectModal.module.scss b/src/components/library/organisms/AddObjectModal/AddObjectModal.module.scss
index 6068c51e..634e324f 100644
--- a/src/components/library/organisms/AddObjectModal/AddObjectModal.module.scss
+++ b/src/components/library/organisms/AddObjectModal/AddObjectModal.module.scss
@@ -11,6 +11,7 @@
.indicatorWrap {
padding: 20px 32px;
+ background: #fffbf4;
border-top: 1px solid var(--brown-border);
border-bottom: 1px solid var(--brown-border);
}
diff --git a/src/components/library/organisms/AddObjectModal/AddObjectModal.tsx b/src/components/library/organisms/AddObjectModal/AddObjectModal.tsx
index dd66f396..6df3e6b1 100644
--- a/src/components/library/organisms/AddObjectModal/AddObjectModal.tsx
+++ b/src/components/library/organisms/AddObjectModal/AddObjectModal.tsx
@@ -257,7 +257,24 @@ export function AddObjectModal(props: AddObjectModalProps): JSX.Element {
size: number;
} | null = null;
if (data.coverImage instanceof File) {
- const uploaded = await uploadFile(data.coverImage);
+ let uploaded: Awaited>;
+ try {
+ uploaded = await uploadFile(data.coverImage);
+ } catch (uploadErr) {
+ // The image goes straight to Strapi. When it exceeds the server's
+ // upload limit the request is rejected with 413 — or dropped by the
+ // proxy so axios sees a bare network error (no response). Either way,
+ // surface a clear size message instead of the generic save error.
+ const status = (uploadErr as { response?: { status?: number } })
+ ?.response?.status;
+ const hasResponse = !!(uploadErr as { response?: unknown })?.response;
+ setSubmitError(
+ status === 413 || !hasResponse
+ ? 'Image is too large. Maximum size is 5 MB.'
+ : "Couldn't upload the image. Please try again.",
+ );
+ return;
+ }
coverImageId = uploaded.id;
uploadedCover = uploaded;
} else if (editing) {
diff --git a/src/components/library/organisms/EditLibraryModal/EditLibraryModal.tsx b/src/components/library/organisms/EditLibraryModal/EditLibraryModal.tsx
index 1edb5087..049f4b75 100644
--- a/src/components/library/organisms/EditLibraryModal/EditLibraryModal.tsx
+++ b/src/components/library/organisms/EditLibraryModal/EditLibraryModal.tsx
@@ -149,7 +149,23 @@ export function EditLibraryModal(props: EditLibraryModalProps): JSX.Element {
// 1. Upload avatar first if a new file was picked — we need the id for the PUT.
let uploadedAvatarId: number | null | undefined;
if (avatarFile) {
- const uploaded = await uploadFile(avatarFile);
+ let uploaded: Awaited>;
+ try {
+ uploaded = await uploadFile(avatarFile);
+ } catch (uploadErr) {
+ // Avatar uploads straight to Strapi; an over-limit image returns 413
+ // (or is dropped by the proxy, surfacing as a bare network error).
+ // Show a size message on the avatar field instead of a generic error.
+ const status = (uploadErr as { response?: { status?: number } })
+ ?.response?.status;
+ const hasResponse = !!(uploadErr as { response?: unknown })?.response;
+ setAvatarError(
+ status === 413 || !hasResponse
+ ? 'Avatar must be 5 MB or smaller.'
+ : "Couldn't upload the avatar. Please try again.",
+ );
+ return;
+ }
uploadedAvatarId = uploaded.id;
} else if (avatarRemoved) {
uploadedAvatarId = null;
diff --git a/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx b/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx
index 9805ba7f..cb0d5055 100644
--- a/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx
+++ b/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx
@@ -276,9 +276,16 @@ export function ObjectOverviewModal(
);
const tagsList = attributes.tags?.data ?? [];
const shelfData = attributes.shelf?.data;
+ // Prefer the live shelf from GlobalState (matched by id) so a rename reflects
+ // instantly — the object's embedded `shelf.data` is frozen at fetch time.
+ const liveShelf = currentShelves.find(s => s.id === currentShelfId);
const shelfDisplayName =
- shelfData?.attributes.name ?? attributes.shelfName ?? '—';
- const shelfPosition = shelfData?.attributes.order;
+ liveShelf?.attributes.name ??
+ shelfData?.attributes.name ??
+ attributes.shelfName ??
+ '—';
+ const shelfPosition =
+ liveShelf?.attributes.order ?? shelfData?.attributes.order;
const publishedFormatted = formatDate(attributes.publicationDate);
const sourceLabel =
attributes.source && attributes.source.length > 0 ? attributes.source : '—';
diff --git a/src/components/library/organisms/Shelf/Shelf.module.scss b/src/components/library/organisms/Shelf/Shelf.module.scss
index d45065d9..488e622c 100644
--- a/src/components/library/organisms/Shelf/Shelf.module.scss
+++ b/src/components/library/organisms/Shelf/Shelf.module.scss
@@ -88,29 +88,29 @@
align-items: center;
position: relative;
overflow: hidden;
+ // Raises the bottom of the scroll row (and so the overflow scrollbar) up
+ // onto the visible shelf board, out of the board PNG's transparent lower
+ // zone. The absolute `.banner` (bottom: 0) is unaffected, and `.items`
+ // drops the same amount from its own padding-bottom so the cards stay
+ // seated. This is the knob: increase to slide the scrollbar up the board.
+ padding-bottom: 65px;
.banner {
width: -webkit-fill-available;
position: absolute;
bottom: 0;
margin-right: -32px;
+ // Sit the shelf board behind the cards so each object rests fully opaque
+ // *on* the shelf instead of bleeding through the board's translucent top
+ // edge. Cards are z-index 1 (see .items).
+ z-index: 0;
+ pointer-events: none;
img {
width: inherit;
}
}
- .arrow {
- width: 46px;
- height: 46px;
- flex-shrink: 0;
- z-index: 2;
- }
-
- .arrowLeft svg {
- transform: rotate(180deg);
- }
-
.items {
flex: 1;
min-width: 0;
@@ -121,7 +121,9 @@
// Give the cards 40px of headroom so their tops aren't clipped by the
// shelf's `overflow: hidden`; they still bottom-align onto the shelf.
padding-top: 40px;
- padding-bottom: 24px;
+ // Paired with `.content`'s 65px padding-bottom: the two sum to 100px so
+ // the cards' seating line is unchanged while the scrollbar rides higher.
+ padding-bottom: 35px;
position: relative;
z-index: 1;
overflow-x: auto;
@@ -130,6 +132,7 @@
// Arrows (and trackpad/touch swipe) drive navigation — hide the native
// scrollbar so it doesn't cut across the shelf artwork.
scrollbar-width: none;
+ padding-left: 58px;
&::-webkit-scrollbar {
display: none;
@@ -139,7 +142,12 @@
// When the row overflows (arrows present), expose a styled horizontal
// scrollbar so the overflow is discoverable by drag, not just the arrows.
.scrollable {
- scrollbar-width: thin;
+ // Must be `auto`, not `thin`: when scrollbar-width is thin/none, Chrome
+ // 121+ renders the standard scrollbar and IGNORES the ::-webkit-scrollbar
+ // rules below — so the custom 12px #c0b6ae bar never showed. `auto` keeps
+ // the webkit pseudo-element styling active. (Overrides `.items`' `none`.)
+ scrollbar-width: auto;
+ // Firefox can't honor the 12px height, but still gets the tint.
scrollbar-color: #c0b6ae transparent;
&::-webkit-scrollbar {
diff --git a/src/components/library/organisms/Shelf/Shelf.tsx b/src/components/library/organisms/Shelf/Shelf.tsx
index f3d1f812..ba6d8be9 100644
--- a/src/components/library/organisms/Shelf/Shelf.tsx
+++ b/src/components/library/organisms/Shelf/Shelf.tsx
@@ -20,7 +20,6 @@ import { updateShelf } from '@api/library/shelf/updateShelf';
import shelfBackground from '@icons/library/images/shelfBackground.png';
import {
- ArrowIcon,
AudioIcon,
BookIcon,
PlusIcon,
@@ -196,9 +195,9 @@ export function Shelf(props: ShelfProps): JSX.Element {
const [renameLoading, setRenameLoading] = useState(false);
const [renameError, setRenameError] = useState(null);
- // Horizontal scroller: keep every card on one row and page through them with
- // the arrows. Arrows only show when the row actually overflows; each click
- // advances by one card width (+ the 24px gap).
+ // Horizontal scroller: keep every card on one row. When the row overflows we
+ // expose the styled scrollbar (`.scrollable`) so the overflow is discoverable
+ // by drag/swipe.
const itemsRef = useRef(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
@@ -224,14 +223,6 @@ export function Shelf(props: ShelfProps): JSX.Element {
};
}, [syncScrollState, objects.length]);
- const scrollByCard = (direction: -1 | 1) => {
- const el = itemsRef.current;
- if (!el) return;
- const firstCard = el.querySelector(`.${styles.cards} > *`);
- const step = firstCard ? firstCard.offsetWidth + 35 : el.clientWidth * 0.8;
- el.scrollBy({ left: direction * step, behavior: 'smooth' });
- };
-
const isOverflowing = canScrollLeft || canScrollRight;
const closeRename = useCallback(() => {
@@ -448,16 +439,6 @@ export function Shelf(props: ShelfProps): JSX.Element {
- {isOverflowing && (
-
scrollByCard(-1)}
- type={ButtonType.Secondary}
- Icon={ }
- ariaLabel="Previous"
- disabled={!canScrollLeft}
- />
- )}
)}
- {isOverflowing && (
- scrollByCard(1)}
- type={ButtonType.Secondary}
- Icon={ }
- ariaLabel="Next"
- disabled={!canScrollRight}
- />
- )}
-
diff --git a/src/layouts/library/Library/Library.tsx b/src/layouts/library/Library/Library.tsx
index 29596a9b..8a1acbee 100644
--- a/src/layouts/library/Library/Library.tsx
+++ b/src/layouts/library/Library/Library.tsx
@@ -57,6 +57,7 @@ export function LibraryTemplate({ libraryId }: LibraryTemplateProps) {
const resequenceTimer = useRef | null>(null);
const { accountData } = useAuth();
const {
+ isGuestMode,
setCurrentShelves,
setCurrentOwner,
setCurrentLibrary,
@@ -83,6 +84,12 @@ export function LibraryTemplate({ libraryId }: LibraryTemplateProps) {
((!!ownerUsername && ownerUsername.toLowerCase() === myUsername) ||
(!!libraryId && libraryId.toLowerCase() === myUsername));
+ // Guest mode lets an owner preview their library exactly as a public visitor
+ // sees it. Owner-only data logic (library bootstrap, create-permission gating)
+ // still keys off the real `isOwner`; everything user-facing — edit/add UI and
+ // private-shelf visibility — keys off this so the preview is faithful.
+ const viewAsOwner = isOwner && !isGuestMode;
+
// Creating a library is gated by the `can-create-library` feature flag from
// GET /api/users/me. The gate only matters before a library exists — once one
// is created, owners keep full control. Decide at render time from the flag,
@@ -237,13 +244,13 @@ export function LibraryTemplate({ libraryId }: LibraryTemplateProps) {
const data = library?.attributes.singleShelves?.data ?? [];
// Private shelves are owner-only — hide them from visitors so the
// public/private toggle actually controls who can see a shelf.
- const visible = isOwner
+ const visible = viewAsOwner
? data
: data.filter(s => s.attributes.visibility !== 'private');
return [...visible].sort(
(a, b) => (a.attributes.order ?? 0) - (b.attributes.order ?? 0),
);
- }, [library, isOwner]);
+ }, [library, viewAsOwner]);
// Publish the current library's shelves so the Header's Jump-to nav can
// render the right list without owning its own fetch. NOTE: no cleanup —
@@ -473,7 +480,7 @@ export function LibraryTemplate({ libraryId }: LibraryTemplateProps) {
return (
- {isOwner && shelves.length > 0 && (
+ {viewAsOwner && shelves.length > 0 && (
- Begin your journey by adding your first shelf
+ {viewAsOwner
+ ? 'Begin your journey by adding your first shelf'
+ : 'This library is empty'}
-
+ {viewAsOwner && (
+
+ )}
) : (
)}
- {isOwner && (
+ {viewAsOwner && (