From 9cd88fd87546040fa28bb58b238710cbe3f35dd3 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 18 Sep 2026 11:07:30 +0800 Subject: [PATCH 01/17] feat: consolidate bookmark management views --- apps/web/src/BookmarkManager.tsx | 101 ++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 22 deletions(-) diff --git a/apps/web/src/BookmarkManager.tsx b/apps/web/src/BookmarkManager.tsx index fcd6f19..fa55a7e 100644 --- a/apps/web/src/BookmarkManager.tsx +++ b/apps/web/src/BookmarkManager.tsx @@ -1,5 +1,7 @@ import { useMemo, useState } from "react"; import type { Bookmark, Category } from "@dockmark/core"; +import { AiOrganizationSettings } from "./AiOrganizationSettings"; +import { BookmarkMaintenance } from "./BookmarkMaintenance"; import { BookmarkManager as ExistingBookmarkManager } from "./BookmarkManagerLegacy"; import { ChangeJournal } from "./ChangeJournal"; import { LibraryBulkMaintenance } from "./LibraryBulkMaintenance"; @@ -13,7 +15,17 @@ interface Props { onChanged: () => Promise; } +type LibraryView = "library" | "organize" | "health" | "history"; + +const views: Array<{ id: LibraryView; label: string; detail: string }> = [ + { id: "library", label: "Library", detail: "Create, edit, categorize and reorder bookmarks." }, + { id: "organize", label: "Organize", detail: "Bulk moves, tags and AI-assisted organization." }, + { id: "health", label: "Health", detail: "Metadata, health checks, cleanup and archive review." }, + { id: "history", label: "History", detail: "Review recent library changes." }, +]; + export function BookmarkManager({ bookmarks, categories, bookmarkTags, loading, onChanged }: Props) { + const [view, setView] = useState("library"); const [journalRevision, setJournalRevision] = useState(0); const categoryNameById = useMemo( () => new Map(categories.map((category) => [category.id, category.name])), @@ -25,28 +37,73 @@ export function BookmarkManager({ bookmarks, categories, bookmarkTags, loading, setJournalRevision((current) => current + 1); } + const active = views.find((item) => item.id === view) ?? views[0]; + return ( - <> - - - - - +
+
+
+

BOOKMARK LIBRARY

+

One library,
one place to maintain it.

+

{active.detail}

+
+ {bookmarks.length} bookmarks +
+ + + + {view === "library" && ( + + )} + + {view === "organize" && ( +
+ + +
+ )} + + {view === "health" && ( +
+ + +
+ )} + + {view === "history" && } +
); } From 360590fdb00fa1c2670e51c62af29ed8ed1442a2 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 18 Sep 2026 11:07:46 +0800 Subject: [PATCH 02/17] feat: split settings into focused subnavigation --- apps/web/src/SettingsPage.tsx | 81 ++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/apps/web/src/SettingsPage.tsx b/apps/web/src/SettingsPage.tsx index 18e9f5f..892e0c7 100644 --- a/apps/web/src/SettingsPage.tsx +++ b/apps/web/src/SettingsPage.tsx @@ -1,52 +1,103 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import type { Bookmark, Category } from "@dockmark/core"; import { listBookmarks, listCategories } from "./api"; -import { AiOrganizationSettings } from "./AiOrganizationSettings"; import { AppearanceSettings } from "./AppearanceSettings"; -import { BookmarkMaintenance } from "./BookmarkMaintenance"; import { BrowserSettingsManager } from "./BrowserSettingsManager"; import { PublicPageSettings } from "./PublicPageSettings"; import { SecuritySettings } from "./SecuritySettings"; import "./settings.css"; import "./security.css"; +type SettingsView = "appearance" | "browser" | "security" | "public"; + +const sections: Array<{ id: SettingsView; label: string; detail: string }> = [ + { id: "appearance", label: "Appearance", detail: "Theme and visual preferences." }, + { id: "browser", label: "Browser", detail: "New Tab and sync behavior." }, + { id: "security", label: "Security", detail: "Pairing and trusted devices." }, + { id: "public", label: "Public Page", detail: "Publish and arrange curated links." }, +]; + +function settingsView(pathname: string): SettingsView { + const segment = pathname.split("/").filter(Boolean).at(-1); + return segment === "browser" || segment === "security" || segment === "public" || segment === "appearance" + ? segment + : "appearance"; +} + +function navigate(view: SettingsView) { + window.history.pushState({}, "", `/app/settings/${view}`); + window.dispatchEvent(new PopStateEvent("popstate")); +} + export function SettingsPage() { + const [path, setPath] = useState(window.location.pathname); const [bookmarks, setBookmarks] = useState([]); const [categories, setCategories] = useState([]); + const [publicDataLoaded, setPublicDataLoaded] = useState(false); const [error, setError] = useState(null); + const view = useMemo(() => settingsView(path), [path]); - const refreshData = useCallback(async () => { + useEffect(() => { + const onPopState = () => setPath(window.location.pathname); + window.addEventListener("popstate", onPopState); + return () => window.removeEventListener("popstate", onPopState); + }, []); + + const refreshPublicData = useCallback(async () => { setError(null); try { const [nextBookmarks, nextCategories] = await Promise.all([listBookmarks(), listCategories()]); setBookmarks(nextBookmarks); setCategories(nextCategories); + setPublicDataLoaded(true); } catch (caught) { - setError(caught instanceof Error ? caught.message : "Could not load settings data."); + setError(caught instanceof Error ? caught.message : "Could not load public-page data."); throw caught; } }, []); useEffect(() => { - void refreshData().catch(() => undefined); - }, [refreshData]); + if (view === "public" && !publicDataLoaded) { + void refreshPublicData().catch(() => undefined); + } + }, [publicDataLoaded, refreshPublicData, view]); + + const active = sections.find((section) => section.id === view) ?? sections[0]; return (

DOCKMARK SETTINGS

-

Private controls,
explicit policies.

-

Manage appearance, security, browser sync behavior, metadata and health review, AI-assisted organization, New Tab preferences and the curated public homepage from one place.

+

Policies and preferences,
without the library clutter.

+

{active.detail} Bookmark editing, organization, health and change history now live together under Workspace → Bookmarks.

+ + + {error &&
{error}
} - - - - - - + {view === "appearance" && } + {view === "browser" && } + {view === "security" && } + {view === "public" && ( + publicDataLoaded + ? + :
Loading public-page library…
+ )}
); } From 1a3ee36424f8cfbc4f791943c7582fb71e5de713 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 18 Sep 2026 11:08:26 +0800 Subject: [PATCH 03/17] feat: add grouped public page curation and ordering --- apps/web/src/PublicPageSettings.tsx | 184 ++++++++++++++++++++++++---- 1 file changed, 157 insertions(+), 27 deletions(-) diff --git a/apps/web/src/PublicPageSettings.tsx b/apps/web/src/PublicPageSettings.tsx index c4da430..2495562 100644 --- a/apps/web/src/PublicPageSettings.tsx +++ b/apps/web/src/PublicPageSettings.tsx @@ -1,9 +1,11 @@ import { useEffect, useMemo, useState } from "react"; import type { Bookmark, Category } from "@dockmark/core"; +import { reorderBookmarks, reorderCategories } from "./reorder-api"; interface Props { bookmarks: Bookmark[]; categories: Category[]; + onChanged: () => Promise; } async function loadSelection() { @@ -22,10 +24,20 @@ async function setPublished(id: string, published: boolean) { if (!response.ok) throw new Error(payload.error?.message ?? `Could not update public page (${response.status}).`); } -export function PublicPageSettings({ bookmarks, categories }: Props) { +function swapIds(ids: string[], leftId: string, rightId: string) { + const left = ids.indexOf(leftId); + const right = ids.indexOf(rightId); + if (left < 0 || right < 0) return ids; + const next = [...ids]; + [next[left], next[right]] = [next[right]!, next[left]!]; + return next; +} + +export function PublicPageSettings({ bookmarks, categories, onChanged }: Props) { const [published, setPublishedIds] = useState>(() => new Set()); const [loading, setLoading] = useState(true); const [busyId, setBusyId] = useState(null); + const [sorting, setSorting] = useState(false); const [error, setError] = useState(null); useEffect(() => { @@ -45,9 +57,33 @@ export function PublicPageSettings({ bookmarks, categories }: Props) { }; }, []); - const categoryNames = useMemo( - () => new Map(categories.map((category) => [category.id, category.name])), - [categories], + const bookmarksByCategory = useMemo(() => { + const map = new Map(); + for (const bookmark of bookmarks) { + const key = bookmark.categoryId ?? ""; + const list = map.get(key) ?? []; + list.push(bookmark); + map.set(key, list); + } + return map; + }, [bookmarks]); + + const groups = useMemo(() => { + const categoryGroups = categories.map((category) => ({ + id: category.id, + name: category.name, + bookmarks: (bookmarksByCategory.get(category.id) ?? []).filter((bookmark) => published.has(bookmark.id)), + })); + const uncategorized = (bookmarksByCategory.get("") ?? []).filter((bookmark) => published.has(bookmark.id)); + return [ + ...categoryGroups.filter((group) => group.bookmarks.length), + ...(uncategorized.length ? [{ id: "", name: "Elsewhere", bookmarks: uncategorized }] : []), + ]; + }, [bookmarksByCategory, categories, published]); + + const unpublished = useMemo( + () => bookmarks.filter((bookmark) => !published.has(bookmark.id)), + [bookmarks, published], ); async function toggle(bookmark: Bookmark) { @@ -69,13 +105,53 @@ export function PublicPageSettings({ bookmarks, categories }: Props) { } } + async function moveCategory(groupId: string, direction: -1 | 1) { + if (!groupId) return; + const publicCategoryIds = groups.map((group) => group.id).filter(Boolean); + const index = publicCategoryIds.indexOf(groupId); + const targetId = publicCategoryIds[index + direction]; + if (!targetId) return; + const allIds = categories.map((category) => category.id); + setSorting(true); + setError(null); + try { + await reorderCategories(swapIds(allIds, groupId, targetId)); + await onChanged(); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not reorder public categories."); + } finally { + setSorting(false); + } + } + + async function moveBookmark(bookmark: Bookmark, direction: -1 | 1) { + const categoryKey = bookmark.categoryId ?? ""; + const publicIds = (bookmarksByCategory.get(categoryKey) ?? []) + .filter((candidate) => published.has(candidate.id)) + .map((candidate) => candidate.id); + const index = publicIds.indexOf(bookmark.id); + const targetId = publicIds[index + direction]; + if (!targetId) return; + const allIds = (bookmarksByCategory.get(categoryKey) ?? []).map((candidate) => candidate.id); + setSorting(true); + setError(null); + try { + await reorderBookmarks(bookmark.categoryId ?? null, swapIds(allIds, bookmark.id, targetId)); + await onChanged(); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not reorder public bookmarks."); + } finally { + setSorting(false); + } + } + return (

PUBLIC PAGE

-

Curate what visitors can see.

-

The public homepage exposes only the bookmarks selected here. Workspaces, Sessions, settings and private bookmarks remain behind authentication.

+

Publish and arrange the curated view.

+

Public groups reuse the canonical Library category and bookmark order. Reordering here updates that shared order, so Web, New Tab and the public page stay consistent instead of drifting into separate layouts.

{published.size} published @@ -84,30 +160,84 @@ export function PublicPageSettings({ bookmarks, categories }: Props) {
{error &&
{error}
} -
-
- {bookmarks.map((bookmark) => { - const checked = published.has(bookmark.id); - return ( -
+
); } From 0c23d1f22834b42ffb327753a4e3207a99697e1f Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 18 Sep 2026 11:09:51 +0800 Subject: [PATCH 04/17] refactor: keep bookmark hub navigation lightweight --- apps/web/src/BookmarkManager.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/web/src/BookmarkManager.tsx b/apps/web/src/BookmarkManager.tsx index fa55a7e..daf791f 100644 --- a/apps/web/src/BookmarkManager.tsx +++ b/apps/web/src/BookmarkManager.tsx @@ -37,19 +37,8 @@ export function BookmarkManager({ bookmarks, categories, bookmarkTags, loading, setJournalRevision((current) => current + 1); } - const active = views.find((item) => item.id === view) ?? views[0]; - return ( -
-
-
-

BOOKMARK LIBRARY

-

One library,
one place to maintain it.

-

{active.detail}

-
- {bookmarks.length} bookmarks -
- +