From ac154322f8689cc74551a1c79fcbe3c95c4d37de Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 00:00:40 +0000 Subject: [PATCH] Add 1/2/3-column layout option to the Bookmarks widget The bookmarks list can now render as 1 (default), 2, or 3 columns instead of always being a single-column list, admin-configurable via a pill picker on the Widgets tab. Column count is a new bookmarksColumns field on GlobalSettings (same idiom as the other scalar admin knobs) and rides along in the public GET /api/bookmarks response so the sidebar picks it up without a separate request. --- backend/src/api/public.ts | 4 +- backend/src/storage/db/index.ts | 4 ++ backend/src/storage/db/settings.ts | 4 +- backend/src/storage/db/types.ts | 2 + frontend/src/lib/adminTypes.ts | 2 + frontend/src/lib/api.ts | 6 +-- .../lib/components/admin/BookmarksTab.svelte | 48 ++++++++++++++++++- .../lib/components/admin/WidgetsTab.svelte | 2 +- .../components/sidebar/BookmarksWidget.svelte | 17 ++++++- .../src/lib/components/sidebar/Sidebar.svelte | 6 +-- frontend/src/lib/types.ts | 6 +++ 11 files changed, 87 insertions(+), 14 deletions(-) diff --git a/backend/src/api/public.ts b/backend/src/api/public.ts index 1dc876b..74909da 100644 --- a/backend/src/api/public.ts +++ b/backend/src/api/public.ts @@ -91,8 +91,8 @@ export async function registerPublicRoutes(app: FastifyInstance) { app.get('/api/bookmarks', async (req) => { const bookmarks = bookmarksDb.listBookmarks(); - if (hasPrivateAccess(req)) return bookmarks; - return bookmarks.filter((b) => !b.isPrivate); + const items = hasPrivateAccess(req) ? bookmarks : bookmarks.filter((b) => !b.isPrivate); + return { items, columns: settingsDb.getSettings().bookmarksColumns }; }); app.get('/api/poe2', async () => { diff --git a/backend/src/storage/db/index.ts b/backend/src/storage/db/index.ts index d16ad59..15aad9b 100644 --- a/backend/src/storage/db/index.ts +++ b/backend/src/storage/db/index.ts @@ -195,6 +195,7 @@ export function migrate() { widget_weather_enabled INTEGER NOT NULL DEFAULT 1, widget_stocks_enabled INTEGER NOT NULL DEFAULT 1, widget_bookmarks_enabled INTEGER NOT NULL DEFAULT 1, + bookmarks_columns INTEGER NOT NULL DEFAULT 1, -- 1 | 2 | 3 — sidebar/admin bookmark list layout width widget_poe2_enabled INTEGER NOT NULL DEFAULT 1, widget_order TEXT NOT NULL DEFAULT '["weather","stocks","poe2","bookmarks"]', -- JSON array, admin-sortable via the Widgets tab weather_location_name TEXT, @@ -418,6 +419,9 @@ export function migrate() { if (!hasColumn('global_settings', 'synthesis_num_predict')) { db.exec('ALTER TABLE global_settings ADD COLUMN synthesis_num_predict INTEGER NOT NULL DEFAULT 700'); } + if (!hasColumn('global_settings', 'bookmarks_columns')) { + db.exec('ALTER TABLE global_settings ADD COLUMN bookmarks_columns INTEGER NOT NULL DEFAULT 1'); + } // Seed default categories if none exist yet. "News" sits right under "Top stories" — // general news sources belong here, not on "Top stories" itself, which isn't a real diff --git a/backend/src/storage/db/settings.ts b/backend/src/storage/db/settings.ts index 73206d3..ccb1c20 100644 --- a/backend/src/storage/db/settings.ts +++ b/backend/src/storage/db/settings.ts @@ -27,6 +27,7 @@ function rowToSettings(row: any): GlobalSettings { poe2: !!row.widget_poe2_enabled }, widgetOrder: JSON.parse(row.widget_order), + bookmarksColumns: row.bookmarks_columns, retention: { publishedArticleMaxAgeDays: row.published_article_max_age_days, rawItemMaxAgeDays: row.raw_item_max_age_days, @@ -88,7 +89,7 @@ export function updateSettings(patch: Partial): GlobalSettings { synthesis_num_ctx=$synthesis_num_ctx, synthesis_num_predict=$synthesis_num_predict, widget_weather_enabled=$widget_weather_enabled, widget_stocks_enabled=$widget_stocks_enabled, widget_bookmarks_enabled=$widget_bookmarks_enabled, widget_poe2_enabled=$widget_poe2_enabled, - widget_order=$widget_order, + widget_order=$widget_order, bookmarks_columns=$bookmarks_columns, published_article_max_age_days=$published_article_max_age_days, raw_item_max_age_days=$raw_item_max_age_days, storage_cap_enabled=$storage_cap_enabled, storage_cap_value=$storage_cap_value, storage_cap_unit=$storage_cap_unit, weather_location_name=$weather_location_name, weather_latitude=$weather_latitude, weather_longitude=$weather_longitude, @@ -120,6 +121,7 @@ export function updateSettings(patch: Partial): GlobalSettings { $widget_bookmarks_enabled: merged.widgets.bookmarks ? 1 : 0, $widget_poe2_enabled: merged.widgets.poe2 ? 1 : 0, $widget_order: JSON.stringify(merged.widgetOrder), + $bookmarks_columns: merged.bookmarksColumns, $published_article_max_age_days: merged.retention.publishedArticleMaxAgeDays, $raw_item_max_age_days: merged.retention.rawItemMaxAgeDays, $storage_cap_enabled: merged.retention.storageCapEnabled ? 1 : 0, diff --git a/backend/src/storage/db/types.ts b/backend/src/storage/db/types.ts index ad597f7..cf07653 100644 --- a/backend/src/storage/db/types.ts +++ b/backend/src/storage/db/types.ts @@ -306,6 +306,8 @@ export interface GlobalSettings { }; /** Sidebar widget display order, admin-sortable via the Widgets tab's up/down arrows — mirrored exactly by Sidebar.svelte. */ widgetOrder: ('weather' | 'stocks' | 'bookmarks' | 'poe2')[]; + /** How many columns the bookmark list lays out in, both in the sidebar and the admin panel. */ + bookmarksColumns: 1 | 2 | 3; retention: { publishedArticleMaxAgeDays: number | null; rawItemMaxAgeDays: number | null; diff --git a/frontend/src/lib/adminTypes.ts b/frontend/src/lib/adminTypes.ts index f7041c9..eabaa7b 100644 --- a/frontend/src/lib/adminTypes.ts +++ b/frontend/src/lib/adminTypes.ts @@ -148,6 +148,8 @@ export interface AdminSettings { synthesisNumPredict: number; widgets: AdminWidgetsEnabled; widgetOrder: ('weather' | 'stocks' | 'bookmarks' | 'poe2')[]; + /** How many columns the bookmark list lays out in, both in the sidebar and the admin panel. */ + bookmarksColumns: 1 | 2 | 3; retention: RetentionSettings; categoryPriority: CategoryPriority[]; weather: AdminWeatherSettings; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 387dd23..aca71f1 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,5 +1,5 @@ import { getBackendUrl } from './config'; -import type { MergedArticle, Tag, TrackedEventPublic, Category, Weather, StockTicker, Bookmark, Poe2Data, WidgetsEnabled } from './types'; +import type { MergedArticle, Tag, TrackedEventPublic, Category, Weather, StockTicker, BookmarksFeed, Poe2Data, WidgetsEnabled } from './types'; async function get(path: string, fetchFn: typeof fetch = fetch): Promise { // credentials: 'include' so the private-access cookie (see lib/privateAccess.ts) @@ -54,8 +54,8 @@ export function getStocks(fetchFn?: typeof fetch): Promise { return get('/api/stocks', fetchFn); } -export function getBookmarks(fetchFn?: typeof fetch): Promise { - return get('/api/bookmarks', fetchFn); +export function getBookmarks(fetchFn?: typeof fetch): Promise { + return get('/api/bookmarks', fetchFn); } export function getPoe2(fetchFn?: typeof fetch): Promise { diff --git a/frontend/src/lib/components/admin/BookmarksTab.svelte b/frontend/src/lib/components/admin/BookmarksTab.svelte index 9cac122..6be47b6 100644 --- a/frontend/src/lib/components/admin/BookmarksTab.svelte +++ b/frontend/src/lib/components/admin/BookmarksTab.svelte @@ -1,12 +1,20 @@
Bookmarks {#if bookmarks.length > 0} -
+
1} style:grid-template-columns={columns > 1 ? `repeat(${columns}, 1fr)` : undefined}> {#each bookmarks as bookmark (bookmark.id)} {bookmark.name} {/each} @@ -33,6 +33,10 @@ flex-direction: column; margin-top: 8px; } + .list.grid { + display: grid; + gap: 6px; + } .row { font-size: 13px; padding: 6px 0; @@ -42,6 +46,15 @@ .row:first-child { border-top: none; } + .list.grid .row { + border-top: none; + padding: 6px 8px; + background: var(--surface-2); + border-radius: var(--radius); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .row:hover { color: var(--text-accent); } diff --git a/frontend/src/lib/components/sidebar/Sidebar.svelte b/frontend/src/lib/components/sidebar/Sidebar.svelte index 8dcda03..ac9f4aa 100644 --- a/frontend/src/lib/components/sidebar/Sidebar.svelte +++ b/frontend/src/lib/components/sidebar/Sidebar.svelte @@ -1,6 +1,6 @@