diff --git a/assets/css/app.css b/assets/css/app.css index e599027..65ab2fc 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -152,31 +152,6 @@ html, body { margin: 0 auto; } -@media (max-width: 960px) { - .mx-root { grid-template-columns: 64px 1fr; } - .mx-rightbar { display: none; } -} -@media (max-width: 640px) { - .mx-root { grid-template-columns: 1fr; } - .mx-sidebar { display: none; } - /* space for fixed bottom nav */ - .mx-main { padding-bottom: 72px; } - /* hide inline compose on mobile — the overlay handles it */ - .mx-compose-wrapper { display: none; } - /* tighten card spacing */ - .mx-feed { padding: 0.625rem 0.625rem; gap: 0.5rem; } - .mx-tweet { padding: 0.875rem 0.875rem; } - /* truncate long email addresses */ - .mx-tweet-handle { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 180px; - } - .mx-header { padding: 0.75rem 1rem; } - .mx-detail { padding: 0.875rem 1rem; } -} - /* ── Sidebar ── */ .mx-sidebar { position: sticky; @@ -841,7 +816,7 @@ html, body { padding-bottom: env(safe-area-inset-bottom, 0px); } -@media (max-width: 640px) { +@media (max-width: 960px) { .mx-mobile-nav { display: flex; } } @@ -906,7 +881,7 @@ html, body { display: none; } -@media (max-width: 640px) { +@media (max-width: 960px) { .mx-compose-overlay { display: flex; flex-direction: column; @@ -959,3 +934,34 @@ html, body { overflow-y: auto; padding: 1.25rem; } + +/* ─────────────────────────────────────────────────────────────────────────────── + Responsive layout overrides + IMPORTANT: these rules must live AFTER all component base rules so that + the cascade works correctly (later rule of equal specificity wins). +─────────────────────────────────────────────────────────────────────────────── */ + +/* Tablet + mobile (≤ 960 px): single column, no side panels, bottom nav */ +@media (max-width: 960px) { + .mx-root { grid-template-columns: 1fr; } + .mx-sidebar { display: none; } + .mx-rightbar { display: none; } + /* room for fixed bottom nav */ + .mx-main { padding-bottom: 72px; } + /* hide inline compose — the overlay FAB handles it */ + .mx-compose-wrapper { display: none; } +} + +/* Narrow phones (≤ 640 px): tighten spacing */ +@media (max-width: 640px) { + .mx-feed { padding: 0.625rem; gap: 0.5rem; } + .mx-tweet { padding: 0.875rem; } + .mx-tweet-handle { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 180px; + } + .mx-header { padding: 0.75rem 1rem; } + .mx-detail { padding: 0.875rem 1rem; } +} diff --git a/assets/js/index.tsx b/assets/js/index.tsx index ee72b4e..2c38e98 100644 --- a/assets/js/index.tsx +++ b/assets/js/index.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useState, useRef, useEffect } from "react"; +import React, { createContext, useContext, useState, useRef, useEffect, useSyncExternalStore } from "react"; import { createRoot } from "react-dom/client"; import { createPortal } from "react-dom"; import { @@ -53,6 +53,28 @@ type Tweet = { const AuthCtx = createContext({ email: "", userId: "" }); +// ── Responsive helper ───────────────────────────────────────────────────────── +// Returns true when the viewport is wider than 960 px (desktop layout). +// Uses useSyncExternalStore so it re-renders on resize without a manual +// useEffect + useState dance. + +const DESKTOP_MQ = typeof window !== "undefined" + ? window.matchMedia("(min-width: 961px)") + : null; + +function subscribe(cb: () => void) { + DESKTOP_MQ?.addEventListener("change", cb); + return () => DESKTOP_MQ?.removeEventListener("change", cb); +} + +function useIsDesktop(): boolean { + return useSyncExternalStore( + subscribe, + () => DESKTOP_MQ?.matches ?? true, + () => true, // SSR snapshot (never actually used here) + ); +} + // ── Helpers ──────────────────────────────────────────────────────────────────── function timeAgo(insertedAt?: string | null): string { @@ -1203,6 +1225,7 @@ function App() { const profileUserId = appEl.dataset.userId || null; const [mobileCompose, setMobileCompose] = useState(false); + const isDesktop = useIsDesktop(); const onFeedPage = page === "feed" || page === "tweet"; const onUsersPage = page === "users" || page === "user-detail"; @@ -1267,58 +1290,62 @@ function App() {
- + {isDesktop && ( + + )}
{renderMain()}
-
-
-

About Mixer

-

- A minimal social feed built with Ash Framework, Phoenix, and React. -

-
- {["Ash 3", "Phoenix 1.8", "AshTypescript", "React 19"].map((s) => ( - {s} - ))} + {isDesktop && ( +
+
+

About Mixer

+

+ A minimal social feed built with Ash Framework, Phoenix, and React. +

+
+ {["Ash 3", "Phoenix 1.8", "AshTypescript", "React 19"].map((s) => ( + {s} + ))} +
-
+ )}
{/* Mobile-only bottom nav — hidden on desktop via CSS */}