From 3d80f1b3f44e65a90b4199ec3f9be9e2215d73f6 Mon Sep 17 00:00:00 2001 From: Shak Date: Sun, 13 Sep 2026 16:55:23 +0330 Subject: [PATCH 1/3] fix: make bookmark reordering stick, and stop the drag lagging the cursor Dragging a bookmark reverted on drop, and the dragged item trailed the pointer before lunging after it. Two separate causes. handleDragEnd looked the dragged bookmark up in a different list than the one on screen. Render: getCurrentFolderItems(currentFolderId, instanceId, isPrimary) Drag end: getCurrentFolderItems(currentFolderId, instanceId) That third argument decides which root bookmarks the widget owns. Omitted, the context falls back to `!widgetId || widgetId === 'bookmarks-default'`, so a widget whose instanceId is anything else gets only bookmarks stamped with that exact id. Reordering requires auth, an authenticated user's layout comes from the server with server-assigned instance ids, and existing bookmarks carry widgetId null or 'bookmarks-default'. So the list on screen had bookmarks and the drag-end list was empty, sourceIndex came back -1, and the handler returned with no state change and no toast. bookmark.test.ts already documents the contract: pass true and a widget with a server id gets its legacy bookmarks, omit it and it gets none. The lag was a class overriding dnd-kit's own guard. sortable-bookmark-item applied both the transition from useSortable and a Tailwind class: className={`w-full h-full transition-transform duration-200 ...`} dnd-kit strips the transition from the drag source so it tracks the pointer exactly: with no DragOverlay, shouldDisplaceDragSource is true and getTransition() returns undefined. React then omits the inline property and the class is left standing, so every pointer move set a transform the browser eased over 200ms. Non-dragged items get the same 200ms shuffle inline from that function, so removing the class costs nothing. Alongside, a behaviour-preserving pass on the same path. During a pointer move BookmarkGrid does not re-render, so the props reaching each item keep their identity for the whole drag, which makes memo effective without touching a single callback: - memo on BookmarkItem and FolderBookmarkItem, so only the dragged item re-renders instead of every tile rebuilding its favicon url, title hash, initials and sticker style array - the folder tile's getCurrentFolderItems call, a find plus filter plus copying sort over the whole bookmark array per folder per render, is now memoised on [bookmarks, bookmark.id] - stable items array for SortableContext and a hoisted pointer sensor options object, both of which were new identities every render Measured: background.js is byte identical, and the newtab chunk moves by 121 bytes for the memo wrappers and dependency arrays. The lazy habit-share-modal chunk keeps its exact size; only its hash moves, because it imports the newtab chunk by content-hashed filename. --- src/layouts/bookmark/bookmark-grid.tsx | 11 +++--- src/layouts/bookmark/bookmarks.tsx | 36 ++++++++++--------- .../bookmark/components/bookmark-folder.tsx | 18 ++++++---- .../bookmark/components/bookmark-item.tsx | 5 +-- .../modal/bookmark-folder.modal.tsx | 14 ++++---- .../components/sortable-bookmark-item.tsx | 2 +- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/src/layouts/bookmark/bookmark-grid.tsx b/src/layouts/bookmark/bookmark-grid.tsx index 36682b9f8..abec3fd38 100644 --- a/src/layouts/bookmark/bookmark-grid.tsx +++ b/src/layouts/bookmark/bookmark-grid.tsx @@ -195,6 +195,11 @@ export function BookmarkGrid({ const isAutoRows = isModal || !rowsCount + const sortableIds = useMemo( + () => displayedBookmarks.filter(Boolean).map((bookmark) => bookmark?.id || ''), + [displayedBookmarks] + ) + return (
bookmark?.id || '')} + items={sortableIds} strategy={rectSortingStrategy} > {displayedBookmarks.map((bookmark, i) => bookmark ? (
([]) - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 5, - }, - }) - ) + const sensors = useSensors(useSensor(PointerSensor, POINTER_SENSOR_OPTIONS)) const colsCount = size ? (size.w === 4 ? 5 : size.w) : 5 const rowsCount = size ? size.h : 2 const TOTAL_BOOKMARKS = colsCount * rowsCount + const primaryBookmarkInstanceId = usePrimaryBookmarkInstanceId() + const isPrimary = (() => { + if (!instanceId || instanceId === 'bookmarks-default') return true + if (primaryBookmarkInstanceId === undefined) return true + if (primaryBookmarkInstanceId === null) return true + return primaryBookmarkInstanceId === instanceId + })() + const handleDragEnd = async (event: DragEndEvent) => { if (!isAuthenticated) return showToast(translateError('UNAUTHORIZED') as string, 'error') @@ -64,7 +72,11 @@ export function BookmarksList({ size, instanceId }: BookmarksListProps = {}) { const { active, over } = event if (!over || active.id === over.id) return - const currentItems = getCurrentFolderItems(currentFolderId, instanceId) + const currentItems = getCurrentFolderItems( + currentFolderId, + instanceId, + isPrimary + ) const sourceIndex = currentItems.findIndex( (item) => item.id === active.id || item.onlineId === active.id @@ -137,14 +149,6 @@ export function BookmarksList({ size, instanceId }: BookmarksListProps = {}) { setFolderModalPath([{ id: targetId, title: folder.title }]) } - const primaryBookmarkInstanceId = usePrimaryBookmarkInstanceId() - const isPrimary = (() => { - if (!instanceId || instanceId === 'bookmarks-default') return true - if (primaryBookmarkInstanceId === undefined) return true - if (primaryBookmarkInstanceId === null) return true - return primaryBookmarkInstanceId === instanceId - })() - const currentFolderItems = getCurrentFolderItems( currentFolderId, instanceId, diff --git a/src/layouts/bookmark/components/bookmark-folder.tsx b/src/layouts/bookmark/components/bookmark-folder.tsx index 63ddec3e3..ef474ed0d 100644 --- a/src/layouts/bookmark/components/bookmark-folder.tsx +++ b/src/layouts/bookmark/components/bookmark-folder.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { memo, useMemo, useState } from 'react' import { addOpacityToColor } from '@/common/color' import type { Bookmark } from '../types/bookmark.types' import { RenderStickerPattern } from './bookmark/bookmark-sticker' @@ -8,7 +8,7 @@ import { BookmarkIcon } from './bookmark/bookmark-icon' import { Icon } from '@/icons' import { cn } from '@/common/utils/cn' -export function FolderBookmarkItem({ +export const FolderBookmarkItem = memo(function FolderBookmarkItem({ bookmark, onClick, isDragging = false, @@ -19,13 +19,17 @@ export function FolderBookmarkItem({ isDragging?: boolean onMenuClick?: (e: React.MouseEvent) => void }) { - const { getCurrentFolderItems } = useBookmarkStore() + const { bookmarks, getCurrentFolderItems } = useBookmarkStore() const [isHovered, setIsHovered] = useState(false) - const folderItems = getCurrentFolderItems(bookmark.id) - .filter((item) => item.type === 'BOOKMARK') - .slice(0, 6) + const folderItems = useMemo( + () => + getCurrentFolderItems(bookmark.id) + .filter((item) => item.type === 'BOOKMARK') + .slice(0, 6), + [bookmarks, bookmark.id] + ) const renderFolderIcons = () => { if (bookmark.icon) { @@ -123,4 +127,4 @@ export function FolderBookmarkItem({
) -} +}) diff --git a/src/layouts/bookmark/components/bookmark-item.tsx b/src/layouts/bookmark/components/bookmark-item.tsx index 79cb163a6..d397e0c95 100644 --- a/src/layouts/bookmark/components/bookmark-item.tsx +++ b/src/layouts/bookmark/components/bookmark-item.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react' import { addOpacityToColor, getContrastingTextColor } from '@/common/color' import type { Bookmark } from '../types/bookmark.types' import { BookmarkIcon } from './bookmark/bookmark-icon' @@ -14,7 +15,7 @@ interface BookmarkItemProps { onMenuClick?: (e: React.MouseEvent) => void } -export function BookmarkItem({ +export const BookmarkItem = memo(function BookmarkItem({ bookmark, onClick, isDragging = false, @@ -91,4 +92,4 @@ export function BookmarkItem({
) -} +}) diff --git a/src/layouts/bookmark/components/modal/bookmark-folder.modal.tsx b/src/layouts/bookmark/components/modal/bookmark-folder.modal.tsx index 30d569c01..01bcb3716 100644 --- a/src/layouts/bookmark/components/modal/bookmark-folder.modal.tsx +++ b/src/layouts/bookmark/components/modal/bookmark-folder.modal.tsx @@ -22,6 +22,12 @@ import { AddBookmarkModal } from './add-bookmark.modal' import { ImportBrowserBookmarksModal } from './import-browser-bookmarks.modal' import { validate } from 'uuid' +const POINTER_SENSOR_OPTIONS = { + activationConstraint: { + distance: 5, + }, +} + interface BookmarkFolderModalProps { isOpen: boolean onClose: () => void @@ -47,13 +53,7 @@ export function BookmarkFolderModal({ const [showAddModal, setShowAddModal] = useState(false) const [showImportModal, setShowImportModal] = useState(false) - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 5, - }, - }) - ) + const sensors = useSensors(useSensor(PointerSensor, POINTER_SENSOR_OPTIONS)) const currentFolder = folderPath[folderPath.length - 1] const currentFolderId = currentFolder diff --git a/src/layouts/bookmark/components/sortable-bookmark-item.tsx b/src/layouts/bookmark/components/sortable-bookmark-item.tsx index bb5505fa5..877e9df55 100644 --- a/src/layouts/bookmark/components/sortable-bookmark-item.tsx +++ b/src/layouts/bookmark/components/sortable-bookmark-item.tsx @@ -32,7 +32,7 @@ export function SortableBookmarkItem({
From 3557440f6a46d4a61a1941d84ce4ecb4a3687bfe Mon Sep 17 00:00:00 2001 From: Shak Date: Sun, 13 Sep 2026 17:11:15 +0330 Subject: [PATCH 2/3] fix: stop the glass theme punching raw page content through widgets In the glass theme, moving the mouse across the page intermittently drew sharp, un-blurred page content over the widgets: wallpaper text crossing a bookmark tile, or a dark slab landing on a neighbouring widget. It never happened in dark or light. That last detail is the whole diagnosis. .bg-glass only adds a filter in two themes: [data-theme="glass"] .widget-wrapper, [data-theme="glass"] .bg-glass, [data-theme="glass"] .tooltip { backdrop-filter: blur(16px) saturate(150%); } dark and light ship no such rule, which is why they were clean. Every widget on the canvas is positioned by canvas-widget-outer with transform: baseTransform // translate3d(...), always present A transformed ancestor gets its own compositing layer, and a backdrop-filter inside that layer cannot reach the wallpaper, which is painted on the body outside it. Given nothing valid to sample, Chromium falls back to the wrong surface and composites raw page content on top. The bookmark widget makes it easy to hit: ten tiles, each its own backdrop-filter layer, each running three animations on hover. So backdrop-filter cannot work inside the canvas, and the fix is to stop asking it to: [data-theme="glass"] .widget-outer .bg-glass { backdrop-filter: none; } Scoped to widgets only. Modals, popovers and the search portals render through createPortal, so they are not descendants of .widget-outer and keep their blur, as do the navbar and tooltips. The tiles stay at #171717b3, so they are now flat translucent rather than blurred inside widgets. Bump that value if they read as too see-through. --- src/styles/theme/glass.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/styles/theme/glass.css b/src/styles/theme/glass.css index 7b671f21b..a567d0b8a 100644 --- a/src/styles/theme/glass.css +++ b/src/styles/theme/glass.css @@ -51,6 +51,11 @@ backdrop-filter: blur(16px) saturate(150%); } +[data-theme="glass"] .widget-outer .bg-glass { + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + [data-theme="glass"] .modal-box { -webkit-backdrop-filter: blur(200px) saturate(200%); backdrop-filter: blur(40px) saturate(10%); From 2926df4cc2cc3dcc8b41b251d8995b1b788dbcbf Mon Sep 17 00:00:00 2001 From: Shak Date: Sun, 13 Sep 2026 17:33:32 +0330 Subject: [PATCH 3/3] fix: standardize indentation and formatting in glass theme styles --- src/styles/theme/glass.css | 86 ++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/src/styles/theme/glass.css b/src/styles/theme/glass.css index a567d0b8a..1ef1a308a 100644 --- a/src/styles/theme/glass.css +++ b/src/styles/theme/glass.css @@ -1,72 +1,68 @@ @plugin "daisyui/theme" { - name: "glass"; - --color-base-100: #171717b3; - --color-base-200: rgba(23, 23, 23, 0.1); - --color-base-300: rgba(255, 255, 255, 0.05); - --color-base-content: rgba(255, 255, 255, 0.95); - --color-primary: rgba(var(--brand-primary-rgb), 0.9); - --color-primary-focus: rgba(var(--brand-primary-rgb), 1); - --color-primary-content: #ffffff; - --color-secondary: rgba(var(--brand-secondary-rgb), 0.8); - --color-secondary-focus: rgba(var(--brand-secondary-rgb), 0.9); - --color-secondary-content: rgba(255, 255, 255, 1); - --color-accent: rgba(34, 197, 94, 0.8); - --color-accent-focus: rgba(22, 163, 74, 0.9); - --color-accent-content: rgba(255, 255, 255, 1); - --color-error: rgba(239, 68, 68, 0.8); - --color-success: rgba(34, 197, 94, 0.8); - --color-warning: rgba(245, 158, 11, 0.8); - --color-warning-content: #271b06; - --color-info: rgba(59, 130, 246, 0.8); - --glass: true; - --glass-opacity: 25%; - --glass-border-opacity: 15%; - --glass-reflex-degree: 120%; - --glass-reflex-opacity: 8%; - --glass-blur: 32px; + name: "glass"; + --color-base-100: #171717b3; + --color-base-200: rgba(23, 23, 23, 0.1); + --color-base-300: rgba(255, 255, 255, 0.05); + --color-base-content: rgba(255, 255, 255, 0.95); + --color-primary: rgba(var(--brand-primary-rgb), 0.9); + --color-primary-focus: rgba(var(--brand-primary-rgb), 1); + --color-primary-content: #ffffff; + --color-secondary: rgba(var(--brand-secondary-rgb), 0.8); + --color-secondary-focus: rgba(var(--brand-secondary-rgb), 0.9); + --color-secondary-content: rgba(255, 255, 255, 1); + --color-accent: rgba(34, 197, 94, 0.8); + --color-accent-focus: rgba(22, 163, 74, 0.9); + --color-accent-content: rgba(255, 255, 255, 1); + --color-error: rgba(239, 68, 68, 0.8); + --color-success: rgba(34, 197, 94, 0.8); + --color-warning: rgba(245, 158, 11, 0.8); + --color-warning-content: #271b06; + --color-info: rgba(59, 130, 246, 0.8); + --glass: true; + --glass-opacity: 25%; + --glass-border-opacity: 15%; + --glass-reflex-degree: 120%; + --glass-reflex-opacity: 8%; + --glass-blur: 32px; } - - - [data-theme="glass"] .menu, [data-theme="glass"] .navbar, [data-theme="glass"] .modal-box, [data-theme="glass"] .drawer-side, [data-theme="glass"] .collapse { - -webkit-backdrop-filter: blur(16px) saturate(180%); - backdrop-filter: blur(16px) saturate(180%); + -webkit-backdrop-filter: blur(16px) saturate(180%); + backdrop-filter: blur(16px) saturate(180%); } [data-theme="glass"] .search-box { - -webkit-backdrop-filter: blur(10px) saturate(180%); - backdrop-filter: blur(10px) saturate(180%); + -webkit-backdrop-filter: blur(10px) saturate(180%); + backdrop-filter: blur(10px) saturate(180%); } [data-theme="glass"] .widget-wrapper, [data-theme="glass"] .bg-glass, [data-theme="glass"] .tooltip { - background-color: #171717b3; - -webkit-backdrop-filter: blur(16px) saturate(150%); - backdrop-filter: blur(16px) saturate(150%); + background-color: #171717b3; + -webkit-backdrop-filter: blur(16px) saturate(150%); + backdrop-filter: blur(16px) saturate(150%); } -[data-theme="glass"] .widget-outer .bg-glass { - -webkit-backdrop-filter: none; - backdrop-filter: none; +[data-theme="glass"] .widget-outer { + -webkit-backdrop-filter: none; + backdrop-filter: none; } [data-theme="glass"] .modal-box { - -webkit-backdrop-filter: blur(200px) saturate(200%); - backdrop-filter: blur(40px) saturate(10%); - background-color: #171717b3; + -webkit-backdrop-filter: blur(200px) saturate(200%); + backdrop-filter: blur(40px) saturate(10%); + background-color: #171717b3; } [data-theme="glass"] .portal { - background-color: #171717fa; + background-color: #171717fa; } - [data-theme="glass"] select:open { - background-color: #171717b3; -} \ No newline at end of file + background-color: #171717b3; +}