diff --git a/client/e2e/auth.setup.ts b/client/e2e/auth.setup.ts index bd97a4444..2a14bdfd2 100644 --- a/client/e2e/auth.setup.ts +++ b/client/e2e/auth.setup.ts @@ -1,9 +1,10 @@ import { test as setup, expect } from '@playwright/test' import { dismissSystemNotices } from './helpers' +import { E2E_STATE_FILE } from '../playwright.config' // Relative to the config dir (client/), matching `storageState` in // playwright.config.ts. Playwright runs from the client workspace root. -const stateFile = 'e2e/.tmp/state.json' +const stateFile = E2E_STATE_FILE // Credentials match e2e/server-launch.mjs (ADMIN_EMAIL/ADMIN_PASSWORD). The // seeded admin is created with must_change_password=1, so the first login goes diff --git a/client/e2e/help/README.md b/client/e2e/help/README.md index 633bf1790..5716b2c69 100644 --- a/client/e2e/help/README.md +++ b/client/e2e/help/README.md @@ -12,9 +12,11 @@ in a script. | What | Where | Value | |---|---|---| | Viewport | `guide.ts` `VIEWPORT` | 1920 × 1080, device scale 2 (`playwright.config.ts`, project `help-media`) | -| Step picture frame | `guide.ts` `frameFor` | 16:10, at least 960 px wide, 64 px padding around the target, centred on it; a target taller than the frame is shown from its top | -| Ring around the target | `guide.ts` `ring` | 3 px in the app's `--accent`, 12 px radius, 6 px outside the element, the rest dimmed 30 %; a numbered badge (step number) at the top-left corner | -| Drag & drop | `guide.ts` `ringDrag` (a step with `dropTo`) | source ringed and numbered, destination ringed dashed, a bowed arrow from the one to the other, both undimmed | +| Step picture frame | `guide.ts` `frameFor` | 16:10, at least 960 px wide, 64 px padding around the target, centred on it, never larger than the largest 16:10 frame the viewport holds (1728 × 1080). A target wider than that, a full-width row for instance, is therefore cut at the sides, and that is accepted: the narrow frame is the format of the whole library, and a picture that widens to fit one row is at a different magnification from the one beside it. A target taller than the frame is anchored at its top, so a long section shows its heading | +| Ring around the target | `guide.ts` `ring` | 3 px in the app's `--accent`, 12 px radius, 6 px outside the element, the rest dimmed 30 %. A white hairline goes outside the ring and the badge only where the surface measured behind them has too little contrast against the accent, which is the lightbox and the other near-black overlays | +| The step's number | `guide.ts` `ring` | a 28 px disc in the accent on the ring's top-left corner, outside it. Where that spot is not inside the frame, which is every target wider than the frame, it moves inside far enough to clear the frame's edge. The number is part of the picture; the panel numbers the step again beside it and puts nothing on top of the picture | +| Raster tiles | `guide.ts` `settle` → `showLoadedTiles` | loaded tiles are forced visible before every shot. Leaflet fades a tile in over 200 ms and takes the progress from `+new Date()`, and the clock is pinned, so without this the satellite imagery and every raster basemap are a grey rectangle although each tile came back 200 | +| Drag & drop | `guide.ts` `ringDrag` (a step with `dropTo`) | source ringed, destination ringed dashed, a bowed arrow from the one to the other, both undimmed | | Hover | `captureGuide` | the target is hovered before the shot (tooltips, hover-only buttons show); `hover` overrides where the centre is wrong | | Result picture | `guide.media.result` | the full frame after the last step, pointer parked in the corner | | Hero (screen overview) | `captureHero` | the full frame of the screen as the reader finds it | @@ -43,8 +45,67 @@ change the theme, the language or the zoom for a picture. 5. Translate the block into all 22 locales (`i18n:parity:strict` is the gate) and run `src/help/registry.test.ts`, which checks every promised picture is on disk. +## What the fixtures may look like + +The documents and pictures the guides upload are drawn in `fixtures.ts`, because +no real person's paperwork and no photograph may live in this repository. They +still have to look like something a traveller would really put into a trip, or +the picture teaches the reader that the feature is a toy. + +- **A document is a document.** `drawPdf` takes an issuer band, a title, a + reference line, a table of labelled fields and a footer, which is the shape of + every confirmation, ticket and voucher. Four lines of Helvetica on white is not + a document; it was the first attempt and it was thrown out. +- **A picture is a graphic, not a painting of a photograph.** A hand-drawn + market or landscape reads as clip art at any size, however it is shaded, and it + was tried three times. What works is the kind of image people actually upload: + a map screenshot with a pin and a search field, a booking confirmation in a + browser window, a printed network map. Those are drawings in reality too, so a + drawing of them is honest. +- **Every date comes from `e2e/dates.ts`**, never from a calendar typed into a + fixture. A confirmation whose check-in is two days before the trip's last day + keeps making sense next month. +- The names of the files are part of the seed: guides find their row by name, so + renaming a fixture means renaming it in the guide too. + +## Changing the style + +`guide.ts` is shared by every picture, so anything changed there is a property of +the whole library, not of the guide in front of you. Before changing it, work out +which pictures actually come out different and re-render exactly those, one +`--grep` away: + +```bash +npm run help:media -- --grep "pictures: (files-trash|files-preview)$|hero: trip-files" +``` + +The staging directory answers most of those questions without a browser, because +the frame is in the file: + +```bash +# which pictures were taken at the frame's maximum, i.e. cut at the sides +python -c "import struct,os;[print(r,f,struct.unpack('>II',open(os.path.join(r,f),'rb').read(24)[16:24])) for r,_,fs in os.walk('e2e/.tmp/help-media') for f in fs if f.endswith('.png')]" +``` + +A change that alters every picture (the frame, the ring's geometry, the viewport) +is a full run of about four hours on one worker. It is not a thing to do twice, +so look at three or four pictures from a subset run before starting it. + ## Rules learned the hard way +- Never run `npx prettier` over `client/` sources: `client/.prettierrc` says + `semi: true` and the sources are written without semicolons, so it rewrites + whole files. Nothing in CI formats the client. +- A killed run leaves its server holding 3011 and 5183, and the next run dies + with "port is already used". Free them first: + `Get-NetTCPConnection -LocalPort 3011 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }`. +- The browser's clock is pinned to the picture day (`PICTURE_DAY`). Anything in + the app or a library that animates from the wall clock therefore never + finishes: Leaflet's tile fade is the one that bit, and the fix is in `settle`. + A new picture that comes out empty or grey is worth suspecting of the same. +- `promote.mjs` converts the whole staging directory, so a guide that was not + re-run keeps the picture it had; staging is the source of truth for what gets + published, not the last run. - Never edit `shared/` while a media run is going: the watcher rebuilds `dist` mid-run and the pictures show raw i18n keys. - Never `npm run build --workspace=shared` while `npm run dev` is running; restart @@ -60,6 +121,29 @@ change the theme, the language or the zoom for a picture. A guide that has to find a date by its label builds it with the same helpers (`short`, `long`, `pickerLabel`, `dotted`) instead of writing a calendar date. +## How the panel shows them + +`src/components/Help/HelpGuideView.tsx` and `HelpHome.tsx` hold the other half of +the style, and the two have to agree: + +- A step picture goes into a 16:10 box, a hero and a result picture into a 16:9 + one, and all three with `object-contain`. Never `object-cover`: the pictures + are already framed, and covering crops them a second time, which took the + sides off every wide one. +- Nothing is drawn over a picture in the panel. The step is numbered beside the + text and the number is in the picture itself. +- A click opens the lightbox, which shows the picture at its own size. + +## Before you promote + +Look at three or four of the new pictures, not the list of passing tests: + +- The ring is in the picture and visible against what is behind it. +- The number is in the picture. +- What the step's text names is in the frame. +- A map shows a map, not a grey rectangle. +- An uploaded document looks like a document someone would upload. + ## The guides that show another service A few guides picture TREK talking to something outside it: AirTrail (flights diff --git a/client/e2e/help/external.ts b/client/e2e/help/external.ts index 34db7e2dc..1af591813 100644 --- a/client/e2e/help/external.ts +++ b/client/e2e/help/external.ts @@ -181,6 +181,15 @@ export const NEXTCLOUD_BASE_PATH = '/Reisen' export const NEXTCLOUD_FOLDER = 'Autumn in Japan' export const NEXTCLOUD_DOCUMENTS = ['ryokan-invoice.pdf', 'shinkansen-eticket.pdf'] +/** What `drawPdf` in fixtures.ts takes; repeated here so this file needs nothing from it. */ +export interface StoreDocument { + issuer: string + title: string + reference: string + fields: readonly (readonly [string, string])[] + footer: string +} + function davRoot(): { url: string; auth: string } { const url = `${env('HELP_MEDIA_NEXTCLOUD_URL')}/remote.php/dav/files/${env('HELP_MEDIA_NEXTCLOUD_USER')}` const auth = 'Basic ' + Buffer.from(`${env('HELP_MEDIA_NEXTCLOUD_USER')}:${env('HELP_MEDIA_NEXTCLOUD_PASSWORD')}`).toString('base64') @@ -198,7 +207,7 @@ async function dav(method: string, path: string, body?: BodyInit, extra: Record< * folder holding two documents and nothing else, however the last run left it. * `pdf` draws the documents, so this file stays free of binary fixtures. */ -export async function ensureNextcloudFolder(pdf: (title: string, lines: string[]) => Buffer): Promise { +export async function ensureNextcloudFolder(pdf: (doc: StoreDocument) => Buffer): Promise { const folder = `${NEXTCLOUD_BASE_PATH}/${NEXTCLOUD_FOLDER}` for (const dir of [NEXTCLOUD_BASE_PATH, folder]) { const made = await dav('MKCOL', `${dir}/`) @@ -214,13 +223,36 @@ export async function ensureNextcloudFolder(pdf: (title: string, lines: string[] const gone = await dav('DELETE', `${folder}/${name}`) if (![204, 404].includes(gone.status)) throw new Error(`Nextcloud DELETE ${name}: HTTP ${gone.status}`) } - const documents: Record = { - 'ryokan-invoice.pdf': ['Hakone Ginyu, invoice 2026-0918', 'Two nights, two guests, half board', 'JPY 96,000, paid by card'], - 'shinkansen-eticket.pdf': ['Nozomi 21, Tokyo to Kyoto', 'Car 7, seats 12A and 12B', 'Reserved, non-smoking'], + const documents: Record = { + 'ryokan-invoice.pdf': { + issuer: 'Hakone Ginyu', + title: 'Invoice', + reference: 'Invoice 2026-0918, confirmation RY-4471', + fields: [ + ['Guests', '2 adults, one room'], + ['Nights', 'Two nights, half board'], + ['Room and board', 'JPY 84,000'], + ['Onsen tax', 'JPY 12,000'], + ['Total, paid by card', 'JPY 96,000'], + ], + footer: 'Paid in full. Thank you for staying with us.', + }, + 'shinkansen-eticket.pdf': { + issuer: 'JR Central', + title: 'Reserved seat ticket', + reference: 'Nozomi 21, Tokyo to Kyoto', + fields: [ + ['Date', 'Day 6 of the trip'], + ['Departure / arrival', 'Tokyo 08:30, Kyoto 10:45'], + ['Car and seats', 'Car 7, seats 12A and 12B'], + ['Class', 'Ordinary, reserved, non-smoking'], + ], + footer: 'Keep the ticket until you leave the station; the gate reads it twice.', + }, } for (const name of NEXTCLOUD_DOCUMENTS) { if (names.includes(name)) continue - const bytes = pdf(name.replace(/\.pdf$/, ''), documents[name]) + const bytes = pdf(documents[name]) const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer const put = await dav('PUT', `${folder}/${name}`, body, { 'Content-Type': 'application/pdf' }) if (![201, 204].includes(put.status)) throw new Error(`Nextcloud PUT ${name}: HTTP ${put.status} ${await put.text()}`) diff --git a/client/e2e/help/fixtures.ts b/client/e2e/help/fixtures.ts index 8fc38970c..17972e882 100644 --- a/client/e2e/help/fixtures.ts +++ b/client/e2e/help/fixtures.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import { gzipSync } from 'node:zlib' import { request as apiRequest, type APIRequestContext, type Page } from '@playwright/test' -import { E2E_BASE_URL } from '../../playwright.config' +import { E2E_BASE_URL, E2E_SEED_FILE } from '../../playwright.config' import { OUT_DIR, PICTURE_DAY } from './guide' import { at, day, emlDate, mailHeaderDate, short } from '../dates' @@ -237,7 +237,7 @@ export async function ensureUsers(api: APIRequestContext, wanted: { username: st /** The demo trip the screenshot seed creates; its ids are on disk because Playwright projects share no memory. */ export function seededTrip(): { tripId: number; dayIds: number[]; placeIds: number[]; memberIds: number[] } { - return JSON.parse(readFileSync(path.join(process.cwd(), 'e2e', '.tmp', 'seed.json'), 'utf8')) + return JSON.parse(readFileSync(path.join(process.cwd(), E2E_SEED_FILE), 'utf8')) } export async function ensureAdminFixtures(api: APIRequestContext): Promise { @@ -1368,64 +1368,324 @@ export async function ensureCostsFixtures(api: APIRequestContext): Promise */ /** One-page PDFs. The body is what the reader would see if they opened the file. */ -const FILE_PDFS = { +const FILE_PDFS: Record = { 'JR-Pass-voucher': { - title: 'Japan Rail Pass', - lines: ['Exchange order 7714-20', 'Ordinary, 14 days, 2 adults', 'Exchange at Haneda T3 arrivals, JR desk', 'Valid from the day it is exchanged'], + issuer: 'Japan Rail Pass', + title: 'Exchange order', + reference: 'Order 7714-20-JP', + fields: [ + ['Pass', 'Japan Rail Pass, Ordinary'], + ['Validity', '14 consecutive days'], + ['Travellers', '2 adults'], + ['Exchange at', 'Haneda Airport Terminal 3, JR East Travel Service Center'], + ['Opening hours', 'Daily 07:30 to 18:30'], + ], + footer: 'The pass is valid from the day it is exchanged, not from the day of purchase.', }, 'LH716-boarding-pass': { - title: 'Boarding pass LH716', - lines: ['Frankfurt FRA to Tokyo Haneda HND', `${short(-9)}, boarding 12:20, gate A34`, 'Seat 34K, window', 'Booking reference LH-4QK2PZ'], + issuer: 'Lufthansa', + title: 'Boarding pass', + reference: 'Booking LH-4QK2PZ, flight LH716', + fields: [ + ['Passenger', 'BOE / MAURICE MR'], + ['From', 'Frankfurt (FRA), Terminal 1'], + ['To', 'Tokyo Haneda (HND), Terminal 3'], + ['Date', short(-9)], + ['Boarding / departure', '12:20 at gate A34, departure 13:05'], + ['Seat / class', '34K, window, Economy'], + ], + footer: 'Be at the gate 20 minutes before departure. Boarding closes 15 minutes before.', }, 'old-draft-itinerary': { + issuer: 'Autumn in Japan', title: 'Itinerary, first draft', - lines: ['Day 1 Asakusa and the river', 'Day 2 Shibuya, Meiji Jingu', 'Day 3 free', 'Superseded by the plan in TREK'], + reference: 'Superseded by the plan in TREK', + fields: [ + ['Day 1', 'Asakusa, Senso-ji and the river'], + ['Day 2', 'Shibuya, Meiji Jingu, Harajuku'], + ['Day 3', 'Free, weather permitting'], + ['Day 4', 'Shinkansen to Kyoto, Gion in the evening'], + ], + footer: 'Kept for the notes on day 4; everything else moved into the trip.', }, 'hakone-ryokan-confirmation': { - title: 'Ryokan booking', - lines: ['Two nights, half board', 'Kaiseki served at 18:30', 'Check in from 15:00, check out 11:00', 'Confirmation RY-4471'], + issuer: 'Hakone Ginyu', + title: 'Reservation confirmed', + reference: 'Confirmation RY-4471', + fields: [ + ['Guests', '2 adults, one room'], + ['Nights', 'Two nights, half board'], + ['Check-in / check-out', 'From 15:00, until 11:00'], + ['Dinner', 'Kaiseki served at 18:30 in the room'], + ['Total', 'JPY 96,000, paid by card'], + ], + footer: 'Cancellation is free up to seven days before arrival.', }, 'kyoto-bus-pass': { - title: 'Kyoto city bus pass', - lines: ['One day, all city lines', 'Show the pass when leaving the bus', 'Not valid on the Kyoto Bus express'], + issuer: 'Kyoto City Bus', + title: 'One day pass', + reference: 'Pass 0421-8837', + fields: [ + ['Valid on', 'All city lines inside the flat-fare zone'], + ['Valid for', 'One calendar day, first use to last bus'], + ['Not valid on', 'Kyoto Bus express services and the airport lines'], + ], + footer: 'Show the pass to the driver when you leave the bus, printed side up.', }, -} as const +} /** Pictures, drawn flat so they read at thumbnail size as well as full screen. */ -const FILE_IMAGES = { +/** + * The pictures of the trip. No photograph may live in this repository, so they + * are composed instead: three layers per picture, the far one thrown out of + * focus, the near one sharp, and grain over the lot. A single flat drawing + * reads as a drawing at any size; a depth of field does not. + */ +interface PhotoLayer { + /** The SVG body of this layer, over the 1600×900 frame. */ + svg: string + /** How far out of focus it is, in pixels. */ + blur?: number +} + +interface PhotoFixture { + ext: 'jpg' | 'png' + layers: readonly PhotoLayer[] +} + +/** Dust and film grain, sprinkled from a fixed seed so a re-run draws the same frame. */ +const grain = (seed: number, count = 900): string => + Array.from({ length: count }, (_, i) => { + const r = (n: number) => (Math.sin(seed + i * n) + 1) / 2 + return `` + }).join('') + +/** Somebody with their back to us, which is what fills a market lane. */ +const person = (x: number, ground: number, h: number, tone: string): string => + `` + + `` + +/** A pile of produce: a handful of small round things, not one big rectangle. */ +const pile = (x: number, y: number, colour: string, n = 9): string => + Array.from({ length: n }, (_, i) => { + const row = Math.floor(i / 4) + return `` + }).join('') + +/** A haze of light: what a lamp or the sun does to the air in front of it. */ +const bloom = (x: number, y: number, r: number, colour: string, opacity = 0.7): string => + `` + +const FILE_IMAGES: Record = { 'nishiki-market': { + // What people photograph for later is rarely the market itself; it is the + // map with the market pinned on it. ext: 'jpg', - svg: ` - - - ${[0, 1, 2, 3, 4].map(i => ``).join('\n ')} - `, + layers: [ + { + svg: ` + + + + + + ${[0, 1, 2, 3, 4, 5, 6, 7].map(i => [0, 1, 2, 3, 4].map(k => ``).join('')).join('\n ')} + + + + + ${[0, 1, 2, 3, 4].map(k => ``).join('\n ')} + ${[0, 1, 2, 3, 4].map(k => ``).join('\n ')} + ${[0, 1, 2, 3, 4, 5, 6, 7].map(i => ``).join('\n ')} + ${[0, 1, 2, 3, 4, 5, 6, 7].map(i => ``).join('\n ')} + + + + + + + + + + Nishiki-koji Street + Shijo-dori + Kamo River + Shinsen-en + Gosho Park + NAKAGYO + ${[[300, 300], [1080, 470], [560, 690], [1180, 660]].map(([x, y]) => ``).join('\n ')} + + + + + + + Nishiki Market + 4.4 (12,806) · Market + + + + + + Nishiki Market, Kyoto + + + + + + + + + 200 m + Map data © OpenStreetMap contributors + ${grain(6.2, 200)}`, + }, + ], }, 'hakone-ryokan': { + // The confirmation as it looked in the browser, saved to the trip. ext: 'jpg', - svg: ` - - - - - ${[0, 1, 2, 3].map(i => ``).join('\n ')} - `, + layers: [ + { + svg: ` + + + + + + + + + + hakone-ginyu.jp/reservations/RY-4471 + + Hakone Ginyu + Ryokan · Miyanoshita, Hakone-machi, Kanagawa + + + Confirmed + + CONFIRMATION + RY-4471 + ${[['Check-in', `${short(-4)}, from 15:00`], ['Check-out', `${short(-2)}, until 11:00`], ['Guests', '2 adults, one room'], ['Room', 'Japanese suite with open-air bath'], ['Meals', 'Kaiseki dinner and breakfast included']].map(([k, v], i) => ` + ${k.toUpperCase()} + ${v} + + `).join('\n ')} + TOTAL, PAID BY CARD + JPY 96,000 + + + + + + + + + + + + ADDRESS + 100-1 Miyanoshita, Hakone-machi + Ashigarashimo, Kanagawa 250-0404 + + + Cancellation + Free until seven days before arrival. + After that, one night is charged. + Questions: +81 460 82 3355 + ${grain(9.4, 160)}`, + }, + ], }, 'jr-pass-map': { ext: 'png', - svg: ` - - - ${[[180, 700], [520, 700], [760, 460], [1120, 460], [1420, 240], [180, 250], [640, 250], [900, 500], [1420, 500]].map(([x, y]) => ``).join('\n ')}`, + layers: [ + { + // A printed network map: paper, its fold and the grid under the lines. + svg: ` + + + + + + + ${Array.from({ length: 9 }, (_, i) => ``).join('\n ')} + ${Array.from({ length: 16 }, (_, i) => ``).join('\n ')} + + + + + + + + + ${[[120, 760, 'Hakata'], [420, 690, 'Hiroshima'], [640, 560, 'Osaka'], [900, 490, 'Kyoto'], [1300, 490, 'Nagoya'], [1470, 420, 'Tokyo'], [150, 230, 'Kanazawa'], [600, 230, 'Toyama'], [1060, 490, 'Gifu']].map(([x, y, name]) => + `${name}`).join('\n ')} + + + Rail pass network + Sanyo line + Hokuriku + + ${grain(5.5, 260)}`, + }, + ], }, arashiyama: { ext: 'jpg', - svg: ` - ${Array.from({ length: 16 }, (_, i) => ``).join('\n ')} - - `, + layers: [ + { + // The grove behind the path: light coming down through it, nothing in focus. + blur: 24, + svg: ` + + + + + + ${bloom(780, 90, 460, '#fbffd8', 0.55)} + ${Array.from({ length: 26 }, (_, i) => { + const x = -30 + i * 64 + const tilt = ((i % 5) - 2) * 9 + return `` + }).join('\n ')}`, + }, + { + // The stems either side of the path, just off the focal plane. + blur: 4, + svg: `${Array.from({ length: 9 }, (_, i) => { + const x = i < 5 ? 20 + i * 120 : 1000 + (i - 5) * 130 + const w = 42 + (i % 3) * 12 + const tilt = ((i % 4) - 1.5) * 12 + return ` + + ${Array.from({ length: 6 }, (_, k) => ``).join('')} + + ` + }).join('\n ')}`, + }, + { + // The path itself, in focus, with the fence along it and the dapple of + // light that falls between the stems. + svg: ` + + + ${[0, 1, 2, 3, 4, 5, 6, 7].map(i => ` + + + `).join('\n ')} + ${[0, 1, 2, 3, 4, 5, 6, 7].map(i => ``).join('\n ')} + ${grain(2.4, 420)} + + + + + ${grain(11.3)}`, + }, + ], }, -} as const +} /** The two plain-text documents; written out rather than drawn. */ const FILE_TEXTS = { @@ -1497,18 +1757,51 @@ const tmpDir = (): string => { * Exported for the document-sync guide, which puts two of these into the store * (`external.ts`) rather than into the trip. */ -export function drawPdf(title: string, lines: readonly string[]): Buffer { - const escape = (s: string) => s.replace(/([\\()])/g, '\\$1') - const content = [ - `BT /F1 26 Tf 64 752 Td (${escape(title)}) Tj ET`, - ...lines.map((line, i) => `BT /F1 13 Tf 64 ${700 - i * 26} Td (${escape(line)}) Tj ET`), - ].join('\n') +export interface PdfDoc { + /** The company or office the paper comes from, printed in the band at the top. */ + issuer: string + /** What the document is, under the issuer. */ + title: string + /** The line under the title: a booking code, an order number. */ + reference: string + /** The body: a label and its value per row, the way a confirmation prints them. */ + fields: readonly (readonly [string, string])[] + /** The small print along the bottom. */ + footer: string +} + +export function drawPdf(doc: PdfDoc): Buffer { + const esc = (v: string) => v.replace(/([\\()])/g, '\\$1') + // A4 in points, the origin at the bottom left. The band is drawn first, then + // the type over it; everything is Helvetica, which every PDF reader has. + const W = 595 + const ink = '0.07 0.09 0.15' + const parts: string[] = [ + `${ink} rg 0 762 ${W} 80 re f`, + `BT /F2 17 Tf 1 1 1 rg 56 806 Td (${esc(doc.issuer.toUpperCase())}) Tj ET`, + `BT /F1 10 Tf 0.75 0.78 0.85 rg 56 786 Td (${esc(doc.footer)}) Tj ET`, + `BT /F2 21 Tf 0.07 0.09 0.15 rg 56 706 Td (${esc(doc.title)}) Tj ET`, + `BT /F1 11 Tf 0.35 0.38 0.45 rg 56 684 Td (${esc(doc.reference)}) Tj ET`, + `0.85 0.86 0.9 RG 1 w 56 668 m ${W - 56} 668 l S`, + ] + doc.fields.forEach(([label, value], i) => { + const y = 636 - i * 34 + parts.push(`BT /F1 9 Tf 0.45 0.48 0.55 rg 56 ${y + 14} Td (${esc(label.toUpperCase())}) Tj ET`) + parts.push(`BT /F2 13 Tf 0.07 0.09 0.15 rg 56 ${y} Td (${esc(value)}) Tj ET`) + parts.push(`0.92 0.93 0.95 RG 0.6 w 56 ${y - 12} m ${W - 56} ${y - 12} l S`) + }) + const bottom = 636 - doc.fields.length * 34 - 40 + parts.push(`0.96 0.97 0.98 rg 56 ${bottom - 46} ${W - 112} 58 re f`) + parts.push(`BT /F1 10 Tf 0.35 0.38 0.45 rg 72 ${bottom - 12} Td (${esc(doc.footer)}) Tj ET`) + parts.push(`BT /F1 9 Tf 0.55 0.58 0.65 rg 72 ${bottom - 30} Td (${esc('This document was issued electronically and needs no signature.')}) Tj ET`) + const content = parts.join('\n') const objects = [ '<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', - '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>', + '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> /Contents 4 0 R >>', `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`, '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>', + '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>', ] let pdf = '%PDF-1.4\n' const offsets: number[] = [] @@ -1530,19 +1823,23 @@ export function drawPdf(title: string, lines: readonly string[]): Buffer { */ export function filesPdfFixture(id: keyof typeof FILE_PDFS): string { const file = path.join(tmpDir(), `${id}.pdf`) - const { title, lines } = FILE_PDFS[id] - writeFileSync(file, drawPdf(title, lines)) + writeFileSync(file, drawPdf(FILE_PDFS[id])) return file } -/** A 1600×900 picture from its SVG, as `coverFixture` does; drawn on first use. */ +/** A 1600×900 picture, its layers blurred to their own degree and laid over each other. */ export async function filesImageFixture(id: keyof typeof FILE_IMAGES): Promise { - const { ext, svg } = FILE_IMAGES[id] + const { ext, layers } = FILE_IMAGES[id] const file = path.join(tmpDir(), `${id}.${ext}`) if (existsSync(file)) return file - const document = `\n ${svg}\n` - const image = sharp(Buffer.from(document)) - await (ext === 'png' ? image.png() : image.jpeg({ quality: 86 })).toFile(file) + const render = async (layer: PhotoLayer): Promise => { + const document = `\n ${layer.svg}\n` + const drawn = sharp(Buffer.from(document)) + return (layer.blur ? drawn.blur(layer.blur) : drawn).png().toBuffer() + } + const [base, ...rest] = await Promise.all(layers.map(render)) + const composed = sharp(base).composite(rest.map(input => ({ input }))) + await (ext === 'png' ? composed.png() : composed.jpeg({ quality: 88 })).toFile(file) return file } diff --git a/client/e2e/help/guide.ts b/client/e2e/help/guide.ts index 88135f7ad..2b16350ad 100644 --- a/client/e2e/help/guide.ts +++ b/client/e2e/help/guide.ts @@ -154,8 +154,9 @@ export async function captureGuide(page: Page, script: GuideScript): Promise Math.min(Math.max(v, lo), hi) -/** Ring the target in the app's accent, dim everything else, number it. */ -async function ring(page: Page, box: { x: number; y: number; width: number; height: number }, n: number): Promise { +/** + * Ring the target, dim everything else, number it. + * + * The ring is the app's accent, which stands out on nearly every surface the + * guides walk over. Over the lightbox and the other near-black overlays it does + * not: a dark accent on a dark ground is a ring nobody can see. So the surface + * behind the ring is measured, and only when it has too little contrast against + * the accent does a white hairline go around the outside of the ring and the + * badge. Everywhere else the ring keeps the single line it always had. + */ +async function ring( + page: Page, + box: { x: number; y: number; width: number; height: number }, + n: number, + frame: { x: number; y: number; width: number; height: number }, +): Promise { await page.evaluate( - ({ box, n }) => { + ({ box, n, frame }) => { const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#111827' const accentText = getComputedStyle(document.documentElement).getPropertyValue('--accent-text').trim() || '#ffffff' const pad = 6 + + const parse = (value: string): [number, number, number, number] | null => { + const text = value.trim() + if (text.startsWith('#')) { + const hex = text.length < 7 ? text.slice(1, 4).replace(/./g, c => c + c) : text.slice(1, 7) + const n = Number.parseInt(hex, 16) + return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1] + } + const inside = /rgba?\(([^)]+)\)/.exec(text) + if (!inside) return null + const parts = inside[1] + .split(/[\s,/]+/) + .filter(Boolean) + .map(Number) + return [parts[0], parts[1], parts[2], parts.length > 3 ? parts[3] : 1] + } + const luminance = (colour: [number, number, number, number]): number => { + const channel = (v: number) => { + const c = v / 255 + return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4 + } + return 0.2126 * channel(colour[0]) + 0.7152 * channel(colour[1]) + 0.0722 * channel(colour[2]) + } + // What lies just outside the ring, through any transparent layer. + const behind = (x: number, y: number): [number, number, number, number] => { + let node = document.elementFromPoint( + Math.max(1, Math.min(window.innerWidth - 2, x)), + Math.max(1, Math.min(window.innerHeight - 2, y)), + ) + while (node) { + const colour = parse(getComputedStyle(node).backgroundColor) + if (colour && colour[3] > 0.2) return colour + node = node.parentElement + } + return [255, 255, 255, 1] + } + // The dim layer darkens that surface before the ring is drawn over it. + const ground = luminance(behind(box.x - pad - 4, box.y + box.height / 2)) * 0.7 + const ink = luminance(parse(accent) || [17, 24, 39, 1]) + const hairline = (Math.max(ground, ink) + 0.05) / (Math.min(ground, ink) + 0.05) < 3 + const el = document.createElement('div') el.id = 'trek-help-ring' Object.assign(el.style, { @@ -231,15 +287,22 @@ async function ring(page: Page, box: { x: number; y: number; width: number; heig width: `${box.width + 2 * pad}px`, height: `${box.height + 2 * pad}px`, borderRadius: '12px', - boxShadow: `0 0 0 3px ${accent}, 0 0 0 100vmax rgba(0,0,0,0.30)`, + boxShadow: `0 0 0 3px ${accent}${hairline ? ', 0 0 0 5px rgba(255,255,255,0.92)' : ''}, 0 0 0 100vmax rgba(0,0,0,0.30)`, pointerEvents: 'none', zIndex: '2147483647', }) + // Outside the corner where that is in shot, and otherwise inside, far + // enough in to clear the frame's edge: a row wider than the picture has + // no outside left of it, and the ring's own corner is off the picture too. + const ringLeft = box.x - pad + const ringTop = box.y - pad + const left = ringLeft - 14 >= frame.x + 2 ? -14 : Math.max(10, frame.x + 10 - ringLeft) + const top = ringTop - 14 >= frame.y + 2 ? -14 : Math.max(10, frame.y + 10 - ringTop) const badge = document.createElement('div') Object.assign(badge.style, { position: 'absolute', - left: '-14px', - top: '-14px', + left: `${left}px`, + top: `${top}px`, width: '28px', height: '28px', borderRadius: '999px', @@ -247,13 +310,15 @@ async function ring(page: Page, box: { x: number; y: number; width: number; heig color: accentText, font: '700 13px/28px Poppins, system-ui, sans-serif', textAlign: 'center', - boxShadow: '0 2px 8px rgba(0,0,0,0.25)', + boxShadow: hairline + ? '0 0 0 2px rgba(255,255,255,0.92), 0 2px 8px rgba(0,0,0,0.25)' + : '0 2px 8px rgba(0,0,0,0.25)', }) badge.textContent = String(n) el.appendChild(badge) document.body.appendChild(el) }, - { box, n }, + { box, n, frame }, ) } @@ -365,5 +430,35 @@ export async function settle(page: Page): Promise { .map(img => new Promise(res => { img.onload = img.onerror = res })) await Promise.race([Promise.all(pending), new Promise(res => setTimeout(res, 3000))]) }) + await showLoadedTiles(page) await page.waitForTimeout(450) } + +/** + * Show the raster tiles that have arrived. + * + * Leaflet fades a tile in over 200 ms and works out how far it has got from + * `+new Date()`. The clock is pinned to the picture day (see PICTURE_DAY), so + * that difference never grows, the tile keeps the opacity 0 it was created + * with, and the map is a grey rectangle in the picture even though every tile + * came back with a 200. Nothing else on the page fades from the wall clock, so + * this only ever touches tiles that are already loaded. + */ +async function showLoadedTiles(page: Page): Promise { + await page.evaluate(() => { + // A rule, not an inline style: Leaflet re-runs the fade on every frame and + // writes the opacity back, so the only thing that holds is a stylesheet + // that outranks it. `leaflet-tile-loaded` is Leaflet's own class, put on a + // tile the moment its image arrives, so a tile still in flight stays hidden. + if (!document.getElementById('trek-help-tiles')) { + const style = document.createElement('style') + style.id = 'trek-help-tiles' + style.textContent = '.leaflet-tile-loaded { opacity: 1 !important; }' + document.head.appendChild(style) + } + for (const tile of document.querySelectorAll('img.leaflet-tile')) { + const img = tile as HTMLImageElement + if (img.complete && img.naturalWidth > 0) img.classList.add('leaflet-tile-loaded') + } + }) +} diff --git a/client/e2e/help/trip-map.guide.ts b/client/e2e/help/trip-map.guide.ts index ac693c918..354ed0a3e 100644 --- a/client/e2e/help/trip-map.guide.ts +++ b/client/e2e/help/trip-map.guide.ts @@ -212,6 +212,29 @@ async function glMapIdle(page: Page): Promise { ) } +/** + * Wait until the raster basemap has actually painted. + * + * `toBeVisible()` on a tile is not enough: Leaflet puts the in the DOM + * with its position and its size before the byte arrives, so a picture taken on + * that signal catches the grey underneath. A tile counts only once the browser + * has decoded it, and a screenful needs several of them. + */ +async function rasterTilesPainted(page: Page, atLeast = 6): Promise { + await page.waitForFunction( + min => { + const tiles = Array.from(document.querySelectorAll('#trek-map img.leaflet-tile')) + const painted = tiles.filter(t => { + const img = t as HTMLImageElement + return img.complete && img.naturalWidth > 0 + }) + return painted.length >= min + }, + atLeast, + { timeout: 60_000 }, + ) +} + async function deleteByName(page: Page, ...names: string[]): Promise { const { tripId } = seededTrip() const res = await page.request.get(`/api/trips/${tripId}/places`) @@ -416,15 +439,16 @@ const SCRIPTS: Record = { act: async p => { await p.getByRole('button', { name: 'Switch to satellite view' }).click() await expect(p.getByRole('button', { name: 'Switch to map view' })).toBeVisible() - // The imagery is raster tiles from a third party; give them a moment - // or the picture catches the grey underneath. - await expect(p.locator('#trek-map img.leaflet-tile').first()).toBeVisible({ timeout: 30_000 }) + // The imagery is raster tiles from a third party, and the switch is + // done long before they are on screen. + await rasterTilesPainted(p) await settle(p) await p.waitForTimeout(1500) }, }, - { target: p => pins(p).nth(pin) }, + { prepare: p => rasterTilesPainted(p), target: p => pins(p).nth(pin) }, { + prepare: p => rasterTilesPainted(p), target: p => p.getByRole('button', { name: 'Switch to map view' }), act: async p => { await p.getByRole('button', { name: 'Switch to map view' }).click() diff --git a/client/e2e/help/trip-places.guide.ts b/client/e2e/help/trip-places.guide.ts index aca703f59..0ffad6e3f 100644 --- a/client/e2e/help/trip-places.guide.ts +++ b/client/e2e/help/trip-places.guide.ts @@ -148,6 +148,24 @@ const SCRIPTS: Record = { await settle(p) }, }, + { + // The other way round, and the one most readers try first: the row is + // dragged onto a day card. The place is off the day again before the + // drag so the picture shows it leaving the column rather than a stop + // that is already there. + prepare: async p => { + await p.getByRole('button', { name: 'Undo' }).click() + await expect(row(p, SPARE.name)).toBeVisible({ timeout: 15_000 }) + await settle(p) + }, + target: p => row(p, SPARE.name), + dropTo: p => p.getByRole('button', { name: /^2 .*Day 2 / }).locator('xpath=..'), + act: async p => { + await row(p, SPARE.name).dragTo(p.getByRole('button', { name: /^2 .*Day 2 / })) + await expect(p.locator('.dp-row[role="button"]').filter({ hasText: SPARE.name }).first()).toBeVisible({ timeout: 15_000 }) + await settle(p) + }, + }, ], cleanup: p => deleteByName(p, SPARE.name), }, diff --git a/client/e2e/screenshots/seed.setup.ts b/client/e2e/screenshots/seed.setup.ts index f630dab64..cadb8fb66 100644 --- a/client/e2e/screenshots/seed.setup.ts +++ b/client/e2e/screenshots/seed.setup.ts @@ -2,7 +2,7 @@ import { test as setup, expect } from '@playwright/test' import { writeFileSync, mkdirSync } from 'node:fs' import path from 'node:path' import { seedDemoData } from './seed' -import { E2E_BASE_URL } from '../../playwright.config' +import { E2E_BASE_URL, E2E_SEED_FILE } from '../../playwright.config' /** * Populates the throwaway E2E database with the demo trip before any screenshot @@ -32,7 +32,6 @@ setup('seed the demo trip', async ({ page, playwright }) => { expect(result.tripId, 'trip was created').toBeTruthy() expect(result.placeIds.length, 'places were created').toBeGreaterThan(0) - const dir = path.join(process.cwd(), 'e2e', '.tmp') - mkdirSync(dir, { recursive: true }) - writeFileSync(path.join(dir, 'seed.json'), JSON.stringify(result, null, 2)) + mkdirSync(path.join(process.cwd(), 'e2e', '.tmp'), { recursive: true }) + writeFileSync(path.join(process.cwd(), E2E_SEED_FILE), JSON.stringify(result, null, 2)) }) diff --git a/client/e2e/server-launch.mjs b/client/e2e/server-launch.mjs index 582632cfe..e64b7a17f 100644 --- a/client/e2e/server-launch.mjs +++ b/client/e2e/server-launch.mjs @@ -12,7 +12,10 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const here = path.dirname(fileURLToPath(import.meta.url)) -const dbFile = path.join(here, '.tmp', 'e2e.db') +// The port is in the name, so two runs on two port pairs keep two databases +// and can render different guide files at the same time. +const apiPort = process.env.E2E_API_PORT || '3001' +const dbFile = path.join(here, '.tmp', `e2e-${apiPort}.db`) const serverDir = path.join(here, '..', '..', 'server') for (const f of [dbFile, `${dbFile}-wal`, `${dbFile}-shm`]) { diff --git a/client/playwright.config.ts b/client/playwright.config.ts index d139843b2..1101f341b 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -20,6 +20,13 @@ import { defineConfig, devices } from '@playwright/test' const WEB_PORT = Number(process.env.E2E_WEB_PORT) || 5173 const API_PORT = Number(process.env.E2E_API_PORT) || 3001 export const E2E_BASE_URL = `http://localhost:${WEB_PORT}` +/** + * The session and the seed's ids belong to the database the run boots, which is + * named after its API port (e2e/server-launch.mjs). Two runs on two port pairs + * therefore share nothing, and the default port keeps the old file names. + */ +export const E2E_STATE_FILE = `e2e/.tmp/state-${API_PORT}.json` +export const E2E_SEED_FILE = `e2e/.tmp/seed-${API_PORT}.json` // The day the seed and the pictures are relative to (e2e/dates.ts). Fixed here // for every project and worker that loads this file, so a run that crosses @@ -53,7 +60,7 @@ export default defineConfig({ name: 'app', testMatch: /\.spec\.ts/, testIgnore: /(\.public\.spec\.ts|auth\.setup\.ts)/, - use: { ...devices['Desktop Chrome'], storageState: 'e2e/.tmp/state.json' }, + use: { ...devices['Desktop Chrome'], storageState: E2E_STATE_FILE }, dependencies: ['setup'], }, // Documentation screenshots (`npm run shots`). Excluded from the normal e2e @@ -64,7 +71,7 @@ export default defineConfig({ { name: 'seed', testMatch: /seed\.setup\.ts/, - use: { ...devices['Desktop Chrome'], storageState: 'e2e/.tmp/state.json' }, + use: { ...devices['Desktop Chrome'], storageState: E2E_STATE_FILE }, dependencies: ['setup'], }, { @@ -72,7 +79,7 @@ export default defineConfig({ testMatch: /\.shot\.ts/, use: { ...devices['Desktop Chrome'], - storageState: 'e2e/.tmp/state.json', + storageState: E2E_STATE_FILE, viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, }, @@ -98,7 +105,7 @@ export default defineConfig({ // and the Files tab's preview embeds a PDF as , which the // shell leaves as a "Download PDF" link. Still headless. channel: 'chromium', - storageState: 'e2e/.tmp/state.json', + storageState: E2E_STATE_FILE, viewport: { width: 1920, height: 1080 }, deviceScaleFactor: 2, }, diff --git a/client/public/help-media/audit-log/step-1.webp b/client/public/help-media/audit-log/step-1.webp index 59df04696..92ef29efe 100644 Binary files a/client/public/help-media/audit-log/step-1.webp and b/client/public/help-media/audit-log/step-1.webp differ diff --git a/client/public/help-media/audit-log/step-2.webp b/client/public/help-media/audit-log/step-2.webp index 3ef3563e9..96a6ebca0 100644 Binary files a/client/public/help-media/audit-log/step-2.webp and b/client/public/help-media/audit-log/step-2.webp differ diff --git a/client/public/help-media/create-place/result.webp b/client/public/help-media/create-place/result.webp index 460ba0e81..fde31ee12 100644 Binary files a/client/public/help-media/create-place/result.webp and b/client/public/help-media/create-place/result.webp differ diff --git a/client/public/help-media/create-place/step-1.webp b/client/public/help-media/create-place/step-1.webp index 04bc6cb5f..910d8fad6 100644 Binary files a/client/public/help-media/create-place/step-1.webp and b/client/public/help-media/create-place/step-1.webp differ diff --git a/client/public/help-media/create-place/step-2.webp b/client/public/help-media/create-place/step-2.webp index 08a82e2df..2be6f6a8d 100644 Binary files a/client/public/help-media/create-place/step-2.webp and b/client/public/help-media/create-place/step-2.webp differ diff --git a/client/public/help-media/create-place/step-3.webp b/client/public/help-media/create-place/step-3.webp index bcee85b10..ef3b7abf7 100644 Binary files a/client/public/help-media/create-place/step-3.webp and b/client/public/help-media/create-place/step-3.webp differ diff --git a/client/public/help-media/create-place/step-4.webp b/client/public/help-media/create-place/step-4.webp index 2861c6c1e..35059e848 100644 Binary files a/client/public/help-media/create-place/step-4.webp and b/client/public/help-media/create-place/step-4.webp differ diff --git a/client/public/help-media/create-place/step-5.webp b/client/public/help-media/create-place/step-5.webp index 330ae989e..5aa738148 100644 Binary files a/client/public/help-media/create-place/step-5.webp and b/client/public/help-media/create-place/step-5.webp differ diff --git a/client/public/help-media/ctx/trip-files.webp b/client/public/help-media/ctx/trip-files.webp index d5bd92423..2b116cbc9 100644 Binary files a/client/public/help-media/ctx/trip-files.webp and b/client/public/help-media/ctx/trip-files.webp differ diff --git a/client/public/help-media/ctx/trip-places.webp b/client/public/help-media/ctx/trip-places.webp index 3db8d368a..65bb64d34 100644 Binary files a/client/public/help-media/ctx/trip-places.webp and b/client/public/help-media/ctx/trip-places.webp differ diff --git a/client/public/help-media/delete-place/step-1.webp b/client/public/help-media/delete-place/step-1.webp index 78d18990f..71ad22a8e 100644 Binary files a/client/public/help-media/delete-place/step-1.webp and b/client/public/help-media/delete-place/step-1.webp differ diff --git a/client/public/help-media/delete-place/step-2.webp b/client/public/help-media/delete-place/step-2.webp index fbf66e2ae..a6829d75e 100644 Binary files a/client/public/help-media/delete-place/step-2.webp and b/client/public/help-media/delete-place/step-2.webp differ diff --git a/client/public/help-media/edit-place/step-1.webp b/client/public/help-media/edit-place/step-1.webp index a4195ca00..6ac34682d 100644 Binary files a/client/public/help-media/edit-place/step-1.webp and b/client/public/help-media/edit-place/step-1.webp differ diff --git a/client/public/help-media/edit-place/step-2.webp b/client/public/help-media/edit-place/step-2.webp index a2879aaf6..509de5225 100644 Binary files a/client/public/help-media/edit-place/step-2.webp and b/client/public/help-media/edit-place/step-2.webp differ diff --git a/client/public/help-media/edit-place/step-3.webp b/client/public/help-media/edit-place/step-3.webp index 42f89d18f..b2b31f546 100644 Binary files a/client/public/help-media/edit-place/step-3.webp and b/client/public/help-media/edit-place/step-3.webp differ diff --git a/client/public/help-media/files-filter/step-1.webp b/client/public/help-media/files-filter/step-1.webp index 4ae9b97b6..fa75e0b7d 100644 Binary files a/client/public/help-media/files-filter/step-1.webp and b/client/public/help-media/files-filter/step-1.webp differ diff --git a/client/public/help-media/files-filter/step-2.webp b/client/public/help-media/files-filter/step-2.webp index a6d5be357..993d3d74f 100644 Binary files a/client/public/help-media/files-filter/step-2.webp and b/client/public/help-media/files-filter/step-2.webp differ diff --git a/client/public/help-media/files-filter/step-3.webp b/client/public/help-media/files-filter/step-3.webp index 72979b9ba..8917de1ef 100644 Binary files a/client/public/help-media/files-filter/step-3.webp and b/client/public/help-media/files-filter/step-3.webp differ diff --git a/client/public/help-media/files-filter/step-4.webp b/client/public/help-media/files-filter/step-4.webp index 862fd7954..81ee4434c 100644 Binary files a/client/public/help-media/files-filter/step-4.webp and b/client/public/help-media/files-filter/step-4.webp differ diff --git a/client/public/help-media/files-link/result.webp b/client/public/help-media/files-link/result.webp index 3b8c3e52f..4118926bb 100644 Binary files a/client/public/help-media/files-link/result.webp and b/client/public/help-media/files-link/result.webp differ diff --git a/client/public/help-media/files-link/step-2.webp b/client/public/help-media/files-link/step-2.webp index 5986b459d..067e9c6b9 100644 Binary files a/client/public/help-media/files-link/step-2.webp and b/client/public/help-media/files-link/step-2.webp differ diff --git a/client/public/help-media/files-link/step-3.webp b/client/public/help-media/files-link/step-3.webp index b08118b92..4fa2c3a6c 100644 Binary files a/client/public/help-media/files-link/step-3.webp and b/client/public/help-media/files-link/step-3.webp differ diff --git a/client/public/help-media/files-link/step-4.webp b/client/public/help-media/files-link/step-4.webp index 27a591cd5..8c5f4a3fa 100644 Binary files a/client/public/help-media/files-link/step-4.webp and b/client/public/help-media/files-link/step-4.webp differ diff --git a/client/public/help-media/files-link/step-5.webp b/client/public/help-media/files-link/step-5.webp index df1588035..de254cc90 100644 Binary files a/client/public/help-media/files-link/step-5.webp and b/client/public/help-media/files-link/step-5.webp differ diff --git a/client/public/help-media/files-preview/step-1.webp b/client/public/help-media/files-preview/step-1.webp index e60c0a7a4..1d58f059c 100644 Binary files a/client/public/help-media/files-preview/step-1.webp and b/client/public/help-media/files-preview/step-1.webp differ diff --git a/client/public/help-media/files-preview/step-2.webp b/client/public/help-media/files-preview/step-2.webp index 3954309ad..4980b51bd 100644 Binary files a/client/public/help-media/files-preview/step-2.webp and b/client/public/help-media/files-preview/step-2.webp differ diff --git a/client/public/help-media/files-preview/step-3.webp b/client/public/help-media/files-preview/step-3.webp index d99029bdb..dacb172ec 100644 Binary files a/client/public/help-media/files-preview/step-3.webp and b/client/public/help-media/files-preview/step-3.webp differ diff --git a/client/public/help-media/files-preview/step-4.webp b/client/public/help-media/files-preview/step-4.webp index 7e4088c32..54d4b844b 100644 Binary files a/client/public/help-media/files-preview/step-4.webp and b/client/public/help-media/files-preview/step-4.webp differ diff --git a/client/public/help-media/files-star/result.webp b/client/public/help-media/files-star/result.webp index 4de2655df..95ac1871f 100644 Binary files a/client/public/help-media/files-star/result.webp and b/client/public/help-media/files-star/result.webp differ diff --git a/client/public/help-media/files-star/step-2.webp b/client/public/help-media/files-star/step-2.webp index ec926f945..d58e3a05b 100644 Binary files a/client/public/help-media/files-star/step-2.webp and b/client/public/help-media/files-star/step-2.webp differ diff --git a/client/public/help-media/files-star/step-3.webp b/client/public/help-media/files-star/step-3.webp index fc698c443..3646171e2 100644 Binary files a/client/public/help-media/files-star/step-3.webp and b/client/public/help-media/files-star/step-3.webp differ diff --git a/client/public/help-media/files-sync/result.webp b/client/public/help-media/files-sync/result.webp index f1b307005..c6f9e6146 100644 Binary files a/client/public/help-media/files-sync/result.webp and b/client/public/help-media/files-sync/result.webp differ diff --git a/client/public/help-media/files-sync/step-2.webp b/client/public/help-media/files-sync/step-2.webp index de8cc45fc..89e18d919 100644 Binary files a/client/public/help-media/files-sync/step-2.webp and b/client/public/help-media/files-sync/step-2.webp differ diff --git a/client/public/help-media/files-sync/step-3.webp b/client/public/help-media/files-sync/step-3.webp index cb9d5ea45..7a382f012 100644 Binary files a/client/public/help-media/files-sync/step-3.webp and b/client/public/help-media/files-sync/step-3.webp differ diff --git a/client/public/help-media/files-trash/result.webp b/client/public/help-media/files-trash/result.webp index d5bd92423..2b116cbc9 100644 Binary files a/client/public/help-media/files-trash/result.webp and b/client/public/help-media/files-trash/result.webp differ diff --git a/client/public/help-media/files-trash/step-3.webp b/client/public/help-media/files-trash/step-3.webp index bebe88b67..7f0c3b97b 100644 Binary files a/client/public/help-media/files-trash/step-3.webp and b/client/public/help-media/files-trash/step-3.webp differ diff --git a/client/public/help-media/files-trash/step-4.webp b/client/public/help-media/files-trash/step-4.webp index 0edfea51b..6676aecfd 100644 Binary files a/client/public/help-media/files-trash/step-4.webp and b/client/public/help-media/files-trash/step-4.webp differ diff --git a/client/public/help-media/files-trash/step-5.webp b/client/public/help-media/files-trash/step-5.webp index 72321b425..20cbe34ec 100644 Binary files a/client/public/help-media/files-trash/step-5.webp and b/client/public/help-media/files-trash/step-5.webp differ diff --git a/client/public/help-media/files-upload/result.webp b/client/public/help-media/files-upload/result.webp index 928a4c592..1b1d336df 100644 Binary files a/client/public/help-media/files-upload/result.webp and b/client/public/help-media/files-upload/result.webp differ diff --git a/client/public/help-media/files-upload/step-2.webp b/client/public/help-media/files-upload/step-2.webp index d0cd1a6f7..f39e7bbd4 100644 Binary files a/client/public/help-media/files-upload/step-2.webp and b/client/public/help-media/files-upload/step-2.webp differ diff --git a/client/public/help-media/files-upload/step-3.webp b/client/public/help-media/files-upload/step-3.webp index 07abafb7d..6df496d5d 100644 Binary files a/client/public/help-media/files-upload/step-3.webp and b/client/public/help-media/files-upload/step-3.webp differ diff --git a/client/public/help-media/files-upload/step-4.webp b/client/public/help-media/files-upload/step-4.webp index fb5616ca0..07421e741 100644 Binary files a/client/public/help-media/files-upload/step-4.webp and b/client/public/help-media/files-upload/step-4.webp differ diff --git a/client/public/help-media/filter-bookings/step-5.webp b/client/public/help-media/filter-bookings/step-5.webp index ef2e0b708..eec6d083a 100644 Binary files a/client/public/help-media/filter-bookings/step-5.webp and b/client/public/help-media/filter-bookings/step-5.webp differ diff --git a/client/public/help-media/filter-places/step-1.webp b/client/public/help-media/filter-places/step-1.webp index 35b32d9e9..7b5aa70aa 100644 Binary files a/client/public/help-media/filter-places/step-1.webp and b/client/public/help-media/filter-places/step-1.webp differ diff --git a/client/public/help-media/filter-places/step-2.webp b/client/public/help-media/filter-places/step-2.webp index 1afdc0220..12f068ac7 100644 Binary files a/client/public/help-media/filter-places/step-2.webp and b/client/public/help-media/filter-places/step-2.webp differ diff --git a/client/public/help-media/filter-places/step-3.webp b/client/public/help-media/filter-places/step-3.webp index f39960c7b..86ae10fbe 100644 Binary files a/client/public/help-media/filter-places/step-3.webp and b/client/public/help-media/filter-places/step-3.webp differ diff --git a/client/public/help-media/filter-places/step-4.webp b/client/public/help-media/filter-places/step-4.webp index 77271a655..01927717c 100644 Binary files a/client/public/help-media/filter-places/step-4.webp and b/client/public/help-media/filter-places/step-4.webp differ diff --git a/client/public/help-media/gallery-map/step-1.webp b/client/public/help-media/gallery-map/step-1.webp index 00d2d9c6c..82cb619b5 100644 Binary files a/client/public/help-media/gallery-map/step-1.webp and b/client/public/help-media/gallery-map/step-1.webp differ diff --git a/client/public/help-media/gallery-map/step-2.webp b/client/public/help-media/gallery-map/step-2.webp index be5262593..3aae3bda2 100644 Binary files a/client/public/help-media/gallery-map/step-2.webp and b/client/public/help-media/gallery-map/step-2.webp differ diff --git a/client/public/help-media/import-booking-file/step-5.webp b/client/public/help-media/import-booking-file/step-5.webp index f4c15bd8f..27ec9fb15 100644 Binary files a/client/public/help-media/import-booking-file/step-5.webp and b/client/public/help-media/import-booking-file/step-5.webp differ diff --git a/client/public/help-media/import-places-file/result.webp b/client/public/help-media/import-places-file/result.webp index 2a269c2d8..a31c7a7db 100644 Binary files a/client/public/help-media/import-places-file/result.webp and b/client/public/help-media/import-places-file/result.webp differ diff --git a/client/public/help-media/import-places-file/step-1.webp b/client/public/help-media/import-places-file/step-1.webp index 6716d65bb..a8eec82ce 100644 Binary files a/client/public/help-media/import-places-file/step-1.webp and b/client/public/help-media/import-places-file/step-1.webp differ diff --git a/client/public/help-media/import-places-file/step-2.webp b/client/public/help-media/import-places-file/step-2.webp index a5b6bc712..a4a83c18c 100644 Binary files a/client/public/help-media/import-places-file/step-2.webp and b/client/public/help-media/import-places-file/step-2.webp differ diff --git a/client/public/help-media/import-places-file/step-3.webp b/client/public/help-media/import-places-file/step-3.webp index b39da6f42..eec837d93 100644 Binary files a/client/public/help-media/import-places-file/step-3.webp and b/client/public/help-media/import-places-file/step-3.webp differ diff --git a/client/public/help-media/import-places-file/step-4.webp b/client/public/help-media/import-places-file/step-4.webp index d0d0e7097..6eb995cd0 100644 Binary files a/client/public/help-media/import-places-file/step-4.webp and b/client/public/help-media/import-places-file/step-4.webp differ diff --git a/client/public/help-media/import-places-list/step-1.webp b/client/public/help-media/import-places-list/step-1.webp index fac8f139e..3835066eb 100644 Binary files a/client/public/help-media/import-places-list/step-1.webp and b/client/public/help-media/import-places-list/step-1.webp differ diff --git a/client/public/help-media/import-places-list/step-2.webp b/client/public/help-media/import-places-list/step-2.webp index 24e4c7b1d..25d6ea2ad 100644 Binary files a/client/public/help-media/import-places-list/step-2.webp and b/client/public/help-media/import-places-list/step-2.webp differ diff --git a/client/public/help-media/import-places-list/step-3.webp b/client/public/help-media/import-places-list/step-3.webp index e4a6a8c83..a53bd1960 100644 Binary files a/client/public/help-media/import-places-list/step-3.webp and b/client/public/help-media/import-places-list/step-3.webp differ diff --git a/client/public/help-media/install-plugin/step-1.webp b/client/public/help-media/install-plugin/step-1.webp index 6568ed63a..2a9cee77a 100644 Binary files a/client/public/help-media/install-plugin/step-1.webp and b/client/public/help-media/install-plugin/step-1.webp differ diff --git a/client/public/help-media/map-add-place/result.webp b/client/public/help-media/map-add-place/result.webp index 3d07dd1f1..eb8f87949 100644 Binary files a/client/public/help-media/map-add-place/result.webp and b/client/public/help-media/map-add-place/result.webp differ diff --git a/client/public/help-media/map-add-place/step-1.webp b/client/public/help-media/map-add-place/step-1.webp index 42b307141..971fb94be 100644 Binary files a/client/public/help-media/map-add-place/step-1.webp and b/client/public/help-media/map-add-place/step-1.webp differ diff --git a/client/public/help-media/map-add-place/step-2.webp b/client/public/help-media/map-add-place/step-2.webp index 82a0b8d89..33a380b6e 100644 Binary files a/client/public/help-media/map-add-place/step-2.webp and b/client/public/help-media/map-add-place/step-2.webp differ diff --git a/client/public/help-media/map-add-place/step-3.webp b/client/public/help-media/map-add-place/step-3.webp index 823e946a6..fff3b47a1 100644 Binary files a/client/public/help-media/map-add-place/step-3.webp and b/client/public/help-media/map-add-place/step-3.webp differ diff --git a/client/public/help-media/map-add-place/step-4.webp b/client/public/help-media/map-add-place/step-4.webp index 68f5e06a6..fd227f45a 100644 Binary files a/client/public/help-media/map-add-place/step-4.webp and b/client/public/help-media/map-add-place/step-4.webp differ diff --git a/client/public/help-media/map-dawarich-trail/result.webp b/client/public/help-media/map-dawarich-trail/result.webp index 683024205..e5590d6ce 100644 Binary files a/client/public/help-media/map-dawarich-trail/result.webp and b/client/public/help-media/map-dawarich-trail/result.webp differ diff --git a/client/public/help-media/map-dawarich-trail/step-1.webp b/client/public/help-media/map-dawarich-trail/step-1.webp index 403314c8c..41e3338b2 100644 Binary files a/client/public/help-media/map-dawarich-trail/step-1.webp and b/client/public/help-media/map-dawarich-trail/step-1.webp differ diff --git a/client/public/help-media/map-dawarich-trail/step-2.webp b/client/public/help-media/map-dawarich-trail/step-2.webp index 4f527789d..7f63319a4 100644 Binary files a/client/public/help-media/map-dawarich-trail/step-2.webp and b/client/public/help-media/map-dawarich-trail/step-2.webp differ diff --git a/client/public/help-media/map-dawarich-trail/step-3.webp b/client/public/help-media/map-dawarich-trail/step-3.webp index 8eaeca7a1..cfc314c64 100644 Binary files a/client/public/help-media/map-dawarich-trail/step-3.webp and b/client/public/help-media/map-dawarich-trail/step-3.webp differ diff --git a/client/public/help-media/map-markers/step-1.webp b/client/public/help-media/map-markers/step-1.webp index eb8122a12..ca2b22a9c 100644 Binary files a/client/public/help-media/map-markers/step-1.webp and b/client/public/help-media/map-markers/step-1.webp differ diff --git a/client/public/help-media/map-markers/step-2.webp b/client/public/help-media/map-markers/step-2.webp index da1e070eb..01e9d0e9a 100644 Binary files a/client/public/help-media/map-markers/step-2.webp and b/client/public/help-media/map-markers/step-2.webp differ diff --git a/client/public/help-media/map-markers/step-3.webp b/client/public/help-media/map-markers/step-3.webp index 684b39824..a36ecaf49 100644 Binary files a/client/public/help-media/map-markers/step-3.webp and b/client/public/help-media/map-markers/step-3.webp differ diff --git a/client/public/help-media/map-markers/step-4.webp b/client/public/help-media/map-markers/step-4.webp index 81b860792..9bf9b75f4 100644 Binary files a/client/public/help-media/map-markers/step-4.webp and b/client/public/help-media/map-markers/step-4.webp differ diff --git a/client/public/help-media/map-markers/step-5.webp b/client/public/help-media/map-markers/step-5.webp index 9dc355ebc..2616dc13e 100644 Binary files a/client/public/help-media/map-markers/step-5.webp and b/client/public/help-media/map-markers/step-5.webp differ diff --git a/client/public/help-media/map-satellite/step-1.webp b/client/public/help-media/map-satellite/step-1.webp index 75fda21ab..d050834da 100644 Binary files a/client/public/help-media/map-satellite/step-1.webp and b/client/public/help-media/map-satellite/step-1.webp differ diff --git a/client/public/help-media/map-satellite/step-2.webp b/client/public/help-media/map-satellite/step-2.webp index b5bf2f00b..cbf9762d1 100644 Binary files a/client/public/help-media/map-satellite/step-2.webp and b/client/public/help-media/map-satellite/step-2.webp differ diff --git a/client/public/help-media/map-satellite/step-3.webp b/client/public/help-media/map-satellite/step-3.webp index f0586c0a4..98f168804 100644 Binary files a/client/public/help-media/map-satellite/step-3.webp and b/client/public/help-media/map-satellite/step-3.webp differ diff --git a/client/public/help-media/map-whole-trip/result.webp b/client/public/help-media/map-whole-trip/result.webp index 8e3463bdf..3923f6fa1 100644 Binary files a/client/public/help-media/map-whole-trip/result.webp and b/client/public/help-media/map-whole-trip/result.webp differ diff --git a/client/public/help-media/map-whole-trip/step-1.webp b/client/public/help-media/map-whole-trip/step-1.webp index b48549f5b..642dd2455 100644 Binary files a/client/public/help-media/map-whole-trip/step-1.webp and b/client/public/help-media/map-whole-trip/step-1.webp differ diff --git a/client/public/help-media/map-whole-trip/step-2.webp b/client/public/help-media/map-whole-trip/step-2.webp index d3f7c9c9e..e51307019 100644 Binary files a/client/public/help-media/map-whole-trip/step-2.webp and b/client/public/help-media/map-whole-trip/step-2.webp differ diff --git a/client/public/help-media/map-whole-trip/step-3.webp b/client/public/help-media/map-whole-trip/step-3.webp index eb31d0c3f..1b91c65f9 100644 Binary files a/client/public/help-media/map-whole-trip/step-3.webp and b/client/public/help-media/map-whole-trip/step-3.webp differ diff --git a/client/public/help-media/map-whole-trip/step-4.webp b/client/public/help-media/map-whole-trip/step-4.webp index c6f3d75b1..13b51d03a 100644 Binary files a/client/public/help-media/map-whole-trip/step-4.webp and b/client/public/help-media/map-whole-trip/step-4.webp differ diff --git a/client/public/help-media/packing-categories/step-1.webp b/client/public/help-media/packing-categories/step-1.webp index cd1883285..2825146d2 100644 Binary files a/client/public/help-media/packing-categories/step-1.webp and b/client/public/help-media/packing-categories/step-1.webp differ diff --git a/client/public/help-media/place-onto-day/result.webp b/client/public/help-media/place-onto-day/result.webp index 819f4f517..c30cbf634 100644 Binary files a/client/public/help-media/place-onto-day/result.webp and b/client/public/help-media/place-onto-day/result.webp differ diff --git a/client/public/help-media/place-onto-day/step-1.webp b/client/public/help-media/place-onto-day/step-1.webp index c9d247db4..02acd21d4 100644 Binary files a/client/public/help-media/place-onto-day/step-1.webp and b/client/public/help-media/place-onto-day/step-1.webp differ diff --git a/client/public/help-media/place-onto-day/step-2.webp b/client/public/help-media/place-onto-day/step-2.webp index aa5bf8973..b4f40492b 100644 Binary files a/client/public/help-media/place-onto-day/step-2.webp and b/client/public/help-media/place-onto-day/step-2.webp differ diff --git a/client/public/help-media/place-onto-day/step-3.webp b/client/public/help-media/place-onto-day/step-3.webp index dd5f9b8bf..f5aec9813 100644 Binary files a/client/public/help-media/place-onto-day/step-3.webp and b/client/public/help-media/place-onto-day/step-3.webp differ diff --git a/client/public/help-media/place-onto-day/step-4.webp b/client/public/help-media/place-onto-day/step-4.webp index d89d2a05b..41a434233 100644 Binary files a/client/public/help-media/place-onto-day/step-4.webp and b/client/public/help-media/place-onto-day/step-4.webp differ diff --git a/client/public/help-media/place-to-open-day/step-1.webp b/client/public/help-media/place-to-open-day/step-1.webp index 084e23a31..308674a79 100644 Binary files a/client/public/help-media/place-to-open-day/step-1.webp and b/client/public/help-media/place-to-open-day/step-1.webp differ diff --git a/client/public/help-media/place-to-open-day/step-2.webp b/client/public/help-media/place-to-open-day/step-2.webp index d304cedc9..75bd17f27 100644 Binary files a/client/public/help-media/place-to-open-day/step-2.webp and b/client/public/help-media/place-to-open-day/step-2.webp differ diff --git a/client/public/help-media/place-to-open-day/step-3.webp b/client/public/help-media/place-to-open-day/step-3.webp index e47ad764c..1c5df4c69 100644 Binary files a/client/public/help-media/place-to-open-day/step-3.webp and b/client/public/help-media/place-to-open-day/step-3.webp differ diff --git a/client/public/help-media/place-to-open-day/step-4.webp b/client/public/help-media/place-to-open-day/step-4.webp new file mode 100644 index 000000000..1615a2137 Binary files /dev/null and b/client/public/help-media/place-to-open-day/step-4.webp differ diff --git a/client/public/help-media/select-places/result.webp b/client/public/help-media/select-places/result.webp index 156db33a5..0d1d7f84a 100644 Binary files a/client/public/help-media/select-places/result.webp and b/client/public/help-media/select-places/result.webp differ diff --git a/client/public/help-media/select-places/step-1.webp b/client/public/help-media/select-places/step-1.webp index 233ae8871..c2837376b 100644 Binary files a/client/public/help-media/select-places/step-1.webp and b/client/public/help-media/select-places/step-1.webp differ diff --git a/client/public/help-media/select-places/step-2.webp b/client/public/help-media/select-places/step-2.webp index b360e142c..60885a644 100644 Binary files a/client/public/help-media/select-places/step-2.webp and b/client/public/help-media/select-places/step-2.webp differ diff --git a/client/public/help-media/select-places/step-3.webp b/client/public/help-media/select-places/step-3.webp index 3993111a9..60885a644 100644 Binary files a/client/public/help-media/select-places/step-3.webp and b/client/public/help-media/select-places/step-3.webp differ diff --git a/client/public/help-media/select-places/step-4.webp b/client/public/help-media/select-places/step-4.webp index 4ed868023..7bfae7b52 100644 Binary files a/client/public/help-media/select-places/step-4.webp and b/client/public/help-media/select-places/step-4.webp differ diff --git a/client/public/help-media/studio-elements/step-1.webp b/client/public/help-media/studio-elements/step-1.webp index 5d839c924..b71e619c4 100644 Binary files a/client/public/help-media/studio-elements/step-1.webp and b/client/public/help-media/studio-elements/step-1.webp differ diff --git a/client/public/help-media/studio-elements/step-2.webp b/client/public/help-media/studio-elements/step-2.webp index 774dfbebe..f9b04dfcf 100644 Binary files a/client/public/help-media/studio-elements/step-2.webp and b/client/public/help-media/studio-elements/step-2.webp differ diff --git a/client/public/help-media/studio-export/step-1.webp b/client/public/help-media/studio-export/step-1.webp index bb1a10d6a..6f485206f 100644 Binary files a/client/public/help-media/studio-export/step-1.webp and b/client/public/help-media/studio-export/step-1.webp differ diff --git a/client/public/help-media/studio-export/step-2.webp b/client/public/help-media/studio-export/step-2.webp index f9806bd9a..8bf79821a 100644 Binary files a/client/public/help-media/studio-export/step-2.webp and b/client/public/help-media/studio-export/step-2.webp differ diff --git a/client/public/help-media/studio-export/step-3.webp b/client/public/help-media/studio-export/step-3.webp index 317679101..0e9f3977c 100644 Binary files a/client/public/help-media/studio-export/step-3.webp and b/client/public/help-media/studio-export/step-3.webp differ diff --git a/client/public/help-media/studio-properties/step-1.webp b/client/public/help-media/studio-properties/step-1.webp index bb05ac2da..2d21f0871 100644 Binary files a/client/public/help-media/studio-properties/step-1.webp and b/client/public/help-media/studio-properties/step-1.webp differ diff --git a/client/public/help-media/studio-properties/step-2.webp b/client/public/help-media/studio-properties/step-2.webp index 5109b4771..ef6196963 100644 Binary files a/client/public/help-media/studio-properties/step-2.webp and b/client/public/help-media/studio-properties/step-2.webp differ diff --git a/client/public/help-media/studio-properties/step-3.webp b/client/public/help-media/studio-properties/step-3.webp index 733745ed1..bb56697b0 100644 Binary files a/client/public/help-media/studio-properties/step-3.webp and b/client/public/help-media/studio-properties/step-3.webp differ diff --git a/client/public/help-media/studio-travel/step-1.webp b/client/public/help-media/studio-travel/step-1.webp index 7f6071579..7b5291ef9 100644 Binary files a/client/public/help-media/studio-travel/step-1.webp and b/client/public/help-media/studio-travel/step-1.webp differ diff --git a/client/public/help-media/studio-travel/step-2.webp b/client/public/help-media/studio-travel/step-2.webp index 96776daab..8390410c0 100644 Binary files a/client/public/help-media/studio-travel/step-2.webp and b/client/public/help-media/studio-travel/step-2.webp differ diff --git a/client/public/help-media/transports-list/step-3.webp b/client/public/help-media/transports-list/step-3.webp index e27ece648..9a3fb72d8 100644 Binary files a/client/public/help-media/transports-list/step-3.webp and b/client/public/help-media/transports-list/step-3.webp differ diff --git a/client/public/help-media/transports-list/step-4.webp b/client/public/help-media/transports-list/step-4.webp index 0fdfd7af0..e88557b10 100644 Binary files a/client/public/help-media/transports-list/step-4.webp and b/client/public/help-media/transports-list/step-4.webp differ diff --git a/client/public/help-media/transports-list/step-5.webp b/client/public/help-media/transports-list/step-5.webp index 06b4398cc..82a60adf5 100644 Binary files a/client/public/help-media/transports-list/step-5.webp and b/client/public/help-media/transports-list/step-5.webp differ diff --git a/client/src/components/Help/HelpGuideView.tsx b/client/src/components/Help/HelpGuideView.tsx index 0caebfdb5..508e26517 100644 --- a/client/src/components/Help/HelpGuideView.tsx +++ b/client/src/components/Help/HelpGuideView.tsx @@ -110,10 +110,13 @@ export default function HelpGuideView({ guide }: { guide: HelpGuide }): React.Re className="trek-help-shot group relative block w-full rounded-xl overflow-hidden border border-edge-secondary bg-surface-tertiary" aria-label={alt} > - - - {t('help.center.screenshot')} - + {/* No number over the picture: the step it belongs to is + numbered beside it, and the picture carries the ring's + own badge. Contained, not covered: a step whose target + did not fit the 16:10 window was shot wider than this + box, and covering it would crop away the sides the + ring is drawn on. */} + @@ -142,7 +145,7 @@ export default function HelpGuideView({ guide }: { guide: HelpGuide }): React.Re onClick={() => openLightbox({ src: helpMedia.result(guide.id), alt: t(guideKey(guide.id, 'result')) })} className="trek-help-shot group relative rounded-xl overflow-hidden border border-edge-secondary bg-surface-tertiary" > - + {t('help.center.result')} diff --git a/client/src/components/Help/HelpHome.tsx b/client/src/components/Help/HelpHome.tsx index 3a68c5352..65fe9c494 100644 --- a/client/src/components/Help/HelpHome.tsx +++ b/client/src/components/Help/HelpHome.tsx @@ -111,7 +111,7 @@ export default function HelpHome({ context }: { context: HelpContext | null }): alt="" loading="lazy" decoding="async" - className="block w-full aspect-video object-cover" + className="block w-full aspect-video object-contain" /> {t(ctxKey(context.id, 'title'))} diff --git a/client/src/help/contexts/tripPlaces.ts b/client/src/help/contexts/tripPlaces.ts index fb73dddd1..3e0f28c9d 100644 --- a/client/src/help/contexts/tripPlaces.ts +++ b/client/src/help/contexts/tripPlaces.ts @@ -18,7 +18,7 @@ export const { context: tripPlacesContext, guides: tripPlacesGuides } = defineSc docs: [{ slug: PLACES }, IMPORT], guides: [ ['create-place', 'plus', 'guide', 5, 3, { slug: PLACES, anchor: 'adding-a-place' }, ['place-to-open-day', 'edit-place'], true], - ['place-to-open-day', 'calendarCheck', 'quick', 3, 2, { slug: PLACES, anchor: 'the-open-day-steers-the-search' }, ['create-place']], + ['place-to-open-day', 'calendarCheck', 'quick', 4, 2, { slug: PLACES, anchor: 'the-open-day-steers-the-search' }, ['create-place', 'place-onto-day']], ['filter-places', 'filter', 'quick', 4, 2, { slug: PLACES, anchor: 'searching-for-a-place' }, ['select-places']], ['edit-place', 'pencil', 'quick', 3, 2, { slug: PLACES, anchor: 'place-fields' }, ['create-place', 'delete-place']], ['delete-place', 'trash', 'quick', 2, 2, { slug: PLACES }, ['edit-place', 'select-places', 'undo-change']], diff --git a/shared/src/i18n/ar/help.ts b/shared/src/i18n/ar/help.ts index 0728c041b..7b0534cdb 100644 --- a/shared/src/i18n/ar/help.ts +++ b/shared/src/i18n/ar/help.ts @@ -1693,8 +1693,11 @@ const help: TranslationStrings = { '«إلى اليوم» يفتح النموذج نفسه الذي يفتحه «مكان جديد»، غير أن المكان يهبط في اليوم المفتوح لحظة نقرك على «إضافة».', 'help.guide.place-to-open-day.step.3': 'المكان الموجود أصلًا ينتقل إلى اليوم المفتوح بعلامة + في نهاية صفه، أو بالنقر بزر الفأرة الأيمن ثم «+ يوم».', + 'help.guide.place-to-open-day.step.4': + 'والعكس يعمل أيضًا، ومن دون فتح يوم أولًا: اسحب صف المكان من العمود وأفلته على بطاقة يوم. وإن أفلته بين محطتين، فسيحطّ هناك بالضبط.', 'help.guide.place-to-open-day.result': 'يُسرد المكان تحت اليوم، في نهايته؛ اسحبه لأعلى أو لأسفل إلى موضعه الصحيح.', - 'help.guide.place-to-open-day.tip.1': 'سحب الصف إلى يوم يعمل كذلك، ويمكنه إفلات المكان بين محطتين مباشرة.', + 'help.guide.place-to-open-day.tip.1': + 'اليوم المفتوح يوجّه البحث أيضًا: مع يوم مفتوح، تبدأ الخريطة والبحث القريب من حيث يمضي ذلك اليوم أصلًا.', 'help.guide.place-to-open-day.tip.2': '«تراجع» في شريط الأدوات فوق الأيام يسترجع الإسناد.', // filter-places 'help.guide.filter-places.title': 'العثور على مكان في القائمة', diff --git a/shared/src/i18n/br/help.ts b/shared/src/i18n/br/help.ts index 0356d0dec..e563301e5 100644 --- a/shared/src/i18n/br/help.ts +++ b/shared/src/i18n/br/help.ts @@ -1857,10 +1857,12 @@ const help: TranslationStrings = { 'Para o dia abre o mesmo formulário que Novo local, só que o lugar cai no dia aberto no momento em que você clica em Adicionar.', 'help.guide.place-to-open-day.step.3': 'Um lugar que já existe vai para o dia aberto com o + no fim da linha dele, ou com o botão direito, + Dia.', + 'help.guide.place-to-open-day.step.4': + 'O caminho inverso também funciona, e sem abrir um dia antes: arraste a linha do lugar para fora da coluna e solte-a em um cartão de dia. Solto entre duas paradas, ele fica exatamente ali.', 'help.guide.place-to-open-day.result': 'O lugar fica listado sob o dia, no fim; arraste-o para cima ou para baixo até onde ele pertence.', 'help.guide.place-to-open-day.tip.1': - 'Arrastar uma linha para um dia funciona igual, e assim dá para soltar o lugar entre duas paradas de uma vez.', + 'O dia aberto também guia a busca: com um dia aberto, o mapa e a busca por perto partem de onde esse dia já passa.', 'help.guide.place-to-open-day.tip.2': 'Desfazer na barra de ferramentas acima dos dias reverte a atribuição.', // filter-places 'help.guide.filter-places.title': 'Achar um lugar na lista', diff --git a/shared/src/i18n/ca/help.ts b/shared/src/i18n/ca/help.ts index a882054b6..6f7a4fdee 100644 --- a/shared/src/i18n/ca/help.ts +++ b/shared/src/i18n/ca/help.ts @@ -1895,10 +1895,12 @@ const help: TranslationStrings = { 'Al dia obre el mateix formulari que Lloc nou, només que el lloc va a parar al dia obert en el moment que fas clic a Afegir.', 'help.guide.place-to-open-day.step.3': 'Un lloc que ja existeix va al dia obert amb el + al final de la seva fila, o amb clic dret, + Dia.', + 'help.guide.place-to-open-day.step.4': + 'A l’inrevés també funciona, i sense obrir abans cap dia: arrossega la fila del lloc fora de la columna i deixa-la anar sobre una targeta de dia. Si la deixes entre dues parades, hi va a parar exactament.', 'help.guide.place-to-open-day.result': 'El lloc queda llistat sota el dia, al final; arrossega’l amunt o avall fins on toca.', 'help.guide.place-to-open-day.tip.1': - 'Arrossegar una fila a un dia funciona igual, i així el lloc es pot deixar de seguida entre dues parades.', + 'El dia obert també guia la cerca: amb un dia obert, el mapa i la cerca dels voltants parteixen d’on ja va aquell dia.', 'help.guide.place-to-open-day.tip.2': 'Desfés, a la barra d’eines sobre els dies, retira l’assignació.', // filter-places 'help.guide.filter-places.title': 'Trobar un lloc a la llista', diff --git a/shared/src/i18n/cs/help.ts b/shared/src/i18n/cs/help.ts index a31f601f6..a57c961bd 100644 --- a/shared/src/i18n/cs/help.ts +++ b/shared/src/i18n/cs/help.ts @@ -1805,10 +1805,12 @@ const help: TranslationStrings = { 'Ke dni otevře stejný formulář jako Nové místo, jen místo přistane na otevřeném dni ve chvíli, kdy kliknete na Přidat.', 'help.guide.place-to-open-day.step.3': 'Místo, které už existuje, jde na otevřený den přes + na konci svého řádku nebo pravým tlačítkem, + Den.', + 'help.guide.place-to-open-day.step.4': + 'Funguje to i obráceně, a bez toho, abys nejdřív otevřel den: přetáhni řádek místa ze sloupce a pusť ho na kartu dne. Když ho pustíš mezi dvě zastávky, přistane přesně tam.', 'help.guide.place-to-open-day.result': 'Místo je vypsané pod dnem, na konci; přetažením nahoru nebo dolů ho dáte tam, kam patří.', 'help.guide.place-to-open-day.tip.1': - 'Přetažení řádku na den funguje také a může místo rovnou položit mezi dvě zastávky.', + 'Otevřený den řídí i hledání: když je den otevřený, mapa i hledání v okolí začínají tam, kam ten den stejně vede.', 'help.guide.place-to-open-day.tip.2': 'Zpět v liště nástrojů nad dny přiřazení vrátí.', // filter-places 'help.guide.filter-places.title': 'Najít místo v seznamu', diff --git a/shared/src/i18n/de/help.ts b/shared/src/i18n/de/help.ts index c3d321fa8..42540157f 100644 --- a/shared/src/i18n/de/help.ts +++ b/shared/src/i18n/de/help.ts @@ -1908,10 +1908,12 @@ const help: TranslationStrings = { 'Zum Tag öffnet dasselbe Formular wie Neuer Ort, nur landet der Ort in dem Moment auf dem geöffneten Tag, in dem du auf Hinzufügen klickst.', 'help.guide.place-to-open-day.step.3': 'Ein Ort, den es schon gibt, kommt mit dem + am Ende seiner Zeile auf den geöffneten Tag, oder per Rechtsklick über + Tag.', + 'help.guide.place-to-open-day.step.4': + 'Andersherum geht es auch, und ohne vorher einen Tag zu öffnen: Zieh die Zeile des Ortes aus der Spalte und lass sie auf einer Tageskarte los. Zwischen zwei Stopps abgelegt, landet er genau dort.', 'help.guide.place-to-open-day.result': 'Der Ort steht unter dem Tag, ganz am Ende; zieh ihn hoch oder runter an seinen Platz.', 'help.guide.place-to-open-day.tip.1': - 'Eine Zeile auf einen Tag zu ziehen geht genauso, und dabei lässt sich der Ort gleich zwischen zwei Stopps ablegen.', + 'Der geöffnete Tag steuert auch die Suche: Ist ein Tag offen, starten die Karte und die Umkreissuche dort, wo dieser Tag ohnehin hinführt.', 'help.guide.place-to-open-day.tip.2': 'Rückgängig in der Werkzeugleiste über den Tagen nimmt die Zuordnung zurück.', // filter-places 'help.guide.filter-places.title': 'Einen Ort in der Liste finden', diff --git a/shared/src/i18n/en/help.ts b/shared/src/i18n/en/help.ts index 6e902eb5f..17efcd8fa 100644 --- a/shared/src/i18n/en/help.ts +++ b/shared/src/i18n/en/help.ts @@ -1804,10 +1804,12 @@ const help: TranslationStrings = { 'To day opens the same form as New place, only the place lands on the open day the moment you click Add.', 'help.guide.place-to-open-day.step.3': 'A place that already exists goes onto the open day with the + at the end of its row, or by right-click, Add to day.', + 'help.guide.place-to-open-day.step.4': + 'The other way round works too, and without opening a day first: drag the place’s row out of the column and drop it on a day card. Dropped between two stops it lands exactly there.', 'help.guide.place-to-open-day.result': 'The place is listed under the day, at the end; drag it up or down to where it belongs.', 'help.guide.place-to-open-day.tip.1': - 'Dragging a row onto a day works as well, and it can drop the place between two stops right away.', + 'The open day also steers the search: with a day open, the map and the nearby search start from where that day already goes.', 'help.guide.place-to-open-day.tip.2': 'Undo in the toolbar above the days takes the assignment back.', // filter-places 'help.guide.filter-places.title': 'Find a place in the list', diff --git a/shared/src/i18n/es/help.ts b/shared/src/i18n/es/help.ts index dc6f460de..0438b3884 100644 --- a/shared/src/i18n/es/help.ts +++ b/shared/src/i18n/es/help.ts @@ -1904,10 +1904,12 @@ const help: TranslationStrings = { 'Al día abre el mismo formulario que Nuevo lugar, solo que el lugar cae en el día abierto en cuanto haces clic en Añadir.', 'help.guide.place-to-open-day.step.3': 'Un lugar que ya existe va al día abierto con el + al final de su fila, o con clic derecho, + Día.', + 'help.guide.place-to-open-day.step.4': + 'Al revés también funciona, y sin abrir antes ningún día: arrastra la fila del lugar fuera de la columna y suéltala sobre una tarjeta de día. Si la sueltas entre dos paradas, aterriza justo ahí.', 'help.guide.place-to-open-day.result': 'El lugar queda listado bajo el día, al final; arrástralo arriba o abajo hasta donde le toca.', 'help.guide.place-to-open-day.tip.1': - 'Arrastrar una fila a un día funciona igual, y así el lugar se puede soltar entre dos paradas de una vez.', + 'El día abierto también guía la búsqueda: con un día abierto, el mapa y la búsqueda cercana parten de donde ya va ese día.', 'help.guide.place-to-open-day.tip.2': 'Deshacer, en la barra de herramientas sobre los días, revierte la asignación.', // filter-places 'help.guide.filter-places.title': 'Encontrar un lugar en la lista', diff --git a/shared/src/i18n/fr/help.ts b/shared/src/i18n/fr/help.ts index 233751f47..33f018548 100644 --- a/shared/src/i18n/fr/help.ts +++ b/shared/src/i18n/fr/help.ts @@ -1946,10 +1946,12 @@ const help: TranslationStrings = { 'Au jour ouvre le même formulaire que Nouveau lieu, sauf que le lieu se pose sur le jour ouvert au moment où vous cliquez sur Ajouter.', 'help.guide.place-to-open-day.step.3': 'Un lieu qui existe déjà va sur le jour ouvert avec le + au bout de sa ligne, ou par un clic droit, + Jour.', + 'help.guide.place-to-open-day.step.4': + 'L’inverse marche aussi, et sans ouvrir de jour au préalable : fais glisser la ligne du lieu hors de la colonne et lâche-la sur une carte de jour. Lâchée entre deux étapes, elle se pose exactement là.', 'help.guide.place-to-open-day.result': 'Le lieu est listé sous le jour, à la fin ; glissez-le vers le haut ou vers le bas jusqu’à sa place.', 'help.guide.place-to-open-day.tip.1': - 'Glisser une ligne sur un jour marche aussi, et cela peut déposer le lieu entre deux étapes tout de suite.', + 'Le jour ouvert oriente aussi la recherche : un jour ouvert, la carte et la recherche à proximité partent de là où ce jour passe déjà.', 'help.guide.place-to-open-day.tip.2': 'Annuler dans la barre d’outils au-dessus des jours reprend l’affectation.', // filter-places 'help.guide.filter-places.title': 'Trouver un lieu dans la liste', diff --git a/shared/src/i18n/gr/help.ts b/shared/src/i18n/gr/help.ts index 6b2c0b2bf..07f4b46d9 100644 --- a/shared/src/i18n/gr/help.ts +++ b/shared/src/i18n/gr/help.ts @@ -1955,10 +1955,12 @@ const help: TranslationStrings = { 'Το Στην ημέρα ανοίγει την ίδια φόρμα με το Νέο σημείο, μόνο που το μέρος προσγειώνεται στην ανοιχτή ημέρα τη στιγμή που κάνετε κλικ στο Προσθήκη.', 'help.guide.place-to-open-day.step.3': 'Ένα μέρος που υπάρχει ήδη πάει στην ανοιχτή ημέρα με το + στο τέλος της σειράς του ή με δεξί κλικ, + Ημέρα.', + 'help.guide.place-to-open-day.step.4': + 'Και το αντίστροφο δουλεύει, χωρίς να ανοίξεις πρώτα μια ημέρα: σύρε τη σειρά του μέρους έξω από τη στήλη και άσε την πάνω σε μια κάρτα ημέρας. Αν την αφήσεις ανάμεσα σε δύο στάσεις, προσγειώνεται ακριβώς εκεί.', 'help.guide.place-to-open-day.result': 'Το μέρος απαριθμείται κάτω από την ημέρα, στο τέλος· σύρετέ το πάνω ή κάτω εκεί που ανήκει.', 'help.guide.place-to-open-day.tip.1': - 'Το σύρσιμο μιας σειράς πάνω σε μια ημέρα δουλεύει εξίσου, και μπορεί να αφήσει το μέρος ανάμεσα σε δύο στάσεις αμέσως.', + 'Η ανοιχτή ημέρα καθοδηγεί και την αναζήτηση: με μια ημέρα ανοιχτή, ο χάρτης και η αναζήτηση κοντά ξεκινούν από εκεί που πάει έτσι κι αλλιώς η ημέρα.', 'help.guide.place-to-open-day.tip.2': 'Η Αναίρεση στη γραμμή εργαλείων πάνω από τις ημέρες παίρνει πίσω την ανάθεση.', // filter-places 'help.guide.filter-places.title': 'Εύρεση μέρους στη λίστα', diff --git a/shared/src/i18n/hu/help.ts b/shared/src/i18n/hu/help.ts index 6e9b60f6a..499f2084a 100644 --- a/shared/src/i18n/hu/help.ts +++ b/shared/src/i18n/hu/help.ts @@ -1901,8 +1901,11 @@ const help: TranslationStrings = { 'A naphoz ugyanazt az űrlapot nyitja meg, mint az Új hely, csak a hely abban a pillanatban a megnyitott napra kerül, ahogy a Hozzáadás gombra kattintasz.', 'help.guide.place-to-open-day.step.3': 'Egy már létező hely a sora végén lévő + gombbal vagy jobb gombbal, a + Nap ponttal kerül a megnyitott napra.', + 'help.guide.place-to-open-day.step.4': + 'Fordítva is megy, és anélkül, hogy előbb megnyitnál egy napot: húzd ki a hely sorát az oszlopból, és ejtsd rá egy nap kártyájára. Két megálló közé ejtve pontosan oda kerül.', 'help.guide.place-to-open-day.result': 'A hely a nap alatt szerepel, a végén; húzd fel vagy le oda, ahová tartozik.', - 'help.guide.place-to-open-day.tip.1': 'Egy sor napra húzása is működik, és rögtön két megálló közé ejtheti a helyet.', + 'help.guide.place-to-open-day.tip.1': + 'A megnyitott nap a keresést is irányítja: nyitott nappal a térkép és a közeli keresés onnan indul, amerre az a nap amúgy is jár.', 'help.guide.place-to-open-day.tip.2': 'A napok fölötti eszköztárban a Visszavonás visszaveszi a hozzárendelést.', // filter-places 'help.guide.filter-places.title': 'Hely megtalálása a listában', diff --git a/shared/src/i18n/id/help.ts b/shared/src/i18n/id/help.ts index d87e228c7..f9631d3cb 100644 --- a/shared/src/i18n/id/help.ts +++ b/shared/src/i18n/id/help.ts @@ -1895,10 +1895,12 @@ const help: TranslationStrings = { '“Ke hari” membuka formulir yang sama seperti “Tempat baru”, hanya saja tempat itu mendarat pada hari yang dibuka begitu Anda klik “Tambah”.', 'help.guide.place-to-open-day.step.3': 'Tempat yang sudah ada masuk ke hari yang dibuka lewat tanda + di ujung barisnya, atau dengan klik kanan, “+ Hari”.', + 'help.guide.place-to-open-day.step.4': + 'Sebaliknya juga bisa, dan tanpa membuka hari lebih dulu: seret baris tempat itu keluar dari kolom lalu jatuhkan ke kartu sebuah hari. Dijatuhkan di antara dua perhentian, ia mendarat tepat di situ.', 'help.guide.place-to-open-day.result': 'Tempat itu terdaftar di bawah hari tersebut, di urutan terakhir; seret ke atas atau ke bawah ke posisi yang semestinya.', 'help.guide.place-to-open-day.tip.1': - 'Menyeret sebuah baris ke sebuah hari juga bisa, dan itu langsung dapat menjatuhkan tempat di antara dua perhentian.', + 'Hari yang terbuka juga mengarahkan pencarian: dengan satu hari terbuka, peta dan pencarian di sekitar berangkat dari tempat yang memang dilalui hari itu.', 'help.guide.place-to-open-day.tip.2': '“Batalkan” di bilah alat di atas hari-hari menarik kembali penugasan itu.', // filter-places 'help.guide.filter-places.title': 'Menemukan tempat dalam daftar', diff --git a/shared/src/i18n/it/help.ts b/shared/src/i18n/it/help.ts index 2c30a07fd..881f4a120 100644 --- a/shared/src/i18n/it/help.ts +++ b/shared/src/i18n/it/help.ts @@ -1879,10 +1879,12 @@ const help: TranslationStrings = { 'Al giorno apre lo stesso modulo di Nuovo luogo, solo che il luogo finisce sul giorno aperto nel momento in cui clicchi Aggiungi.', 'help.guide.place-to-open-day.step.3': 'Un luogo che esiste già va sul giorno aperto con il + in fondo alla sua riga, oppure con clic destro, + Giorno.', + 'help.guide.place-to-open-day.step.4': + 'Funziona anche al contrario, e senza aprire prima un giorno: trascina la riga del luogo fuori dalla colonna e lasciala su una scheda di giorno. Lasciata tra due tappe, finisce esattamente lì.', 'help.guide.place-to-open-day.result': 'Il luogo è elencato sotto il giorno, in fondo; trascinalo su o giù dove gli spetta.', 'help.guide.place-to-open-day.tip.1': - 'Trascinare una riga su un giorno funziona ugualmente, e così il luogo si può lasciare subito tra due tappe.', + 'Il giorno aperto guida anche la ricerca: con un giorno aperto, la mappa e la ricerca nei dintorni partono da dove quel giorno passa già.', 'help.guide.place-to-open-day.tip.2': 'Annulla nella barra degli strumenti sopra i giorni ritira l’assegnazione.', // filter-places 'help.guide.filter-places.title': 'Trovare un luogo nella lista', diff --git a/shared/src/i18n/ja/help.ts b/shared/src/i18n/ja/help.ts index dc8568a54..1ff00c588 100644 --- a/shared/src/i18n/ja/help.ts +++ b/shared/src/i18n/ja/help.ts @@ -1803,10 +1803,12 @@ const help: TranslationStrings = { '「この日へ」は「新しい場所」と同じフォームを開きますが、「追加」をクリックした瞬間に場所が開いている日に入ります。', 'help.guide.place-to-open-day.step.3': 'すでにある場所は、行の末尾の + で、または右クリックの「+ 日」で開いている日に入ります。', + 'help.guide.place-to-open-day.step.4': + '逆の手順も使えますし、先に日を開いておく必要もありません。地点の行を列から外へドラッグし、日のカードの上で離してください。2つの立ち寄り先の間で離せば、ちょうどそこに入ります。', 'help.guide.place-to-open-day.result': '場所はその日の下、最後に並びます。上下にドラッグして、あるべき位置へ動かしてください。', 'help.guide.place-to-open-day.tip.1': - '行を日にドラッグする方法もあり、その場で2つの立ち寄り先の間に落とすこともできます。', + '開いている日は検索にも効きます。日が開いていると、地図も周辺検索も、その日がもともと向かう場所から始まります。', 'help.guide.place-to-open-day.tip.2': '日程の上のツールバーにある「元に戻す」は、この割り当てを取り消します。', // filter-places 'help.guide.filter-places.title': 'リストで場所を見つける', diff --git a/shared/src/i18n/ko/help.ts b/shared/src/i18n/ko/help.ts index b4df4509e..cf92efb68 100644 --- a/shared/src/i18n/ko/help.ts +++ b/shared/src/i18n/ko/help.ts @@ -1728,9 +1728,11 @@ const help: TranslationStrings = { '“해당 날짜로”는 “새 장소”와 같은 양식을 열지만, “추가”를 클릭하는 순간 장소가 열린 날짜에 놓입니다.', 'help.guide.place-to-open-day.step.3': '이미 있는 장소는 행 끝의 +로, 또는 오른쪽 클릭 후 “+ 날에 추가”로 열린 날짜에 들어갑니다.', + 'help.guide.place-to-open-day.step.4': + '반대 방향도 됩니다. 게다가 날짜를 먼저 열어 둘 필요도 없습니다. 장소의 행을 열에서 끌어내 날짜 카드 위에 놓으세요. 두 경유지 사이에 놓으면 정확히 그 자리에 들어갑니다.', 'help.guide.place-to-open-day.result': '장소가 그 날짜 아래 맨 끝에 나열됩니다. 위아래로 끌어 제자리로 옮기세요.', 'help.guide.place-to-open-day.tip.1': - '행을 날짜로 끌어다 놓아도 되고, 그렇게 하면 두 경유지 사이에 바로 떨어뜨릴 수 있습니다.', + '열어 둔 날짜는 검색에도 반영됩니다. 날짜가 열려 있으면 지도와 주변 검색이 그날이 이미 향하는 곳에서 시작합니다.', 'help.guide.place-to-open-day.tip.2': '일자 위의 도구 모음에 있는 “실행 취소”가 이 배정을 되돌립니다.', // filter-places 'help.guide.filter-places.title': '목록에서 장소 찾기', diff --git a/shared/src/i18n/nl/help.ts b/shared/src/i18n/nl/help.ts index 553918e5d..aa3dc3db7 100644 --- a/shared/src/i18n/nl/help.ts +++ b/shared/src/i18n/nl/help.ts @@ -1899,10 +1899,12 @@ const help: TranslationStrings = { 'Naar dag opent hetzelfde formulier als Nieuwe plek, alleen komt de plek op de open dag terecht op het moment dat je op Toevoegen klikt.', 'help.guide.place-to-open-day.step.3': 'Een plek die al bestaat gaat naar de open dag met de + aan het eind van zijn rij, of met rechts klikken, + Dag.', + 'help.guide.place-to-open-day.step.4': + 'Andersom kan ook, en zonder eerst een dag te openen: sleep de rij van de plek uit de kolom en laat hem los op een dagkaart. Laat je hem tussen twee stops los, dan belandt hij precies daar.', 'help.guide.place-to-open-day.result': 'De plek staat onder de dag, helemaal onderaan; sleep hem omhoog of omlaag naar waar hij hoort.', 'help.guide.place-to-open-day.tip.1': - 'Een rij op een dag slepen werkt net zo goed, en daarbij kun je de plek meteen tussen twee stops laten vallen.', + 'De geopende dag stuurt ook het zoeken: staat een dag open, dan beginnen de kaart en het zoeken in de buurt daar waar die dag toch al komt.', 'help.guide.place-to-open-day.tip.2': 'Ongedaan maken in de werkbalk boven de dagen neemt de toewijzing terug.', // filter-places 'help.guide.filter-places.title': 'Een plek in de lijst vinden', diff --git a/shared/src/i18n/pl/help.ts b/shared/src/i18n/pl/help.ts index f34dbeb9f..b60e3b46b 100644 --- a/shared/src/i18n/pl/help.ts +++ b/shared/src/i18n/pl/help.ts @@ -1849,10 +1849,12 @@ const help: TranslationStrings = { 'Do dnia otwiera ten sam formularz co Nowe miejsce, tylko miejsce ląduje w otwartym dniu w chwili, gdy klikniesz Dodaj.', 'help.guide.place-to-open-day.step.3': 'Miejsce, które już istnieje, trafia do otwartego dnia przez + na końcu swojego wiersza albo prawym przyciskiem, + Dzień.', + 'help.guide.place-to-open-day.step.4': + 'Odwrotnie też działa, i to bez otwierania dnia: przeciągnij wiersz miejsca poza kolumnę i upuść go na kartę dnia. Upuszczone między dwa przystanki trafia dokładnie tam.', 'help.guide.place-to-open-day.result': 'Miejsce jest wypisane pod dniem, na końcu; przeciągnij je w górę lub w dół tam, gdzie pasuje.', 'help.guide.place-to-open-day.tip.1': - 'Przeciągnięcie wiersza na dzień działa tak samo i może od razu upuścić miejsce między dwa przystanki.', + 'Otwarty dzień steruje też wyszukiwaniem: gdy dzień jest otwarty, mapa i wyszukiwanie w pobliżu zaczynają tam, dokąd ten dzień i tak prowadzi.', 'help.guide.place-to-open-day.tip.2': 'Cofnij na pasku narzędzi nad dniami cofa przypisanie.', // filter-places 'help.guide.filter-places.title': 'Znaleźć miejsce na liście', diff --git a/shared/src/i18n/ru/help.ts b/shared/src/i18n/ru/help.ts index 475e63650..8253bed3b 100644 --- a/shared/src/i18n/ru/help.ts +++ b/shared/src/i18n/ru/help.ts @@ -1863,10 +1863,12 @@ const help: TranslationStrings = { 'В день открывает ту же форму, что и Новое место, только место попадает на открытый день в тот момент, когда вы нажимаете Добавить.', 'help.guide.place-to-open-day.step.3': 'Место, которое уже есть, уходит на открытый день через + в конце своей строки или правой кнопкой, + День.', + 'help.guide.place-to-open-day.step.4': + 'Работает и наоборот, причём день открывать заранее не нужно: перетащите строку места из колонки и отпустите её на карточке дня. Отпустите между двумя остановками — место встанет ровно туда.', 'help.guide.place-to-open-day.result': 'Место перечислено под днём, в конце; перетащите его вверх или вниз туда, где ему место.', 'help.guide.place-to-open-day.tip.1': - 'Перетаскивание строки на день работает так же и может сразу положить место между двумя остановками.', + 'Открытый день направляет и поиск: когда день открыт, карта и поиск поблизости начинаются оттуда, куда этот день и так ведёт.', 'help.guide.place-to-open-day.tip.2': 'Отменить на панели инструментов над днями возвращает назначение назад.', // filter-places 'help.guide.filter-places.title': 'Найти место в списке', diff --git a/shared/src/i18n/sv/help.ts b/shared/src/i18n/sv/help.ts index 02d783c43..7ba000e79 100644 --- a/shared/src/i18n/sv/help.ts +++ b/shared/src/i18n/sv/help.ts @@ -1825,10 +1825,12 @@ const help: TranslationStrings = { 'Till dagen öppnar samma formulär som Ny plats, bara att platsen hamnar på den öppna dagen i samma stund som du klickar på Lägg till.', 'help.guide.place-to-open-day.step.3': 'En plats som redan finns hamnar på den öppna dagen med + i slutet av sin rad, eller via högerklick, + Dag.', + 'help.guide.place-to-open-day.step.4': + 'Det går lika bra åt andra hållet, och utan att först öppna en dag: dra platsens rad ut ur kolumnen och släpp den på ett dagskort. Släpper du den mellan två stopp hamnar den precis där.', 'help.guide.place-to-open-day.result': 'Platsen listas under dagen, sist; dra den uppåt eller nedåt dit den hör hemma.', 'help.guide.place-to-open-day.tip.1': - 'Att dra en rad till en dag fungerar lika bra, och den kan släppa platsen mellan två stopp med en gång.', + 'Den öppna dagen styr även sökningen: med en dag öppen utgår kartan och sökningen i närheten från dit dagen ändå går.', 'help.guide.place-to-open-day.tip.2': 'Ångra i verktygsraden ovanför dagarna tar tillbaka tilldelningen.', // filter-places 'help.guide.filter-places.title': 'Hitta en plats i listan', diff --git a/shared/src/i18n/tr/help.ts b/shared/src/i18n/tr/help.ts index c0efe4aa6..2ba6e2e46 100644 --- a/shared/src/i18n/tr/help.ts +++ b/shared/src/i18n/tr/help.ts @@ -1857,10 +1857,12 @@ const help: TranslationStrings = { 'Güne, Yeni yer ile aynı formu açar, yalnızca Ekle düğmesine tıkladığınız anda yer açık güne iner.', 'help.guide.place-to-open-day.step.3': 'Zaten var olan bir yer, satırının sonundaki + ile ya da sağ tıklayıp + Gün ile açık güne gider.', + 'help.guide.place-to-open-day.step.4': + 'Tersi de olur, üstelik önce bir gün açmanız gerekmez: yerin satırını sütundan dışarı sürükleyip bir gün kartının üzerine bırakın. İki durağın arasına bırakırsanız tam oraya oturur.', 'help.guide.place-to-open-day.result': 'Yer günün altında, en sonda listelenir; ait olduğu yere yukarı ya da aşağı sürükleyin.', 'help.guide.place-to-open-day.tip.1': - 'Bir satırı bir güne sürüklemek de olur ve yeri hemen iki durak arasına bırakabilir.', + 'Açık gün aramayı da yönlendirir: bir gün açıkken harita ve yakındakiler araması, o günün zaten uğradığı yerden başlar.', 'help.guide.place-to-open-day.tip.2': 'Günlerin üstündeki araç çubuğundaki Geri al, atamayı geri alır.', // filter-places 'help.guide.filter-places.title': 'Listede bir yer bulun', diff --git a/shared/src/i18n/uk/help.ts b/shared/src/i18n/uk/help.ts index 2380aa84a..07de68b6d 100644 --- a/shared/src/i18n/uk/help.ts +++ b/shared/src/i18n/uk/help.ts @@ -1837,10 +1837,12 @@ const help: TranslationStrings = { 'До дня відкриває ту саму форму, що й Нове місце, тільки місце потрапляє на відкритий день тієї миті, коли ви натискаєте Додати.', 'help.guide.place-to-open-day.step.3': 'Місце, яке вже існує, іде на відкритий день через + наприкінці свого рядка або правою кнопкою, + День.', + 'help.guide.place-to-open-day.step.4': + 'Працює і навпаки, і день відкривати наперед не треба: перетягніть рядок місця зі стовпця й відпустіть на картці дня. Відпустите між двома зупинками — місце стане саме туди.', 'help.guide.place-to-open-day.result': 'Місце перелічене під днем, у кінці; перетягніть його вгору або вниз туди, де йому місце.', 'help.guide.place-to-open-day.tip.1': - 'Перетягування рядка на день працює так само й може одразу покласти місце між двома зупинками.', + 'Відкритий день керує і пошуком: коли день відкрито, карта й пошук поблизу починаються звідти, куди цей день і так веде.', 'help.guide.place-to-open-day.tip.2': 'Відмінити на панелі інструментів над днями повертає призначення назад.', // filter-places 'help.guide.filter-places.title': 'Знайти місце в списку', diff --git a/shared/src/i18n/vi/help.ts b/shared/src/i18n/vi/help.ts index af85f5361..c048dab3b 100644 --- a/shared/src/i18n/vi/help.ts +++ b/shared/src/i18n/vi/help.ts @@ -1840,10 +1840,12 @@ const help: TranslationStrings = { '“Vào ngày” mở đúng biểu mẫu như “Địa điểm mới”, chỉ khác là địa điểm rơi vào ngày đang mở ngay khi bạn nhấp “Thêm”.', 'help.guide.place-to-open-day.step.3': 'Một địa điểm đã có sẵn sẽ vào ngày đang mở bằng dấu + ở cuối hàng của nó, hoặc bằng nhấp chuột phải rồi “+ Ngày”.', + 'help.guide.place-to-open-day.step.4': + 'Chiều ngược lại cũng được, mà không cần mở ngày trước: kéo hàng của địa điểm ra khỏi cột rồi thả lên thẻ của một ngày. Thả vào giữa hai điểm dừng thì nó nằm đúng chỗ đó.', 'help.guide.place-to-open-day.result': 'Địa điểm được liệt kê dưới ngày đó, ở cuối; kéo lên hoặc xuống tới chỗ nó thuộc về.', 'help.guide.place-to-open-day.tip.1': - 'Kéo một hàng lên một ngày cũng được, và nó có thể thả địa điểm vào giữa hai điểm dừng ngay lập tức.', + 'Ngày đang mở cũng dẫn hướng việc tìm kiếm: khi một ngày đang mở, bản đồ và tìm quanh đây bắt đầu từ nơi ngày đó vốn đã đi qua.', 'help.guide.place-to-open-day.tip.2': '“Hoàn tác” trên thanh công cụ phía trên các ngày lấy lại việc gán đó.', // filter-places 'help.guide.filter-places.title': 'Tìm một địa điểm trong danh sách', diff --git a/shared/src/i18n/zh-TW/help.ts b/shared/src/i18n/zh-TW/help.ts index 3bc9bcc4d..14dfa961e 100644 --- a/shared/src/i18n/zh-TW/help.ts +++ b/shared/src/i18n/zh-TW/help.ts @@ -1534,8 +1534,11 @@ const help: TranslationStrings = { 'help.guide.place-to-open-day.step.2': '「加入當天」開啟的表單和「新地點」一樣,只是你點選「新增」的那一刻地點就落在開啟的那一天上。', 'help.guide.place-to-open-day.step.3': '已經存在的地點,用它那一列末尾的 + 或者按右鍵後的「+ 天」放到開啟的那一天。', + 'help.guide.place-to-open-day.step.4': + '反過來也行,而且不用先打開某一天:把地點的那一列拖出清單,放到某天的卡片上。放在兩個停靠點之間,它就正好落在那裡。', 'help.guide.place-to-open-day.result': '地點列在那一天下面,排在最後;上下拖曳把它放到該在的位置。', - 'help.guide.place-to-open-day.tip.1': '把一列拖曳到某一天同樣可行,而且可以直接把地點放到兩個停靠點之間。', + 'help.guide.place-to-open-day.tip.1': + '打開的那一天也會影響搜尋:有一天處於打開狀態時,地圖和附近搜尋都從這一天本來要去的地方開始。', 'help.guide.place-to-open-day.tip.2': '天數上方工具列裡的「撤銷」收回這次指派。', // filter-places 'help.guide.filter-places.title': '在清單裡找到地點', diff --git a/shared/src/i18n/zh/help.ts b/shared/src/i18n/zh/help.ts index 6deda5c24..d56d58b9a 100644 --- a/shared/src/i18n/zh/help.ts +++ b/shared/src/i18n/zh/help.ts @@ -1500,8 +1500,11 @@ const help: TranslationStrings = { 'help.guide.place-to-open-day.step.2': '“加入当天”打开的表单和“新地点”一样,只是你点击“添加”的那一刻地点就落在打开的那一天上。', 'help.guide.place-to-open-day.step.3': '已经存在的地点,用它那一行末尾的 + 或者右键点击后的“+ 天”放到打开的那一天。', + 'help.guide.place-to-open-day.step.4': + '反过来也行,而且不用先打开某一天:把地点的那一行拖出列表,放到某天的卡片上。放在两个停靠点之间,它就正好落在那里。', 'help.guide.place-to-open-day.result': '地点列在那一天下面,排在最后;上下拖动把它放到该在的位置。', - 'help.guide.place-to-open-day.tip.1': '把一行拖到某一天同样可行,而且可以直接把地点放到两个停靠点之间。', + 'help.guide.place-to-open-day.tip.1': + '打开的那一天也会影响搜索:有一天处于打开状态时,地图和附近搜索都从这一天本来要去的地方开始。', 'help.guide.place-to-open-day.tip.2': '天数上方工具栏里的“撤销”收回这次分配。', // filter-places 'help.guide.filter-places.title': '在列表里找到地点',