diff --git a/client/native-shell/main.ts b/client/native-shell/main.ts index e408a38cd..6f99b96a5 100644 --- a/client/native-shell/main.ts +++ b/client/native-shell/main.ts @@ -1,16 +1,18 @@ import { CapacitorHttp } from '@capacitor/core' +import { Haptics, ImpactStyle, NotificationType } from '@capacitor/haptics' import { SplashScreen } from '@capacitor/splash-screen' import { TrekShell } from '../src/native/trekShell' import { probeDemo, probeServer, type ProbeFailure, type ProbeGet } from '../src/native/serverAddress' -import { PRIVACY_URL, SERVER_DOCS_URL } from '../src/native/links' +import { CLOUD_URL, PRIVACY_URL, SERVER_DOCS_URL } from '../src/native/links' import { pickLocale, translator } from './i18n' import logoUrl from '../public/icons/icon-white.svg' import './shell.css' /** - * The only page the app ships itself. It asks for a server on first start and - * stands in when the remembered one cannot be reached; everything else is the - * UI that server delivers. + * The only page the app ships itself. It welcomes someone on first start, asks + * for the TREK server they open in their browser, offers TREK Cloud to someone + * without one, and stands in when a remembered server cannot be reached. + * Everything past it is the UI that server delivers. */ const locale = pickLocale(navigator.languages?.length ? navigator.languages : [navigator.language]) @@ -19,7 +21,6 @@ document.documentElement.lang = locale document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr' const root = document.getElementById('shell')! - const PROBE_TIMEOUT_MS = 8000 // Native HTTP rather than fetch: the probe goes to a server of the user's @@ -42,115 +43,310 @@ function failureMessage(reason: ProbeFailure): string { } } +// ── Tiny DOM + haptics helpers ────────────────────────────────────────────── function el( tag: K, - props: Partial = {}, + props: Partial & { dataset?: Record } = {}, ...children: (Node | string)[] ): HTMLElementTagNameMap[K] { - // Typed as the plain element, not as Object.assign's intersection with the - // props, which strict mode cannot represent for an . - const node: HTMLElementTagNameMap[K] = document.createElement(tag) - Object.assign(node, props) - node.append(...children) + const { dataset, ...rest } = props + const node = document.createElement(tag) + Object.assign(node, rest) + if (dataset) for (const [k, v] of Object.entries(dataset)) node.dataset[k] = v + node.append(...children.filter((c) => c !== null && c !== undefined)) return node } -function header(title: string, hint: string) { - return [ - el('img', { src: logoUrl, alt: '', className: 'shell-logo' }), - el('h1', { className: 'shell-title' }, title), - el('p', { className: 'shell-hint' }, hint), - ] +function icon(path: string, size = 22, className?: string): SVGSVGElement { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('viewBox', '0 0 24 24') + svg.setAttribute('width', String(size)) + svg.setAttribute('height', String(size)) + svg.setAttribute('fill', 'none') + svg.setAttribute('stroke', 'currentColor') + svg.setAttribute('stroke-width', '2') + svg.setAttribute('stroke-linecap', 'round') + svg.setAttribute('stroke-linejoin', 'round') + if (className) svg.setAttribute('class', className) + const p = document.createElementNS('http://www.w3.org/2000/svg', 'path') + p.setAttribute('d', path) + svg.appendChild(p) + return svg +} +const I = { + chevron: 'M9 18l6-6-6-6', + cloud: 'M17.5 19a4.5 4.5 0 0 0 .5-9 6 6 0 0 0-11.6-1.6A4 4 0 0 0 6 19z', + globe: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20', + bolt: 'M13 2 4 14h7l-1 8 9-12h-7z', + shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10', + check: 'M20 6 9 17l-5-5', } -/** A page outside the app; the WebView hands it to the system browser. */ -function externalLink(text: string, href: string) { - return el('a', { href, target: '_blank', rel: 'noopener noreferrer', className: 'shell-link' }, text) +async function tap(style: ImpactStyle = ImpactStyle.Light): Promise { + try { + await Haptics.impact({ style }) + } catch { + /* web / unavailable */ + } +} +async function notify(type: NotificationType): Promise { + try { + await Haptics.notification({ type }) + } catch { + /* web / unavailable */ + } } -function showConnect(prefill = '') { +// ── Screen transitions ────────────────────────────────────────────────────── +let current: HTMLElement | null = null + +function show(screen: HTMLElement, opts: { back?: boolean } = {}): void { + screen.classList.add('screen', 'screen-enter') + if (opts.back) screen.classList.add('screen-back') + const previous = current + if (previous) { + previous.classList.remove('screen-enter') + previous.classList.add('screen-leave') + if (opts.back) previous.classList.add('screen-back') + previous.addEventListener('animationend', () => previous.remove(), { once: true }) + setTimeout(() => previous.remove(), 400) + } + root.append(screen) + current = screen +} + +function externalLink(text: string, href: string): HTMLAnchorElement { + return el('a', { href, target: '_blank', rel: 'noopener noreferrer' }, text) +} +function footLinks(...extra: Node[]): HTMLElement { + return el('div', { className: 'foot-links' }, ...extra, externalLink(t('native.privacy'), PRIVACY_URL)) +} +function topbar(onBack: () => void): HTMLElement { + const back = el('button', { type: 'button', className: 'back' }, icon('M15 18l-6-6 6-6', 18), t('native.back')) + back.addEventListener('click', () => { + void tap() + onBack() + }) + return el('div', { className: 'topbar' }, back) +} + +// ── Welcome ───────────────────────────────────────────────────────────────── +function showWelcome(opts: { back?: boolean } = {}): void { + const hero = el('img', { src: logoUrl, alt: '', className: 'hero-mark' }) + + const connect = el( + 'button', + { type: 'button', className: 'btn' }, + el('span', { className: 'btn-row' }, el('b', {}, t('native.welcome.connect')), el('small', {}, t('native.welcome.connectSub'))), + icon(I.chevron, 20, 'chev'), + ) + connect.addEventListener('click', () => { + void tap() + showConnect() + }) + + const cloud = el( + 'button', + { type: 'button', className: 'btn btn-ghost' }, + el('span', { className: 'btn-row' }, el('b', {}, t('native.welcome.getCloud')), el('small', {}, t('native.welcome.getCloudSub'))), + icon(I.chevron, 20, 'chev'), + ) + cloud.addEventListener('click', () => { + void tap() + showCloud() + }) + + const demo = el('button', { type: 'button', className: 'linkbtn' }, t('native.connect.tryDemo')) + demo.addEventListener('click', () => runDemo(demo)) + + show( + el( + 'div', + {}, + hero, + el('h1', { className: 'title' }, t('native.welcome.headline')), + el('p', { className: 'sub' }, t('native.welcome.sub')), + connect, + cloud, + demo, + footLinks(externalLink(t('native.connect.whatIsServer'), SERVER_DOCS_URL)), + ), + opts, + ) +} + +// ── Connect (address) ─────────────────────────────────────────────────────── +function showConnect(prefill = '', opts: { back?: boolean } = {}): void { const input = el('input', { type: 'url', - className: 'shell-input', placeholder: t('native.connect.placeholder'), value: prefill, autocapitalize: 'off', spellcheck: false, - required: true, }) input.setAttribute('inputmode', 'url') input.setAttribute('autocomplete', 'url') input.setAttribute('aria-label', t('native.connect.title')) - const error = el('p', { className: 'shell-error', role: 'alert' }) - const submit = el('button', { type: 'submit', className: 'shell-button' }, t('native.connect.submit')) - const form = el('form', { className: 'shell-form', noValidate: true }, input, submit, error) + const field = el('div', { className: 'field' }, input) + + const label = el('span', {}, t('native.connect.submit')) + const submit = el('button', { type: 'submit', className: 'btn' }, label) + const error = el('p', { className: 'error', role: 'alert' }) + const form = el('form', { className: 'form', noValidate: true }, field, submit, error) + + const fail = (reason: ProbeFailure): void => { + void notify(NotificationType.Error) + field.classList.add('bad') + setTimeout(() => field.classList.remove('bad'), 350) + error.textContent = failureMessage(reason) + input.disabled = false + submit.disabled = false + label.textContent = t('native.connect.submit') + submit.replaceChildren(label) + input.focus() + } form.addEventListener('submit', async (event) => { event.preventDefault() + void tap(ImpactStyle.Medium) error.textContent = '' input.disabled = true submit.disabled = true - submit.textContent = t('native.connect.checking') + submit.replaceChildren(el('span', { className: 'spinner' }), t('native.connect.checking')) const result = await probeServer(input.value, get) if (result.ok) { - await TrekShell.setServer({ url: result.url }) + await connected(result.url) return } - error.textContent = failureMessage(result.reason) - input.disabled = false - submit.disabled = false - submit.textContent = t('native.connect.submit') - input.focus() + fail(result.reason) }) - // For someone without a server of their own yet: the public demo, through the - // same probe, so an outdated or unreachable demo says so like any server. - const demo = el('button', { type: 'button', className: 'shell-link' }, t('native.connect.tryDemo')) - demo.addEventListener('click', async () => { - error.textContent = '' - demo.disabled = true - const result = await probeDemo(get) - if (result.ok) { - await TrekShell.setServer({ url: result.url }) - return - } - error.textContent = failureMessage(result.reason) - demo.disabled = false - }) + const demo = el('button', { type: 'button', className: 'linkbtn' }, t('native.connect.tryDemo')) + demo.addEventListener('click', () => runDemo(demo, error)) - root.replaceChildren( - ...header(t('native.connect.title'), t('native.connect.hint')), - form, - demo, + show( el( 'div', - { className: 'shell-links' }, - externalLink(t('native.connect.whatIsServer'), SERVER_DOCS_URL), - externalLink(t('native.privacy'), PRIVACY_URL), + {}, + topbar(() => showWelcome({ back: true })), + el('h1', { className: 'title-sm' }, t('native.connect.title')), + el('p', { className: 'sub' }, t('native.connect.hint')), + form, + demo, + footLinks(externalLink(t('native.connect.whatIsServer'), SERVER_DOCS_URL)), + ), + opts, + ) + requestAnimationFrame(() => input.focus()) +} + +// ── Get TREK Cloud ────────────────────────────────────────────────────────── +function feature(path: string, text: string): HTMLElement { + return el('div', { className: 'feature' }, el('span', { className: 'ic' }, icon(path, 18)), el('span', { className: 'tx' }, text)) +} + +function showCloud(opts: { back?: boolean } = {}): void { + const cta = el('a', { className: 'btn', href: CLOUD_URL, target: '_blank', rel: 'noopener noreferrer' }, t('native.cloud.cta'), icon(I.chevron, 20, 'chev')) + cta.addEventListener('click', () => void tap()) + + const have = el('button', { type: 'button', className: 'linkbtn' }, t('native.cloud.haveServer')) + have.addEventListener('click', () => { + void tap() + showConnect() + }) + + show( + el( + 'div', + {}, + topbar(() => showWelcome({ back: true })), + el('h1', { className: 'title-sm' }, t('native.cloud.title')), + el('p', { className: 'sub' }, t('native.cloud.sub')), + el('div', { className: 'features' }, feature(I.globe, t('native.cloud.f1')), feature(I.bolt, t('native.cloud.f2')), feature(I.shield, t('native.cloud.f3'))), + cta, + have, + footLinks(), + ), + opts, + ) +} + +// ── Shared actions ────────────────────────────────────────────────────────── +async function runDemo(button: HTMLButtonElement, error?: HTMLElement): Promise { + void tap() + if (error) error.textContent = '' + button.disabled = true + const result = await probeDemo(get) + if (result.ok) { + await connected(result.url) + return + } + button.disabled = false + if (error) { + error.textContent = failureMessage(result.reason) + } else { + void notify(NotificationType.Error) + } +} + +/** The tick, then the server takes over. A beat so the success registers. */ +async function connected(url: string): Promise { + void notify(NotificationType.Success) + const host = (() => { + try { + return new URL(url).host + } catch { + return url + } + })() + const tick = el('div', { className: 'tick' }) + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('viewBox', '0 0 24 24') + svg.setAttribute('fill', 'none') + svg.setAttribute('stroke', 'currentColor') + svg.setAttribute('stroke-width', '2.5') + svg.setAttribute('stroke-linecap', 'round') + svg.setAttribute('stroke-linejoin', 'round') + const p = document.createElementNS('http://www.w3.org/2000/svg', 'path') + p.setAttribute('d', I.check) + svg.appendChild(p) + tick.appendChild(svg) + show(el('div', { className: 'success' }, tick, el('h1', { className: 'title-sm' }, t('native.connecting', { host })))) + await new Promise((r) => setTimeout(r, 700)) + await TrekShell.setServer({ url }) +} + +function showUnreachable(server: string): void { + const retry = el('button', { type: 'button', className: 'btn' }, t('native.offline.retry')) + retry.addEventListener('click', () => { + void tap() + retry.disabled = true + void TrekShell.reload() + }) + const change = el('button', { type: 'button', className: 'linkbtn' }, t('native.changeServer')) + change.addEventListener('click', () => { + void tap() + void TrekShell.resetServer() + }) + show( + el( + 'div', + {}, + el('img', { src: logoUrl, alt: '', className: 'shell-logo' }), + el('h1', { className: 'title-sm' }, t('native.offline.title')), + el('p', { className: 'sub' }, t('native.offline.hint', { server: new URL(server).host })), + retry, + change, + footLinks(), ), ) } -function showUnreachable(server: string) { - const retry = el('button', { type: 'button', className: 'shell-button' }, t('native.offline.retry')) - retry.addEventListener('click', () => { - retry.disabled = true - void TrekShell.reload() - }) - const change = el('button', { type: 'button', className: 'shell-link' }, t('native.changeServer')) - change.addEventListener('click', () => { void TrekShell.resetServer() }) - - root.replaceChildren( - ...header(t('native.offline.title'), t('native.offline.hint', { server: new URL(server).host })), - el('div', { className: 'shell-form' }, retry, change), - el('div', { className: 'shell-links' }, externalLink(t('native.privacy'), PRIVACY_URL)), - ) -} - -async function start() { +// ── Start ─────────────────────────────────────────────────────────────────── +async function start(): Promise { const { url } = await TrekShell.getServer() if (!url) { - showConnect() + showWelcome() } else { // Only reached when loading the server failed. On iOS the stored copy of // its start page brings the offline-capable UI up anyway. diff --git a/client/native-shell/shell.css b/client/native-shell/shell.css index 4135d1a95..8793c757f 100644 --- a/client/native-shell/shell.css +++ b/client/native-shell/shell.css @@ -1,99 +1,519 @@ -/* Mirrors the app's neutral default scheme (client/src/index.css), dark first: - this page shares the launch screen's background and must not flash. */ +/* + * The address screen the app ships. Dark first, mirroring the app's neutral + * default scheme (client/src/index.css), so it shares the launch screen's + * background and never flashes. Everything past this screen is the UI the + * chosen server delivers. + */ :root { --bg: #09090b; + --bg-2: #101013; --surface: #18181b; + --surface-2: #1f1f23; --edge: #27272a; + --edge-strong: #3f3f46; --ink: #fafafa; --muted: #a1a1aa; - --accent: #e4e4e7; + --faint: #71717a; + --accent: #fafafa; --accent-ink: #09090b; --danger: #f87171; + --ok: #4ade80; + --radius: 16px; + --ease: cubic-bezier(0.22, 1, 0.36, 1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); color-scheme: dark; } @media (prefers-color-scheme: light) { :root { --bg: #ffffff; + --bg-2: #fafafa; --surface: #f4f4f5; + --surface-2: #ececef; --edge: #e4e4e7; + --edge-strong: #d4d4d8; --ink: #111827; - --muted: #6b7280; + --muted: #52525b; + --faint: #9ca3af; --accent: #111827; --accent-ink: #ffffff; --danger: #dc2626; + --ok: #16a34a; color-scheme: light; } - .shell-logo { filter: invert(1); } + .shell-logo, + .mark { + filter: none; + } } -* { box-sizing: border-box; } +* { + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} -html, body { +html, +body { margin: 0; min-height: 100%; background: var(--bg); color: var(--ink); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, system-ui, sans-serif; -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + user-select: none; + -webkit-user-select: none; +} + +/* An ambient glow behind the content, so the flat dark gains a little depth. */ +body::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(60% 50% at 50% -5%, color-mix(in srgb, var(--ink) 7%, transparent), transparent 70%), + radial-gradient(50% 40% at 50% 110%, color-mix(in srgb, var(--ink) 4%, transparent), transparent 70%); + pointer-events: none; + z-index: 0; } .shell { + position: relative; + z-index: 1; min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; justify-content: center; - gap: 12px; - max-width: 420px; + max-width: 440px; margin: 0 auto; - padding: calc(env(safe-area-inset-top) + 24px) 24px calc(env(safe-area-inset-bottom) + 24px); + padding: calc(env(safe-area-inset-top) + 28px) 24px calc(env(safe-area-inset-bottom) + 24px); + overflow: hidden; } -.shell-logo { width: 56px; height: 56px; margin-bottom: 8px; } -.shell-title { font-size: 1.5rem; font-weight: 600; margin: 0; } -.shell-hint { color: var(--muted); margin: 0 0 12px; line-height: 1.5; overflow-wrap: anywhere; } +/* ── Screen transitions ──────────────────────────────────────────────────── */ +.screen { + display: flex; + flex-direction: column; + gap: 14px; +} +.screen-enter { + animation: screenIn 0.5s var(--ease) both; +} +.screen-leave { + animation: screenOut 0.28s var(--ease-in) both; + position: absolute; + inset: calc(env(safe-area-inset-top) + 28px) 24px calc(env(safe-area-inset-bottom) + 24px); +} +.screen-back.screen-enter { + animation-name: screenInBack; +} +.screen-back.screen-leave { + animation-name: screenOutBack; +} +@keyframes screenIn { + from { + opacity: 0; + transform: translateX(26px); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes screenOut { + to { + opacity: 0; + transform: translateX(-22px); + } +} +@keyframes screenInBack { + from { + opacity: 0; + transform: translateX(-26px); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes screenOutBack { + to { + opacity: 0; + transform: translateX(22px); + } +} -.shell-form { display: flex; flex-direction: column; gap: 12px; } +/* Children stagger in, so a screen assembles rather than snaps. */ +.screen-enter > * { + animation: riseIn 0.55s var(--ease) both; +} +.screen-enter > *:nth-child(1) { + animation-delay: 0.02s; +} +.screen-enter > *:nth-child(2) { + animation-delay: 0.07s; +} +.screen-enter > *:nth-child(3) { + animation-delay: 0.12s; +} +.screen-enter > *:nth-child(4) { + animation-delay: 0.17s; +} +.screen-enter > *:nth-child(5) { + animation-delay: 0.22s; +} +.screen-enter > *:nth-child(6) { + animation-delay: 0.27s; +} +@keyframes riseIn { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: none; + } +} -.shell-input { +/* ── Brand ───────────────────────────────────────────────────────────────── */ +.brand { + display: inline-flex; + align-items: center; + gap: 11px; + margin-bottom: 6px; +} +.brand .mark { + width: 40px; + height: 40px; +} +.brand .wordmark { + font-weight: 700; + font-size: 1.5rem; + letter-spacing: -0.01em; +} +.brand .tag { + font-style: italic; + font-weight: 600; + font-size: 0.9rem; + color: var(--accent-ink); + background: var(--accent); + border-radius: 999px; + padding: 2px 10px; + line-height: 1.3; +} + +.shell-logo { + width: 52px; + height: 52px; + margin-bottom: 4px; +} + +/* The big hero mark on the welcome screen, with a gentle breathing float. */ +.hero-mark { + width: 76px; + height: 76px; + margin-bottom: 18px; + animation: floaty 6s ease-in-out infinite; + filter: drop-shadow(0 10px 30px color-mix(in srgb, var(--ink) 18%, transparent)); +} +@keyframes floaty { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-6px); + } +} + +/* ── Type ────────────────────────────────────────────────────────────────── */ +.title { + font-size: 1.9rem; + line-height: 1.12; + font-weight: 700; + letter-spacing: -0.02em; + margin: 0; +} +.title-sm { + font-size: 1.45rem; + line-height: 1.2; + font-weight: 700; + letter-spacing: -0.015em; + margin: 0; +} +.sub { + color: var(--muted); + margin: 0; + line-height: 1.55; + font-size: 1.02rem; + overflow-wrap: anywhere; +} + +/* ── Buttons ─────────────────────────────────────────────────────────────── */ +.btn { font: inherit; - font-size: 1rem; - padding: 14px 16px; - border-radius: 12px; + font-weight: 600; + font-size: 1.02rem; + padding: 16px 18px; + border: 1px solid transparent; + border-radius: var(--radius); + background: var(--accent); + color: var(--accent-ink); + cursor: pointer; + transition: + transform 0.14s var(--ease), + opacity 0.2s, + background 0.2s, + border-color 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + min-height: 54px; +} +.btn:active { + transform: scale(0.975); +} +.btn:disabled { + opacity: 0.55; +} +.btn-ghost { + background: var(--surface); + color: var(--ink); + border-color: var(--edge); +} +.btn-ghost:active { + background: var(--surface-2); +} +.btn-row { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} +.btn-row b { + font-weight: 650; +} +.btn-row small { + font-weight: 500; + font-size: 0.86rem; + opacity: 0.7; +} +.btn .chev { + margin-left: auto; + opacity: 0.5; +} + +.linkbtn { + font: inherit; + font-size: 0.95rem; + padding: 12px; + border: 0; + background: none; + color: var(--muted); + cursor: pointer; + align-self: center; + transition: color 0.2s; +} +.linkbtn:active { + color: var(--ink); +} +a.linkbtn { + text-decoration: none; +} + +.foot-links { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 2px 18px; + margin-top: 6px; + font-size: 0.86rem; +} +.foot-links a { + color: var(--faint); + text-decoration: none; + padding: 8px 2px; +} + +/* ── Address form ────────────────────────────────────────────────────────── */ +.form { + display: flex; + flex-direction: column; + gap: 12px; +} +.field { + position: relative; +} +.field input { + width: 100%; + font: inherit; + font-size: 1.05rem; + padding: 16px 18px; + border-radius: var(--radius); border: 1px solid var(--edge); background: var(--surface); color: var(--ink); outline: none; + transition: + border-color 0.2s, + box-shadow 0.25s; + user-select: text; + -webkit-user-select: text; } -.shell-input:focus { border-color: var(--accent); } - -.shell-button { - font: inherit; - font-weight: 600; - padding: 14px 16px; - border: 0; - border-radius: 12px; - background: var(--accent); - color: var(--accent-ink); +.field input::placeholder { + color: var(--faint); +} +.field input:focus { + border-color: var(--edge-strong); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--ink) 9%, transparent); +} +.field.bad input { + border-color: var(--danger); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 16%, transparent); + animation: shake 0.3s; +} +@keyframes shake { + 25% { + transform: translateX(-5px); + } + 50% { + transform: translateX(5px); + } + 75% { + transform: translateX(-3px); + } } -.shell-button:disabled { opacity: 0.6; } -.shell-link { +.error { + color: var(--danger); + margin: 0; + min-height: 1.4em; + line-height: 1.45; + font-size: 0.95rem; + transition: opacity 0.2s; +} + +/* Spinner inside the connect button while probing. */ +.spinner { + width: 18px; + height: 18px; + border: 2px solid color-mix(in srgb, var(--accent-ink) 35%, transparent); + border-top-color: var(--accent-ink); + border-radius: 50%; + animation: spin 0.7s linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ── Feature list (cloud screen) ─────────────────────────────────────────── */ +.features { + display: flex; + flex-direction: column; + gap: 2px; + margin: 4px 0 2px; +} +.feature { + display: flex; + align-items: flex-start; + gap: 13px; + padding: 11px 0; + border-top: 1px solid var(--edge); +} +.feature:first-child { + border-top: 0; +} +.feature .ic { + flex: none; + width: 34px; + height: 34px; + border-radius: 10px; + background: var(--surface); + border: 1px solid var(--edge); + display: grid; + place-items: center; + color: var(--ink); +} +.feature .tx { + line-height: 1.4; + padding-top: 5px; + font-size: 0.98rem; +} + +/* ── Success tick before the server loads ────────────────────────────────── */ +.success { + align-items: center; + text-align: center; + justify-content: center; +} +.tick { + width: 72px; + height: 72px; + border-radius: 50%; + background: color-mix(in srgb, var(--ok) 16%, transparent); + display: grid; + place-items: center; + margin-bottom: 8px; + animation: pop 0.45s var(--ease) both; +} +.tick svg { + width: 38px; + height: 38px; + color: var(--ok); +} +.tick path { + stroke-dasharray: 48; + stroke-dashoffset: 48; + animation: draw 0.45s 0.15s var(--ease) forwards; +} +@keyframes pop { + from { + transform: scale(0.4); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} +@keyframes draw { + to { + stroke-dashoffset: 0; + } +} + +.topbar { + display: flex; + align-items: center; + margin: 0 0 2px -8px; +} +.back { font: inherit; - padding: 10px; border: 0; background: none; color: var(--muted); + font-size: 0.98rem; + padding: 8px; + display: inline-flex; + align-items: center; + gap: 4px; + cursor: pointer; +} +.back:active { + color: var(--ink); } -.shell-links { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 4px 16px; - font-size: 0.875rem; +@media (prefers-reduced-motion: reduce) { + *, + *::before { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + } } -a.shell-link { text-decoration: underline; text-underline-offset: 2px; } - -.shell-error { color: var(--danger); margin: 0; min-height: 1.5em; line-height: 1.5; } diff --git a/client/src/native/links.ts b/client/src/native/links.ts index e70e0b8aa..f367a8f9d 100644 --- a/client/src/native/links.ts +++ b/client/src/native/links.ts @@ -11,3 +11,6 @@ export const SERVER_DOCS_URL = 'https://github.com/liketrek/TREK#readme'; /** The public demo the address screen offers to someone without a server. */ export const DEMO_SERVER_URL = 'https://demo.liketrek.com'; + +/** TREK Cloud, for someone who wants a hosted TREK rather than running their own. */ +export const CLOUD_URL = 'https://liketrek.com'; diff --git a/shared/src/i18n/ar/native.ts b/shared/src/i18n/ar/native.ts index 9b7611189..07649ce9c 100644 --- a/shared/src/i18n/ar/native.ts +++ b/shared/src/i18n/ar/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'سياسة الخصوصية', 'native.settings.server': 'الخادم', 'native.settings.serverHint': 'هذا التطبيق متصل بـ {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/az/native.ts b/shared/src/i18n/az/native.ts index 032d218bd..87038e4ba 100644 --- a/shared/src/i18n/az/native.ts +++ b/shared/src/i18n/az/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Məxfilik siyasəti', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'Bu tətbiq {server} serverinə qoşulub.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/br/native.ts b/shared/src/i18n/br/native.ts index 6d555e279..7621cefba 100644 --- a/shared/src/i18n/br/native.ts +++ b/shared/src/i18n/br/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Política de privacidade', 'native.settings.server': 'Servidor', 'native.settings.serverHint': 'Este app está conectado a {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/ca/native.ts b/shared/src/i18n/ca/native.ts index 0a883ac80..b3aff0bcb 100644 --- a/shared/src/i18n/ca/native.ts +++ b/shared/src/i18n/ca/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Política de privadesa', 'native.settings.server': 'Servidor', 'native.settings.serverHint': 'Aquesta app està connectada a {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/cs/native.ts b/shared/src/i18n/cs/native.ts index aaab4fda7..854f0382d 100644 --- a/shared/src/i18n/cs/native.ts +++ b/shared/src/i18n/cs/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Zásady ochrany osobních údajů', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'Tato aplikace je připojena k {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/de/native.ts b/shared/src/i18n/de/native.ts index 8a7474f80..c0a851f97 100644 --- a/shared/src/i18n/de/native.ts +++ b/shared/src/i18n/de/native.ts @@ -25,6 +25,21 @@ const native: TranslationStrings = { 'native.privacy': 'Datenschutzerklärung', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'Diese App ist mit {server} verbunden.', + 'native.welcome.headline': 'Reisen gemeinsam planen.', + 'native.welcome.sub': 'TREK hält jeden Tag, Ort, jede Buchung und Kosten in einem gemeinsamen Plan, den eure ganze Gruppe live bearbeitet.', + 'native.welcome.connect': 'Mit deinem TREK verbinden', + 'native.welcome.connectSub': 'Die Adresse öffnen, die du schon nutzt', + 'native.welcome.getCloud': 'TREK Cloud holen', + 'native.welcome.getCloudSub': 'Für dich gehostet, ohne Einrichtung', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'Wir hosten TREK für dich, auf deiner eigenen Adresse. Nichts installieren, nichts warten — in Minuten bereit.', + 'native.cloud.f1': 'Eigene Adresse, auf Servern in Deutschland', + 'native.cloud.f2': 'In Minuten eingerichtet, Updates übernehmen wir', + 'native.cloud.f3': 'Deine Daten bleiben deine — keine Werbung, kein Tracking', + 'native.cloud.cta': 'Loslegen', + 'native.cloud.haveServer': 'Ich habe schon ein TREK', + 'native.connecting': 'Verbinde mit {host}', + 'native.back': 'Zurück', }; export default native; diff --git a/shared/src/i18n/en/native.ts b/shared/src/i18n/en/native.ts index 8561f2d61..fe8da4971 100644 --- a/shared/src/i18n/en/native.ts +++ b/shared/src/i18n/en/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Privacy policy', 'native.settings.server': 'Server', 'native.settings.serverHint': 'This app is connected to {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/es/native.ts b/shared/src/i18n/es/native.ts index 25044b2ad..b0b7af1cf 100644 --- a/shared/src/i18n/es/native.ts +++ b/shared/src/i18n/es/native.ts @@ -25,6 +25,21 @@ const native: TranslationStrings = { 'native.privacy': 'Política de privacidad', 'native.settings.server': 'Servidor', 'native.settings.serverHint': 'Esta app está conectada a {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/et/native.ts b/shared/src/i18n/et/native.ts index 2ee2fb754..649be95de 100644 --- a/shared/src/i18n/et/native.ts +++ b/shared/src/i18n/et/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Privaatsuspoliitika', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'See rakendus on ühendatud serveriga {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/fr/native.ts b/shared/src/i18n/fr/native.ts index ea97a8421..fb3a98150 100644 --- a/shared/src/i18n/fr/native.ts +++ b/shared/src/i18n/fr/native.ts @@ -25,6 +25,21 @@ const native: TranslationStrings = { 'native.privacy': 'Politique de confidentialité', 'native.settings.server': 'Serveur', 'native.settings.serverHint': 'Cette application est connectée à {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/gr/native.ts b/shared/src/i18n/gr/native.ts index b9e9da2a7..ab66261ee 100644 --- a/shared/src/i18n/gr/native.ts +++ b/shared/src/i18n/gr/native.ts @@ -26,6 +26,21 @@ const native: TranslationStrings = { 'native.privacy': 'Πολιτική απορρήτου', 'native.settings.server': 'Διακομιστής', 'native.settings.serverHint': 'Αυτή η εφαρμογή είναι συνδεδεμένη στο {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/hu/native.ts b/shared/src/i18n/hu/native.ts index a27472897..4e4a64bd4 100644 --- a/shared/src/i18n/hu/native.ts +++ b/shared/src/i18n/hu/native.ts @@ -25,6 +25,21 @@ const native: TranslationStrings = { 'native.privacy': 'Adatvédelmi irányelvek', 'native.settings.server': 'Szerver', 'native.settings.serverHint': 'Ez az alkalmazás ehhez csatlakozik: {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/id/native.ts b/shared/src/i18n/id/native.ts index 6b9b497c5..2abf64497 100644 --- a/shared/src/i18n/id/native.ts +++ b/shared/src/i18n/id/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Kebijakan privasi', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'Aplikasi ini terhubung ke {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/it/native.ts b/shared/src/i18n/it/native.ts index 8e812103b..a858aafb4 100644 --- a/shared/src/i18n/it/native.ts +++ b/shared/src/i18n/it/native.ts @@ -25,6 +25,21 @@ const native: TranslationStrings = { 'native.privacy': 'Informativa sulla privacy', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'Questa app è collegata a {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/ja/native.ts b/shared/src/i18n/ja/native.ts index 3cc41f5a6..22b89231a 100644 --- a/shared/src/i18n/ja/native.ts +++ b/shared/src/i18n/ja/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'プライバシーポリシー', 'native.settings.server': 'サーバー', 'native.settings.serverHint': 'このアプリは {server} に接続されています。', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/ko/native.ts b/shared/src/i18n/ko/native.ts index 50667dffd..bdbe07eb2 100644 --- a/shared/src/i18n/ko/native.ts +++ b/shared/src/i18n/ko/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': '개인정보 처리방침', 'native.settings.server': '서버', 'native.settings.serverHint': '이 앱은 {server}에 연결되어 있습니다.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/nl/native.ts b/shared/src/i18n/nl/native.ts index 477e15fb7..e2cdbd579 100644 --- a/shared/src/i18n/nl/native.ts +++ b/shared/src/i18n/nl/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Privacybeleid', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'Deze app is verbonden met {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/pl/native.ts b/shared/src/i18n/pl/native.ts index da15ab95d..2a82571da 100644 --- a/shared/src/i18n/pl/native.ts +++ b/shared/src/i18n/pl/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Polityka prywatności', 'native.settings.server': 'Serwer', 'native.settings.serverHint': 'Ta aplikacja jest połączona z {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/ru/native.ts b/shared/src/i18n/ru/native.ts index cc5a38724..1b25e3d39 100644 --- a/shared/src/i18n/ru/native.ts +++ b/shared/src/i18n/ru/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Политика конфиденциальности', 'native.settings.server': 'Сервер', 'native.settings.serverHint': 'Приложение подключено к {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/sk/native.ts b/shared/src/i18n/sk/native.ts index 2faba5d44..8a9f05161 100644 --- a/shared/src/i18n/sk/native.ts +++ b/shared/src/i18n/sk/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Zásady ochrany osobných údajov', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'Táto aplikácia je pripojená k serveru {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/sv/native.ts b/shared/src/i18n/sv/native.ts index 48fdac8c4..974e07111 100644 --- a/shared/src/i18n/sv/native.ts +++ b/shared/src/i18n/sv/native.ts @@ -25,6 +25,21 @@ const native: TranslationStrings = { 'native.privacy': 'Integritetspolicy', 'native.settings.server': 'Server', // same-as-en 'native.settings.serverHint': 'Den här appen är ansluten till {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/th/native.ts b/shared/src/i18n/th/native.ts index 9f7a3c1ff..018915996 100644 --- a/shared/src/i18n/th/native.ts +++ b/shared/src/i18n/th/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'นโยบายความเป็นส่วนตัว', 'native.settings.server': 'เซิร์ฟเวอร์', 'native.settings.serverHint': 'แอปนี้เชื่อมต่อกับ {server} อยู่', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/tr/native.ts b/shared/src/i18n/tr/native.ts index 8998140ef..603b9e03a 100644 --- a/shared/src/i18n/tr/native.ts +++ b/shared/src/i18n/tr/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Gizlilik politikası', 'native.settings.server': 'Sunucu', 'native.settings.serverHint': 'Bu uygulama {server} adresine bağlı.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/uk/native.ts b/shared/src/i18n/uk/native.ts index 2da8ec2c0..df7e5d499 100644 --- a/shared/src/i18n/uk/native.ts +++ b/shared/src/i18n/uk/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': 'Політика конфіденційності', 'native.settings.server': 'Сервер', 'native.settings.serverHint': 'Застосунок підключено до {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/vi/native.ts b/shared/src/i18n/vi/native.ts index 3402c7a56..575009ece 100644 --- a/shared/src/i18n/vi/native.ts +++ b/shared/src/i18n/vi/native.ts @@ -25,6 +25,21 @@ const native: TranslationStrings = { 'native.privacy': 'Chính sách quyền riêng tư', 'native.settings.server': 'Máy chủ', 'native.settings.serverHint': 'Ứng dụng này đang kết nối với {server}.', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/zh-TW/native.ts b/shared/src/i18n/zh-TW/native.ts index 1dd7607bb..2a2f837f4 100644 --- a/shared/src/i18n/zh-TW/native.ts +++ b/shared/src/i18n/zh-TW/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': '隱私權政策', 'native.settings.server': '伺服器', 'native.settings.serverHint': '此 App 已連線到 {server}。', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native; diff --git a/shared/src/i18n/zh/native.ts b/shared/src/i18n/zh/native.ts index 68c6367d3..ced5e69ad 100644 --- a/shared/src/i18n/zh/native.ts +++ b/shared/src/i18n/zh/native.ts @@ -24,6 +24,21 @@ const native: TranslationStrings = { 'native.privacy': '隐私政策', 'native.settings.server': '服务器', 'native.settings.serverHint': '此应用已连接到 {server}。', + 'native.welcome.headline': 'Plan trips together.', + 'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.', + 'native.welcome.connect': 'Connect your TREK', + 'native.welcome.connectSub': 'Open the address you already use', + 'native.welcome.getCloud': 'Get TREK Cloud', + 'native.welcome.getCloudSub': 'Hosted for you, no setup', + 'native.cloud.title': 'TREK Cloud', + 'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.', + 'native.cloud.f1': 'Your own address, on servers in Germany', + 'native.cloud.f2': 'Set up in minutes, updates handled for you', + 'native.cloud.f3': 'Your data stays yours — no ads, no tracking', + 'native.cloud.cta': 'Get started', + 'native.cloud.haveServer': 'I already have a TREK', + 'native.connecting': 'Connecting to {host}', + 'native.back': 'Back', }; export default native;