diff --git a/Initial Plan.md b/Initial Plan.md index e59f641..3040e24 100644 --- a/Initial Plan.md +++ b/Initial Plan.md @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 9bca008..15094b2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 @@ -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 diff --git a/apps/extension/README.md b/apps/extension/README.md index f8dd45d..c665fa0 100644 --- a/apps/extension/README.md +++ b/apps/extension/README.md @@ -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 @@ -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 diff --git a/apps/extension/public/sidepanel.html b/apps/extension/public/sidepanel.html index 0063f1a..5701e1e 100644 --- a/apps/extension/public/sidepanel.html +++ b/apps/extension/public/sidepanel.html @@ -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; @@ -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; + } + .ff-post[data-role="admin"] .ff-post__role { + background: #9333ea; + color: #fff; + } .ff-post__time { color: #888; font-size: 12px; diff --git a/apps/extension/src/render.ts b/apps/extension/src/render.ts index b1c98c3..2b9fe75 100644 --- a/apps/extension/src/render.ts +++ b/apps/extension/src/render.ts @@ -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 = { + op: "OP", + mod: "Mod", + admin: "Admin", + user: "", +}; + /** * Build a clean, read-only view of an extracted thread. * @@ -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); } diff --git a/apps/extension/test/render.test.ts b/apps/extension/test/render.test.ts index 024817b..fdf33a5 100644 --- a/apps/extension/test/render.test.ts +++ b/apps/extension/test/render.test.ts @@ -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.");