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 +337,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…

}
diff --git a/apps/web/src/settings.css b/apps/web/src/settings.css index e3537f1..06385b8 100644 --- a/apps/web/src/settings.css +++ b/apps/web/src/settings.css @@ -104,3 +104,106 @@ .public-layout-item { grid-template-columns: auto minmax(0, 1fr); } .public-layout-item .public-order-actions { grid-column: 2; } } + + +.public-publish-tools { display: grid; gap: 10px; margin-bottom: 12px; } +.public-publish-search { + display: flex; + align-items: center; + gap: 9px; + min-height: 42px; + padding: 0 11px; + border: 1px solid var(--dm-border); + border-radius: 10px; + background: var(--dm-bg-soft); +} +.public-publish-search > span { flex: 0 0 auto; color: var(--dm-text-muted); font-size: 18px; } +.public-publish-search input { + flex: 1; + min-width: 0; + border: 0; + outline: 0; + color: var(--dm-text); + background: transparent; +} +.public-publish-search input::placeholder { color: var(--dm-text-muted); } +.public-publish-search:focus-within { border-color: var(--dm-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-accent) 14%, transparent); } +.public-publish-filter { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.public-publish-filter button { padding-inline: 8px; font-size: 10px; } + +.public-bulk-bar { + display: flex; + align-items: center; + gap: 10px; + min-height: 46px; + margin-bottom: 10px; + padding: 8px 10px; + border: 1px solid var(--dm-border); + border-radius: 11px; + background: color-mix(in srgb, var(--dm-bg-soft) 78%, transparent); +} +.public-select-visible { display: inline-flex; align-items: center; gap: 7px; color: var(--dm-text-soft); font-size: 10px; cursor: pointer; white-space: nowrap; } +.public-select-visible input, +.public-bookmark-select input { width: 16px; height: 16px; margin: 0; accent-color: var(--dm-accent); } +.public-selected-count { color: var(--dm-text-muted); font-size: 10px; white-space: nowrap; } +.public-bulk-actions { display: flex; align-items: center; gap: 7px; margin-left: auto; } +.public-bulk-actions .secondary { min-height: 32px; padding-inline: 9px; font-size: 10px; } + +.public-bookmark-manager { align-content: start; } +.public-bookmark-row.public-bookmark-manage { + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 10px; + cursor: default; +} +.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); + align-items: center; + gap: 10px; + min-width: 0; + cursor: pointer; +} +.public-bookmark-select > span { display: grid; gap: 3px; min-width: 0; } +.public-bookmark-select strong, +.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-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-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)); } + .public-bulk-bar { align-items: flex-start; flex-wrap: wrap; } + .public-bulk-actions { width: 100%; margin-left: 0; } +} +@media (max-width: 560px) { + .public-publish-filter { grid-template-columns: 1fr; } + .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); }