import { useState, useEffect, useRef } from "react"; import { Link } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; import { MessageSquare, Highlighter, Users, ArrowRight, Github, Database, Shield, Zap, } from "lucide-react"; import { SiFirefox, SiGooglechrome, SiBluesky } from "react-icons/si"; import { FaEdge } from "react-icons/fa"; import logo from "../assets/logo.svg"; const isFirefox = typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); const isEdge = typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); function getExtensionInfo() { if (isFirefox) { return { url: "https://addons.mozilla.org/en-US/firefox/addon/margin/", Icon: SiFirefox, label: "Firefox", }; } if (isEdge) { return { url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn", Icon: FaEdge, label: "Edge", }; } return { url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/", Icon: SiGooglechrome, label: "Chrome", }; } import { getAnnotations, normalizeAnnotation } from "../api/client"; import { formatDistanceToNow } from "date-fns"; function DemoAnnotation() { const [annotations, setAnnotations] = useState([]); const [loading, setLoading] = useState(true); const [hoverPos, setHoverPos] = useState(null); const [hoverVisible, setHoverVisible] = useState(false); const [hoverAuthors, setHoverAuthors] = useState([]); const [showPopover, setShowPopover] = useState(false); const [popoverPos, setPopoverPos] = useState(null); const [popoverAnnotations, setPopoverAnnotations] = useState([]); const highlightRef = useRef(null); const articleRef = useRef(null); useEffect(() => { getAnnotations({ source: "https://en.wikipedia.org/wiki/AT_Protocol" }) .then((res) => { const rawItems = res.items || (Array.isArray(res) ? res : []); const normalized = rawItems.map(normalizeAnnotation); setAnnotations(normalized); }) .catch((err) => { console.error("Failed to fetch demo annotations:", err); }) .finally(() => { setLoading(false); }); }, []); useEffect(() => { if (!showPopover) return; const handleClickOutside = () => setShowPopover(false); document.addEventListener("click", handleClickOutside); return () => document.removeEventListener("click", handleClickOutside); }, [showPopover]); const getMatches = () => { return annotations.filter( (a) => (a.selector?.exact && a.selector.exact.includes("A handle serves as")) || (a.quote && a.quote.includes("A handle serves as")), ); }; const handleMouseEnter = () => { const matches = getMatches(); const authorsMap = new Map(); matches.forEach((a) => { const author = a.author || a.creator || { handle: "unknown" }; const id = author.did || author.handle; if (!authorsMap.has(id)) authorsMap.set(id, author); }); const unique = Array.from(authorsMap.values()); setHoverAuthors(unique); if (highlightRef.current && articleRef.current) { const spanRect = highlightRef.current.getBoundingClientRect(); const articleRect = articleRef.current.getBoundingClientRect(); const visibleCount = Math.min(unique.length, 3); const hasOverflow = unique.length > 3; const countForCalc = visibleCount + (hasOverflow ? 1 : 0); const width = countForCalc > 0 ? countForCalc * 18 + 10 : 0; const top = spanRect.top - articleRect.top + spanRect.height / 2 - 14; const left = spanRect.left - articleRect.left - width; setHoverPos({ top, left }); setHoverVisible(true); } }; const handleMouseLeave = () => { setHoverVisible(false); }; const handleHighlightClick = (e) => { e.stopPropagation(); const matches = getMatches(); setPopoverAnnotations(matches); if (highlightRef.current && articleRef.current) { const spanRect = highlightRef.current.getBoundingClientRect(); const articleRect = articleRef.current.getBoundingClientRect(); const top = spanRect.top - articleRect.top + spanRect.height + 10; let left = spanRect.left - articleRect.left; if (left + 300 > articleRect.width) { left = articleRect.width - 300; } setPopoverPos({ top, left }); setShowPopover(true); } }; const maxShow = 3; const displayHoverAuthors = hoverAuthors.slice(0, maxShow); const hoverOverflow = hoverAuthors.length - maxShow; return (
en.wikipedia.org/wiki/AT_Protocol
{hoverPos && hoverAuthors.length > 0 && (
{displayHoverAuthors.map((author, i) => author.avatar ? ( {author.handle} { e.target.style.display = "none"; e.target.nextSibling.style.display = "flex"; }} /> ) : (
{author.handle?.[0]?.toUpperCase() || "U"}
), )} {hoverOverflow > 0 && (
+{hoverOverflow}
)}
)} {showPopover && popoverPos && (
e.stopPropagation()} >
{popoverAnnotations.length}{" "} {popoverAnnotations.length === 1 ? "Comment" : "Comments"}
{popoverAnnotations.length === 0 ? (
No comments
) : ( popoverAnnotations.map((ann, i) => (
(e.target.src = logo)} alt="" /> @{ann.author?.handle || "user"}
{ann.text || ann.body?.value}
)) )}
)}

The AT Protocol utilizes a dual identifier system: a mutable handle, in the form of a domain name, and an immutable decentralized identifier (DID).

A handle serves as a verifiable user identifier. {" "} Verification is by either of two equivalent methods proving control of the domain name: Either a DNS query of a resource record with the same name as the handle, or a request for a text file from a Web service with the same name.

DIDs resolve to DID documents, which contain references to key user metadata, such as the user's handle, public keys, and data repository. While any DID method could, in theory, be used by the protocol if its components provide support for the method, in practice only two methods are supported ('blessed') by the protocol's reference implementations: did:plc and did:web. The validity of these identifiers can be verified by a registry which hosts the DID's associated document and a file that is hosted at a well-known location on the connected domain name, respectively.

Margin
@margin.at
en.wikipedia.org
{loading ? (
Loading...
) : annotations.length > 0 ? ( annotations.map((ann, i) => (
0 ? "demo-annotation-secondary" : ""}`} >
{ann.author?.handle { e.target.src = logo; }} />
@{ann.author?.handle || "margin.at"} {ann.createdAt ? formatDistanceToNow(new Date(ann.createdAt), { addSuffix: true, }) : "recently"}
{ann.selector?.exact && (

“{ann.selector.exact}”

)}

{ann.text || ann.body?.value}

)) ) : (
No annotations found.
)}
); } export default function Landing() { const { user } = useAuth(); const ext = getExtensionInfo(); return (
Built on ATProto

Write in the margins
of the web.

Margin is a social layer for reading online. Highlight passages, leave thoughts in the margins, and see what others are thinking about the pages you read.

Install for {ext.label} {user ? ( Open App ) : ( Sign In with ATProto )}

Also available for{" "} {isFirefox ? ( <> Edge {" "} and{" "} Chrome ) : isEdge ? ( <> Firefox {" "} and{" "} Chrome ) : ( <> Firefox {" "} and{" "} Edge )}

How it works

1

Install & Login

Add Margin to your browser and sign in with your AT Protocol handle. No new account needed, just your existing handle.

2

Annotate the Web

Highlight text on any page. Leave notes in the margins, ask questions, or add context to the conversation precisely where it belongs.

3

Share & Discover

Your annotations are published to your PDS. Discover what the community is reading and discussing across the web.

Universal Highlights

Save passages from any article, paper, or post. Your collection travels with you, independent of any single platform.

Universal Notes

Move the discussion out of the comments section. Contextual conversations that live right alongside the content.

Open Identity

Your data, your handle, your graph. Built on the AT Protocol for true ownership and portability.

Community Context

See the web with fresh eyes. Discover highlights and notes from other readers directly on the page.

Your data, your identity

Margin is built on the{" "} AT Protocol , the same open protocol that powers Bluesky. Sign in with your existing Bluesky account or create a new one in your preferred PDS.

Your annotations are stored in your PDS. You can export them anytime, use them with other apps, or self-host your own server. No vendor lock-in.

Portable data Export or migrate anytime
You own your identity Use your own domain as handle
Interoperable Works with the ATProto ecosystem
Open source Audit, contribute, self-host

Start annotating today

Free and open source. Sign in with ATProto to get started.

Get the Extension
); }