an independent Bluesky client using Constellation, PDS Queries, and other services
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

conven color

+1331
+53
conven-docs/color.md
··· 1 + # Conven Color Defs 2 + 3 + [See the CSS definition here](../src/styles/app.css) 4 + tailwind atoms are defined in app.css 5 + 6 + ```css 7 + /* CONVEN COLORS */ 8 + :root { 9 + --hue-base: var(--system-hue-base, 260); 10 + --hue-accent: var(--system-hue-accent, 290); 11 + --hue-contrast: var(--system-hue-contrast, 80); 12 + --hue-muted: var(--system-hue-muted, 220); 13 + } 14 + ``` 15 + 16 + These hues are user-configurable (set from community definition files). All colors in the UI derive from them, allowing full theme customization without breaking the palette structure. 17 + 18 + this will produce standard 4 tailwind palettes to be used in all conven components. they are registered in tailwind as base, accent, contrast, and muted. 19 + 20 + !!! please avoid using any other (including the preincluded tailwind) palettes. they are not responsive to the dynamic color palette system and will look out of place. 21 + 22 + for all usage please consider dark mode. 23 + 24 + example usage: 25 + 26 + ```html 27 + <div className="bg-base-100 dark:bg-base-900 text-base-900 dark:text-base-100 p-6 rounded-lg space-y-4"> 28 + <h1 className="text-accent-600 dark:text-accent-400 text-lg font-semibold"> 29 + Dashboard 30 + </h1> 31 + 32 + <p className="text-base-600 dark:text-base-300"> 33 + This is a sample description showing how text adapts across light and dark modes. 34 + </p> 35 + 36 + <div className="bg-contrast-100 dark:bg-contrast-800 p-4 rounded-md"> 37 + <span className="text-muted-700 dark:text-muted-300 text-sm"> 38 + Secondary content 39 + </span> 40 + </div> 41 + 42 + <button className="bg-accent-500 hover:bg-accent-600 dark:bg-accent-500 dark:hover:bg-accent-400 text-base-50 dark:text-base-950 px-4 py-2 rounded-md"> 43 + Primary Action 44 + </button> 45 + </div> 46 + ``` 47 + 48 + ### Palette roles 49 + 50 + - `base` — neutral UI surfaces, text, borders 51 + - `accent` — primary actions, links, interactive elements 52 + - `contrast` — separating sections, secondary surfaces 53 + - `muted` — subtle UI (badges, tags, low-emphasis elements)
+21
src/routeTree.gen.ts
··· 11 11 import { Route as rootRouteImport } from './routes/__root' 12 12 import { Route as SettingsRouteImport } from './routes/settings' 13 13 import { Route as SearchRouteImport } from './routes/search' 14 + import { Route as RootCopyRouteImport } from './routes/root-copy' 14 15 import { Route as NotificationsRouteImport } from './routes/notifications' 15 16 import { Route as ModerationRouteImport } from './routes/moderation' 16 17 import { Route as FeedsRouteImport } from './routes/feeds' ··· 39 40 const SearchRoute = SearchRouteImport.update({ 40 41 id: '/search', 41 42 path: '/search', 43 + getParentRoute: () => rootRouteImport, 44 + } as any) 45 + const RootCopyRoute = RootCopyRouteImport.update({ 46 + id: '/root-copy', 47 + path: '/root-copy', 42 48 getParentRoute: () => rootRouteImport, 43 49 } as any) 44 50 const NotificationsRoute = NotificationsRouteImport.update({ ··· 148 154 '/feeds': typeof FeedsRoute 149 155 '/moderation': typeof ModerationRoute 150 156 '/notifications': typeof NotificationsRoute 157 + '/root-copy': typeof RootCopyRoute 151 158 '/search': typeof SearchRoute 152 159 '/settings': typeof SettingsRoute 153 160 '/callback': typeof CallbackIndexRoute ··· 169 176 '/feeds': typeof FeedsRoute 170 177 '/moderation': typeof ModerationRoute 171 178 '/notifications': typeof NotificationsRoute 179 + '/root-copy': typeof RootCopyRoute 172 180 '/search': typeof SearchRoute 173 181 '/settings': typeof SettingsRoute 174 182 '/callback': typeof CallbackIndexRoute ··· 192 200 '/feeds': typeof FeedsRoute 193 201 '/moderation': typeof ModerationRoute 194 202 '/notifications': typeof NotificationsRoute 203 + '/root-copy': typeof RootCopyRoute 195 204 '/search': typeof SearchRoute 196 205 '/settings': typeof SettingsRoute 197 206 '/_pathlessLayout/_nested-layout': typeof PathlessLayoutNestedLayoutRouteWithChildren ··· 216 225 | '/feeds' 217 226 | '/moderation' 218 227 | '/notifications' 228 + | '/root-copy' 219 229 | '/search' 220 230 | '/settings' 221 231 | '/callback' ··· 237 247 | '/feeds' 238 248 | '/moderation' 239 249 | '/notifications' 250 + | '/root-copy' 240 251 | '/search' 241 252 | '/settings' 242 253 | '/callback' ··· 259 270 | '/feeds' 260 271 | '/moderation' 261 272 | '/notifications' 273 + | '/root-copy' 262 274 | '/search' 263 275 | '/settings' 264 276 | '/_pathlessLayout/_nested-layout' ··· 283 295 FeedsRoute: typeof FeedsRoute 284 296 ModerationRoute: typeof ModerationRoute 285 297 NotificationsRoute: typeof NotificationsRoute 298 + RootCopyRoute: typeof RootCopyRoute 286 299 SearchRoute: typeof SearchRoute 287 300 SettingsRoute: typeof SettingsRoute 288 301 CallbackIndexRoute: typeof CallbackIndexRoute ··· 307 320 path: '/search' 308 321 fullPath: '/search' 309 322 preLoaderRoute: typeof SearchRouteImport 323 + parentRoute: typeof rootRouteImport 324 + } 325 + '/root-copy': { 326 + id: '/root-copy' 327 + path: '/root-copy' 328 + fullPath: '/root-copy' 329 + preLoaderRoute: typeof RootCopyRouteImport 310 330 parentRoute: typeof rootRouteImport 311 331 } 312 332 '/notifications': { ··· 499 519 FeedsRoute: FeedsRoute, 500 520 ModerationRoute: ModerationRoute, 501 521 NotificationsRoute: NotificationsRoute, 522 + RootCopyRoute: RootCopyRoute, 502 523 SearchRoute: SearchRoute, 503 524 SettingsRoute: SettingsRoute, 504 525 CallbackIndexRoute: CallbackIndexRoute,
+1176
src/routes/root-copy.tsx
··· 1 + /// <reference types="vite/client" /> 2 + 3 + // dont forget to run this 4 + // npx @tanstack/router-cli generate 5 + import type { $Typed } from "@atproto/api"; 6 + import type { SavedFeedsPrefV2 } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 7 + import { 8 + createFileRoute, Link, // Link, 9 + // Outlet, 10 + Scripts, useLocation, useNavigate} from "@tanstack/react-router"; 11 + import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 12 + import { useAtom } from "jotai"; 13 + import * as React from "react"; 14 + import { toast as sonnerToast } from "sonner"; 15 + import { Toaster } from "sonner"; 16 + import { KeepAliveOutlet, KeepAliveProvider } from "tanstack-router-keepalive"; 17 + 18 + import { HOST_ADMIN, HOST_DESCRIPTION, HOST_HERO, HOST_LOGIN_BLURB, HOST_MAIN_TITLE, HOST_SIGNUP_PDS, HOST_SUB_TITLE, HOST_UNAUTHED_DEFAULT_FEEDS } from "~/../policy"; 19 + import { Composer } from "~/components/Composer"; 20 + import { Import } from "~/components/Import"; 21 + //import Login from "~/components/Login"; 22 + import Logo from "~/components/LogoSvg"; 23 + //import { ModerationBatcher } from "~/components/ModerationBatcher"; 24 + //import { ModerationInitializer } from "~/components/ModerationInitializer"; 25 + //import { AutoLabelProvider } from "~/providers/AutoLabelProvider"; 26 + import { LikeMutationQueueProvider } from "~/providers/LikeMutationQueueProvider"; 27 + //import { PollMutationQueueProvider } from "~/providers/PollMutationQueueProvider"; 28 + import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 29 + import { FeedTabOnTop } from "~/routes/index"; 30 + import { composerAtom, hueAtom, imgCDNAtom, quickAuthAtom, useAtomCssVar } from "~/utils/atoms"; 31 + import { useQueryIdentity, useQueryPreferences, useQueryProfile } from "~/utils/useQuery"; 32 + 33 + export const Route = createFileRoute("/root-copy")({ 34 + component: RootComponent, 35 + }); 36 + 37 + function RootComponent() { 38 + return ( 39 + <UnifiedAuthProvider> 40 + {/* <AutoLabelProvider> */} 41 + <LikeMutationQueueProvider> 42 + {/* <PollMutationQueueProvider> 43 + <ModerationInitializer /> 44 + <ModerationBatcher /> */} 45 + <RootDocument> 46 + <KeepAliveProvider> 47 + <AppToaster /> 48 + <KeepAliveOutlet /> 49 + </KeepAliveProvider> 50 + </RootDocument> 51 + {/* </PollMutationQueueProvider> */} 52 + </LikeMutationQueueProvider> 53 + {/* </AutoLabelProvider> */} 54 + </UnifiedAuthProvider> 55 + ); 56 + } 57 + 58 + export function AppToaster() { 59 + return ( 60 + <Toaster 61 + position="bottom-center" 62 + toastOptions={{ 63 + duration: 4000, 64 + }} 65 + /> 66 + ); 67 + } 68 + 69 + export function renderSnack({ 70 + title, 71 + description, 72 + button, 73 + }: Omit<ToastProps, "id">) { 74 + return sonnerToast.custom((id) => ( 75 + <Snack 76 + id={id} 77 + title={title} 78 + description={description} 79 + button={ 80 + button?.label 81 + ? { 82 + label: button?.label, 83 + onClick: () => { 84 + button?.onClick?.(); 85 + }, 86 + } 87 + : undefined 88 + } 89 + /> 90 + )); 91 + } 92 + 93 + function Snack(props: ToastProps) { 94 + const { title, description, button, id } = props; 95 + 96 + return ( 97 + <div 98 + role="status" 99 + aria-live="polite" 100 + className=" 101 + w-full md:max-w-[520px] 102 + flex items-center justify-between 103 + rounded-md 104 + px-4 py-3 105 + shadow-sm 106 + dark:bg-gray-300 dark:text-gray-900 107 + bg-gray-700 text-gray-100 108 + ring-1 dark:ring-gray-200 ring-gray-800 109 + " 110 + > 111 + <div className="flex-1 min-w-0"> 112 + <p className="text-sm font-medium truncate">{title}</p> 113 + {description ? ( 114 + <p className="mt-1 text-sm dark:text-gray-600 text-gray-300 truncate"> 115 + {description} 116 + </p> 117 + ) : null} 118 + </div> 119 + 120 + {button ? ( 121 + <div className="ml-4 flex-shrink-0"> 122 + <button 123 + className=" 124 + text-sm font-medium 125 + px-3 py-1 rounded-md 126 + bg-gray-200 text-gray-900 127 + hover:bg-gray-300 128 + dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 129 + focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 dark:focus:ring-gray-700 130 + " 131 + onClick={() => { 132 + button.onClick(); 133 + sonnerToast.dismiss(id); 134 + }} 135 + > 136 + {button.label} 137 + </button> 138 + </div> 139 + ) : null} 140 + <button 141 + className=" ml-4" 142 + onClick={() => { 143 + sonnerToast.dismiss(id); 144 + }} 145 + > 146 + <IconMdiClose /> 147 + </button> 148 + </div> 149 + ); 150 + } 151 + 152 + /* Types */ 153 + interface ToastProps { 154 + id: string | number; 155 + title: string; 156 + description?: string; 157 + button?: { 158 + label: string; 159 + onClick: () => void; 160 + }; 161 + } 162 + 163 + function RootDocument({ children }: { children: React.ReactNode }) { 164 + useAtomCssVar(hueAtom, "--tw-gray-hue"); 165 + const location = useLocation(); 166 + const navigate = useNavigate(); 167 + const { agent } = useAuth(); 168 + const isNotifications = location.pathname.startsWith("/notifications"); 169 + const authed = !!agent?.did; 170 + const isProfile = 171 + agent && 172 + (location.pathname === `/profile/${agent?.did}` || 173 + location.pathname === `/profile/${encodeURIComponent(agent?.did ?? "")}`); 174 + const isSettings = location.pathname.startsWith("/settings"); 175 + const isSearch = location.pathname.startsWith("/search"); 176 + const isFeeds = location.pathname.startsWith("/feeds"); 177 + const isModeration = location.pathname.startsWith("/moderation"); 178 + const isAbout = location.pathname.startsWith("/about"); 179 + 180 + const locationEnum: 181 + | "feeds" 182 + | "search" 183 + | "settings" 184 + | "notifications" 185 + | "profile" 186 + | "moderation" 187 + | "about" 188 + | "home" = isFeeds 189 + ? "feeds" 190 + : isSearch 191 + ? "search" 192 + : isSettings 193 + ? "settings" 194 + : isNotifications 195 + ? "notifications" 196 + : isProfile 197 + ? "profile" 198 + : isModeration 199 + ? "moderation" 200 + : isAbout ? 201 + "about" 202 + : "home"; 203 + 204 + const [, setComposerPost] = useAtom(composerAtom); 205 + 206 + return ( 207 + <> 208 + <Composer /> 209 + 210 + <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 211 + <nav className="hidden lg:flex h-screen w-[250px] xl:ml-[50px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start"> 212 + <div className="flex items-center gap-3 mb-4 pl-3"> 213 + <Logo 214 + className="h-8 w-8" 215 + style={{ 216 + color: 217 + "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))", 218 + }} 219 + /> 220 + <span className="font-extrabold text-2xl text-gray-900 dark:text-gray-100"> 221 + {HOST_MAIN_TITLE} 222 + {HOST_SUB_TITLE && (<span className="text-gray-500 dark:text-gray-400 text-sm"> 223 + {HOST_SUB_TITLE} 224 + </span>) } 225 + </span> 226 + </div> 227 + <MaterialNavItem 228 + InactiveIcon={ 229 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 230 + } 231 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 232 + active={locationEnum === "home"} 233 + onClickCallbback={() => 234 + navigate({ 235 + to: "/", 236 + //params: { did: agent.assertDid }, 237 + }) 238 + } 239 + text="Home" 240 + /> 241 + 242 + <MaterialNavItem 243 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 244 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 245 + active={locationEnum === "search"} 246 + onClickCallbback={() => 247 + navigate({ 248 + to: "/search", 249 + //params: { did: agent.assertDid }, 250 + }) 251 + } 252 + text="Explore" 253 + /> 254 + <MaterialNavItem 255 + visible={!!agent?.did} 256 + InactiveIcon={ 257 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 258 + } 259 + ActiveIcon={ 260 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 261 + } 262 + active={locationEnum === "notifications"} 263 + onClickCallbback={() => 264 + navigate({ 265 + to: "/notifications", 266 + //params: { did: agent.assertDid }, 267 + }) 268 + } 269 + text="Notifications" 270 + /> 271 + <MaterialNavItem 272 + visible={!!agent?.did} 273 + InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 274 + ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 275 + active={locationEnum === "feeds"} 276 + onClickCallbback={() => 277 + navigate({ 278 + to: "/feeds", 279 + //params: { did: agent.assertDid }, 280 + }) 281 + } 282 + text="Feeds" 283 + /> 284 + <MaterialNavItem 285 + visible={!!agent?.did} 286 + InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 287 + ActiveIcon={<IconMdiShield className="w-6 h-6" />} 288 + active={locationEnum === "moderation"} 289 + onClickCallbback={() => 290 + navigate({ 291 + to: "/moderation", 292 + //params: { did: agent.assertDid }, 293 + }) 294 + } 295 + text="Moderation" 296 + /> 297 + <MaterialNavItem 298 + visible={!!agent?.did} 299 + InactiveIcon={ 300 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 301 + } 302 + ActiveIcon={ 303 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 304 + } 305 + active={locationEnum === "profile"} 306 + onClickCallbback={() => { 307 + if (authed && agent && agent.assertDid) { 308 + //window.location.href = `/profile/${agent.assertDid}`; 309 + navigate({ 310 + to: "/profile/$did", 311 + params: { did: agent.assertDid }, 312 + }); 313 + } 314 + }} 315 + text="Profile" 316 + /> 317 + <MaterialNavItem 318 + InactiveIcon={ 319 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 320 + } 321 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 322 + active={locationEnum === "settings"} 323 + onClickCallbback={() => 324 + navigate({ 325 + to: "/settings", 326 + //params: { did: agent.assertDid }, 327 + }) 328 + } 329 + text="Settings" 330 + /> 331 + {!agent?.did && ( 332 + <MaterialNavItem 333 + InactiveIcon={ 334 + <IconMaterialSymbolsInfoOutline className="w-6 h-6" /> 335 + } 336 + ActiveIcon={<IconMaterialSymbolsInfo className="w-6 h-6" />} 337 + active={locationEnum === "about"} 338 + onClickCallbback={() => 339 + navigate({ 340 + to: "/about", 341 + //params: { did: agent.assertDid }, 342 + }) 343 + } 344 + text="About" 345 + /> 346 + )} 347 + {agent?.did && ( 348 + <div className="flex flex-row items-center justify-center mt-3"> 349 + <MaterialPillButton 350 + InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 351 + ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 352 + //active={true} 353 + onClickCallbback={() => setComposerPost({ kind: "root" })} 354 + text="Post" 355 + /> 356 + </div> 357 + )} 358 + {!agent?.did && ( 359 + <> 360 + <div className="mt-4 mb-2 w-full h-[1px] bg-gray-200 dark:bg-gray-800" /> 361 + {/* <Login /> */} 362 + <LoginRedirect /> 363 + </> 364 + )} 365 + {/* <Link 366 + to="/" 367 + className={ 368 + `py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` + 369 + (isHome ? "font-bold" : "") 370 + } 371 + > 372 + {!isHome ? ( 373 + <IconMaterialSymbolsHomeOutline width={28} height={28} /> 374 + ) : ( 375 + <IconMaterialSymbolsHome width={28} height={28} /> 376 + )} 377 + <span>Home</span> 378 + </Link> 379 + <Link 380 + to="/notifications" 381 + className={ 382 + `py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` + 383 + (isNotifications ? "font-bold" : "") 384 + } 385 + > 386 + {!isNotifications ? ( 387 + <IconMaterialSymbolsNotificationsOutline width={28} height={28} /> 388 + ) : ( 389 + <IconMaterialSymbolsNotifications width={28} height={28} /> 390 + )} 391 + <span>Notifications</span> 392 + </Link> 393 + <Link 394 + to="/feeds" 395 + className={`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ${ 396 + location.pathname.startsWith("/feeds") ? "font-bold" : "" 397 + }`} 398 + > 399 + {location.pathname.startsWith("/feeds") ? ( 400 + <IconMaterialSymbolsTag width={28} height={28} /> 401 + ) : ( 402 + <IconMaterialSymbolsTag width={28} height={28} /> 403 + )} 404 + <span>Feeds</span> 405 + </Link> 406 + 407 + <Link 408 + to="/search" 409 + className={`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ${ 410 + location.pathname.startsWith("/search") ? "font-bold" : "" 411 + }`} 412 + > 413 + {location.pathname.startsWith("/search") ? ( 414 + <IconMaterialSymbolsSearch width={28} height={28} /> 415 + ) : ( 416 + <IconMaterialSymbolsSearch width={28} height={28} /> 417 + )} 418 + <span>Search</span> 419 + </Link> 420 + <button 421 + className={`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 w-full text-left ${ 422 + isProfile ? "bg-gray-100 dark:bg-gray-900 font-bold" : "" 423 + }`} 424 + onClick={() => { 425 + if (authed && agent && agent.assertDid) { 426 + //window.location.href = `/profile/${agent.assertDid}`; 427 + navigate({ 428 + to: "/profile/$did", 429 + params: { did: agent.assertDid }, 430 + }); 431 + } 432 + }} 433 + type="button" 434 + > 435 + {!isProfile ? ( 436 + <IconMaterialSymbolsAccountCircleOutline width={28} height={28} /> 437 + ) : ( 438 + <IconMaterialSymbolsAccountCircle width={28} height={28} /> 439 + )} 440 + <span>Profile</span> 441 + </button> 442 + <Link 443 + to="/settings" 444 + className={`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ${ 445 + location.pathname.startsWith("/settings") ? "font-bold" : "" 446 + }`} 447 + > 448 + {!location.pathname.startsWith("/settings") ? ( 449 + <IconMaterialSymbolsSettingsOutline width={28} height={28} /> 450 + ) : ( 451 + <IconMaterialSymbolsSettings width={28} height={28} /> 452 + )} 453 + <span>Settings</span> 454 + </Link> */} 455 + {/* <button 456 + className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow" 457 + onClick={() => setPostOpen(true)} 458 + type="button" 459 + > 460 + <IconMdiPencilOutline 461 + width={24} 462 + height={24} 463 + className="text-gray-600 dark:text-gray-400" 464 + /> 465 + <span>Post</span> 466 + </button> */} 467 + <div className="flex-1"></div> 468 + {!!agent?.did && ( 469 + <div className="flex flex-row items-center lg:mb-1"> 470 + <div className="flex p-2 h-12 flex-1 rounded-full hover:dark:bg-gray-800 hover:bg-gray-200"> 471 + <ProfileSmall did={agent.did} /> 472 + </div> 473 + <Link 474 + to="/settings" 475 + className="flex p-3 h-12 w-12 rounded-full hover:dark:bg-gray-800 hover:bg-gray-200 items-center justify-center" 476 + > 477 + <IconMaterialSymbolsMoreVert /> 478 + </Link> 479 + </div> 480 + )} 481 + {/* 482 + <a 483 + href="https://tangled.sh/@whey.party/red-dwarf" 484 + target="_blank" 485 + rel="noopener noreferrer" 486 + className="mt-1 text-xs text-gray-400 dark:text-gray-500 text-center hover:underline" 487 + > 488 + git repo 489 + </a> 490 + <a 491 + href="https://whey.party/" 492 + target="_blank" 493 + rel="noopener noreferrer" 494 + className="mt-1 text-xs text-gray-400 dark:text-gray-500 text-center hover:underline" 495 + > 496 + made by @whey.party 497 + </a> 498 + <div className="mt-2 text-xs text-gray-400 dark:text-gray-500 text-center"> 499 + powered by{" "} 500 + <a 501 + href="https://microcosm.blue" 502 + target="_blank" 503 + rel="noopener noreferrer" 504 + className="underline hover:text-blue-500" 505 + > 506 + microcosm.blue 507 + </a> 508 + </div> 509 + */} 510 + </nav> 511 + 512 + <nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start"> 513 + <div className="flex items-center gap-3 mb-4"> 514 + <Logo 515 + className="h-8 w-8" 516 + style={{ 517 + color: 518 + "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))", 519 + }} 520 + /> 521 + </div> 522 + <MaterialNavItem 523 + small 524 + InactiveIcon={ 525 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 526 + } 527 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 528 + active={locationEnum === "home"} 529 + onClickCallbback={() => 530 + navigate({ 531 + to: "/", 532 + //params: { did: agent.assertDid }, 533 + }) 534 + } 535 + text="Home" 536 + /> 537 + 538 + <MaterialNavItem 539 + small 540 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 541 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 542 + active={locationEnum === "search"} 543 + onClickCallbback={() => 544 + navigate({ 545 + to: "/search", 546 + //params: { did: agent.assertDid }, 547 + }) 548 + } 549 + text="Explore" 550 + /> 551 + <MaterialNavItem 552 + small 553 + visible={!!agent?.did} 554 + InactiveIcon={ 555 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 556 + } 557 + ActiveIcon={ 558 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 559 + } 560 + active={locationEnum === "notifications"} 561 + onClickCallbback={() => 562 + navigate({ 563 + to: "/notifications", 564 + //params: { did: agent.assertDid }, 565 + }) 566 + } 567 + text="Notifications" 568 + /> 569 + <MaterialNavItem 570 + small 571 + visible={!!agent?.did} 572 + InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 573 + ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 574 + active={locationEnum === "feeds"} 575 + onClickCallbback={() => 576 + navigate({ 577 + to: "/feeds", 578 + //params: { did: agent.assertDid }, 579 + }) 580 + } 581 + text="Feeds" 582 + /> 583 + <MaterialNavItem 584 + small 585 + visible={!!agent?.did} 586 + InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 587 + ActiveIcon={<IconMdiShield className="w-6 h-6" />} 588 + active={locationEnum === "moderation"} 589 + onClickCallbback={() => 590 + navigate({ 591 + to: "/moderation", 592 + //params: { did: agent.assertDid }, 593 + }) 594 + } 595 + text="Moderation" 596 + /> 597 + <MaterialNavItem 598 + small 599 + visible={!!agent?.did} 600 + InactiveIcon={ 601 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 602 + } 603 + ActiveIcon={ 604 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 605 + } 606 + active={locationEnum === "profile"} 607 + onClickCallbback={() => { 608 + if (authed && agent && agent.assertDid) { 609 + //window.location.href = `/profile/${agent.assertDid}`; 610 + navigate({ 611 + to: "/profile/$did", 612 + params: { did: agent.assertDid }, 613 + }); 614 + } 615 + }} 616 + text="Profile" 617 + /> 618 + <MaterialNavItem 619 + small 620 + InactiveIcon={ 621 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 622 + } 623 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 624 + active={locationEnum === "settings"} 625 + onClickCallbback={() => 626 + navigate({ 627 + to: "/settings", 628 + //params: { did: agent.assertDid }, 629 + }) 630 + } 631 + text="Settings" 632 + /> 633 + 634 + {!agent?.did && ( 635 + <MaterialNavItem 636 + small 637 + InactiveIcon={ 638 + <IconMaterialSymbolsInfoOutline className="w-6 h-6" /> 639 + } 640 + ActiveIcon={<IconMaterialSymbolsInfo className="w-6 h-6" />} 641 + active={locationEnum === "about"} 642 + onClickCallbback={() => 643 + navigate({ 644 + to: "/about", 645 + //params: { did: agent.assertDid }, 646 + }) 647 + } 648 + text="About" 649 + /> 650 + )} 651 + {!!agent?.did && ( 652 + <div className="flex flex-row items-center justify-center mt-3"> 653 + <MaterialPillButton 654 + small 655 + InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 656 + ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 657 + //active={true} 658 + onClickCallbback={() => setComposerPost({ kind: "root" })} 659 + text="Post" 660 + /> 661 + </div> 662 + )} 663 + </nav> 664 + 665 + {agent?.did && ( 666 + <button 667 + className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all" 668 + style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 669 + onClick={() => setComposerPost({ kind: "root" })} 670 + type="button" 671 + aria-label="Create Post" 672 + > 673 + <IconMdiPencilOutline 674 + width={24} 675 + height={24} 676 + className="text-gray-600 dark:text-gray-400" 677 + /> 678 + </button> 679 + )} 680 + 681 + <main className="w-full max-w-[600px] sm:border-x border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 pb-16 lg:pb-0 overflow-x-clip"> 682 + {children} 683 + </main> 684 + 685 + <aside className="hidden lg:flex h-screen xl:w-[300px] w-[250px] sticky top-0 self-start flex-col"> 686 + <div className="px-4 pt-4 gap-4 flex flex-col"> 687 + <Import /> 688 + </div> 689 + <div className="px-4 pt-4 gap-4 flex flex-col max-h-[calc(100dvh - 80px)] overflow-y-auto"> 690 + {( 691 + (!agent?.did && HOST_UNAUTHED_DEFAULT_FEEDS.length > 0) 692 + || (!!agent?.did) 693 + ) && ( 694 + <FeedListDesktopSidebar /> 695 + )} 696 + {!agent?.did && ( 697 + <> 698 + <span className=" text-gray-500 dark:text-gray-400 text-sm leading-tight"><span className=" font-bold">{window.location.host}</span> is a hosted Red Dwarf instance that you can use to participate in the Bluesky social network.</span> 699 + <img className="rounded-sm" src={HOST_HERO} /> 700 + <span className=" text-gray-500 dark:text-gray-400 text-sm">{HOST_DESCRIPTION}</span> 701 + <div className="flex flex-col gap-1 "> 702 + <span className="text-gray-500 dark:text-gray-400 text-sm font-bold">ADMINISTERED BY:</span> 703 + <ProfileSmall did={HOST_ADMIN} /> 704 + </div> 705 + </> 706 + )} 707 + </div> 708 + <div className="flex-1"></div> 709 + {/* todo */} 710 + <span>TODO: add red dwarf the software policy along with instance policy here</span> 711 + </aside> 712 + </div> 713 + 714 + {agent?.did ? ( 715 + <nav className="sm:hidden fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-0 border-t-1 dark:border-t-0 shadow border-gray-200 dark:border-gray-700 z-40"> 716 + <div className="flex justify-around items-center p-2"> 717 + <MaterialNavItem 718 + small 719 + InactiveIcon={ 720 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 721 + } 722 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 723 + active={locationEnum === "home"} 724 + onClickCallbback={() => 725 + navigate({ 726 + to: "/", 727 + //params: { did: agent.assertDid }, 728 + }) 729 + } 730 + text="Home" 731 + /> 732 + {/* <Link 733 + to="/" 734 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 735 + isHome 736 + ? "text-gray-900 dark:text-gray-100" 737 + : "text-gray-600 dark:text-gray-400" 738 + }`} 739 + > 740 + {!isHome ? ( 741 + <IconMaterialSymbolsHomeOutline width={24} height={24} /> 742 + ) : ( 743 + <IconMaterialSymbolsHome width={24} height={24} /> 744 + )} 745 + <span className="text-xs mt-1">Home</span> 746 + </Link> */} 747 + <MaterialNavItem 748 + small 749 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 750 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 751 + active={locationEnum === "search"} 752 + onClickCallbback={() => 753 + navigate({ 754 + to: "/search", 755 + //params: { did: agent.assertDid }, 756 + }) 757 + } 758 + text="Explore" 759 + /> 760 + {/* <Link 761 + to="/search" 762 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 763 + location.pathname.startsWith("/search") 764 + ? "text-gray-900 dark:text-gray-100" 765 + : "text-gray-600 dark:text-gray-400" 766 + }`} 767 + > 768 + {!location.pathname.startsWith("/search") ? ( 769 + <IconMaterialSymbolsSearch width={24} height={24} /> 770 + ) : ( 771 + <IconMaterialSymbolsSearch width={24} height={24} /> 772 + )} 773 + <span className="text-xs mt-1">Search</span> 774 + </Link> */} 775 + <MaterialNavItem 776 + small 777 + InactiveIcon={ 778 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 779 + } 780 + ActiveIcon={ 781 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 782 + } 783 + active={locationEnum === "notifications"} 784 + onClickCallbback={() => 785 + navigate({ 786 + to: "/notifications", 787 + //params: { did: agent.assertDid }, 788 + }) 789 + } 790 + text="Notifications" 791 + /> 792 + {/* <Link 793 + to="/notifications" 794 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 795 + isNotifications 796 + ? "text-gray-900 dark:text-gray-100" 797 + : "text-gray-600 dark:text-gray-400" 798 + }`} 799 + > 800 + {!isNotifications ? ( 801 + <IconMaterialSymbolsNotificationsOutline 802 + width={24} 803 + height={24} 804 + /> 805 + ) : ( 806 + <IconMaterialSymbolsNotifications width={24} height={24} /> 807 + )} 808 + <span className="text-xs mt-1">Notifications</span> 809 + </Link> */} 810 + <MaterialNavItem 811 + small 812 + InactiveIcon={ 813 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 814 + } 815 + ActiveIcon={ 816 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 817 + } 818 + active={locationEnum === "profile"} 819 + onClickCallbback={() => { 820 + if (authed && agent && agent.assertDid) { 821 + //window.location.href = `/profile/${agent.assertDid}`; 822 + navigate({ 823 + to: "/profile/$did", 824 + params: { did: agent.assertDid }, 825 + }); 826 + } 827 + }} 828 + text="Profile" 829 + /> 830 + {/* <button 831 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 832 + isProfile 833 + ? "text-gray-900 dark:text-gray-100" 834 + : "text-gray-600 dark:text-gray-400" 835 + }`} 836 + onClick={() => { 837 + if (authed && agent && agent.assertDid) { 838 + //window.location.href = `/profile/${agent.assertDid}`; 839 + navigate({ 840 + to: "/profile/$did", 841 + params: { did: agent.assertDid }, 842 + }); 843 + } 844 + }} 845 + type="button" 846 + > 847 + <IconMaterialSymbolsAccountCircleOutline width={24} height={24} /> 848 + <span className="text-xs mt-1">Profile</span> 849 + </button> */} 850 + <MaterialNavItem 851 + small 852 + InactiveIcon={ 853 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 854 + } 855 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 856 + active={ 857 + locationEnum === "settings" || 858 + locationEnum === "feeds" || 859 + locationEnum === "moderation" 860 + } 861 + onClickCallbback={() => 862 + navigate({ 863 + to: "/settings", 864 + //params: { did: agent.assertDid }, 865 + }) 866 + } 867 + text="Settings" 868 + /> 869 + {/* <Link 870 + to="/settings" 871 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 872 + location.pathname.startsWith("/settings") 873 + ? "text-gray-900 dark:text-gray-100" 874 + : "text-gray-600 dark:text-gray-400" 875 + }`} 876 + > 877 + {!location.pathname.startsWith("/settings") ? ( 878 + <IconMaterialSymbolsSettingsOutline width={24} height={24} /> 879 + ) : ( 880 + <IconMaterialSymbolsSettings width={24} height={24} /> 881 + )} 882 + <span className="text-xs mt-1">Settings</span> 883 + </Link> */} 884 + </div> 885 + </nav> 886 + ) : ( 887 + <div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 border-t-1 dark:border-t-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10"> 888 + <div className="flex items-center gap-2"> 889 + <Logo 890 + className="h-6 w-6" 891 + style={{ 892 + color: 893 + "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))", 894 + }} 895 + /> 896 + <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 897 + {HOST_MAIN_TITLE} 898 + {HOST_SUB_TITLE && (<span className="text-gray-500 dark:text-gray-400 text-sm"> 899 + {HOST_SUB_TITLE} 900 + </span>) } 901 + </span> 902 + </div> 903 + <div className="flex items-center gap-2"> 904 + {/* <Login compact={true} popup={true} /> */} 905 + <Link 906 + to="/settings" 907 + className="rounded-full bg-gray-600 text-gray-100 dark:bg-gray-400 dark:text-gray-900 px-4 py-2 text-sm font-medium text-center" 908 + > 909 + Log in 910 + </Link> 911 + </div> 912 + </div> 913 + )} 914 + 915 + <TanStackRouterDevtools position="bottom-left" /> 916 + <Scripts /> 917 + </> 918 + ); 919 + } 920 + 921 + export function MaterialNavItem({ 922 + visible = true, 923 + InactiveIcon, 924 + ActiveIcon, 925 + text, 926 + active, 927 + onClickCallbback, 928 + small, 929 + }: { 930 + visible?: boolean; 931 + InactiveIcon: React.ReactElement; 932 + ActiveIcon: React.ReactElement; 933 + text: string; 934 + active: boolean; 935 + onClickCallbback: () => void; 936 + small?: boolean | string; 937 + }) { 938 + if (!visible) return null 939 + if (small) 940 + return ( 941 + <button 942 + className={`flex flex-col items-center rounded-lg transition-colors ${small} gap-1 ${active 943 + ? "text-gray-900 dark:text-gray-100" 944 + : "text-gray-600 dark:text-gray-400" 945 + }`} 946 + onClick={() => { 947 + onClickCallbback(); 948 + }} 949 + > 950 + <div 951 + className={`px-4 py-1 rounded-full flex items-center justify-center ${active ? " bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 hover:dark:bg-gray-700" : "hover:bg-gray-50 hover:dark:bg-gray-900"}`} 952 + > 953 + {active ? ActiveIcon : InactiveIcon} 954 + </div> 955 + <span 956 + className={`text-[12.8px] text-roboto ${active ? "font-medium" : ""}`} 957 + > 958 + {text} 959 + </span> 960 + </button> 961 + ); 962 + 963 + return ( 964 + <button 965 + 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 ${active 966 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-800 bg-gray-200 hover:dark:bg-gray-700" 967 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-900" 968 + }`} 969 + onClick={() => { 970 + onClickCallbback(); 971 + }} 972 + > 973 + <div className={`mr-4 ${active ? " " : " "}`}> 974 + {active ? ActiveIcon : InactiveIcon} 975 + </div> 976 + <span 977 + className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 978 + > 979 + {text} 980 + </span> 981 + </button> 982 + ); 983 + } 984 + 985 + function MaterialPillButton({ 986 + InactiveIcon, 987 + ActiveIcon, 988 + text, 989 + //active, 990 + onClickCallbback, 991 + small, 992 + }: { 993 + InactiveIcon: React.ReactElement; 994 + ActiveIcon: React.ReactElement; 995 + text: string; 996 + //active: boolean; 997 + onClickCallbback: () => void; 998 + small?: boolean | string; 999 + }) { 1000 + const active = false; 1001 + return ( 1002 + <button 1003 + className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 ${small ? "p-3 w-12" : "px-4 py-0.5"} items-center rounded-full transition-colors gap-1 ${active 1004 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 1005 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 1006 + }`} 1007 + onClick={() => { 1008 + onClickCallbback(); 1009 + }} 1010 + > 1011 + <div className={`${!small && "mr-2"} ${active ? " " : " "}`}> 1012 + {active ? ActiveIcon : InactiveIcon} 1013 + </div> 1014 + {!small && ( 1015 + <span 1016 + className={`text-[17px] text-roboto ${active ? "font-medium" : ""}`} 1017 + > 1018 + {text} 1019 + </span> 1020 + )} 1021 + </button> 1022 + ); 1023 + } 1024 + 1025 + 1026 + export const ProfileSmall = ({ 1027 + did, 1028 + large = false, 1029 + }: { 1030 + did: string, 1031 + large?: boolean; 1032 + }) => { 1033 + const navigate = useNavigate(); 1034 + const { data: identity } = useQueryIdentity(did); 1035 + const { data: profiledata } = useQueryProfile( 1036 + `at://${did}/app.bsky.actor.profile/self` 1037 + ); 1038 + const profile = profiledata?.value; 1039 + 1040 + const [imgcdn] = useAtom(imgCDNAtom) 1041 + 1042 + function getAvatarUrl(p: typeof profile) { 1043 + const link = p?.avatar?.ref?.["$link"]; 1044 + if (!link || !did) return null; 1045 + return `https://${imgcdn}/img/avatar/plain/${did}/${link}@jpeg`; 1046 + } 1047 + 1048 + const onProfileClick = (e: React.MouseEvent<Element, MouseEvent>) => { 1049 + e.stopPropagation(); 1050 + navigate({ 1051 + to: "/profile/$did", 1052 + params: { did: did }, 1053 + }); 1054 + } 1055 + 1056 + if (!profiledata) { 1057 + return ( 1058 + // Skeleton loader 1059 + <div 1060 + onClick={onProfileClick} 1061 + className={`hover:cursor-pointer flex items-center gap-2.5 animate-pulse ${large ? "mb-1" : ""}`} 1062 + > 1063 + <div 1064 + className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 1065 + /> 1066 + <div className="flex flex-col gap-2"> 1067 + <div 1068 + className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-28" : "h-3 w-20"}`} 1069 + /> 1070 + <div 1071 + className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-20" : "h-3 w-16"}`} 1072 + /> 1073 + </div> 1074 + </div> 1075 + ) 1076 + } 1077 + 1078 + return ( 1079 + <div 1080 + onClick={onProfileClick} 1081 + className={`hover:cursor-pointer flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 1082 + > 1083 + <img 1084 + src={getAvatarUrl(profile) ?? undefined} 1085 + alt="avatar" 1086 + className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 1087 + /> 1088 + <div className="flex flex-col items-start text-left"> 1089 + <div 1090 + className={`font-medium ${large ? "text-gray-800 dark:text-gray-100 text-md" : "text-gray-800 dark:text-gray-100 text-sm"}`} 1091 + > 1092 + {profile?.displayName} 1093 + </div> 1094 + <div 1095 + className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 1096 + > 1097 + @{identity?.handle} 1098 + </div> 1099 + </div> 1100 + </div> 1101 + ); 1102 + }; 1103 + 1104 + 1105 + function FeedListDesktopSidebar() { 1106 + const { agent, status } = useAuth(); 1107 + const [quickAuth] = useAtom(quickAuthAtom); 1108 + const isAuthRestoring = quickAuth ? status === "loading" : false; 1109 + 1110 + const identityresultmaybe = useQueryIdentity( 1111 + !isAuthRestoring ? agent?.did : undefined, 1112 + ); 1113 + const identity = identityresultmaybe?.data; 1114 + 1115 + const prefsresultmaybe = useQueryPreferences({ 1116 + agent: !isAuthRestoring ? (agent ?? undefined) : undefined, 1117 + pdsUrl: !isAuthRestoring ? identity?.pds : undefined, 1118 + }); 1119 + const prefs = prefsresultmaybe?.data; 1120 + 1121 + const savedFeeds = React.useMemo(() => { 1122 + const savedFeedsPref = prefs?.preferences?.find( 1123 + (p): p is $Typed<SavedFeedsPrefV2> => 1124 + p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2" 1125 + ); 1126 + 1127 + return savedFeedsPref?.items || []; 1128 + }, [prefs]); 1129 + 1130 + const pinnedFeeds = React.useMemo(() => { 1131 + return savedFeeds.filter((feed: any) => feed.pinned); 1132 + }, [savedFeeds]); 1133 + 1134 + const shimmedunautheddefault = HOST_UNAUTHED_DEFAULT_FEEDS.map((aturi: string, idx: number) => { 1135 + return { 1136 + value: aturi, 1137 + pinned: true, 1138 + } 1139 + }) 1140 + 1141 + const feedsmap = agent?.did ? pinnedFeeds : shimmedunautheddefault; 1142 + 1143 + return ( 1144 + <div className="flex flex-col gap-1 items-start "> 1145 + {feedsmap.map((item: any, idx: number) => { return <FeedTabOnTop key={item} item={item} idx={idx} rightDesktopSidebar={true} /> })} 1146 + </div> 1147 + ) 1148 + } 1149 + 1150 + function LoginRedirect() { 1151 + const location = useLocation(); 1152 + const dontShowLoginButton = location.pathname === "/settings" 1153 + return ( 1154 + <div className=""> 1155 + <span className="text-gray-500 dark:text-gray-400 text-sm leading-tight"> 1156 + {HOST_LOGIN_BLURB} 1157 + </span> 1158 + <div className="flex flex-col gap-2 my-4"> 1159 + {!dontShowLoginButton && (<Link 1160 + to="/settings" 1161 + className="w-full rounded-full bg-gray-600 text-gray-100 dark:bg-gray-400 dark:text-gray-900 px-4 py-2 text-sm font-medium text-center" 1162 + > 1163 + Log in 1164 + </Link>)} 1165 + 1166 + {HOST_SIGNUP_PDS && ( 1167 + // todo make signup actually work 1168 + (<button 1169 + className="w-full rounded-sm border border-gray-300 dark:border-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300" 1170 + >Sign up 1171 + </button>) 1172 + )} 1173 + </div> 1174 + </div> 1175 + ) 1176 + }
+81
src/styles/app.css
··· 20 20 } 21 21 22 22 @theme { 23 + /* DEPRECATED red dwarf-ism leaking go kill it 24 + Conven usage should make entirely new components 25 + Universal Post Renderer should be abstracted so this does not matter 26 + Please use Primary Secondary Surface palletes 27 + */ 23 28 --color-gray-50: oklch(0.984 0.012 var(--safe-hue)); 24 29 --color-gray-100: oklch(0.968 0.017 var(--safe-hue)); 25 30 --color-gray-200: oklch(0.929 0.025 var(--safe-hue)); ··· 31 36 --color-gray-800: oklch(0.279 0.055 var(--safe-hue)); 32 37 --color-gray-900: oklch(0.208 0.055 var(--safe-hue)); 33 38 --color-gray-950: oklch(0.129 0.055 var(--safe-hue)); 39 + } 40 + 41 + /* CONVEN COLORS */ 42 + :root { 43 + --hue-base: var(--system-hue-base, 260); 44 + --hue-accent: var(--system-hue-accent, 290); 45 + --hue-contrast: var(--system-hue-contrast, 80); 46 + --hue-muted: var(--system-hue-muted, 220); 47 + } 48 + :root { 49 + /* shared lightness steps */ 50 + --l-50: 0.98; 51 + --l-100: 0.95; 52 + --l-200: 0.90; 53 + --l-300: 0.82; 54 + --l-400: 0.70; 55 + --l-500: 0.58; 56 + --l-600: 0.48; 57 + --l-700: 0.40; 58 + --l-800: 0.30; 59 + --l-900: 0.22; 60 + --l-950: 0.14; 61 + } 62 + 63 + @theme { 64 + /* BASE (surfaces, text, borders) */ 65 + --color-base-50: oklch(var(--l-50) 0.012 var(--hue-base)); 66 + --color-base-100: oklch(var(--l-100) 0.015 var(--hue-base)); 67 + --color-base-200: oklch(var(--l-200) 0.020 var(--hue-base)); 68 + --color-base-300: oklch(var(--l-300) 0.025 var(--hue-base)); 69 + --color-base-400: oklch(var(--l-400) 0.030 var(--hue-base)); 70 + --color-base-500: oklch(var(--l-500) 0.035 var(--hue-base)); 71 + --color-base-600: oklch(var(--l-600) 0.035 var(--hue-base)); 72 + --color-base-700: oklch(var(--l-700) 0.035 var(--hue-base)); 73 + --color-base-800: oklch(var(--l-800) 0.032 var(--hue-base)); 74 + --color-base-900: oklch(var(--l-900) 0.030 var(--hue-base)); 75 + --color-base-950: oklch(var(--l-950) 0.028 var(--hue-base)); 76 + 77 + /* ACCENT (primary actions, links) */ 78 + --color-accent-50: oklch(var(--l-50) 0.06 var(--hue-accent)); 79 + --color-accent-100: oklch(var(--l-100) 0.08 var(--hue-accent)); 80 + --color-accent-200: oklch(var(--l-200) 0.12 var(--hue-accent)); 81 + --color-accent-300: oklch(var(--l-300) 0.16 var(--hue-accent)); 82 + --color-accent-400: oklch(var(--l-400) 0.20 var(--hue-accent)); 83 + --color-accent-500: oklch(var(--l-500) 0.22 var(--hue-accent)); 84 + --color-accent-600: oklch(var(--l-600) 0.24 var(--hue-accent)); 85 + --color-accent-700: oklch(var(--l-700) 0.22 var(--hue-accent)); 86 + --color-accent-800: oklch(var(--l-800) 0.20 var(--hue-accent)); 87 + --color-accent-900: oklch(var(--l-900) 0.18 var(--hue-accent)); 88 + --color-accent-950: oklch(var(--l-950) 0.16 var(--hue-accent)); 89 + 90 + /* CONTRAST (cards, sections, secondary emphasis) */ 91 + --color-contrast-50: oklch(var(--l-50) 0.04 var(--hue-contrast)); 92 + --color-contrast-100: oklch(var(--l-100) 0.06 var(--hue-contrast)); 93 + --color-contrast-200: oklch(var(--l-200) 0.08 var(--hue-contrast)); 94 + --color-contrast-300: oklch(var(--l-300) 0.10 var(--hue-contrast)); 95 + --color-contrast-400: oklch(var(--l-400) 0.12 var(--hue-contrast)); 96 + --color-contrast-500: oklch(var(--l-500) 0.14 var(--hue-contrast)); 97 + --color-contrast-600: oklch(var(--l-600) 0.14 var(--hue-contrast)); 98 + --color-contrast-700: oklch(var(--l-700) 0.13 var(--hue-contrast)); 99 + --color-contrast-800: oklch(var(--l-800) 0.12 var(--hue-contrast)); 100 + --color-contrast-900: oklch(var(--l-900) 0.11 var(--hue-contrast)); 101 + --color-contrast-950: oklch(var(--l-950) 0.10 var(--hue-contrast)); 102 + 103 + /* MUTED (badges, subtle highlights) */ 104 + --color-muted-50: oklch(var(--l-50) 0.03 var(--hue-muted)); 105 + --color-muted-100: oklch(var(--l-100) 0.04 var(--hue-muted)); 106 + --color-muted-200: oklch(var(--l-200) 0.05 var(--hue-muted)); 107 + --color-muted-300: oklch(var(--l-300) 0.06 var(--hue-muted)); 108 + --color-muted-400: oklch(var(--l-400) 0.07 var(--hue-muted)); 109 + --color-muted-500: oklch(var(--l-500) 0.08 var(--hue-muted)); 110 + --color-muted-600: oklch(var(--l-600) 0.08 var(--hue-muted)); 111 + --color-muted-700: oklch(var(--l-700) 0.075 var(--hue-muted)); 112 + --color-muted-800: oklch(var(--l-800) 0.07 var(--hue-muted)); 113 + --color-muted-900: oklch(var(--l-900) 0.065 var(--hue-muted)); 114 + --color-muted-950: oklch(var(--l-950) 0.06 var(--hue-muted)); 34 115 } 35 116 36 117 :root {