From d4e365e5b5212fd6af3bb4a46287cb0e238a78b7 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 18 Sep 2026 11:38:56 +0800 Subject: [PATCH 1/8] feat: route workspace sections by URL --- apps/web/src/App.tsx | 56 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 28824a6..986c011 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -52,6 +52,31 @@ const sourceLabel: Record = { type View = "launcher" | "workspaces" | "sessions" | "bookmarks" | "search" | "transfer" | "extension"; +const viewPaths: Record = { + launcher: "/app", + workspaces: "/app/workspaces", + sessions: "/app/sessions", + bookmarks: "/app/bookmarks", + search: "/app/search", + transfer: "/app/transfer", + extension: "/app/extension", +}; + +function viewFromPath(pathname: string): View { + for (const [view, path] of Object.entries(viewPaths) as Array<[View, string]>) { + if (view === "launcher") continue; + if (pathname === path || pathname.startsWith(`${path}/`)) return view; + } + return "launcher"; +} + +function navigateView(view: View) { + const path = viewPaths[view]; + if (window.location.pathname === path) return; + window.history.pushState({}, "", path); + window.dispatchEvent(new PopStateEvent("popstate")); +} + function openUrls(urls: string[]) { for (const url of urls) { const opened = window.open(url, "_blank", "noopener,noreferrer"); @@ -72,7 +97,7 @@ function collectionResultId(id: string) { } export function App() { - const [view, setView] = useState("launcher"); + const [view, setView] = useState(() => viewFromPath(window.location.pathname)); const [query, setQuery] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); const [bookmarks, setBookmarks] = useState([]); @@ -146,6 +171,12 @@ export function App() { void refresh(); }, [refresh]); + useEffect(() => { + const onPopState = () => setView(viewFromPath(window.location.pathname)); + window.addEventListener("popstate", onPopState); + return () => window.removeEventListener("popstate", onPopState); + }, []); + useEffect(() => { const unsubscribe = onBridgeEvent((event) => { if (event === "ready" || event === "tabs-changed") void refreshBridge(); @@ -161,6 +192,7 @@ export function App() { useEffect(() => { const focusLauncher = () => { + navigateView("launcher"); setView("launcher"); void refreshBridge(); requestAnimationFrame(() => commandInputRef.current?.focus()); @@ -361,7 +393,7 @@ export function App() { } if (item.source === "navigation" && item.id === "navigation:extension") { - setView("extension"); + navigateView("extension"); return; } @@ -493,18 +525,18 @@ export function App() { return (
-
@@ -530,7 +562,7 @@ export function App() { ) : view === "search" ? ( ) : view === "transfer" ? ( - setView("extension")} /> + navigateView("extension")} /> ) : view === "extension" ? ( ) : ( @@ -603,7 +635,7 @@ export function App() { {!bridgeConnected && ( - + - + ))} ))} - {!loading && !groups.length &&

Nothing is public yet. Publish bookmarks from the list beside this preview.

} + {!loading && !groups.length &&

Nothing is public yet. Publish bookmarks from the manager beside this preview.

} {loading &&

Loading public selection…

} @@ -214,27 +332,86 @@ export function PublicPageSettings({ bookmarks, categories, onChanged }: Props)

Add to public page

-

Private bookmarks remain private until explicitly published.

+

Search the full library, filter by publication state, then update one bookmark or a selected batch.

- {unpublished.length} + {filteredBookmarks.length} shown
-
- {unpublished.map((bookmark) => ( - + + +
+ + +
+ + {selectedIds.size} selected +
+ - ))} - {!loading && !unpublished.length &&

Every active bookmark is currently published.

} + + +
+
+ +
+ {filteredBookmarks.map((bookmark) => { + const isPublished = published.has(bookmark.id); + const categoryName = bookmark.categoryId ? categoryNameById.get(bookmark.categoryId) ?? "Uncategorized" : "Uncategorized"; + return ( +
+ + {isPublished ? "PUBLISHED" : "PRIVATE"} + +
+ ); + })} + {!loading && !filteredBookmarks.length && ( +

{query.trim() ? "No bookmarks match this filter." : "No bookmarks match this publication state."}

+ )} + {loading &&

Loading public selection…

}
From 947b4cda22f650828a2c1f99edfba55605ed311e Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 18 Sep 2026 11:40:29 +0800 Subject: [PATCH 4/8] fix: lock public row actions during mutations --- apps/web/src/PublicPageSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/PublicPageSettings.tsx b/apps/web/src/PublicPageSettings.tsx index dbf2d1d..0afd0ce 100644 --- a/apps/web/src/PublicPageSettings.tsx +++ b/apps/web/src/PublicPageSettings.tsx @@ -400,7 +400,7 @@ export function PublicPageSettings({ bookmarks, categories, onChanged }: Props) - + ))} From 5032a28982305bc6de18509522736f1b591a8153 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 18 Sep 2026 11:41:26 +0800 Subject: [PATCH 7/8] fix: separate selection styling from publication state --- apps/web/src/settings.css | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/web/src/settings.css b/apps/web/src/settings.css index 9d57b8e..06385b8 100644 --- a/apps/web/src/settings.css +++ b/apps/web/src/settings.css @@ -150,12 +150,12 @@ .public-bulk-actions .secondary { min-height: 32px; padding-inline: 9px; font-size: 10px; } .public-bookmark-manager { align-content: start; } -.public-bookmark-manage { +.public-bookmark-row.public-bookmark-manage { grid-template-columns: minmax(0, 1fr) auto auto; gap: 10px; cursor: default; } -.public-bookmark-manage.is-published { border-color: color-mix(in srgb, var(--dm-accent) 55%, var(--dm-border)); } +.public-bookmark-row.public-bookmark-manage.is-published { border-color: color-mix(in srgb, var(--dm-accent) 55%, var(--dm-border)); } .public-bookmark-select { display: grid; grid-template-columns: auto minmax(0, 1fr); @@ -169,16 +169,16 @@ .public-bookmark-select small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .public-bookmark-select strong { font-size: 12px; } .public-bookmark-select small { color: var(--dm-text-muted); font-size: 10px; } -.public-bookmark-manage em { min-width: 64px; color: var(--dm-text-muted); text-align: right; } -.public-bookmark-manage.is-published em { color: var(--dm-accent-strong); } -.public-bookmark-manage > button { min-width: 54px; text-align: right; } -.public-bookmark-manage > button:disabled, +.public-bookmark-row.public-bookmark-manage em { min-width: 64px; color: var(--dm-text-muted); text-align: right; } +.public-bookmark-row.public-bookmark-manage.is-published em { color: var(--dm-accent-strong); } +.public-bookmark-row.public-bookmark-manage > button { min-width: 54px; text-align: right; } +.public-bookmark-row.public-bookmark-manage > button:disabled, .public-bookmark-select input:disabled, .public-select-visible input:disabled { opacity: .45; cursor: default; } html[data-theme="light"] .public-publish-search, html[data-theme="light"] .public-bulk-bar { border-color: var(--dm-border); background: #fff; } -html[data-theme="light"] .public-bookmark-manage.is-published { border-color: #9fc5ae; background: var(--dm-selected-bg); } +html[data-theme="light"] .public-bookmark-row.public-bookmark-manage.is-published { border-color: #9fc5ae; background: var(--dm-selected-bg); } @media (max-width: 760px) { .public-publish-filter { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -187,8 +187,23 @@ html[data-theme="light"] .public-bookmark-manage.is-published { border-color: #9 } @media (max-width: 560px) { .public-publish-filter { grid-template-columns: 1fr; } - .public-bookmark-manage { grid-template-columns: minmax(0, 1fr) auto; } + .public-bookmark-row.public-bookmark-manage { grid-template-columns: minmax(0, 1fr) auto; } .public-bookmark-select { grid-column: 1 / -1; } .public-bookmark-manage em { text-align: left; } .public-bulk-actions { flex-wrap: wrap; } } + + +.public-bookmark-row.public-bookmark-manage:has(input:checked) { + border-color: color-mix(in srgb, var(--dm-accent) 72%, var(--dm-border)); + background: color-mix(in srgb, var(--dm-accent-bg) 58%, var(--dm-bg-card)); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dm-accent) 16%, transparent); +} +.public-bookmark-row.public-bookmark-manage:not(.is-published) em { color: var(--dm-text-muted); } +.public-bookmark-row.public-bookmark-manage.is-published em { color: var(--dm-accent-strong); } +html[data-theme="light"] .public-bookmark-row.public-bookmark-manage:has(input:checked) { + border-color: #8eb79c; + background: var(--dm-selected-bg); +} +html[data-theme="light"] .public-bookmark-row.public-bookmark-manage:not(.is-published) em { color: var(--dm-text-muted); } +html[data-theme="light"] .public-bookmark-row.public-bookmark-manage.is-published em { color: var(--dm-accent-strong); } From e02833986c5a0450f5d70ce5df3fba3b3a4554d2 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 18 Sep 2026 11:42:35 +0800 Subject: [PATCH 8/8] fix: clear row selection after individual publication changes --- apps/web/src/PublicPageSettings.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/web/src/PublicPageSettings.tsx b/apps/web/src/PublicPageSettings.tsx index 2b3bd73..ae07135 100644 --- a/apps/web/src/PublicPageSettings.tsx +++ b/apps/web/src/PublicPageSettings.tsx @@ -140,6 +140,11 @@ export function PublicPageSettings({ bookmarks, categories, onChanged }: Props) else next.delete(bookmark.id); return next; }); + setSelectedIds((current) => { + const next = new Set(current); + next.delete(bookmark.id); + return next; + }); setNotice(`${nextPublished ? "Published" : "Unpublished"} “${bookmark.title}”.`); } catch (caught) { setError(caught instanceof Error ? caught.message : "Could not update this bookmark.");