diff --git a/web/package-lock.json b/web/package-lock.json
index 15224ea..20f09a7 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -10,15 +10,16 @@
"dependencies": {
"@atcute/atproto": "^3.1.11",
"@atcute/client": "^4.2.1",
- "@atcute/lex-cli": "^2.7.0",
"@atcute/oauth-browser-client": "^3.0.0",
- "@tailwindcss/typography": "^0.5.19",
+ "lucide-react": "^1.8.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.14.0"
},
"devDependencies": {
+ "@atcute/lex-cli": "^2.7.0",
+ "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -42,6 +43,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@atcute/car/-/car-5.1.1.tgz",
"integrity": "sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/cbor": "^2.3.2",
@@ -54,6 +56,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@atcute/cbor/-/cbor-2.3.2.tgz",
"integrity": "sha512-xP2SORSau/VVI00x2V4BjwIkHr6EQ7l/MXEOPaa4LGYtePFc4gnD4L1yN10dT5NEuUnvGEuCh6arLB7gz1smVQ==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/cid": "^2.4.1",
@@ -65,6 +68,7 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atcute/cid/-/cid-2.4.1.tgz",
"integrity": "sha512-bwhna69RCv7yetXudtj+2qrMPYvhhIQqvJz6YUpUS98v7OdF3X2dnye9Nig2NDrklZcuyOsu7sQo7GOykJXRLQ==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/multibase": "^1.1.8",
@@ -85,6 +89,7 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atcute/crypto/-/crypto-2.4.1.tgz",
"integrity": "sha512-tJ3Pi/XYcAsABKtqSlSOTKfO5YiQ4XdqlTuPS8HiRZSezOPcXBFFzAFWpSIJPURbVPFQL3LLrrK0Ea24wl5qeQ==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/multibase": "^1.2.0",
@@ -120,6 +125,7 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@atcute/lex-cli/-/lex-cli-2.7.0.tgz",
"integrity": "sha512-U/M2+KGYtxe5pXleDgGsmrByMg0i7QpNJQRsoP4zPp1sEtPeLK6+7I0axJA2wkRiw+426B9IuPygVcS2FHLaDQ==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/identity": "^1.1.4",
@@ -141,6 +147,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@atcute/lexicon-doc/-/lexicon-doc-2.2.0.tgz",
"integrity": "sha512-6l4lDlL6KPLDGknRh6HlfGbv98haUgQ0DFaAr1yA4vA95b8YYZUZ4/370ENpiq+d6Lv0tdDAMvOon2mynrp3pQ==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/identity": "^1.1.4",
@@ -154,6 +161,7 @@
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@atcute/lexicon-resolver/-/lexicon-resolver-0.1.6.tgz",
"integrity": "sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/crypto": "^2.3.0",
@@ -184,6 +192,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@atcute/mst/-/mst-1.0.0.tgz",
"integrity": "sha512-pMce2efib+dmKtnGnIvJZitVncJkpr3AmhyfgfYllni8KzsaDGsJmuGavSVpuojAhQe+6jYwHFtpm/beiiH4uw==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/cbor": "^2.3.2",
@@ -252,6 +261,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@atcute/repo/-/repo-0.1.4.tgz",
"integrity": "sha512-uzbGJkE+1A8UFviosJrtw7HW87u8nCCH1V3yOQ79FPrRhS67EvEHF6GTg4aMkP21ze/pRtttJ1k9pFfDmyTlTg==",
+ "dev": true,
"license": "0BSD",
"dependencies": {
"@atcute/car": "^5.1.1",
@@ -291,6 +301,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@atcute/varint/-/varint-2.0.0.tgz",
"integrity": "sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw==",
+ "dev": true,
"license": "0BSD"
},
"node_modules/@babel/code-frame": {
@@ -691,6 +702,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz",
"integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==",
+ "dev": true,
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
@@ -700,6 +712,7 @@
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@optique/core/-/core-0.10.7.tgz",
"integrity": "sha512-FwSX8ILFqzcCqZi6Xetsa4flJp/yyqFG4d4eFD98BtqdzxxuylzdrKvsXj/ow8mcoVjYkTuaIkqHSBxonqMcQg==",
+ "dev": true,
"funding": [
"https://github.com/sponsors/dahlia"
],
@@ -714,6 +727,7 @@
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@optique/run/-/run-0.10.7.tgz",
"integrity": "sha512-1CVdH8uyptj1nFGS2MLacSmZceRClez4LD/G/Gm38wrAVnJq6I+9Fvyh2bVHErsZLQzR0a12CYMUWIgDKY3X1w==",
+ "dev": true,
"funding": [
"https://github.com/sponsors/dahlia"
],
@@ -1298,6 +1312,7 @@
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
@@ -1639,6 +1654,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -2266,6 +2282,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
+ "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -2931,6 +2956,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -2979,6 +3005,7 @@
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -3011,6 +3038,7 @@
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
"integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
+ "dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
@@ -3280,6 +3308,7 @@
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
+ "dev": true,
"license": "MIT"
},
"node_modules/tapable": {
@@ -3483,6 +3512,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/vfile": {
diff --git a/web/package.json b/web/package.json
index 32d3d50..f1cf196 100644
--- a/web/package.json
+++ b/web/package.json
@@ -12,6 +12,7 @@
"@atcute/atproto": "^3.1.11",
"@atcute/client": "^4.2.1",
"@atcute/oauth-browser-client": "^3.0.0",
+ "lucide-react": "^1.8.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-markdown": "^10.1.0",
diff --git a/web/src/components/ActivityList.tsx b/web/src/components/ActivityList.tsx
index 8de2995..c7562ed 100644
--- a/web/src/components/ActivityList.tsx
+++ b/web/src/components/ActivityList.tsx
@@ -1,4 +1,5 @@
import { useState } from "react";
+import { ChevronDown } from "lucide-react";
import { Link } from "react-router-dom";
import { parseAtUri } from "../lib/util";
import PostBody from "./post/PostBody";
@@ -46,9 +47,9 @@ export default function ActivityList({ items, userHandle }: ActivityListProps) {
)}
diff --git a/web/src/components/BBSPanel.tsx b/web/src/components/BBSPanel.tsx
index 437d01f..b26b164 100644
--- a/web/src/components/BBSPanel.tsx
+++ b/web/src/components/BBSPanel.tsx
@@ -1,4 +1,5 @@
import { Link } from "react-router-dom";
+import { Monitor, Pencil, Shield, Trash2, Plus } from "lucide-react";
import { ActionLink } from "./nav/ActionButton";
const cardStyle =
@@ -17,7 +18,7 @@ export default function BBSPanel({ hasBBS, userHandle, onDelete }: BBSPanelProps
No BBS yet.
- create a bbs
+ create a bbs
>
);
}
@@ -25,22 +26,22 @@ export default function BBSPanel({ hasBBS, userHandle, onDelete }: BBSPanelProps
return (
-
Browse
+
Browse
View your BBS.
-
Edit
+
Name, boards, intro.
-
Moderate
+
Moderate
Bans and hidden posts.
diff --git a/web/src/components/DialBBS.tsx b/web/src/components/DialBBS.tsx
index b4ff0fa..e408f1c 100644
--- a/web/src/components/DialBBS.tsx
+++ b/web/src/components/DialBBS.tsx
@@ -1,4 +1,5 @@
import { useState, type SyntheticEvent } from "react";
+import { ArrowRight, Dices } from "lucide-react";
import { Link, useNavigate } from "react-router-dom";
import HandleInput from "./form/HandleInput";
import { Button } from "./form/Form";
@@ -92,8 +93,8 @@ export default function DialBBS({ discovered, suggestions }: DialBBSProps) {
}
aria-label="Dial a BBS by handle"
/>
-
-
+
+
{dropdownOpen && (
diff --git a/web/src/components/MyThreadList.tsx b/web/src/components/MyThreadList.tsx
index b79277e..c25fb05 100644
--- a/web/src/components/MyThreadList.tsx
+++ b/web/src/components/MyThreadList.tsx
@@ -1,4 +1,5 @@
import { useState } from "react";
+import { ChevronDown } from "lucide-react";
import { Link } from "react-router-dom";
import { parseAtUri, formatFullDate, relativeDate } from "../lib/util";
import type { MyThread } from "../router/loaders";
@@ -49,9 +50,9 @@ export default function MyThreadList({ threads }: MyThreadListProps) {
)}
diff --git a/web/src/components/PinButton.tsx b/web/src/components/PinButton.tsx
index f363b9f..eba11a5 100644
--- a/web/src/components/PinButton.tsx
+++ b/web/src/components/PinButton.tsx
@@ -4,6 +4,7 @@ import { PIN } from "../lib/lexicon";
import { parseAtUri } from "../lib/util";
import { createPin, deleteRecord } from "../lib/writes";
import { ActionButton } from "./nav/ActionButton";
+import { Pin, PinOff } from "lucide-react";
interface PinButtonProps {
bbsDid: string;
@@ -29,8 +30,8 @@ export default function PinButton({ bbsDid, initialRkey }: PinButtonProps) {
if (!user) return null;
return (
-
- {pinRkey ? "✕ unpin" : "+ pin"}
+
+ {pinRkey ? "unpin" : "pin"}
);
}
diff --git a/web/src/components/PinnedList.tsx b/web/src/components/PinnedList.tsx
index 5a3d64f..d2c4c8c 100644
--- a/web/src/components/PinnedList.tsx
+++ b/web/src/components/PinnedList.tsx
@@ -1,4 +1,5 @@
import { useState } from "react";
+import { ChevronDown } from "lucide-react";
import ListLink from "./nav/ListLink";
import type { PinnedBBS } from "../router/loaders";
@@ -31,9 +32,9 @@ export default function PinnedList({ pins }: PinnedListProps) {
{shown < pins.length && (
)}
diff --git a/web/src/components/form/ComposeForm.tsx b/web/src/components/form/ComposeForm.tsx
index e781589..76578ca 100644
--- a/web/src/components/form/ComposeForm.tsx
+++ b/web/src/components/form/ComposeForm.tsx
@@ -1,4 +1,5 @@
import type { SyntheticEvent } from "react";
+import { Send, Paperclip } from "lucide-react";
import { Input, Textarea, Button } from "./Form";
import FileChips from "./FileChips";
import { MAX_ATTACHMENTS } from "../../lib/limits";
@@ -103,11 +104,11 @@ export default function ComposeForm({
{!attachmentsAtLimit && (
-
+
diff --git a/web/src/components/profile/ViewProfile.tsx b/web/src/components/profile/ViewProfile.tsx
index 174c76e..49fb0c1 100644
--- a/web/src/components/profile/ViewProfile.tsx
+++ b/web/src/components/profile/ViewProfile.tsx
@@ -1,4 +1,5 @@
import { Link } from "react-router-dom";
+import { Pencil, ChevronRight, Monitor } from "lucide-react";
import PostBody from "../post/PostBody";
import { ActionButton } from "../nav/ActionButton";
import type { Profile } from "../../lib/profile";
@@ -23,7 +24,7 @@ export default function ViewProfile({
{profile?.name ?? handle}
{isOwner && (
- edit profile
+ edit
)}
@@ -42,8 +43,8 @@ export default function ViewProfile({
)}
{profile?.bbsName && (
-
- →
-
+
)}
diff --git a/web/src/pages/BBS.tsx b/web/src/pages/BBS.tsx
index 659e70f..36f6cde 100644
--- a/web/src/pages/BBS.tsx
+++ b/web/src/pages/BBS.tsx
@@ -13,6 +13,7 @@ import ListLink from "../components/nav/ListLink";
import ActionBar from "../components/nav/ActionBar";
import { ActionLink } from "../components/nav/ActionButton";
import PinButton from "../components/PinButton";
+import { User, Pencil, Shield, LayoutGrid, Newspaper, Megaphone, ChevronDown } from "lucide-react";
import type { News } from "../lib/bbs";
import type { BBSLoaderData } from "../router/loaders";
import PostBody from "../components/post/PostBody";
@@ -83,11 +84,11 @@ export default function BBSPage() {
{bbs.site.description}
-
- operator
+
+ owner
- {isSysop && edit}
- {isSysop && moderate}
+ {isSysop && edit}
+ {isSysop && moderate}
@@ -98,8 +99,8 @@ export default function BBSPage() {
)}
-
- Boards
+
+ Boards
{bbs.site.boards.map((board) => (
@@ -114,14 +115,14 @@ export default function BBSPage() {
-
- News
+
+ News
{isSysop && (
-
- post news
+
+ post news
3 && (
)}
>
diff --git a/web/src/pages/Board.tsx b/web/src/pages/Board.tsx
index fe5ef20..f41886e 100644
--- a/web/src/pages/Board.tsx
+++ b/web/src/pages/Board.tsx
@@ -1,4 +1,5 @@
import { useEffect, useState, type SyntheticEvent } from "react";
+import { PenLine } from "lucide-react";
import {
useLoaderData,
useNavigate,
@@ -111,8 +112,8 @@ export default function BoardPage() {
{user && (
-
- new thread
+
+ new thread
("pip");
+ const [copied, setCopied] = useState(false);
usePageTitle("atbbs");
+ const installCommands: Record = {
+ pip: "pip install atbbs\natbbs",
+ uv: "uv tool install atbbs\natbbs",
+ brew: "brew install alyraffauf/tap/atbbs\natbbs",
+ telnet: "telnet tel.atbbs.xyz",
+ };
+
+ function handleCopy() {
+ navigator.clipboard.writeText(installCommands[tab]);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ }
+
const activeTab = "py-2 border-b-2 text-neutral-200 border-neutral-200";
const inactiveTab =
"py-2 border-b-2 text-neutral-400 hover:text-neutral-300 border-transparent";
return (
-
+
-
Dial a BBS
+
Dial a BBS
@@ -74,36 +89,23 @@ export default function LoggedOutHome() {
))}
- {tab === "pip" && (
-
- $ pip install
- atbbs
- {"\n"}
- $ atbbs
-
- )}
- {tab === "uv" && (
-
- $ uv tool
- install atbbs
- {"\n"}
- $ atbbs
-
- )}
- {tab === "brew" && (
-
- $ brew install
- alyraffauf/tap/atbbs
- {"\n"}
- $ atbbs
+
+
+ {installCommands[tab].split("\n").map((line, i) => (
+
+ {i > 0 && "\n"}
+ $ {line}
+
+ ))}
- )}
- {tab === "telnet" && (
-
- $ telnet
- tel.atbbs.xyz
-
- )}
+
+
);
diff --git a/web/src/pages/Login.tsx b/web/src/pages/Login.tsx
index 9c87de5..3162ec4 100644
--- a/web/src/pages/Login.tsx
+++ b/web/src/pages/Login.tsx
@@ -1,4 +1,5 @@
import { useState, type SyntheticEvent } from "react";
+import { LogIn, MessageSquare, Pin, User, Monitor } from "lucide-react";
import { useAuth } from "../lib/auth";
import { usePageTitle } from "../hooks/usePageTitle";
import { useHandleSearch } from "../hooks/useHandleSearch";
@@ -88,7 +89,7 @@ export default function Login() {
aria-label="Enter your handle"
/>
{dropdownOpen && (
@@ -132,11 +133,11 @@ export default function Login() {
Once signed in, you can:
-
- - Post threads and replies
- - Pin boards you like
- - Set up a profile
- - Start your own community
+
+ - Post threads and replies
+ - Pin boards you like
+ - Set up a profile
+ - Start your own community
We'll redirect you to your provider to continue.
diff --git a/web/src/pages/Profile.tsx b/web/src/pages/Profile.tsx
index fd08f09..8ad0b79 100644
--- a/web/src/pages/Profile.tsx
+++ b/web/src/pages/Profile.tsx
@@ -1,4 +1,5 @@
import { Suspense, useState } from "react";
+import { MessageSquare } from "lucide-react";
import { Await, useLoaderData, useRevalidator } from "react-router-dom";
import { useAuth } from "../lib/auth";
import { usePageTitle } from "../hooks/usePageTitle";
@@ -45,8 +46,8 @@ export default function Profile() {
onEdit={() => setEditing(true)}
/>
-
- Recent Threads
+
+ Recent Threads
loading...}