Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@ ESLint finner feil i koden, mens Prettier bestemmer formateringen.
ESLint-regler som ellers ville kollidert med Prettier. Byggfiler,
avhengigheter og `package-lock.json` formateres ikke.

## Mappestruktur

```
src/
types/ Felles datatyper, for eksempel Listing
data/ Mockdata. Byttes med GraphQL-backend senere
utils/ Formatering av pris og etiketter
assets/ Illustrasjoner til eksempelannonsene
styles/ Designvariabler og grunnstiler
components/ Gjenbrukbare komponenter, én mappe per komponent
design/ Designreferanse, bare under utvikling
```

Komponenter i `components/` får data gjennom props og importerer ikke
`data/` direkte. Da kan datakilden byttes uten at visningene må skrives om.

## Design

Felles farger, typografi, avstander og komponenter er beskrevet i
[docs/design.md](docs/design.md). Under utvikling viser
<http://localhost:5173/design.html> en designreferanse med eksempler. Den er
ikke med i produksjonsbygget.

## Datamodell og mockdata

Alle annonser følger typen `Listing` i `src/types/listing.ts`:
Expand Down
17 changes: 17 additions & 0 deletions design.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<!--
Designreferanse som bare brukes under utvikling. Vite serverer den på
/design.html med npm run dev, men den er ikke med i npm run build.
-->
<html lang="nb">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>Designreferanse – Verktøyutlån Trondheim</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/design/main.tsx"></script>
</body>
</html>
120 changes: 120 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Design

Felles visuelt grunnlag for Verktøyutlån Trondheim. Målet er et rolig og
tydelig uttrykk der pris, gratis utlån og område er lett å se.

## Designreferanse

Kjør `npm run dev` og åpne <http://localhost:5173/design.html>. Siden viser
farger, typografi, avstander, prismerking og annonsekort med eksempeldata.
Den finnes bare under utvikling og er ikke med i `npm run build`.

## Retning

- Lys, varm bakgrunn, hvite flater og nesten svart tekst.
- Én dempet grønn aksentfarge for hovedhandlinger, lenker og fokus.
- Diskrete rammer, 8 px avrunding og god luft.
- Systemfont for rask lasting og kjent utseende.
- Status og pris vises alltid med tekst, ikke bare med farge.

## Farger

Variablene ligger i `src/styles/tokens.css`. Kontrasten er regnet ut etter
WCAG 2.1. Krav: 4,5:1 for vanlig tekst og 3:1 for rammer på skjemafelt og
fokusmarkering.

| Variabel | Verdi | Bruk |
| ----------------------- | --------- | ----------------------------- |
| `--color-bg` | `#faf8f4` | Sidebakgrunn |
| `--color-surface` | `#ffffff` | Kort og felt |
| `--color-surface-muted` | `#f1ede5` | Prismerke og bildeplassholder |
| `--color-text` | `#1f2421` | Brødtekst og overskrifter |
| `--color-text-muted` | `#5b625d` | Sekundær tekst |
| `--color-border` | `#ddd8ce` | Dekorative rammer |
| `--color-border-strong` | `#767c78` | Rammer på skjemafelt |
| `--color-accent` | `#2f6b4f` | Hovedknapp, lenker og fokus |
| `--color-accent-hover` | `#24543e` | Hover på knapp og lenke |
| `--color-on-accent` | `#ffffff` | Tekst på aksentfarge |
| `--color-free-bg` | `#e3f0e8` | Gratismerke, bakgrunn |
| `--color-free-text` | `#1e5239` | Gratismerke, tekst |
| `--color-error` | `#b3261e` | Feilmeldinger |

Kontrollerte fargepar:

| Forgrunn | Bakgrunn | Kontrast | Krav |
| ----------------------- | ----------------------- | -------- | ----- |
| `--color-text` | `--color-bg` | 14,9:1 | 4,5:1 |
| `--color-text-muted` | `--color-bg` | 5,9:1 | 4,5:1 |
| `--color-text-muted` | `--color-surface-muted` | 5,4:1 | 4,5:1 |
| `--color-on-accent` | `--color-accent` | 6,3:1 | 4,5:1 |
| `--color-on-accent` | `--color-accent-hover` | 8,7:1 | 4,5:1 |
| `--color-accent` | `--color-bg` | 5,9:1 | 4,5:1 |
| `--color-free-text` | `--color-free-bg` | 7,7:1 | 4,5:1 |
| `--color-text` | `--color-surface-muted` | 13,5:1 | 4,5:1 |
| `--color-error` | `--color-surface` | 6,5:1 | 4,5:1 |
| `--color-border-strong` | `--color-surface` | 4,3:1 | 3:1 |

`--color-border` har lav kontrast (1,3:1) og brukes bare til dekorative rammer,
der innholdet er forståelig uten rammen.

## Typografi

| Variabel | Størrelse | Bruk |
| ----------------- | --------- | -------------------------- |
| `--font-size-sm` | 14 px | Sekundær tekst og metadata |
| `--font-size-md` | 16 px | Brødtekst |
| `--font-size-lg` | 20 px | Korttitler, `h3` |
| `--font-size-xl` | 24 px | `h2` |
| `--font-size-2xl` | 32 px | `h1` |

Brødtekst har linjehøyde 1,5 og overskrifter 1,25. Fet skrift er vekt 600.

## Avstander, form og fokus

- Avstander går i steg på 4 px: `--space-1` (4 px) til `--space-12` (48 px).
Bruk `--space-4` (16 px) som vanlig sidemarg på mobil.
- `--radius-md` (8 px) for kort og felt, `--radius-pill` for merker.
- `--content-width` (72rem) er maks bredde på innholdet.
- `--tap-target` (44 px) er minste høyde for knapper og andre trykkflater.
- Fokus vises med `:focus-visible` som en 3 px grønn ring med 2 px avstand.
Ikke fjern `outline` uten å erstatte den med noe like tydelig.

## Komponenter

Komponentene får data gjennom props og bruker CSS-moduler, slik at klassenavn
ikke kolliderer mellom filer. Hver komponent har sin egen mappe med komponent,
stil og etter hvert tester:

```
src/components/PriceTag/
PriceTag.tsx
PriceTag.module.css
```

### PriceTag

Viser døgnpris, eller «Gratis utlån» når prisen er 0. Gratis utlån får grønn
bakgrunn, men teksten alene er nok til å skille dem.

```tsx
<PriceTag dailyPrice={0} /> // Gratis utlån
<PriceTag dailyPrice={150} /> // 150 kr/døgn
```

### ListingCard

Visuell referanse for et annonsekort med bilde, tittel, kategori, område og
pris. Når `imageUrl` er `null`, vises en plassholder med teksten «Ingen bilde».
Bildet har tom alt-tekst fordi tittelen rett under beskriver det samme.

```tsx
<ListingCard listing={listing} />
```

Tittelen er en `h2`. Katalogen kan gjøre tittelen til en lenke til
detaljvisningen når den finnes.

## Ikke laget ennå

- Knapp, skjemafelt, sidemal og navigasjon kommer i egen oppgave.
- Mørk modus er ikke planlagt for første innlevering.
48 changes: 48 additions & 0 deletions src/components/ListingCard/ListingCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.card {
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}

.image,
.imagePlaceholder {
width: 100%;
aspect-ratio: 4 / 3;
}

.image {
object-fit: cover;
}

.imagePlaceholder {
display: grid;
place-items: center;
background: var(--color-surface-muted);
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}

.body {
display: flex;
flex: 1;
flex-direction: column;
align-items: flex-start;
gap: var(--space-2);
padding: var(--space-4);
}

.title {
margin: 0;
font-size: var(--font-size-lg);
/* Lange titler brytes i stedet for å gi horisontal scrolling. */
overflow-wrap: anywhere;
}

.meta {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
41 changes: 41 additions & 0 deletions src/components/ListingCard/ListingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Listing } from '../../types/listing.ts';
import { categoryLabels } from '../../utils/listingFormat.ts';
import PriceTag from '../PriceTag/PriceTag.tsx';
import styles from './ListingCard.module.css';

interface ListingCardProps {
listing: Listing;
}

/**
* Visuell referanse for et annonsekort. Katalogen kan bygge videre på denne,
* for eksempel ved å gjøre tittelen til en lenke til detaljvisningen.
*/
function ListingCard({ listing }: ListingCardProps) {
return (
<article className={styles.card}>
{listing.imageUrl ? (
// Tom alt-tekst fordi tittelen rett under beskriver det samme.
<img
className={styles.image}
src={listing.imageUrl}
alt=""
width={400}
height={300}
loading="lazy"
/>
) : (
<div className={styles.imagePlaceholder}>Ingen bilde</div>
)}
<div className={styles.body}>
<h2 className={styles.title}>{listing.title}</h2>
<p className={styles.meta}>
{categoryLabels[listing.category]} · {listing.area}
</p>
<PriceTag dailyPrice={listing.dailyPrice} />
</div>
</article>
);
}

export default ListingCard;
19 changes: 19 additions & 0 deletions src/components/PriceTag/PriceTag.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.paid,
.free {
display: inline-block;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-pill);
font-weight: var(--font-weight-bold);
white-space: nowrap;
}

.paid {
background: var(--color-surface-muted);
color: var(--color-text);
}

/* Gratis utlån skilles med både tekst og farge, ikke bare farge. */
.free {
background: var(--color-free-bg);
color: var(--color-free-text);
}
19 changes: 19 additions & 0 deletions src/components/PriceTag/PriceTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { formatDailyPrice } from '../../utils/listingFormat.ts';
import styles from './PriceTag.module.css';

interface PriceTagProps {
/** Pris i hele kroner per døgn. 0 vises som «Gratis utlån». */
dailyPrice: number;
}

function PriceTag({ dailyPrice }: PriceTagProps) {
const isFree = dailyPrice === 0;

return (
<span className={isFree ? styles.free : styles.paid}>
{formatDailyPrice(dailyPrice)}
</span>
);
}

export default PriceTag;
79 changes: 79 additions & 0 deletions src/design/DesignPreview.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.page {
max-width: var(--content-width);
margin: 0 auto;
padding: var(--space-8) var(--space-4);
}

.lead {
max-width: 40rem;
color: var(--color-text-muted);
}

.section {
margin-top: var(--space-12);
}

.muted {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}

.sizeXl {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-tight);
}

.sizeLg {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
}

.swatches,
.spaces {
margin: 0;
padding: 0;
list-style: none;
}

.swatches {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
gap: var(--space-4);
}

.swatch {
display: flex;
flex-direction: column;
gap: var(--space-1);
}

.swatchColor {
height: 3rem;
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-md);
}

.space {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-2);
}

.spaceBar {
height: var(--space-4);
background: var(--color-accent);
}

.row {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
}

.cardGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: var(--space-6);
}
Loading