+5
-1
src/components/Header.tsx
+5
-1
src/components/Header.tsx
···
1
import { Link, useRouter } from "@tanstack/react-router";
2
3
export function Header({
4
backButtonCallback,
···
8
title?: string;
9
}) {
10
const router = useRouter();
11
//const what = router.history.
12
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">
14
{backButtonCallback ? (<Link
15
to=".."
16
//className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
···
1
import { Link, useRouter } from "@tanstack/react-router";
2
+
import { useAtom } from "jotai";
3
+
4
+
import { isAtTopAtom } from "~/utils/atoms";
5
6
export function Header({
7
backButtonCallback,
···
11
title?: string;
12
}) {
13
const router = useRouter();
14
+
const [isAtTop] = useAtom(isAtTopAtom);
15
//const what = router.history.
16
return (
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`}>
18
{backButtonCallback ? (<Link
19
to=".."
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
<button
114
onClick={handleRefresh}
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"
117
aria-label="Refresh feed"
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" />}
120
</button>
121
</>
122
);
···
113
<button
114
onClick={handleRefresh}
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:dark:bg-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
117
aria-label="Refresh feed"
118
>
119
+
<RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} />
120
</button>
121
</>
122
);
+15
-15
src/components/UniversalPostRenderer.tsx
+15
-15
src/components/UniversalPostRenderer.tsx
···
1248
// dont cursor: "pointer",
1249
borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0,
1250
}}
1251
-
className="border-gray-300 dark:border-gray-600"
1252
>
1253
{isRepost && (
1254
<div
···
1316
width: isQuote ? 16 : 42,
1317
height: isQuote ? 16 : 42,
1318
}}
1319
-
className="border border-gray-300 dark:border-gray-600 bg-gray-300 dark:bg-gray-600"
1320
/>
1321
</div>
1322
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
···
1521
hydrate embeds this deep but the connection here is implicit
1522
todo: idk make this a real part of the embed shim so its not implicit */
1523
<>
1524
-
<div className="border-gray-300 dark:border-gray-600 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1525
(there is an embed here thats too deep to render)
1526
</div>
1527
</>
···
1544
borderBottomWidth: 1,
1545
marginBottom: 8,
1546
}} // important for height animation
1547
-
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"
1548
>
1549
{fullDateTimeFormat(post.indexedAt)}
1550
</div>
···
1780
//boxShadow: theme.cardShadow,
1781
overflow: "hidden",
1782
}}
1783
-
className="shadow border border-gray-200 dark:border-gray-700"
1784
>
1785
<UniversalPostRenderer
1786
post={post}
···
1897
//boxShadow: theme.cardShadow,
1898
overflow: "hidden",
1899
}}
1900
-
className="shadow border border-gray-200 dark:border-gray-700"
1901
>
1902
<UniversalPostRenderer
1903
post={post}
···
1970
//border: `1px solid ${theme.border}`,
1971
overflow: "hidden",
1972
}}
1973
-
className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900"
1974
>
1975
{lightboxIndex !== null && (
1976
<Lightbox
···
2011
overflow: "hidden",
2012
//border: `1px solid ${theme.border}`,
2013
}}
2014
-
className="border border-gray-200 dark:border-gray-700"
2015
>
2016
{lightboxIndex !== null && (
2017
<Lightbox
···
2061
//border: `1px solid ${theme.border}`,
2062
// height: 240, // fixed height for cropping
2063
}}
2064
-
className="border border-gray-200 dark:border-gray-700"
2065
>
2066
{lightboxIndex !== null && (
2067
<Lightbox
···
2146
//border: `1px solid ${theme.border}`,
2147
//aspectRatio: "3 / 2", // overall grid aspect
2148
}}
2149
-
className="border border-gray-200 dark:border-gray-700"
2150
>
2151
{lightboxIndex !== null && (
2152
<Lightbox
···
2283
e.stopPropagation();
2284
e.nativeEvent.stopImmediatePropagation();
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"
2287
>
2288
<ProfilePostComponent
2289
did={post.did}
···
2587
>
2588
<div
2589
style={containerStyle as React.CSSProperties}
2590
-
className="border border-gray-200 dark:border-gray-700"
2591
>
2592
{thumb && (
2593
<div
···
2601
marginBottom: 8,
2602
//borderBottom: `1px solid ${theme.border}`,
2603
}}
2604
-
className="border-b border-gray-200 dark:border-gray-700"
2605
>
2606
<img
2607
src={thumb}
···
2727
borderRadius: 12,
2728
//border: `1px solid ${theme.border}`,
2729
}}
2730
-
className="border border-gray-200 dark:border-gray-700"
2731
onClick={async (e) => {
2732
e.stopPropagation();
2733
setPlaying(true);
···
2768
100 / (aspect ? aspect.width / aspect.height : 16 / 9)
2769
}%`, // 16:9 = 56.25%, 4:3 = 75%
2770
}}
2771
-
className="border border-gray-200 dark:border-gray-700"
2772
>
2773
<ReactPlayer
2774
src={url}
···
1248
// dont cursor: "pointer",
1249
borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0,
1250
}}
1251
+
className="border-gray-300 dark:border-gray-800"
1252
>
1253
{isRepost && (
1254
<div
···
1316
width: isQuote ? 16 : 42,
1317
height: isQuote ? 16 : 42,
1318
}}
1319
+
className="border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600"
1320
/>
1321
</div>
1322
<div style={{ display: "flex", alignItems: "flex-start", zIndex: 2 }}>
···
1521
hydrate embeds this deep but the connection here is implicit
1522
todo: idk make this a real part of the embed shim so its not implicit */
1523
<>
1524
+
<div className="border-gray-300 dark:border-gray-800 p-3 rounded-xl border italic text-gray-400 text-[14px]">
1525
(there is an embed here thats too deep to render)
1526
</div>
1527
</>
···
1544
borderBottomWidth: 1,
1545
marginBottom: 8,
1546
}} // important for height animation
1547
+
className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-800 was7"
1548
>
1549
{fullDateTimeFormat(post.indexedAt)}
1550
</div>
···
1780
//boxShadow: theme.cardShadow,
1781
overflow: "hidden",
1782
}}
1783
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
1784
>
1785
<UniversalPostRenderer
1786
post={post}
···
1897
//boxShadow: theme.cardShadow,
1898
overflow: "hidden",
1899
}}
1900
+
className="shadow border border-gray-200 dark:border-gray-800 was7"
1901
>
1902
<UniversalPostRenderer
1903
post={post}
···
1970
//border: `1px solid ${theme.border}`,
1971
overflow: "hidden",
1972
}}
1973
+
className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900"
1974
>
1975
{lightboxIndex !== null && (
1976
<Lightbox
···
2011
overflow: "hidden",
2012
//border: `1px solid ${theme.border}`,
2013
}}
2014
+
className="border border-gray-200 dark:border-gray-800 was7"
2015
>
2016
{lightboxIndex !== null && (
2017
<Lightbox
···
2061
//border: `1px solid ${theme.border}`,
2062
// height: 240, // fixed height for cropping
2063
}}
2064
+
className="border border-gray-200 dark:border-gray-800 was7"
2065
>
2066
{lightboxIndex !== null && (
2067
<Lightbox
···
2146
//border: `1px solid ${theme.border}`,
2147
//aspectRatio: "3 / 2", // overall grid aspect
2148
}}
2149
+
className="border border-gray-200 dark:border-gray-800 was7"
2150
>
2151
{lightboxIndex !== null && (
2152
<Lightbox
···
2283
e.stopPropagation();
2284
e.nativeEvent.stopImmediatePropagation();
2285
}}
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
>
2288
<ProfilePostComponent
2289
did={post.did}
···
2587
>
2588
<div
2589
style={containerStyle as React.CSSProperties}
2590
+
className="border border-gray-200 dark:border-gray-800 was7"
2591
>
2592
{thumb && (
2593
<div
···
2601
marginBottom: 8,
2602
//borderBottom: `1px solid ${theme.border}`,
2603
}}
2604
+
className="border-b border-gray-200 dark:border-gray-800 was7"
2605
>
2606
<img
2607
src={thumb}
···
2727
borderRadius: 12,
2728
//border: `1px solid ${theme.border}`,
2729
}}
2730
+
className="border border-gray-200 dark:border-gray-800 was7"
2731
onClick={async (e) => {
2732
e.stopPropagation();
2733
setPlaying(true);
···
2768
100 / (aspect ? aspect.width / aspect.height : 16 / 9)
2769
}%`, // 16:9 = 56.25%, 4:3 = 75%
2770
}}
2771
+
className="border border-gray-200 dark:border-gray-800 was7"
2772
>
2773
<ReactPlayer
2774
src={url}
+53
-10
src/main.tsx
+53
-10
src/main.tsx
···
1
import "~/styles/app.css";
2
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";
9
//import { StrictMode } from "react";
10
import ReactDOM from "react-dom/client";
11
12
import reportWebVitals from "./reportWebVitals.ts";
13
// Import the generated route tree
14
import { routeTree } from "./routeTree.gen";
15
-
16
17
const queryClient = new QueryClient({
18
defaultOptions: {
···
28
persistQueryClient({
29
queryClient,
30
persister: localStoragePersister,
31
-
})
32
33
// Create a new router instance
34
const router = createRouter({
···
54
root.render(
55
// double queries annoys me
56
// <StrictMode>
57
-
<QueryClientProvider client={queryClient}>
58
-
<RouterProvider router={router} />
59
-
</QueryClientProvider>
60
// </StrictMode>
61
);
62
}
···
65
// to log results (for example: reportWebVitals(// /*mass comment*/ console.log))
66
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
67
reportWebVitals();
···
1
import "~/styles/app.css";
2
3
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
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
//import { StrictMode } from "react";
10
import ReactDOM from "react-dom/client";
11
12
import reportWebVitals from "./reportWebVitals.ts";
13
// Import the generated route tree
14
import { routeTree } from "./routeTree.gen";
15
+
import { isAtTopAtom } from "./utils/atoms.ts";
16
17
const queryClient = new QueryClient({
18
defaultOptions: {
···
28
persistQueryClient({
29
queryClient,
30
persister: localStoragePersister,
31
+
});
32
33
// Create a new router instance
34
const router = createRouter({
···
54
root.render(
55
// double queries annoys me
56
// <StrictMode>
57
+
<QueryClientProvider client={queryClient}>
58
+
<ScrollTopWatcher />
59
+
<RouterProvider router={router} />
60
+
</QueryClientProvider>
61
// </StrictMode>
62
);
63
}
···
66
// to log results (for example: reportWebVitals(// /*mass comment*/ console.log))
67
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
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
</button>
432
)}
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">
435
{children}
436
</main>
437
···
448
</div>
449
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">
452
<div className="flex justify-around items-center p-2">
453
<MaterialNavItem
454
small
···
616
</div>
617
</nav>
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">
620
<div className="flex items-center gap-2">
621
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" />
622
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
···
682
<button
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
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"
687
}`}
688
onClick={() => {
689
onClickCallbback();
···
693
{active ? ActiveIcon : InactiveIcon}
694
</div>
695
<span
696
-
className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`}
697
>
698
{text}
699
</span>
···
732
{active ? ActiveIcon : InactiveIcon}
733
</div>
734
<span
735
-
className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`}
736
>
737
{text}
738
</span>
···
431
</button>
432
)}
433
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
{children}
436
</main>
437
···
448
</div>
449
450
{agent?.did ? (
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
<div className="flex justify-around items-center p-2">
453
<MaterialNavItem
454
small
···
616
</div>
617
</nav>
618
) : (
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
<div className="flex items-center gap-2">
621
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" />
622
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
···
682
<button
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
active
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
}`}
688
onClick={() => {
689
onClickCallbback();
···
693
{active ? ActiveIcon : InactiveIcon}
694
</div>
695
<span
696
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
697
>
698
{text}
699
</span>
···
732
{active ? ActiveIcon : InactiveIcon}
733
</div>
734
<span
735
+
className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`}
736
>
737
{text}
738
</span>
+6
-2
src/routes/index.tsx
+6
-2
src/routes/index.tsx
···
10
agentAtom,
11
authedAtom,
12
feedScrollPositionsAtom,
13
selectedFeedUriAtom,
14
store,
15
} from "~/utils/atoms";
···
350
authed && agent && identity?.pds && feedServiceDid;
351
const isReadyForUnauthedFeed = !authed && selectedFeed;
352
353
return (
354
<div
355
-
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`}
356
>
357
{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">
359
{savedFeeds.map((item: any, idx: number) => {
360
const label = item.value.split("/").pop() || item.value;
361
const isActive = selectedFeed === item.value;
···
10
agentAtom,
11
authedAtom,
12
feedScrollPositionsAtom,
13
+
isAtTopAtom,
14
selectedFeedUriAtom,
15
store,
16
} from "~/utils/atoms";
···
351
authed && agent && identity?.pds && feedServiceDid;
352
const isReadyForUnauthedFeed = !authed && selectedFeed;
353
354
+
355
+
const [isAtTop] = useAtom(isAtTopAtom);
356
+
357
return (
358
<div
359
+
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"} ${!isAtTop && "shadow"}`}
360
>
361
{savedFeeds.length > 0 ? (
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">
363
{savedFeeds.map((item: any, idx: number) => {
364
const label = item.value.split("/").pop() || item.value;
365
const isActive = selectedFeed === item.value;
+19
src/styles/app.css
+19
src/styles/app.css
···
86
}
87
.font-roboto {
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));
108
}
+2
src/utils/atoms.ts
+2
src/utils/atoms.ts