+5
-1
src/components/Header.tsx
+5
-1
src/components/Header.tsx
···
1
1
import { Link, useRouter } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
3
+
4
+
import { isAtTopAtom } from "~/utils/atoms";
2
5
3
6
export function Header({
4
7
backButtonCallback,
···
8
11
title?: string;
9
12
}) {
10
13
const router = useRouter();
14
+
const [isAtTop] = useAtom(isAtTopAtom);
11
15
//const what = router.history.
12
16
return (
13
-
<div className="flex items-center gap-4 px-4 py-3 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
17
+
<div className={`flex items-center gap-3 px-3 py-3 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-10 border-0 ${!isAtTop && "shadow"} border-gray-200 dark:border-gray-700`}>
14
18
{backButtonCallback ? (<Link
15
19
to=".."
16
20
//className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
+2
-2
src/components/InfiniteCustomFeed.tsx
+2
-2
src/components/InfiniteCustomFeed.tsx
···
113
113
<button
114
114
onClick={handleRefresh}
115
115
disabled={isRefetching}
116
-
className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed"
116
+
className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:dark:bg-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
117
117
aria-label="Refresh feed"
118
118
>
119
-
{isRefetching ? <RefreshIcon className="h-6 w-6 text-gray-600 dark:text-gray-400 animate-spin" /> : <RefreshIcon className="h-6 w-6 text-gray-600 dark:text-gray-400" />}
119
+
<RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} />
120
120
</button>
121
121
</>
122
122
);
+15
-15
src/components/UniversalPostRenderer.tsx
+15
-15
src/components/UniversalPostRenderer.tsx
···
1248
1248
// dont cursor: "pointer",
1249
1249
borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0,
1250
1250
}}
1251
-
className="border-gray-300 dark:border-gray-600"
1251
+
className="border-gray-300 dark:border-gray-800"
1252
1252
>
1253
1253
{isRepost && (
1254
1254
<div
···
1316
1316
width: isQuote ? 16 : 42,
1317
1317
height: isQuote ? 16 : 42,
1318
1318
}}
1319
-
className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600"
1319
+
className="border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600"
1320
1320
/>
1321
1321
</div>
1322
1322
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
···
1521
1521
hydrate embeds this deep but the connection here is implicit
1522
1522
todo: idk make this a real part of the embed shim so its not implicit */
1523
1523
<>
1524
-
<div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1524
+
<div className="border-gray-300 dark:border-gray-800 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1525
1525
(there is an embed here thats too deep to render)
1526
1526
</div>
1527
1527
</>
···
1544
1544
borderBottomWidth: 1,
1545
1545
marginBottom: 8,
1546
1546
}} // important for height animation
1547
-
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"
1547
+
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-800 was7"
1548
1548
>
1549
1549
{fullDateTimeFormat(post.indexedAt)}
1550
1550
</div>
···
1780
1780
//boxShadow: theme.cardShadow,
1781
1781
overflow: "hidden",
1782
1782
}}
1783
-
className="shadow border border-gray-200 dark:border-gray-700"
1783
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
1784
1784
>
1785
1785
<UniversalPostRenderer
1786
1786
post={post}
···
1897
1897
//boxShadow: theme.cardShadow,
1898
1898
overflow: "hidden",
1899
1899
}}
1900
-
className="shadow border border-gray-200 dark:border-gray-700"
1900
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
1901
1901
>
1902
1902
<UniversalPostRenderer
1903
1903
post={post}
···
1970
1970
//border: `1px solid ${theme.border}`,
1971
1971
overflow: "hidden",
1972
1972
}}
1973
-
className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900"
1973
+
className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900"
1974
1974
>
1975
1975
{lightboxIndex !== null && (
1976
1976
<Lightbox
···
2011
2011
overflow: "hidden",
2012
2012
//border: `1px solid ${theme.border}`,
2013
2013
}}
2014
-
className="border border-gray-200 dark:border-gray-700"
2014
+
className="border border-gray-200 dark:border-gray-800 was7"
2015
2015
>
2016
2016
{lightboxIndex !== null && (
2017
2017
<Lightbox
···
2061
2061
//border: `1px solid ${theme.border}`,
2062
2062
// height: 240, // fixed height for cropping
2063
2063
}}
2064
-
className="border border-gray-200 dark:border-gray-700"
2064
+
className="border border-gray-200 dark:border-gray-800 was7"
2065
2065
>
2066
2066
{lightboxIndex !== null && (
2067
2067
<Lightbox
···
2146
2146
//border: `1px solid ${theme.border}`,
2147
2147
//aspectRatio: "3 / 2", // overall grid aspect
2148
2148
}}
2149
-
className="border border-gray-200 dark:border-gray-700"
2149
+
className="border border-gray-200 dark:border-gray-800 was7"
2150
2150
>
2151
2151
{lightboxIndex !== null && (
2152
2152
<Lightbox
···
2283
2283
e.stopPropagation();
2284
2284
e.nativeEvent.stopImmediatePropagation();
2285
2285
}}
2286
-
className="lightbox-sidebar overscroll-none disablegutter border-l dark:border-gray-700 border-gray-300 fixed z-50 flex top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white"
2286
+
className="lightbox-sidebar overscroll-none disablegutter border-l dark:border-gray-800 was7 border-gray-300 fixed z-50 flex top-0 right-0 flex-col max-w-[350px] min-w-[350px] max-h-screen overflow-y-scroll dark:bg-gray-950 bg-white"
2287
2287
>
2288
2288
<ProfilePostComponent
2289
2289
did={post.did}
···
2587
2587
>
2588
2588
<div
2589
2589
style={containerStyle as React.CSSProperties}
2590
-
className="border border-gray-200 dark:border-gray-700"
2590
+
className="border border-gray-200 dark:border-gray-800 was7"
2591
2591
>
2592
2592
{thumb && (
2593
2593
<div
···
2601
2601
marginBottom: 8,
2602
2602
//borderBottom: `1px solid ${theme.border}`,
2603
2603
}}
2604
-
className="border-b border-gray-200 dark:border-gray-700"
2604
+
className="border-b border-gray-200 dark:border-gray-800 was7"
2605
2605
>
2606
2606
<img
2607
2607
src={thumb}
···
2727
2727
borderRadius: 12,
2728
2728
//border: `1px solid ${theme.border}`,
2729
2729
}}
2730
-
className="border border-gray-200 dark:border-gray-700"
2730
+
className="border border-gray-200 dark:border-gray-800 was7"
2731
2731
onClick={async (e) => {
2732
2732
e.stopPropagation();
2733
2733
setPlaying(true);
···
2768
2768
100 / (aspect ? aspect.width / aspect.height : 16 / 9)
2769
2769
}%`, // 16:9 = 56.25%, 4:3 = 75%
2770
2770
}}
2771
-
className="border border-gray-200 dark:border-gray-700"
2771
+
className="border border-gray-200 dark:border-gray-800 was7"
2772
2772
>
2773
2773
<ReactPlayer
2774
2774
src={url}
+53
-10
src/main.tsx
+53
-10
src/main.tsx
···
1
1
import "~/styles/app.css";
2
2
3
3
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
4
-
import { QueryClient, QueryClientProvider, } from "@tanstack/react-query";
5
-
import {
6
-
persistQueryClient,
7
-
} from "@tanstack/react-query-persist-client";
8
-
import { createRouter,RouterProvider } from "@tanstack/react-router";
4
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+
import { persistQueryClient } from "@tanstack/react-query-persist-client";
6
+
import { createRouter, RouterProvider } from "@tanstack/react-router";
7
+
import { useSetAtom } from "jotai";
8
+
import { useEffect } from "react";
9
9
//import { StrictMode } from "react";
10
10
import ReactDOM from "react-dom/client";
11
11
12
12
import reportWebVitals from "./reportWebVitals.ts";
13
13
// Import the generated route tree
14
14
import { routeTree } from "./routeTree.gen";
15
-
15
+
import { isAtTopAtom } from "./utils/atoms.ts";
16
16
17
17
const queryClient = new QueryClient({
18
18
defaultOptions: {
···
28
28
persistQueryClient({
29
29
queryClient,
30
30
persister: localStoragePersister,
31
-
})
31
+
});
32
32
33
33
// Create a new router instance
34
34
const router = createRouter({
···
54
54
root.render(
55
55
// double queries annoys me
56
56
// <StrictMode>
57
-
<QueryClientProvider client={queryClient}>
58
-
<RouterProvider router={router} />
59
-
</QueryClientProvider>
57
+
<QueryClientProvider client={queryClient}>
58
+
<ScrollTopWatcher />
59
+
<RouterProvider router={router} />
60
+
</QueryClientProvider>
60
61
// </StrictMode>
61
62
);
62
63
}
···
65
66
// to log results (for example: reportWebVitals(// /*mass comment*/ console.log))
66
67
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
67
68
reportWebVitals();
69
+
70
+
export default function ScrollTopWatcher() {
71
+
const setIsAtTop = useSetAtom(isAtTopAtom);
72
+
useEffect(() => {
73
+
const meta = document.querySelector('meta[name="theme-color"]');
74
+
let lastAtTop = window.scrollY === 0;
75
+
let timeoutId: number | undefined;
76
+
77
+
const setVars = (atTop: boolean) => {
78
+
const root = document.documentElement;
79
+
root.style.setProperty("--is-top", atTop ? "1" : "0");
80
+
81
+
const bg = getComputedStyle(root).getPropertyValue("--header-bg").trim();
82
+
if (meta && bg) meta.setAttribute("content", bg);
83
+
setIsAtTop(atTop);
84
+
};
85
+
86
+
const check = () => {
87
+
const atTop = window.scrollY === 0;
88
+
if (atTop !== lastAtTop) {
89
+
lastAtTop = atTop;
90
+
setVars(atTop);
91
+
}
92
+
};
93
+
94
+
const handleScroll = () => {
95
+
if (timeoutId) clearTimeout(timeoutId);
96
+
timeoutId = window.setTimeout(check, 2);
97
+
};
98
+
99
+
// initialize
100
+
setVars(lastAtTop);
101
+
window.addEventListener("scroll", handleScroll, { passive: true });
102
+
103
+
return () => {
104
+
window.removeEventListener("scroll", handleScroll);
105
+
if (timeoutId) clearTimeout(timeoutId);
106
+
};
107
+
}, []);
108
+
109
+
return null;
110
+
}
+7
-7
src/routes/__root.tsx
+7
-7
src/routes/__root.tsx
···
431
431
</button>
432
432
)}
433
433
434
-
<main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 pb-16 lg:pb-0">
434
+
<main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 pb-16 lg:pb-0 overflow-x-clip">
435
435
{children}
436
436
</main>
437
437
···
448
448
</div>
449
449
450
450
{agent?.did ? (
451
-
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40">
451
+
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 shadow border-gray-200 dark:border-gray-700 z-40">
452
452
<div className="flex justify-around items-center p-2">
453
453
<MaterialNavItem
454
454
small
···
616
616
</div>
617
617
</nav>
618
618
) : (
619
-
<div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 z-10">
619
+
<div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10">
620
620
<div className="flex items-center gap-2">
621
621
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" />
622
622
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
···
682
682
<button
683
683
className={`flex flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 w-full items-center rounded-full transition-colors flex-1 gap-1 ${
684
684
active
685
-
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
686
-
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
685
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700"
686
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900"
687
687
}`}
688
688
onClick={() => {
689
689
onClickCallbback();
···
693
693
{active ? ActiveIcon : InactiveIcon}
694
694
</div>
695
695
<span
696
-
className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`}
696
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
697
697
>
698
698
{text}
699
699
</span>
···
732
732
{active ? ActiveIcon : InactiveIcon}
733
733
</div>
734
734
<span
735
-
className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`}
735
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
736
736
>
737
737
{text}
738
738
</span>
+6
-2
src/routes/index.tsx
+6
-2
src/routes/index.tsx
···
10
10
agentAtom,
11
11
authedAtom,
12
12
feedScrollPositionsAtom,
13
+
isAtTopAtom,
13
14
selectedFeedUriAtom,
14
15
store,
15
16
} from "~/utils/atoms";
···
350
351
authed && agent && identity?.pds && feedServiceDid;
351
352
const isReadyForUnauthedFeed = !authed && selectedFeed;
352
353
354
+
355
+
const [isAtTop] = useAtom(isAtTopAtom);
356
+
353
357
return (
354
358
<div
355
-
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`}
359
+
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"} ${!isAtTop && "shadow"}`}
356
360
>
357
361
{savedFeeds.length > 0 ? (
358
-
<div className="flex items-center px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin">
362
+
<div className="flex items-center px-4 py-2 h-[52px] sticky top-0 bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-10 border-0 border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin">
359
363
{savedFeeds.map((item: any, idx: number) => {
360
364
const label = item.value.split("/").pop() || item.value;
361
365
const isActive = selectedFeed === item.value;
+19
src/styles/app.css
+19
src/styles/app.css
···
86
86
}
87
87
.font-roboto {
88
88
font-family: "Roboto", sans-serif;
89
+
}
90
+
91
+
:root {
92
+
--header-bg-light: color-mix(in srgb, var(--color-white) calc(var(--is-top) * 100%), var(--color-gray-50));
93
+
--header-bg-dark: color-mix(in srgb, var(--color-gray-950) calc(var(--is-top) * 100%), var(--color-gray-900));
94
+
}
95
+
96
+
:root {
97
+
--header-bg: var(--header-bg-light);
98
+
}
99
+
@media (prefers-color-scheme: dark) {
100
+
:root {
101
+
--header-bg: var(--header-bg-dark);
102
+
}
103
+
}
104
+
105
+
:root {
106
+
--shadow-opacity: calc(1 - var(--is-top));
107
+
--tw-shadow-header: 0 2px 8px hsl(0 0% 0% / calc(var(--shadow-opacity) * 0.15));
89
108
}