+1
-1
src/components/PostError.tsx
+1
-1
src/components/PostError.tsx
+144
-3
src/components/UniversalPostRenderer.tsx
+144
-3
src/components/UniversalPostRenderer.tsx
···
1167
1167
// const agent = new AtpAgent({
1168
1168
// service: 'https://public.api.bsky.app'
1169
1169
// })
1170
+
type HitSlopButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
1171
+
hitSlop?: number;
1172
+
};
1173
+
1174
+
const HitSlopButtonCustom: React.FC<HitSlopButtonProps> = ({
1175
+
children,
1176
+
hitSlop = 8,
1177
+
style,
1178
+
...rest
1179
+
}) => (
1180
+
<button
1181
+
{...rest}
1182
+
style={{
1183
+
position: "relative",
1184
+
background: "none",
1185
+
border: "none",
1186
+
padding: 0,
1187
+
cursor: "pointer",
1188
+
...style,
1189
+
}}
1190
+
>
1191
+
{/* Invisible hit slop area */}
1192
+
<span
1193
+
style={{
1194
+
position: "absolute",
1195
+
top: -hitSlop,
1196
+
left: -hitSlop,
1197
+
right: -hitSlop,
1198
+
bottom: -hitSlop,
1199
+
}}
1200
+
/>
1201
+
{/* Actual button content stays positioned normally */}
1202
+
<span style={{ position: "relative", zIndex: 1 }}>{children}</span>
1203
+
</button>
1204
+
);
1170
1205
1171
1206
const HitSlopButton = ({
1172
1207
onClick,
1173
1208
children,
1174
1209
style = {},
1175
-
}: {
1210
+
...rest
1211
+
}: React.HTMLAttributes<HTMLSpanElement> & {
1176
1212
onClick?: (e: React.MouseEvent) => void;
1177
1213
children: React.ReactNode;
1178
1214
style?: React.CSSProperties;
···
1201
1237
zIndex: 1,
1202
1238
pointerEvents: "none",
1203
1239
}}
1240
+
{...rest}
1204
1241
>
1205
1242
{children}
1206
1243
</span>
···
1350
1387
//boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
1351
1388
position: "relative",
1352
1389
// dont cursor: "pointer",
1353
-
borderBottomWidth: bottomBorder ? 1 : 0,
1390
+
borderBottomWidth: bottomBorder ? isQuote ? 0 : 1 : 0,
1354
1391
}}
1355
1392
className="border-gray-300 dark:border-gray-600"
1356
1393
>
···
1577
1614
<div
1578
1615
style={{
1579
1616
fontSize: 16,
1580
-
marginBottom: 8,
1617
+
marginBottom: (!post.embed && !expanded) ? 0 : 8,
1581
1618
whiteSpace: "pre-wrap",
1582
1619
textAlign: "left",
1583
1620
overflowWrap: "anywhere",
···
1799
1836
salt: string;
1800
1837
navigate: ({}: any) => void;
1801
1838
}) {
1839
+
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
1802
1840
if (
1803
1841
AppBskyEmbedRecordWithMedia.isView(embed) &&
1804
1842
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
···
2000
2038
if (AppBskyEmbedImages.isView(embed)) {
2001
2039
const { images } = embed;
2002
2040
2041
+
const lightboxImages = images.map((img) => ({
2042
+
src: img.fullsize,
2043
+
alt: img.alt,
2044
+
}));
2045
+
2046
+
2003
2047
if (images.length > 0) {
2004
2048
// const items = embed.images.map(img => ({
2005
2049
// uri: img.fullsize,
···
2030
2074
}}
2031
2075
className="border border-gray-200 dark:border-gray-700 bg-gray-200 dark:bg-gray-900"
2032
2076
>
2077
+
{lightboxIndex !== null && (
2078
+
<Lightbox
2079
+
images={lightboxImages}
2080
+
index={lightboxIndex}
2081
+
onClose={() => setLightboxIndex(null)}
2082
+
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2083
+
/>
2084
+
)}
2033
2085
<img
2034
2086
src={image.fullsize}
2035
2087
alt={image.alt}
···
2038
2090
height: "100%",
2039
2091
objectFit: "contain", // letterbox or scale to fit
2040
2092
}}
2093
+
onClick={(e) => {e.stopPropagation();setLightboxIndex(0)}}
2041
2094
/>
2042
2095
</div>
2043
2096
</div>
···
2058
2111
}}
2059
2112
className="border border-gray-200 dark:border-gray-700"
2060
2113
>
2114
+
{lightboxIndex !== null && (
2115
+
<Lightbox
2116
+
images={lightboxImages}
2117
+
index={lightboxIndex}
2118
+
onClose={() => setLightboxIndex(null)}
2119
+
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2120
+
/>
2121
+
)}
2061
2122
{images.map((img, i) => (
2062
2123
<div
2063
2124
key={i}
···
2072
2133
objectFit: "cover",
2073
2134
borderRadius: i === 0 ? "12px 0 0 12px" : "0 12px 12px 0",
2074
2135
}}
2136
+
onClick={(e) => {e.stopPropagation();setLightboxIndex(i)}}
2075
2137
/>
2076
2138
</div>
2077
2139
))}
···
2095
2157
}}
2096
2158
className="border border-gray-200 dark:border-gray-700"
2097
2159
>
2160
+
{lightboxIndex !== null && (
2161
+
<Lightbox
2162
+
images={lightboxImages}
2163
+
index={lightboxIndex}
2164
+
onClose={() => setLightboxIndex(null)}
2165
+
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2166
+
/>
2167
+
)}
2098
2168
{/* Left: 1:1 */}
2099
2169
<div
2100
2170
style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }}
···
2108
2178
objectFit: "cover",
2109
2179
borderRadius: "12px 0 0 12px",
2110
2180
}}
2181
+
onClick={(e) => {e.stopPropagation();setLightboxIndex(0)}}
2111
2182
/>
2112
2183
</div>
2113
2184
{/* Right: two stacked 2:1 */}
···
2137
2208
objectFit: "cover",
2138
2209
borderRadius: i === 1 ? "0 12px 0 0" : "0 0 12px 0",
2139
2210
}}
2211
+
onClick={(e) => {e.stopPropagation();setLightboxIndex(i+1)}}
2140
2212
/>
2141
2213
</div>
2142
2214
))}
···
2163
2235
}}
2164
2236
className="border border-gray-200 dark:border-gray-700"
2165
2237
>
2238
+
{lightboxIndex !== null && (
2239
+
<Lightbox
2240
+
images={lightboxImages}
2241
+
index={lightboxIndex}
2242
+
onClose={() => setLightboxIndex(null)}
2243
+
onNavigate={(newIndex) => setLightboxIndex(newIndex)}
2244
+
/>
2245
+
)}
2166
2246
{images.map((img, i) => (
2167
2247
<div
2168
2248
key={i}
···
2189
2269
? "0 0 0 12px"
2190
2270
: "0 0 12px 0",
2191
2271
}}
2272
+
onClick={(e) => {e.stopPropagation();setLightboxIndex(i)}}
2192
2273
/>
2193
2274
</div>
2194
2275
))}
···
2248
2329
2249
2330
return <div />;
2250
2331
}
2332
+
2333
+
import { createPortal } from "react-dom";
2334
+
type LightboxProps = {
2335
+
images: { src: string; alt?: string }[];
2336
+
index: number;
2337
+
onClose: () => void;
2338
+
onNavigate?: (newIndex: number) => void;
2339
+
};
2340
+
export function Lightbox({ images, index, onClose, onNavigate }: LightboxProps) {
2341
+
const image = images[index];
2342
+
2343
+
useEffect(() => {
2344
+
function handleKey(e: KeyboardEvent) {
2345
+
if (e.key === "Escape") onClose();
2346
+
if (e.key === "ArrowRight" && onNavigate) onNavigate((index + 1) % images.length);
2347
+
if (e.key === "ArrowLeft" && onNavigate) onNavigate((index - 1 + images.length) % images.length);
2348
+
}
2349
+
window.addEventListener("keydown", handleKey);
2350
+
return () => window.removeEventListener("keydown", handleKey);
2351
+
}, [index, images.length, onClose, onNavigate]);
2352
+
2353
+
return createPortal(
2354
+
<div
2355
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80"
2356
+
onClick={(e)=>{e.stopPropagation();onClose()}}
2357
+
>
2358
+
<img
2359
+
src={image.src}
2360
+
alt={image.alt}
2361
+
className="max-h-[90vh] max-w-[90vw] object-contain rounded-lg shadow-lg"
2362
+
onClick={(e) => e.stopPropagation()}
2363
+
/>
2364
+
2365
+
{images.length > 1 && (
2366
+
<>
2367
+
<button
2368
+
onClick={(e) => {
2369
+
e.stopPropagation();
2370
+
onNavigate?.((index - 1 + images.length) % images.length);
2371
+
}}
2372
+
className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
2373
+
>
2374
+
<svg xmlns="http://www.w3.org/2000/svg" width={28} height={28} viewBox="0 0 24 24"><g fill="none" fillRule="evenodd"><path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path><path fill="currentColor" d="M8.293 12.707a1 1 0 0 1 0-1.414l5.657-5.657a1 1 0 1 1 1.414 1.414L10.414 12l4.95 4.95a1 1 0 0 1-1.414 1.414z"></path></g></svg>
2375
+
</button>
2376
+
<button
2377
+
onClick={(e) => {
2378
+
e.stopPropagation();
2379
+
onNavigate?.((index + 1) % images.length);
2380
+
}}
2381
+
className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl h-8 w-8 rounded-full bg-gray-900 flex items-center justify-center"
2382
+
>
2383
+
<svg xmlns="http://www.w3.org/2000/svg" width={28} height={28} viewBox="0 0 24 24"><g fill="none" fillRule="evenodd"><path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path><path fill="currentColor" d="M15.707 11.293a1 1 0 0 1 0 1.414l-5.657 5.657a1 1 0 1 1-1.414-1.414l4.95-4.95l-4.95-4.95a1 1 0 0 1 1.414-1.414z"></path></g></svg>
2384
+
</button>
2385
+
</>
2386
+
)}
2387
+
</div>,
2388
+
document.body
2389
+
);
2390
+
}
2391
+
2251
2392
function getDomain(url: string) {
2252
2393
try {
2253
2394
const { hostname } = new URL(url);
+1
-1
src/components/UserError.tsx
+1
-1
src/components/UserError.tsx
+2
-2
src/routes/__root.tsx
+2
-2
src/routes/__root.tsx
···
329
329
/>
330
330
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
331
331
Red Dwarf{" "}
332
-
<span className="text-gray-500 dark:text-gray-400 text-sm">
332
+
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
333
333
lite
334
-
</span>
334
+
</span> */}
335
335
</span>
336
336
</div>
337
337
<div className="flex items-center gap-2">