From 1e5c77d9164e7a773dc69477e5d1fdaeaea693ad Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:37:11 +0800 Subject: [PATCH] feat(ui): polish living room presentation (#355) --- src/client/components/living/BrandMark.tsx | 25 ++++++- src/client/components/living/RouteTree.tsx | 25 +++++-- .../components/living/route-tree-layout.ts | 8 ++- src/client/pages/ViewerPage.tsx | 4 ++ src/client/styles.css | 72 +++++++++++++------ tests/living-components.test.ts | 2 +- tests/route-tree.test.ts | 58 ++++++++++++++- 7 files changed, 158 insertions(+), 36 deletions(-) diff --git a/src/client/components/living/BrandMark.tsx b/src/client/components/living/BrandMark.tsx index 4cbc718..3811ec0 100644 --- a/src/client/components/living/BrandMark.tsx +++ b/src/client/components/living/BrandMark.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState, type AnimationEvent } from "react"; import { bindSvgReplayOnPointerEnter } from "../../ui/animation"; export type BrandMotion = "static" | "once" | "loop"; @@ -11,15 +11,30 @@ export function BrandMark({ motion?: BrandMotion; }) { const animated = motion !== "static"; + const [loopPhase, setLoopPhase] = useState<"intro" | "active">("intro"); const motionClass = - motion === "loop" ? " is-animated" : motion === "once" ? " is-once" : ""; + motion === "loop" + ? ` is-animated is-loop-${loopPhase}` + : motion === "once" + ? " is-once" + : ""; const markRef = useRef(null); + useEffect(() => { + if (motion === "loop") setLoopPhase("intro"); + }, [motion]); + useEffect(() => { if (motion !== "once" || !markRef.current) return; return bindSvgReplayOnPointerEnter(markRef.current); }, [motion]); + const finishLoopIntro = (event: AnimationEvent): void => { + if (motion === "loop" && event.animationName === "lr-brand-sparkle-alt") { + setLoopPhase("active"); + } + }; + return ( - + ); diff --git a/src/client/components/living/RouteTree.tsx b/src/client/components/living/RouteTree.tsx index 2eb68d8..6004750 100644 --- a/src/client/components/living/RouteTree.tsx +++ b/src/client/components/living/RouteTree.tsx @@ -28,7 +28,7 @@ interface TreeNode { const ROW_BASE = 40; const DEFAULT_TOPOLOGY_WIDTH = 640; const PAWN_CENTER_X = 20; -const PAWN_SCALE = 0.72; +const PAWN_SCALE = 0.82; const PAWN_CENTER_Y = 24; const LABEL_GAP = 12; @@ -75,6 +75,20 @@ function compactVisibleLabel(label: string, maximumCodePoints: number): string { return `${codePoints.slice(0, maximumCodePoints - 1).join("")}…`; } +function topologyVisibleLabel( + label: string, + peerId: string | null, + maximumCodePoints: number, +): string { + const peerIdSuffix = peerId?.slice(-6) ?? ""; + const withoutRedundantRole = + peerIdSuffix && + (label === `👑-${peerIdSuffix}` || label === `👤-${peerIdSuffix}`) + ? peerIdSuffix + : label; + return compactVisibleLabel(withoutRedundantRole, maximumCodePoints); +} + function xForDepth(depth: number, layout: TopologyLayout): number { return layout.hostX + (depth + 1) * layout.columnGap; } @@ -355,8 +369,9 @@ export const RouteTree = memo(function RouteTree({ y={pawnLabelY(hostPos.y, PAWN_SCALE)} textAnchor="middle" > - {compactVisibleLabel( + {topologyVisibleLabel( hostLabel, + hostPeerId, layoutConfig.maxVisibleLabelCodePoints, )} @@ -438,8 +453,9 @@ export const RouteTree = memo(function RouteTree({ y={pawnLabelY(point.y, PAWN_SCALE)} textAnchor="middle" > - {compactVisibleLabel( + {topologyVisibleLabel( node.label, + node.key, layoutConfig.maxVisibleLabelCodePoints, )} @@ -453,8 +469,9 @@ export const RouteTree = memo(function RouteTree({ y={pawnLabelY(point.y, PAWN_SCALE)} textAnchor="middle" > - {compactVisibleLabel( + {topologyVisibleLabel( point.viewer.label, + point.viewer.peerId, layoutConfig.maxVisibleLabelCodePoints, )} diff --git a/src/client/components/living/route-tree-layout.ts b/src/client/components/living/route-tree-layout.ts index e82cda2..703d85c 100644 --- a/src/client/components/living/route-tree-layout.ts +++ b/src/client/components/living/route-tree-layout.ts @@ -23,11 +23,13 @@ export function topologyLayoutForWidth(containerWidth: number): TopologyLayout { const progress = (baseWidth - MIN_LAYOUT_WIDTH) / (MAX_LAYOUT_WIDTH - MIN_LAYOUT_WIDTH); + const columnGap = interpolate(130, 220, progress); + const hostX = (baseWidth - columnGap) / 2; return { baseWidth, - hostX: interpolate(54, 170, progress), - columnGap: interpolate(130, 220, progress), - rightLabelReserve: interpolate(65, 170, progress), + hostX, + columnGap, + rightLabelReserve: hostX, maxVisibleLabelCodePoints: baseWidth < 420 ? 10 : baseWidth < 560 ? 13 : 16, }; diff --git a/src/client/pages/ViewerPage.tsx b/src/client/pages/ViewerPage.tsx index e8c7edc..26eefd2 100644 --- a/src/client/pages/ViewerPage.tsx +++ b/src/client/pages/ViewerPage.tsx @@ -2633,6 +2633,10 @@ export function ViewerPage({ roomId, viewerGrant }: ViewerPageProps) { )}
+