diff --git a/web/src/components/MasonryView/MasonryColumn.tsx b/web/src/components/MasonryView/MasonryColumn.tsx
index 9876435f2..4fbc19dc1 100644
--- a/web/src/components/MasonryView/MasonryColumn.tsx
+++ b/web/src/components/MasonryView/MasonryColumn.tsx
@@ -8,11 +8,12 @@ export function MasonryColumn({
renderContext,
onHeightChange,
isFirstColumn,
+ listMode,
prefixElement,
prefixElementRef,
}: MasonryColumnProps) {
return (
-
+
{/* Prefix element (like memo editor) goes in first column */}
{isFirstColumn && prefixElement &&
{prefixElement}
}
diff --git a/web/src/components/MasonryView/MasonryView.tsx b/web/src/components/MasonryView/MasonryView.tsx
index f20067b16..f94c49b24 100644
--- a/web/src/components/MasonryView/MasonryView.tsx
+++ b/web/src/components/MasonryView/MasonryView.tsx
@@ -36,6 +36,7 @@ const MasonryView = ({ memoList, renderer, prefixElement, listMode = false }: Ma
renderContext={renderContext}
onHeightChange={handleHeightChange}
isFirstColumn={columnIndex === 0}
+ listMode={listMode}
prefixElement={prefixElement}
prefixElementRef={prefixElementRef}
/>
diff --git a/web/src/components/MasonryView/types.ts b/web/src/components/MasonryView/types.ts
index 862ea4ddd..7810049e7 100644
--- a/web/src/components/MasonryView/types.ts
+++ b/web/src/components/MasonryView/types.ts
@@ -26,6 +26,7 @@ export interface MasonryColumnProps {
renderContext: MemoRenderContext;
onHeightChange: (memoName: string, height: number) => void;
isFirstColumn: boolean;
+ listMode?: boolean;
prefixElement?: JSX.Element;
prefixElementRef?: React.RefObject
;
}
diff --git a/web/src/components/MemoContent/MermaidBlock.tsx b/web/src/components/MemoContent/MermaidBlock.tsx
index 48ec20bb1..cbd210e42 100644
--- a/web/src/components/MemoContent/MermaidBlock.tsx
+++ b/web/src/components/MemoContent/MermaidBlock.tsx
@@ -11,7 +11,7 @@ interface MermaidBlockProps {
}
const getMermaidTheme = (appTheme: string): "default" | "dark" => {
- return appTheme === "default-dark" ? "dark" : "default";
+ return appTheme.includes("dark") ? "dark" : "default";
};
export const MermaidBlock = ({ children, className }: MermaidBlockProps) => {
diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx
index 88f6d4590..67afa122b 100644
--- a/web/src/components/PagedMemoList/PagedMemoList.tsx
+++ b/web/src/components/PagedMemoList/PagedMemoList.tsx
@@ -1,6 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
-import { ArchiveIcon, ArrowUpIcon, BookmarkPlusIcon, TrashIcon, XIcon } from "lucide-react";
+import { ArchiveIcon, ArrowUpIcon, BookmarkPlusIcon, ChevronDownIcon, ChevronRightIcon, TrashIcon, XIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { matchPath } from "react-router-dom";
@@ -33,6 +33,7 @@ interface Props {
pageSize?: number;
showCreator?: boolean;
enabled?: boolean;
+ collapsiblePinned?: boolean;
}
function useAutoFetchWhenNotScrollable({
@@ -112,6 +113,29 @@ const PagedMemoList = (props: Props) => {
// Apply custom sorting if provided, otherwise use memos directly
const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]);
+ const enablePinnedSection = props.collapsiblePinned === true;
+ const pinnedStorageKey = "memos.ui.pinsCollapsed";
+
+ const [isPinnedCollapsed, setIsPinnedCollapsed] = useState(() => {
+ if (!enablePinnedSection) return false;
+ if (typeof window === "undefined") return false;
+ return window.localStorage.getItem(pinnedStorageKey) === "true";
+ });
+
+ const pinnedMemos = useMemo(() => {
+ if (!enablePinnedSection) return [];
+ return sortedMemoList.filter((memo) => memo.pinned);
+ }, [enablePinnedSection, sortedMemoList]);
+
+ const unpinnedMemos = useMemo(() => {
+ if (!enablePinnedSection) return sortedMemoList;
+ return sortedMemoList.filter((memo) => !memo.pinned);
+ }, [enablePinnedSection, sortedMemoList]);
+
+ useEffect(() => {
+ if (!enablePinnedSection || typeof window === "undefined") return;
+ window.localStorage.setItem(pinnedStorageKey, String(isPinnedCollapsed));
+ }, [enablePinnedSection, isPinnedCollapsed]);
const selectionContextValue = useMemo(() => {
const selectedCount = selectedMemoNames.size;
@@ -221,19 +245,71 @@ const PagedMemoList = (props: Props) => {
) : (
<>
- {
+ const hasPinned = pinnedMemos.length > 0;
+ const pinnedToggle = enablePinnedSection && hasPinned && (
+
+
+
+
+ );
+
+ const prefixElement = (
<>
{showMemoEditor ? (
) : undefined}
+ {pinnedToggle}
>
+ );
+
+ if (!enablePinnedSection) {
+ return ;
}
- listMode={layout === "LIST"}
- />
+
+ if (layout === "LIST") {
+ const listMemoList = isPinnedCollapsed ? unpinnedMemos : sortedMemoList;
+ const lastPinnedName = !isPinnedCollapsed && hasPinned ? pinnedMemos[pinnedMemos.length - 1]?.name : undefined;
+ const listRenderer = lastPinnedName
+ ? (memo: Memo, context?: MemoRenderContext) => (
+ <>
+ {props.renderer(memo, context)}
+ {memo.name === lastPinnedName && (
+
+ )}
+ >
+ )
+ : props.renderer;
+
+ return ;
+ }
+ return (
+ <>
+
+ {hasPinned && !isPinnedCollapsed && }
+
+
+
+ >
+ );
+ })()}
{/* Loading indicator for pagination */}
{isFetchingNextPage && }
diff --git a/web/src/components/StatisticsView/MonthNavigator.tsx b/web/src/components/StatisticsView/MonthNavigator.tsx
index fe4eadb46..58a67d201 100644
--- a/web/src/components/StatisticsView/MonthNavigator.tsx
+++ b/web/src/components/StatisticsView/MonthNavigator.tsx
@@ -1,3 +1,4 @@
+import dayjs from "dayjs";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useState } from "react";
import { YearCalendar } from "@/components/ActivityCalendar";
@@ -8,7 +9,7 @@ import type { MonthNavigatorProps } from "@/types/statistics";
export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: MonthNavigatorProps) => {
const [isOpen, setIsOpen] = useState(false);
- const currentMonth = new Date(visibleMonth);
+ const currentMonth = dayjs(visibleMonth).toDate();
const currentYear = getYearFromDate(visibleMonth);
const currentMonthNum = getMonthFromDate(visibleMonth);
diff --git a/web/src/components/ThemeSelect.tsx b/web/src/components/ThemeSelect.tsx
index 7b9b4bbf1..9ae5bef4e 100644
--- a/web/src/components/ThemeSelect.tsx
+++ b/web/src/components/ThemeSelect.tsx
@@ -1,4 +1,4 @@
-import { Monitor, Moon, MoonStar, Palette, Sun, Wallpaper } from "lucide-react";
+import { CloudMoon, Leaf, Monitor, Moon, MoonStar, Palette, Sun, Wallpaper } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { loadTheme, THEME_OPTIONS } from "@/utils/theme";
@@ -15,6 +15,8 @@ const THEME_ICONS: Record = {
midnight: ,
paper: ,
whitewall: ,
+ "solarized-light": ,
+ "solarized-dark": ,
};
const ThemeSelect = ({ value, onValueChange, className }: ThemeSelectProps = {}) => {
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx
index 417a0cd10..c055b6dd4 100644
--- a/web/src/pages/Home.tsx
+++ b/web/src/pages/Home.tsx
@@ -28,6 +28,7 @@ const Home = () => {
listSort={listSort}
orderBy={orderBy}
filter={memoFilter}
+ collapsiblePinned
enabled={isInitialized && !!user} // Wait for contexts to stabilize before fetching
/>
diff --git a/web/src/themes/solarized-dark.css b/web/src/themes/solarized-dark.css
new file mode 100644
index 000000000..8de6adbc5
--- /dev/null
+++ b/web/src/themes/solarized-dark.css
@@ -0,0 +1,103 @@
+:root {
+ --background: #002b36;
+ --foreground: #839496;
+ --card: #073642;
+ --card-foreground: #839496;
+ --popover: #073642;
+ --popover-foreground: #839496;
+ --primary: #268bd2;
+ --primary-foreground: #002b36;
+ --secondary: #073642;
+ --secondary-foreground: #93a1a1;
+ --muted: #073642;
+ --muted-foreground: #93a1a1;
+ --accent: #2aa198;
+ --accent-foreground: #002b36;
+ --destructive: #dc322f;
+ --destructive-foreground: #fdf6e3;
+ --border: #0b3d48;
+ --input: #0b3d48;
+ --ring: #268bd2;
+ --chart-1: #b58900;
+ --chart-2: #2aa198;
+ --chart-3: #268bd2;
+ --chart-4: #6c71c4;
+ --chart-5: #859900;
+ --sidebar: #00212a;
+ --sidebar-foreground: #93a1a1;
+ --sidebar-primary: #268bd2;
+ --sidebar-primary-foreground: #002b36;
+ --sidebar-accent: #2aa198;
+ --sidebar-accent-foreground: #002b36;
+ --sidebar-border: #0b3d48;
+ --sidebar-ring: #268bd2;
+ --font-sans:
+ ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ --radius: 0.5rem;
+ --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
+ --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
+ --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
+ --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
+ --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
+ --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
+ --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
+ --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
+ --tracking-normal: 0em;
+ --spacing: 0.25rem;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+
+ --font-sans: var(--font-sans);
+ --font-mono: var(--font-mono);
+ --font-serif: var(--font-serif);
+
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+
+ --shadow-2xs: var(--shadow-2xs);
+ --shadow-xs: var(--shadow-xs);
+ --shadow-sm: var(--shadow-sm);
+ --shadow: var(--shadow);
+ --shadow-md: var(--shadow-md);
+ --shadow-lg: var(--shadow-lg);
+ --shadow-xl: var(--shadow-xl);
+ --shadow-2xl: var(--shadow-2xl);
+}
diff --git a/web/src/themes/solarized-light.css b/web/src/themes/solarized-light.css
new file mode 100644
index 000000000..84b4ab85f
--- /dev/null
+++ b/web/src/themes/solarized-light.css
@@ -0,0 +1,103 @@
+:root {
+ --background: #fdf6e3;
+ --foreground: #657b83;
+ --card: #eee8d5;
+ --card-foreground: #657b83;
+ --popover: #eee8d5;
+ --popover-foreground: #657b83;
+ --primary: #268bd2;
+ --primary-foreground: #fdf6e3;
+ --secondary: #eee8d5;
+ --secondary-foreground: #586e75;
+ --muted: #eee8d5;
+ --muted-foreground: #586e75;
+ --accent: #2aa198;
+ --accent-foreground: #fdf6e3;
+ --destructive: #dc322f;
+ --destructive-foreground: #fdf6e3;
+ --border: #e4ddc5;
+ --input: #e4ddc5;
+ --ring: #268bd2;
+ --chart-1: #b58900;
+ --chart-2: #2aa198;
+ --chart-3: #268bd2;
+ --chart-4: #6c71c4;
+ --chart-5: #859900;
+ --sidebar: #f6f0da;
+ --sidebar-foreground: #586e75;
+ --sidebar-primary: #268bd2;
+ --sidebar-primary-foreground: #fdf6e3;
+ --sidebar-accent: #2aa198;
+ --sidebar-accent-foreground: #fdf6e3;
+ --sidebar-border: #e4ddc5;
+ --sidebar-ring: #268bd2;
+ --font-sans:
+ ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ --radius: 0.5rem;
+ --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
+ --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
+ --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
+ --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
+ --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
+ --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
+ --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
+ --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
+ --tracking-normal: 0em;
+ --spacing: 0.25rem;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+
+ --font-sans: var(--font-sans);
+ --font-mono: var(--font-mono);
+ --font-serif: var(--font-serif);
+
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+
+ --shadow-2xs: var(--shadow-2xs);
+ --shadow-xs: var(--shadow-xs);
+ --shadow-sm: var(--shadow-sm);
+ --shadow: var(--shadow);
+ --shadow-md: var(--shadow-md);
+ --shadow-lg: var(--shadow-lg);
+ --shadow-xl: var(--shadow-xl);
+ --shadow-2xl: var(--shadow-2xl);
+}
diff --git a/web/src/types/modules/setting.d.ts b/web/src/types/modules/setting.d.ts
index 64e9bd2fe..6b557cfe6 100644
--- a/web/src/types/modules/setting.d.ts
+++ b/web/src/types/modules/setting.d.ts
@@ -1 +1 @@
-type Theme = "system" | "default" | "default-dark" | "midnight" | "paper" | "whitewall";
+type Theme = "system" | "default" | "default-dark" | "midnight" | "paper" | "whitewall" | "solarized-light" | "solarized-dark";
diff --git a/web/src/utils/theme.ts b/web/src/utils/theme.ts
index 4b10bec68..8688a0e23 100644
--- a/web/src/utils/theme.ts
+++ b/web/src/utils/theme.ts
@@ -1,13 +1,24 @@
import defaultDarkThemeContent from "../themes/default-dark.css?raw";
import midnightThemeContent from "../themes/midnight.css?raw";
import paperThemeContent from "../themes/paper.css?raw";
+import solarizedDarkThemeContent from "../themes/solarized-dark.css?raw";
+import solarizedLightThemeContent from "../themes/solarized-light.css?raw";
import whitewallThemeContent from "../themes/whitewall.css?raw";
// ============================================================================
// Types and Constants
// ============================================================================
-const VALID_THEMES = ["system", "default", "default-dark", "midnight", "paper", "whitewall"] as const;
+const VALID_THEMES = [
+ "system",
+ "default",
+ "default-dark",
+ "midnight",
+ "paper",
+ "whitewall",
+ "solarized-light",
+ "solarized-dark",
+] as const;
export type Theme = (typeof VALID_THEMES)[number];
export type ResolvedTheme = Exclude
;
@@ -25,6 +36,8 @@ const THEME_CONTENT: Record = {
"default-dark": defaultDarkThemeContent,
midnight: midnightThemeContent,
paper: paperThemeContent,
+ "solarized-light": solarizedLightThemeContent,
+ "solarized-dark": solarizedDarkThemeContent,
whitewall: whitewallThemeContent,
};
@@ -35,6 +48,8 @@ export const THEME_OPTIONS: ThemeOption[] = [
{ value: "midnight", label: "Midnight" },
{ value: "paper", label: "Paper" },
{ value: "whitewall", label: "Whitewall" },
+ { value: "solarized-light", label: "Solarized Light" },
+ { value: "solarized-dark", label: "Solarized Dark" },
];
// ============================================================================