aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-07-26 12:58:01 +0000
committerMohamed Bassem <me@mbassem.com>2025-07-26 12:58:01 +0000
commit154efe17421ca96d433fcc1f820ad460e1675bdc (patch)
tree4336090648fe7196818bcc371104d3b603a68c0e /apps/web/components
parent8b4fb49cc066eef602d9d089e7b71d183231a8fd (diff)
downloadkarakeep-154efe17421ca96d433fcc1f820ad460e1675bdc.tar.zst
feat: Configurable number of grid columns. Fixes #1713
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/dashboard/ChangeLayout.tsx64
-rw-r--r--apps/web/components/dashboard/GlobalActions.tsx4
-rw-r--r--apps/web/components/dashboard/ViewOptions.tsx115
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx23
4 files changed, 134 insertions, 72 deletions
diff --git a/apps/web/components/dashboard/ChangeLayout.tsx b/apps/web/components/dashboard/ChangeLayout.tsx
deleted file mode 100644
index c7f44a73..00000000
--- a/apps/web/components/dashboard/ChangeLayout.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-"use client";
-
-import React from "react";
-import { ButtonWithTooltip } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { useTranslation } from "@/lib/i18n/client";
-import { useBookmarkLayout } from "@/lib/userLocalSettings/bookmarksLayout";
-import { updateBookmarksLayout } from "@/lib/userLocalSettings/userLocalSettings";
-import {
- Check,
- LayoutDashboard,
- LayoutGrid,
- LayoutList,
- List,
- LucideIcon,
-} from "lucide-react";
-
-type LayoutType = "masonry" | "grid" | "list" | "compact";
-
-const iconMap: Record<LayoutType, LucideIcon> = {
- masonry: LayoutDashboard,
- grid: LayoutGrid,
- list: LayoutList,
- compact: List,
-};
-
-export default function ChangeLayout() {
- const { t } = useTranslation();
- const layout = useBookmarkLayout();
-
- return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <ButtonWithTooltip
- tooltip={t("actions.change_layout")}
- delayDuration={100}
- variant="ghost"
- >
- {React.createElement(iconMap[layout], { size: 18 })}
- </ButtonWithTooltip>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-fit">
- {(Object.keys(iconMap) as LayoutType[]).map((key) => (
- <DropdownMenuItem
- key={key}
- className="cursor-pointer justify-between"
- onClick={async () => await updateBookmarksLayout(key as LayoutType)}
- >
- <div className="flex items-center gap-2">
- {React.createElement(iconMap[key as LayoutType], { size: 18 })}
- <span>{t(`layouts.${key}`)}</span>
- </div>
- {layout == key && <Check className="ml-2 size-4" />}
- </DropdownMenuItem>
- ))}
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/apps/web/components/dashboard/GlobalActions.tsx b/apps/web/components/dashboard/GlobalActions.tsx
index ecbb70bf..d36b93d9 100644
--- a/apps/web/components/dashboard/GlobalActions.tsx
+++ b/apps/web/components/dashboard/GlobalActions.tsx
@@ -1,13 +1,13 @@
"use client";
import BulkBookmarksAction from "@/components/dashboard/BulkBookmarksAction";
-import ChangeLayout from "@/components/dashboard/ChangeLayout";
import SortOrderToggle from "@/components/dashboard/SortOrderToggle";
+import ViewOptions from "@/components/dashboard/ViewOptions";
export default function GlobalActions() {
return (
<div className="flex min-w-max flex-wrap overflow-hidden">
- <ChangeLayout />
+ <ViewOptions />
<SortOrderToggle />
<BulkBookmarksAction />
</div>
diff --git a/apps/web/components/dashboard/ViewOptions.tsx b/apps/web/components/dashboard/ViewOptions.tsx
new file mode 100644
index 00000000..6367421f
--- /dev/null
+++ b/apps/web/components/dashboard/ViewOptions.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import React from "react";
+import { ButtonWithTooltip } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Slider } from "@/components/ui/slider";
+import {
+ useBookmarkLayout,
+ useGridColumns,
+} from "@/lib/userLocalSettings/bookmarksLayout";
+import {
+ updateBookmarksLayout,
+ updateGridColumns,
+} from "@/lib/userLocalSettings/userLocalSettings";
+import {
+ Check,
+ LayoutDashboard,
+ LayoutGrid,
+ LayoutList,
+ List,
+ LucideIcon,
+ Settings,
+} from "lucide-react";
+
+type LayoutType = "masonry" | "grid" | "list" | "compact";
+
+const iconMap: Record<LayoutType, LucideIcon> = {
+ masonry: LayoutDashboard,
+ grid: LayoutGrid,
+ list: LayoutList,
+ compact: List,
+};
+
+const layoutNames: Record<LayoutType, string> = {
+ masonry: "Masonry",
+ grid: "Grid",
+ list: "List",
+ compact: "Compact",
+};
+
+export default function ViewOptions() {
+ const layout = useBookmarkLayout();
+ const gridColumns = useGridColumns();
+ const [tempColumns, setTempColumns] = React.useState(gridColumns);
+
+ const showColumnSlider = layout === "grid" || layout === "masonry";
+
+ // Update temp value when actual value changes
+ React.useEffect(() => {
+ setTempColumns(gridColumns);
+ }, [gridColumns]);
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <ButtonWithTooltip
+ tooltip="View Options"
+ delayDuration={100}
+ variant="ghost"
+ >
+ <Settings size={18} />
+ </ButtonWithTooltip>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="w-56">
+ <div className="px-2 py-1.5 text-sm font-semibold">Layout</div>
+ {(Object.keys(iconMap) as LayoutType[]).map((key) => (
+ <DropdownMenuItem
+ key={key}
+ className="cursor-pointer justify-between"
+ onClick={async () => await updateBookmarksLayout(key as LayoutType)}
+ >
+ <div className="flex items-center gap-2">
+ {React.createElement(iconMap[key as LayoutType], { size: 18 })}
+ <span>{layoutNames[key]}</span>
+ </div>
+ {layout === key && <Check className="ml-2 size-4" />}
+ </DropdownMenuItem>
+ ))}
+
+ {showColumnSlider && (
+ <>
+ <DropdownMenuSeparator />
+ <div className="px-2 py-3">
+ <div className="mb-2 flex items-center justify-between">
+ <span className="text-sm font-semibold">Columns</span>
+ <span className="text-sm text-muted-foreground">
+ {tempColumns}
+ </span>
+ </div>
+ <Slider
+ value={[tempColumns]}
+ onValueChange={([value]) => setTempColumns(value)}
+ onValueCommit={([value]) => updateGridColumns(value)}
+ min={1}
+ max={6}
+ step={1}
+ className="w-full"
+ />
+ <div className="mt-1 flex justify-between text-xs text-muted-foreground">
+ <span>1</span>
+ <span>6</span>
+ </div>
+ </div>
+ </>
+ )}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
index 21bc5fed..954a7751 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
@@ -5,6 +5,7 @@ import useBulkActionsStore from "@/lib/bulkActions";
import {
bookmarkLayoutSwitch,
useBookmarkLayout,
+ useGridColumns,
} from "@/lib/userLocalSettings/bookmarksLayout";
import tailwindConfig from "@/tailwind.config";
import { Slot } from "@radix-ui/react-slot";
@@ -27,15 +28,21 @@ function StyledBookmarkCard({ children }: { children: React.ReactNode }) {
);
}
-function getBreakpointConfig() {
+function getBreakpointConfig(userColumns: number) {
const fullConfig = resolveConfig(tailwindConfig);
const breakpointColumnsObj: { [key: number]: number; default: number } = {
- default: 3,
+ default: userColumns,
};
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = 2;
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = 1;
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = 1;
+
+ // Responsive behavior: reduce columns on smaller screens
+ const lgColumns = Math.max(1, Math.min(userColumns, userColumns - 1));
+ const mdColumns = Math.max(1, Math.min(userColumns, 2));
+ const smColumns = 1;
+
+ breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = lgColumns;
+ breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = mdColumns;
+ breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = smColumns;
return breakpointColumnsObj;
}
@@ -53,8 +60,12 @@ export default function BookmarksGrid({
fetchNextPage?: () => void;
}) {
const layout = useBookmarkLayout();
+ const gridColumns = useGridColumns();
const bulkActionsStore = useBulkActionsStore();
- const breakpointConfig = useMemo(() => getBreakpointConfig(), []);
+ const breakpointConfig = useMemo(
+ () => getBreakpointConfig(gridColumns),
+ [gridColumns],
+ );
const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView();
useEffect(() => {