fix(tg): mini-app responsiveness — safe-area activation, truncation, touch targets (#647)

Root cause first: no viewport export existed anywhere, so viewport-fit
was never 'cover' and every env(safe-area-inset-*) resolved to 0 on
notched iPhones — content under the status bar, dock without real
home-indicator clearance. The export lives on the (tg) group layout
(server component), NOT app-wide: the dashboard shell has no safe-area
padding and must not inherit cover.

Also: min-w-0 on four truncating flex children that overflowed their
justify-between rows (chat names/previews, fleet task titles);
object-contain on the approvals video (letterbox instead of distort on
short phones); break-words on the changelog pre / task description /
quoted mention; touch targets bumped to >=36px (sheet close, segmented
controls, ack button, chips, back button, bell, jump-to-latest,
cut-toggle); overflow-x-hidden backstop on the (tg) main scroller; fleet
avatar strip sliced to 3 with a +N badge instead of silent clipping.

Verified: pnpm typecheck clean, lint 0 errors, panel suite 870/870.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-22 20:29:57 +02:00
committed by GitHub
co-authored by Renn F
parent 583d43f213
commit d1f9d21a68
11 changed files with 36 additions and 20 deletions
+12 -1
View File
@@ -1,6 +1,17 @@
import type { Viewport } from "next";
import Script from "next/script";
import localFont from "next/font/local";
// Scoped to the (tg) group: viewport-fit=cover is what makes the
// env(safe-area-inset-*) paddings below resolve to real values on notched
// phones. The dashboard shell has no safe-area handling, so it must NOT
// inherit cover (content would sit under the sensor housing in landscape).
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
};
// The brand display face — the same vendored Share Tech Mono the motion/
// video compositions use for their headline moments. Exposed as a CSS
// variable on the shell so `.tg-display` (globals.css) can reach it; body
@@ -45,7 +56,7 @@ export default function TelegramLayout({
src="https://telegram.org/js/telegram-web-app.js"
strategy="afterInteractive"
/>
<main className="flex-1 overflow-auto pt-[env(safe-area-inset-top)]">
<main className="flex-1 overflow-x-hidden overflow-y-auto pt-[env(safe-area-inset-top)]">
{children}
</main>
</div>
+1 -1
View File
@@ -187,7 +187,7 @@ function CockpitShell() {
aria-label={unread > 0 ? `Inbox, ${unread} unread` : "Inbox"}
onClick={() => setInboxOpen(true)}
className={cn(
"relative flex h-9 w-9 items-center justify-center rounded-full bg-card text-muted-foreground",
"relative flex h-10 w-10 items-center justify-center rounded-full bg-card text-muted-foreground",
TG_PRESS,
)}
>
@@ -110,7 +110,7 @@ export function ReleaseDetail({
)}
<TgSection title="Changelog draft">
<pre className="whitespace-pre-wrap rounded-xl bg-muted p-2 text-xs leading-relaxed">
<pre className="whitespace-pre-wrap break-words rounded-xl bg-muted p-2 text-xs leading-relaxed">
{report.drafted_changelog}
</pre>
</TgSection>
@@ -72,7 +72,7 @@ function CutPlayer({ post }: { post: VideoPost }) {
{(Object.keys(CUT_LABELS) as VideoCut[]).map((c) => (
<Button
key={c}
size="sm"
size="default"
variant={cut === c ? "default" : "outline"}
disabled={!paths[c]}
onClick={() => setCut(c)}
@@ -88,7 +88,7 @@ function CutPlayer({ post }: { post: VideoPost }) {
controls
playsInline
className={cn(
"w-full rounded-md bg-black",
"w-full rounded-md bg-black object-contain",
cut === "vertical"
? "aspect-[9/16] max-h-[60dvh]"
: "aspect-square",
@@ -81,7 +81,7 @@ export function XPostDetail({
</div>
{post.mention && (
<blockquote className="border-l-2 pl-2 text-xs text-muted-foreground">
<blockquote className="break-words border-l-2 pl-2 text-xs text-muted-foreground">
{post.mention.text}
</blockquote>
)}
+1 -1
View File
@@ -89,7 +89,7 @@ export function TgSheet({
type="button"
aria-label="Close"
onClick={onClose}
className="flex h-7 w-7 items-center justify-center rounded-full bg-muted text-muted-foreground transition-transform active:scale-90"
className="flex h-9 w-9 items-center justify-center rounded-full bg-muted text-muted-foreground transition-transform active:scale-90"
>
<X weight="bold" className="h-4 w-4" />
</button>
+6 -6
View File
@@ -254,7 +254,7 @@ function SecretaryView({ onBack }: { onBack: () => void }) {
haptics.tap();
void stop().then(onBack);
}}
className="rounded-full bg-muted/60 px-3 py-1.5 text-xs font-medium text-muted-foreground"
className="rounded-full bg-muted/60 px-3 py-2 text-xs font-medium text-muted-foreground"
>
End
</button>
@@ -399,7 +399,7 @@ function MineList({
<TgAvatar name={c.other_agent} />
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2">
<span className="truncate text-[15px] font-medium">
<span className="min-w-0 truncate text-[15px] font-medium">
{getAgentDisplayName(c.other_agent)}
</span>
{c.last_message_at && (
@@ -411,7 +411,7 @@ function MineList({
<div className="mt-0.5 flex items-center justify-between gap-2">
<p
className={cn(
"truncate text-[13px] leading-snug",
"min-w-0 truncate text-[13px] leading-snug",
c.unread_count > 0
? "font-medium text-foreground/80"
: "text-muted-foreground",
@@ -478,7 +478,7 @@ function FleetList({
<PairAvatars a={c.agent_a} b={c.agent_b} />
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2">
<span className="truncate text-[15px] font-medium">
<span className="min-w-0 truncate text-[15px] font-medium">
{getAgentDisplayName(c.agent_a)}
<span className="mx-1 text-muted-foreground/50"></span>
{getAgentDisplayName(c.agent_b)}
@@ -838,7 +838,7 @@ function ThreadView({
setShowJump(false);
}}
className={cn(
"absolute bottom-2 right-2 flex h-9 w-9 items-center justify-center rounded-full bg-card text-primary shadow-lg ring-1 ring-white/[0.08]",
"absolute bottom-2 right-2 flex h-10 w-10 items-center justify-center rounded-full bg-card text-primary shadow-lg ring-1 ring-white/[0.08]",
TG_PRESS,
)}
>
@@ -863,7 +863,7 @@ function ThreadView({
fleetTarget === threadRef.a ? threadRef.b : threadRef.a,
)
}
className="mt-2 inline-flex items-center gap-1 rounded-full bg-muted/60 px-3 py-1 text-xs font-medium text-muted-foreground"
className="mt-2 inline-flex items-center gap-1 rounded-full bg-muted/60 px-3 py-2 text-xs font-medium text-muted-foreground"
>
To: {getAgentDisplayName(fleetTarget)}
<span className="text-muted-foreground/50">· tap to switch</span>
+1 -1
View File
@@ -145,7 +145,7 @@ function NotificationRow({
type="button"
disabled={ackPending}
onClick={onAck}
className="flex h-7 shrink-0 items-center gap-1 rounded-full bg-primary px-2.5 text-xs font-semibold text-primary-foreground disabled:opacity-60"
className="flex h-9 shrink-0 items-center gap-1 rounded-full bg-primary px-2.5 text-xs font-semibold text-primary-foreground disabled:opacity-60"
>
<Check className="h-3.5 w-3.5" />
Ack
+1 -1
View File
@@ -268,7 +268,7 @@ export function TgTaskSheet({
</div>
{task.description && (
<p className="whitespace-pre-line text-sm leading-relaxed text-muted-foreground">
<p className="whitespace-pre-line break-words text-sm leading-relaxed text-muted-foreground">
{task.description}
</p>
)}
+8 -3
View File
@@ -198,7 +198,7 @@ function NeedsYouBanner({
key={key}
type="button"
onClick={onApprovals}
className="rounded-full bg-violet-500/15 px-2.5 py-1 text-xs font-medium tabular-nums text-violet-300 transition-colors active:bg-violet-500/25"
className="rounded-full bg-violet-500/15 px-2.5 py-2 text-xs font-medium tabular-nums text-violet-300 transition-colors active:bg-violet-500/25"
>
{DRAFT_LABELS[key] ?? key} · {count}
</button>
@@ -474,9 +474,14 @@ export function TgTodayTab({
className="w-full space-y-2 text-left"
>
<div className="flex gap-2 overflow-hidden">
{fleet.working.map((a) => (
{fleet.working.slice(0, 3).map((a) => (
<TgAvatar key={a.name} name={a.name} active />
))}
{fleet.working.length > 3 && (
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-muted/70 text-[10px] font-semibold text-muted-foreground">
+{fleet.working.length - 3}
</span>
)}
</div>
<ul className="space-y-1">
{fleet.working.slice(0, 3).map((agent) => (
@@ -488,7 +493,7 @@ export function TgTodayTab({
{agent.name}
</span>
{agent.task_title && (
<span className="truncate text-muted-foreground">
<span className="min-w-0 truncate text-muted-foreground">
{agent.task_title}
</span>
)}
+2 -2
View File
@@ -344,7 +344,7 @@ export function TgSegmented<T extends string>({
onChange(o.value);
}}
className={cn(
"relative z-10 rounded-full py-1.5 text-center text-[13px] font-medium transition-colors duration-200",
"relative z-10 rounded-full py-2 text-center text-[13px] font-medium transition-colors duration-200",
o.value === value ? "text-foreground" : "text-muted-foreground",
)}
>
@@ -385,7 +385,7 @@ export function TgSubPage({
aria-label="Back"
onClick={onBack}
className={cn(
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-card text-muted-foreground",
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-card text-muted-foreground",
TG_PRESS,
)}
>