diff --git a/web/eslint.config.js b/web/eslint.config.js
deleted file mode 100644
index 38602d2..0000000
--- a/web/eslint.config.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import js from "@eslint/js";
-import globals from "globals";
-import react from "eslint-plugin-react";
-import reactHooks from "eslint-plugin-react-hooks";
-import reactRefresh from "eslint-plugin-react-refresh";
-
-export default [
- { ignores: ["dist"] },
- {
- files: ["**/*.{js,jsx}"],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- parserOptions: {
- ecmaVersion: "latest",
- ecmaFeatures: { jsx: true },
- sourceType: "module",
- },
- },
- settings: { react: { version: "18.3" } },
- plugins: {
- react,
- "react-hooks": reactHooks,
- "react-refresh": reactRefresh,
- },
- rules: {
- ...js.configs.recommended.rules,
- ...react.configs.recommended.rules,
- ...react.configs["jsx-runtime"].rules,
- ...reactHooks.configs.recommended.rules,
- "react/jsx-no-target-blank": "off",
- "react-refresh/only-export-components": [
- "warn",
- { allowConstantExport: true },
- ],
- "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
- "react/prop-types": "off",
- },
- },
-];
diff --git a/web/index.html b/web/index.html
deleted file mode 100644
index 5c26768..0000000
--- a/web/index.html
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- setCreateModalOpen(false)}
- onSuccess={() => {
- loadCollections();
- }}
- />
- >
- );
-}
diff --git a/web/src/components/AnnotationCard.jsx b/web/src/components/AnnotationCard.jsx
deleted file mode 100644
index c325d60..0000000
--- a/web/src/components/AnnotationCard.jsx
+++ /dev/null
@@ -1,674 +0,0 @@
-import { useState, useEffect } from "react";
-import { useAuth } from "../context/AuthContext";
-import ReplyList from "./ReplyList";
-import { Link } from "react-router-dom";
-import RichText from "./RichText";
-import {
- normalizeAnnotation,
- normalizeHighlight,
- likeAnnotation,
- unlikeAnnotation,
- getReplies,
- createReply,
- deleteReply,
- updateAnnotation,
- updateHighlight,
- getEditHistory,
- deleteAnnotation,
-} from "../api/client";
-import {
- MessageSquare,
- Heart,
- Trash2,
- Folder,
- Edit2,
- Save,
- X,
- Clock,
-} from "lucide-react";
-import { HighlightIcon, TrashIcon } from "./Icons";
-import ShareMenu from "./ShareMenu";
-import UserMeta from "./UserMeta";
-
-function buildTextFragmentUrl(baseUrl, selector) {
- if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) {
- return baseUrl;
- }
- let fragment = ":~:text=";
- if (selector.prefix) {
- fragment += encodeURIComponent(selector.prefix) + "-,";
- }
- fragment += encodeURIComponent(selector.exact);
- if (selector.suffix) {
- fragment += ",-" + encodeURIComponent(selector.suffix);
- }
- return baseUrl + "#" + fragment;
-}
-
-const truncateUrl = (url, maxLength = 50) => {
- if (!url) return "";
- try {
- const parsed = new URL(url);
- const fullPath = parsed.host + parsed.pathname;
- if (fullPath.length > maxLength)
- return fullPath.substring(0, maxLength) + "...";
- return fullPath;
- } catch {
- return url.length > maxLength ? url.substring(0, maxLength) + "..." : url;
- }
-};
-
-function SembleBadge() {
- return (
-
-
via Semble
-
-
- );
-}
-
-export default function AnnotationCard({
- annotation,
- onDelete,
- onAddToCollection,
-}) {
- const { user, login } = useAuth();
- const data = normalizeAnnotation(annotation);
-
- const [likeCount, setLikeCount] = useState(data.likeCount || 0);
- const [isLiked, setIsLiked] = useState(data.viewerHasLiked || false);
- const [deleting, setDeleting] = useState(false);
- const [isEditing, setIsEditing] = useState(false);
- const [editText, setEditText] = useState(data.text || "");
- const [editTags, setEditTags] = useState(data.tags?.join(", ") || "");
- const [saving, setSaving] = useState(false);
- const [showHistory, setShowHistory] = useState(false);
- const [editHistory, setEditHistory] = useState([]);
- const [loadingHistory, setLoadingHistory] = useState(false);
- const [replies, setReplies] = useState([]);
- const [replyCount, setReplyCount] = useState(data.replyCount || 0);
- const [showReplies, setShowReplies] = useState(false);
- const [replyingTo, setReplyingTo] = useState(null);
- const [replyText, setReplyText] = useState("");
- const [posting, setPosting] = useState(false);
- const [hasEditHistory, setHasEditHistory] = useState(false);
-
- const isOwner = user?.did && data.author?.did === user.did;
- const isSemble = data.uri?.includes("network.cosmik");
- const highlightedText =
- data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null;
- const fragmentUrl = buildTextFragmentUrl(data.url, data.selector);
-
- useEffect(() => {
- if (data.uri && !data.color && !data.description) {
- getEditHistory(data.uri)
- .then((history) => {
- if (history?.length > 0) setHasEditHistory(true);
- })
- .catch(() => {});
- }
- }, [data.uri, data.color, data.description]);
-
- const fetchHistory = async () => {
- if (showHistory) {
- setShowHistory(false);
- return;
- }
- try {
- setLoadingHistory(true);
- setShowHistory(true);
- const history = await getEditHistory(data.uri);
- setEditHistory(history);
- } catch (err) {
- console.error("Failed to fetch history:", err);
- } finally {
- setLoadingHistory(false);
- }
- };
-
- const handlePostReply = async (parentReply) => {
- if (!replyText.trim()) return;
- try {
- setPosting(true);
- const parentUri = parentReply
- ? parentReply.id || parentReply.uri
- : data.uri;
- const parentCid = parentReply
- ? parentReply.cid
- : annotation.cid || data.cid;
-
- await createReply({
- parentUri,
- parentCid: parentCid || "",
- rootUri: data.uri,
- rootCid: annotation.cid || data.cid || "",
- text: replyText,
- });
-
- setReplyText("");
- setReplyingTo(null);
-
- const res = await getReplies(data.uri);
- if (res.items) {
- setReplies(res.items);
- setReplyCount(res.items.length);
- }
- } catch (err) {
- alert("Failed to post reply: " + err.message);
- } finally {
- setPosting(false);
- }
- };
-
- const handleSaveEdit = async () => {
- try {
- setSaving(true);
- const tagList = editTags
- .split(",")
- .map((t) => t.trim())
- .filter(Boolean);
- await updateAnnotation(data.uri, editText, tagList);
- setIsEditing(false);
- if (annotation.body) annotation.body.value = editText;
- else if (annotation.text) annotation.text = editText;
- if (annotation.tags) annotation.tags = tagList;
- data.tags = tagList;
- } catch (err) {
- alert("Failed to update: " + err.message);
- } finally {
- setSaving(false);
- }
- };
-
- const handleLike = async () => {
- if (!user) {
- login();
- return;
- }
- try {
- if (isLiked) {
- setIsLiked(false);
- setLikeCount((prev) => Math.max(0, prev - 1));
- await unlikeAnnotation(data.uri);
- } else {
- setIsLiked(true);
- setLikeCount((prev) => prev + 1);
- const cid = annotation.cid || data.cid || "";
- if (data.uri && cid) await likeAnnotation(data.uri, cid);
- }
- } catch {
- setIsLiked(!isLiked);
- setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1));
- }
- };
-
- const handleDelete = async () => {
- if (!confirm("Delete this annotation? This cannot be undone.")) return;
- try {
- setDeleting(true);
- const parts = data.uri.split("/");
- const rkey = parts[parts.length - 1];
- await deleteAnnotation(rkey);
- if (onDelete) onDelete(data.uri);
- else window.location.reload();
- } catch (err) {
- alert("Failed to delete: " + err.message);
- } finally {
- setDeleting(false);
- }
- };
-
- const loadReplies = async () => {
- if (!showReplies && replies.length === 0) {
- try {
- const res = await getReplies(data.uri);
- if (res.items) setReplies(res.items);
- } catch (err) {
- console.error("Failed to load replies:", err);
- }
- }
- setShowReplies(!showReplies);
- };
-
- const handleCollect = () => {
- if (!user) {
- login();
- return;
- }
- if (onAddToCollection) onAddToCollection();
- };
-
- return (
-
-
-
- {showHistory && (
-
-
-
Edit History
- setShowHistory(false)}
- >
-
-
-
- {loadingHistory ? (
-
Loading history...
- ) : editHistory.length === 0 ? (
-
No edit history found.
- ) : (
-
- )}
-
- )}
-
-
-
-
-
- {showReplies && (
-
-
setReplyingTo(reply)}
- onDelete={async (reply) => {
- if (!confirm("Delete this reply?")) return;
- try {
- await deleteReply(reply.id || reply.uri);
- const res = await getReplies(data.uri);
- if (res.items) {
- setReplies(res.items);
- setReplyCount(res.items.length);
- }
- } catch (err) {
- alert("Failed to delete: " + err.message);
- }
- }}
- isInline={true}
- />
-
-
- {replyingTo && (
-
-
- Replying to @
- {(replyingTo.creator || replyingTo.author)?.handle ||
- "unknown"}
-
- setReplyingTo(null)}
- className="cancel-reply"
- >
- ร
-
-
- )}
-
-
- )}
-
- );
-}
-
-export function HighlightCard({
- highlight,
- onDelete,
- onAddToCollection,
- onUpdate,
-}) {
- const { user, login } = useAuth();
- const data = normalizeHighlight(highlight);
- const highlightedText =
- data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null;
- const fragmentUrl = buildTextFragmentUrl(data.url, data.selector);
- const isOwner = user?.did && data.author?.did === user.did;
- const isSemble = data.uri?.includes("network.cosmik");
-
- const [isEditing, setIsEditing] = useState(false);
- const [editColor, setEditColor] = useState(data.color || "#f59e0b");
- const [editTags, setEditTags] = useState(data.tags?.join(", ") || "");
-
- const handleSaveEdit = async () => {
- try {
- const tagList = editTags
- .split(",")
- .map((t) => t.trim())
- .filter(Boolean);
- await updateHighlight(data.uri, editColor, tagList);
- setIsEditing(false);
- if (typeof onUpdate === "function") {
- onUpdate({ ...highlight, color: editColor, tags: tagList });
- }
- } catch (err) {
- alert("Failed to update: " + err.message);
- }
- };
-
- const handleCollect = () => {
- if (!user) {
- login();
- return;
- }
- if (onAddToCollection) onAddToCollection();
- };
-
- return (
-
-
-
-
-
- {truncateUrl(data.url)}
-
-
- {highlightedText && (
-
- “{highlightedText}”
-
- )}
-
- {isEditing && (
-
-
-
-
setEditColor(e.target.value)}
- className="color-input"
- />
-
-
setEditTags(e.target.value)}
- style={{ flex: 1, margin: 0 }}
- />
-
-
-
-
- )}
-
- {data.tags?.length > 0 && (
-
- {data.tags.map((tag, i) => (
-
- #{tag}
-
- ))}
-
- )}
-
-
-
-
- );
-}
diff --git a/web/src/components/AnnotationSkeleton.jsx b/web/src/components/AnnotationSkeleton.jsx
deleted file mode 100644
index a270eb3..0000000
--- a/web/src/components/AnnotationSkeleton.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-export default function AnnotationSkeleton() {
- return (
-
- );
-}
diff --git a/web/src/components/AppLayout.jsx b/web/src/components/AppLayout.jsx
deleted file mode 100644
index cf3ed19..0000000
--- a/web/src/components/AppLayout.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import LeftSidebar from "./LeftSidebar";
-import RightSidebar from "./RightSidebar";
-import TopNav from "./TopNav";
-import MobileNav from "./MobileNav";
-
-import { useTheme } from "../context/ThemeContext";
-
-export default function AppLayout({ children }) {
- const { layout } = useTheme();
-
- return (
- <>
-
-
-
- {layout !== "topnav" && }
- {children}
- {layout !== "topnav" && }
-
-
-
- >
- );
-}
diff --git a/web/src/components/BookmarkCard.jsx b/web/src/components/BookmarkCard.jsx
deleted file mode 100644
index 9bbde84..0000000
--- a/web/src/components/BookmarkCard.jsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import { useState, useEffect } from "react";
-import { useAuth } from "../context/AuthContext";
-import {
- normalizeAnnotation,
- normalizeBookmark,
- likeAnnotation,
- unlikeAnnotation,
- getLikeCount,
- deleteBookmark,
-} from "../api/client";
-import { HeartIcon, TrashIcon } from "./Icons";
-import { Folder, ExternalLink } from "lucide-react";
-import ShareMenu from "./ShareMenu";
-import UserMeta from "./UserMeta";
-
-export default function BookmarkCard({
- bookmark,
- onAddToCollection,
- onDelete,
-}) {
- const { user, login } = useAuth();
- const raw = bookmark;
- const data =
- raw.type === "Bookmark" ? normalizeBookmark(raw) : normalizeAnnotation(raw);
-
- const [likeCount, setLikeCount] = useState(0);
- const [isLiked, setIsLiked] = useState(false);
- const [deleting, setDeleting] = useState(false);
-
- const isOwner = user?.did && data.author?.did === user.did;
- const isSemble = data.uri?.includes("network.cosmik");
-
- let domain = "";
- try {
- if (data.url) domain = new URL(data.url).hostname.replace("www.", "");
- } catch {
- /* ignore */
- }
-
- useEffect(() => {
- let mounted = true;
- async function fetchData() {
- try {
- const likeRes = await getLikeCount(data.uri);
- if (mounted) {
- if (likeRes.count !== undefined) setLikeCount(likeRes.count);
- if (likeRes.liked !== undefined) setIsLiked(likeRes.liked);
- }
- } catch {
- /* ignore */
- }
- }
- if (data.uri) fetchData();
- return () => {
- mounted = false;
- };
- }, [data.uri]);
-
- const handleLike = async () => {
- if (!user) {
- login();
- return;
- }
- try {
- if (isLiked) {
- setIsLiked(false);
- setLikeCount((prev) => Math.max(0, prev - 1));
- await unlikeAnnotation(data.uri);
- } else {
- setIsLiked(true);
- setLikeCount((prev) => prev + 1);
- const cid = data.cid || "";
- if (data.uri && cid) await likeAnnotation(data.uri, cid);
- }
- } catch {
- setIsLiked(!isLiked);
- setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1));
- }
- };
-
- const handleDelete = async () => {
- if (onDelete) {
- onDelete(data.uri);
- return;
- }
- if (!confirm("Delete this bookmark?")) return;
- try {
- setDeleting(true);
- const parts = data.uri.split("/");
- const rkey = parts[parts.length - 1];
- await deleteBookmark(rkey);
- window.location.reload();
- } catch (err) {
- alert("Failed to delete: " + err.message);
- } finally {
- setDeleting(false);
- }
- };
-
- const handleCollect = () => {
- if (!user) {
- login();
- return;
- }
- if (onAddToCollection) onAddToCollection();
- };
-
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/web/src/components/CollectionIcon.jsx b/web/src/components/CollectionIcon.jsx
deleted file mode 100644
index c306232..0000000
--- a/web/src/components/CollectionIcon.jsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import {
- Folder,
- Star,
- Heart,
- Bookmark,
- Lightbulb,
- Zap,
- Coffee,
- Music,
- Camera,
- Code,
- Globe,
- Flag,
- Tag,
- Box,
- Archive,
- FileText,
- Image,
- Video,
- Mail,
- MapPin,
- Calendar,
- Clock,
- Search,
- Settings,
- User,
- Users,
- Home,
- Briefcase,
- Gift,
- Award,
- Target,
- TrendingUp,
- Activity,
- Cpu,
- Database,
- Cloud,
- Sun,
- Moon,
- Flame,
- Leaf,
-} from "lucide-react";
-
-const ICON_MAP = {
- folder: Folder,
- star: Star,
- heart: Heart,
- bookmark: Bookmark,
- lightbulb: Lightbulb,
- zap: Zap,
- coffee: Coffee,
- music: Music,
- camera: Camera,
- code: Code,
- globe: Globe,
- flag: Flag,
- tag: Tag,
- box: Box,
- archive: Archive,
- file: FileText,
- image: Image,
- video: Video,
- mail: Mail,
- pin: MapPin,
- calendar: Calendar,
- clock: Clock,
- search: Search,
- settings: Settings,
- user: User,
- users: Users,
- home: Home,
- briefcase: Briefcase,
- gift: Gift,
- award: Award,
- target: Target,
- trending: TrendingUp,
- activity: Activity,
- cpu: Cpu,
- database: Database,
- cloud: Cloud,
- sun: Sun,
- moon: Moon,
- flame: Flame,
- leaf: Leaf,
-};
-
-export default function CollectionIcon({ icon, size = 22, className = "" }) {
- if (!icon) {
- return ;
- }
-
- if (icon === "icon:semble") {
- return (
-
- );
- }
-
- if (icon.startsWith("icon:")) {
- const iconName = icon.replace("icon:", "");
- const IconComponent = ICON_MAP[iconName];
- if (IconComponent) {
- return ;
- }
- return ;
- }
-
- return (
-
- {icon}
-
- );
-}
diff --git a/web/src/components/CollectionItemCard.jsx b/web/src/components/CollectionItemCard.jsx
deleted file mode 100644
index d0f1734..0000000
--- a/web/src/components/CollectionItemCard.jsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Link } from "react-router-dom";
-import AnnotationCard, { HighlightCard } from "./AnnotationCard";
-import BookmarkCard from "./BookmarkCard";
-
-import CollectionIcon from "./CollectionIcon";
-import ShareMenu from "./ShareMenu";
-
-export default function CollectionItemCard({ item, onAddToCollection }) {
- const author = item.creator;
- const collection = item.collection;
-
- if (!author || !collection) return null;
-
- const innerItem = item.annotation || item.highlight || item.bookmark;
- if (!innerItem) return null;
-
- const innerUri = innerItem.uri || innerItem.id;
-
- return (
-
-
-
- {author.avatar && (
-
- )}
-
-
- {author.displayName || author.handle}
- {" "}
- added to{" "}
-
-
- {collection.name}
-
-
-
-
-
-
- {item.annotation && (
-
onAddToCollection?.(innerUri)}
- />
- )}
- {item.highlight && (
- onAddToCollection?.(innerUri)}
- />
- )}
- {item.bookmark && (
- onAddToCollection?.(innerUri)}
- />
- )}
-
- );
-}
diff --git a/web/src/components/CollectionModal.jsx b/web/src/components/CollectionModal.jsx
deleted file mode 100644
index 34391af..0000000
--- a/web/src/components/CollectionModal.jsx
+++ /dev/null
@@ -1,396 +0,0 @@
-import { useState, useEffect } from "react";
-import {
- X,
- Folder,
- Star,
- Heart,
- Bookmark,
- Lightbulb,
- Zap,
- Coffee,
- Music,
- Camera,
- Code,
- Globe,
- Flag,
- Tag,
- Box,
- Archive,
- FileText,
- Image,
- Video,
- Mail,
- MapPin,
- Calendar,
- Clock,
- Search,
- Settings,
- User,
- Users,
- Home,
- Briefcase,
- Gift,
- Award,
- Target,
- TrendingUp,
- Activity,
- Cpu,
- Database,
- Cloud,
- Sun,
- Moon,
- Flame,
- Leaf,
- Trash2,
-} from "lucide-react";
-import {
- createCollection,
- updateCollection,
- deleteCollection,
-} from "../api/client";
-
-const EMOJI_OPTIONS = [
- "๐",
- "๐",
- "๐ก",
- "โญ",
- "๐",
- "๐ป",
- "๐จ",
- "๐",
- "๐ฌ",
- "๐ฏ",
- "๐",
- "๐",
- "๐",
- "๐",
- "๐ผ",
- "๐ฎ",
- "๐ต",
- "๐ฌ",
- "โค๏ธ",
- "๐ฅ",
- "๐",
- "๐ธ",
- "๐ฟ",
- "๐ง ",
- "๐",
- "๐",
- "๐",
- "โจ",
- "๐ง",
- "โก",
-];
-
-const ICON_OPTIONS = [
- { icon: Folder, name: "folder" },
- { icon: Star, name: "star" },
- { icon: Heart, name: "heart" },
- { icon: Bookmark, name: "bookmark" },
- { icon: Lightbulb, name: "lightbulb" },
- { icon: Zap, name: "zap" },
- { icon: Coffee, name: "coffee" },
- { icon: Music, name: "music" },
- { icon: Camera, name: "camera" },
- { icon: Code, name: "code" },
- { icon: Globe, name: "globe" },
- { icon: Flag, name: "flag" },
- { icon: Tag, name: "tag" },
- { icon: Box, name: "box" },
- { icon: Archive, name: "archive" },
- { icon: FileText, name: "file" },
- { icon: Image, name: "image" },
- { icon: Video, name: "video" },
- { icon: Mail, name: "mail" },
- { icon: MapPin, name: "pin" },
- { icon: Calendar, name: "calendar" },
- { icon: Clock, name: "clock" },
- { icon: Search, name: "search" },
- { icon: Settings, name: "settings" },
- { icon: User, name: "user" },
- { icon: Users, name: "users" },
- { icon: Home, name: "home" },
- { icon: Briefcase, name: "briefcase" },
- { icon: Gift, name: "gift" },
- { icon: Award, name: "award" },
- { icon: Target, name: "target" },
- { icon: TrendingUp, name: "trending" },
- { icon: Activity, name: "activity" },
- { icon: Cpu, name: "cpu" },
- { icon: Database, name: "database" },
- { icon: Cloud, name: "cloud" },
- { icon: Sun, name: "sun" },
- { icon: Moon, name: "moon" },
- { icon: Flame, name: "flame" },
- { icon: Leaf, name: "leaf" },
-];
-
-export default function CollectionModal({
- isOpen,
- onClose,
- onSuccess,
- collectionToEdit,
- onDelete,
-}) {
- const [name, setName] = useState("");
- const [description, setDescription] = useState("");
- const [icon, setIcon] = useState("");
- const [customEmoji, setCustomEmoji] = useState("");
- const [activeTab, setActiveTab] = useState("emoji");
- const [loading, setLoading] = useState(false);
- const [deleting, setDeleting] = useState(false);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- if (collectionToEdit) {
- setName(collectionToEdit.name);
- setDescription(collectionToEdit.description || "");
- const savedIcon = collectionToEdit.icon || "";
- setIcon(savedIcon);
- setCustomEmoji(savedIcon);
-
- if (savedIcon.startsWith("icon:")) {
- setActiveTab("icons");
- }
- } else {
- setName("");
- setDescription("");
- setIcon("");
- setCustomEmoji("");
- }
- setError(null);
- }, [collectionToEdit, isOpen]);
-
- if (!isOpen) return null;
-
- const handleEmojiSelect = (emoji) => {
- if (icon === emoji) {
- setIcon("");
- setCustomEmoji("");
- } else {
- setIcon(emoji);
- setCustomEmoji(emoji);
- }
- };
-
- const handleIconSelect = (iconName) => {
- const value = `icon:${iconName}`;
- if (icon === value) {
- setIcon("");
- setCustomEmoji("");
- } else {
- setIcon(value);
- setCustomEmoji(value);
- }
- };
-
- const handleCustomEmojiChange = (e) => {
- const value = e.target.value;
- setCustomEmoji(value);
- const emojiMatch = value.match(
- /(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)/gu,
- );
- if (emojiMatch && emojiMatch.length > 0) {
- setIcon(emojiMatch[emojiMatch.length - 1]);
- } else if (value === "") {
- setIcon("");
- }
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- setLoading(true);
- setError(null);
-
- try {
- if (collectionToEdit) {
- await updateCollection(collectionToEdit.uri, name, description, icon);
- } else {
- await createCollection(name, description, icon);
- }
- onSuccess();
- onClose();
- } catch (err) {
- console.error(err);
- setError(err.message || "Failed to save collection");
- } finally {
- setLoading(false);
- }
- };
-
- const handleDelete = async () => {
- if (
- !confirm(
- "Delete this collection and all its items? This cannot be undone.",
- )
- ) {
- return;
- }
- setDeleting(true);
- setError(null);
-
- try {
- await deleteCollection(collectionToEdit.uri);
- if (onDelete) {
- onDelete();
- } else {
- onSuccess();
- }
- onClose();
- } catch (err) {
- console.error(err);
- setError(err.message || "Failed to delete collection");
- } finally {
- setDeleting(false);
- }
- };
-
- return (
-
-
e.stopPropagation()}
- >
-
-
- {collectionToEdit ? "Edit Collection" : "New Collection"}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/web/src/components/CollectionRow.jsx b/web/src/components/CollectionRow.jsx
deleted file mode 100644
index 2724ccc..0000000
--- a/web/src/components/CollectionRow.jsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Link } from "react-router-dom";
-import { ChevronRight, Edit2 } from "lucide-react";
-import CollectionIcon from "./CollectionIcon";
-
-export default function CollectionRow({ collection, onEdit }) {
- return (
-
-
-
-
-
-
-
{collection.name}
- {collection.description && (
-
{collection.description}
- )}
-
-
-
- {onEdit && !collection.uri.includes("network.cosmik") && (
-
{
- e.preventDefault();
- e.stopPropagation();
- onEdit();
- }}
- className="collection-row-edit"
- title="Edit collection"
- >
-
-
- )}
-
- );
-}
diff --git a/web/src/components/Composer.jsx b/web/src/components/Composer.jsx
deleted file mode 100644
index d3f72f8..0000000
--- a/web/src/components/Composer.jsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import { useState } from "react";
-import { createAnnotation, createHighlight } from "../api/client";
-
-export default function Composer({
- url,
- selector: initialSelector,
- onSuccess,
- onCancel,
-}) {
- const [text, setText] = useState("");
- const [quoteText, setQuoteText] = useState("");
- const [tags, setTags] = useState("");
- const [selector, setSelector] = useState(initialSelector);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [showQuoteInput, setShowQuoteInput] = useState(false);
-
- const highlightedText =
- selector?.type === "TextQuoteSelector" ? selector.exact : null;
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- if (!text.trim() && !highlightedText && !quoteText.trim()) return;
-
- try {
- setLoading(true);
- setError(null);
-
- let finalSelector = selector;
- if (!finalSelector && quoteText.trim()) {
- finalSelector = {
- type: "TextQuoteSelector",
- exact: quoteText.trim(),
- };
- }
-
- const tagList = tags
- .split(",")
- .map((t) => t.trim())
- .filter(Boolean);
-
- if (!text.trim()) {
- await createHighlight({
- url,
- selector: finalSelector,
- color: "yellow",
- tags: tagList,
- });
- } else {
- await createAnnotation({
- url,
- text,
- selector: finalSelector || undefined,
- tags: tagList,
- });
- }
-
- setText("");
- setQuoteText("");
- setSelector(null);
- if (onSuccess) onSuccess();
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- };
-
- const handleRemoveSelector = () => {
- setSelector(null);
- setQuoteText("");
- setShowQuoteInput(false);
- };
-
- return (
-
-
-
New Annotation
- {url &&
{url}
}
-
-
- {}
- {highlightedText && (
-
-
- ร
-
-
- "{highlightedText}"
-
-
- )}
-
- {}
- {!highlightedText && (
- <>
- {!showQuoteInput ? (
- setShowQuoteInput(true)}
- >
- + Add a quote from the page
-
- ) : (
-
- setQuoteText(e.target.value)}
- placeholder="Paste or type the text you're annotating..."
- className="composer-quote-input"
- rows={2}
- />
-
- Remove
-
-
- )}
- >
- )}
-
- setText(e.target.value)}
- placeholder={
- highlightedText || quoteText
- ? "Add your comment about this selection..."
- : "Write your annotation..."
- }
- className="composer-input"
- rows={4}
- maxLength={3000}
- disabled={loading}
- />
-
-
- setTags(e.target.value)}
- placeholder="Add tags (comma separated)..."
- className="composer-tags-input"
- disabled={loading}
- />
-
-
-
-
{text.length}/3000
-
- {onCancel && (
-
- Cancel
-
- )}
-
- {loading ? "Posting..." : "Post"}
-
-
-
-
- {error && {error}
}
-
- );
-}
diff --git a/web/src/components/EditProfileModal.jsx b/web/src/components/EditProfileModal.jsx
deleted file mode 100644
index 079ed3d..0000000
--- a/web/src/components/EditProfileModal.jsx
+++ /dev/null
@@ -1,258 +0,0 @@
-import { useState, useRef } from "react";
-import { updateProfile, uploadAvatar } from "../api/client";
-
-export default function EditProfileModal({ profile, onClose, onUpdate }) {
- const [displayName, setDisplayName] = useState(profile?.displayName || "");
- const [avatarBlob, setAvatarBlob] = useState(null);
- const [avatarPreview, setAvatarPreview] = useState(null);
- const [bio, setBio] = useState(profile?.bio || "");
- const [website, setWebsite] = useState(profile?.website || "");
- const [links, setLinks] = useState(profile?.links || []);
- const [newLink, setNewLink] = useState("");
- const [saving, setSaving] = useState(false);
- const [uploading, setUploading] = useState(false);
- const [error, setError] = useState(null);
- const fileInputRef = useRef(null);
-
- const handleAvatarChange = async (e) => {
- const file = e.target.files?.[0];
- if (!file) return;
-
- if (!["image/jpeg", "image/png"].includes(file.type)) {
- setError("Please select a JPEG or PNG image");
- return;
- }
-
- if (file.size > 1024 * 1024) {
- setError("Image must be under 1MB");
- return;
- }
-
- setAvatarPreview(URL.createObjectURL(file));
- setUploading(true);
- setError(null);
-
- try {
- const result = await uploadAvatar(file);
- setAvatarBlob(result.blob);
- } catch (err) {
- setError("Failed to upload avatar: " + err.message);
- setAvatarPreview(null);
- } finally {
- setUploading(false);
- }
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- setSaving(true);
- setError(null);
-
- try {
- await updateProfile({
- displayName,
- avatar: avatarBlob,
- bio,
- website,
- links,
- });
- onUpdate();
- onClose();
- } catch (err) {
- setError(err.message);
- } finally {
- setSaving(false);
- }
- };
-
- const addLink = () => {
- if (!newLink) return;
-
- if (!links.includes(newLink)) {
- setLinks([...links, newLink]);
- setNewLink("");
- setError(null);
- }
- };
-
- const removeLink = (index) => {
- setLinks(links.filter((_, i) => i !== index));
- };
-
- const currentAvatar =
- avatarPreview || (profile?.did ? `/api/avatar/${profile.did}` : null);
-
- return (
-
-
e.stopPropagation()}>
-
-
Edit Profile
-
-
-
-
-
-
-
-
- {error && {error}
}
-
-
-
Avatar
-
-
fileInputRef.current?.click()}
- style={{ cursor: "pointer" }}
- >
- {currentAvatar ? (
-
- ) : (
-
- )}
- {uploading && (
-
- Uploading...
-
- )}
-
-
-
fileInputRef.current?.click()}
- disabled={uploading}
- >
- {uploading ? "Uploading..." : "Change Avatar"}
-
-
-
-
-
-
Display Name
-
setDisplayName(e.target.value)}
- placeholder="Your name"
- maxLength={64}
- />
-
{displayName.length}/64
-
-
-
-
Bio
-
setBio(e.target.value)}
- placeholder="Tell us about yourself..."
- rows={4}
- maxLength={5000}
- />
- {bio.length}/5000
-
-
-
- Website
- setWebsite(e.target.value)}
- placeholder="https://example.com"
- maxLength={1000}
- />
-
-
-
-
Links
-
- setNewLink(e.target.value)}
- placeholder="Add a link (e.g. GitHub, LinkedIn)..."
- onKeyDown={(e) =>
- e.key === "Enter" && (e.preventDefault(), addLink())
- }
- />
-
- Add
-
-
-
- {links.map((link, i) => (
-
- {link}
- removeLink(i)}
- >
- ร
-
-
- ))}
-
-
-
-
-
- Cancel
-
-
- {saving ? "Saving..." : "Save Profile"}
-
-
-
-
-
- );
-}
diff --git a/web/src/components/IOSInstallBanner.jsx b/web/src/components/IOSInstallBanner.jsx
deleted file mode 100644
index 22d4303..0000000
--- a/web/src/components/IOSInstallBanner.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { useState } from "react";
-import { X } from "lucide-react";
-import { SiApple } from "react-icons/si";
-
-function shouldShowBanner() {
- if (typeof window === "undefined") return false;
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
- if (!isIOS) return false;
-
- const dismissedAt = localStorage.getItem("ios-shortcut-dismissed");
- const daysSinceDismissed = dismissedAt
- ? (Date.now() - parseInt(dismissedAt, 10)) / (1000 * 60 * 60 * 24)
- : Infinity;
- return daysSinceDismissed > 7;
-}
-
-export default function IOSInstallBanner() {
- const [show, setShow] = useState(shouldShowBanner);
-
- const handleDismiss = () => {
- setShow(false);
- localStorage.setItem("ios-shortcut-dismissed", Date.now().toString());
- };
-
- if (!show) return null;
-
- return (
-
- );
-}
diff --git a/web/src/components/Icons.jsx b/web/src/components/Icons.jsx
deleted file mode 100644
index a7ad401..0000000
--- a/web/src/components/Icons.jsx
+++ /dev/null
@@ -1,505 +0,0 @@
-import tangledLogo from "../assets/tangled.svg";
-import { FaGithub, FaLinkedin } from "react-icons/fa";
-
-export function HeartIcon({ filled = false, size = 18 }) {
- return filled ? (
-
-
-
- ) : (
-
-
-
- );
-}
-
-export function MessageIcon({ size = 18 }) {
- return (
-
-
-
- );
-}
-
-export function ShareIcon({ size = 18 }) {
- return (
-
-
-
-
-
- );
-}
-
-export function TrashIcon({ size = 18 }) {
- return (
-
-
-
-
-
- );
-}
-
-export function LinkIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function ExternalLinkIcon({ size = 14 }) {
- return (
-
-
-
-
-
- );
-}
-
-export function PenIcon({ size = 18 }) {
- return (
-
-
-
- );
-}
-
-export function HighlightIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function BookmarkIcon({ size = 18 }) {
- return (
-
-
-
- );
-}
-
-export function TagIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function AlertIcon({ size = 18 }) {
- return (
-
-
-
-
-
- );
-}
-
-export function FileTextIcon({ size = 18 }) {
- return (
-
-
-
-
-
-
-
- );
-}
-
-export function SearchIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function InboxIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function BlueskyIcon({ size = 18, color = "currentColor" }) {
- return (
-
-
-
- );
-}
-
-export function MarginIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function LogoutIcon({ size = 18 }) {
- return (
-
-
-
-
-
- );
-}
-
-export function BellIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function ReplyIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function AturiIcon({ size = 18 }) {
- return (
-
-
-
-
- );
-}
-
-export function BlackskyIcon({ size = 18 }) {
- return (
-
-
-
-
-
-
- );
-}
-
-export function NorthskyIcon({ size = 18 }) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export function TophhieIcon({ size = 18 }) {
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-export function GithubIcon({ size = 18 }) {
- return ;
-}
-
-export function LinkedinIcon({ size = 18 }) {
- return ;
-}
-
-export function TangledIcon({ size = 18 }) {
- return (
-
- );
-}
diff --git a/web/src/components/LeftSidebar.jsx b/web/src/components/LeftSidebar.jsx
deleted file mode 100644
index 246e8e7..0000000
--- a/web/src/components/LeftSidebar.jsx
+++ /dev/null
@@ -1,189 +0,0 @@
-import { Link, useLocation } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import { useState, useEffect, useRef } from "react";
-import {
- Home,
- Search,
- Highlighter,
- Bookmark,
- Folder,
- Bell,
- PenSquare,
- User,
- LogOut,
- Settings,
- ChevronUp,
-} from "lucide-react";
-import { getUnreadNotificationCount } from "../api/client";
-
-export default function LeftSidebar() {
- const { user, isAuthenticated, logout } = useAuth();
- const location = useLocation();
- const [unreadCount, setUnreadCount] = useState(0);
- const [userMenuOpen, setUserMenuOpen] = useState(false);
- const userMenuRef = useRef(null);
-
- const isActive = (path) => {
- if (path === "/") return location.pathname === "/";
- return location.pathname.startsWith(path);
- };
-
- useEffect(() => {
- if (isAuthenticated) {
- getUnreadNotificationCount()
- .then((data) => setUnreadCount(data.count || 0))
- .catch(() => {});
- const interval = setInterval(() => {
- getUnreadNotificationCount()
- .then((data) => setUnreadCount(data.count || 0))
- .catch(() => {});
- }, 60000);
- return () => clearInterval(interval);
- }
- }, [isAuthenticated]);
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (userMenuRef.current && !userMenuRef.current.contains(e.target)) {
- setUserMenuOpen(false);
- }
- };
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, []);
-
- const handleLogout = () => {
- logout();
- setUserMenuOpen(false);
- };
-
- const navItems = [
- { path: "/home", icon: Home, label: "Home" },
- { path: "/url", icon: Search, label: "Browse" },
- ];
-
- const authNavItems = [
- { path: "/highlights", icon: Highlighter, label: "Highlights" },
- { path: "/bookmarks", icon: Bookmark, label: "Bookmarks" },
- { path: "/collections", icon: Folder, label: "Collections" },
- {
- path: "/notifications",
- icon: Bell,
- label: "Notifications",
- badge: unreadCount,
- },
- ];
-
- return (
-
- );
-}
diff --git a/web/src/components/MobileNav.jsx b/web/src/components/MobileNav.jsx
deleted file mode 100644
index d2e98e7..0000000
--- a/web/src/components/MobileNav.jsx
+++ /dev/null
@@ -1,248 +0,0 @@
-import { Link, useLocation } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import { useState, useEffect } from "react";
-import { getUnreadNotificationCount } from "../api/client";
-import {
- Home,
- Search,
- Folder,
- User,
- PenSquare,
- Bookmark,
- Settings,
- MoreHorizontal,
- LogOut,
- Bell,
- Highlighter,
-} from "lucide-react";
-
-export default function MobileNav() {
- const { user, isAuthenticated, logout } = useAuth();
- const location = useLocation();
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const [unreadCount, setUnreadCount] = useState(0);
-
- const isActive = (path) => {
- if (path === "/") return location.pathname === "/";
- return location.pathname.startsWith(path);
- };
-
- useEffect(() => {
- if (isAuthenticated) {
- getUnreadNotificationCount()
- .then((data) => setUnreadCount(data.count || 0))
- .catch(() => {});
- }
- }, [isAuthenticated]);
-
- const closeMenu = () => setIsMenuOpen(false);
-
- return (
- <>
- {isMenuOpen && (
- <>
-
-
- {isAuthenticated ? (
- <>
-
- {user.avatar ? (
-
- ) : (
-
-
-
- )}
-
-
- {user.displayName || user.handle}
-
-
- @{user.handle}
-
-
-
-
-
-
-
Highlights
-
-
-
-
-
Bookmarks
-
-
-
-
-
Collections
-
-
-
-
-
Settings
-
-
-
-
-
{
- logout();
- closeMenu();
- }}
- >
-
- Log Out
-
- >
- ) : (
- <>
-
-
-
Sign In
-
-
-
-
Collections
-
-
-
-
Settings
-
- >
- )}
-
- >
- )}
-
-
-
-
-
-
-
-
-
-
- {isAuthenticated ? (
- <>
-
-
-
-
-
-
-
- {unreadCount > 0 && (
-
- )}
-
-
- >
- ) : (
-
-
-
-
-
- )}
-
- setIsMenuOpen(!isMenuOpen)}
- >
-
-
-
- >
- );
-}
diff --git a/web/src/components/ReplyList.jsx b/web/src/components/ReplyList.jsx
deleted file mode 100644
index e369d80..0000000
--- a/web/src/components/ReplyList.jsx
+++ /dev/null
@@ -1,337 +0,0 @@
-import { Link } from "react-router-dom";
-import { MessageSquare, Trash2, Reply } from "lucide-react";
-
-function formatDate(dateString) {
- if (!dateString) return "";
- const date = new Date(dateString);
- const now = new Date();
- const diff = now - date;
- const minutes = Math.floor(diff / 60000);
- const hours = Math.floor(diff / 3600000);
- const days = Math.floor(diff / 86400000);
- if (minutes < 1) return "just now";
- if (minutes < 60) return `${minutes}m`;
- if (hours < 24) return `${hours}h`;
- if (days < 7) return `${days}d`;
- return date.toLocaleDateString();
-}
-
-function ReplyItem({ reply, depth = 0, user, onReply, onDelete, isInline }) {
- const author = reply.creator || reply.author || {};
- const isReplyOwner = user?.did && author.did === user.did;
-
- const containerStyle = isInline
- ? {
- display: "flex",
- gap: "10px",
- padding: depth > 0 ? "10px 12px 10px 16px" : "12px 16px",
- marginLeft: depth * 20,
- borderLeft: depth > 0 ? "2px solid var(--accent-subtle)" : "none",
- background: depth > 0 ? "rgba(168, 85, 247, 0.03)" : "transparent",
- }
- : {
- marginLeft: depth * 24,
- borderLeft: depth > 0 ? "2px solid var(--accent-subtle)" : "none",
- paddingLeft: depth > 0 ? "16px" : "0",
- background: depth > 0 ? "rgba(168, 85, 247, 0.02)" : "transparent",
- marginBottom: "12px",
- };
-
- const avatarSize = isInline ? (depth > 0 ? 28 : 32) : depth > 0 ? 28 : 36;
-
- return (
-
-
- {isInline ? (
- <>
-
- {author.avatar ? (
-
- ) : (
-
0 ? "0.65rem" : "0.75rem",
- fontWeight: 600,
- color: "white",
- }}
- >
- {(author.displayName ||
- author.handle ||
- "?")[0].toUpperCase()}
-
- )}
-
-
-
-
0 ? "0.8rem" : "0.85rem",
- color: "var(--text-primary)",
- }}
- >
- {author.displayName || author.handle}
-
-
0 ? "0.75rem" : "0.8rem",
- textDecoration: "none",
- }}
- >
- @{author.handle}
-
-
- ยท
-
-
- {formatDate(reply.created || reply.createdAt)}
-
-
-
- onReply(reply)}
- style={{
- background: "none",
- border: "none",
- color: "var(--text-tertiary)",
- cursor: "pointer",
- padding: "2px 6px",
- fontSize: "0.7rem",
- display: "flex",
- alignItems: "center",
- gap: "3px",
- borderRadius: "4px",
- }}
- >
-
-
- {isReplyOwner && (
- onDelete(reply)}
- style={{
- background: "none",
- border: "none",
- color: "var(--text-tertiary)",
- cursor: "pointer",
- padding: "2px 6px",
- fontSize: "0.7rem",
- display: "flex",
- alignItems: "center",
- gap: "3px",
- borderRadius: "4px",
- }}
- >
-
-
- )}
-
-
-
0 ? "0.85rem" : "0.9rem",
- lineHeight: 1.5,
- color: "var(--text-primary)",
- }}
- >
- {reply.text || reply.body?.value}
-
-
- >
- ) : (
- <>
-
-
-
- {author.avatar ? (
-
- ) : (
-
- {(author.displayName ||
- author.handle ||
- "?")[0].toUpperCase()}
-
- )}
-
-
-
-
- {author.displayName || author.handle}
-
- {author.handle && (
-
- @{author.handle}
-
- )}
- ยท
-
- {formatDate(reply.created || reply.createdAt)}
-
-
-
- onReply(reply)}
- title="Reply"
- >
-
-
- {isReplyOwner && (
- onDelete(reply)}
- title="Delete"
- >
-
-
- )}
-
-
-
{reply.text || reply.body?.value}
- >
- )}
-
- {reply.children &&
- reply.children.map((child) => (
-
- ))}
-
- );
-}
-
-export default function ReplyList({
- replies,
- rootUri,
- user,
- onReply,
- onDelete,
- isInline = false,
-}) {
- if (!replies || replies.length === 0) {
- if (isInline) {
- return (
-
- No replies yet
-
- );
- }
- return (
-
-
- No replies yet. Be the first to reply!
-
-
- );
- }
-
- const buildReplyTree = () => {
- const replyMap = {};
- const rootReplies = [];
-
- replies.forEach((r) => {
- replyMap[r.id || r.uri] = { ...r, children: [] };
- });
-
- replies.forEach((r) => {
- const parentUri = r.inReplyTo || r.parentUri;
- if (parentUri === rootUri) {
- rootReplies.push(replyMap[r.id || r.uri]);
- } else if (replyMap[parentUri]) {
- replyMap[parentUri].children.push(replyMap[r.id || r.uri]);
- } else {
- rootReplies.push(replyMap[r.id || r.uri]);
- }
- });
-
- return rootReplies;
- };
-
- const replyTree = buildReplyTree();
-
- return (
-
- {replyTree.map((reply) => (
-
- ))}
-
- );
-}
diff --git a/web/src/components/RichText.jsx b/web/src/components/RichText.jsx
deleted file mode 100644
index 1c23192..0000000
--- a/web/src/components/RichText.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from "react";
-import { Link } from "react-router-dom";
-
-const URL_REGEX = /(https?:\/\/[^\s]+)/g;
-
-export default function RichText({ text }) {
- if (!text) return null;
-
- const parts = text.split(URL_REGEX);
-
- return (
-
- {parts.map((part, i) => {
- if (part.match(URL_REGEX)) {
- return (
- e.stopPropagation()}
- className="rich-text-link"
- >
- {part}
-
- );
- }
-
- const subParts = part.split(/((?:^|\s)@[a-zA-Z0-9.-]+\b)/g);
-
- return (
-
- {subParts.map((subPart, j) => {
- const mentionMatch = subPart.match(/^(\s*)@([a-zA-Z0-9.-]+)$/);
- if (mentionMatch) {
- const prefix = mentionMatch[1];
- const handle = mentionMatch[2];
- if (handle.includes(".")) {
- return (
-
- {prefix}
- e.stopPropagation()}
- >
- @{handle}
-
-
- );
- }
- }
- return subPart;
- })}
-
- );
- })}
-
- );
-}
diff --git a/web/src/components/RightSidebar.jsx b/web/src/components/RightSidebar.jsx
deleted file mode 100644
index dc7e260..0000000
--- a/web/src/components/RightSidebar.jsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { Link } from "react-router-dom";
-import { useState, useEffect } from "react";
-import { useTheme } from "../context/ThemeContext";
-import { Sun, Moon, Monitor, ExternalLink } from "lucide-react";
-import {
- SiFirefox,
- SiGooglechrome,
- SiGithub,
- SiBluesky,
- SiDiscord,
-} from "react-icons/si";
-import { FaEdge } from "react-icons/fa";
-import tangledLogo from "../assets/tangled.svg";
-import { getTrendingTags } from "../api/client";
-
-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",
- };
-}
-
-export default function RightSidebar() {
- const { theme, setTheme } = useTheme();
- const [trendingTags, setTrendingTags] = useState([]);
- const ext = getExtensionInfo();
- const ExtIcon = ext.icon;
-
- useEffect(() => {
- getTrendingTags(10)
- .then((data) => setTrendingTags(data.tags || []))
- .catch(() => {});
- }, []);
-
- const cycleTheme = () => {
- const next =
- theme === "system" ? "light" : theme === "light" ? "dark" : "system";
- setTheme(next);
- };
-
- return (
-
- {trendingTags.length > 0 && (
-
-
Trending Tags
-
- {trendingTags.map((tag) => (
-
- #{tag}
-
- ))}
-
-
- )}
-
-
-
-
-
-
-
- {theme === "system" && }
- {theme === "dark" && }
- {theme === "light" && }
- Theme: {theme}
-
-
-
-
- Privacy
- ยท
- Terms
-
-
- );
-}
diff --git a/web/src/components/ScrollToTop.jsx b/web/src/components/ScrollToTop.jsx
deleted file mode 100644
index d1adaa1..0000000
--- a/web/src/components/ScrollToTop.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useEffect } from "react";
-import { useLocation } from "react-router-dom";
-
-export default function ScrollToTop() {
- const { pathname } = useLocation();
-
- useEffect(() => {
- window.scrollTo(0, 0);
- }, [pathname]);
-
- return null;
-}
diff --git a/web/src/components/ShareMenu.jsx b/web/src/components/ShareMenu.jsx
deleted file mode 100644
index 190a791..0000000
--- a/web/src/components/ShareMenu.jsx
+++ /dev/null
@@ -1,401 +0,0 @@
-import { useState, useRef, useEffect } from "react";
-import { Copy, ExternalLink, Check } from "lucide-react";
-import { BlueskyIcon, AturiIcon } from "./Icons";
-
-const BLUESKY_COLOR = "#1185fe";
-
-const WitchskyIcon = () => (
-
-
-
-);
-
-const BlackskyIcon = () => (
-
-
-
-
-
-
-);
-
-const CatskyIcon = () => (
-
-
-
-
-
-
-
-
-
-
-
-);
-
-const DeerIcon = () => (
-
-
-
-);
-
-const BLUESKY_FORKS = [
- {
- name: "Bluesky",
- domain: "bsky.app",
- Icon: () => ,
- },
- { name: "Witchsky", domain: "witchsky.app", Icon: WitchskyIcon },
- { name: "Blacksky", domain: "blacksky.community", Icon: BlackskyIcon },
- { name: "Catsky", domain: "catsky.social", Icon: CatskyIcon },
- { name: "Deer", domain: "deer.social", Icon: DeerIcon },
-];
-
-export default function ShareMenu({ uri, text, customUrl, handle, type, url }) {
- const [isOpen, setIsOpen] = useState(false);
- const [copied, setCopied] = useState(false);
- const [copiedAturi, setCopiedAturi] = useState(false);
- const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
- const menuRef = useRef(null);
- const buttonRef = useRef(null);
-
- const calculatePosition = () => {
- if (!buttonRef.current) return;
- const rect = buttonRef.current.getBoundingClientRect();
- const menuWidth = 220;
- const menuHeight = 300;
- const padding = 16;
-
- let left = rect.left + rect.width / 2 - menuWidth / 2;
- let top = rect.bottom + 8;
-
- if (left < padding) {
- left = padding;
- } else if (left + menuWidth > window.innerWidth - padding) {
- left = window.innerWidth - menuWidth - padding;
- }
-
- if (top + menuHeight > window.innerHeight - padding) {
- top = rect.top - menuHeight - 8;
- if (top < padding) top = padding;
- }
-
- setMenuPosition({ top, left });
- };
-
- const handleToggle = () => {
- if (!isOpen) {
- calculatePosition();
- }
- setIsOpen(!isOpen);
- };
-
- const getShareUrl = () => {
- if (customUrl) return customUrl;
- if (!uri) return "";
-
- const uriParts = uri.split("/");
- const rkey = uriParts[uriParts.length - 1];
- const did = uriParts[2];
-
- if (uri.includes("network.cosmik.card")) {
- return `${window.location.origin}/at/${did}/${rkey}`;
- }
-
- if (handle && type) {
- return `${window.location.origin}/${handle}/${type.toLowerCase()}/${rkey}`;
- }
-
- return `${window.location.origin}/at/${did}/${rkey}`;
- };
-
- const shareUrl = getShareUrl();
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (
- menuRef.current &&
- !menuRef.current.contains(e.target) &&
- buttonRef.current &&
- !buttonRef.current.contains(e.target)
- ) {
- setIsOpen(false);
- }
- };
-
- const card = buttonRef.current?.closest(
- ".card, .annotation-card, .bookmark-card",
- );
- if (card) {
- if (isOpen) {
- card.classList.add("has-open-menu");
- } else {
- card.classList.remove("has-open-menu");
- }
- }
-
- if (isOpen) {
- document.addEventListener("mousedown", handleClickOutside);
- window.addEventListener("scroll", calculatePosition, true);
- window.addEventListener("resize", calculatePosition);
- }
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- window.removeEventListener("scroll", calculatePosition, true);
- window.removeEventListener("resize", calculatePosition);
- if (card) {
- card.classList.remove("has-open-menu");
- }
- };
- }, [isOpen]);
-
- const handleShareToFork = (domain) => {
- const composeText = text
- ? `${text.substring(0, 200)}...\n\n${shareUrl}`
- : shareUrl;
- const composeUrl = `https://${domain}/intent/compose?text=${encodeURIComponent(composeText)}`;
- window.open(composeUrl, "_blank");
- setIsOpen(false);
- };
-
- const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(shareUrl);
- setCopied(true);
- setTimeout(() => {
- setCopied(false);
- setIsOpen(false);
- }, 1500);
- } catch {
- prompt("Copy this link:", shareUrl);
- }
- };
-
- const handleCopyAturi = async () => {
- const aturiUrl = uri ? uri.replace("at://", "https://aturi.to/") : "";
- if (!aturiUrl) return;
-
- try {
- await navigator.clipboard.writeText(aturiUrl);
- setCopiedAturi(true);
- setTimeout(() => {
- setCopiedAturi(false);
- setIsOpen(false);
- }, 1500);
- } catch {
- prompt("Copy this link:", aturiUrl);
- }
- };
-
- const handleSystemShare = async () => {
- if (navigator.share) {
- try {
- await navigator.share({
- title: "Margin Annotation",
- text: text?.substring(0, 100),
- url: shareUrl,
- });
- } catch {
- /* ignore */
- }
- }
- setIsOpen(false);
- };
-
- const isSemble = uri && uri.includes("network.cosmik");
- const sembleUrl = (() => {
- if (!isSemble) return "";
- const parts = uri.split("/");
- const rkey = parts[parts.length - 1];
- const userHandle = handle || (parts.length > 2 ? parts[2] : "");
-
- if (uri.includes("network.cosmik.collection")) {
- return `https://semble.so/profile/${userHandle}/collections/${rkey}`;
- }
-
- if (uri.includes("network.cosmik.card") && url) {
- return `https://semble.so/url?id=${encodeURIComponent(url)}`;
- }
-
- return `https://semble.so/profile/${userHandle}`;
- })();
-
- const handleCopySemble = async () => {
- try {
- await navigator.clipboard.writeText(sembleUrl);
- setCopied(true);
- setTimeout(() => {
- setCopied(false);
- setIsOpen(false);
- }, 1500);
- } catch {
- prompt("Copy this link:", sembleUrl);
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {isOpen && (
-
- {isSemble ? (
- <>
-
-
-
- Semble
-
-
-
- Open on Semble
-
-
- {copied ? : }
- {copied ? "Copied!" : "Copy Semble Link"}
-
-
-
-
- {copiedAturi ? : }
- {copiedAturi ? "Copied!" : "Copy Universal URL"}
-
- >
- ) : (
- <>
-
-
Share to
- {BLUESKY_FORKS.map((fork) => (
-
handleShareToFork(fork.domain)}
- >
-
-
-
- {fork.name}
-
- ))}
-
-
-
- {copied ? : }
- {copied ? "Copied!" : "Copy Link"}
-
-
- {copiedAturi ? : }
- {copiedAturi ? "Copied!" : "Copy Universal Link"}
-
- {navigator.share && (
-
-
- More...
-
- )}
- >
- )}
-
- )}
-
- );
-}
diff --git a/web/src/components/SignUpModal.jsx b/web/src/components/SignUpModal.jsx
deleted file mode 100644
index 88406f6..0000000
--- a/web/src/components/SignUpModal.jsx
+++ /dev/null
@@ -1,267 +0,0 @@
-import { useState, useEffect } from "react";
-import { X, ChevronRight, Loader2, AlertCircle } from "lucide-react";
-import {
- BlackskyIcon,
- NorthskyIcon,
- BlueskyIcon,
- TophhieIcon,
- MarginIcon,
-} from "./Icons";
-import { startSignup } from "../api/client";
-
-const RECOMMENDED_PROVIDER = {
- id: "margin",
- name: "Margin",
- service: "https://margin.cafe",
- Icon: MarginIcon,
- description: "Hosted by Margin, the easiest way to get started",
-};
-
-const OTHER_PROVIDERS = [
- {
- id: "bluesky",
- name: "Bluesky",
- service: "https://bsky.social",
- Icon: BlueskyIcon,
- description: "The most popular option on the AT Protocol",
- },
- {
- id: "blacksky",
- name: "Blacksky",
- service: "https://blacksky.app",
- Icon: BlackskyIcon,
- description: "For the Culture. A safe space for Black users and allies",
- },
- {
- id: "selfhosted.social",
- name: "selfhosted.social",
- service: "https://selfhosted.social",
- Icon: null,
- description:
- "For hackers, designers, developers, ATProto enthusiasts, scrobblers, tinkerers, friends, and curious minds.",
- },
- {
- id: "northsky",
- name: "Northsky",
- service: "https://northsky.social",
- Icon: NorthskyIcon,
- description: "A Canadian-based worker-owned cooperative",
- },
- {
- id: "tophhie",
- name: "Tophhie",
- service: "https://tophhie.social",
- Icon: TophhieIcon,
- description: "A welcoming and friendly community",
- },
- {
- id: "altq",
- name: "AltQ",
- service: "https://altq.net",
- Icon: null,
- description: "An independent, self-hosted PDS instance",
- },
- {
- id: "custom",
- name: "Custom",
- service: "",
- custom: true,
- Icon: null,
- description: "Connect to your own or another custom PDS",
- },
-];
-
-export default function SignUpModal({ onClose }) {
- const [showOtherProviders, setShowOtherProviders] = useState(false);
- const [showCustomInput, setShowCustomInput] = useState(false);
- const [customService, setCustomService] = useState("");
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- document.body.style.overflow = "hidden";
- return () => {
- document.body.style.overflow = "unset";
- };
- }, []);
-
- const handleProviderSelect = async (provider) => {
- if (provider.custom) {
- setShowCustomInput(true);
- return;
- }
-
- setLoading(true);
- setError(null);
-
- try {
- const result = await startSignup(provider.service);
- if (result.authorizationUrl) {
- window.location.href = result.authorizationUrl;
- }
- } catch (err) {
- console.error(err);
- setError("Could not connect to this provider. Please try again.");
- setLoading(false);
- }
- };
-
- const handleCustomSubmit = async (e) => {
- e.preventDefault();
- if (!customService.trim()) return;
-
- setLoading(true);
- setError(null);
-
- let serviceUrl = customService.trim();
- if (!serviceUrl.startsWith("http")) {
- serviceUrl = `https://${serviceUrl}`;
- }
-
- try {
- const result = await startSignup(serviceUrl);
- if (result.authorizationUrl) {
- window.location.href = result.authorizationUrl;
- }
- } catch (err) {
- console.error(err);
- setError("Could not connect to this PDS. Please check the URL.");
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
-
-
- {loading ? (
-
-
-
- Connecting to provider...
-
-
- ) : showCustomInput ? (
-
-
Custom Provider
-
-
-
- PDS address (e.g. pds.example.com)
-
- setCustomService(e.target.value)}
- placeholder="pds.example.com"
- autoFocus
- />
-
-
- {error && (
-
- )}
-
-
- {
- setShowCustomInput(false);
- setError(null);
- }}
- >
- Back
-
-
- Continue
-
-
-
-
- ) : (
-
-
Create your account
-
- Margin uses the AT Protocol, the same decentralized network that
- powers Bluesky. Your account will be hosted on a server of your
- choice.
-
-
- {error && (
-
- )}
-
-
-
Recommended
-
handleProviderSelect(RECOMMENDED_PROVIDER)}
- >
-
-
-
-
-
{RECOMMENDED_PROVIDER.name}
- {RECOMMENDED_PROVIDER.description}
-
-
-
-
-
-
setShowOtherProviders(!showOtherProviders)}
- >
- {showOtherProviders ? "Hide other options" : "More options"}
-
-
-
- {showOtherProviders && (
-
- {OTHER_PROVIDERS.map((p) => (
-
handleProviderSelect(p)}
- >
-
- {p.Icon ? (
-
- ) : (
-
{p.name[0]}
- )}
-
-
-
{p.name}
- {p.description}
-
-
-
- ))}
-
- )}
-
- )}
-
-
- );
-}
diff --git a/web/src/components/TopNav.jsx b/web/src/components/TopNav.jsx
deleted file mode 100644
index d2e7e8c..0000000
--- a/web/src/components/TopNav.jsx
+++ /dev/null
@@ -1,425 +0,0 @@
-import { useState, useRef, useEffect } from "react";
-import { Link, useLocation } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import { useTheme } from "../context/ThemeContext";
-import {
- Home,
- Search,
- Folder,
- Bell,
- PenSquare,
- User,
- Settings,
- LogOut,
- ChevronDown,
- Highlighter,
- Bookmark,
- Sun,
- Moon,
- Monitor,
- ExternalLink,
- Menu,
- X,
-} from "lucide-react";
-import {
- SiFirefox,
- SiGooglechrome,
- SiGithub,
- SiBluesky,
- SiDiscord,
-} from "react-icons/si";
-import { FaEdge } from "react-icons/fa";
-import tangledLogo from "../assets/tangled.svg";
-import { getUnreadNotificationCount } from "../api/client";
-
-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",
- };
-}
-
-export default function TopNav() {
- const { user, isAuthenticated, logout, loading } = useAuth();
- const { theme, setTheme } = useTheme();
- const location = useLocation();
- const [userMenuOpen, setUserMenuOpen] = useState(false);
- const [moreMenuOpen, setMoreMenuOpen] = useState(false);
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
- const [unreadCount, setUnreadCount] = useState(0);
- const userMenuRef = useRef(null);
- const moreMenuRef = useRef(null);
-
- const isActive = (path) => {
- if (path === "/") return location.pathname === "/";
- return location.pathname.startsWith(path);
- };
-
- const ext = getExtensionInfo();
- const ExtIcon = ext.icon;
-
- useEffect(() => {
- if (isAuthenticated) {
- getUnreadNotificationCount()
- .then((data) => setUnreadCount(data.count || 0))
- .catch(() => {});
- const interval = setInterval(() => {
- getUnreadNotificationCount()
- .then((data) => setUnreadCount(data.count || 0))
- .catch(() => {});
- }, 60000);
- return () => clearInterval(interval);
- }
- }, [isAuthenticated]);
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (userMenuRef.current && !userMenuRef.current.contains(e.target)) {
- setUserMenuOpen(false);
- }
- if (moreMenuRef.current && !moreMenuRef.current.contains(e.target)) {
- setMoreMenuOpen(false);
- }
- };
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, []);
-
- const closeMobileMenu = () => setMobileMenuOpen(false);
-
- const getInitials = () => {
- if (user?.displayName)
- return user.displayName.substring(0, 2).toUpperCase();
- if (user?.handle) return user.handle.substring(0, 2).toUpperCase();
- return "U";
- };
-
- const cycleTheme = () => {
- const next =
- theme === "system" ? "light" : theme === "light" ? "dark" : "system";
- setTheme(next);
- };
-
- return (
-
- );
-}
diff --git a/web/src/components/UserMeta.jsx b/web/src/components/UserMeta.jsx
deleted file mode 100644
index e134a48..0000000
--- a/web/src/components/UserMeta.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Link } from "react-router-dom";
-
-const formatDate = (dateString, simple = true) => {
- if (!dateString) return "";
- const date = new Date(dateString);
- const now = new Date();
- const diff = now - date;
- const minutes = Math.floor(diff / 60000);
- const hours = Math.floor(diff / 3600000);
- const days = Math.floor(diff / 86400000);
- if (minutes < 1) return "just now";
- if (minutes < 60) return `${minutes}m`;
- if (hours < 24) return `${hours}h`;
- if (days < 7) return `${days}d`;
- if (simple)
- return date.toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- });
- return date.toLocaleString();
-};
-
-export default function UserMeta({ author, createdAt }) {
- const authorDisplayName = author?.displayName || author?.handle || "Unknown";
- const authorHandle = author?.handle;
- const authorAvatar = author?.avatar;
- const authorDid = author?.did;
- const marginProfileUrl = authorDid ? `/profile/${authorDid}` : "#";
-
- return (
- <>
-
-
- {authorAvatar ? (
-
- ) : (
-
- {authorDisplayName?.substring(0, 2).toUpperCase() || "??"}
-
- )}
-
-
-
-
-
{formatDate(createdAt)}
-
- >
- );
-}
diff --git a/web/src/context/AuthContext.jsx b/web/src/context/AuthContext.jsx
deleted file mode 100644
index 7a78dc3..0000000
--- a/web/src/context/AuthContext.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { useState, createContext, useContext, useEffect } from "react";
-import { getSession, logout } from "../api/client";
-
-const AuthContext = createContext(null);
-
-export function AuthProvider({ children }) {
- const [user, setUser] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- checkSession();
- }, []);
-
- const checkSession = async () => {
- try {
- const data = await getSession();
- if (data.authenticated) {
- let avatar = null;
- let displayName = null;
- try {
- const profileRes = await fetch(
- `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(data.did)}`,
- );
- if (profileRes.ok) {
- const profile = await profileRes.json();
- avatar = profile.avatar;
- displayName = profile.displayName;
- }
- } catch (e) {
- console.error("Failed to fetch profile:", e);
- }
- setUser({
- did: data.did,
- handle: data.handle,
- avatar,
- displayName: displayName || data.handle,
- });
- } else {
- setUser(null);
- }
- } catch {
- setUser(null);
- } finally {
- setLoading(false);
- }
- };
-
- const handleLogout = async () => {
- try {
- await logout();
- } catch (e) {
- console.warn("Logout failed", e);
- }
- setUser(null);
- };
-
- const value = {
- user,
- loading,
- isAuthenticated: !!user,
- login: () => (window.location.href = "/login"),
- logout: handleLogout,
- refresh: checkSession,
- };
-
- return {children} ;
-}
-
-// eslint-disable-next-line react-refresh/only-export-components
-export function useAuth() {
- const context = useContext(AuthContext);
- if (!context) {
- throw new Error("useAuth must be used within AuthProvider");
- }
- return context;
-}
diff --git a/web/src/context/ThemeContext.jsx b/web/src/context/ThemeContext.jsx
deleted file mode 100644
index 3afaad2..0000000
--- a/web/src/context/ThemeContext.jsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { createContext, useContext, useEffect, useState } from "react";
-
-const ThemeContext = createContext({
- theme: "system",
- setTheme: () => null,
- layout: "sidebar",
- setLayout: () => null,
-});
-
-export function ThemeProvider({ children }) {
- const [theme, setTheme] = useState(() => {
- return localStorage.getItem("theme") || "system";
- });
- const [layout, setLayout] = useState(() => {
- return localStorage.getItem("layout_preference") || "sidebar";
- });
-
- useEffect(() => {
- localStorage.setItem("theme", theme);
-
- const root = window.document.documentElement;
- root.classList.remove("light", "dark");
-
- delete root.dataset.theme;
-
- if (theme === "system") {
- const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
- .matches
- ? "dark"
- : "light";
-
- if (systemTheme === "light") {
- root.dataset.theme = "light";
- } else {
- root.dataset.theme = "dark";
- }
- return;
- }
-
- if (theme === "light") {
- root.dataset.theme = "light";
- }
- }, [theme]);
-
- useEffect(() => {
- if (theme !== "system") return;
-
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
- const handleChange = () => {
- const root = window.document.documentElement;
- if (mediaQuery.matches) {
- delete root.dataset.theme;
- } else {
- root.dataset.theme = "light";
- }
- };
-
- mediaQuery.addEventListener("change", handleChange);
- return () => mediaQuery.removeEventListener("change", handleChange);
- }, [theme]);
-
- useEffect(() => {
- localStorage.setItem("layout_preference", layout);
- }, [layout]);
-
- const value = {
- theme,
- setTheme: (newTheme) => {
- setTheme(newTheme);
- },
- layout,
- setLayout: (newLayout) => {
- setLayout(newLayout);
- },
- };
-
- return (
- {children}
- );
-}
-
-// eslint-disable-next-line react-refresh/only-export-components
-export function useTheme() {
- const context = useContext(ThemeContext);
- if (context === undefined)
- throw new Error("useTheme must be used within a ThemeProvider");
- return context;
-}
diff --git a/web/src/css/annotations.css b/web/src/css/annotations.css
deleted file mode 100644
index 1dc6022..0000000
--- a/web/src/css/annotations.css
+++ /dev/null
@@ -1,560 +0,0 @@
-.annotation-detail-page {
- max-width: 640px;
- margin: 0 auto;
- min-height: 100vh;
-}
-
-.annotation-detail-header {
- margin-bottom: var(--spacing-md);
-}
-
-.back-link {
- display: inline-flex;
- align-items: center;
- color: var(--text-tertiary);
- text-decoration: none;
- font-size: 0.8rem;
- font-weight: 500;
- transition: color 0.15s;
-}
-
-.back-link:hover {
- color: var(--text-primary);
-}
-
-.replies-section {
- margin-top: var(--spacing-lg);
- border-top: 1px solid var(--border);
- padding-top: var(--spacing-md);
-}
-
-.replies-title {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.9rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: var(--spacing-md);
-}
-
-.annotation-card {
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding: 16px 20px;
- transition: all 0.15s ease;
- width: 100%;
- box-sizing: border-box;
- overflow: visible;
- background: var(--bg-primary);
- border: none;
- position: relative;
-}
-
-.feed > .annotation-card,
-.feed > .card {
- border-radius: var(--radius-lg);
- border: 1px solid var(--border);
- background: var(--bg-card) !important;
- overflow: visible;
-}
-
-/*
-.feed > .annotation-card:first-child,
-.feed > .card:first-child {
- border-top-left-radius: var(--radius-lg) !important;
- border-top-right-radius: var(--radius-lg) !important;
-}
-
-.feed > .annotation-card:last-child,
-.feed > .card:last-child {
- border-bottom-left-radius: var(--radius-lg) !important;
- border-bottom-right-radius: var(--radius-lg) !important;
-}
-
-.feed > .annotation-card:only-child,
-.feed > .card:only-child {
- border-radius: var(--radius-lg) !important;
-}
-*/
-
-.annotation-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: var(--spacing-sm);
-}
-
-.annotation-header-left {
- display: flex;
- align-items: center;
- gap: 8px;
- flex: 1;
- min-width: 0;
-}
-
-.annotation-avatar {
- width: 32px;
- height: 32px;
- min-width: 32px;
- border-radius: var(--radius-full);
- background: var(--bg-tertiary);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 0.75rem;
- color: var(--text-secondary);
- overflow: hidden;
-}
-
-.annotation-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.annotation-meta {
- display: flex;
- flex-direction: column;
- justify-content: center;
- line-height: 1.4;
- min-width: 0;
- flex: 1;
-}
-
-.annotation-avatar-link {
- text-decoration: none;
- border-radius: var(--radius-full);
-}
-
-.annotation-author-row {
- display: flex;
- align-items: baseline;
- gap: 8px;
- flex-wrap: wrap;
-}
-
-.annotation-author {
- font-weight: 600;
- color: var(--text-primary);
- font-size: 0.875rem;
-}
-
-.annotation-handle {
- font-size: 0.8rem;
- color: var(--text-tertiary);
- text-decoration: none;
-}
-
-.annotation-handle:hover {
- color: var(--text-secondary);
-}
-
-.annotation-time {
- font-size: 0.75rem;
- color: var(--text-tertiary);
-}
-
-.annotation-content {
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding-left: 0;
- max-width: 100%;
- overflow: visible;
-}
-
-.annotation-source {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 0.8rem;
- color: var(--accent);
- text-decoration: none;
- transition: color 0.15s ease;
- max-width: 100%;
- overflow: hidden;
-}
-
-.annotation-source:hover {
- text-decoration: underline;
-}
-
-.annotation-source-title {
- color: var(--text-primary);
- font-weight: 500;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.annotation-highlight {
- display: block;
- position: relative;
- padding: 10px 14px;
- margin: 0;
- text-decoration: none;
- background: var(--bg-tertiary);
- border-left: 3px solid var(--accent);
- border-radius: 0 var(--radius-md) var(--radius-md) 0;
- transition: all 0.15s ease;
- max-width: 100%;
- overflow: hidden;
-}
-
-.annotation-highlight:hover {
- background: var(--bg-hover);
-}
-
-.annotation-highlight mark {
- background: transparent;
- color: var(--text-primary);
- font-style: italic;
- font-size: 0.875rem;
- line-height: 1.5;
- font-weight: 400;
- display: block;
- overflow-wrap: break-word;
- word-break: break-word;
-}
-
-.annotation-text {
- font-size: 1rem;
- line-height: 1.7;
- color: var(--text-primary);
- white-space: pre-wrap;
-}
-
-.annotation-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-top: 2px;
-}
-
-.annotation-tag {
- font-size: 0.75rem;
- color: var(--accent);
- text-decoration: none;
- font-weight: 500;
- transition: opacity 0.15s;
-}
-
-.annotation-tag:hover {
- opacity: 0.8;
- text-decoration: underline;
-}
-
-.annotation-actions {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- gap: 4px;
- padding-left: 0;
- margin-top: 4px;
- position: relative;
-}
-
-.annotation-actions-left {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.annotation-action {
- display: flex;
- align-items: center;
- gap: 5px;
- color: var(--text-tertiary);
- font-size: 0.8rem;
- font-weight: 500;
- padding: 6px 10px;
- border-radius: var(--radius-md);
- transition: all 0.15s ease;
- background: transparent;
- cursor: pointer;
- border: none;
-}
-
-.annotation-action:hover {
- color: var(--text-secondary);
- background: var(--bg-tertiary);
-}
-
-.annotation-action.liked {
- color: var(--error);
-}
-
-.annotation-action.liked svg {
- fill: var(--error);
-}
-
-.annotation-action.active {
- color: var(--accent);
-}
-
-.action-icon-only {
- padding: 4px;
-}
-
-.annotation-header-right {
- opacity: 0;
- transition: opacity 0.15s;
-}
-
-.annotation-card:hover .annotation-header-right {
- opacity: 1;
-}
-
-.inline-replies {
- margin-top: var(--spacing-sm);
- padding-left: 0;
- position: relative;
-}
-
-.annotation-text,
-.reply-text,
-.history-content {
- overflow-wrap: break-word;
- word-break: break-word;
- max-width: 100%;
-}
-
-.annotation-header-left,
-.annotation-meta,
-.reply-meta {
- min-width: 0;
- max-width: 100%;
-}
-
-.annotation-author-row,
-.reply-author {
- max-width: 100%;
-}
-
-@media (max-width: 768px) {
- .annotation-content,
- .annotation-actions,
- .inline-replies {
- padding-left: 0;
- }
-
- .annotation-header-right {
- opacity: 1;
- }
-
- .annotation-card {
- padding: 16px;
- }
-
- .annotation-avatar {
- width: 36px;
- height: 36px;
- min-width: 36px;
- }
-}
-
-.replies-list-threaded {
- margin-top: var(--spacing-md);
- display: flex;
- flex-direction: column;
-}
-
-.reply-card-threaded {
- position: relative;
- padding-left: 0;
- padding: var(--spacing-sm) 0;
- transition: background 0.15s;
-}
-
-.reply-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 4px;
-}
-
-.reply-avatar {
- width: 28px;
- height: 28px;
- border-radius: var(--radius-full);
- background: var(--bg-tertiary);
- overflow: hidden;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.reply-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.reply-avatar span {
- font-size: 0.7rem;
- font-weight: 600;
- color: var(--text-secondary);
-}
-
-.reply-meta {
- display: flex;
- align-items: baseline;
- gap: 8px;
- flex: 1;
- min-width: 0;
-}
-
-.reply-author {
- font-weight: 600;
- font-size: 0.875rem;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.reply-handle {
- font-size: 0.8rem;
- color: var(--text-tertiary);
- text-decoration: none;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.reply-time {
- font-size: 0.8rem;
- color: var(--text-tertiary);
- white-space: nowrap;
-}
-
-.reply-dot {
- color: var(--text-tertiary);
- font-size: 0.7rem;
-}
-
-.reply-text {
- font-size: 0.9rem;
- line-height: 1.5;
- color: var(--text-primary);
- margin: 0;
- padding-left: 36px;
-}
-
-.reply-actions {
- display: flex;
- align-items: center;
- gap: 4px;
- opacity: 0;
- transition: opacity 0.15s;
-}
-
-.reply-card-threaded:hover .reply-actions {
- opacity: 1;
-}
-
-.reply-action-btn {
- background: none;
- border: none;
- padding: 4px;
- color: var(--text-tertiary);
- cursor: pointer;
- border-radius: var(--radius-sm);
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.reply-action-btn:hover {
- background: var(--bg-tertiary);
- color: var(--text-secondary);
-}
-
-.reply-action-delete:hover {
- color: var(--error);
- background: rgba(255, 69, 58, 0.1);
-}
-
-.reply-form {
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: var(--spacing-md);
- background: var(--bg-secondary);
- margin-bottom: var(--spacing-md);
-}
-
-.replying-to-banner {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: var(--bg-tertiary);
- padding: 6px 10px;
- border-radius: var(--radius-sm);
- margin-bottom: var(--spacing-sm);
- font-size: 0.8rem;
- color: var(--text-secondary);
-}
-
-.cancel-reply {
- background: none;
- border: none;
- color: var(--text-tertiary);
- cursor: pointer;
- font-size: 1rem;
- padding: 0 4px;
- line-height: 1;
-}
-
-.cancel-reply:hover {
- color: var(--text-primary);
-}
-
-.reply-input {
- width: 100%;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- padding: 10px 12px;
- color: var(--text-primary);
- font-family: inherit;
- font-size: 0.875rem;
- resize: vertical;
- min-height: 60px;
- transition: border-color 0.15s;
- display: block;
- box-sizing: border-box;
-}
-
-.reply-input:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.reply-form-actions {
- display: flex;
- justify-content: flex-end;
- margin-top: var(--spacing-sm);
-}
-
-.rich-text-link {
- color: var(--accent);
- text-decoration: none;
-}
-
-.rich-text-link:hover {
- text-decoration: underline;
-}
-
-.rich-text-mention {
- color: var(--accent);
- font-weight: 500;
- text-decoration: none;
-}
-
-.rich-text-mention:hover {
- text-decoration: underline;
-}
diff --git a/web/src/css/base.css b/web/src/css/base.css
deleted file mode 100644
index 825adef..0000000
--- a/web/src/css/base.css
+++ /dev/null
@@ -1,224 +0,0 @@
-@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap");
-
-:root {
- --bg-primary: #0a0a0d;
- --bg-secondary: #121216;
- --bg-tertiary: #1a1a1f;
- --bg-card: #0f0f13;
- --bg-elevated: #18181d;
- --bg-hover: #1e1e24;
-
- --glass-border: rgba(234, 234, 238, 0.08);
- --glass-bg: rgba(10, 10, 13, 0.92);
-
- --text-primary: #eaeaee;
- --text-secondary: #b7b6c5;
- --text-tertiary: #6e6d7a;
-
- --border: rgba(183, 182, 197, 0.12);
- --border-hover: rgba(183, 182, 197, 0.2);
- --accent: #957a86;
- --accent-hover: #a98d98;
- --accent-subtle: rgba(149, 122, 134, 0.15);
- --accent-text: #c4a8b2;
-
- --success: #7fb069;
- --error: #d97766;
- --warning: #e8a54b;
- --info: #6eb5ff;
-
- --radius-xs: 4px;
- --radius-sm: 6px;
- --radius-md: 8px;
- --radius-lg: 12px;
- --radius-xl: 16px;
- --radius-full: 9999px;
-
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
- --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
- --shadow-glow: 0 0 20px rgba(149, 122, 134, 0.2);
-
- --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
- --font-display: "IBM Plex Sans", sans-serif;
- --font-mono: "IBM Plex Mono", monospace;
-
- --nav-bg: rgba(10, 10, 13, 0.95);
-
- --sidebar-width: 200px;
- --right-sidebar-width: 260px;
- --content-max-width: 600px;
- --spacing-xs: 4px;
- --spacing-sm: 8px;
- --spacing-md: 12px;
- --spacing-lg: 20px;
- --spacing-xl: 28px;
-}
-
-[data-theme="light"] {
- --bg-primary: #f8f8fa;
- --bg-secondary: #ffffff;
- --bg-tertiary: #f0f0f4;
- --bg-card: #ffffff;
- --bg-elevated: #ffffff;
- --bg-hover: #eeeef2;
-
- --glass-border: rgba(92, 73, 90, 0.1);
- --glass-bg: rgba(248, 248, 250, 0.95);
-
- --text-primary: #18171c;
- --text-secondary: #5c495a;
- --text-tertiary: #8a8494;
-
- --border: rgba(92, 73, 90, 0.12);
- --border-hover: rgba(92, 73, 90, 0.22);
-
- --accent: #7a5f6d;
- --accent-hover: #664e5b;
- --accent-subtle: rgba(149, 122, 134, 0.12);
- --accent-text: #5c495a;
-
- --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
- --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
- --shadow-lg: 0 8px 24px rgba(92, 73, 90, 0.1);
- --shadow-glow: 0 0 20px rgba(149, 122, 134, 0.1);
-
- --nav-bg: rgba(255, 255, 255, 0.95);
-}
-
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-html {
- font-size: 16px;
- -webkit-text-size-adjust: 100%;
- overflow-x: hidden;
-}
-
-body {
- font-family: var(--font-sans);
- background: var(--bg-primary);
- color: var(--text-primary);
- line-height: 1.55;
- min-height: 100vh;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- overflow-x: hidden;
- max-width: 100vw;
- font-size: 0.9375rem;
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- font-family: var(--font-display);
- font-weight: 600;
- letter-spacing: -0.02em;
- color: var(--text-primary);
- line-height: 1.3;
-}
-
-h1 {
- font-size: 1.5rem;
-}
-h2 {
- font-size: 1.25rem;
-}
-h3 {
- font-size: 1.1rem;
-}
-
-p {
- color: var(--text-secondary);
- line-height: 1.6;
-}
-
-a {
- color: inherit;
- text-decoration: none;
- transition: color 0.2s ease;
-}
-
-button {
- font-family: inherit;
- cursor: pointer;
- border: none;
- background: none;
-}
-
-input,
-textarea,
-select {
- font-family: inherit;
- font-size: inherit;
- color: var(--text-primary);
-}
-
-::selection {
- background: var(--accent-subtle);
- color: var(--accent-text);
-}
-
-::-webkit-scrollbar {
- width: 10px;
- height: 10px;
-}
-
-::-webkit-scrollbar-track {
- background: var(--bg-secondary);
-}
-
-::-webkit-scrollbar-thumb {
- background: var(--bg-hover);
- border-radius: var(--radius-full);
- border: 2px solid var(--bg-secondary);
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: var(--text-tertiary);
-}
-
-:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 3px;
-}
-
-.text-sm {
- font-size: 0.9rem;
-}
-
-.text-xs {
- font-size: 0.8rem;
-}
-
-.font-medium {
- font-weight: 500;
-}
-
-.font-semibold {
- font-weight: 600;
-}
-
-.font-mono {
- font-family: var(--font-mono);
-}
-
-.text-muted {
- color: var(--text-secondary);
-}
-
-.text-faint {
- color: var(--text-tertiary);
-}
-
-.card {
- background: var(--bg-card);
- border-radius: var(--radius-lg);
- border: 1px solid var(--border);
-}
diff --git a/web/src/css/buttons.css b/web/src/css/buttons.css
deleted file mode 100644
index a645bd1..0000000
--- a/web/src/css/buttons.css
+++ /dev/null
@@ -1,143 +0,0 @@
-.btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- padding: 8px 16px;
- font-size: 0.85rem;
- font-weight: 500;
- border-radius: var(--radius-md);
- transition: all 0.15s ease;
- white-space: nowrap;
- border: none;
- cursor: pointer;
-}
-
-.btn-primary {
- background: var(--accent);
- color: white;
-}
-
-.btn-primary:hover {
- background: var(--accent-hover);
- box-shadow: var(--shadow-glow);
-}
-
-.btn-secondary {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border);
-}
-
-.btn-secondary:hover {
- background: var(--bg-hover);
- border-color: var(--border-hover);
-}
-
-.btn-ghost {
- color: var(--text-secondary);
- padding: 8px 12px;
- background: transparent;
-}
-
-.btn-ghost:hover {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.btn-bluesky {
- background: #0085ff;
- color: white;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- transition: all 0.15s;
-}
-
-.btn-bluesky:hover {
- background: #0070dd;
-}
-
-.btn-sm {
- padding: 6px 12px;
- font-size: 0.8rem;
-}
-
-.btn-text {
- background: none;
- border: none;
- color: var(--text-secondary);
- font-size: 0.85rem;
- padding: 6px 10px;
- cursor: pointer;
- transition: color 0.15s;
- border-radius: var(--radius-sm);
-}
-
-.btn-text:hover {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.btn-block {
- width: 100%;
- text-align: left;
- padding: 10px 14px;
- color: var(--text-secondary);
- background: var(--bg-tertiary);
- border-radius: var(--radius-md);
- margin-top: 8px;
- font-size: 0.85rem;
- cursor: pointer;
- transition: all 0.15s;
- border: 1px solid transparent;
-}
-
-.btn-block:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- border-color: var(--border);
-}
-
-.btn-icon-danger {
- padding: 8px;
- background: rgba(255, 69, 58, 0.1);
- color: var(--error);
- border: none;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.15s ease;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.btn-icon-danger:hover {
- background: var(--error);
- color: white;
-}
-
-.btn-danger {
- background: rgba(255, 69, 58, 0.1);
- color: var(--error);
- border: 1px solid rgba(255, 69, 58, 0.2);
-}
-
-.btn-danger:hover {
- background: var(--error);
- color: white;
- border-color: var(--error);
-}
-
-.action-buttons {
- display: flex;
- gap: var(--spacing-sm);
- flex-wrap: wrap;
-}
-
-.action-buttons-end {
- display: flex;
- justify-content: flex-end;
- gap: var(--spacing-sm);
-}
diff --git a/web/src/css/cards.css b/web/src/css/cards.css
deleted file mode 100644
index f6a2826..0000000
--- a/web/src/css/cards.css
+++ /dev/null
@@ -1,278 +0,0 @@
-.card {
- background: var(--bg-primary);
- border: none;
- border-radius: 0;
- transition: all 0.15s ease;
- position: relative;
- overflow: visible;
-}
-
-.semble-badge {
- display: flex;
- align-items: center;
- gap: 4px;
- font-size: 0.75rem;
- color: var(--text-tertiary);
- margin-right: 4px;
-}
-
-.semble-badge img {
- width: 14px;
- height: 14px;
-}
-
-.bookmark-preview {
- display: block;
- padding: 14px 16px;
- background: linear-gradient(
- 135deg,
- var(--bg-tertiary) 0%,
- var(--bg-secondary) 100%
- );
- border: 1px solid var(--border);
- border-left: 3px solid var(--accent);
- border-radius: var(--radius-md);
- text-decoration: none;
- transition: all 0.2s ease;
- position: relative;
-}
-
-.bookmark-preview:hover {
- background: var(--bg-hover);
- border-left-color: var(--accent-hover);
-}
-
-.bookmark-preview-content {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.bookmark-preview-site {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.7rem;
- color: var(--text-tertiary);
- text-transform: uppercase;
- letter-spacing: 0.06em;
- font-weight: 500;
-}
-
-.bookmark-preview-site svg {
- color: var(--accent);
-}
-
-.bookmark-preview-title {
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--text-primary);
- line-height: 1.35;
- margin: 0;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.bookmark-preview-desc {
- font-size: 0.8rem;
- color: var(--text-secondary);
- line-height: 1.45;
- margin: 0;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.bookmark-card .annotation-content {
- padding-left: 0;
- overflow: visible;
-}
-
-.bookmark-card {
- overflow: visible !important;
-}
-
-.bookmark-card:hover {
- z-index: 10;
- overflow: visible !important;
-}
-
-.bookmark-card.has-open-menu,
-.annotation-card.has-open-menu,
-.card.has-open-menu {
- z-index: 200 !important;
-}
-
-.bookmark-site {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.8rem;
- color: var(--text-tertiary);
- text-transform: uppercase;
- letter-spacing: 0.02em;
-}
-
-.bookmark-title {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- line-height: 1.4;
- margin: 0;
-}
-
-.bookmark-desc {
- font-size: 0.875rem;
- color: var(--text-secondary);
- line-height: 1.5;
- margin: 0;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.edit-form {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.edit-textarea,
-.edit-input {
- width: 100%;
- padding: 10px 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-family: inherit;
- font-size: 0.9rem;
- transition: border-color 0.15s ease;
-}
-
-.edit-textarea {
- resize: vertical;
- min-height: 80px;
-}
-
-.edit-textarea:focus,
-.edit-input:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.edit-actions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
-}
-
-.color-edit-form {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 12px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
-}
-
-.color-picker-wrapper {
- position: relative;
- width: 28px;
- height: 28px;
- flex-shrink: 0;
-}
-
-.color-preview {
- width: 100%;
- height: 100%;
- border-radius: 50%;
- border: 2px solid var(--bg-card);
- box-shadow: 0 0 0 1px var(--border);
-}
-
-.color-input {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- cursor: pointer;
-}
-
-.color-edit-form .edit-input {
- margin: 0;
- flex: 1;
- padding: 6px 10px;
- height: 32px;
- border: none;
- background: transparent;
-}
-
-.btn-icon {
- padding: 0 10px;
- height: 32px;
- min-width: auto;
-}
-
-.history-panel {
- padding: 12px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
-}
-
-.history-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
-}
-
-.history-title {
- font-size: 0.9rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.history-status {
- font-size: 0.85rem;
- color: var(--text-tertiary);
- font-style: italic;
-}
-
-.history-list {
- list-style: none;
- padding: 0;
- margin: 0;
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.history-item {
- padding: 8px 10px;
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
-}
-
-.history-date {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- margin-bottom: 4px;
-}
-
-.history-content {
- font-size: 0.85rem;
- color: var(--text-secondary);
- line-height: 1.5;
-}
diff --git a/web/src/css/collections.css b/web/src/css/collections.css
deleted file mode 100644
index 44deffa..0000000
--- a/web/src/css/collections.css
+++ /dev/null
@@ -1,340 +0,0 @@
-.collection-feed-item {
- display: flex;
- flex-direction: column;
- background: var(--bg-primary);
- overflow: visible;
-}
-
-.collection-context-badge {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--spacing-sm);
- padding: 10px 20px;
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--border);
- border-top-left-radius: var(--radius-lg);
- border-top-right-radius: var(--radius-lg);
-}
-
-.collection-context-inner {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 0.8rem;
- color: var(--text-secondary);
-}
-
-.collection-context-avatar {
- width: 20px;
- height: 20px;
- border-radius: var(--radius-full);
- object-fit: cover;
-}
-
-.collection-context-text {
- display: flex;
- align-items: center;
- gap: 4px;
- flex-wrap: wrap;
-}
-
-.collection-context-author {
- font-weight: 600;
- color: var(--text-primary);
- text-decoration: none;
-}
-
-.collection-context-author:hover {
- text-decoration: underline;
-}
-
-.collection-context-link {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- font-weight: 600;
- color: var(--accent);
- text-decoration: none;
- background: var(--accent-subtle);
- padding: 2px 8px;
- border-radius: var(--radius-sm);
-}
-
-.collection-context-link:hover {
- background: var(--accent);
- color: var(--bg-primary);
-}
-
-.collections-list {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.collections-list > * {
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
-}
-
-.collection-row {
- display: flex;
- align-items: center;
- transition: background 0.15s ease;
-}
-
-.collection-row:hover {
- background: var(--bg-secondary);
-}
-
-.collection-row-content {
- flex: 1;
- display: flex;
- align-items: center;
- gap: var(--spacing-md);
- padding: var(--spacing-md);
- text-decoration: none;
- min-width: 0;
-}
-
-.collection-row-icon {
- width: 40px;
- height: 40px;
- min-width: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-tertiary);
- color: var(--accent);
- border-radius: var(--radius-md);
- transition: all 0.15s ease;
- font-size: 1.1rem;
-}
-
-.collection-row:hover .collection-row-icon {
- background: var(--accent-subtle);
-}
-
-.collection-row-info {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.collection-row-name {
- font-size: 0.9rem;
- font-weight: 600;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.collection-row-desc {
- font-size: 0.8rem;
- color: var(--text-secondary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.collection-row-arrow {
- color: var(--text-tertiary);
- opacity: 0;
- transition: opacity 0.15s;
-}
-
-.collection-row:hover .collection-row-arrow {
- opacity: 1;
-}
-
-.collection-row-edit {
- padding: 8px;
- margin-right: var(--spacing-sm);
- color: var(--text-tertiary);
- background: transparent;
- border-radius: var(--radius-sm);
- transition: all 0.15s;
- opacity: 0;
- border: none;
- cursor: pointer;
-}
-
-.collection-row:hover .collection-row-edit {
- opacity: 1;
-}
-
-.collection-row-edit:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
-}
-
-.collection-detail-header {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-md);
- padding: var(--spacing-lg);
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- margin-bottom: var(--spacing-lg);
- position: relative;
-}
-
-.collection-detail-icon {
- width: 56px;
- height: 56px;
- min-width: 56px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-tertiary);
- color: var(--accent);
- border-radius: var(--radius-lg);
- font-size: 1.5rem;
-}
-
-.collection-detail-info {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.collection-detail-visibility {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- font-size: 0.65rem;
- font-weight: 600;
- letter-spacing: 0.05em;
- text-transform: uppercase;
- color: var(--accent);
- padding: 2px 8px;
- background: var(--accent-subtle);
- border-radius: var(--radius-full);
- width: fit-content;
-}
-
-.collection-detail-title {
- font-family: var(--font-display);
- font-size: 1.5rem;
- font-weight: 700;
- color: var(--text-primary);
- line-height: 1.2;
- letter-spacing: -0.02em;
-}
-
-.collection-detail-desc {
- color: var(--text-secondary);
- font-size: 0.9rem;
- line-height: 1.5;
-}
-
-.collection-detail-stats {
- display: flex;
- align-items: center;
- gap: var(--spacing-md);
- font-size: 0.8rem;
- color: var(--text-tertiary);
- margin-top: var(--spacing-xs);
-}
-
-.collection-detail-actions {
- position: absolute;
- top: var(--spacing-md);
- right: var(--spacing-md);
- display: flex;
- gap: var(--spacing-xs);
-}
-
-.collection-detail-actions .annotation-action,
-.collection-detail-edit,
-.collection-detail-delete {
- padding: 6px;
- color: var(--text-tertiary);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- transition: all 0.15s;
- border: none;
- cursor: pointer;
-}
-
-.collection-detail-actions .annotation-action:hover,
-.collection-detail-edit:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.collection-detail-delete:hover {
- background: rgba(255, 69, 58, 0.1);
- color: var(--error);
-}
-
-.collection-list-item {
- width: 100%;
- text-align: left;
- padding: 12px 14px;
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- color: var(--text-primary);
- transition: all 0.15s;
- display: flex;
- align-items: center;
- justify-content: space-between;
- cursor: pointer;
- margin-bottom: var(--spacing-sm);
-}
-
-.collection-list-item:hover {
- background: var(--bg-hover);
- border-color: var(--accent);
-}
-
-.collection-list-item:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.collection-item-wrapper {
- position: relative;
-}
-
-.collection-item-remove {
- position: absolute;
- left: -40px;
- top: 20px;
- width: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- color: var(--text-tertiary);
- cursor: pointer;
- transition: all 0.15s ease;
- opacity: 0;
-}
-
-.collection-item-wrapper:hover .collection-item-remove {
- opacity: 1;
-}
-
-.collection-item-remove:hover {
- background: rgba(255, 69, 58, 0.1);
- border-color: rgba(255, 69, 58, 0.3);
- color: var(--error);
-}
-
-.collection-item-wrapper .card,
-.collection-feed-item .card {
- background: transparent !important;
- border: none !important;
- box-shadow: none;
- border-radius: 0;
-}
diff --git a/web/src/css/feed.css b/web/src/css/feed.css
deleted file mode 100644
index 5f987ee..0000000
--- a/web/src/css/feed.css
+++ /dev/null
@@ -1,445 +0,0 @@
-.feed-container {
- /* background: var(--bg-elevated); */
- /* border: 1px solid var(--border-hover); */
- /* border-radius: var(--radius-xl); */
- overflow: visible;
- /* padding: 8px; */
- position: relative;
-}
-
-.feed {
- display: flex;
- flex-direction: column;
- gap: 12px;
- width: 100%;
- overflow: visible;
- border-radius: var(--radius-lg);
- position: relative;
-}
-
-.feed-load-more {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 10px 24px;
- background: var(--bg-tertiary);
- border: none;
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-weight: 500;
- font-size: 0.9rem;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.feed-load-more:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.feed-load-more:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-.feed > * {
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- position: relative;
- overflow: visible;
-}
-
-.feed > *:last-child {
- border-bottom: 1px solid var(--border);
-}
-
-.feed > *.has-open-menu,
-.feed > *:focus-within {
- z-index: 200 !important;
- overflow: visible !important;
-}
-
-.feed-page {
- animation: fadeIn 0.3s ease-out;
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-.feed-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20px;
-}
-
-.feed-title {
- font-family: var(--font-display);
- font-size: 1.25rem;
- font-weight: 600;
- letter-spacing: -0.02em;
-}
-
-.feed-filters {
- display: flex;
- gap: 4px;
- margin-bottom: 20px;
- background: transparent;
- padding: 0;
- border: none;
- flex-wrap: wrap;
-}
-
-.filter-tab {
- padding: 8px 14px;
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--text-tertiary);
- background: transparent;
- border: none;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.filter-tab:hover {
- color: var(--text-secondary);
- background: var(--bg-tertiary);
-}
-
-.filter-tab.active {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.filter-pill {
- padding: 8px 14px;
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--text-secondary);
- background: var(--bg-tertiary);
- border: none;
- border-radius: var(--radius-full);
- cursor: pointer;
- transition: all 0.15s;
-}
-
-.filter-pill:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.filter-pill.active {
- background: var(--accent);
- color: var(--bg-primary);
-}
-
-.page-header {
- margin-bottom: 28px;
-}
-
-.page-title {
- font-family: var(--font-display);
- font-size: 2rem;
- font-weight: 700;
- margin-bottom: 8px;
- letter-spacing: -0.02em;
- color: var(--text-primary);
-}
-
-.page-description {
- color: var(--text-secondary);
- font-size: 1.1rem;
- line-height: 1.5;
-}
-
-.url-input-wrapper {
- margin-bottom: var(--spacing-lg);
- position: relative;
-}
-
-.url-input-container {
- display: flex;
- gap: var(--spacing-sm);
-}
-
-.url-input {
- width: 100%;
- padding: 12px 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 0.9rem;
- transition: all 0.15s ease;
-}
-
-.url-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.url-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.url-results-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: var(--spacing-md);
-}
-
-.back-link {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- color: var(--text-secondary);
- font-size: 0.8rem;
- font-weight: 500;
- text-decoration: none;
- margin-bottom: var(--spacing-lg);
- padding: 6px 12px;
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- transition: all 0.15s;
-}
-
-.back-link:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.url-target-info {
- display: flex;
- flex-direction: column;
- gap: 4px;
- padding: 12px 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- margin-bottom: var(--spacing-lg);
-}
-
-.url-target-label {
- font-size: 0.65rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- font-weight: 600;
- color: var(--text-tertiary);
-}
-
-.url-target-link {
- color: var(--accent);
- font-size: 0.85rem;
- font-weight: 500;
- text-decoration: none;
- word-break: break-all;
- line-height: 1.4;
-}
-
-.url-target-link:hover {
- text-decoration: underline;
-}
-
-.share-notes-banner {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--spacing-md);
- padding: 12px 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- margin-bottom: var(--spacing-md);
-}
-
-.share-notes-info {
- display: flex;
- align-items: center;
- gap: var(--spacing-sm);
- color: var(--text-primary);
- font-size: 0.85rem;
- font-weight: 500;
-}
-
-.share-notes-actions {
- display: flex;
- gap: var(--spacing-sm);
-}
-
-.empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 48px 24px;
- text-align: center;
-}
-
-.empty-state-icon {
- width: 56px;
- height: 56px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-tertiary);
- border-radius: var(--radius-lg);
- color: var(--text-tertiary);
- margin-bottom: 16px;
-}
-
-.empty-state-title {
- font-size: 1.1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 6px;
-}
-
-.empty-state-text {
- font-size: 0.9rem;
- color: var(--text-secondary);
- max-width: 300px;
- line-height: 1.5;
-}
-
-@media (max-width: 640px) {
- .feed-filters {
- gap: 4px;
- }
-
- .filter-tab,
- .filter-pill {
- padding: 6px 10px;
- font-size: 0.75rem;
- }
-}
-
-.feed-controls {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-sm);
- margin-bottom: var(--spacing-lg);
-}
-
-.active-filter-banner {
- display: inline-flex;
- align-items: center;
- gap: var(--spacing-sm);
- padding: 6px 10px 6px 12px;
- background: var(--accent-subtle);
- border: 1px solid var(--accent);
- border-radius: var(--radius-full);
- font-size: 0.8rem;
- color: var(--accent);
- margin-bottom: var(--spacing-md);
- width: fit-content;
-}
-
-.active-filter-banner strong {
- color: var(--accent-text);
-}
-
-.active-filter-clear {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- background: transparent;
- border: none;
- border-radius: var(--radius-full);
- color: var(--accent);
- cursor: pointer;
- transition: all 0.15s;
-}
-
-.active-filter-clear:hover {
- background: var(--accent);
- color: white;
-}
-
-.keyboard-hint {
- display: none;
- align-items: center;
- gap: 4px;
- font-size: 0.7rem;
- color: var(--text-tertiary);
- margin-left: auto;
-}
-
-@media (min-width: 768px) {
- .keyboard-hint {
- display: flex;
- }
-}
-
-.kbd {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 20px;
- height: 20px;
- padding: 0 6px;
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- border-radius: var(--radius-xs);
- font-size: 0.65rem;
- font-family: var(--font-mono);
- color: var(--text-secondary);
-}
-
-.back-to-top-btn {
- position: fixed;
- bottom: 24px;
- right: 24px;
- width: 44px;
- height: 44px;
- border-radius: var(--radius-full);
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- color: var(--text-secondary);
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- box-shadow: var(--shadow-md);
- transition: all 0.2s ease;
- z-index: 9999;
- opacity: 0;
- visibility: hidden;
- transform: translateY(10px);
-}
-
-.back-to-top-btn.has-sidebar {
- right: 24px;
-}
-
-@media (min-width: 1025px) {
- .back-to-top-btn.has-sidebar {
- right: 320px;
- }
-}
-
-.back-to-top-btn.visible {
- opacity: 1;
- visibility: visible;
- transform: translateY(0);
-}
-
-.back-to-top-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- border-color: var(--accent);
- transform: translateY(-2px);
- box-shadow: var(--shadow-lg);
-}
diff --git a/web/src/css/landing.css b/web/src/css/landing.css
deleted file mode 100644
index 0d4f7b0..0000000
--- a/web/src/css/landing.css
+++ /dev/null
@@ -1,925 +0,0 @@
-.landing-page {
- min-height: 100vh;
- background: var(--bg-primary);
-}
-
-.landing-nav {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 32px;
- max-width: 1200px;
- margin: 0 auto;
-}
-
-.landing-logo {
- display: flex;
- align-items: center;
- gap: 10px;
- text-decoration: none;
- color: var(--text-primary);
- font-weight: 600;
- font-size: 1.1rem;
-}
-
-.landing-logo img {
- width: 28px;
- height: 28px;
-}
-
-.landing-nav-links {
- display: flex;
- align-items: center;
- gap: 24px;
-}
-
-.landing-nav-links a:not(.btn) {
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.9rem;
- transition: color 0.15s;
-}
-
-.landing-nav-links a:not(.btn):hover {
- color: var(--text-primary);
-}
-
-.landing-hero {
- padding: 80px 32px 40px;
- max-width: 800px;
- margin: 0 auto;
- text-align: center;
-}
-
-.landing-hero-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 24px;
-}
-
-.landing-badge {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- font-size: 0.8rem;
- font-weight: 500;
- color: var(--accent);
- background: var(--accent-subtle);
- padding: 6px 14px;
- border-radius: var(--radius-full);
-}
-
-.landing-title {
- font-size: 3.5rem;
- font-weight: 700;
- line-height: 1.1;
- letter-spacing: -0.03em;
- color: var(--text-primary);
- margin: 0;
-}
-
-.landing-title-accent {
- color: var(--accent);
-}
-
-.landing-subtitle {
- font-size: 1.2rem;
- line-height: 1.7;
- color: var(--text-secondary);
- max-width: 580px;
- margin: 0;
-}
-
-.landing-cta {
- display: flex;
- gap: 12px;
- flex-wrap: wrap;
- justify-content: center;
- margin-top: 8px;
-}
-
-.btn-lg {
- padding: 10px 20px;
- font-size: 0.95rem;
-}
-
-.landing-browsers {
- font-size: 0.85rem;
- color: var(--text-tertiary);
- margin: 0;
-}
-
-.landing-browsers a {
- color: var(--text-secondary);
- text-decoration: underline;
- text-underline-offset: 2px;
-}
-
-.landing-browsers a:hover {
- color: var(--text-primary);
-}
-
-.landing-demo {
- padding: 40px 32px 80px;
- max-width: 1100px;
- margin: 0 auto;
-}
-
-.demo-window {
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-xl);
- overflow: hidden;
- box-shadow: var(--shadow-lg);
-}
-
-.demo-browser-bar {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 12px 16px;
- background: var(--bg-tertiary);
- border-bottom: 1px solid var(--border);
-}
-
-.demo-browser-dots {
- display: flex;
- gap: 6px;
-}
-
-.demo-browser-dots span {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- background: var(--border);
-}
-
-.demo-browser-url {
- flex: 1;
- background: var(--bg-primary);
- border-radius: var(--radius-md);
- padding: 8px 14px;
- font-size: 0.8rem;
- color: var(--text-tertiary);
-}
-
-.demo-content {
- display: grid;
- grid-template-columns: 1fr 340px;
- min-height: 380px;
-}
-
-.demo-article {
- padding: 32px;
- border-right: 1px solid var(--border);
-}
-
-.demo-text {
- font-size: 1.05rem;
- line-height: 1.9;
- color: var(--text-primary);
- margin: 0 0 20px 0;
-}
-
-.demo-text:last-child {
- margin-bottom: 0;
-}
-
-.demo-highlight {
- background-color: transparent;
- color: inherit;
- border-bottom: 2px solid var(--accent);
-}
-
-.demo-sidebar {
- padding: 0;
- background: var(--bg-primary);
- display: flex;
- flex-direction: column;
- gap: 0;
- overflow-y: auto;
- font-family:
- "IBM Plex Sans",
- -apple-system,
- BlinkMacSystemFont,
- sans-serif;
-}
-
-.demo-sidebar-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 14px 16px;
- border-bottom: 1px solid var(--border);
- background: var(--bg-primary);
-}
-
-.demo-logo-section {
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.demo-logo-icon {
- color: var(--accent);
- display: flex;
- align-items: center;
-}
-
-.demo-logo-text {
- font-weight: 600;
- font-size: 15px;
- color: var(--text-primary);
- letter-spacing: -0.02em;
-}
-
-.demo-user-section {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.demo-user-handle {
- font-size: 12px;
- color: var(--text-secondary);
- background: var(--bg-tertiary);
- padding: 4px 10px;
- border-radius: 9999px;
-}
-
-.demo-user-avatar {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- background: var(--bg-hover);
- color: var(--text-secondary);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- font-weight: 600;
-}
-
-.demo-page-info {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 16px;
- background: var(--bg-primary);
- border-bottom: 1px solid var(--border);
- font-size: 12px;
- color: var(--text-tertiary);
-}
-
-.demo-annotations-list {
- display: flex;
- flex-direction: column;
- gap: 1px;
- background: var(--border);
-}
-
-.demo-annotation {
- background: var(--bg-primary);
- border: none;
- border-radius: 0;
- padding: 14px 16px;
-}
-
-.demo-annotation-secondary {
- opacity: 1;
-}
-
-.demo-annotation-header {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 8px;
-}
-
-.demo-avatar {
- width: 26px;
- height: 26px;
- border-radius: 50%;
- background: var(--accent);
- color: var(--bg-primary);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 10px;
- font-weight: 600;
-}
-
-.demo-meta {
- display: flex;
- flex-direction: column;
- gap: 0;
-}
-
-.demo-author {
- font-size: 12px;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.demo-time {
- font-size: 11px;
- color: var(--text-tertiary);
-}
-
-.demo-quote {
- font-size: 12px;
- font-style: italic;
- color: var(--text-secondary);
- padding: 8px 12px;
- border-left: 2px solid var(--accent);
- margin: 0 0 8px 0;
- background: var(--accent-subtle);
- border-radius: 0 6px 6px 0;
- line-height: 1.5;
-}
-
-.demo-comment {
- font-size: 13px;
- line-height: 1.5;
- color: var(--text-primary);
- margin: 0 0 12px 0;
-}
-
-.demo-jump-btn {
- background: transparent;
- border: none;
- padding: 0;
- color: var(--accent);
- font-size: 11px;
- font-weight: 500;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- margin-top: 4px;
-}
-
-.demo-jump-btn:hover {
- text-decoration: underline;
- text-underline-offset: 2px;
-}
-
-.landing-section {
- padding: 80px 32px;
- max-width: 1000px;
- margin: 0 auto;
-}
-
-.landing-section-alt {
- background: var(--bg-secondary);
- max-width: none;
-}
-
-.landing-section-alt > * {
- max-width: 1000px;
- margin-left: auto;
- margin-right: auto;
-}
-
-.landing-section-title {
- font-size: 2rem;
- font-weight: 700;
- text-align: center;
- margin: 0 0 48px 0;
- color: var(--text-primary);
-}
-
-.landing-steps {
- display: flex;
- flex-direction: column;
- gap: 32px;
-}
-
-.landing-step {
- display: flex;
- gap: 24px;
- align-items: flex-start;
-}
-
-.landing-step-num {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: var(--accent);
- color: white;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- font-size: 1.1rem;
- flex-shrink: 0;
-}
-
-.landing-step-content h3 {
- font-size: 1.15rem;
- font-weight: 600;
- margin: 0 0 8px 0;
- color: var(--text-primary);
-}
-
-.landing-step-content p {
- font-size: 1rem;
- color: var(--text-secondary);
- margin: 0;
- line-height: 1.6;
-}
-
-.landing-features-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 32px;
-}
-
-.landing-feature {
- text-align: center;
- padding: 24px 16px;
-}
-
-.landing-feature-icon {
- width: 52px;
- height: 52px;
- border-radius: var(--radius-lg);
- background: var(--accent-subtle);
- color: var(--accent);
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 0 auto 16px;
-}
-
-.landing-feature h3 {
- font-size: 1.05rem;
- font-weight: 600;
- margin: 0 0 8px 0;
- color: var(--text-primary);
-}
-
-.landing-feature p {
- font-size: 0.9rem;
- color: var(--text-secondary);
- margin: 0;
- line-height: 1.6;
-}
-
-.landing-protocol {
- background: var(--bg-secondary);
- max-width: none;
- border-top: 1px solid var(--border);
- border-bottom: 1px solid var(--border);
-}
-
-.landing-protocol-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 64px;
- align-items: center;
- max-width: 1000px;
- margin: 0 auto;
-}
-
-.landing-protocol-main h2 {
- font-size: 1.75rem;
- font-weight: 700;
- margin: 0 0 16px 0;
- color: var(--text-primary);
-}
-
-.landing-protocol-main p {
- font-size: 1rem;
- color: var(--text-secondary);
- margin: 0 0 16px 0;
- line-height: 1.7;
-}
-
-.landing-protocol-main a {
- color: var(--accent);
- text-decoration: underline;
- text-underline-offset: 2px;
-}
-
-.landing-protocol-features {
- display: flex;
- flex-direction: column;
- gap: 20px;
-}
-
-.landing-protocol-item {
- display: flex;
- gap: 16px;
- align-items: flex-start;
- color: var(--accent);
-}
-
-.landing-protocol-item div {
- display: flex;
- flex-direction: column;
-}
-
-.landing-protocol-item strong {
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.landing-protocol-item span {
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
-
-.landing-final-cta {
- text-align: center;
-}
-
-.landing-final-cta h2 {
- font-size: 2rem;
- font-weight: 700;
- margin: 0 0 12px 0;
- color: var(--text-primary);
-}
-
-.landing-final-cta p {
- font-size: 1.1rem;
- color: var(--text-secondary);
- margin: 0 0 28px 0;
-}
-
-.landing-footer {
- border-top: 1px solid var(--border);
- padding: 48px 32px 32px;
-}
-
-.landing-footer-grid {
- display: flex;
- justify-content: space-between;
- max-width: 1000px;
- margin: 0 auto 40px;
-}
-
-.landing-footer-brand {
- max-width: 280px;
-}
-
-.landing-footer-brand p {
- font-size: 0.9rem;
- color: var(--text-tertiary);
- margin: 12px 0 0 0;
-}
-
-.landing-footer-links {
- display: flex;
- gap: 64px;
-}
-
-.landing-footer-col {
- display: flex;
- flex-direction: column;
- gap: 10px;
-}
-
-.landing-footer-col h4 {
- font-size: 0.75rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- color: var(--text-tertiary);
- margin: 0 0 4px 0;
-}
-
-.landing-footer-col a {
- font-size: 0.9rem;
- color: var(--text-secondary);
- text-decoration: none;
-}
-
-.landing-footer-col a:hover {
- color: var(--text-primary);
-}
-
-.landing-footer-bottom {
- text-align: center;
- padding-top: 24px;
- border-top: 1px solid var(--border);
- max-width: 1000px;
- margin: 0 auto;
-}
-
-.landing-footer-bottom p {
- font-size: 0.85rem;
- color: var(--text-tertiary);
- margin: 0;
-}
-
-@media (max-width: 900px) {
- .demo-content {
- grid-template-columns: 1fr;
- }
-
- .demo-article {
- border-right: none;
- border-bottom: 1px solid var(--border);
- }
-
- .demo-sidebar {
- max-height: 340px;
- }
-
- .landing-protocol-grid {
- grid-template-columns: 1fr;
- gap: 40px;
- }
-}
-
-@media (max-width: 768px) {
- .landing-nav {
- padding: 16px 20px;
- }
-
- .landing-nav-links a:not(.btn) {
- display: none;
- }
-
- .landing-hero {
- padding: 60px 20px 30px;
- }
-
- .landing-title {
- font-size: 2.5rem;
- }
-
- .landing-subtitle {
- font-size: 1.1rem;
- }
-
- .landing-cta {
- flex-direction: column;
- width: 100%;
- }
-
- .landing-cta .btn {
- width: 100%;
- justify-content: center;
- }
-
- .landing-demo {
- padding: 30px 16px 60px;
- }
-
- .demo-browser-bar {
- padding: 10px 12px;
- }
-
- .demo-browser-dots {
- display: none;
- }
-
- .demo-article {
- padding: 20px;
- }
-
- .demo-text {
- font-size: 0.95rem;
- }
-
- .demo-sidebar {
- padding: 16px;
- }
-
- .landing-section {
- padding: 60px 20px;
- }
-
- .landing-section-title {
- font-size: 1.5rem;
- margin-bottom: 32px;
- }
-
- .landing-step {
- gap: 16px;
- }
-
- .landing-step-num {
- width: 32px;
- height: 32px;
- font-size: 0.95rem;
- }
-
- .landing-features-grid {
- grid-template-columns: 1fr;
- gap: 24px;
- }
-
- .landing-feature {
- text-align: left;
- display: flex;
- gap: 16px;
- padding: 16px 0;
- }
-
- .landing-feature-icon {
- margin: 0;
- width: 44px;
- height: 44px;
- flex-shrink: 0;
- }
-
- .landing-protocol-main h2 {
- font-size: 1.5rem;
- }
-
- .landing-footer {
- padding: 40px 20px 24px;
- }
-
- .landing-footer-grid {
- flex-direction: column;
- gap: 40px;
- }
-
- .landing-footer-links {
- flex-wrap: wrap;
- gap: 32px;
- }
-}
-
-.demo-hover-indicator {
- position: absolute;
- display: flex;
- align-items: center;
- z-index: 100;
- pointer-events: none;
- background: transparent;
- opacity: 0;
- transform: scale(0.8);
- transition:
- opacity 0.15s ease-out,
- transform 0.15s ease-out;
-}
-
-.demo-hover-indicator.visible {
- opacity: 1;
- transform: scale(1);
-}
-
-.demo-hover-avatar {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- object-fit: cover;
- border: 2px solid var(--bg-primary);
- margin-left: -10px;
- background: var(--bg-elevated);
-}
-
-.demo-hover-avatar:first-child {
- margin-left: 0;
-}
-
-.demo-hover-avatar-fallback {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- background: #6366f1;
- color: white;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- font-weight: 600;
- font-family: -apple-system, sans-serif;
- border: 2px solid var(--bg-primary);
- margin-left: -10px;
-}
-
-.demo-hover-avatar-fallback:first-child {
- margin-left: 0;
-}
-
-@keyframes demo-popover-in {
- from {
- opacity: 0;
- transform: translateY(-4px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.demo-popover {
- position: absolute;
- width: 300px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 0;
- box-shadow: var(--shadow-lg);
- display: flex;
- flex-direction: column;
- z-index: 200;
- font-family: inherit;
- color: var(--text-primary);
- opacity: 0;
- animation: demo-popover-in 0.15s forwards;
- max-height: 400px;
- overflow: hidden;
-}
-
-.demo-popover-header {
- padding: 10px 14px;
- border-bottom: 1px solid var(--border);
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: var(--bg-primary);
- border-radius: 12px 12px 0 0;
- font-weight: 500;
- font-size: 11px;
- color: var(--text-tertiary);
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.demo-popover-close {
- background: none;
- border: none;
- color: var(--text-tertiary);
- cursor: pointer;
- padding: 2px;
- font-size: 16px;
- line-height: 1;
- opacity: 0.6;
- transition: opacity 0.15s;
-}
-
-.demo-popover-close:hover {
- opacity: 1;
-}
-
-.demo-popover-scroll-area {
- overflow-y: auto;
- max-height: 340px;
-}
-
-.demo-comment-item {
- padding: 12px 14px;
- border-bottom: 1px solid var(--border);
-}
-
-.demo-comment-item:last-child {
- border-bottom: none;
-}
-
-.demo-comment-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 6px;
-}
-
-.demo-comment-avatar {
- width: 22px;
- height: 22px;
- border-radius: 50%;
- object-fit: cover;
- background: var(--accent);
-}
-
-.demo-comment-handle {
- font-size: 12px;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.demo-comment-text {
- font-size: 13px;
- line-height: 1.5;
- color: var(--text-primary);
- margin-bottom: 8px;
-}
-
-.demo-comment-actions {
- display: flex;
- gap: 8px;
-}
-
-.demo-comment-action-btn {
- background: none;
- border: none;
- padding: 4px 8px;
- color: var(--text-tertiary);
- font-size: 11px;
- cursor: pointer;
- border-radius: 4px;
- transition: all 0.15s;
-}
-
-.demo-comment-action-btn:hover {
- background: var(--bg-hover);
- color: var(--text-secondary);
-}
diff --git a/web/src/css/layout.css b/web/src/css/layout.css
deleted file mode 100644
index 0bb49fa..0000000
--- a/web/src/css/layout.css
+++ /dev/null
@@ -1,737 +0,0 @@
-.app {
- min-height: 100vh;
- background: var(--bg-primary);
-}
-
-.app-layout {
- display: grid;
- grid-template-columns: 240px 1fr 280px;
- width: 100%;
- height: 100vh;
- overflow: hidden;
-}
-
-@media (max-width: 1024px) {
- .app-layout {
- grid-template-columns: 220px 1fr;
- }
-}
-
-@media (max-width: 768px) {
- .app-layout {
- grid-template-columns: 1fr;
- height: auto;
- overflow: visible;
- }
-}
-
-.top-nav {
- display: none;
- position: sticky;
- top: 0;
- z-index: 100;
- background: var(--nav-bg);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border-bottom: 1px solid var(--border);
-}
-
-@media (max-width: 768px) {
- .top-nav {
- display: block;
- }
-}
-
-.layout-mode-topnav .app-layout {
- grid-template-columns: 1fr;
- max-width: 100%;
- height: auto;
- overflow: visible;
-}
-
-.layout-mode-topnav .top-nav {
- display: block;
-}
-
-.layout-mode-topnav .main-content {
- height: auto;
- overflow: visible;
-}
-
-.layout-mode-topnav .main-content > * {
- max-width: 1100px;
- margin: 0 auto;
- padding: 32px 32px 80px;
-}
-
-@media (min-width: 1400px) {
- .layout-mode-topnav .main-content > * {
- max-width: 1300px;
- }
-}
-
-@media (min-width: 1700px) {
- .layout-mode-topnav .main-content > * {
- max-width: 1500px;
- }
-}
-
-@media (max-width: 768px) {
- .layout-mode-topnav .app-layout {
- grid-template-columns: 1fr;
- max-width: none;
- height: auto;
- overflow: visible;
- }
-
- .layout-mode-topnav .top-nav {
- display: none;
- }
-
- .layout-mode-topnav .main-content {
- padding: 16px 12px 100px;
- }
-
- .layout-mode-topnav .main-content > * {
- max-width: 100%;
- padding: 0 4px;
- }
-}
-
-.top-nav-inner {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 32px;
- height: 56px;
- display: flex;
- align-items: center;
- gap: 32px;
-}
-
-.top-nav-logo {
- display: flex;
- align-items: center;
- gap: 10px;
- text-decoration: none;
- color: var(--text-primary);
- font-weight: 700;
- font-size: 1.1rem;
- flex-shrink: 0;
-}
-
-.top-nav-logo svg {
- width: 26px;
- height: 26px;
- transition: color 0.2s;
- color: var(--accent);
-}
-
-.top-nav-logo:hover svg {
- color: var(--accent);
-}
-
-.top-nav-links {
- display: flex;
- align-items: center;
- gap: 4px;
- flex: 1;
-}
-
-.top-nav-link {
- padding: 8px 14px;
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.9rem;
- font-weight: 500;
- border-radius: var(--radius-md);
- transition: all 0.15s;
-}
-
-.top-nav-link:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
-}
-
-.top-nav-link.active {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.top-nav-link.extension-link {
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-.top-nav-actions {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.top-nav-icon-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border-radius: var(--radius-md);
- background: transparent;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 0.15s;
- position: relative;
- text-decoration: none;
-}
-
-.top-nav-icon-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.notif-dot {
- position: absolute;
- top: 6px;
- right: 6px;
- width: 8px;
- height: 8px;
- background: var(--accent);
- border-radius: 50%;
- border: 2px solid var(--bg-primary);
-}
-
-.top-nav-new-btn {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 16px;
- background: var(--accent);
- color: var(--bg-primary);
- border-radius: var(--radius-md);
- font-size: 0.875rem;
- font-weight: 600;
- text-decoration: none;
- transition: all 0.15s;
-}
-
-.top-nav-new-btn:hover {
- background: var(--accent-hover);
-}
-
-.top-nav-avatar {
- width: 34px;
- height: 34px;
- border-radius: var(--radius-md);
- background: var(--bg-tertiary);
- border: none;
- cursor: pointer;
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-secondary);
- font-size: 0.8rem;
- font-weight: 600;
- transition: opacity 0.15s;
-}
-
-.top-nav-avatar:hover {
- opacity: 0.85;
-}
-
-.top-nav-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.top-nav-mobile-toggle {
- display: none;
- align-items: center;
- justify-content: center;
- width: 40px;
- height: 40px;
- border: none;
- background: transparent;
- color: var(--text-primary);
- cursor: pointer;
-}
-
-.top-nav-dropdown {
- position: relative;
-}
-
-.dropdown-menu {
- position: absolute;
- top: calc(100% + 8px);
- min-width: 200px;
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- padding: 6px;
- box-shadow: var(--shadow-lg);
- z-index: 100;
-}
-
-.dropdown-right {
- right: 0;
-}
-
-.dropdown-item {
- display: flex;
- align-items: center;
- gap: 10px;
- width: 100%;
- padding: 10px 12px;
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: 0.875rem;
- font-weight: 500;
- text-decoration: none;
- transition: all 0.15s;
- background: none;
- border: none;
- cursor: pointer;
- text-align: left;
-}
-
-.dropdown-item:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.dropdown-item.danger:hover {
- background: rgba(217, 119, 102, 0.12);
- color: var(--error);
-}
-
-.dropdown-external {
- margin-left: auto;
- opacity: 0.4;
-}
-
-.tangled-icon-wrapper {
- width: 16px;
- height: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.tangled-icon-wrapper img {
- width: 16px;
- height: 16px;
- filter: grayscale(100%) brightness(1.5);
- opacity: 0.6;
- transition: all 0.15s;
-}
-
-.dropdown-item:hover .tangled-icon-wrapper img {
- opacity: 0.9;
-}
-
-[data-theme="light"] .tangled-icon-wrapper img {
- filter: grayscale(100%) brightness(0) invert(0.35);
- opacity: 1;
-}
-
-[data-theme="light"] .dropdown-item:hover .tangled-icon-wrapper img {
- filter: grayscale(100%) brightness(0) invert(0.1);
- opacity: 1;
-}
-
-.dropdown-divider {
- height: 1px;
- background: var(--border);
- margin: 6px 0;
-}
-
-.dropdown-user-info {
- padding: 8px 12px;
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.dropdown-user-name {
- font-weight: 600;
- color: var(--text-primary);
- font-size: 0.9rem;
-}
-
-.dropdown-user-handle {
- color: var(--text-tertiary);
- font-size: 0.8rem;
-}
-
-.main-content {
- width: 100%;
- padding: 0;
- overflow-y: auto;
- overflow-x: hidden;
- height: 100%;
- scrollbar-width: thin;
- scrollbar-color: var(--bg-hover) transparent;
-}
-
-.main-content > * {
- max-width: 100%;
- margin: 0 auto;
- padding: 32px 40px 80px;
-}
-
-.main-content::-webkit-scrollbar {
- width: 8px;
-}
-
-.main-content::-webkit-scrollbar-track {
- background: var(--bg-secondary);
-}
-
-.main-content::-webkit-scrollbar-thumb {
- background: var(--bg-hover);
- border-radius: var(--radius-full);
-}
-
-.main-content::-webkit-scrollbar-thumb:hover {
- background: var(--text-tertiary);
-}
-
-.mobile-menu {
- display: none;
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--border);
- padding: 12px 16px;
-}
-
-.mobile-menu-link {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px 16px;
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.95rem;
- font-weight: 500;
- border-radius: var(--radius-md);
- transition: all 0.15s;
-}
-
-.mobile-menu-link:hover,
-.mobile-menu-link.active {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.mobile-menu-link.active {
- color: var(--accent);
-}
-
-.mobile-menu-divider {
- height: 1px;
- background: var(--border);
- margin: 8px 0;
-}
-
-.notification-badge {
- background: var(--accent);
- color: var(--bg-primary);
- font-size: 0.7rem;
- font-weight: 700;
- padding: 2px 6px;
- border-radius: var(--radius-full);
- margin-left: auto;
-}
-
-.mobile-bottom-nav {
- display: none;
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- background: var(--nav-bg);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border-top: 1px solid var(--border);
- padding: 8px 8px calc(8px + env(safe-area-inset-bottom));
- z-index: 200;
- justify-content: space-around;
- align-items: center;
-}
-
-.mobile-bottom-nav-item {
- display: flex;
- flex: 1;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 4px;
- padding: 6px 0;
- color: var(--text-tertiary);
- text-decoration: none;
- font-size: 0.65rem;
- font-weight: 500;
- transition: color 0.15s;
- min-width: 0;
-}
-
-.mobile-bottom-nav-item.active {
- color: var(--accent);
-}
-
-.mobile-bottom-nav-item:active {
- transform: scale(0.95);
-}
-
-.mobile-bottom-nav-new {
- padding: 6px 0;
-}
-
-.mobile-nav-new-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 44px;
- height: 44px;
- background: var(--accent);
- color: var(--bg-primary);
- border-radius: var(--radius-full);
- box-shadow: var(--shadow-md);
-}
-
-.mobile-nav-avatar {
- width: 24px;
- height: 24px;
- border-radius: var(--radius-full);
- object-fit: cover;
-}
-
-.ios-shortcut-banner {
- display: none;
- position: relative;
- padding: 20px;
- margin-bottom: 12px;
- text-align: center;
-}
-
-.ios-shortcut-banner-close {
- position: absolute;
- top: 8px;
- right: 8px;
- background: none;
- border: none;
- color: var(--text-tertiary);
- cursor: pointer;
- padding: 6px;
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0.5;
- transition: opacity 0.15s;
-}
-
-.ios-shortcut-banner-close:hover {
- opacity: 1;
-}
-
-.ios-shortcut-banner-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 12px;
-}
-
-.ios-shortcut-banner-icon {
- display: none;
-}
-
-.ios-shortcut-banner-text {
- text-align: center;
-}
-
-.ios-shortcut-banner-text strong {
- display: none;
-}
-
-.ios-shortcut-banner-text p {
- font-size: 0.8rem;
- color: var(--text-tertiary);
- margin: 0;
- line-height: 1.4;
-}
-
-.ios-shortcut-banner-btn {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 10px 20px;
- background: transparent;
- color: var(--text-secondary);
- font-size: 0.85rem;
- font-weight: 500;
- border: 1px solid var(--border);
- border-radius: 100px;
- text-decoration: none;
- transition: all 0.15s;
-}
-
-.ios-shortcut-banner-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-@media (max-width: 768px) {
- .ios-shortcut-banner {
- display: block;
- }
-}
-
-@media (max-width: 768px) {
- .top-nav {
- display: none;
- }
-
- .mobile-bottom-nav {
- display: flex;
- }
-
- .main-content {
- padding: 16px 12px 100px;
- }
-
- .main-content > * {
- padding: 0 4px;
- max-width: 100%;
- }
-
- .feed-container {
- border-radius: var(--radius-md);
- padding: 0;
- }
-}
-
-@media (max-width: 480px) {
- .main-content {
- padding: 16px 12px 100px;
- }
-
- .page-title {
- font-size: 1.25rem;
- }
-
- .page-description {
- font-size: 0.85rem;
- }
-}
-
-.mobile-nav-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.5);
- z-index: 300;
- backdrop-filter: blur(2px);
- -webkit-backdrop-filter: blur(2px);
- animation: fadeIn 0.15s ease-out;
-}
-
-.mobile-nav-menu {
- position: fixed;
- bottom: calc(70px + env(safe-area-inset-bottom));
- right: 12px;
- width: 250px;
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-xl);
- padding: 6px;
- z-index: 301;
- box-shadow: var(--shadow-2xl);
- animation: mobileMenuSlide 0.2s cubic-bezier(0.16, 1, 0.3, 1);
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-@keyframes mobileMenuSlide {
- from {
- opacity: 0;
- transform: translateY(20px) scale(0.95);
- }
-
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
-}
-
-.mobile-menu-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px 14px;
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.95rem;
- font-weight: 500;
- border-radius: var(--radius-md);
- transition: all 0.1s;
- background: transparent;
- border: none;
- width: 100%;
- text-align: left;
- cursor: pointer;
-}
-
-.mobile-menu-item:hover,
-.mobile-menu-item:active {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.mobile-menu-item svg {
- opacity: 0.8;
-}
-
-.mobile-menu-item.active {
- background: var(--bg-tertiary);
- color: var(--accent);
-}
-
-.mobile-menu-item.danger {
- color: var(--error);
-}
-
-.mobile-menu-item.danger:hover {
- background: rgba(239, 68, 68, 0.1);
-}
-
-.mobile-menu-profile-card {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px;
- background: var(--bg-tertiary);
- border-radius: var(--radius-lg);
- margin-bottom: 6px;
- text-decoration: none;
- border: 1px solid transparent;
-}
-
-.mobile-menu-profile-card:active {
- background: var(--bg-hover);
- transform: scale(0.98);
-}
-
-.mobile-menu-badge {
- margin-left: auto;
- background: var(--accent);
- color: white;
- font-size: 0.75rem;
- font-weight: 700;
- padding: 2px 8px;
- border-radius: 99px;
-}
diff --git a/web/src/css/login.css b/web/src/css/login.css
deleted file mode 100644
index 031b3ad..0000000
--- a/web/src/css/login.css
+++ /dev/null
@@ -1,380 +0,0 @@
-.login-page {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- min-height: 80vh;
- padding: 40px 20px;
- width: 100%;
- max-width: 440px;
- margin: 0 auto;
-}
-
-.login-header-group {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- gap: 24px;
- margin-bottom: 48px;
- width: auto;
-}
-
-.login-logo-img {
- width: 60px;
- height: 60px;
- object-fit: contain;
- display: block;
-}
-
-.login-x {
- font-size: 2rem;
- color: var(--text-tertiary);
- font-weight: 300;
- line-height: 1;
- padding-bottom: 4px;
-}
-
-.login-atproto-icon {
- color: #3b83f6 !important;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.login-heading {
- font-size: 1.5rem;
- font-weight: 700;
- margin-bottom: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-wrap: wrap;
- gap: 8px;
- text-align: center;
- line-height: 1.3;
- color: var(--text-primary);
-}
-
-.login-help-btn {
- background: none;
- border: none;
- color: var(--text-tertiary);
- cursor: pointer;
- padding: 4px;
- display: flex;
- align-items: center;
- transition: color 0.15s;
- flex-shrink: 0;
-}
-
-.login-help-btn:hover {
- color: var(--accent);
-}
-
-.login-help-text {
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 16px;
- margin-bottom: 24px;
- font-size: 0.9rem;
- color: var(--text-secondary);
- line-height: 1.5;
- text-align: center;
- width: 100%;
-}
-
-.login-help-text code {
- background: rgba(255, 255, 255, 0.05);
- padding: 2px 6px;
- border-radius: var(--radius-sm);
- font-size: 0.85rem;
- font-family: var(--font-mono);
-}
-
-.login-form {
- display: flex;
- flex-direction: column;
- gap: 20px;
- width: 100%;
-}
-
-.login-input-wrapper {
- position: relative;
-}
-
-.login-input {
- width: 100%;
- padding: 14px 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 1rem;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-
-.login-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 4px var(--accent-subtle);
- background: var(--bg-primary);
-}
-
-.login-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.login-suggestions {
- position: absolute;
- top: calc(100% + 8px);
- left: 0;
- right: 0;
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-lg);
- overflow: hidden;
- z-index: 100;
- max-height: 300px;
- overflow-y: auto;
-}
-
-.login-suggestion {
- display: flex;
- align-items: center;
- gap: 12px;
- width: 100%;
- padding: 12px 16px;
- background: transparent;
- border: none;
- cursor: pointer;
- text-align: left;
- transition: background 0.1s;
- border-bottom: 1px solid var(--border);
-}
-
-.login-suggestion:last-child {
- border-bottom: none;
-}
-
-.login-suggestion:hover,
-.login-suggestion.selected {
- background: var(--bg-tertiary);
-}
-
-.login-suggestion-avatar {
- width: 36px;
- height: 36px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- overflow: hidden;
- font-size: 0.8rem;
- font-weight: 600;
- color: white;
-}
-
-.login-suggestion-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.login-suggestion-info {
- display: flex;
- flex-direction: column;
- min-width: 0;
- gap: 2px;
-}
-
-.login-suggestion-name {
- font-weight: 600;
- font-size: 0.95rem;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.login-suggestion-handle {
- font-size: 0.85rem;
- color: var(--text-secondary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.login-error {
- padding: 12px 16px;
- background: rgba(239, 68, 68, 0.1);
- border: 1px solid rgba(239, 68, 68, 0.2);
- border-radius: var(--radius-md);
- color: var(--error);
- font-size: 0.875rem;
- text-align: center;
-}
-
-.login-submit {
- padding: 14px 24px;
- font-size: 1rem;
- font-weight: 600;
- width: 100%;
- justify-content: center;
-}
-
-.login-avatar-large {
- width: 80px;
- height: 80px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 20px;
- font-weight: 700;
- font-size: 2rem;
- color: white;
- overflow: hidden;
- box-shadow: var(--shadow-md);
-}
-
-.login-avatar-large img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.login-welcome {
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 32px;
- text-align: center;
- color: var(--text-primary);
-}
-
-.login-actions {
- display: flex;
- flex-direction: column;
- gap: 12px;
- width: 100%;
-}
-
-.morph-container {
- display: inline-block;
- color: var(--text-primary);
- font-weight: 700;
- transition:
- opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1),
- transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
- filter 0.4s cubic-bezier(0.4, 0, 0.2, 1);
- white-space: nowrap;
- vertical-align: bottom;
-}
-
-.morph-out {
- opacity: 0;
- transform: translateY(8px) scale(0.95);
- filter: blur(4px);
-}
-
-.morph-in {
- opacity: 1;
- transform: translateY(0) scale(1);
- filter: blur(0);
-}
-
-.login-legal {
- margin-top: 24px;
- font-size: 0.85rem;
- color: var(--text-tertiary);
- text-align: center;
- line-height: 1.5;
-}
-
-.login-legal a {
- color: var(--accent);
- text-decoration: underline;
- text-decoration-color: var(--accent);
- text-underline-offset: 4px;
- font-weight: 500;
-}
-
-.login-legal a:hover {
- text-decoration-thickness: 2px;
- opacity: 0.8;
-}
-
-@media (max-width: 480px) {
- .login-heading {
- font-size: 1.25rem;
- flex-wrap: wrap;
- gap: 4px;
- padding: 0 10px;
- }
-
- .login-header-group {
- margin-bottom: 32px;
- gap: 16px;
- }
-
- .login-logo-img {
- width: 48px;
- height: 48px;
- }
-
- .login-x {
- font-size: 1.5rem;
- }
-
- .login-atproto-icon svg {
- width: 48px;
- height: 48px;
- }
-}
-
-.login-divider {
- display: flex;
- align-items: center;
- text-align: center;
- margin: 24px 0;
- color: var(--text-tertiary);
- font-size: 13px;
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.login-divider::before,
-.login-divider::after {
- content: "";
- flex: 1;
- border-bottom: 1px solid var(--border);
-}
-
-.login-divider::before {
- margin-right: 16px;
-}
-
-.login-divider::after {
- margin-left: 16px;
-}
-
-.login-signup-btn {
- width: 100%;
- border: 1px solid var(--border);
- background: transparent;
- color: var(--text-primary);
- transition: all 0.2s;
-}
-
-.login-signup-btn:hover {
- border-color: var(--accent);
- background: var(--bg-hover);
- color: var(--accent);
-}
diff --git a/web/src/css/modals.css b/web/src/css/modals.css
deleted file mode 100644
index 5dd820c..0000000
--- a/web/src/css/modals.css
+++ /dev/null
@@ -1,630 +0,0 @@
-.modal-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.6);
- display: flex;
- align-items: center;
- justify-content: center;
- padding: var(--spacing-md);
- z-index: 500;
- animation: fadeIn 0.15s ease-out;
-}
-
-.spinner {
- animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
- from {
- transform: rotate(0deg);
- }
-
- to {
- transform: rotate(360deg);
- }
-}
-
-.modal-container {
- background: var(--bg-secondary);
- border-radius: var(--radius-lg);
- width: 100%;
- max-width: 420px;
- border: 1px solid var(--border);
- box-shadow: var(--shadow-lg);
- animation: modalIn 0.2s ease-out;
-}
-
-.modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--spacing-md);
- border-bottom: 1px solid var(--border);
-}
-
-.modal-title {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.modal-close-btn {
- padding: 6px;
- color: var(--text-tertiary);
- border-radius: var(--radius-sm);
- transition: all 0.15s;
- background: none;
- border: none;
- cursor: pointer;
-}
-
-.modal-close-btn:hover {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.modal-form {
- padding: var(--spacing-md);
- display: flex;
- flex-direction: column;
- gap: var(--spacing-md);
-}
-
-.modal-body {
- padding: var(--spacing-md);
- display: flex;
- flex-direction: column;
- gap: var(--spacing-md);
-}
-
-.modal-actions {
- display: flex;
- justify-content: flex-end;
- gap: var(--spacing-sm);
- padding-top: var(--spacing-sm);
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-@keyframes modalIn {
- from {
- opacity: 0;
- transform: scale(0.96) translateY(-8px);
- }
-
- to {
- opacity: 1;
- transform: scale(1) translateY(0);
- }
-}
-
-.form-group {
- margin-bottom: 0;
-}
-
-.form-label {
- display: block;
- font-size: 0.8rem;
- font-weight: 500;
- color: var(--text-secondary);
- margin-bottom: 6px;
-}
-
-.form-input,
-.form-textarea,
-.form-select {
- width: 100%;
- padding: 10px 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 0.875rem;
- transition: all 0.15s;
-}
-
-.form-input:focus,
-.form-textarea:focus,
-.form-select:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 2px var(--accent-subtle);
-}
-
-.form-textarea {
- resize: none;
- min-height: 80px;
-}
-
-.input {
- width: 100%;
- padding: 10px 12px;
- font-size: 0.875rem;
- color: var(--text-primary);
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- outline: none;
- transition: all 0.15s ease;
-}
-
-.input:focus {
- border-color: var(--accent);
- box-shadow: 0 0 0 2px var(--accent-subtle);
-}
-
-.input::placeholder {
- color: var(--text-tertiary);
-}
-
-.icon-picker-tabs {
- display: flex;
- gap: 4px;
- margin-bottom: var(--spacing-sm);
-}
-
-.icon-picker-tab {
- flex: 1;
- padding: 8px 12px;
- background: var(--bg-tertiary);
- border: none;
- border-radius: var(--radius-sm);
- color: var(--text-secondary);
- font-size: 0.8rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.icon-picker-tab:hover {
- background: var(--bg-hover);
-}
-
-.icon-picker-tab.active {
- background: var(--accent);
- color: white;
-}
-
-.emoji-picker-wrapper {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-sm);
-}
-
-.emoji-picker,
-.icon-picker {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- max-height: 120px;
- overflow-y: auto;
- padding: var(--spacing-sm);
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
-}
-
-.emoji-option,
-.icon-option {
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1rem;
- background: transparent;
- border: 2px solid transparent;
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: all 0.15s ease;
- color: var(--text-secondary);
-}
-
-.emoji-option:hover,
-.icon-option:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
-}
-
-.emoji-option.selected,
-.icon-option.selected {
- border-color: var(--accent);
- background: var(--accent-subtle);
- color: var(--accent);
-}
-
-.color-input-container {
- display: flex;
- align-items: center;
- gap: var(--spacing-sm);
- background: var(--bg-tertiary);
- padding: 8px 12px;
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
- width: fit-content;
-}
-
-.color-input-wrapper {
- position: relative;
- width: 28px;
- height: 28px;
- border-radius: var(--radius-full);
- overflow: hidden;
- border: 2px solid var(--border);
- cursor: pointer;
- transition: transform 0.1s;
-}
-
-.color-input-wrapper:hover {
- transform: scale(1.1);
- border-color: var(--accent);
-}
-
-.color-input-wrapper input[type="color"] {
- position: absolute;
- top: -50%;
- left: -50%;
- width: 200%;
- height: 200%;
- padding: 0;
- margin: 0;
- border: none;
- cursor: pointer;
- opacity: 0;
-}
-
-.signup-modal {
- background: var(--bg-secondary);
- width: 100%;
- max-width: 440px;
- border-radius: var(--radius-lg);
- padding: var(--spacing-lg);
- border: 1px solid var(--border);
- position: relative;
- max-height: 85vh;
- overflow-y: auto;
- box-shadow: var(--shadow-lg);
-}
-
-.modal-close {
- position: absolute;
- top: var(--spacing-md);
- right: var(--spacing-md);
- background: none;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- padding: 4px;
- border-radius: var(--radius-sm);
-}
-
-.modal-close:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
-}
-
-.signup-step h2 {
- font-size: 1.25rem;
- margin-bottom: 8px;
- font-weight: 600;
-}
-
-.signup-subtitle {
- color: var(--text-secondary);
- font-size: 0.875rem;
- margin-bottom: var(--spacing-lg);
-}
-
-.provider-grid {
- display: grid;
- grid-template-columns: 1fr;
- gap: var(--spacing-sm);
-}
-
-.provider-card {
- display: flex;
- align-items: center;
- gap: var(--spacing-md);
- padding: var(--spacing-md);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- background: var(--bg-primary);
- cursor: pointer;
- text-align: left;
- transition: all 0.15s ease;
-}
-
-.provider-card:hover {
- border-color: var(--accent);
- background: var(--bg-tertiary);
-}
-
-.provider-icon {
- width: 40px;
- height: 40px;
- border-radius: var(--radius-md);
- background: var(--bg-tertiary);
- display: flex;
- align-items: center;
- justify-content: center;
- border: 1px solid var(--border);
- color: var(--text-primary);
- flex-shrink: 0;
-}
-
-.provider-icon.wide {
- width: auto;
- padding: 0 10px;
- border: none;
- background: transparent;
-}
-
-.provider-icon.wide img {
- max-height: 36px !important;
- height: 36px !important;
- width: auto !important;
-}
-
-.provider-initial {
- font-size: 1rem;
- font-weight: 600;
-}
-
-.provider-info {
- flex: 1;
-}
-
-.provider-info h3 {
- font-weight: 600;
- font-size: 0.9rem;
- margin-bottom: 2px;
-}
-
-.provider-info span {
- color: var(--text-secondary);
- font-size: 0.8rem;
-}
-
-.provider-arrow {
- color: var(--text-tertiary);
-}
-
-.signup-recommended {
- position: relative;
- margin-bottom: var(--spacing-md);
-}
-
-.signup-recommended-badge {
- position: absolute;
- top: -8px;
- left: 12px;
- background: var(--accent);
- color: white;
- font-size: 0.7rem;
- font-weight: 600;
- padding: 2px 8px;
- border-radius: var(--radius-sm);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- z-index: 1;
-}
-
-.provider-card-featured {
- border-color: var(--accent);
- background: var(--accent-subtle);
-}
-
-.provider-card-featured:hover {
- border-color: var(--accent);
- background: var(--bg-tertiary);
-}
-
-.signup-toggle-others {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- width: 100%;
- padding: 10px;
- background: transparent;
- border: none;
- color: var(--text-secondary);
- font-size: 0.85rem;
- cursor: pointer;
- transition: color 0.15s;
-}
-
-.signup-toggle-others:hover {
- color: var(--text-primary);
-}
-
-.toggle-chevron {
- transition: transform 0.2s ease;
- transform: rotate(90deg);
-}
-
-.toggle-chevron.open {
- transform: rotate(-90deg);
-}
-
-.signup-form {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-md);
-}
-
-.handle-input-group {
- display: flex;
- align-items: center;
- gap: var(--spacing-sm);
-}
-
-.handle-suffix {
- color: var(--text-tertiary);
- font-size: 0.85rem;
- white-space: nowrap;
-}
-
-.error-message {
- color: var(--error);
- background: rgba(255, 69, 58, 0.1);
- padding: 10px 12px;
- border-radius: var(--radius-md);
- font-size: 0.8rem;
- display: flex;
- align-items: center;
- gap: var(--spacing-sm);
-}
-
-.step-header {
- display: flex;
- align-items: center;
- gap: var(--spacing-sm);
- margin-bottom: var(--spacing-lg);
-}
-
-.step-header h2 {
- margin: 0;
- font-size: 1.1rem;
-}
-
-.btn-back {
- background: none;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- font-size: 0.85rem;
- padding: 0;
-}
-
-.btn-back:hover {
- color: var(--text-primary);
-}
-
-.legal-text {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- text-align: center;
- margin-top: var(--spacing-sm);
-}
-
-.links-input-group {
- display: flex;
- gap: var(--spacing-sm);
- margin-bottom: var(--spacing-sm);
-}
-
-.links-input-group input {
- flex: 1;
-}
-
-.links-list {
- list-style: none;
- padding: 0;
- margin: 0;
- display: flex;
- flex-direction: column;
- gap: var(--spacing-sm);
-}
-
-.link-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--spacing-sm);
- padding: 8px 12px;
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- font-size: 0.85rem;
- color: var(--text-primary);
- word-break: break-all;
-}
-
-.link-item span {
- flex: 1;
-}
-
-.btn-icon-sm {
- background: none;
- border: none;
- color: var(--text-tertiary);
- cursor: pointer;
- padding: 4px;
- border-radius: var(--radius-sm);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1rem;
- line-height: 1;
-}
-
-.btn-icon-sm:hover {
- background: var(--bg-hover);
- color: var(--error);
-}
-
-.char-count {
- text-align: right;
- font-size: 0.7rem;
- color: var(--text-tertiary);
- margin-top: 4px;
-}
-
-.avatar-upload-container {
- display: flex;
- align-items: center;
- gap: var(--spacing-md);
-}
-
-.avatar-preview {
- width: 72px;
- height: 72px;
- border-radius: var(--radius-full);
- background: var(--bg-tertiary);
- border: 2px solid var(--border);
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- transition: border-color 0.15s ease;
-}
-
-.avatar-preview:hover {
- border-color: var(--accent);
-}
-
-.avatar-preview-img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.avatar-placeholder {
- color: var(--text-tertiary);
-}
-
-.avatar-uploading {
- position: absolute;
- inset: 0;
- background: rgba(0, 0, 0, 0.6);
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- font-size: 0.7rem;
-}
-
-.btn-sm {
- padding: 6px 12px;
- font-size: 0.8rem;
-}
diff --git a/web/src/css/notifications.css b/web/src/css/notifications.css
deleted file mode 100644
index 98dd0bd..0000000
--- a/web/src/css/notifications.css
+++ /dev/null
@@ -1,67 +0,0 @@
-.notifications-page {
- max-width: 680px;
- margin: 0 auto;
-}
-
-.notifications-list {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.notification-item {
- display: flex;
- gap: 16px;
- align-items: flex-start;
- text-decoration: none;
- color: inherit;
-}
-
-.notification-item:hover {
- background: var(--bg-hover);
-}
-
-.notification-icon {
- width: 36px;
- height: 36px;
- border-radius: var(--radius-full);
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-tertiary);
- color: var(--text-secondary);
- flex-shrink: 0;
-}
-
-.notification-icon[data-type="like"] {
- color: #ef4444;
- background: rgba(239, 68, 68, 0.1);
-}
-
-.notification-icon[data-type="reply"] {
- color: #3b82f6;
- background: rgba(59, 130, 246, 0.1);
-}
-
-.notification-content {
- flex: 1;
- min-width: 0;
-}
-
-.notification-text {
- font-size: 0.95rem;
- margin-bottom: 4px;
- line-height: 1.4;
- color: var(--text-primary);
- overflow-wrap: break-word;
- word-break: break-word;
-}
-
-.notification-text strong {
- font-weight: 600;
-}
-
-.notification-time {
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
diff --git a/web/src/css/profile.css b/web/src/css/profile.css
deleted file mode 100644
index c9a501c..0000000
--- a/web/src/css/profile.css
+++ /dev/null
@@ -1,311 +0,0 @@
-.profile-header {
- display: flex;
- align-items: flex-start;
- gap: 24px;
- margin-bottom: 32px;
- padding-bottom: 24px;
- border-bottom: 1px solid var(--border);
-}
-
-.profile-avatar {
- width: 80px;
- height: 80px;
- min-width: 80px;
- border-radius: 50%;
- background: var(--bg-tertiary);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 2rem;
- color: var(--text-secondary);
- overflow: hidden;
- border: 1px solid var(--border);
-}
-
-.profile-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.profile-avatar-link {
- text-decoration: none;
-}
-
-.profile-info {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.profile-name {
- font-size: 1.5rem;
- font-weight: 700;
- color: var(--text-primary);
- line-height: 1.2;
- overflow-wrap: break-word;
- word-break: break-word;
-}
-
-.profile-handle-row {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-top: 4px;
- flex-wrap: wrap;
-}
-
-.profile-handle-link {
- color: var(--text-tertiary);
- text-decoration: none;
- font-size: 1rem;
- transition: color 0.15s;
- overflow-wrap: break-word;
- word-break: break-all;
-}
-
-.profile-handle-link:hover {
- color: var(--text-secondary);
-}
-
-.profile-bluesky-link {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- color: #3b82f6;
- text-decoration: none;
- font-size: 0.85rem;
- font-weight: 500;
- padding: 2px 8px;
- border-radius: var(--radius-sm);
- background: rgba(59, 130, 246, 0.1);
- transition: all 0.15s ease;
- width: fit-content;
-}
-
-.profile-bluesky-link:hover {
- background: rgba(59, 130, 246, 0.15);
-}
-
-.profile-stats {
- display: flex;
- gap: 24px;
- margin-top: 12px;
-}
-
-.profile-stat {
- color: var(--text-tertiary);
- font-size: 0.9rem;
-}
-
-.profile-stat strong {
- color: var(--text-primary);
- font-weight: 600;
-}
-
-.profile-tabs {
- display: flex;
- gap: 24px;
- margin-bottom: 24px;
- border-bottom: 1px solid var(--border);
- flex-wrap: wrap;
- row-gap: 8px;
-}
-
-.profile-tab {
- padding: 12px 0;
- font-size: 0.95rem;
- font-weight: 500;
- color: var(--text-tertiary);
- background: transparent;
- border: none;
- cursor: pointer;
- transition: all 0.15s ease;
- position: relative;
-}
-
-.profile-tab:hover {
- color: var(--text-primary);
-}
-
-.profile-tab.active {
- color: var(--text-primary);
-}
-
-.profile-tab.active::after {
- content: "";
- position: absolute;
- bottom: -1px;
- left: 0;
- right: 0;
- height: 2px;
- background: var(--text-primary);
-}
-
-.profile-badge-wrapper {
- display: inline-flex;
- align-items: center;
-}
-
-.profile-badge-clickable {
- position: relative;
- display: inline-flex;
- align-items: center;
- cursor: pointer;
- margin-left: 8px;
-}
-
-.badge-info-popover {
- position: absolute;
- top: calc(100% + 8px);
- left: 50%;
- transform: translateX(-50%);
- padding: 16px;
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-lg);
- font-size: 0.85rem;
- white-space: nowrap;
- z-index: 100;
- min-width: 200px;
-}
-
-.badge-info-title {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 8px;
-}
-
-.verifier-link {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px;
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- text-decoration: none;
- transition: background 0.15s ease;
-}
-
-.verifier-link:hover {
- background: var(--bg-hover);
-}
-
-.verifier-avatar {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- object-fit: cover;
-}
-
-.verifier-name {
- color: var(--text-primary);
- font-size: 0.85rem;
- font-weight: 500;
-}
-
-.profile-suspended {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px 20px;
- text-align: center;
- background: var(--bg-secondary);
- border-radius: var(--radius-lg);
- margin-top: 20px;
- border: 1px solid var(--border);
-}
-
-.suspended-icon {
- font-size: 40px;
- margin-bottom: 16px;
- color: var(--text-tertiary);
-}
-
-.profile-suspended h2 {
- color: var(--text-primary);
- margin-bottom: 8px;
- font-size: 1.25rem;
-}
-
-@media (max-width: 640px) {
- .profile-header {
- flex-direction: column;
- text-align: center;
- }
-
- .profile-info {
- align-items: center;
- }
-
- .profile-handle-row {
- justify-content: center;
- }
-
- .profile-stats {
- justify-content: center;
- }
-
- .profile-tabs {
- justify-content: center;
- gap: 16px;
- }
-}
-
-.profile-margin-details {
- margin-top: 16px;
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.profile-bio {
- font-size: 0.95rem;
- color: var(--text-primary);
- line-height: 1.5;
- white-space: pre-wrap;
- max-width: 600px;
-}
-
-.profile-links {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- align-items: center;
-}
-
-.profile-link-chip {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 12px;
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- border-radius: 8px;
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.85rem;
- font-weight: 500;
- transition: all 0.2s ease;
-}
-
-.profile-link-chip:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- border-color: var(--text-tertiary);
- transform: translateY(-1px);
-}
-
-.profile-link-chip.main-website {
- background: rgba(var(--accent-rgb), 0.1);
- color: var(--accent);
- border-color: var(--accent);
-}
-
-.profile-link-chip.main-website:hover {
- background: rgba(var(--accent-rgb), 0.15);
-}
diff --git a/web/src/css/sidebar.css b/web/src/css/sidebar.css
deleted file mode 100644
index 252d896..0000000
--- a/web/src/css/sidebar.css
+++ /dev/null
@@ -1,499 +0,0 @@
-.left-sidebar {
- height: 100%;
- display: flex;
- flex-direction: column;
- background: var(--bg-primary);
- border-right: 1px solid var(--border);
- padding: 20px 16px;
- font-family: var(--font-sans);
-}
-
-.sidebar-header {
- margin-bottom: 24px;
- padding: 0 8px;
- display: flex;
- justify-content: center;
-}
-
-.sidebar-logo {
- display: flex;
- align-items: center;
- justify-content: center;
- text-decoration: none;
- opacity: 0.9;
- transition: all 0.2s ease;
- padding: 8px;
- border-radius: var(--radius-md);
-}
-
-.sidebar-logo-icon {
- display: none;
-}
-
-.sidebar-logo {
- display: flex;
- align-items: center;
- justify-content: center;
- text-decoration: none;
- color: var(--accent);
- opacity: 0.9;
- transition: all 0.2s ease;
- padding: 8px;
- border-radius: var(--radius-md);
-}
-
-.sidebar-logo svg {
- width: 32px;
- height: 32px;
-}
-
-.sidebar-logo:hover {
- opacity: 1;
- background: var(--bg-hover);
- color: var(--accent);
-}
-
-.sidebar-logo:hover .sidebar-logo-icon {
- background-color: var(--accent);
-}
-
-.sidebar-nav {
- display: flex;
- flex-direction: column;
- gap: 4px;
- flex: 1;
-}
-
-.sidebar-nav-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 8px 12px;
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.9rem;
- font-weight: 500;
- border-radius: var(--radius-md);
- transition: all 0.15s ease;
- border: 1px solid transparent;
-}
-
-.sidebar-nav-item:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
-}
-
-.sidebar-nav-item.active {
- background: var(--bg-card);
- color: var(--text-primary);
- font-weight: 600;
- border-color: var(--border);
- box-shadow: var(--shadow-sm);
-}
-
-.sidebar-nav-item svg {
- flex-shrink: 0;
- width: 18px;
- height: 18px;
- opacity: 0.8;
-}
-
-.sidebar-nav-item.active svg {
- opacity: 1;
- color: var(--accent);
-}
-
-.sidebar-badge {
- margin-left: auto;
- background: var(--accent);
- color: #fff;
- font-size: 0.7rem;
- font-weight: 600;
- padding: 2px 8px;
- border-radius: 99px;
- min-width: 20px;
- text-align: center;
-}
-
-.sidebar-new-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 10px;
- margin-bottom: 16px;
- background: var(--accent);
- color: #fff;
- border-radius: var(--radius-md);
- font-size: 0.9rem;
- font-weight: 600;
- text-decoration: none;
- transition: all 0.15s ease;
- box-shadow: var(--shadow-sm);
-}
-
-.sidebar-new-btn:hover {
- background: var(--accent-hover);
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
-}
-
-.sidebar-footer {
- padding-top: 16px;
- border-top: 1px solid var(--border);
- position: relative;
-}
-
-.sidebar-signin-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- padding: 10px;
- background: var(--accent);
- color: white;
- border-radius: var(--radius-md);
- font-weight: 600;
- text-decoration: none;
- transition: all 0.2s;
- box-shadow: var(--shadow-sm);
-}
-
-.sidebar-signin-btn:hover {
- background: var(--accent-hover);
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
-}
-
-.sidebar-user-btn {
- display: flex;
- align-items: center;
- gap: 12px;
- width: 100%;
- padding: 8px;
- border-radius: var(--radius-md);
- background: transparent;
- border: 1px solid transparent;
- cursor: pointer;
- transition: all 0.15s ease;
- text-align: left;
-}
-
-.sidebar-user-btn:hover,
-.sidebar-user-btn.active {
- background: var(--bg-card);
- border-color: var(--border);
-}
-
-.sidebar-user-avatar {
- width: 32px;
- height: 32px;
- border-radius: var(--radius-full);
- object-fit: cover;
- border: 1px solid var(--border);
-}
-
-.sidebar-user-avatar-placeholder {
- width: 32px;
- height: 32px;
- border-radius: var(--radius-full);
- background: var(--bg-tertiary);
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-tertiary);
- border: 1px solid var(--border);
-}
-
-.sidebar-user-info {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- flex: 1;
-}
-
-.sidebar-user-name {
- font-size: 0.85rem;
- font-weight: 600;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.sidebar-user-handle {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.sidebar-user-menu {
- position: absolute;
- bottom: calc(100% + 12px);
- left: 0;
- right: 0;
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- padding: 6px;
- box-shadow: var(--shadow-lg);
- z-index: 50;
- animation: slideUp 0.15s ease-out;
-}
-
-@keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(4px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.sidebar-user-menu-item {
- display: flex;
- align-items: center;
- gap: 10px;
- width: 100%;
- padding: 8px 12px;
- border: none;
- background: transparent;
- color: var(--text-secondary);
- font-size: 0.85rem;
- font-weight: 500;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.1s;
- text-decoration: none;
-}
-
-.sidebar-user-menu-item:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.sidebar-user-menu-item.danger {
- color: var(--error);
-}
-
-.sidebar-user-menu-item.danger:hover {
- background: rgba(217, 119, 102, 0.1);
-}
-
-.right-sidebar {
- height: 100%;
- display: flex;
- flex-direction: column;
- gap: 32px;
- background: var(--bg-primary);
- border-left: 1px solid var(--border);
- padding: 24px 20px;
- overflow-y: auto;
- font-family: var(--font-sans);
-}
-
-.sidebar-section {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.sidebar-section-title {
- font-size: 0.75rem;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-tertiary);
- margin-bottom: 4px;
-}
-
-.sidebar-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-}
-
-.sidebar-tag-pill {
- padding: 6px 12px;
- background: var(--bg-tertiary);
- color: var(--text-secondary);
- border-radius: var(--radius-md);
- font-size: 0.8rem;
- font-weight: 500;
- text-decoration: none;
- transition: all 0.15s ease;
- border: 1px solid transparent;
-}
-
-.sidebar-tag-pill:hover {
- background: var(--bg-card);
- border-color: var(--border);
- color: var(--text-primary);
- transform: translateY(-1px);
- box-shadow: var(--shadow-sm);
-}
-
-.sidebar-extension-link {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px 14px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- color: var(--text-primary);
- font-size: 0.85rem;
- font-weight: 600;
- text-decoration: none;
- transition: all 0.15s ease;
- box-shadow: var(--shadow-sm);
-}
-
-.sidebar-extension-link:hover {
- background: var(--bg-hover);
- border-color: var(--border-hover);
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
-}
-
-.sidebar-external-icon {
- margin-left: auto;
- opacity: 0.5;
- transition: transform 0.2s;
-}
-
-.sidebar-extension-link:hover .sidebar-external-icon {
- opacity: 1;
- color: var(--accent);
- transform: translate(2px, -2px);
-}
-
-.sidebar-links {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.sidebar-link-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 8px 12px;
- width: fit-content;
- color: var(--text-secondary);
- font-size: 0.85rem;
- font-weight: 500;
- text-decoration: none;
- transition: all 0.15s ease;
- border-radius: var(--radius-md);
-}
-
-.sidebar-link-item:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
- transform: translateX(4px);
-}
-
-.sidebar-link-item svg {
- width: 18px;
- height: 18px;
- opacity: 0.7;
-}
-
-.sidebar-tangled-icon {
- width: 18px;
- height: 18px;
- background-color: var(--text-secondary);
- -webkit-mask-image: var(--tangled-logo);
- mask-image: var(--tangled-logo);
- -webkit-mask-size: contain;
- mask-size: contain;
- -webkit-mask-repeat: no-repeat;
- mask-repeat: no-repeat;
- -webkit-mask-position: center;
- mask-position: center;
- opacity: 0.7;
- transition:
- background-color 0.15s,
- opacity 0.15s;
-}
-
-.sidebar-link-item:hover svg {
- opacity: 1;
- color: var(--accent);
-}
-
-.sidebar-link-item:hover .sidebar-tangled-icon {
- background-color: var(--accent);
- opacity: 1;
-}
-
-.sidebar-theme-toggle {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 10px;
- background: var(--bg-tertiary);
- border: 1px solid transparent;
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: 0.85rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.15s ease;
- width: 100%;
- text-align: left;
-}
-
-.sidebar-theme-toggle:hover {
- background: var(--bg-card);
- border-color: var(--border);
- color: var(--text-primary);
-}
-
-.sidebar-footer-links {
- display: flex;
- align-items: center;
- gap: 12px;
- font-size: 0.75rem;
- color: var(--text-tertiary);
- margin-top: auto;
- padding-top: 12px;
- border-top: 1px solid var(--border);
-}
-
-.sidebar-footer-links a {
- color: var(--text-tertiary);
- text-decoration: none;
- transition: color 0.15s;
-}
-
-.sidebar-footer-links a:hover {
- color: var(--text-secondary);
- text-decoration: underline;
-}
-
-@media (max-width: 1024px) {
- .right-sidebar {
- display: none;
- }
-
- .app-layout {
- grid-template-columns: 240px 1fr;
- }
-}
-
-@media (max-width: 768px) {
- .left-sidebar {
- display: none;
- }
-
- .app-layout {
- grid-template-columns: 1fr;
- }
-}
diff --git a/web/src/css/skeleton.css b/web/src/css/skeleton.css
deleted file mode 100644
index bb70640..0000000
--- a/web/src/css/skeleton.css
+++ /dev/null
@@ -1,104 +0,0 @@
-@keyframes shimmer {
- 0% {
- background-position: -200% 0;
- }
- 100% {
- background-position: 200% 0;
- }
-}
-
-.skeleton {
- background: linear-gradient(
- 90deg,
- var(--bg-tertiary) 25%,
- var(--bg-hover) 50%,
- var(--bg-tertiary) 75%
- );
- background-size: 200% 100%;
- animation: shimmer 1.5s infinite;
- border-radius: var(--radius-sm);
-}
-
-.skeleton-card {
- padding: var(--spacing-md);
- display: flex;
- flex-direction: column;
- gap: var(--spacing-sm);
-}
-
-.skeleton-header {
- display: flex;
- align-items: center;
- gap: var(--spacing-sm);
-}
-
-.skeleton-avatar {
- width: 32px;
- height: 32px;
- border-radius: var(--radius-full);
- flex-shrink: 0;
-}
-
-.skeleton-meta {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.skeleton-name {
- width: 100px;
- height: 12px;
-}
-
-.skeleton-handle {
- width: 70px;
- height: 10px;
-}
-
-.skeleton-content {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-sm);
- padding-left: 40px;
-}
-
-.skeleton-source {
- width: 140px;
- height: 10px;
-}
-
-.skeleton-highlight {
- width: 100%;
- height: 48px;
- border-radius: var(--radius-sm);
-}
-
-.skeleton-text-1 {
- width: 85%;
- height: 12px;
-}
-
-.skeleton-text-2 {
- width: 55%;
- height: 12px;
-}
-
-.skeleton-actions {
- display: flex;
- gap: var(--spacing-md);
- padding-left: 40px;
- margin-top: var(--spacing-xs);
-}
-
-.skeleton-action {
- width: 20px;
- height: 20px;
- border-radius: var(--radius-sm);
-}
-
-@media (max-width: 768px) {
- .skeleton-content,
- .skeleton-actions {
- padding-left: 0;
- }
-}
diff --git a/web/src/css/utilities.css b/web/src/css/utilities.css
deleted file mode 100644
index 854c9ed..0000000
--- a/web/src/css/utilities.css
+++ /dev/null
@@ -1,747 +0,0 @@
-.legal-content {
- max-width: 800px;
- margin: 0 auto;
- padding: 20px;
-}
-
-.legal-content h1 {
- font-size: 2rem;
- margin-bottom: 8px;
- color: var(--text-primary);
-}
-
-.legal-content h2 {
- font-size: 1.4rem;
- margin-top: 32px;
- margin-bottom: 12px;
- color: var(--text-primary);
-}
-
-.legal-content h3 {
- font-size: 1.1rem;
- margin-top: 20px;
- margin-bottom: 8px;
- color: var(--text-primary);
-}
-
-.legal-content p {
- color: var(--text-secondary);
- line-height: 1.7;
- margin-bottom: 12px;
-}
-
-.legal-content ul {
- color: var(--text-secondary);
- line-height: 1.7;
- margin-left: 24px;
- margin-bottom: 12px;
-}
-
-.legal-content li {
- margin-bottom: 6px;
-}
-
-.legal-content a {
- color: var(--accent);
- text-decoration: none;
-}
-
-.legal-content a:hover {
- text-decoration: underline;
-}
-
-.legal-content section {
- margin-bottom: 24px;
-}
-
-.text-secondary {
- color: var(--text-secondary);
-}
-
-.text-error {
- color: var(--error);
-}
-
-.text-center {
- text-align: center;
-}
-
-.flex {
- display: flex;
-}
-
-.items-center {
- align-items: center;
-}
-
-.justify-center {
- justify-content: center;
-}
-
-.justify-end {
- justify-content: flex-end;
-}
-
-.gap-2 {
- gap: 8px;
-}
-
-.gap-3 {
- gap: 12px;
-}
-
-.mt-3 {
- margin-top: 12px;
-}
-
-.mb-6 {
- margin-bottom: 24px;
-}
-
-.composer {
- margin-bottom: 24px;
-}
-
-.composer-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
-}
-
-.composer-title {
- font-size: 1.1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0;
-}
-
-.composer-input {
- width: 100%;
- min-height: 120px;
- padding: 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 1rem;
- resize: vertical;
- transition: all 0.15s ease;
-}
-
-.composer-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.composer-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 12px;
-}
-
-.composer-actions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
-}
-
-.composer-count {
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
-
-.composer-count.warning {
- color: var(--warning);
-}
-
-.composer-count.error {
- color: var(--error);
-}
-
-.composer-char-count.warning {
- color: var(--warning);
-}
-
-.composer-char-count.error {
- color: var(--error);
-}
-
-.composer-add-quote {
- width: 100%;
- padding: 12px 16px;
- margin-bottom: 12px;
- background: var(--bg-tertiary);
- border: 1px dashed var(--border);
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: 0.9rem;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.composer-add-quote:hover {
- border-color: var(--accent);
- color: var(--accent);
- background: var(--accent-subtle);
-}
-
-.composer-quote-input-wrapper {
- margin-bottom: 12px;
-}
-
-.composer-quote-input {
- width: 100%;
- padding: 12px 16px;
- background: linear-gradient(
- 135deg,
- rgba(79, 70, 229, 0.05),
- rgba(168, 85, 247, 0.05)
- );
- border: 1px solid var(--border);
- border-left: 3px solid var(--accent);
- border-radius: 0 var(--radius-md) var(--radius-md) 0;
- color: var(--text-primary);
- font-size: 0.95rem;
- font-style: italic;
- resize: vertical;
- font-family: inherit;
- transition: all 0.15s ease;
-}
-
-.composer-quote-input:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.composer-quote-input::placeholder {
- color: var(--text-tertiary);
- font-style: italic;
-}
-
-.composer-quote-remove-btn {
- margin-top: 8px;
- padding: 6px 12px;
- background: none;
- border: none;
- color: var(--text-tertiary);
- font-size: 0.85rem;
- cursor: pointer;
-}
-
-.composer-quote-remove-btn:hover {
- color: var(--error);
-}
-
-.composer-error {
- margin-top: 12px;
- padding: 12px;
- background: rgba(239, 68, 68, 0.1);
- border: 1px solid rgba(239, 68, 68, 0.3);
- border-radius: var(--radius-md);
- color: var(--error);
- font-size: 0.9rem;
-}
-
-.composer-url {
- font-size: 0.85rem;
- color: var(--text-secondary);
- word-break: break-all;
-}
-
-.composer-quote {
- position: relative;
- padding: 12px 16px;
- padding-right: 36px;
- background: var(--bg-secondary);
- border-left: 3px solid var(--accent);
- border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
- margin-bottom: 16px;
- font-style: italic;
- color: var(--text-secondary);
- overflow-wrap: break-word;
- word-break: break-word;
- max-width: 100%;
-}
-
-.composer-quote-remove {
- position: absolute;
- top: 8px;
- right: 8px;
- width: 24px;
- height: 24px;
- border-radius: var(--radius-full);
- background: var(--bg-tertiary);
- color: var(--text-secondary);
- font-size: 1rem;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.composer-quote-remove:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.composer-tags {
- flex: 1;
-}
-
-.composer-meta-row {
- display: flex;
- gap: 12px;
- margin-top: 12px;
- align-items: flex-start;
-}
-
-.composer-labels-wrapper {
- position: relative;
-}
-
-.composer-labels-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 42px;
- height: 42px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- cursor: pointer;
- color: var(--text-tertiary);
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- position: relative;
-}
-
-.composer-labels-btn:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
- border-color: var(--text-tertiary);
-}
-
-.composer-labels-btn.active {
- color: var(--accent);
- background: var(--accent-subtle);
- border-color: var(--accent);
-}
-
-.composer-labels-badge {
- position: absolute;
- top: -4px;
- right: -4px;
- background: var(--error);
- color: white;
- font-size: 0.7rem;
- width: 18px;
- height: 18px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: bold;
- border: 2px solid var(--bg-primary);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
-}
-
-.composer-labels-picker {
- position: absolute;
- bottom: 100%;
- right: 0;
- margin-bottom: 12px;
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 8px 0;
- min-width: 200px;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
- z-index: 50;
- animation: scaleIn 0.2s ease-out forwards;
- transform-origin: bottom right;
-}
-
-@keyframes scaleIn {
- from {
- opacity: 0;
- transform: scale(0.95) translateY(5px);
- }
-
- to {
- opacity: 1;
- transform: scale(1) translateY(0);
- }
-}
-
-.picker-header {
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--text-tertiary);
- text-transform: uppercase;
- letter-spacing: 0.05em;
- margin-bottom: 4px;
- padding: 4px 12px 8px;
- border-bottom: 1px solid var(--border);
-}
-
-.picker-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 14px;
- cursor: pointer;
- color: var(--text-secondary);
- font-size: 0.9rem;
- transition: all 0.15s ease;
- user-select: none;
-}
-
-.picker-item:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.picker-checkbox-wrapper {
- position: relative;
- width: 18px;
- height: 18px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.picker-checkbox-wrapper input {
- position: absolute;
- opacity: 0;
- width: 100%;
- height: 100%;
- cursor: pointer;
- z-index: 10;
-}
-
-.picker-checkbox-custom {
- width: 18px;
- height: 18px;
- border: 2px solid var(--text-tertiary);
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: transparent;
- transition: all 0.2s ease;
- color: white;
-}
-
-.picker-item:hover .picker-checkbox-custom {
- border-color: var(--text-secondary);
-}
-
-.picker-checkbox-wrapper input:checked + .picker-checkbox-custom {
- background: var(--accent);
- border-color: var(--accent);
- color: white;
-}
-
-.composer-tags-input {
- width: 100%;
- padding: 12px 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 0.95rem;
- transition: all 0.15s ease;
-}
-
-.composer-tags-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.composer-tags-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.history-panel {
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 1rem;
- margin-bottom: 1rem;
- font-size: 0.9rem;
- animation: fadeIn 0.2s ease-out;
-}
-
-.history-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1rem;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--border);
-}
-
-.history-title {
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- font-size: 0.75rem;
- color: var(--text-secondary);
-}
-
-.history-list {
- list-style: none;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.history-item {
- position: relative;
- padding-left: 1rem;
- border-left: 2px solid var(--border);
-}
-
-.history-date {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- margin-bottom: 0.25rem;
-}
-
-.history-content {
- color: var(--text-secondary);
- white-space: pre-wrap;
-}
-
-.history-close-btn {
- color: var(--text-tertiary);
- padding: 4px;
- border-radius: var(--radius-sm);
- transition: all 0.2s;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.history-close-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.history-status {
- text-align: center;
- color: var(--text-tertiary);
- font-style: italic;
- padding: 1rem;
-}
-
-.share-menu-container {
- position: relative;
-}
-
-.share-menu {
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-lg);
- min-width: 220px;
- max-width: calc(100vw - 32px);
- padding: 8px;
- z-index: 9999;
- animation: fadeInUp 0.15s ease;
-}
-
-@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(-8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.share-menu-section {
- display: flex;
- flex-direction: column;
-}
-
-.share-menu-label {
- padding: 4px 12px 8px;
- font-size: 0.7rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-tertiary);
-}
-
-.share-menu-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 14px;
- background: none;
- border: none;
- border-radius: var(--radius-md);
- width: 100%;
- text-align: left;
- font-size: 0.875rem;
- color: var(--text-primary);
- cursor: pointer;
- transition: all 0.1s ease;
-}
-
-.share-menu-item:hover {
- background: var(--bg-hover);
-}
-
-.share-menu-icon {
- font-size: 1.1rem;
- width: 24px;
- text-align: center;
-}
-
-.share-menu-divider {
- height: 1px;
- background: var(--border);
- margin: 6px 0;
-}
-
-.bookmark-card {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.bookmark-preview {
- display: flex;
- flex-direction: column;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- overflow: hidden;
- text-decoration: none;
- transition: all 0.2s ease;
- position: relative;
-}
-
-.bookmark-preview:hover {
- border-color: var(--accent);
- box-shadow: var(--shadow-sm);
- transform: translateY(-1px);
-}
-
-.bookmark-preview::before {
- content: "";
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 4px;
- background: var(--accent);
- opacity: 0.7;
-}
-
-.bookmark-preview-content {
- padding: 16px 20px;
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.bookmark-preview-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 4px;
-}
-
-.bookmark-preview-site {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--accent);
- text-transform: uppercase;
- letter-spacing: 0.03em;
-}
-
-.bookmark-preview-title {
- font-size: 1rem;
- font-weight: 600;
- line-height: 1.4;
- color: var(--text-primary);
- margin: 0;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.bookmark-preview-desc {
- font-size: 0.875rem;
- color: var(--text-secondary);
- line-height: 1.5;
- margin: 0;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.bookmark-preview-arrow {
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-tertiary);
- padding: 0 4px;
- transition: all 0.2s ease;
-}
-
-.bookmark-preview:hover .bookmark-preview-arrow {
- color: var(--accent);
- transform: translateX(2px);
-}
-
-.bookmark-description {
- font-size: 0.9rem;
- color: var(--text-secondary);
- margin: 0;
- line-height: 1.5;
-}
-
-.bookmark-meta {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-top: 12px;
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
-
-.bookmark-time {
- color: var(--text-tertiary);
-}
-
-.bookmark-preview {
- max-width: 100%;
- width: 100%;
- box-sizing: border-box;
-}
-
-@media (max-width: 600px) {
- .bookmark-preview-content {
- padding: 12px 14px;
- }
-
- .legal-content {
- padding: 16px;
- }
-}
diff --git a/web/src/index.css b/web/src/index.css
deleted file mode 100644
index 9731910..0000000
--- a/web/src/index.css
+++ /dev/null
@@ -1,15 +0,0 @@
-@import "./css/layout.css";
-@import "./css/sidebar.css";
-@import "./css/base.css";
-@import "./css/buttons.css";
-@import "./css/cards.css";
-@import "./css/feed.css";
-@import "./css/profile.css";
-@import "./css/login.css";
-@import "./css/annotations.css";
-@import "./css/collections.css";
-@import "./css/modals.css";
-@import "./css/notifications.css";
-@import "./css/skeleton.css";
-@import "./css/utilities.css";
-@import "./css/landing.css";
diff --git a/web/src/main.jsx b/web/src/main.jsx
deleted file mode 100644
index 2993a02..0000000
--- a/web/src/main.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-import { BrowserRouter } from "react-router-dom";
-import App from "./App";
-import "./index.css";
-
-ReactDOM.createRoot(document.getElementById("root")).render(
-
-
-
-
- ,
-);
diff --git a/web/src/pages/AnnotationDetail.jsx b/web/src/pages/AnnotationDetail.jsx
deleted file mode 100644
index 2ed48e3..0000000
--- a/web/src/pages/AnnotationDetail.jsx
+++ /dev/null
@@ -1,254 +0,0 @@
-import { useState, useEffect } from "react";
-import { useParams, Link, useLocation, useNavigate } from "react-router-dom";
-import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
-import BookmarkCard from "../components/BookmarkCard";
-import ReplyList from "../components/ReplyList";
-import {
- getAnnotation,
- getReplies,
- createReply,
- deleteReply,
- resolveHandle,
- normalizeAnnotation,
-} from "../api/client";
-import { useAuth } from "../context/AuthContext";
-import { MessageSquare } from "lucide-react";
-
-export default function AnnotationDetail() {
- const { uri, did, rkey, handle, type } = useParams();
- const location = useLocation();
- const navigate = useNavigate();
- const { isAuthenticated, user } = useAuth();
- const [annotation, setAnnotation] = useState(null);
- const [replies, setReplies] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- const [replyText, setReplyText] = useState("");
- const [posting, setPosting] = useState(false);
- const [replyingTo, setReplyingTo] = useState(null);
-
- const [targetUri, setTargetUri] = useState(uri);
-
- useEffect(() => {
- async function resolve() {
- if (uri) {
- setTargetUri(uri);
- return;
- }
-
- if (handle && rkey) {
- let collection = "at.margin.annotation";
- if (type === "highlight") collection = "at.margin.highlight";
- if (type === "bookmark") collection = "at.margin.bookmark";
-
- try {
- const resolvedDid = await resolveHandle(handle);
- if (resolvedDid) {
- setTargetUri(`at://${resolvedDid}/${collection}/${rkey}`);
- }
- } catch (e) {
- console.error("Failed to resolve handle:", e);
- }
- } else if (did && rkey) {
- setTargetUri(`at://${did}/at.margin.annotation/${rkey}`);
- } else {
- const pathParts = location.pathname.split("/");
- const atIndex = pathParts.indexOf("at");
- if (
- atIndex !== -1 &&
- pathParts[atIndex + 1] &&
- pathParts[atIndex + 2]
- ) {
- setTargetUri(
- `at://${pathParts[atIndex + 1]}/at.margin.annotation/${pathParts[atIndex + 2]}`,
- );
- }
- }
- }
- resolve();
- }, [uri, did, rkey, handle, type, location.pathname]);
-
- const refreshReplies = async () => {
- if (!targetUri) return;
- const repliesData = await getReplies(targetUri);
- setReplies(repliesData.items || []);
- };
-
- useEffect(() => {
- async function fetchData() {
- if (!targetUri) return;
-
- try {
- setLoading(true);
- const [annData, repliesData] = await Promise.all([
- getAnnotation(targetUri),
- getReplies(targetUri).catch(() => ({ items: [] })),
- ]);
- setAnnotation(normalizeAnnotation(annData));
- setReplies(repliesData.items || []);
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- }
- fetchData();
- }, [targetUri]);
-
- const handleReply = async (e) => {
- if (e) e.preventDefault();
- if (!replyText.trim()) return;
-
- try {
- setPosting(true);
- const parentUri = replyingTo
- ? replyingTo.id || replyingTo.uri
- : targetUri;
- const parentCid = replyingTo
- ? replyingTo.cid || ""
- : annotation?.cid || "";
-
- await createReply({
- parentUri,
- parentCid,
- rootUri: targetUri,
- rootCid: annotation?.cid || "",
- text: replyText,
- });
- setReplyText("");
- setReplyingTo(null);
- await refreshReplies();
- } catch (err) {
- alert("Failed to post reply: " + err.message);
- } finally {
- setPosting(false);
- }
- };
-
- const handleDeleteReply = async (reply) => {
- if (!confirm("Delete this reply?")) return;
- try {
- await deleteReply(reply.id || reply.uri);
- await refreshReplies();
- } catch (err) {
- alert("Failed to delete: " + err.message);
- }
- };
-
- if (loading) {
- return (
-
- );
- }
-
- if (error || !annotation) {
- return (
-
-
-
โ ๏ธ
-
Annotation not found
-
- {error || "This annotation may have been deleted."}
-
-
- Back to Feed
-
-
-
- );
- }
-
- return (
-
-
-
- โ Back to Feed
-
-
-
- {annotation.type === "Highlight" ? (
-
navigate("/home")}
- />
- ) : annotation.type === "Bookmark" ? (
- navigate("/home")}
- />
- ) : (
-
- )}
-
- {annotation.type !== "Bookmark" && annotation.type !== "Highlight" && (
-
-
-
- Replies ({replies.length})
-
-
- {isAuthenticated && (
-
- {replyingTo && (
-
-
- Replying to @
- {(replyingTo.creator || replyingTo.author)?.handle ||
- "unknown"}
-
- setReplyingTo(null)}
- className="cancel-reply"
- >
- ร
-
-
- )}
-
setReplyText(e.target.value)}
- placeholder={
- replyingTo
- ? `Reply to @${(replyingTo.creator || replyingTo.author)?.handle}...`
- : "Write a reply..."
- }
- className="reply-input"
- rows={3}
- disabled={posting}
- />
-
- handleReply()}
- >
- {posting ? "Posting..." : "Reply"}
-
-
-
- )}
-
-
setReplyingTo(reply)}
- onDelete={handleDeleteReply}
- isInline={false}
- />
-
- )}
-
- );
-}
diff --git a/web/src/pages/Bookmarks.jsx b/web/src/pages/Bookmarks.jsx
deleted file mode 100644
index bac1128..0000000
--- a/web/src/pages/Bookmarks.jsx
+++ /dev/null
@@ -1,333 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { Link } from "react-router-dom";
-import { Plus } from "lucide-react";
-import { useAuth } from "../context/AuthContext";
-import {
- getUserBookmarks,
- deleteBookmark,
- createBookmark,
- getURLMetadata,
-} from "../api/client";
-import { BookmarkIcon } from "../components/Icons";
-import BookmarkCard from "../components/BookmarkCard";
-import CollectionItemCard from "../components/CollectionItemCard";
-import AddToCollectionModal from "../components/AddToCollectionModal";
-
-export default function Bookmarks() {
- const { user, isAuthenticated, loading } = useAuth();
- const [bookmarks, setBookmarks] = useState([]);
- const [loadingBookmarks, setLoadingBookmarks] = useState(true);
- const [error, setError] = useState(null);
- const [showAddForm, setShowAddForm] = useState(false);
- const [newUrl, setNewUrl] = useState("");
- const [newTitle, setNewTitle] = useState("");
- const [submitting, setSubmitting] = useState(false);
- const [fetchingTitle, setFetchingTitle] = useState(false);
- const [collectionModalState, setCollectionModalState] = useState({
- isOpen: false,
- uri: null,
- });
-
- const loadBookmarks = useCallback(async () => {
- if (!user?.did) return;
-
- try {
- setLoadingBookmarks(true);
- const data = await getUserBookmarks(user.did);
- setBookmarks(data.items || []);
- } catch (err) {
- console.error("Failed to load bookmarks:", err);
- setError(err.message);
- } finally {
- setLoadingBookmarks(false);
- }
- }, [user]);
-
- useEffect(() => {
- if (isAuthenticated && user) {
- loadBookmarks();
- }
- }, [isAuthenticated, user, loadBookmarks]);
-
- const handleDelete = async (uri) => {
- if (!confirm("Delete this bookmark?")) return;
-
- try {
- const parts = uri.split("/");
- const rkey = parts[parts.length - 1];
- await deleteBookmark(rkey);
- setBookmarks((prev) => prev.filter((b) => (b.id || b.uri) !== uri));
- } catch (err) {
- alert("Failed to delete: " + err.message);
- }
- };
-
- const handleUrlBlur = async () => {
- if (!newUrl.trim() || newTitle.trim()) return;
- try {
- new URL(newUrl);
- } catch {
- return;
- }
- try {
- setFetchingTitle(true);
- const data = await getURLMetadata(newUrl.trim());
- if (data.title && !newTitle) {
- setNewTitle(data.title);
- }
- } catch (err) {
- console.error("Failed to fetch title:", err);
- } finally {
- setFetchingTitle(false);
- }
- };
-
- const handleAddBookmark = async (e) => {
- e.preventDefault();
- if (!newUrl.trim()) return;
-
- try {
- setSubmitting(true);
- await createBookmark(newUrl.trim(), newTitle.trim() || undefined);
- setNewUrl("");
- setNewTitle("");
- setShowAddForm(false);
- await loadBookmarks();
- } catch (err) {
- alert("Failed to add bookmark: " + err.message);
- } finally {
- setSubmitting(false);
- }
- };
-
- if (loading)
- return (
-
- );
-
- if (!isAuthenticated) {
- return (
-
-
-
Sign in to view your bookmarks
-
- You need to be logged in with your Bluesky account
-
-
- Sign in with Bluesky
-
-
-
- );
- }
-
- return (
-
-
-
-
My Bookmarks
-
Pages you've saved for later
-
-
setShowAddForm(!showAddForm)}
- className="btn btn-primary"
- >
-
- Add Bookmark
-
-
-
- {showAddForm && (
-
-
- Add a Bookmark
-
-
-
-
-
- URL *
-
- setNewUrl(e.target.value)}
- onBlur={handleUrlBlur}
- className="input"
- style={{ width: "100%" }}
- required
- autoFocus
- />
-
-
-
- Title{" "}
- {fetchingTitle ? (
- Fetching...
- ) : (
-
- (auto-fetched)
-
- )}
-
- setNewTitle(e.target.value)}
- className="input"
- style={{ width: "100%" }}
- />
-
-
- {
- setShowAddForm(false);
- setNewUrl("");
- setNewTitle("");
- }}
- className="btn btn-secondary"
- >
- Cancel
-
-
- {submitting ? "Adding..." : "Save Bookmark"}
-
-
-
-
-
- )}
-
- {loadingBookmarks ? (
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
- ) : error ? (
-
-
โ ๏ธ
-
Error loading bookmarks
-
{error}
-
- ) : bookmarks.length === 0 ? (
-
-
-
-
-
No bookmarks yet
-
- Click "Add Bookmark" above to save a page, or use the
- browser extension.
-
-
- ) : (
-
-
- {bookmarks.map((bookmark) => {
- if (bookmark.type === "CollectionItem") {
- return (
-
- setCollectionModalState({
- isOpen: true,
- uri: uri,
- })
- }
- />
- );
- }
- return (
-
- setCollectionModalState({
- isOpen: true,
- uri: bookmark.uri || bookmark.id,
- })
- }
- />
- );
- })}
-
-
- )}
- {collectionModalState.isOpen && (
-
setCollectionModalState({ isOpen: false, uri: null })}
- annotationUri={collectionModalState.uri}
- />
- )}
-
- );
-}
diff --git a/web/src/pages/CollectionDetail.jsx b/web/src/pages/CollectionDetail.jsx
deleted file mode 100644
index 073f212..0000000
--- a/web/src/pages/CollectionDetail.jsx
+++ /dev/null
@@ -1,314 +0,0 @@
-import { useState, useEffect } from "react";
-import { useParams, useNavigate, Link, useLocation } from "react-router-dom";
-import { ArrowLeft, Edit2, Trash2, Plus, ExternalLink } from "lucide-react";
-import {
- getCollection,
- getCollectionItems,
- removeItemFromCollection,
- deleteCollection,
- resolveHandle,
-} from "../api/client";
-import { useAuth } from "../context/AuthContext";
-import CollectionModal from "../components/CollectionModal";
-import CollectionIcon from "../components/CollectionIcon";
-import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
-import BookmarkCard from "../components/BookmarkCard";
-import ShareMenu from "../components/ShareMenu";
-
-export default function CollectionDetail() {
- const { rkey, handle, "*": wildcardPath } = useParams();
- const location = useLocation();
- const navigate = useNavigate();
- const { user } = useAuth();
-
- const [collection, setCollection] = useState(null);
- const [items, setItems] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [isEditModalOpen, setIsEditModalOpen] = useState(false);
-
- const [refreshTrigger, setRefreshTrigger] = useState(0);
-
- const searchParams = new URLSearchParams(location.search);
- const paramAuthorDid = searchParams.get("author");
-
- const isOwner =
- user?.did &&
- (collection?.creator?.did === user.did || paramAuthorDid === user.did);
-
- useEffect(() => {
- let active = true;
-
- const fetchContext = async () => {
- if (active) {
- setLoading(true);
- setError(null);
- }
-
- try {
- let targetUri = null;
- let targetDid = paramAuthorDid || user?.did;
-
- if (handle && rkey) {
- try {
- targetDid = await resolveHandle(handle);
- if (!active) return;
- targetUri = `at://${targetDid}/at.margin.collection/${rkey}`;
- } catch (e) {
- console.error("Failed to resolve handle", e);
- if (active) setError("Could not resolve user handle");
- }
- } else if (wildcardPath) {
- targetUri = decodeURIComponent(wildcardPath);
- } else if (rkey && targetDid) {
- targetUri = `at://${targetDid}/at.margin.collection/${rkey}`;
- }
-
- if (!targetUri) {
- if (active) {
- if (!user && !handle && !paramAuthorDid) {
- setError("Please log in to view your collections");
- } else if (!error) {
- setError("Invalid collection URL");
- }
- }
- return;
- }
-
- if (!targetDid && targetUri.startsWith("at://")) {
- const parts = targetUri.split("/");
- if (parts.length > 2) targetDid = parts[2];
- }
-
- const collectionData = await getCollection(targetUri);
- if (!active) return;
-
- setCollection(collectionData);
-
- const itemsData = await getCollectionItems(collectionData.uri);
- if (!active) return;
-
- setItems(itemsData || []);
- } catch (err) {
- console.error("Fetch failed:", err);
- if (active) {
- if (
- err.message.includes("404") ||
- err.message.includes("not found")
- ) {
- setError("Collection not found");
- } else {
- setError(err.message || "Failed to load collection");
- }
- }
- } finally {
- if (active) setLoading(false);
- }
- };
-
- fetchContext();
-
- return () => {
- active = false;
- };
- }, [
- paramAuthorDid,
- user?.did,
- handle,
- rkey,
- wildcardPath,
- refreshTrigger,
- error,
- user,
- ]);
-
- const handleEditSuccess = () => {
- setIsEditModalOpen(false);
- setRefreshTrigger((v) => v + 1);
- };
-
- const handleDeleteItem = async (itemUri) => {
- if (!confirm("Remove this item from the collection?")) return;
- try {
- await removeItemFromCollection(itemUri);
- setItems((prev) => prev.filter((i) => i.uri !== itemUri));
- } catch (err) {
- console.error(err);
- alert("Failed to remove item");
- }
- };
-
- if (loading) {
- return (
-
- );
- }
-
- if (error || !collection) {
- return (
-
-
-
โ ๏ธ
-
- {error || "Collection not found"}
-
-
navigate("/collections")}
- className="btn btn-secondary"
- style={{ marginTop: "16px" }}
- >
- Back to Collections
-
-
-
- );
- }
-
- return (
-
-
-
-
Collections
-
-
-
-
-
-
-
-
{collection.name}
- {collection.description && (
-
{collection.description}
- )}
-
-
- {items.length} {items.length === 1 ? "item" : "items"}
-
- ยท
-
- Created {new Date(collection.createdAt).toLocaleDateString()}
-
-
-
-
-
- {isOwner && (
- <>
- {collection.uri.includes("network.cosmik.collection") ? (
-
- Manage on Semble
-
-
- ) : (
- <>
-
setIsEditModalOpen(true)}
- className="collection-detail-edit"
- title="Edit Collection"
- >
-
-
-
{
- if (
- confirm("Delete this collection and all its items?")
- ) {
- await deleteCollection(collection.uri);
- navigate("/collections");
- }
- }}
- className="collection-detail-delete"
- title="Delete Collection"
- >
-
-
- >
- )}
- >
- )}
-
-
-
-
-
- {items.length === 0 ? (
-
-
-
Collection is empty
-
- {isOwner
- ? 'Add items to this collection from your feed or bookmarks using the "Collect" button.'
- : "This collection has no items yet."}
-
-
- ) : (
- items.map((item) => (
-
- {isOwner &&
- !collection.uri.includes("network.cosmik.collection") && (
-
handleDeleteItem(item.uri)}
- className="collection-item-remove"
- title="Remove from collection"
- >
-
-
- )}
-
- {item.annotation ? (
-
- ) : item.highlight ? (
-
- ) : item.bookmark ? (
-
- ) : (
-
-
Item could not be loaded
-
- )}
-
- ))
- )}
-
-
-
- {isOwner && (
-
setIsEditModalOpen(false)}
- onSuccess={handleEditSuccess}
- collectionToEdit={collection}
- />
- )}
-
- );
-}
diff --git a/web/src/pages/Collections.jsx b/web/src/pages/Collections.jsx
deleted file mode 100644
index c437b3c..0000000
--- a/web/src/pages/Collections.jsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { Folder, Plus } from "lucide-react";
-import { getCollections } from "../api/client";
-import { useAuth } from "../context/AuthContext";
-import CollectionModal from "../components/CollectionModal";
-import CollectionRow from "../components/CollectionRow";
-
-export default function Collections() {
- const { user } = useAuth();
- const [collections, setCollections] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
- const [editingCollection, setEditingCollection] = useState(null);
-
- const fetchCollections = useCallback(async () => {
- try {
- setLoading(true);
- const data = await getCollections(user.did);
- setCollections(data.items || []);
- } catch (err) {
- console.error(err);
- setError("Failed to load collections");
- } finally {
- setLoading(false);
- }
- }, [user]);
-
- useEffect(() => {
- if (user) {
- fetchCollections();
- }
- }, [user, fetchCollections]);
-
- const handleCreateSuccess = () => {
- fetchCollections();
- setIsCreateModalOpen(false);
- setEditingCollection(null);
- };
-
- const handleDelete = () => {
- fetchCollections();
- setEditingCollection(null);
- };
-
- if (loading) {
- return (
-
- );
- }
-
- return (
-
-
-
-
Collections
-
- Organize your annotations, highlights, and bookmarks
-
-
-
setIsCreateModalOpen(true)}
- className="btn btn-primary"
- >
-
- New Collection
-
-
-
- {error ? (
-
-
โ ๏ธ
-
Something went wrong
-
{error}
-
- ) : collections.length === 0 ? (
-
-
-
-
-
No collections yet
-
- Create your first collection to start organizing your web
- annotations.
-
-
setIsCreateModalOpen(true)}
- className="btn btn-secondary"
- >
- Create Collection
-
-
- ) : (
-
- {collections.map((collection) => (
- setEditingCollection(collection)}
- />
- ))}
-
- )}
-
-
{
- setIsCreateModalOpen(false);
- setEditingCollection(null);
- }}
- onSuccess={handleCreateSuccess}
- onDelete={handleDelete}
- collectionToEdit={editingCollection}
- />
-
- );
-}
diff --git a/web/src/pages/Feed.jsx b/web/src/pages/Feed.jsx
deleted file mode 100644
index 5cfbed0..0000000
--- a/web/src/pages/Feed.jsx
+++ /dev/null
@@ -1,429 +0,0 @@
-import { useState, useEffect, useMemo, useCallback } from "react";
-import { useSearchParams } from "react-router-dom";
-import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
-import BookmarkCard from "../components/BookmarkCard";
-import CollectionItemCard from "../components/CollectionItemCard";
-import AnnotationSkeleton from "../components/AnnotationSkeleton";
-import IOSInstallBanner from "../components/IOSInstallBanner";
-import { getAnnotationFeed, deleteHighlight } from "../api/client";
-import { AlertIcon, InboxIcon } from "../components/Icons";
-import { useAuth } from "../context/AuthContext";
-import { useTheme } from "../context/ThemeContext";
-import { X, ArrowUp } from "lucide-react";
-
-import AddToCollectionModal from "../components/AddToCollectionModal";
-
-export default function Feed() {
- const [searchParams, setSearchParams] = useSearchParams();
- const tagFilter = searchParams.get("tag");
-
- const [filter, setFilter] = useState(() => {
- return localStorage.getItem("feedFilter") || "all";
- });
-
- const [feedType, setFeedType] = useState(() => {
- return localStorage.getItem("feedType") || "all";
- });
-
- const [annotations, setAnnotations] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [hasMore, setHasMore] = useState(true);
- const [loadingMore, setLoadingMore] = useState(false);
-
- useEffect(() => {
- localStorage.setItem("feedFilter", filter);
- }, [filter]);
-
- useEffect(() => {
- localStorage.setItem("feedType", feedType);
- }, [feedType]);
-
- const [collectionModalState, setCollectionModalState] = useState({
- isOpen: false,
- uri: null,
- });
-
- const { user } = useAuth();
-
- const fetchFeed = useCallback(
- async (offset = 0) => {
- try {
- const isLoadMore = offset > 0;
- if (isLoadMore) {
- setLoadingMore(true);
- } else {
- setLoading(true);
- }
-
- let creatorDid = "";
-
- if (feedType === "my-feed") {
- if (user?.did) {
- creatorDid = user.did;
- } else {
- setAnnotations([]);
- setLoading(false);
- setLoadingMore(false);
- return;
- }
- }
-
- const motivationMap = {
- commenting: "commenting",
- highlighting: "highlighting",
- bookmarking: "bookmarking",
- };
- const motivation = motivationMap[filter] || "";
- const limit = 50;
-
- const data = await getAnnotationFeed(
- limit,
- offset,
- tagFilter || "",
- creatorDid,
- feedType,
- motivation,
- );
-
- const newItems = data.items || [];
- if (newItems.length < limit) {
- setHasMore(false);
- } else {
- setHasMore(true);
- }
-
- if (isLoadMore) {
- setAnnotations((prev) => [...prev, ...newItems]);
- } else {
- setAnnotations(newItems);
- }
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- setLoadingMore(false);
- }
- },
- [tagFilter, feedType, filter, user],
- );
-
- useEffect(() => {
- fetchFeed(0);
- }, [fetchFeed]);
-
- const deduplicatedAnnotations = useMemo(() => {
- const inCollectionUris = new Set();
- for (const item of annotations) {
- if (item.type === "CollectionItem") {
- const inner = item.annotation || item.highlight || item.bookmark;
- if (inner) {
- if (inner.uri) inCollectionUris.add(inner.uri.trim());
- if (inner.id) inCollectionUris.add(inner.id.trim());
- }
- }
- }
-
- const result = [];
-
- for (const item of annotations) {
- if (item.type !== "CollectionItem") {
- const itemUri = (item.uri || "").trim();
- const itemId = (item.id || "").trim();
- if (
- (itemUri && inCollectionUris.has(itemUri)) ||
- (itemId && inCollectionUris.has(itemId))
- ) {
- continue;
- }
- }
-
- result.push(item);
- }
-
- return result;
- }, [annotations]);
-
- const filteredAnnotations =
- feedType === "all" ||
- feedType === "popular" ||
- feedType === "semble" ||
- feedType === "margin" ||
- feedType === "shelved" ||
- feedType === "my-feed"
- ? filter === "all"
- ? deduplicatedAnnotations
- : deduplicatedAnnotations.filter((a) => {
- if (a.type === "CollectionItem") {
- if (filter === "commenting") return !!a.annotation;
- if (filter === "highlighting") return !!a.highlight;
- if (filter === "bookmarking") return !!a.bookmark;
- }
- if (filter === "commenting")
- return a.motivation === "commenting" || a.type === "Annotation";
- if (filter === "highlighting")
- return a.motivation === "highlighting" || a.type === "Highlight";
- if (filter === "bookmarking")
- return a.motivation === "bookmarking" || a.type === "Bookmark";
- return a.motivation === filter;
- })
- : deduplicatedAnnotations;
-
- return (
-
-
-
Feed
-
- See what people are annotating and bookmarking
-
-
-
- {tagFilter && (
-
-
- Filtering by #{tagFilter}
-
-
- setSearchParams((prev) => {
- const next = new URLSearchParams(prev);
- next.delete("tag");
- return next;
- })
- }
- className="active-filter-clear"
- aria-label="Clear filter"
- >
-
-
-
- )}
-
-
-
- {[
- { key: "all", label: "All" },
- { key: "popular", label: "Popular" },
- { key: "shelved", label: "Shelved" },
- { key: "margin", label: "Margin" },
- { key: "semble", label: "Semble" },
- ...(user ? [{ key: "my-feed", label: "Mine" }] : []),
- ].map(({ key, label }) => (
- setFeedType(key)}
- >
- {label}
-
- ))}
-
-
-
- {[
- { key: "all", label: "All" },
- { key: "commenting", label: "Notes" },
- { key: "highlighting", label: "Highlights" },
- { key: "bookmarking", label: "Bookmarks" },
- ].map(({ key, label }) => (
- setFilter(key)}
- >
- {label}
-
- ))}
-
-
-
-
-
- {loading ? (
-
-
- {[1, 2, 3, 4, 5].map((i) => (
-
- ))}
-
-
- ) : (
- <>
- {error && (
-
-
-
Something went wrong
-
{error}
-
- )}
-
- {!error && filteredAnnotations.length === 0 && (
-
-
-
-
-
No items yet
-
- {filter === "all"
- ? "Be the first to annotate something!"
- : `No ${filter} items found.`}
-
-
- )}
-
- {!error && filteredAnnotations.length > 0 && (
-
-
- {filteredAnnotations.map((item) => {
- if (item.type === "CollectionItem") {
- return (
-
- setCollectionModalState({
- isOpen: true,
- uri: uri,
- })
- }
- />
- );
- }
- if (
- item.type === "Highlight" ||
- item.motivation === "highlighting"
- ) {
- return (
- {
- const rkey = uri.split("/").pop();
- await deleteHighlight(rkey);
- setAnnotations((prev) =>
- prev.filter((a) => a.id !== item.id),
- );
- }}
- onAddToCollection={() =>
- setCollectionModalState({
- isOpen: true,
- uri: item.uri || item.id,
- })
- }
- />
- );
- }
- if (
- item.type === "Bookmark" ||
- item.motivation === "bookmarking"
- ) {
- return (
-
- setCollectionModalState({
- isOpen: true,
- uri: item.uri || item.id,
- })
- }
- />
- );
- }
- return (
-
- setCollectionModalState({
- isOpen: true,
- uri: item.uri || item.id,
- })
- }
- />
- );
- })}
-
-
- {hasMore && (
-
- fetchFeed(annotations.length)}
- disabled={loadingMore}
- className="feed-load-more"
- >
- {loadingMore ? "Loading..." : "View More"}
-
-
- )}
-
- )}
- >
- )}
-
- {collectionModalState.isOpen && (
-
setCollectionModalState({ isOpen: false, uri: null })}
- annotationUri={collectionModalState.uri}
- />
- )}
-
-
-
- );
-}
-
-function BackToTopButton() {
- const [isVisible, setIsVisible] = useState(false);
- const { layout } = useTheme();
-
- useEffect(() => {
- let scrollContainer = window;
- if (layout !== "topnav") {
- const mainContent = document.querySelector(".main-content");
- if (mainContent) scrollContainer = mainContent;
- }
-
- const toggleVisibility = () => {
- const scrolled =
- scrollContainer instanceof Window
- ? scrollContainer.scrollY
- : scrollContainer.scrollTop;
-
- setIsVisible(scrolled > 300);
- };
-
- scrollContainer.addEventListener("scroll", toggleVisibility);
- return () =>
- scrollContainer.removeEventListener("scroll", toggleVisibility);
- }, [layout]);
-
- const scrollToTop = () => {
- if (layout === "topnav") {
- window.scrollTo({ top: 0, behavior: "smooth" });
- } else {
- const mainContent = document.querySelector(".main-content");
- if (mainContent) mainContent.scrollTo({ top: 0, behavior: "smooth" });
- }
- };
-
- return (
-
-
-
- );
-}
diff --git a/web/src/pages/Highlights.jsx b/web/src/pages/Highlights.jsx
deleted file mode 100644
index 1edd65b..0000000
--- a/web/src/pages/Highlights.jsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import { useState, useEffect } from "react";
-import { Link } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import { getUserHighlights, deleteHighlight } from "../api/client";
-import { HighlightIcon } from "../components/Icons";
-import { HighlightCard } from "../components/AnnotationCard";
-
-export default function Highlights() {
- const { user, isAuthenticated, loading } = useAuth();
- const [highlights, setHighlights] = useState([]);
- const [loadingHighlights, setLoadingHighlights] = useState(true);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- async function loadHighlights() {
- if (!user?.did) return;
-
- try {
- setLoadingHighlights(true);
- const data = await getUserHighlights(user.did);
- setHighlights(data.items || []);
- } catch (err) {
- console.error("Failed to load highlights:", err);
- setError(err.message);
- } finally {
- setLoadingHighlights(false);
- }
- }
-
- if (isAuthenticated && user) {
- loadHighlights();
- }
- }, [isAuthenticated, user]);
-
- const handleDelete = async (uri) => {
- if (!confirm("Delete this highlight?")) return;
-
- try {
- const parts = uri.split("/");
- const rkey = parts[parts.length - 1];
- await deleteHighlight(rkey);
- setHighlights((prev) => prev.filter((h) => (h.id || h.uri) !== uri));
- } catch (err) {
- alert("Failed to delete: " + err.message);
- }
- };
-
- if (loading)
- return (
-
- );
-
- if (!isAuthenticated) {
- return (
-
-
-
Sign in to view your highlights
-
- You need to be logged in with your Bluesky account
-
-
- Sign in with Bluesky
-
-
-
- );
- }
-
- return (
-
-
-
My Highlights
-
- Text you've highlighted across the web
-
-
-
- {loadingHighlights ? (
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
- ) : error ? (
-
-
โ ๏ธ
-
Error loading highlights
-
{error}
-
- ) : highlights.length === 0 ? (
-
-
-
-
-
No highlights yet
-
- Highlight text on any page using the browser extension.
-
-
- ) : (
-
-
- {highlights.map((highlight) => (
-
- ))}
-
-
- )}
-
- );
-}
diff --git a/web/src/pages/Landing.jsx b/web/src/pages/Landing.jsx
deleted file mode 100644
index 0ce280f..0000000
--- a/web/src/pages/Landing.jsx
+++ /dev/null
@@ -1,790 +0,0 @@
-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, SiApple } 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);
-const isIOS =
- typeof navigator !== "undefined" &&
- /iPad|iPhone|iPod/.test(navigator.userAgent);
-const isSafari =
- typeof navigator !== "undefined" &&
- /Safari/.test(navigator.userAgent) &&
- !/Chrome/.test(navigator.userAgent);
-const isIOSSafari = isIOS && isSafari;
-
-function getExtensionInfo() {
- if (isIOSSafari) {
- return {
- url: "https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd",
- Icon: SiApple,
- label: "iOS",
- isShortcut: true,
- };
- }
- 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 ? (
-
{
- 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"}
-
- setShowPopover(false)}
- >
- โ
-
-
-
- {popoverAnnotations.length === 0 ? (
-
- No comments
-
- ) : (
- popoverAnnotations.map((ann, i) => (
-
-
-
(e.target.src = logo)}
- alt=""
- />
-
- @{ann.author?.handle || "user"}
-
-
-
- {ann.text || ann.body?.value}
-
-
-
- Reply
-
-
- Share
-
-
-
- ))
- )}
-
-
- )}
-
- 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.
-
-
-
-
-
- en.wikipedia.org
-
-
- {loading ? (
-
- Loading...
-
- ) : annotations.length > 0 ? (
- annotations.map((ann, i) => (
-
0 ? "demo-annotation-secondary" : ""}`}
- >
-
-
-
{
- 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}
-
Jump to text โ
-
- ))
- ) : (
-
- No annotations found.
-
- )}
-
-
-
-
- );
-}
-
-export default function Landing() {
- const { user } = useAuth();
- const ext = getExtensionInfo();
-
- return (
-
-
-
-
-
-
-
- Margin
-
-
-
-
-
-
-
-
- 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.
-
-
- {!isIOSSafari && (
-
- 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.
-
-
-
-
-
- );
-}
diff --git a/web/src/pages/Login.jsx b/web/src/pages/Login.jsx
deleted file mode 100644
index 2672e5b..0000000
--- a/web/src/pages/Login.jsx
+++ /dev/null
@@ -1,282 +0,0 @@
-import { useState, useEffect, useRef } from "react";
-import { Link } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import { searchActors, startLogin } from "../api/client";
-import { AtSign } from "lucide-react";
-import SignUpModal from "../components/SignUpModal";
-
-export default function Login() {
- const { isAuthenticated, user, logout } = useAuth();
- const [showSignUp, setShowSignUp] = useState(false);
- const [handle, setHandle] = useState("");
- const [suggestions, setSuggestions] = useState([]);
- const [showSuggestions, setShowSuggestions] = useState(false);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [selectedIndex, setSelectedIndex] = useState(-1);
- const inputRef = useRef(null);
- const suggestionsRef = useRef(null);
-
- const [providerIndex, setProviderIndex] = useState(0);
- const [morphClass, setMorphClass] = useState("morph-in");
- const providers = [
- "AT Protocol",
- "Margin",
- "Bluesky",
- "Blacksky",
- "Tangled",
- "Northsky",
- "witchcraft.systems",
- "tophhie.social",
- "altq.net",
- ];
-
- useEffect(() => {
- const cycleText = () => {
- setMorphClass("morph-out");
-
- setTimeout(() => {
- setProviderIndex((prev) => (prev + 1) % providers.length);
- setMorphClass("morph-in");
- }, 400);
- };
-
- const interval = setInterval(cycleText, 3000);
- return () => clearInterval(interval);
- }, [providers.length]);
-
- const isSelectionRef = useRef(false);
-
- useEffect(() => {
- if (handle.length >= 3) {
- if (isSelectionRef.current) {
- isSelectionRef.current = false;
- return;
- }
-
- const timer = setTimeout(async () => {
- try {
- const data = await searchActors(handle);
- setSuggestions(data.actors || []);
- setShowSuggestions(true);
- setSelectedIndex(-1);
- } catch (e) {
- console.error("Search failed:", e);
- }
- }, 300);
- return () => clearTimeout(timer);
- }
- }, [handle]);
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (
- suggestionsRef.current &&
- !suggestionsRef.current.contains(e.target) &&
- inputRef.current &&
- !inputRef.current.contains(e.target)
- ) {
- setShowSuggestions(false);
- }
- };
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, []);
-
- if (isAuthenticated) {
- return (
-
-
- {user?.avatar ? (
-
- ) : (
-
- {(user?.displayName || user?.handle || "??")
- .substring(0, 2)
- .toUpperCase()}
-
- )}
-
-
- Welcome back, {user?.displayName || user?.handle}
-
-
-
- View Profile
-
-
- Sign out
-
-
-
- );
- }
-
- const handleKeyDown = (e) => {
- if (!showSuggestions || suggestions.length === 0) return;
-
- if (e.key === "ArrowDown") {
- e.preventDefault();
- setSelectedIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- setSelectedIndex((prev) => Math.max(prev - 1, -1));
- } else if (e.key === "Enter" && selectedIndex >= 0) {
- e.preventDefault();
- selectSuggestion(suggestions[selectedIndex]);
- } else if (e.key === "Escape") {
- setShowSuggestions(false);
- }
- };
-
- const selectSuggestion = (actor) => {
- isSelectionRef.current = true;
- setHandle(actor.handle);
- setSuggestions([]);
- setShowSuggestions(false);
- inputRef.current?.blur();
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- if (!handle.trim()) return;
-
- setLoading(true);
- setError(null);
-
- try {
- const result = await startLogin(handle.trim());
- if (result.authorizationUrl) {
- window.location.href = result.authorizationUrl;
- }
- } catch (err) {
- console.error("Login error:", err);
- setError(err.message || "Failed to start login");
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
- Sign in with your{" "}
-
- {providers[providerIndex]}
- {" "}
- handle
-
-
-
-
-
{
- const val = e.target.value;
- setHandle(val);
- if (val.length < 3) {
- setSuggestions([]);
- setShowSuggestions(false);
- }
- }}
- onKeyDown={handleKeyDown}
- onFocus={() =>
- handle.length >= 3 &&
- suggestions.length > 0 &&
- !handle.includes(".") &&
- setShowSuggestions(true)
- }
- autoComplete="off"
- autoCapitalize="off"
- autoCorrect="off"
- spellCheck="false"
- disabled={loading}
- />
-
- {showSuggestions && suggestions.length > 0 && (
-
- {suggestions.map((actor, index) => (
-
selectSuggestion(actor)}
- >
-
- {actor.avatar ? (
-
- ) : (
-
- {(actor.displayName || actor.handle)
- .substring(0, 2)
- .toUpperCase()}
-
- )}
-
-
-
- {actor.displayName || actor.handle}
-
-
- @{actor.handle}
-
-
-
- ))}
-
- )}
-
-
- {error && {error}
}
-
-
- {loading ? "Connecting..." : "Continue"}
-
-
-
- By signing in, you agree to our{" "}
- Terms of Service and{" "}
- Privacy Policy.
-
-
-
- or
-
-
- setShowSignUp(true)}
- >
- Create New Account
-
-
-
- {showSignUp &&
setShowSignUp(false)} />}
-
- );
-}
diff --git a/web/src/pages/New.jsx b/web/src/pages/New.jsx
deleted file mode 100644
index 5aeddf9..0000000
--- a/web/src/pages/New.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { useState } from "react";
-import { useNavigate, useSearchParams, Link } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import Composer from "../components/Composer";
-
-export default function New() {
- const { isAuthenticated, loading } = useAuth();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
-
- const initialUrl = searchParams.get("url") || "";
-
- let initialSelector = null;
- const selectorParam = searchParams.get("selector");
- if (selectorParam) {
- try {
- initialSelector = JSON.parse(selectorParam);
- } catch (e) {
- console.error("Failed to parse selector:", e);
- }
- }
-
- const legacyQuote = searchParams.get("quote") || "";
- if (legacyQuote && !initialSelector) {
- initialSelector = {
- type: "TextQuoteSelector",
- exact: legacyQuote,
- };
- }
-
- const [url, setUrl] = useState(initialUrl);
-
- if (loading) {
- return (
-
- );
- }
-
- if (!isAuthenticated) {
- return (
-
-
-
Sign in to create annotations
-
- You need to be logged in with your Bluesky account
-
-
- Sign in with Bluesky
-
-
-
- );
- }
-
- const handleSuccess = () => {
- navigate("/home");
- };
-
- return (
-
-
-
New Annotation
-
Write in the margins of the web
-
-
- {!initialUrl && (
-
- setUrl(e.target.value)}
- placeholder="https://example.com/article"
- className="url-input"
- required
- />
-
- )}
-
-
- navigate(-1)}
- />
-
-
- );
-}
diff --git a/web/src/pages/Notifications.jsx b/web/src/pages/Notifications.jsx
deleted file mode 100644
index badd5c6..0000000
--- a/web/src/pages/Notifications.jsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import { useState, useEffect } from "react";
-import { Link } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import { getNotifications, markNotificationsRead } from "../api/client";
-import { BellIcon, HeartIcon, ReplyIcon } from "../components/Icons";
-
-function getNotificationRoute(n) {
- if (n.type === "reply" && n.subject?.inReplyTo) {
- return `/annotation/${encodeURIComponent(n.subject.inReplyTo)}`;
- }
- if (!n.subjectUri) return "/";
- if (n.subjectUri.includes("at.margin.bookmark")) {
- return `/bookmarks`;
- }
- if (n.subjectUri.includes("at.margin.highlight")) {
- return `/highlights`;
- }
- return `/annotation/${encodeURIComponent(n.subjectUri)}`;
-}
-
-export default function Notifications() {
- const { user } = useAuth();
- const [notifications, setNotifications] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- if (!user?.did) return;
-
- async function load() {
- try {
- setLoading(true);
- const data = await getNotifications();
- setNotifications(data.items || []);
- await markNotificationsRead();
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- }
- load();
- }, [user?.did]);
-
- const formatTime = (dateStr) => {
- const date = new Date(dateStr);
- const now = new Date();
- const diffMs = now - date;
- const diffMins = Math.floor(diffMs / 60000);
- const diffHours = Math.floor(diffMs / 3600000);
- const diffDays = Math.floor(diffMs / 86400000);
-
- if (diffMins < 1) return "just now";
- if (diffMins < 60) return `${diffMins}m ago`;
- if (diffHours < 24) return `${diffHours}h ago`;
- if (diffDays < 7) return `${diffDays}d ago`;
- return date.toLocaleDateString();
- };
-
- const getNotificationIcon = (type) => {
- switch (type) {
- case "like":
- return ;
- case "reply":
- return ;
- default:
- return ;
- }
- };
-
- const getNotificationText = (n) => {
- const name = n.actor?.displayName || n.actor?.handle || "Unknown";
- const handle = n.actor?.handle;
-
- switch (n.type) {
- case "like":
- return (
-
- e.stopPropagation()}
- >
- {name}
- {" "}
- liked your annotation
-
- );
- case "reply":
- return (
-
- e.stopPropagation()}
- >
- {name}
- {" "}
- replied to your annotation
-
- );
- default:
- return (
-
- e.stopPropagation()}
- >
- {name}
- {" "}
- interacted with your content
-
- );
- }
- };
-
- if (!user) {
- return (
-
-
-
Notifications
-
-
-
-
Sign in to see notifications
-
Get notified when people like or reply to your content
-
-
- );
- }
-
- return (
-
-
-
Notifications
-
- Likes and replies on your annotations
-
-
-
- {loading && (
-
- )}
-
- {error && (
-
- )}
-
- {!loading && !error && notifications.length === 0 && (
-
-
-
No notifications yet
-
- When someone likes or replies to your content, you'll see it
- here
-
-
- )}
-
- {!loading && !error && notifications.length > 0 && (
-
- {notifications.map((n, i) => (
-
-
- {n.actor?.avatar ? (
-
- ) : (
-
- {(n.actor?.handle || "?")[0].toUpperCase()}
-
- )}
-
- {getNotificationIcon(n.type)}
-
-
-
-
{getNotificationText(n)}
-
- {formatTime(n.createdAt)}
-
-
-
- ))}
-
- )}
-
- );
-}
diff --git a/web/src/pages/Privacy.jsx b/web/src/pages/Privacy.jsx
deleted file mode 100644
index 5cabcdc..0000000
--- a/web/src/pages/Privacy.jsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { ArrowLeft } from "lucide-react";
-import { Link } from "react-router-dom";
-
-export default function Privacy() {
- return (
-
-
-
-
Home
-
-
-
-
Privacy Policy
-
Last updated: January 11, 2026
-
-
- Overview
-
- Margin ("we", "our", or "us") is a web
- annotation tool that lets you highlight, annotate, and bookmark any
- webpage. Your data is stored on the decentralized AT Protocol
- network, giving you ownership and control over your content.
-
-
-
-
- Data We Collect
- Account Information
-
- When you log in with your Bluesky/AT Protocol account, we access
- your:
-
-
- Decentralized Identifier (DID)
- Handle (username)
- Display name and avatar (for showing your profile)
-
-
- Annotations & Content
- When you use Margin, we store:
-
- URLs of pages you annotate
- Text you highlight or select
- Annotations and comments you create
- Bookmarks you save
- Collections you organize content into
-
-
- Authentication
-
- We store OAuth session tokens locally in your browser to keep you
- logged in. These tokens are used solely for authenticating API
- requests.
-
-
-
-
- How We Use Your Data
- Your data is used exclusively to:
-
- Display your annotations on webpages
- Sync your content across devices
- Show your public annotations to other users
- Enable social features like replies and likes
-
-
-
-
- Data Storage
-
- Your annotations are stored on the AT Protocol network through your
- Personal Data Server (PDS). This means:
-
-
- You own your data
- You can export or delete it at any time
- Your data is portable across AT Protocol services
-
-
- We also maintain a local index of annotations to provide faster
- search and discovery features.
-
-
-
-
- Data Sharing
-
- We do not sell your data. We do not share your data
- with third parties for advertising or marketing purposes.
-
- Your public annotations may be visible to:
-
- Other Margin users viewing the same webpage
- Anyone on the AT Protocol network (for public content)
-
-
-
-
- Browser Extension Permissions
- The Margin browser extension requires certain permissions:
-
-
- All URLs: To display and create annotations on
- any webpage
-
-
- Storage: To save your preferences and session
- locally
-
-
- Cookies: To maintain your logged-in session
-
-
- Tabs: To know which page you're viewing
-
-
-
-
-
- Your Rights
- You can:
-
-
- Delete any annotation, highlight, or bookmark you've created
-
- Delete your collections
- Export your data from your PDS
- Revoke the extension's access at any time
-
-
-
-
- Contact
-
- For privacy questions or concerns, contact us at{" "}
- hello@margin.at
-
-
-
-
- );
-}
diff --git a/web/src/pages/Profile.jsx b/web/src/pages/Profile.jsx
deleted file mode 100644
index ed24d76..0000000
--- a/web/src/pages/Profile.jsx
+++ /dev/null
@@ -1,410 +0,0 @@
-import { useState, useEffect } from "react";
-import { useParams, Navigate } from "react-router-dom";
-import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
-import BookmarkCard from "../components/BookmarkCard";
-import { getLinkIconType, formatUrl } from "../utils/formatting";
-import {
- getUserAnnotations,
- getUserHighlights,
- getUserBookmarks,
- getCollections,
- getProfile,
-} from "../api/client";
-import { useAuth } from "../context/AuthContext";
-import EditProfileModal from "../components/EditProfileModal";
-import CollectionIcon from "../components/CollectionIcon";
-import CollectionRow from "../components/CollectionRow";
-import {
- PenIcon,
- HighlightIcon,
- BookmarkIcon,
- BlueskyIcon,
- GithubIcon,
- LinkedinIcon,
- TangledIcon,
- LinkIcon,
-} from "../components/Icons";
-
-function LinkIconComponent({ url }) {
- const type = getLinkIconType(url);
- switch (type) {
- case "github":
- return ;
- case "bluesky":
- return ;
- case "linkedin":
- return ;
- case "tangled":
- return ;
- default:
- return ;
- }
-}
-
-export default function Profile() {
- const { handle: routeHandle } = useParams();
- const { user, loading: authLoading } = useAuth();
- const [activeTab, setActiveTab] = useState("annotations");
- const [profile, setProfile] = useState(null);
- const [annotations, setAnnotations] = useState([]);
- const [highlights, setHighlights] = useState([]);
- const [bookmarks, setBookmarks] = useState([]);
- const [collections, setCollections] = useState([]);
-
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [showEditModal, setShowEditModal] = useState(false);
-
- const handle = routeHandle || user?.did || user?.handle;
- const isOwnProfile = user && (user.did === handle || user.handle === handle);
-
- useEffect(() => {
- if (!handle) return;
- async function fetchProfile() {
- try {
- setLoading(true);
-
- const bskyPromise = fetch(
- `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`,
- ).then((res) => (res.ok ? res.json() : null));
-
- const marginPromise = getProfile(handle).catch(() => null);
-
- const marginData = await marginPromise;
- let did = handle.startsWith("did:") ? handle : marginData?.did;
- if (!did) {
- const bskyData = await bskyPromise;
- if (bskyData) {
- did = bskyData.did;
- setProfile(bskyData);
- }
- } else {
- if (marginData) {
- setProfile((prev) => ({ ...prev, ...marginData }));
- }
- }
-
- if (did) {
- const [annData, hlData, bmData, collData] = await Promise.all([
- getUserAnnotations(did),
- getUserHighlights(did).catch(() => ({ items: [] })),
- getUserBookmarks(did).catch(() => ({ items: [] })),
- getCollections(did).catch(() => ({ items: [] })),
- ]);
- setAnnotations(annData.items || []);
- setHighlights(hlData.items || []);
- setBookmarks(bmData.items || []);
- setCollections(collData.items || []);
-
- const bskyData = await bskyPromise;
- if (bskyData || marginData) {
- const merged = {
- ...(bskyData || {}),
- };
- if (marginData) {
- merged.did = marginData.did || merged.did;
- if (marginData.displayName)
- merged.displayName = marginData.displayName;
- if (marginData.avatar) merged.avatar = marginData.avatar;
- if (marginData.bio) merged.bio = marginData.bio;
- if (marginData.website) merged.website = marginData.website;
- if (marginData.links?.length) merged.links = marginData.links;
- }
- setProfile(merged);
- }
- }
- } catch (err) {
- console.error(err);
- setError(err.message);
- } finally {
- setLoading(false);
- }
- }
- fetchProfile();
- }, [handle]);
-
- if (authLoading) {
- return (
-
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
-
- );
- }
-
- if (!handle) {
- return ;
- }
-
- const displayName = profile?.displayName || profile?.handle || handle;
- const displayHandle =
- profile?.handle || (handle?.startsWith("did:") ? null : handle);
- const avatarUrl = profile?.did
- ? `/api/avatar/${encodeURIComponent(profile.did)}`
- : null;
-
- const getInitial = () => {
- return (displayName || displayHandle || "??")
- ?.substring(0, 2)
- .toUpperCase();
- };
-
- const totalItems =
- annotations.length +
- highlights.length +
- bookmarks.length +
- collections.length;
-
- const renderContent = () => {
- if (activeTab === "annotations") {
- if (annotations.length === 0) {
- return (
-
-
-
No annotations
-
- This user hasn't posted any annotations.
-
-
- );
- }
- return annotations.map((a) => (
-
- ));
- }
-
- if (activeTab === "highlights") {
- if (highlights.length === 0) {
- return (
-
-
-
-
-
No highlights
-
- This user hasn't saved any highlights.
-
-
- );
- }
- return highlights.map((h) => );
- }
-
- if (activeTab === "bookmarks") {
- if (bookmarks.length === 0) {
- return (
-
-
-
-
-
No bookmarks
-
- This user hasn't bookmarked any pages.
-
-
- );
- }
- return bookmarks.map((b) => );
- }
-
- if (activeTab === "collections") {
- if (collections.length === 0) {
- return (
-
-
-
-
-
No collections
-
- This user hasn't created any collections.
-
-
- );
- }
- return (
-
- {collections.map((c) => (
-
- ))}
-
- );
- }
- };
-
- const bskyProfileUrl = displayHandle
- ? `https://bsky.app/profile/${displayHandle}`
- : `https://bsky.app/profile/${handle}`;
-
- return (
-
-
-
- {showEditModal && (
-
setShowEditModal(false)}
- onUpdate={() => {
- window.location.reload();
- }}
- />
- )}
-
-
- setActiveTab("annotations")}
- >
- Annotations ({annotations.length})
-
- setActiveTab("highlights")}
- >
- Highlights ({highlights.length})
-
- setActiveTab("bookmarks")}
- >
- Bookmarks ({bookmarks.length})
-
-
- setActiveTab("collections")}
- >
- Collections ({collections.length})
-
-
-
- {loading && (
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
- )}
-
- {error && (
-
-
โ ๏ธ
-
Error loading profile
-
{error}
-
- )}
-
- {!loading && !error && (
-
- )}
-
- );
-}
diff --git a/web/src/pages/Settings.jsx b/web/src/pages/Settings.jsx
deleted file mode 100644
index 9ce956f..0000000
--- a/web/src/pages/Settings.jsx
+++ /dev/null
@@ -1,344 +0,0 @@
-import { useState, useEffect } from "react";
-import { getAPIKeys, createAPIKey, deleteAPIKey } from "../api/client";
-import { useTheme } from "../context/ThemeContext";
-import { useAuth } from "../context/AuthContext";
-import { Navigate } from "react-router-dom";
-import { Columns, Layout } from "lucide-react";
-
-function KeyIcon({ size = 16 }) {
- return (
-
-
-
- );
-}
-
-function AppleIcon({ size = 16 }) {
- return (
-
-
-
- );
-}
-
-export default function Settings() {
- const { isAuthenticated, loading } = useAuth();
- const { layout, setLayout } = useTheme();
- const [apiKeys, setApiKeys] = useState([]);
- const [newKeyName, setNewKeyName] = useState("");
- const [newKey, setNewKey] = useState(null);
- const [keysLoading, setKeysLoading] = useState(false);
-
- useEffect(() => {
- if (isAuthenticated) {
- loadAPIKeys();
- }
- }, [isAuthenticated]);
-
- const loadAPIKeys = async () => {
- setKeysLoading(true);
- try {
- const data = await getAPIKeys();
- setApiKeys(data.keys || []);
- } catch {
- setApiKeys([]);
- } finally {
- setKeysLoading(false);
- }
- };
-
- const handleCreateKey = async () => {
- if (!newKeyName.trim()) return;
- try {
- const data = await createAPIKey(newKeyName.trim());
- setNewKey(data.key);
- setNewKeyName("");
- loadAPIKeys();
- } catch (err) {
- alert("Failed to create key: " + err.message);
- }
- };
-
- const handleDeleteKey = async (id) => {
- if (!confirm("Delete this API key? This cannot be undone.")) return;
- try {
- await deleteAPIKey(id);
- loadAPIKeys();
- } catch (err) {
- alert("Failed to delete key: " + err.message);
- }
- };
-
- if (loading) return null;
- if (!isAuthenticated) return ;
-
- return (
-
-
Settings
-
Manage your preferences and API keys.
-
-
-
Layout
-
-
setLayout("sidebar")}
- >
-
-
-
Three Column (Default)
-
Sidebars for navigation and tools
-
-
-
setLayout("topnav")}
- >
-
-
-
Top Navigation
-
Cleaner view with top menu
-
-
-
-
-
-
-
API Keys
-
- Use API keys to create bookmarks from iOS Shortcuts or other tools.
-
-
-
-
Create API Key
-
- setNewKeyName(e.target.value)}
- placeholder="Key name (e.g., iOS Shortcut)"
- className="input"
- style={{ flex: 1 }}
- />
-
- Generate
-
-
- {newKey && (
-
-
- โ Key created! Copy it now, you won't see it again.
-
-
- {newKey}
-
-
{
- navigator.clipboard.writeText(newKey);
- alert("Copied!");
- }}
- >
- Copy to clipboard
-
-
- )}
-
-
- {keysLoading ? (
-
- ) : apiKeys.length === 0 ? (
-
-
-
-
-
No API keys
-
- Create a key to use with customized tools.
-
-
- ) : (
-
-
Your API Keys
- {apiKeys.map((key) => (
-
-
-
{key.name}
-
- Created {new Date(key.createdAt).toLocaleDateString()}
- {key.lastUsedAt &&
- ` โข Last used ${new Date(key.lastUsedAt).toLocaleDateString()}`}
-
-
-
handleDeleteKey(key.id)}
- >
- Revoke
-
-
- ))}
-
- )}
-
-
-
iOS Shortcut
-
- Save bookmarks from Safari's share sheet.
-
-
- Get Shortcut
-
-
-
-
-
-
- );
-}
diff --git a/web/src/pages/Terms.jsx b/web/src/pages/Terms.jsx
deleted file mode 100644
index 38666ca..0000000
--- a/web/src/pages/Terms.jsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { ArrowLeft } from "lucide-react";
-import { Link } from "react-router-dom";
-
-export default function Terms() {
- return (
-
-
-
-
Home
-
-
-
-
Terms of Service
-
Last updated: January 17, 2026
-
-
- Overview
-
- Margin is an open-source project. By using our service, you agree to
- these terms ("Terms"). If you do not agree to these Terms,
- please do not use the Service.
-
-
-
-
- Open Source
-
- Margin is open source software. The code is available publicly and
- is provided "as is", without warranty of any kind, express
- or implied.
-
-
-
-
- User Conduct
-
- You are responsible for your use of the Service and for any content
- you provide, including compliance with applicable laws, rules, and
- regulations.
-
-
- We reserve the right to remove any content that violates these
- terms, including but not limited to:
-
-
- Illegal content
- Harassment or hate speech
- Spam or malicious content
-
-
-
-
- Decentralized Nature
-
- Margin interacts with the AT Protocol network. We do not control the
- network itself or the data stored on your Personal Data Server
- (PDS). Please refer to the terms of your PDS provider for data
- storage policies.
-
-
-
-
- Disclaimer
-
- THE SERVICE IS PROVIDED "AS IS" AND "AS
- AVAILABLE". WE DISCLAIM ALL CONDITIONS, REPRESENTATIONS AND
- WARRANTIES NOT EXPRESSLY SET OUT IN THESE TERMS.
-
-
-
-
- Contact
-
- For questions about these Terms, please contact us at{" "}
- hello@margin.at
-
-
-
-
- );
-}
diff --git a/web/src/pages/Url.jsx b/web/src/pages/Url.jsx
deleted file mode 100644
index 4d591d8..0000000
--- a/web/src/pages/Url.jsx
+++ /dev/null
@@ -1,390 +0,0 @@
-import { useState, useEffect, useRef } from "react";
-import { Link, useNavigate } from "react-router-dom";
-import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
-import { getByTarget, searchActors } from "../api/client";
-import { useAuth } from "../context/AuthContext";
-import { PenIcon, AlertIcon, SearchIcon } from "../components/Icons";
-import { Copy, Check, ExternalLink } from "lucide-react";
-
-export default function Url() {
- const { user } = useAuth();
- const navigate = useNavigate();
- const [url, setUrl] = useState("");
- const [annotations, setAnnotations] = useState([]);
- const [highlights, setHighlights] = useState([]);
- const [loading, setLoading] = useState(false);
- const [searched, setSearched] = useState(false);
- const [error, setError] = useState(null);
- const [activeTab, setActiveTab] = useState("all");
- const [copied, setCopied] = useState(false);
-
- const [suggestions, setSuggestions] = useState([]);
- const [showSuggestions, setShowSuggestions] = useState(false);
- const [selectedIndex, setSelectedIndex] = useState(-1);
- const inputRef = useRef(null);
- const suggestionsRef = useRef(null);
-
- useEffect(() => {
- const timer = setTimeout(async () => {
- const isUrl = url.includes("http") || url.includes("://");
- if (url.length >= 2 && !isUrl) {
- try {
- const data = await searchActors(url);
- setSuggestions(data.actors || []);
- setShowSuggestions(true);
- } catch {
- // ignore
- }
- } else {
- setSuggestions([]);
- setShowSuggestions(false);
- }
- }, 300);
- return () => clearTimeout(timer);
- }, [url]);
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (
- suggestionsRef.current &&
- !suggestionsRef.current.contains(e.target) &&
- inputRef.current &&
- !inputRef.current.contains(e.target)
- ) {
- setShowSuggestions(false);
- }
- };
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, []);
-
- const handleKeyDown = (e) => {
- if (!showSuggestions || suggestions.length === 0) return;
-
- if (e.key === "ArrowDown") {
- e.preventDefault();
- setSelectedIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- setSelectedIndex((prev) => Math.max(prev - 1, -1));
- } else if (e.key === "Enter" && selectedIndex >= 0) {
- e.preventDefault();
- selectSuggestion(suggestions[selectedIndex]);
- } else if (e.key === "Escape") {
- setShowSuggestions(false);
- }
- };
-
- const selectSuggestion = (actor) => {
- navigate(`/profile/${encodeURIComponent(actor.handle)}`);
- };
-
- const handleSearch = async (e) => {
- e.preventDefault();
- if (!url.trim()) return;
-
- setLoading(true);
- setError(null);
- setSearched(true);
-
- const isProtocol = url.startsWith("http://") || url.startsWith("https://");
- if (!isProtocol) {
- try {
- const actorRes = await searchActors(url);
- if (actorRes?.actors?.length > 0) {
- const match = actorRes.actors[0];
- navigate(`/profile/${encodeURIComponent(match.handle)}`);
- return;
- }
- } catch {
- // ignore
- }
- }
-
- try {
- const data = await getByTarget(url);
- setAnnotations(data.annotations || []);
- setHighlights(data.highlights || []);
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- };
-
- const myAnnotations = user
- ? annotations.filter((a) => (a.creator?.did || a.author?.did) === user.did)
- : [];
- const myHighlights = user
- ? highlights.filter((h) => (h.creator?.did || h.author?.did) === user.did)
- : [];
- const myItemsCount = myAnnotations.length + myHighlights.length;
-
- const getShareUrl = () => {
- if (!user?.handle || !url) return null;
- return `${window.location.origin}/${user.handle}/url/${url}`;
- };
-
- const handleCopyShareLink = async () => {
- const shareUrl = getShareUrl();
- if (!shareUrl) return;
- try {
- await navigator.clipboard.writeText(shareUrl);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- } catch {
- prompt("Copy this link:", shareUrl);
- }
- };
-
- const totalItems = annotations.length + highlights.length;
-
- const renderResults = () => {
- if (activeTab === "annotations" && annotations.length === 0) {
- return (
-
- );
- }
-
- return (
- <>
- {(activeTab === "all" || activeTab === "annotations") &&
- annotations.map((a) => )}
- {(activeTab === "all" || activeTab === "highlights") &&
- highlights.map((h) => )}
- >
- );
- };
-
- return (
-
-
-
Explore
-
- Search for a URL to view its context layer, or find a user by their
- handle
-
-
-
-
-
- setUrl(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="https://... or handle"
- className="url-input"
- autoComplete="off"
- required
- />
-
- {loading ? "Searching..." : "Search"}
-
-
-
- {showSuggestions && suggestions.length > 0 && (
-
- {suggestions.map((actor, index) => (
-
selectSuggestion(actor)}
- style={{
- width: "100%",
- textAlign: "left",
- padding: "12px",
- display: "flex",
- alignItems: "center",
- gap: "12px",
- border: "none",
- background:
- index === selectedIndex
- ? "var(--bg-secondary)"
- : "transparent",
- cursor: "pointer",
- }}
- >
-
- {actor.avatar ? (
-
- ) : (
-
- {(actor.displayName || actor.handle)
- .substring(0, 2)
- .toUpperCase()}
-
- )}
-
-
-
- {actor.displayName || actor.handle}
-
-
- @{actor.handle}
-
-
-
- ))}
-
- )}
-
-
- {error && (
-
- )}
-
- {searched && !loading && !error && totalItems === 0 && (
-
-
-
-
-
No annotations found
-
- Be the first to annotate this URL! Sign in to add your thoughts.
-
-
- )}
-
- {searched && totalItems > 0 && (
- <>
-
-
- {totalItems} item{totalItems !== 1 ? "s" : ""}
-
-
- setActiveTab("all")}
- >
- All ({totalItems})
-
- setActiveTab("annotations")}
- >
- Annotations ({annotations.length})
-
- setActiveTab("highlights")}
- >
- Highlights ({highlights.length})
-
-
-
-
- {user && myItemsCount > 0 && (
-
-
-
-
- You have {myItemsCount} note{myItemsCount !== 1 ? "s" : ""} on
- this page
-
-
-
-
- View
-
-
- {copied ? (
- <>
- Copied!
- >
- ) : (
- <>
- Copy Share Link
- >
- )}
-
-
-
- )}
-
-
- >
- )}
-
- );
-}
diff --git a/web/src/pages/UserUrl.jsx b/web/src/pages/UserUrl.jsx
deleted file mode 100644
index 44e3879..0000000
--- a/web/src/pages/UserUrl.jsx
+++ /dev/null
@@ -1,239 +0,0 @@
-import { useState, useEffect } from "react";
-import { useParams } from "react-router-dom";
-import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
-import { getUserTargetItems } from "../api/client";
-import {
- PenIcon,
- HighlightIcon,
- SearchIcon,
- BlueskyIcon,
-} from "../components/Icons";
-
-export default function UserUrl() {
- const { handle, "*": urlPath } = useParams();
- const targetUrl = urlPath || "";
-
- const [profile, setProfile] = useState(null);
- const [annotations, setAnnotations] = useState([]);
- const [highlights, setHighlights] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [activeTab, setActiveTab] = useState("all");
-
- useEffect(() => {
- async function fetchData() {
- if (!targetUrl) {
- setLoading(false);
- return;
- }
-
- try {
- setLoading(true);
- setError(null);
-
- const profileRes = await fetch(
- `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`,
- );
- let did = handle;
- if (profileRes.ok) {
- const profileData = await profileRes.json();
- setProfile(profileData);
- did = profileData.did;
- }
-
- const data = await getUserTargetItems(did, targetUrl);
- setAnnotations(data.annotations || []);
- setHighlights(data.highlights || []);
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- }
- fetchData();
- }, [handle, targetUrl]);
-
- const displayName = profile?.displayName || profile?.handle || handle;
- const displayHandle =
- profile?.handle || (handle?.startsWith("did:") ? null : handle);
- const avatarUrl = profile?.avatar;
-
- const getInitial = () => {
- return (displayName || displayHandle || "??")
- ?.substring(0, 2)
- .toUpperCase();
- };
-
- const totalItems = annotations.length + highlights.length;
- const bskyProfileUrl = displayHandle
- ? `https://bsky.app/profile/${displayHandle}`
- : `https://bsky.app/profile/${handle}`;
-
- const renderResults = () => {
- if (activeTab === "annotations" && annotations.length === 0) {
- return (
-
- );
- }
-
- if (activeTab === "highlights" && highlights.length === 0) {
- return (
-
- );
- }
-
- return (
- <>
- {(activeTab === "all" || activeTab === "annotations") &&
- annotations.map((a) => )}
- {(activeTab === "all" || activeTab === "highlights") &&
- highlights.map((h) => )}
- >
- );
- };
-
- if (!targetUrl) {
- return (
-
-
-
-
-
-
No URL specified
-
- Please provide a URL to view annotations.
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- {loading && (
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
- )}
-
- {error && (
-
-
โ ๏ธ
-
Error
-
{error}
-
- )}
-
- {!loading && !error && totalItems === 0 && (
-
-
-
-
-
No items found
-
- {displayName} hasn't annotated this page yet.
-
-
- )}
-
- {!loading && !error && totalItems > 0 && (
- <>
-
-
- {totalItems} item{totalItems !== 1 ? "s" : ""}
-
-
- setActiveTab("all")}
- >
- All ({totalItems})
-
- setActiveTab("annotations")}
- >
- Annotations ({annotations.length})
-
- setActiveTab("highlights")}
- >
- Highlights ({highlights.length})
-
-
-
-
- >
- )}
-
- );
-}
diff --git a/web/src/utils/formatting.js b/web/src/utils/formatting.js
deleted file mode 100644
index bacf14d..0000000
--- a/web/src/utils/formatting.js
+++ /dev/null
@@ -1,23 +0,0 @@
-export function getLinkIconType(url) {
- if (!url) return "link";
- try {
- const hostname = new URL(url).hostname;
- if (hostname.includes("github.com")) return "github";
- if (hostname.includes("bsky.app")) return "bluesky";
- if (hostname.includes("linkedin.com")) return "linkedin";
- if (hostname.includes("tangled.org")) return "tangled";
- if (hostname.includes("youtube.com")) return "youtube";
- } catch {
- /* ignore */
- }
- return "link";
-}
-
-export function formatUrl(url) {
- try {
- return new URL(url).hostname;
- } catch {
- /* ignore */
- return url;
- }
-}
diff --git a/web/vite.config.js b/web/vite.config.js
deleted file mode 100644
index a6ee3d9..0000000
--- a/web/vite.config.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-
-export default defineConfig({
- plugins: [react()],
- build: {
- outDir: "dist",
- emptyOutDir: true,
- },
- server: {
- port: 3000,
- proxy: {
- "/api": {
- target: "http://localhost:8080",
- changeOrigin: true,
- },
- "/auth": {
- target: "http://localhost:8080",
- changeOrigin: true,
- },
- },
- },
-});