social components inlay.at
atproto components sdui
86
fork

Configure Feed

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

add embed, remove dead /view

+146 -107
+1 -1
app/at/[did]/[collection]/[rkey]/page.tsx
··· 25 25 redirect(`/edit/at/${paramDid}/${paramCollection}/${paramRkey}${qsSuffix}`); 26 26 } 27 27 if (layout === "page") { 28 - redirect(`/view/at/${paramDid}/${paramCollection}/${paramRkey}${qsSuffix}`); 28 + redirect(`/list/at/${paramDid}/${paramCollection}/${paramRkey}${qsSuffix}`); 29 29 } 30 30 redirect(`/list/at/${paramDid}/${paramCollection}/${paramRkey}${qsSuffix}`); 31 31 }
+10 -3
app/sandbox/browse/browse-shell.tsx
··· 18 18 import { PrimaryButton } from "../ui/primary-button"; 19 19 import { BrowsePill } from "../ui/browse-pill"; 20 20 import { BrowseCard } from "../ui/browse-card"; 21 + import { EmbedButton } from "../ui/embed-button"; 21 22 import { DotGrid } from "../ui/dot-grid"; 22 23 23 24 export function BrowseShell({ ··· 159 160 prevHref={prevHref} 160 161 nextHref={nextHref} 161 162 rkey={rkey} 163 + recordUri={sourceUri} 162 164 > 163 165 <BrowseProvider 164 166 sourceCollection={collection} ··· 181 183 prevHref, 182 184 nextHref, 183 185 rkey, 186 + recordUri, 184 187 children, 185 188 }: { 186 189 selected?: BrowseComponentCard; ··· 191 194 prevHref: string | null; 192 195 nextHref: string | null; 193 196 rkey: string; 197 + recordUri: string; 194 198 children: ReactNode; 195 199 }) { 196 200 const isOwn = selected && authDid && selected.authorDid === authDid; 197 - const afterLabel = ( 198 - <ExternalLinkIcon href={expandHref} title="Open fullscreen in new tab" /> 199 - ); 201 + const afterLabel = selected ? ( 202 + <> 203 + <EmbedButton componentUri={selected.uri} recordUri={recordUri} /> 204 + <ExternalLinkIcon href={expandHref} title="Open fullscreen in new tab" /> 205 + </> 206 + ) : undefined; 200 207 201 208 const primary = isOwn ? ( 202 209 editHref ? (
+1 -10
app/sandbox/render/client.tsx
··· 163 163 : {}; 164 164 165 165 if (resolved) { 166 - const recordUri = `at://${resolved.did}/${resolved.collection}/${resolved.rkey}`; 167 - const parts = componentUri!.replace("at://", "").split("/"); 168 - const componentPath = `${parts[0]}/${parts[1]}/${parts[2]}`; 169 - 170 - let finalHref: string; 171 - if (route === "view") { 172 - finalHref = `/view/at/${componentPath}?uri=${encodeURIComponent(recordUri)}`; 173 - } else { 174 - finalHref = `/list/at/${resolved.did}/${resolved.collection}/${resolved.rkey}?componentUri=${encodeURIComponent(componentUri!)}`; 175 - } 166 + const finalHref = `/list/at/${resolved.did}/${resolved.collection}/${resolved.rkey}?componentUri=${encodeURIComponent(componentUri!)}`; 176 167 177 168 return ( 178 169 <NextLink
+133
app/sandbox/ui/embed-button.tsx
··· 1 + "use client"; 2 + 3 + import { useState, useRef } from "react"; 4 + import { Dropdown } from "@/app/ui/dropdown"; 5 + import s from "./preview.module.css"; 6 + 7 + export function EmbedButton({ 8 + componentUri, 9 + recordUri, 10 + }: { 11 + componentUri: string; 12 + recordUri: string; 13 + }) { 14 + const [copied, setCopied] = useState(false); 15 + const preRef = useRef<HTMLPreElement>(null); 16 + 17 + const snippet = `<inlay-at 18 + component="${componentUri}" 19 + props='${JSON.stringify({ uri: recordUri })}'> 20 + </inlay-at> 21 + <script src="https://inlay.at/inlay.js"><\/script>`; 22 + 23 + function selectAll() { 24 + if (!preRef.current) return; 25 + const range = document.createRange(); 26 + range.selectNodeContents(preRef.current); 27 + const sel = window.getSelection(); 28 + sel?.removeAllRanges(); 29 + sel?.addRange(range); 30 + } 31 + 32 + function handleCopy() { 33 + navigator.clipboard.writeText(snippet).then(() => { 34 + setCopied(true); 35 + setTimeout(() => setCopied(false), 2000); 36 + }); 37 + } 38 + 39 + return ( 40 + <Dropdown 41 + trigger={ 42 + <button 43 + title="Get embed code" 44 + className={s.inlineIconLink} 45 + style={{ 46 + background: "none", 47 + border: "none", 48 + cursor: "pointer", 49 + padding: 0, 50 + display: "inline-flex", 51 + marginLeft: 14, 52 + }} 53 + > 54 + <svg 55 + width="12" 56 + height="12" 57 + viewBox="0 0 16 16" 58 + fill="none" 59 + stroke="currentColor" 60 + strokeWidth="2" 61 + strokeLinecap="round" 62 + strokeLinejoin="round" 63 + > 64 + <polyline points="5,2 1,8 5,14" /> 65 + <polyline points="11,2 15,8 11,14" /> 66 + </svg> 67 + </button> 68 + } 69 + width={360} 70 + > 71 + <div style={{ padding: 8 }}> 72 + <div 73 + style={{ 74 + display: "flex", 75 + justifyContent: "space-between", 76 + alignItems: "center", 77 + marginBottom: 8, 78 + }} 79 + > 80 + <span 81 + style={{ 82 + fontSize: 11, 83 + fontWeight: 600, 84 + color: "var(--fg-2)", 85 + textTransform: "uppercase", 86 + letterSpacing: "0.04em", 87 + fontFamily: "'Rubik', sans-serif", 88 + }} 89 + > 90 + Embed 91 + </span> 92 + <button 93 + onClick={handleCopy} 94 + style={{ 95 + padding: "3px 10px", 96 + fontSize: 11, 97 + fontWeight: 500, 98 + fontFamily: "'Rubik', sans-serif", 99 + borderRadius: 5, 100 + border: "1px solid var(--border-10)", 101 + background: copied ? "var(--fg-0)" : "var(--bg-0)", 102 + color: copied ? "var(--bg-0)" : "var(--fg-2)", 103 + cursor: "pointer", 104 + transition: "all .15s", 105 + }} 106 + > 107 + {copied ? "Copied" : "Copy"} 108 + </button> 109 + </div> 110 + <pre 111 + ref={preRef} 112 + onClick={selectAll} 113 + style={{ 114 + margin: 0, 115 + padding: 10, 116 + background: "var(--bg-0)", 117 + border: "1px solid var(--border-8)", 118 + borderRadius: 6, 119 + fontSize: 11, 120 + lineHeight: 1.6, 121 + fontFamily: "'JetBrains Mono', monospace", 122 + color: "var(--fg-1)", 123 + whiteSpace: "pre-wrap", 124 + wordBreak: "break-all", 125 + cursor: "text", 126 + }} 127 + > 128 + {snippet} 129 + </pre> 130 + </div> 131 + </Dropdown> 132 + ); 133 + }
+1 -1
app/sandbox/ui/preview.module.css
··· 99 99 /* inline icon link (inside labels) */ 100 100 .inlineIconLink { 101 101 display: inline-flex; 102 - margin-left: 7px; 102 + margin-left: 14px; 103 103 color: var(--fg-2); 104 104 text-decoration: none; 105 105 transition: color 0.12s;
-69
app/view/at/[did]/[collection]/[rkey]/page.tsx
··· 1 - import { createElement } from "react"; 2 - import { renderNode, createContext } from "@/app/sandbox/render/render"; 3 - import { $ } from "@inlay/core"; 4 - import { resolveHandleToDid } from "@/data"; 5 - import { resolveView } from "@/data/queries"; 6 - import { BrowseProvider } from "@/app/sandbox/render/client"; 7 - import { redirect } from "next/navigation"; 8 - import { getCurrentDid } from "@/auth"; 9 - import "@/app/sandbox/render/client"; 10 - 11 - export default async function ViewPage({ 12 - params, 13 - searchParams, 14 - }: { 15 - params: Promise<{ did: string; collection: string; rkey: string }>; 16 - searchParams: Promise<Record<string, string | undefined>>; 17 - }) { 18 - const raw = await params; 19 - const sp = await searchParams; 20 - const paramDid = decodeURIComponent(raw.did); 21 - const paramCollection = decodeURIComponent(raw.collection); 22 - const paramRkey = decodeURIComponent(raw.rkey); 23 - 24 - const did = paramDid.startsWith("did:") 25 - ? paramDid 26 - : await resolveHandleToDid(paramDid); 27 - if (!did) redirect("/list/at"); 28 - 29 - const authDid = await getCurrentDid(); 30 - 31 - const componentUri = `at://${did}/${paramCollection}/${paramRkey}`; 32 - const resolved = await resolveView(paramCollection, authDid, componentUri); 33 - 34 - // The uri to render: either explicit ?uri= param, or fall back to the component's own uri 35 - const uri = sp.uri ? decodeURIComponent(sp.uri) : componentUri; 36 - 37 - // Pass remaining search params as props, excluding uri (already decoded above) 38 - const { uri: _uri, ...restSp } = sp; 39 - const element = $(resolved.nsid, { uri, ...restSp }); 40 - const ctx = createContext(resolved.record, resolved.uri); 41 - const rendered = renderNode(element, ctx); 42 - 43 - // Source collection comes from the record being viewed, not the component 44 - const parsedUri = uri.replace("at://", "").split("/"); 45 - const sourceCollection = parsedUri[1] || paramCollection; 46 - const sourceRkey = parsedUri[2] || paramRkey; 47 - 48 - return ( 49 - <BrowseProvider 50 - sourceCollection={sourceCollection} 51 - sourceRkey={sourceRkey} 52 - componentUri={componentUri} 53 - route="view" 54 - > 55 - <div 56 - style={{ 57 - flex: 1, 58 - display: "flex", 59 - flexDirection: "column", 60 - alignItems: "center", 61 - overflowY: "auto", 62 - boxSizing: "border-box", 63 - }} 64 - > 65 - {createElement("at-inlay-root", { "data-page": true }, rendered)} 66 - </div> 67 - </BrowseProvider> 68 - ); 69 - }
-23
app/view/at/layout.tsx
··· 1 - import { NavigationBlockerProvider } from "@/app/link"; 2 - 3 - export default function ViewLayout({ 4 - children, 5 - }: { 6 - children: React.ReactNode; 7 - }) { 8 - return ( 9 - <NavigationBlockerProvider> 10 - <div 11 - style={{ 12 - display: "flex", 13 - flexDirection: "column", 14 - height: "100vh", 15 - fontFamily: "'Rubik', -apple-system, sans-serif", 16 - background: "var(--bg-0)", 17 - }} 18 - > 19 - <div style={{ display: "flex", flex: 1, minHeight: 0 }}>{children}</div> 20 - </div> 21 - </NavigationBlockerProvider> 22 - ); 23 - }