+ );
+}
\ No newline at end of file
diff --git a/client/src/components/BookCard/BookCard.tsx b/client/src/components/BookCard/BookCard.tsx
index 6461e67..decd191 100644
--- a/client/src/components/BookCard/BookCard.tsx
+++ b/client/src/components/BookCard/BookCard.tsx
@@ -2,6 +2,8 @@ import { Link } from 'react-router-dom';
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';
/* ---------- Types: the contract with BookGrid and the pages ---------- */
type BookCardProps = {
@@ -66,16 +68,7 @@ function StarRow({ value }: { value: number }) {
// One image for screen readers, so the five SVGs are hidden
{[1, 2, 3, 4, 5].map((n) => (
-
+
))}
);
@@ -99,9 +92,7 @@ function FavoriteRibbon({
aria-label={`Save "${title}" to My shelf`} // ...so the label stays constant
onClick={onToggle}
>
-
+
);
}
diff --git a/client/src/components/BookCover/BookCover.tsx b/client/src/components/BookCover/BookCover.tsx
index a9f2ef7..dda4490 100644
--- a/client/src/components/BookCover/BookCover.tsx
+++ b/client/src/components/BookCover/BookCover.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
import styles from './BookCover.module.css';
+import { SparkleIcon } from '../../icons/SparkleIcon';
const TONE_COUNT = 6;
@@ -39,12 +40,7 @@ export function BookCover({ id, title, author, coverUrl, eager = false }: Props)
aria-hidden="true"
>
-
+ {title}
{author && {author}}
diff --git a/client/src/components/Button/Button.module.css b/client/src/components/Button/Button.module.css
new file mode 100644
index 0000000..0fad132
--- /dev/null
+++ b/client/src/components/Button/Button.module.css
@@ -0,0 +1,38 @@
+.button {
+ height: 44px;
+ border-radius: 12px;
+ border: 1.5px solid transparent;
+ font-size: 15px;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.primary {
+ border-color: var(--color-focus);
+ background: transparent;
+ color: var(--color-focus);
+}
+
+.secondary {
+ border-color: var(--color-accent);
+ background: var(--color-accent);
+ color: var(--color-on-accent);
+}
+
+.narrow {
+ padding: 0 20px;
+}
+
+.wide {
+ padding: 0 40px;
+}
+
+.button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.button:focus-visible {
+ outline: 2px solid var(--color-focus);
+ outline-offset: 3px;
+}
\ No newline at end of file
diff --git a/client/src/components/Button/Button.tsx b/client/src/components/Button/Button.tsx
new file mode 100644
index 0000000..711a58f
--- /dev/null
+++ b/client/src/components/Button/Button.tsx
@@ -0,0 +1,30 @@
+import styles from './Button.module.css';
+
+type ButtonProps = {
+ children: React.ReactNode;
+ variant?: "primary" | "secondary";
+ size?: "narrow" | "wide";
+ type?: "button" | "submit";
+ onClick?: () => void;
+ disabled?: boolean;
+};
+
+export function Button({
+ children,
+ variant = "primary",
+ size = "narrow",
+ type = "button",
+ onClick,
+ disabled = false,
+}: ButtonProps) {
+ return (
+
+ );
+}
diff --git a/client/src/components/SearchField/SearchField.tsx b/client/src/components/SearchField/SearchField.tsx
index 8ae089e..e1f0f96 100644
--- a/client/src/components/SearchField/SearchField.tsx
+++ b/client/src/components/SearchField/SearchField.tsx
@@ -1,5 +1,6 @@
import { useId, useState } from 'react';
import styles from './SearchField.module.css';
+import { SearchIcon } from '../../icons/SearchIcons';
/* ---------- Props ---------- */
type Props = {
@@ -44,19 +45,7 @@ export function SearchField({ value, onSearch }: Props) {
{/* Icon-only button, so it needs an aria-label. */}
);
diff --git a/client/src/components/StarRatingInput/StarRatingInput.module.css b/client/src/components/StarRatingInput/StarRatingInput.module.css
new file mode 100644
index 0000000..203716e
--- /dev/null
+++ b/client/src/components/StarRatingInput/StarRatingInput.module.css
@@ -0,0 +1,22 @@
+/* Row of buttons; the negative margin lines the first star up with the text above it */
+.group {
+ display: flex;
+ margin-left: -8px;
+}
+
+/* 44x44 keeps the tap target large enough */
+.starButton {
+ width: 44px;
+ height: 44px;
+ background: none;
+ border: 0;
+ padding: 0;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Star colors (same values as in BookCard.module.css) */
+.starOn { fill: var(--color-star); }
+.starOff { fill: var(--color-star-off); }
\ No newline at end of file
diff --git a/client/src/components/StarRatingInput/StarRatingInput.tsx b/client/src/components/StarRatingInput/StarRatingInput.tsx
new file mode 100644
index 0000000..b658b14
--- /dev/null
+++ b/client/src/components/StarRatingInput/StarRatingInput.tsx
@@ -0,0 +1,37 @@
+import { StarIcon } from '../../icons/StarIcon';
+import styles from './StarRatingInput.module.css';
+
+/* ---------- Use ------------ */
+/*
+
+*/
+
+type StarRatingInputProps = {
+ value?: number; // current rating 1β5; undefined = not rated yet
+ onChange: (rating: number) => void;
+};
+
+/* ---------- StarRatingInput: editable rating (details page) ---------- */
+export function StarRatingInput({ value, onChange }: StarRatingInputProps) {
+ return (
+
+ {[1, 2, 3, 4, 5].map((n) => (
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/BrowseIcon.tsx b/client/src/icons/BrowseIcon.tsx
new file mode 100644
index 0000000..8d6c212
--- /dev/null
+++ b/client/src/icons/BrowseIcon.tsx
@@ -0,0 +1,21 @@
+/* ---------- Props ---------- */
+type Props = { size?: number };
+
+/* ---------- BrowseIcon: open book (nav tab for Browse) ---------- */
+export function BrowseIcon({ size = 22 }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/ChevronIcon.tsx b/client/src/icons/ChevronIcon.tsx
new file mode 100644
index 0000000..8ecc5a9
--- /dev/null
+++ b/client/src/icons/ChevronIcon.tsx
@@ -0,0 +1,20 @@
+type Props = { size?: number };
+
+/* ---------- ChevronIcon: left chevron (Back to results) ---------- */
+export function ChevronIcon({ size = 20 }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/FilterIcon.tsx b/client/src/icons/FilterIcon.tsx
new file mode 100644
index 0000000..b1f6c90
--- /dev/null
+++ b/client/src/icons/FilterIcon.tsx
@@ -0,0 +1,19 @@
+type Props = { size?: number };
+
+/* ---------- FilterIcon: three lines of decreasing length (mobile Filters button) ---------- */
+export function FilterIcon({ size = 16 }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/LogoIcon.tsx b/client/src/icons/LogoIcon.tsx
new file mode 100644
index 0000000..2134ed7
--- /dev/null
+++ b/client/src/icons/LogoIcon.tsx
@@ -0,0 +1,31 @@
+type Props = { size?: number };
+
+/* ---------- LogoIcon: open book with a gold sparkle (header logo) ---------- */
+export function LogoIcon({ size = 30 }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/RibbonIcon.tsx b/client/src/icons/RibbonIcon.tsx
new file mode 100644
index 0000000..de0e5bb
--- /dev/null
+++ b/client/src/icons/RibbonIcon.tsx
@@ -0,0 +1,10 @@
+type Props = { width?: number; height?: number };
+
+/* ---------- RibbonIcon: bookmark ribbon, fill/stroke are set by the parent's CSS ---------- */
+export function RibbonIcon({ width = 24, height = 40 }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/SearchIcons.tsx b/client/src/icons/SearchIcons.tsx
new file mode 100644
index 0000000..415c23f
--- /dev/null
+++ b/client/src/icons/SearchIcons.tsx
@@ -0,0 +1,20 @@
+type Props = { size?: number };
+
+/* ---------- SearchIcon: magnifying glass (search button) ---------- */
+export function SearchIcon({ size = 18 }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/ShelfIcon.tsx b/client/src/icons/ShelfIcon.tsx
new file mode 100644
index 0000000..ddce6c6
--- /dev/null
+++ b/client/src/icons/ShelfIcon.tsx
@@ -0,0 +1,19 @@
+type Props = { size?: number };
+
+/* ---------- ShelfIcon: bookmark (nav tab for My shelf) ---------- */
+export function ShelfIcon({ size = 22 }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/SparkleIcon.tsx b/client/src/icons/SparkleIcon.tsx
new file mode 100644
index 0000000..8476bb6
--- /dev/null
+++ b/client/src/icons/SparkleIcon.tsx
@@ -0,0 +1,21 @@
+/* ---------- SparkleIcon.tsx ---------- */
+type Props = { size?: number; className?: string };
+
+/* ---------- SparkleIcon: four-pointed sparkle, color follows the parent's text color ---------- */
+// size is optional: leave it out when CSS (className) sets the width/height
+export function SparkleIcon({ size, className }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/icons/StarIcon.tsx b/client/src/icons/StarIcon.tsx
new file mode 100644
index 0000000..30ec101
--- /dev/null
+++ b/client/src/icons/StarIcon.tsx
@@ -0,0 +1,15 @@
+type Props = { size?: number; className?: string };
+/* ---------- StarIcon: rating star, color comes from the className's CSS (fill) ---------- */
+export function StarIcon({ size = 15, className }: Props) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/main.tsx b/client/src/main.tsx
index bef5202..d68fac9 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -1,6 +1,6 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
-import './index.css'
+import './styles/index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
diff --git a/client/src/styles/tokens.css b/client/src/styles/tokens.css
index ed40073..f04842e 100644
--- a/client/src/styles/tokens.css
+++ b/client/src/styles/tokens.css
@@ -7,22 +7,22 @@
--color-input-border: #8a6f7c;
--color-header: #4a1f4f;
--color-header-text: #fbeef3;
+ --color-header-tint: #f3b2cb;
+ --color-header-soft: #ffd8e6;
--color-accent: #a3264f;
+ --color-accent-hover: #8a1f42;
+ --color-on-accent: #ffffff;
--color-ribbon: #c2810f;
--color-highlight: #eeb34c;
--color-star: #c78a12;
+ --color-star-off: #ead9d6;
--color-chip-bg: #f8bbd2;
--color-chip-text: #7a1f45;
--color-shelf: #7b5535;
--color-shelf-edge: #9a7050;
- --color-header-tint: #f3b2cb;
- --color-header-soft: #ffd8e6;
-
- /* TODO:check contrast */
- --color-on-accent: #ffffff; /* text on accent buttons */
- --color-accent-hover: #8a1f42; /* darker accent for hover */
- --color-focus: #4a1f4f; /* focus ring on light areas; use --color-highlight inside the header */
- --color-danger: #b3261e; /* destructive actions, e.g. delete review */
+ --color-focus: #4a1f4f;
+ --color-danger: #b3261e;
+
/* Generated book cover tones (bg / text pairs) */
--cover-0-bg: #6a2a5e;
diff --git a/docs/decisions-and-contracts.md b/docs/decisions-and-contracts.md
index d05477a..807f4a6 100644
--- a/docs/decisions-and-contracts.md
+++ b/docs/decisions-and-contracts.md
@@ -1,8 +1,9 @@
# Decisions and contracts
-v1: created by Ilse, 29.09.2026
+[Mockup](bookish-mockup.html) (light mode)
-Mockup: `docs/bookish-mockups.html/` (light mode)
+Mockup is a pure HTML file - open in a browser to render visually.
+**Note:** The mockup was created before we started building the app, so the real design might change during the project, and the mockup might not get updated when this happens.
## The contract between backend and frontend
@@ -89,4 +90,5 @@ UserID: Client only knows its own ID. Server stores IDs along with shelf entries
## Changelog
- v1 (29.09.2026): created by Ilse.
-- v2 (date): `Book` type aligned with the backend model. CSS approach decided. Added decisions 12 and 13, the routes table, and more open questions.
\ No newline at end of file
+- v2 (01.10.2026): `Book` type aligned with the backend model. CSS approach decided. Added decisions 12 and 13, the routes table, and more open questions.
+- v3 (02.10.2026): Added disclaimer about real design possibly diverging from initial design mockup.
\ No newline at end of file
diff --git a/docs/project-checklist.md b/docs/project-checklist.md
index 3173737..bac14f8 100644
--- a/docs/project-checklist.md
+++ b/docs/project-checklist.md
@@ -1,6 +1,6 @@
# Project checklist: things to keep in mind
-Drafted 2026-09-29 from the Project 2 description, the lectures and the Project 1 peer review. "Suggested owner" is a proposal for the group to change. Deadlines are on Canvas and are not in this file.
+Drafted 2026-09-29 from the Project 2 description, the lectures and the Project 1 peer review. This document was created in the planning phase and might not get updated regularly.
Roles: **A** Data, **B** API, **C** Browse + Shelf, **D** Design + Detail. See `roles.md`.
diff --git a/docs/styles-and-components.md b/docs/styles-and-components.md
new file mode 100644
index 0000000..f30ea2c
--- /dev/null
+++ b/docs/styles-and-components.md
@@ -0,0 +1,75 @@
+# Rules and information regarding styles, design and components
+
+## Folder structure
+```
+src/
+βββ styles/ tokens.css, reset.css, base.css, index.css
+βββ icons/ Logo, Browse, Shelf, Search
+βββ components/
+β βββ Button/ 3 variants used throughout app
+β βββ BookCover/ image, fallback, color pick
+β βββ BookCard/ BookCard + ribbon + starsdisplay
+β βββ BookGrid/ render auto grid of BookCard, with shelf option
+β βββ SearchField/ draft state, submit on Enter or button
+β βββ FilterPanel/ genre, year, clear, mobile toggle
+β βββ RatingInput/ radio group styled as stars
+β βββ MyNotesPanel/ empty / saved / editing
+β βββ StateMessage/ empty, error, (loading)
+β βββ AppShell/ shell + header + nav + footer
+βββ pages/
+ βββ BrowsePage/ includes "Load more" + results count
+ βββ BookDetailPage/ includes hero, facts, description, ribbon, tags
+ βββ ShelfPage/ includes page heading + sort
+```
+
+- Global CSS holds only tokens and element defaults.
+- Every component owns its own `.module.css` and reads only `var(--token)` values, never raw hex.
+
+## Global CSS
+|File|Purpose|Rules|
+|----|-------|-----|
+|index.css|Imports the three below in order (tokens, reset, base)|No rules of its own. Order matters because base uses tokens|
+|tokens.css|Design decisions, as CSS variables|Only :root{--...}, with no selectors or layout. The only place for raw hex/px values. Name by role (`--color-accent`), not appearance (`--color-purple`). Add a token only when it's a real decision or reused. Keep it tidy and readable!|
+|reset.css|Removes browser inconsistencies (box-sixing, margins, image behavior)|Tiny and generic. No colors or fonts, since "brand" values belong to tokens|
+|base.css|Default look of plain HTML elements: body, headings, links, focus ring, `.visually-hidden`|Element selectors plus a few utilities only. If you write a specific class here, it belongs in a component module instead!|
+
+
+## AppShell
+**AppShell is in** (`components/AppShell/`). It's the frame around every page: header (logo, search, nav), the main area, and the footer.
+
+**For anyone building a page:**
+- **Don't render a ``, a header, a footer, or a search field in your page.** AppShell renders all of them, and your page appears inside its ``.
+
+- **Don't set a page width or side padding.** AppShell owns a constant content width, so your page fills the space it's given. Vertical spacing inside the page is yours.
+
+- **Start your page with an `
`.**
+
+- **Routes:** AppShell is a layout route, so every page goes in as a child of it in `App.tsx` (`/`, `/shelf`, `/book/:id`). The current `App.tsx` has temporary placeholder pages you can swap out.
+
+- **On mobile, the nav is a fixed tab bar at the bottom.** The shell already leaves room for it, so you don't need to.
+
+## Search
+
+- The search field is in the header on every page. On submit it navigates to `/?q=`. An empty search goes to plain `/`.
+
+- Browse reads `q` from the URL with `useSearchParams`. There's no prop and no context. **The URL is the shared state.**
+
+- A new search drops the other params (genres, years, sort), so it starts fresh.
+
+- **Browse** is responsible for restarting the paging when `q` changes.
+
+- `SearchField` resets its text to match `q` when it changes (Back button, clearing), so the field always matches the URL.
+
+- **Param names must match exactly.** We have `genre` in one place and `genres` in another, so let's pick one.
+
+- When your filter panel updates the URL, keep `q` and the other params. Setting params naively replaces them all.
+
+
+
+## Icons
+Every SVG now lives in `src/icons/` as a small component (``, optional `size` prop). Please add new icons there instead of inline in components. They use `currentColor`, so you color them with CSS.
+
+## Changelog
+
+- v1 (29.09.2026): created by Ilse
+- v2 (02.10.2026): Added information about AppShell, Search and Icons. Updated folder structure to reflect relevant changes.
\ No newline at end of file
diff --git a/docs/styles_documentation.md b/docs/styles_documentation.md
deleted file mode 100644
index c6f6acd..0000000
--- a/docs/styles_documentation.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Rules and information regarding styles, design, components
-
-## 1. Folder structure
-```
-src/
-βββ styles/ tokens.css, reset.css, base.css, index.css
-βββ components/
-β βββ Button/ 3 variants used throughout app
-β βββ BookCover/ image, fallback, color pick
-β βββ BookCard/ BookCard + ribbon + starsdisplay
-β βββ BookGrid/ render auto grid of BookCard, with shelf option
-β βββ SearchField/ draft state, submit on Enter or button
-β βββ FilterPanel/ genre, year, clear, mobile toggle
-β βββ RatingInput/ radio group styled as stars
-β βββ MyNotesPanel/ empty / saved / editing
-β βββ StateMessage/ empty, error, (loading)
-β βββ AppShell/ shell + header + nav + footer
-βββ pages/
- βββ BrowsePage/ includes "Load more" + results count
- βββ BookDetailPage/ includes hero, facts, description, ribbon, tags
- βββ ShelfPage/ includes page heading + sort
-```
-
-- Global CSS holds only tokens and element defaults.
-- Every component owns its own `.module.css` and reads only `var(--token)` values, never raw hex.
-
-## Global CSS
-|File|Purpose|Rules|
-|----|-------|-----|
-|index.css|Imports the three below in order (tokens, reset, base)|No rules of its own. Order matters because base uses tokens|
-|tokens.css|Design decisions, as CSS variables|Only :root{--...}, with no selectors or layout. The only place for raw hex/px values. Name by role (`--color-accent`), not appearance (`--color-purple`). Add a token only when it's a real decision or reused. Keep it tidy and readable!|
-|reset.css|Removes browser inconsistencies (box-sixing, margins, image behavior)|Tiny and generic. No colors or fonts, since "brand" values belong to tokens|
-|base.css|Default look of plain HTML elements: body, headings, links, focus ring, `.visually-hidden`|Element selectors plus a few utilities only. If you write a specific class here, it belongs in a component module instead!|