Files
TREK/client/src/components/Studio/TravelPreview.tsx
T
Maurice a7b53988ff feat(studio): rethink the travel tab with many new looks, a saved tile and a newer-book overlay
The travel tab is four tabs now, figures, maps, countries and marks, and
offers only the newer looks, each dressed in the book's theme and linked to
it. The looks books already hold still draw on their pages exactly as
before; they are just no longer offered.

- Figures: a summary as a poster, a ticket, tiles, a road sign, an
  odometer, a handwritten note, a pencil sketch or a band, and each figure
  on its own as a medal, on odometer drums or ringed by hand.
- Maps: the route with its photographs, its stops numbered, its two ends
  marked or on a dark ground, over satellite or relief imagery, a map per
  trip, and a map of each country with its stops alone, cut to its shape.
- Countries: each country as a passport stamp, a visa, an airmail
  postcard, its flag in cloth, a map pin, a luggage label or a sketch.
- Marks: the date, the day, the distance, the coordinates and the flag as
  stamps, tags, pills, ribbons, die-cut stickers, embroidered patches and
  sketches.

The schema gains a summary's and a country's look, seven mark styles and
the numbered and ends stops of a map, all additive, as revision 11.

Also in Studio: the save state is a tile beside who is here, a book a newer
Studio saved is said by an overlay that holds the whole Studio until it
reloads, and the page rail draws the first and the last page beside the
blank inside of their cover. The notifications page and the bell's panel
take the dialogs' look: a head with a tile, cards with an unread dot, and
tools that come forward on hover.
2026-10-24 14:45:04 +02:00

82 lines
3.4 KiB
TypeScript

import type { BookElement } from '@trek/shared'
import { useElementSize } from '../../hooks/useElementSize'
import { ElementView } from './SpreadView'
/**
* A travel element, drawn small.
*
* The same component the page uses, at a smaller scale — not a second drawing
* of it. This is the trick the page thumbnails already use, and it is worth
* more here than anywhere else in Studio: these elements differ from each other
* by *what they say*, not by their shape, so a generic icon of a map tells you
* nothing about which of the four map styles you are about to place. Rendered
* for real, with this journey's own figures, the tile answers the question the
* label was only gesturing at.
*
* It also cannot drift. If the map gains a layer or the summary changes how it
* sets a figure, the preview shows that on the same commit.
*
* ── Why the width is measured ─────────────────────────────────────────────
*
* The tile is a grid cell, so its width is the panel's minus padding, gaps, the
* card's own border and whatever the scrollbar takes. Passing a guess in was
* tried and was two pixels out, which `overflow: hidden` turned into a clipped
* right edge. Measuring costs one ResizeObserver and cannot be wrong.
*/
/** CSS defines 1in as 96px and 25.4mm, so this factor is exact. */
const PX_PER_MM = 96 / 25.4
export function TravelPreview({ el, minHeight = 30, maxHeight = 90, inset = 0 }: {
el: BookElement
minHeight?: number
maxHeight?: number
/**
* Room kept round the element, as a share of the tile: a stamp is set a few
* degrees off square and a medal's ring runs to its edge, and drawn edge to
* edge they would be clipped by the tile.
*/
inset?: number
}) {
const box = useElementSize<HTMLSpanElement>()
const width = box.width
/*
* The tile takes the element's proportions rather than a fixed box.
*
* A stats panel is three times wider than it is tall and a country list is
* taller than it is wide. Fitted into one shape, the wide ones would sit in a
* thin band with empty tile above and below — and, worse, would be scaled
* down to fit a height they did not need, which is what made the figures
* unreadable. Following the element uses the full width for everything and
* costs only a slightly uneven grid.
*/
const drawnW = el.frame.w * PX_PER_MM
const drawnH = el.frame.h * PX_PER_MM
const height = Math.round(Math.min(maxHeight, Math.max(minHeight, (width * drawnH) / drawnW || minHeight)))
const room = 1 - 2 * inset
const scale = width > 0 ? Math.min((width * room) / drawnW, (height * room) / drawnH) : 0
return (
<span className="st-travel-preview" style={{ height }} ref={box.ref} aria-hidden>
{scale > 0 && (
<span
style={{
position: 'absolute',
left: (width - drawnW * scale) / 2,
top: (height - drawnH * scale) / 2,
width: `${el.frame.w}mm`,
height: `${el.frame.h}mm`,
transform: `scale(${scale})`,
transformOrigin: 'top left',
}}
>
{/* Drawn at the origin: the tile is the frame, so the element's own
position on the spread is not part of what is being shown. */}
<ElementView el={{ ...el, frame: { ...el.frame, x: 0, y: 0 } } as BookElement} big={false} />
</span>
)}
</span>
)
}