# Jesperhus prototype: UX and component specification

Status: implementation-ready prototype specification, 3 August 2026.

This document translates the product specification and the approved prototype scope into UI contracts. It is not an official brand guide. All schedules, opening hours, routes, walking times, accommodation details and operational statuses in the prototype are illustrative fixture data unless a source and retrieval time are shown.

## Experience principles

1. Lead with the next useful action, not a menu or content catalogue.
2. Keep Program, Open and My plan separate in data, labels and presentation.
3. Show time, place and action together whenever a guest must make a decision.
4. Use one clear primary action per card; keep secondary actions available but quiet.
5. Explain unavailable, stale and offline data in plain language and preserve a useful fallback.
6. Never imply that manually entered visit information is a verified booking.

The prototype uses a single local plan per device. It has no login, booking connection, family-member assignment or live operational feed.

## Design tokens

Token names are semantic Flutter theme names; values are initial prototype defaults and can be replaced by approved brand tokens later.

### Colour

| Token | Value | Use |
|---|---:|---|
| `color.primary` | `#2E7D32` | Primary actions, selected navigation, positive emphasis |
| `color.primaryPressed` | `#1F5D24` | Pressed primary action |
| `color.onPrimary` | `#FFFFFF` | Content on primary |
| `color.secondary` | `#FFC857` | Friendly accent and selected filter background |
| `color.accent` | `#C9443A` | Warnings and urgent attention; adjusted from the concept coral for AA contrast |
| `color.background` | `#FAF8F3` | App background |
| `color.surface` | `#FFFFFF` | Cards, sheets and bars |
| `color.surfaceMuted` | `#F1EFE8` | Chips, grouped rows and skeletons |
| `color.textPrimary` | `#172019` | Main text |
| `color.textSecondary` | `#526057` | Supporting text |
| `color.border` | `#D9DED9` | Dividers, input outlines |
| `color.info` | `#1769AA` | Location and informational state |
| `color.warningSurface` | `#FFF3D6` | Stale/mock/attention banners |
| `color.error` | `#B3261E` | Error text and destructive actions |
| `color.scrim` | `#172019` at 48% | Modal scrim |

Do not encode Program, Open and My plan with colour alone. Pair them with the visible labels `Program`, `Åbent` and `Min plan` plus distinct icons.

### Type

Use the platform system font for the prototype (SF Pro on iOS; Roboto on Android). Respect OS text scaling through at least 200%.

| Style | Size / line height | Weight | Typical use |
|---|---:|---:|---|
| `display` | 32 / 38 | 700 | Short hero greeting only |
| `titleLarge` | 24 / 30 | 700 | Screen title |
| `titleMedium` | 20 / 26 | 700 | Section/card title |
| `titleSmall` | 17 / 23 | 600 | Row and sheet title |
| `bodyLarge` | 17 / 25 | 400 | Important supporting copy |
| `body` | 15 / 22 | 400 | Default copy |
| `label` | 14 / 18 | 600 | Buttons, chips and tabs |
| `caption` | 12 / 17 | 400 | Metadata and freshness |

Do not truncate decision-critical times, places, statuses or button labels. Allow cards and rows to grow vertically.

### Space, shape, elevation and motion

- Spacing scale: 4, 8, 12, 16, 24, 32 and 48 logical pixels.
- Screen horizontal inset: 16; wide layout content max-width: 720.
- Card padding: 16; compact row padding: 12; section gap: 24.
- Radius: button 12, card 20, input 14, chip 999.
- Minimum interactive target: 48 x 48 logical pixels (never below 44 x 44).
- Surface elevation: 0 by default with a 1 px border; modal sheet 3; floating map controls 2.
- Standard motion: 180 ms ease-out; sheet transition 240 ms. Disable nonessential movement when reduced motion is requested.
- Icons: one outline family with consistent weight; never use an icon without an accessible name when it is the only visible label.

## Global shell

Five destinations remain available in a bottom navigation bar: `Nu`, `Plan`, `Kort`, `Mad`, `Mit besøg`. At text scaling above 140%, use a scroll-safe accessible navigation implementation without clipping; labels stay visible.

Every primary screen has a top app bar with its title and a `Søg` action. `Nu` may visually integrate its title into the greeting, but the route still exposes the semantic screen name `Nu`. Search opens as a full-screen route and preserves the originating tab.

On phones, content is a single vertical scroll. On widths 600–839, center content at max-width 720. At 840 and above, use two columns only where the relationship is useful: Plan timeline + selected detail; map/list + place detail. Never stretch cards edge to edge on large screens.

## Component and state inventory

### Core components

| Component | Required content and behavior |
|---|---|
| `PrimaryButton` | Verb-first label, optional leading icon, progress state, full-width only for the dominant screen action |
| `SecondaryButton` | Outlined/tonal action; same target and text rules as primary |
| `IconAction` | 48 x 48 target, tooltip and screen-reader label |
| `SearchField` | Visible label/hint `Hvad leder du efter?`, clear action, submit action; never auto-submit every keystroke |
| `FilterChip` | Selected checkmark + text, removable where relevant; horizontal wrapping rather than clipped scrolling at large text |
| `StatusBadge` | Icon + explicit text such as `Åbent`, `Lukket`, `Gemte oplysninger` |
| `SectionHeader` | Heading plus optional text action such as `Se hele dagen` |
| `ActionCard` | Eyebrow/status, title, concise decision detail, one primary and up to two secondary actions |
| `EventCard` | Layer label, start/end, title, place, duration, source/freshness, save/reminder/route state |
| `PlaceCard` | Open status, name, category, walk estimate, closing time, route action |
| `VisitSummaryCard` | Explicitly user-entered visit type/dates/home base; edit action |
| `FreshnessBanner` | Scope, state, timestamp when known, retry/details action |
| `PrototypeBanner` | Persistent but compact label: `Prototype · tider og ruter er eksempler` |
| `EmptyState` | Specific reason, useful next action, no decorative dead end |
| `ErrorState` | Plain-language cause where known, retained data/fallback, retry action |
| `Skeleton` | Mirrors final layout; announced once as loading, not per placeholder |
| `BottomSheet` | Title, drag affordance, close action, focus trap/restoration, scroll-safe buttons |
| `MapMarker` | Category shape/icon plus selected state; accessible through the equivalent place list |
| `RouteSummary` | Destination, estimated minutes/metres, route status and `Start rute`; identifies mock estimate |

### Shared data states

Every data-bearing screen or section implements these states:

| State | Presentation and prototype copy |
|---|---|
| Loading | Layout skeleton; `Henter oplysninger …` announced once |
| Loaded | Content plus source/freshness where relevant |
| Empty | `Her er ikke noget at vise endnu.` followed by a contextual action |
| Partial | Banner: `Nogle oplysninger kunne ikke hentes. Du kan stadig se det, vi har gemt.` |
| Offline cached | Banner: `Du er offline. Vi viser gemte oplysninger fra {time}.` |
| Stale | Banner: `Oplysningerne kan være forældede. Sidst opdateret {time}.` |
| Error, no cache | `Vi kunne ikke hente oplysningerne. Prøv igen, eller brug de praktiske genveje.` |
| Mock | Prototype banner and `Eksempeldata` beside times/statuses whose apparent authority matters |
| Permission denied | Explain the lost benefit and provide `Åbn indstillinger` plus a non-permission fallback |

Times and routes from fixtures must never use `live`, `aktuel`, `bekræftet` or `opdateret` without an actual source. Use `Estimeret gangtid · eksempeldata`.

## Screen contracts and Danish copy

### Onboarding and manual visit setup

Onboarding is skippable and no permission prompt appears on first launch.

1. Welcome
   - Title: `Få mere tid sammen`
   - Body: `Find det næste, se vejen og saml dagens planer ét sted.`
   - Primary: `Kom i gang`
   - Secondary: `Udforsk uden at vælge besøg`
   - Footer: `Prototype · oplysningerne er eksempler og er ikke knyttet til en booking.`
2. Visit type
   - Title: `Hvordan besøger I Jesperhus?`
   - Options: `Dagsbesøg` and `Overnatning`
   - If overnight, reveal accommodation choices from fixtures plus `Andet/ved ikke endnu`.
   - Primary: `Fortsæt`
3. Dates
   - Day guest: `Hvilken dag kommer I?`
   - Overnight: `Hvornår er jeres ophold?`
   - Validate departure is not before arrival; error: `Afrejsedato skal være efter ankomstdato.`
4. Home base (optional)
   - Title: `Vælg jeres hjemmebase`
   - Body: `Så kan appen vise vejen tilbage og beregne estimeret gangtid.`
   - Primary: `Vælg på kortet`; secondary: `Gør det senere`
5. Completion
   - Title: `Jeres besøg er klar`
   - Body: `Du kan altid ændre oplysningerne under Mit besøg.`
   - Primary: `Se, hvad der sker nu`

### Nu

Order content by urgency:

1. `PrototypeBanner`.
2. Context greeting: `God {morgen/eftermiddag/aften}` and a short user-entered context such as `Overnatning · 3.–5. august`. Never say `Dag 2 af 4` outside the selected date range.
3. Dominant next-action card:
   - Eyebrow: `Næste i din plan`
   - Example: `Piratshow · kl. 17.00`
   - Detail: `Piratkroen · ca. 7 min. gang · eksempeldata`
   - Advice: `For at være fremme i god tid kan I gå kl. 16.48.`
   - Primary: `Vis vej`; secondary: `Påmind mig`
   - Empty alternative: `Du har ikke gemt noget endnu.` + `Find noget i programmet`
4. `Det sker snart`: maximum three event cards, then `Se hele dagen`.
5. `Åbent i nærheden`: maximum three place cards; do not claim open-now unless fixture time logic supports it and label remains example data.
6. `Hurtige handlinger`: `Nærmeste toilet`, `Tilbage til hjemmebase`, `Find mad`, `Åbn kort`.

If visit dates are missing, replace personalized assumptions with: `Vælg jeres besøgsdatoer for at få en mere relevant plan.` + `Vælg besøg`.

### Plan

- Title: `Plan`
- Date selector is horizontally scrollable with full semantic dates, e.g. `Mandag 3. august, valgt`.
- Three independently toggled filter chips: `Min plan`, `Program`, `Åbent`. Default: all selected; preserve at least one selected.
- Timeline rows group by time and retain the layer label on every card.
- Event actions: `Gem` / `Fjern fra min plan`, `Påmind mig`, `Vis vej`.
- Open interval copy: `Åbent 10.00–18.00` rather than treating opening as an event at 10.00.
- Conflict banner: `Der er kun 5 minutter mellem aktiviteterne, men turen er estimeret til 8 minutter.` Label estimate as example data.
- Saved confirmation announcement: `{title} er gemt i Min plan.`
- Empty My plan: `Din plan er tom. Gem aktiviteter fra programmet, så samler vi dem her.`

### Kort

- Title: `Kort`; top controls: search, category filters and `Min placering`.
- Default phone layout: map with a persistent `Vis som liste` button and a collapsed selected-place sheet. The list is an equal, fully functional alternative—not a reduced accessibility mode.
- First use of `Min placering` triggers contextual permission education:
  - `Brug din placering på kortet?`
  - `Din placering bruges på telefonen til at vise, hvor du er, og estimere ruter. Prototypen gemmer ikke din bevægelseshistorik.`
  - Actions: `Fortsæt`, `Ikke nu`.
- Denied state: `Placering er slået fra. Vælg et startsted på kortet, eller åbn telefonens indstillinger.`
- Selected place sheet shows category, name, explicit example status, route estimate, facilities and actions `Vis rute`, `Gem`.
- Route mode shows destination, estimate, path and `Afslut rute`. Do not describe the provisional route as accessible, stroller-friendly or surveyed.
- Offline: retain fixture map/POIs and say `Kortet viser gemte eksempeldata. Din GPS kan stadig virke offline.`

Map accessibility: expose no arbitrary marker traversal. The semantic map is one control labeled `Interaktivt kort`; destinations are browsed in the ordered list. Announce selected place and route summary in a live region without stealing focus.

### Mad

- Title: `Mad`
- Intro action choices: `Kaffe`, `Morgenmad`, `Frokost`, `Aftensmad`.
- Filters: `Åbent nu`, `Under 10 min.`, `Vegetarisk`, `Takeaway` only when corresponding fixture fields exist.
- Sort default: `Kortest estimeret gangtid`; alternative `Lukker først`.
- Place card: name, food type, `Eksempeldata`, illustrative opening interval, estimated walk and `Vis vej`.
- Empty filter state: `Ingen steder matcher alle valgene. Prøv at fjerne et filter.`
- Ordering remains an external handoff. Label action `Se muligheder` unless a verified destination exists; do not promise ordering, allergens, prices, queue time or capacity.

### Mit besøg

- Title: `Mit besøg`
- `Dit valgte besøg`: visit type, dates, accommodation/home base and `Rediger`.
- Supporting label: `Oplysningerne er indtastet på denne telefon og er ikke bekræftet mod en booking.`
- Sections: `Billetter og adgang`, `Praktisk information`, `Påmindelser`, `Tilgængelighed`, `Offline og data`, `Privatliv og hjælp`.
- Ticket prototype row: `Åbn billetter` with explanation `Billetter åbnes via Jesperhus' eksisterende billet- eller wallet-løsning.` Never render a mock barcode that could be mistaken for admission.
- Data row: `Prototypeindhold · eksempeldata` and a destructive `Nulstil lokale data` action behind confirmation.
- Reset dialog: `Nulstil oplysninger på denne telefon?` / `Besøgsvalg, gemte aktiviteter og påmindelser fjernes. Det kan ikke fortrydes.` Actions: `Annuller`, `Nulstil`.

### Search

- Full-screen route titled `Søg` with autofocus only when it will not unexpectedly open the keyboard after back navigation.
- Field hint: `Fx aftenshow, kaffe eller toilet`.
- Before input, show recent searches locally and task shortcuts: `Aftenshow`, `Kaffe`, `Nærmeste toilet`, `Tilbage til hjemmebase`.
- Results prioritize one `Direkte svar` card, followed by `Flere resultater` grouped as activities, places and practical information.
- Example direct answer:
  - `Godnatshow er kl. 19.30 ved Træls Tårnet.`
  - `Varighed ca. 20 min. · eksempeldata`
  - Actions: `Vis vej`, `Påmind mig`.
- No result: `Vi fandt ikke noget for “{query}”. Prøv fx show, kaffe, toilet eller navnet på et sted.`
- Search is deterministic over fixture titles, categories and Danish synonyms. Never present it as AI or as a live answer service.

## Accessibility contract

- Target WCAG 2.2 AA. Verify colour pairs, including all selected, disabled, error and map-overlay states.
- Use native semantic roles for headings, buttons, tabs, text fields, toggles and alerts. Do not build tappable generic containers.
- Focus order follows visual reading order. Opening a sheet moves focus to its title; closing restores focus to the invoking control.
- Announce asynchronous state changes and save/reminder outcomes once, politely. Errors requiring action use an assertive announcement.
- A card is not one giant focus target when it contains multiple actions. Its title/detail is one semantic group; each action remains separate.
- Every time is read with context (`17.00, starttid`), and every date includes weekday, day and month.
- Support 200% text scaling, bold text, high contrast, reduced motion and both portrait orientations. No critical feature depends on landscape.
- Do not communicate status, selection, route or conflict by colour alone.
- Images are decorative unless they carry unique venue information. Decorative images are excluded from semantics; meaningful images receive concise Danish alternatives.
- Location, notification and external-app permission prompts occur only after the user invokes the related feature, with a usable fallback.
- Map actions, POIs and route summaries remain available in the list view with equivalent names and outcomes.

## Responsive and system behavior

- At 320 logical-pixel width, buttons may stack and cards grow; no horizontal page scrolling.
- At large text, bottom sheets may become full-screen routes to keep content and actions reachable.
- Respect safe areas, display cut-outs, keyboard insets and platform back gestures.
- On tablets, retain the five destinations in an adaptive navigation rail if it improves reachability; destination names remain unchanged.
- In split-screen, collapse two-column arrangements below 840 logical pixels.
- Dark mode is not required for the first demonstrator. If the OS requests dark mode, the prototype may retain the tested light theme rather than expose an unverified palette.
- All local dates/times use Europe/Copenhagen for prototype fixtures and are formatted in Danish; domain values remain timezone-aware.

## Prototype content and review rules

1. Show the persistent prototype banner on every root tab and on shareable/full-screen result routes.
2. Place `Eksempeldata` next to any schedule, opening status, walking estimate or operational claim.
3. Use invented unit labels only when visibly prefixed with `Eksempel`, e.g. `Eksempel: Pirathus 37`.
4. Never use official-looking ticket barcodes, emergency numbers, allergen guarantees, accessibility guarantees, prices or closure alerts as fixtures.
5. Fixture imagery must be approved, licensed, generated and labeled, or replaced with neutral shapes/icons.
6. A future verified source may remove the mock label only when it provides source attribution, retrieval time, validation behavior and a stale/error fallback.

## UX acceptance checklist

- A first-time user can select day visit or overnight stay, dates and optional home base without an account.
- Tonight's example show is reachable in at most three actions from Nu or Search.
- Program, Open and My plan are visually and semantically distinct.
- Saving an activity updates Plan and Nu and produces an accessible confirmation.
- Every place/event result that has geometry can open an illustrative route and equivalent list view.
- Offline, stale, partial, denied-permission, loading, empty and error states are demonstrable.
- No screen implies a verified booking, live schedule, surveyed route or valid admission ticket.
- Core flows remain complete at 320 px width and 200% text scaling with VoiceOver and TalkBack semantics.
