diff --git a/src/components/MobileFeed.tsx b/src/components/MobileFeed.tsx index 86b1244..91e55db 100644 --- a/src/components/MobileFeed.tsx +++ b/src/components/MobileFeed.tsx @@ -7,6 +7,8 @@ import { HexAvatar } from "./HexAvatar"; import { PlayIcon, PostMenu } from "./PostCard"; import { VideoPlayer } from "./VideoPlayer"; import { FeaturedCarousel } from "./FeaturedCarousel"; +import { Logo } from "./Logo"; +import { useTheme } from "@/lib/use-theme"; import { MobileDock } from "./MobileDock"; import { BottomSheet } from "./BottomSheet"; import { CommentsSection } from "./CommentsSection"; @@ -220,6 +222,35 @@ function PostSlide({ post, index, current, p }: { post: CardPost; index: number; ); } +/** Height of the progress rail, as a percent: the slide on screen out of the loaded slides. */ +export function railPercent(index: number, count: number): number { + if (count <= 0) return 0; + return Math.round((Math.min(Math.max(index, 0), count - 1) + 1) / count * 100); +} + +const topIcon = { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true } as const; + +/** Frosted pills over the feed (spec "Top bar"): logo and wordmark left, theme toggle right. */ +function TopBar() { + const { theme, toggleTheme } = useTheme(); + const next = theme === "dark" ? "light" : "dark"; + return ( +
+
+ + MINNECT +
+ +
+ ); +} + function ChevronDown() { return (
) : null} + +
* { pointer-events: auto; } +.ph-glass { + background: var(--glass); + -webkit-backdrop-filter: blur(16px) saturate(1.6); + backdrop-filter: blur(16px) saturate(1.6); + box-shadow: inset 0 0 0 1px var(--line); + color: var(--ink); + transition: background .3s var(--ease), color .3s var(--ease); +} +.reel-feed[data-on-media="true"] .ph-glass { + background: var(--media-glass); + color: var(--on-media); + box-shadow: inset 0 0 0 1px var(--on-media-line); +} +.ph-brand { display: flex; align-items: center; gap: 8px; height: 42px; padding: 0 14px 0 10px; border-radius: 14px; } +.ph-brand .type-wordmark { font-size: 13px; } +.ph-theme { appearance: none; border: 0; margin: 0; display: grid; place-items: center; width: 42px; height: 42px; padding: 0; border-radius: 14px; cursor: pointer; } +.ph-theme svg { width: 20px; height: 20px; } +.ph-theme:active { transform: scale(.94); } + +/* Progress rail: position in the loaded feed, on the right edge. */ +.rail-v { + position: absolute; + right: 5px; + top: 50%; + height: 110px; + margin-top: -55px; + width: 3px; + border-radius: 2px; + background: var(--line-2); + z-index: 15; + overflow: hidden; + pointer-events: none; +} +.rail-v i { display: block; width: 100%; background: var(--accent); border-radius: 2px; transition: height .5s var(--ease); } +@media (prefers-reduced-motion: reduce) { .rail-v i { transition: none; } } diff --git a/tests/mobile-feed.test.ts b/tests/mobile-feed.test.ts index f007e86..618ffae 100644 --- a/tests/mobile-feed.test.ts +++ b/tests/mobile-feed.test.ts @@ -3,7 +3,7 @@ import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { readFileSync } from "node:fs"; import { FeedView, type FeedViewProps } from "@/components/FeedView"; -import { pickCurrent, isDoubleTap, isTapTarget, featuredSlideIndex } from "@/components/MobileFeed"; +import { pickCurrent, isDoubleTap, isTapTarget, featuredSlideIndex, railPercent } from "@/components/MobileFeed"; import { FIXTURE_DISCOVER } from "@/dev/fixtures"; import { CompactLayoutContext, isCompactWidth } from "@/components/layout-context"; import { toCardPosts } from "@/lib/feed-model"; @@ -171,3 +171,27 @@ describe("featured experts slide", () => { expect(render({})).not.toContain("feed-slide-featured"); }); }); + +describe("top bar and progress rail", () => { + it("renders the brand pill, the theme toggle and the rail", () => { + const html = render({}); + expect(html).toContain('data-testid="top-bar"'); + expect(html).toContain("MINNECT"); + expect(html).toMatch(/data-testid="top-theme"[^>]*aria-label="Switch to (light|dark) theme"/); + expect(html).toContain('data-testid="feed-rail"'); + expect(html).toContain('data-on-media="false"'); + }); + + it("sizes the rail by the slide on screen out of the loaded slides", () => { + expect(railPercent(0, 4)).toBe(25); + expect(railPercent(2, 4)).toBe(75); + expect(railPercent(9, 4)).toBe(100); + expect(railPercent(0, 0)).toBe(0); + }); + + it("keeps the own-post menu below the bar and the safe area", () => { + const css = readFileSync("src/styles/mobile.css", "utf8"); + expect(css).toMatch(/\.slide-menu \{[^}]*top: calc\(70px \+ env\(safe-area-inset-top/); + expect(css).toMatch(/\.ph-top \{[^}]*safe-area-inset-top/); + }); +});