Top-tabs above the custom-domains grid: Seiten (web) and Mails (mail_*).
2px underline highlight in colors.brandOrange for the active tab, the
muted label otherwise — matches the community/feed tab style we already
use. Pill segmented control would have needed extra inset math for two
tabs without adding clarity.
- DomainGrid filters items by the active tab. Tab-specific empty-state
copy and icon (mail-outline for the Mails tab) so the empty Mails tab
doesn't read like a broken Web view.
- mail_display_name tiles hide the submit-to-global button entirely —
matches the v1.0 backend lock; the user can't accidentally tap into a
400 from the API.
- useCustomDomains exposes countsByType + limits. Provisional client-
side estimation until the new API response shape (extended in the
parallel backend commit f2b81ee) is wired through — same TS shape,
so dropping the estimation is a one-line swap when ready.
- AddDomainSheet picks up initialType so tapping "+" while the Mails tab
is active opens the sheet pre-selected to E-Mail. Plan-limit error
handling maps WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED to the right
per-bucket message.
i18n: tabs_web / tabs_mail / count_label / error_web_limit_reached /
error_mail_limit_reached / empty_web / empty_mail across DE/EN/FR with
%{var} placeholders.
AddDomainSheet now opens with a Seite / E-Mail segmented control.
Web keeps the existing flow (label, placeholder, favicon preview,
domain normalization). Mail switches to a free-form pattern input
(address / domain / display-name — user types what they see in
their inbox) with a mail-icon preview after the field is filled.
addDomain(pattern, kind) now sends { pattern, kind: 'web' | 'mail' }
and the server decides the concrete type. Type field flows through
the CustomDomain type so DomainGrid tiles render the mail-outline
icon for mail entries instead of the favicon fallback.
i18n: blocker.type_web / type_mail / add_web_* / add_mail_* across
de/en/fr with %{var} placeholders per repo convention.
Removes 2-tab Groups/DMs layout; Chat screen is now DM-only for v1.0.
Groups tab state, rooms query, RoomCard/CreateRoomSheet imports removed.
Replaces static title+create-button header with sticky search field
(client-side filter on partnerName + lastMessage). No create-DM button
added — /dm-new route does not exist yet (follow-up task).
All #007AFF in chat.tsx replaced with colors.brandOrange.
Adds chat.search_placeholder to de/en/fr locales.
Tab-bar styles kept in makeStyles (dead code, v1.1 Groups comeback path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four issues from the screenshot review plus one new affordance:
1. Modal overflowing on small devices — capped at maxHeight: '85%'. Header
(handle bar + Lyra avatar + title + subtitle) stays fixed above a
ScrollView body; action buttons stay fixed below with a border separator.
Stat cards, star rating, and TextInput now live inside the scrollable body.
2. Keyboard pushed the TextInput out of sight — replaced the bespoke
Keyboard.addListener + Animated.multiply lift hack (Easing, keyboardLiftY,
the whole apparatus) with a plain KeyboardAvoidingView wrapper
(behavior="padding" iOS / "height" Android). ScrollView already had
keyboardShouldPersistTaps="handled" so taps on Posten/Abbrechen still
work while the keyboard is up.
3. All four action buttons (Nochmal, Beenden, Abbrechen, Posten) plus the
inner Save-Rating CTA now route through components/Button.tsx — picks
up the slimmer paddingVertical:12 default from the central component.
Posten gets the paper-plane icon. Nochmal + Posten = primary, Beenden +
Abbrechen = secondary.
4. New "Neuer Vorschlag" regenerate button (ghost variant, sm size,
refresh-outline icon) sits between the TextInput and the Abbrechen/
Posten row. Reuses POST /api/games/share-text — no new endpoint. Tracks
the last Lyra-generated text in a ref so we can detect user edits; if
the user has modified the suggestion, taps go through an Alert.alert
confirm before overwrite. Spinner during the regen call, Posten /
Abbrechen stay active. i18n keys gameOver.regen_* across DE/EN/FR.
- Add auth.device_locked_* keys (DE/EN/FR): headline, body, countdown,
email_hint, use_original CTA, back link
- Add devices.bound_badge + devices.release_* keys (DE/EN/FR) for the
bound-device / release-flow in the Devices page
- Extend UserDevice interface with boundToPlan and releaseRequestedAt
- Add requestRelease + cancelRelease store actions calling the new
POST /api/devices/:id/request-release|cancel-release endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The new counter_some / counter_limit keys (added in e8ea005) used
i18next default {{var}} braces, but lib/i18n.ts configures the
interpolator with prefix: '%{', suffix: '}' (legacy Nuxt locale-file
convention, kept verbatim when ported to RN). Result: the placeholders
rendered literally on screen ("{{count}} von {{max}} Geräten…").
Switched all three locales (DE/EN/FR) to %{var}. Also dropped the
literal "+ " prefix from the add_device label — the button now renders
an Ionicons `add-circle-outline`, so the duplicate "+" was redundant.
- MobileDeviceRow: collapse to 2 lines (name+badge / lastSeen · seit date)
- ProtectedDeviceRow: collapse to 2 lines (name+badge / seit date or degraded hint)
- Both rows now use alignItems:center for visual parity
- Replace dual Mac/Windows buttons with single UIMenu "+ neues Gerät hinzufügen"
- MenuView disabled (no-op TouchableOpacity) when at device limit
- Dynamic counter below subtitle: "X von 3 Geräten · noch Y frei" / "Maximum erreicht"
- paddingVertical 16→12 on all primary CTAs in devices.tsx, AddMacSheet, AddWindowsSheet
- New i18n keys: devices.add_device, devices.counter_some, devices.counter_limit (DE/EN/FR)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lib/api.ts: sends x-device-name + x-device-model + x-device-os headers
(cached per session, URL-encoded). Backend persists into user_devices for
visual differentiation in DeviceLimitSheet.
- DeviceLimitReachedSheet: renders name (primary) + model · OS-version
(secondary), "Dieses Gerät"-Pill on isCurrent. Stale phantoms become
distinguishable.
- Profile i18n sweep: 8 keys × 3 languages = 24 fixes — all {{var}} placeholders
switched to %{var} matching i18next config (Vue-i18n leftover from Nuxt-port).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>