Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Initial Plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ Advanced JavaScript or TypeScript adapters should be clearly marked and reviewed
### Phase 1 — First useful version

* [x] Clean reading mode
* [ ] OP highlighting
* [x] OP highlighting
* [ ] New posts since last visit
* [ ] Save comments
* [ ] Local user notes
Expand Down
8 changes: 5 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ features before the adapter ecosystem, AI last and always optional.
> **Status:** Phase 0 complete; Phase 1 underway. The foundation — core post
> model, generic extractor, local storage layer — and the `apps/extension` MV3
> shell (background, on-demand content script, side panel) are built, unit-tested,
> and bundled via esbuild. Phase 1 has begun with **clean reading mode**: the side
> panel now renders each post's rich `contentHtml` through an allowlist sanitizer.
> and bundled via esbuild. Phase 1 has begun with **clean reading mode** (the side
> panel renders each post's rich `contentHtml` through an allowlist sanitizer) and
> **OP highlighting** (OP / mod / admin posts get a readable badge and a colored
> edge so the important voices stand out across the thread).

## Phase 0 — Foundation

Expand All @@ -23,7 +25,7 @@ features before the adapter ecosystem, AI last and always optional.
## Phase 1 — First useful version

- [x] Clean reading mode — sanitized rich `contentHtml` rendering in the side panel (`apps/extension`)
- [ ] OP highlighting
- [x] OP highlighting — OP / mod / admin posts get a readable badge and a colored edge in the side panel (`apps/extension`)
- [ ] New posts since last visit
- [ ] Save comments
- [ ] Local user notes
Expand Down
9 changes: 7 additions & 2 deletions apps/extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

The ForumForge **browser extension** — a Manifest V3 app that turns the thread on
the current page into a clean, readable list in a side panel. It is the Phase 0
shell that later features (clean reading mode, OP highlighting, notes, saved
posts) build on.
shell that later features (notes, saved posts, new-post tracking) build on, and
it already ships the first Phase 1 reading features — clean reading mode and OP
highlighting.

It wires together the foundation packages: the active page's DOM →
[`@forumforge/parser`](../../packages/parser) → the
Expand All @@ -28,6 +29,10 @@ side panel "Read this thread" ─▶ inject content.js (activeTab) ─▶ extrac
- **`src/render.ts`** — builds the read-only view. Author, role and timestamp are
written with `textContent`; the body renders the post's rich `contentHtml`
through the sanitizer (clean reading mode), falling back to plain text.
**OP highlighting:** OP / moderator / admin posts get a readable role badge and
a colored edge (driven by a `data-role` attribute and styled in
`public/sidepanel.html`); the role is set by the parser and follows the OP
through the whole thread. The plain "user" role is left unmarked.
- **`src/sanitize.ts`** — the **clean reading mode** sanitizer. Untrusted post
HTML is parsed inertly and rebuilt from an allowlist of safe, semantic tags and
attributes, so no script, inline handler, style, embed or unsafe URL survives
Expand Down
36 changes: 35 additions & 1 deletion apps/extension/public/sidepanel.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@
padding: 10px 0;
border-top: 1px solid #8883;
}
/* OP highlighting: posts by an important voice stand out for the whole
thread, with a colored edge and a faint tint (not color alone — the
badge text carries the same signal). */
.ff-post[data-role="op"],
.ff-post[data-role="mod"],
.ff-post[data-role="admin"] {
border-left: 3px solid #8884;
padding-left: 10px;
}
.ff-post[data-role="op"] {
border-left-color: #2563eb;
background: #2563eb14;
}
.ff-post[data-role="mod"] {
border-left-color: #16a34a;
background: #16a34a14;
}
.ff-post[data-role="admin"] {
border-left-color: #9333ea;
background: #9333ea14;
}
.ff-post__meta {
display: flex;
gap: 8px;
Expand All @@ -57,12 +78,25 @@
}
.ff-post__role {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 6px;
border-radius: 999px;
background: #8882;
}
.ff-post[data-role="op"] .ff-post__role {
background: #2563eb;
color: #fff;
}
.ff-post[data-role="mod"] .ff-post__role {
/* Darker green than the edge so white badge text clears WCAG AA
contrast (~5:1) — the badge text is the non-color role signal. */
background: #15803d;
color: #fff;
Comment thread
erichuang1425 marked this conversation as resolved.
}
.ff-post[data-role="admin"] .ff-post__role {
background: #9333ea;
color: #fff;
}
.ff-post__time {
color: #888;
font-size: 12px;
Expand Down
18 changes: 15 additions & 3 deletions apps/extension/src/render.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import type { ForumForgePost } from "@forumforge/core";
import type { ForumForgePost, ForumRole } from "@forumforge/core";
import type { ExtractedThread } from "@forumforge/parser";
import { sanitizeHtml } from "./sanitize";

/**
* Human-readable badge text per role. The OP highlighting feature surfaces who
* is who in a thread; "user" is the unmarked default, so it gets no badge.
*/
const ROLE_LABELS: Record<ForumRole, string> = {
op: "OP",
mod: "Mod",
admin: "Admin",
user: "",
};

/**
* Build a clean, read-only view of an extracted thread.
*
Expand Down Expand Up @@ -53,10 +64,11 @@ function renderPost(doc: Document, post: ForumForgePost, baseUrl?: string): HTML
author.textContent = post.author;
meta.append(author);

if (post.role) {
const roleLabel = post.role ? ROLE_LABELS[post.role] : "";
if (roleLabel) {
const role = doc.createElement("span");
role.className = "ff-post__role";
role.textContent = post.role;
role.textContent = roleLabel;
meta.append(role);
}

Expand Down
17 changes: 16 additions & 1 deletion apps/extension/test/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,25 @@ describe("renderThread", () => {
expect(view.querySelector(".ff-thread__title")?.textContent).toBe("Monitor no signal");
expect(view.querySelectorAll(".ff-post")).toHaveLength(2);
expect(view.querySelector(".ff-post__author")?.textContent).toBe("ada");
expect(view.querySelector(".ff-post__role")?.textContent).toBe("op");
expect(view.querySelector(".ff-post__role")?.textContent).toBe("OP");
expect(view.querySelector(".ff-post[data-role='op']")).not.toBeNull();
});

it("shows a readable badge for highlighted roles but none for plain users", () => {
const thread: ExtractedThread = {
posts: [
{ id: "1", author: "ada", role: "mod", contentText: "Moved to the right forum." },
{ id: "2", author: "grace", role: "user", contentText: "Thanks." },
],
};
const view = renderThread(freshDocument(), thread);
const badges = view.querySelectorAll(".ff-post__role");

// The moderator post is the only one with a badge; "user" is the unmarked default.
expect(Array.from(badges).map((b) => b.textContent)).toEqual(["Mod"]);
expect(view.querySelector(".ff-post[data-role='mod']")).not.toBeNull();
});

it("shows an empty state when there are no posts", () => {
const view = renderThread(freshDocument(), { posts: [] });
expect(view.querySelector(".ff-empty")?.textContent).toBe("No posts found on this page.");
Expand Down
Loading