diff --git a/web/src/components/ConfirmDialog/index.tsx b/web/src/components/ConfirmDialog/index.tsx index 495d68ceb..f868a6a72 100644 --- a/web/src/components/ConfirmDialog/index.tsx +++ b/web/src/components/ConfirmDialog/index.tsx @@ -3,30 +3,16 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; export interface ConfirmDialogProps { - /** Whether the dialog is open */ open: boolean; - /** Open state change callback (closing disabled while loading) */ onOpenChange: (open: boolean) => void; - /** Title content (plain text or React nodes) */ title: React.ReactNode; - /** Optional description (plain text or React nodes) */ description?: React.ReactNode; - /** Confirm / primary action button label */ confirmLabel: string; - /** Cancel button label */ cancelLabel: string; - /** Async or sync confirm handler. Dialog auto-closes on resolve, stays open on reject */ onConfirm: () => void | Promise; - /** Variant style of confirm button */ confirmVariant?: "default" | "destructive"; } -/** - * Accessible confirmation dialog. - * - Renders optional description content - * - Prevents closing while async confirm action is in-flight - * - Minimal opinionated styling; leverages existing UI primitives - */ export default function ConfirmDialog({ open, onOpenChange, diff --git a/web/src/components/MasonryView/MasonryColumn.tsx b/web/src/components/MasonryView/MasonryColumn.tsx index 74b8d6ce1..9876435f2 100644 --- a/web/src/components/MasonryView/MasonryColumn.tsx +++ b/web/src/components/MasonryView/MasonryColumn.tsx @@ -1,15 +1,6 @@ import { MasonryItem } from "./MasonryItem"; import { MasonryColumnProps } from "./types"; -/** - * Column component for masonry layout - * - * Responsibilities: - * - Render a single column in the masonry grid - * - Display prefix element in the first column (e.g., memo editor) - * - Render all assigned memo items in order - * - Pass render context to items (includes compact mode flag) - */ export function MasonryColumn({ memoIndices, memoList, diff --git a/web/src/components/MasonryView/MasonryItem.tsx b/web/src/components/MasonryView/MasonryItem.tsx index 110461cf4..7151a8626 100644 --- a/web/src/components/MasonryView/MasonryItem.tsx +++ b/web/src/components/MasonryView/MasonryItem.tsx @@ -1,19 +1,6 @@ import { useEffect, useRef } from "react"; import { MasonryItemProps } from "./types"; -/** - * Individual item wrapper component for masonry layout - * - * Responsibilities: - * - Render the memo using the provided renderer with context - * - Measure its own height using ResizeObserver - * - Report height changes to parent for redistribution - * - * The ResizeObserver automatically tracks dynamic content changes such as: - * - Images loading - * - Expanded/collapsed text - * - Any other content size changes - */ export function MasonryItem({ memo, renderer, renderContext, onHeightChange }: MasonryItemProps) { const itemRef = useRef(null); const resizeObserverRef = useRef(null); diff --git a/web/src/components/MasonryView/MasonryView.tsx b/web/src/components/MasonryView/MasonryView.tsx index 249f8cb90..dd87c68a4 100644 --- a/web/src/components/MasonryView/MasonryView.tsx +++ b/web/src/components/MasonryView/MasonryView.tsx @@ -4,24 +4,6 @@ import { MasonryColumn } from "./MasonryColumn"; import { MasonryViewProps, MemoRenderContext } from "./types"; import { useMasonryLayout } from "./useMasonryLayout"; -/** - * Masonry layout component for displaying memos in a balanced, multi-column grid - * - * Features: - * - Responsive column count based on viewport width - * - Longest Processing-Time First (LPT) algorithm for optimal distribution - * - Pins editor and first memo to first column for stability - * - Debounced redistribution for performance - * - Automatic height tracking with ResizeObserver - * - Auto-enables compact mode in multi-column layouts - * - * The layout automatically adjusts to: - * - Window resizing - * - Content changes (images loading, text expansion) - * - Dynamic memo additions/removals - * - * Algorithm guarantee: Layout is never more than 34% longer than optimal (proven) - */ const MasonryView = ({ memoList, renderer, prefixElement, listMode = false }: MasonryViewProps) => { const containerRef = useRef(null); const prefixElementRef = useRef(null); diff --git a/web/src/components/MasonryView/constants.ts b/web/src/components/MasonryView/constants.ts index 8f46b7dad..a234505d8 100644 --- a/web/src/components/MasonryView/constants.ts +++ b/web/src/components/MasonryView/constants.ts @@ -1,11 +1,3 @@ -/** - * Minimum width required to show more than one column in masonry layout - * When viewport is narrower, layout falls back to single column - */ export const MINIMUM_MEMO_VIEWPORT_WIDTH = 512; -/** - * Debounce delay for redistribution in milliseconds - * Balances responsiveness with performance by batching rapid height changes - */ export const REDISTRIBUTION_DEBOUNCE_MS = 100; diff --git a/web/src/components/MasonryView/distributeItems.ts b/web/src/components/MasonryView/distributeItems.ts index 2802639dd..0fb1c22e7 100644 --- a/web/src/components/MasonryView/distributeItems.ts +++ b/web/src/components/MasonryView/distributeItems.ts @@ -1,27 +1,12 @@ import { Memo } from "@/types/proto/api/v1/memo_service"; import { DistributionResult } from "./types"; -/** - * Distributes memos into columns using a height-aware greedy approach. - * - * Algorithm steps: - * 1. Pin editor and first memo to the first column (keep feed stable) - * 2. Place remaining memos into the currently shortest column - * 3. Break height ties by preferring the column with fewer items - * - * @param memos - Array of memos to distribute - * @param columns - Number of columns to distribute across - * @param itemHeights - Map of memo names to their measured heights - * @param prefixElementHeight - Height of prefix element (e.g., editor) in first column - * @returns Distribution result with memo indices per column and column heights - */ export function distributeItemsToColumns( memos: Memo[], columns: number, itemHeights: Map, prefixElementHeight: number = 0, ): DistributionResult { - // Single column mode: all memos in one column if (columns === 1) { const totalHeight = memos.reduce((sum, memo) => sum + (itemHeights.get(memo.name) || 0), prefixElementHeight); return { @@ -30,19 +15,16 @@ export function distributeItemsToColumns( }; } - // Initialize columns and their heights const distribution: number[][] = Array.from({ length: columns }, () => []); const columnHeights: number[] = Array(columns).fill(0); const columnCounts: number[] = Array(columns).fill(0); - // Add prefix element height to first column if (prefixElementHeight > 0) { columnHeights[0] = prefixElementHeight; } let startIndex = 0; - // Pin the first memo to the first column to keep top-of-feed stable if (memos.length > 0) { const firstMemoHeight = itemHeights.get(memos[0].name) || 0; distribution[0].push(0); @@ -55,7 +37,6 @@ export function distributeItemsToColumns( const memo = memos[i]; const height = itemHeights.get(memo.name) || 0; - // Find column with minimum height const shortestColumnIndex = findShortestColumnIndex(columnHeights, columnCounts); distribution[shortestColumnIndex].push(i); @@ -66,12 +47,6 @@ export function distributeItemsToColumns( return { distribution, columnHeights }; } -/** - * Finds the index of the column with the minimum height - * @param columnHeights - Array of column heights - * @param columnCounts - Array of items per column (for tie-breaking) - * @returns Index of the shortest column - */ function findShortestColumnIndex(columnHeights: number[], columnCounts: number[]): number { let minIndex = 0; let minHeight = columnHeights[0]; @@ -84,7 +59,6 @@ function findShortestColumnIndex(columnHeights: number[], columnCounts: number[] continue; } - // Tie-breaker: prefer column with fewer items to avoid stacking if (currentHeight === minHeight && columnCounts[i] < columnCounts[minIndex]) { minIndex = i; } diff --git a/web/src/components/MasonryView/types.ts b/web/src/components/MasonryView/types.ts index c14730849..61eeee7b5 100644 --- a/web/src/components/MasonryView/types.ts +++ b/web/src/components/MasonryView/types.ts @@ -1,81 +1,41 @@ import { Memo } from "@/types/proto/api/v1/memo_service"; -/** - * Render context passed to memo renderer - */ export interface MemoRenderContext { - /** Whether to render in compact mode (automatically enabled for multi-column layouts) */ compact: boolean; - /** Current number of columns in the layout */ columns: number; } -/** - * Props for the main MasonryView component - */ export interface MasonryViewProps { - /** List of memos to display in masonry layout */ memoList: Memo[]; - /** Render function for each memo. Second parameter provides layout context. */ renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element; - /** Optional element to display at the top of the first column (e.g., memo editor) */ prefixElement?: JSX.Element; - /** Force single column layout regardless of viewport width */ listMode?: boolean; } -/** - * Props for individual MasonryItem component - */ export interface MasonryItemProps { - /** The memo to render */ memo: Memo; - /** Render function for the memo */ renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element; - /** Render context for the memo */ renderContext: MemoRenderContext; - /** Callback when item height changes */ onHeightChange: (memoName: string, height: number) => void; } -/** - * Props for MasonryColumn component - */ export interface MasonryColumnProps { - /** Indices of memos in this column */ memoIndices: number[]; - /** Full list of memos */ memoList: Memo[]; - /** Render function for each memo */ renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element; - /** Render context for memos */ renderContext: MemoRenderContext; - /** Callback when item height changes */ onHeightChange: (memoName: string, height: number) => void; - /** Whether this is the first column (for prefix element) */ isFirstColumn: boolean; - /** Optional prefix element (only rendered in first column) */ prefixElement?: JSX.Element; - /** Ref for prefix element height measurement */ prefixElementRef?: React.RefObject; } -/** - * Result of the distribution algorithm - */ export interface DistributionResult { - /** Array of arrays, where each inner array contains memo indices for that column */ distribution: number[][]; - /** Height of each column after distribution */ columnHeights: number[]; } -/** - * Memo item with measured height - */ export interface MemoWithHeight { - /** Index of the memo in the original list */ index: number; - /** Measured height in pixels */ height: number; } diff --git a/web/src/components/MasonryView/useMasonryLayout.ts b/web/src/components/MasonryView/useMasonryLayout.ts index 12b7ad6d1..07faebdcc 100644 --- a/web/src/components/MasonryView/useMasonryLayout.ts +++ b/web/src/components/MasonryView/useMasonryLayout.ts @@ -3,21 +3,6 @@ import { Memo } from "@/types/proto/api/v1/memo_service"; import { MINIMUM_MEMO_VIEWPORT_WIDTH, REDISTRIBUTION_DEBOUNCE_MS } from "./constants"; import { distributeItemsToColumns } from "./distributeItems"; -/** - * Custom hook for managing masonry layout state and logic - * - * Responsibilities: - * - Calculate optimal number of columns based on viewport width - * - Track item heights and trigger redistribution - * - Debounce redistribution to prevent excessive reflows - * - Handle window resize events - * - * @param memoList - Array of memos to layout - * @param listMode - Force single column mode - * @param containerRef - Reference to the container element - * @param prefixElementRef - Reference to the prefix element - * @returns Layout state and handlers - */ export function useMasonryLayout( memoList: Memo[], listMode: boolean, @@ -31,28 +16,18 @@ export function useMasonryLayout( const redistributionTimeoutRef = useRef(null); const itemHeightsRef = useRef>(itemHeights); - // Keep ref in sync with state useEffect(() => { itemHeightsRef.current = itemHeights; }, [itemHeights]); - /** - * Calculate optimal number of columns based on container width - * Uses a scale factor to determine column count - */ const calculateColumns = useCallback(() => { if (!containerRef.current || listMode) return 1; const containerWidth = containerRef.current.offsetWidth; const scale = containerWidth / MINIMUM_MEMO_VIEWPORT_WIDTH; - // Use ceiling to maximize columns: 688px (1.34x) → 2 cols, 1280px (2.5x) → 3 cols - // Only use single column if scale is very small (< 1.2) return scale >= 1.2 ? Math.ceil(scale) : 1; }, [containerRef, listMode]); - /** - * Recalculate memo distribution when layout changes - */ const redistributeMemos = useCallback(() => { const prefixHeight = prefixElementRef.current?.offsetHeight || 0; setDistribution(() => { @@ -61,17 +36,12 @@ export function useMasonryLayout( }); }, [memoList, columns, prefixElementRef]); - /** - * Debounced redistribution to batch multiple height changes and prevent excessive reflows - */ const debouncedRedistribute = useCallback( (newItemHeights: Map) => { - // Clear any pending redistribution if (redistributionTimeoutRef.current) { clearTimeout(redistributionTimeoutRef.current); } - // Schedule new redistribution after debounce delay redistributionTimeoutRef.current = window.setTimeout(() => { const prefixHeight = prefixElementRef.current?.offsetHeight || 0; setDistribution(() => { @@ -83,34 +53,24 @@ export function useMasonryLayout( [memoList, columns, prefixElementRef], ); - /** - * Handle height changes from individual memo items - */ const handleHeightChange = useCallback( (memoName: string, height: number) => { setItemHeights((prevHeights) => { const newItemHeights = new Map(prevHeights); const previousHeight = prevHeights.get(memoName); - // Skip if height hasn't changed (avoid unnecessary updates) if (previousHeight === height) { return prevHeights; } newItemHeights.set(memoName, height); - - // Use debounced redistribution to batch updates debouncedRedistribute(newItemHeights); - return newItemHeights; }); }, [debouncedRedistribute], ); - /** - * Handle window resize and calculate new column count - */ useEffect(() => { const handleResize = () => { if (!containerRef.current) return; @@ -126,16 +86,10 @@ export function useMasonryLayout( return () => window.removeEventListener("resize", handleResize); }, [calculateColumns, columns, containerRef]); - /** - * Redistribute memos when columns or memo list change - */ useEffect(() => { redistributeMemos(); }, [columns, memoList, redistributeMemos]); - /** - * Cleanup timeout on unmount - */ useEffect(() => { return () => { if (redistributionTimeoutRef.current) { diff --git a/web/src/components/MemoActionMenu/MemoActionMenu.tsx b/web/src/components/MemoActionMenu/MemoActionMenu.tsx index dc55b2ba4..58d069135 100644 --- a/web/src/components/MemoActionMenu/MemoActionMenu.tsx +++ b/web/src/components/MemoActionMenu/MemoActionMenu.tsx @@ -30,15 +30,6 @@ import { hasCompletedTasks } from "@/utils/markdown-manipulation"; import { useMemoActionHandlers } from "./hooks"; import type { MemoActionMenuProps } from "./types"; -/** - * MemoActionMenu component provides a dropdown menu with actions for a memo: - * - Pin/Unpin - * - Edit - * - Copy (link/content) - * - Remove completed tasks - * - Archive/Restore - * - Delete - */ const MemoActionMenu = observer((props: MemoActionMenuProps) => { const { memo, readonly } = props; const t = useTranslate(); diff --git a/web/src/components/MemoActionMenu/hooks.ts b/web/src/components/MemoActionMenu/hooks.ts index ce3bb8308..b637adc69 100644 --- a/web/src/components/MemoActionMenu/hooks.ts +++ b/web/src/components/MemoActionMenu/hooks.ts @@ -16,9 +16,6 @@ interface UseMemoActionHandlersOptions { setRemoveTasksDialogOpen: (open: boolean) => void; } -/** - * Hook for handling memo action menu operations - */ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRemoveTasksDialogOpen }: UseMemoActionHandlersOptions) => { const t = useTranslate(); const location = useLocation(); diff --git a/web/src/components/MemoActionMenu/types.ts b/web/src/components/MemoActionMenu/types.ts index c5e0ddbce..bca4d8794 100644 --- a/web/src/components/MemoActionMenu/types.ts +++ b/web/src/components/MemoActionMenu/types.ts @@ -1,22 +1,12 @@ import type { Memo } from "@/types/proto/api/v1/memo_service"; -/** - * Props for MemoActionMenu component - */ export interface MemoActionMenuProps { - /** The memo to display actions for */ memo: Memo; - /** Whether the current user can only view (not edit) */ readonly?: boolean; - /** Additional CSS classes */ className?: string; - /** Callback when edit action is triggered */ onEdit?: () => void; } -/** - * Return type for useMemoActionHandlers hook - */ export interface UseMemoActionHandlersReturn { handleTogglePinMemoBtnClick: () => Promise; handleEditMemoClick: () => void; diff --git a/web/src/components/MemoContent/ConditionalComponent.tsx b/web/src/components/MemoContent/ConditionalComponent.tsx index d1df56edb..7fcce0647 100644 --- a/web/src/components/MemoContent/ConditionalComponent.tsx +++ b/web/src/components/MemoContent/ConditionalComponent.tsx @@ -1,16 +1,5 @@ import React from "react"; -/** - * Creates a conditional component wrapper that checks AST node properties - * before deciding which component to render. - * - * This is more efficient than having every component check its own props, - * and allows us to use specific HTML element types as defaults. - * - * @param CustomComponent - Component to render when condition is met - * @param DefaultComponent - Component/element to render otherwise - * @param condition - Function to check if node matches custom component criteria - */ export const createConditionalComponent =

>( CustomComponent: React.ComponentType

, DefaultComponent: React.ComponentType

| keyof JSX.IntrinsicElements, @@ -32,13 +21,7 @@ export const createConditionalComponent =

>( }; }; -/** - * Condition checkers for AST node types - * - * These check the original MDAST node type preserved during transformation: - * - First checks node.data.mdastType (preserved by remarkPreserveType plugin) - * - Falls back to checking HAST properties/className for compatibility - */ +// Condition checkers for AST node types export const isTagNode = (node: any): boolean => { // Check preserved mdast type first if (node?.data?.mdastType === "tagNode") { diff --git a/web/src/components/MemoContent/MemoContentContext.tsx b/web/src/components/MemoContent/MemoContentContext.tsx index 3be3eeb97..320f9f87f 100644 --- a/web/src/components/MemoContent/MemoContentContext.tsx +++ b/web/src/components/MemoContent/MemoContentContext.tsx @@ -1,26 +1,10 @@ import { createContext } from "react"; -/** - * Context for MemoContent rendering - * - * Provides memo metadata and configuration to child components - * Used by custom react-markdown components (TaskListItem, Tag, etc.) - */ - export interface MemoContentContextType { - /** The memo resource name (e.g., "memos/123") */ memoName?: string; - - /** Whether content is readonly (non-editable) */ readonly: boolean; - - /** Whether to disable tag/link filtering */ disableFilter?: boolean; - - /** Parent page path (for navigation) */ parentPage?: string; - - /** Reference to the container element for the memo content */ containerRef?: React.RefObject; } diff --git a/web/src/components/MemoContent/MermaidBlock.tsx b/web/src/components/MemoContent/MermaidBlock.tsx index 97516e4e4..f28978c92 100644 --- a/web/src/components/MemoContent/MermaidBlock.tsx +++ b/web/src/components/MemoContent/MermaidBlock.tsx @@ -10,9 +10,6 @@ interface MermaidBlockProps { className?: string; } -/** - * Maps app theme to Mermaid theme - */ const getMermaidTheme = (appTheme: string): "default" | "dark" => { return appTheme === "default-dark" ? "dark" : "default"; }; diff --git a/web/src/components/MemoContent/Tag.tsx b/web/src/components/MemoContent/Tag.tsx index 66e975c7c..ece2224a8 100644 --- a/web/src/components/MemoContent/Tag.tsx +++ b/web/src/components/MemoContent/Tag.tsx @@ -7,16 +7,6 @@ import { memoFilterStore } from "@/store"; import { MemoFilter, stringifyFilters } from "@/store/memoFilter"; import { MemoContentContext } from "./MemoContentContext"; -/** - * Custom span component for #tag elements - * - * Handles tag clicks for filtering memos. - * The remark-tag plugin creates span elements with class="tag". - * - * Note: This component should only be used for tags. - * Regular spans are handled by the default span element. - */ - interface TagProps extends React.HTMLAttributes { node?: any; // AST node from react-markdown "data-tag"?: string; diff --git a/web/src/components/MemoContent/TaskListItem.tsx b/web/src/components/MemoContent/TaskListItem.tsx index d90df7a6c..db007fbe9 100644 --- a/web/src/components/MemoContent/TaskListItem.tsx +++ b/web/src/components/MemoContent/TaskListItem.tsx @@ -4,16 +4,6 @@ import { memoStore } from "@/store"; import { toggleTaskAtIndex } from "@/utils/markdown-manipulation"; import { MemoContentContext } from "./MemoContentContext"; -/** - * Custom checkbox component for react-markdown task lists - * - * Handles interactive task checkbox clicks and updates memo content. - * This component is used via react-markdown's components prop. - * - * Note: This component should only be used for task list checkboxes. - * Regular inputs are handled by the default input element. - */ - interface TaskListItemProps extends React.InputHTMLAttributes { node?: any; // AST node from react-markdown checked?: boolean; diff --git a/web/src/components/MemoEditor/Editor/CommandSuggestions.tsx b/web/src/components/MemoEditor/Editor/CommandSuggestions.tsx index 731426956..2abf56255 100644 --- a/web/src/components/MemoEditor/Editor/CommandSuggestions.tsx +++ b/web/src/components/MemoEditor/Editor/CommandSuggestions.tsx @@ -11,15 +11,6 @@ interface CommandSuggestionsProps { commands: Command[]; } -/** - * Command suggestions popup that appears when typing "/" in the editor. - * Shows available editor commands like formatting options, insertions, etc. - * - * Usage: - * - Type "/" to trigger - * - Continue typing to filter commands - * - Use Arrow keys to navigate, Enter/Tab to select - */ const CommandSuggestions = observer(({ editorRef, editorActions, commands }: CommandSuggestionsProps) => { const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({ editorRef, diff --git a/web/src/components/MemoEditor/Editor/SuggestionsPopup.tsx b/web/src/components/MemoEditor/Editor/SuggestionsPopup.tsx index e165e88a1..e2b0c4362 100644 --- a/web/src/components/MemoEditor/Editor/SuggestionsPopup.tsx +++ b/web/src/components/MemoEditor/Editor/SuggestionsPopup.tsx @@ -11,16 +11,6 @@ interface SuggestionsPopupProps { getItemKey: (item: T, index: number) => string; } -/** - * Shared popup component for displaying suggestion items. - * Provides consistent styling and behavior across different suggestion types. - * - * Features: - * - Automatically scrolls selected item into view - * - Handles keyboard navigation highlighting - * - Prevents text selection during mouse interaction - * - Consistent styling with max height constraints - */ export function SuggestionsPopup({ position, suggestions, diff --git a/web/src/components/MemoEditor/Editor/TagSuggestions.tsx b/web/src/components/MemoEditor/Editor/TagSuggestions.tsx index da2141aef..9204b24f1 100644 --- a/web/src/components/MemoEditor/Editor/TagSuggestions.tsx +++ b/web/src/components/MemoEditor/Editor/TagSuggestions.tsx @@ -11,16 +11,6 @@ interface TagSuggestionsProps { editorActions: React.ForwardedRef; } -/** - * Tag suggestions popup that appears when typing "#" in the editor. - * Shows previously used tags sorted by frequency. - * - * Usage: - * - Type "#" to trigger - * - Continue typing to filter tags - * - Use Arrow keys to navigate, Enter/Tab to select - * - Tags are sorted by usage count (most used first) - */ const TagSuggestions = observer(({ editorRef, editorActions }: TagSuggestionsProps) => { // Sort tags by usage count (descending), then alphabetically for ties const sortedTags = useMemo( diff --git a/web/src/components/MemoEditor/Editor/commands.ts b/web/src/components/MemoEditor/Editor/commands.ts index 6c863fa5c..e07f92cb5 100644 --- a/web/src/components/MemoEditor/Editor/commands.ts +++ b/web/src/components/MemoEditor/Editor/commands.ts @@ -1,6 +1,3 @@ -/** - * Command type for slash commands in the editor - */ export interface Command { name: string; run: () => string; diff --git a/web/src/components/MemoEditor/Editor/index.tsx b/web/src/components/MemoEditor/Editor/index.tsx index 290e9eabe..736a44f85 100644 --- a/web/src/components/MemoEditor/Editor/index.tsx +++ b/web/src/components/MemoEditor/Editor/index.tsx @@ -28,13 +28,9 @@ interface Props { placeholder: string; onContentChange: (content: string) => void; onPaste: (event: React.ClipboardEvent) => void; - /** Whether Focus Mode is active - adjusts height constraints for immersive writing */ isFocusMode?: boolean; - /** Whether IME composition is in progress (for Asian language input) */ isInIME?: boolean; - /** Called when IME composition starts */ onCompositionStart?: () => void; - /** Called when IME composition ends */ onCompositionEnd?: () => void; } diff --git a/web/src/components/MemoEditor/Editor/markdownShortcuts.ts b/web/src/components/MemoEditor/Editor/markdownShortcuts.ts index 4192042cb..ff3eb5a18 100644 --- a/web/src/components/MemoEditor/Editor/markdownShortcuts.ts +++ b/web/src/components/MemoEditor/Editor/markdownShortcuts.ts @@ -1,18 +1,14 @@ import type { EditorRefActions } from "./index"; -/** - * Handles keyboard shortcuts for markdown formatting - * Requires Cmd/Ctrl key to be pressed - */ export function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: EditorRefActions): void { switch (event.key.toLowerCase()) { case "b": event.preventDefault(); - toggleTextStyle(editor, "**"); // Bold + toggleTextStyle(editor, "**"); break; case "i": event.preventDefault(); - toggleTextStyle(editor, "*"); // Italic + toggleTextStyle(editor, "*"); break; case "k": event.preventDefault(); @@ -21,21 +17,14 @@ export function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: Edit } } -/** - * Inserts a hyperlink for the selected text - * If selected text is a URL, creates a link with empty text - * Otherwise, creates a link with placeholder URL - */ export function insertHyperlink(editor: EditorRefActions, url?: string): void { const cursorPosition = editor.getCursorPosition(); const selectedContent = editor.getSelectedContent(); const placeholderUrl = "url"; const urlRegex = /^https?:\/\/[^\s]+$/; - // If selected content looks like a URL and no URL provided, use it as the href if (!url && urlRegex.test(selectedContent.trim())) { editor.insertText(`[](${selectedContent})`); - // Move cursor between brackets for text input editor.setCursorPosition(cursorPosition + 1, cursorPosition + 1); return; } @@ -43,44 +32,32 @@ export function insertHyperlink(editor: EditorRefActions, url?: string): void { const href = url ?? placeholderUrl; editor.insertText(`[${selectedContent}](${href})`); - // If using placeholder URL, select it for easy replacement if (href === placeholderUrl) { - const urlStart = cursorPosition + selectedContent.length + 3; // After "](" + const urlStart = cursorPosition + selectedContent.length + 3; editor.setCursorPosition(urlStart, urlStart + href.length); } } -/** - * Toggles text styling (bold, italic, etc.) - * If already styled, removes the style; otherwise adds it - */ function toggleTextStyle(editor: EditorRefActions, delimiter: string): void { const cursorPosition = editor.getCursorPosition(); const selectedContent = editor.getSelectedContent(); - // Check if already styled - remove style if (selectedContent.startsWith(delimiter) && selectedContent.endsWith(delimiter)) { const unstyled = selectedContent.slice(delimiter.length, -delimiter.length); editor.insertText(unstyled); editor.setCursorPosition(cursorPosition, cursorPosition + unstyled.length); } else { - // Add style editor.insertText(`${delimiter}${selectedContent}${delimiter}`); editor.setCursorPosition(cursorPosition + delimiter.length, cursorPosition + delimiter.length + selectedContent.length); } } -/** - * Hyperlinks the currently highlighted/selected text with the given URL - * Used when pasting a URL while text is selected - */ export function hyperlinkHighlightedText(editor: EditorRefActions, url: string): void { const selectedContent = editor.getSelectedContent(); const cursorPosition = editor.getCursorPosition(); editor.insertText(`[${selectedContent}](${url})`); - // Position cursor after the link - const newPosition = cursorPosition + selectedContent.length + url.length + 4; // []() + const newPosition = cursorPosition + selectedContent.length + url.length + 4; editor.setCursorPosition(newPosition, newPosition); } diff --git a/web/src/components/MemoEditor/Editor/useListAutoCompletion.ts b/web/src/components/MemoEditor/Editor/useListAutoCompletion.ts index 408018ddf..938650028 100644 --- a/web/src/components/MemoEditor/Editor/useListAutoCompletion.ts +++ b/web/src/components/MemoEditor/Editor/useListAutoCompletion.ts @@ -8,19 +8,6 @@ interface UseListAutoCompletionOptions { isInIME: boolean; } -/** - * Custom hook for handling markdown list auto-completion. - * When the user presses Enter on a list item, this hook automatically - * continues the list with the appropriate formatting. - * - * Supports: - * - Ordered lists (1. item, 2. item, etc.) - * - Unordered lists (- item, * item, + item) - * - Task lists (- [ ] task, - [x] task) - * - Nested lists with proper indentation - * - * This hook manages its own event listeners and cleanup. - */ export function useListAutoCompletion({ editorRef, editorActions, isInIME }: UseListAutoCompletionOptions) { // Use refs to avoid stale closures in event handlers const isInIMERef = useRef(isInIME); diff --git a/web/src/components/MemoEditor/Editor/useSuggestions.ts b/web/src/components/MemoEditor/Editor/useSuggestions.ts index d3a318875..6c5c1e2c0 100644 --- a/web/src/components/MemoEditor/Editor/useSuggestions.ts +++ b/web/src/components/MemoEditor/Editor/useSuggestions.ts @@ -9,58 +9,22 @@ export interface Position { } export interface UseSuggestionsOptions { - /** Reference to the textarea element */ editorRef: React.RefObject; - /** Reference to editor actions for text manipulation */ editorActions: React.ForwardedRef; - /** Character that triggers the suggestions (e.g., '/', '#', '@') */ triggerChar: string; - /** Array of items to show in suggestions */ items: T[]; - /** Function to filter items based on search query */ filterItems: (items: T[], searchQuery: string) => T[]; - /** Callback when an item is selected for autocomplete */ onAutocomplete: (item: T, word: string, startIndex: number, actions: EditorRefActions) => void; } export interface UseSuggestionsReturn { - /** Current position of the popup, or null if hidden */ position: Position | null; - /** Filtered suggestions based on current search */ suggestions: T[]; - /** Index of the currently selected suggestion */ selectedIndex: number; - /** Whether the suggestions popup is visible */ isVisible: boolean; - /** Handler to select a suggestion item */ handleItemSelect: (item: T) => void; } -/** - * Shared hook for managing suggestion popups in the editor. - * Handles positioning, keyboard navigation, filtering, and autocomplete logic. - * - * Features: - * - Auto-positioning based on caret location - * - Keyboard navigation (Arrow Up/Down, Enter, Tab, Escape) - * - Smart filtering based on trigger character - * - Proper event cleanup - * - * @example - * ```tsx - * const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({ - * editorRef, - * editorActions, - * triggerChar: '#', - * items: tags, - * filterItems: (items, query) => items.filter(tag => tag.includes(query)), - * onAutocomplete: (tag, word, index, actions) => { - * actions.removeText(index, word.length); - * actions.insertText(`#${tag}`); - * }, - * }); - * ``` - */ export function useSuggestions({ editorRef, editorActions, diff --git a/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx b/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx index ae138c179..3c51a04c5 100644 --- a/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx +++ b/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx @@ -84,15 +84,11 @@ const InsertMenu = observer((props: Props) => { }; const handleLocationCancel = () => { - abortGeocoding(); // Cancel any pending geocoding request + abortGeocoding(); location.reset(); setLocationDialogOpen(false); }; - /** - * Fetches human-readable address from coordinates using reverse geocoding - * Falls back to coordinate string if geocoding fails - */ const fetchReverseGeocode = async (position: LatLng, signal: AbortSignal): Promise => { const coordString = `${position.lat.toFixed(6)}, ${position.lng.toFixed(6)}`; try { diff --git a/web/src/components/MemoEditor/components/ErrorBoundary.tsx b/web/src/components/MemoEditor/components/ErrorBoundary.tsx index f82ba9987..e8bf69bdc 100644 --- a/web/src/components/MemoEditor/components/ErrorBoundary.tsx +++ b/web/src/components/MemoEditor/components/ErrorBoundary.tsx @@ -11,11 +11,6 @@ interface State { error: Error | null; } -/** - * Error Boundary for MemoEditor - * Catches JavaScript errors anywhere in the editor component tree, - * logs the error, and displays a fallback UI instead of crashing the entire app. - */ class MemoEditorErrorBoundary extends React.Component { constructor(props: Props) { super(props); diff --git a/web/src/components/MemoEditor/components/FocusModeOverlay.tsx b/web/src/components/MemoEditor/components/FocusModeOverlay.tsx index 346479d4e..6b8ac289d 100644 --- a/web/src/components/MemoEditor/components/FocusModeOverlay.tsx +++ b/web/src/components/MemoEditor/components/FocusModeOverlay.tsx @@ -7,10 +7,6 @@ interface FocusModeOverlayProps { onToggle: () => void; } -/** - * Focus mode overlay with backdrop and exit button - * Renders the semi-transparent backdrop when focus mode is active - */ export function FocusModeOverlay({ isActive, onToggle }: FocusModeOverlayProps) { if (!isActive) return null; @@ -31,10 +27,6 @@ interface FocusModeExitButtonProps { title: string; } -/** - * Exit button for focus mode - * Displayed in the top-right corner when focus mode is active - */ export function FocusModeExitButton({ isActive, onToggle, title }: FocusModeExitButtonProps) { if (!isActive) return null; diff --git a/web/src/components/MemoEditor/components/LinkMemoDialog.tsx b/web/src/components/MemoEditor/components/LinkMemoDialog.tsx index 9198cadf1..5a53e5788 100644 --- a/web/src/components/MemoEditor/components/LinkMemoDialog.tsx +++ b/web/src/components/MemoEditor/components/LinkMemoDialog.tsx @@ -3,9 +3,6 @@ import { Input } from "@/components/ui/input"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; -/** - * Highlights search text within content string - */ function highlightSearchText(content: string, searchText: string): React.ReactNode { if (!searchText) return content; diff --git a/web/src/components/MemoEditor/constants.ts b/web/src/components/MemoEditor/constants.ts index c3c4356fd..f3897d14d 100644 --- a/web/src/components/MemoEditor/constants.ts +++ b/web/src/components/MemoEditor/constants.ts @@ -1,47 +1,18 @@ -/** - * MemoEditor Constants - * Centralized configuration for the memo editor component - */ - -/** - * Debounce delay for localStorage writes (in milliseconds) - * Prevents excessive writes on every keystroke - */ export const LOCALSTORAGE_DEBOUNCE_DELAY = 500; -/** - * Focus Mode styling constants - * Centralized to make it easy to adjust appearance - */ export const FOCUS_MODE_STYLES = { backdrop: "fixed inset-0 bg-black/20 backdrop-blur-sm z-40", container: { base: "fixed z-50 w-auto max-w-5xl mx-auto shadow-2xl border-border h-auto overflow-y-auto", - /** - * Responsive spacing using explicit positioning: - * - Mobile (< 640px): 8px margin - * - Tablet (640-768px): 16px margin - * - Desktop (> 768px): 32px margin - */ spacing: "top-2 left-2 right-2 bottom-2 sm:top-4 sm:left-4 sm:right-4 sm:bottom-4 md:top-8 md:left-8 md:right-8 md:bottom-8", }, transition: "transition-all duration-300 ease-in-out", exitButton: "absolute top-2 right-2 z-10 opacity-60 hover:opacity-100", } as const; -/** - * Focus Mode keyboard shortcuts - * - Toggle: Cmd/Ctrl + Shift + F (matches GitHub, Google Docs convention) - * - Exit: Escape key - */ export const FOCUS_MODE_TOGGLE_KEY = "f"; export const FOCUS_MODE_EXIT_KEY = "Escape"; -/** - * Editor height constraints - * - Normal mode: Limited to 50% viewport height to avoid excessive scrolling - * - Focus mode: Minimum 50vh on mobile, 60vh on desktop for immersive writing - */ export const EDITOR_HEIGHT = { normal: "max-h-[50vh]", focusMode: { @@ -50,9 +21,6 @@ export const EDITOR_HEIGHT = { }, } as const; -/** - * Geocoding API configuration - */ export const GEOCODING = { endpoint: "https://nominatim.openstreetmap.org/reverse", userAgent: "Memos/1.0 (https://github.com/usememos/memos)", diff --git a/web/src/components/MemoEditor/hooks/useAbortController.ts b/web/src/components/MemoEditor/hooks/useAbortController.ts index d65b6c84f..4e0b1c886 100644 --- a/web/src/components/MemoEditor/hooks/useAbortController.ts +++ b/web/src/components/MemoEditor/hooks/useAbortController.ts @@ -1,8 +1,5 @@ import { useEffect, useRef } from "react"; -/** - * Hook for managing AbortController lifecycle - */ export function useAbortController() { const controllerRef = useRef(null); diff --git a/web/src/components/MemoEditor/hooks/useBlobUrls.ts b/web/src/components/MemoEditor/hooks/useBlobUrls.ts index 4e0db7fff..72959cc78 100644 --- a/web/src/components/MemoEditor/hooks/useBlobUrls.ts +++ b/web/src/components/MemoEditor/hooks/useBlobUrls.ts @@ -1,8 +1,5 @@ import { useEffect, useRef } from "react"; -/** - * Hook for managing blob URLs lifecycle with automatic cleanup - */ export function useBlobUrls() { const urlsRef = useRef>(new Set()); diff --git a/web/src/components/MemoEditor/hooks/useDragAndDrop.ts b/web/src/components/MemoEditor/hooks/useDragAndDrop.ts index 5397bd1c7..85668c0f1 100644 --- a/web/src/components/MemoEditor/hooks/useDragAndDrop.ts +++ b/web/src/components/MemoEditor/hooks/useDragAndDrop.ts @@ -1,8 +1,5 @@ import { useState } from "react"; -/** - * Hook for handling drag-and-drop file uploads - */ export function useDragAndDrop(onDrop: (files: FileList) => void) { const [isDragging, setIsDragging] = useState(false); diff --git a/web/src/components/MemoEditor/hooks/useFocusMode.ts b/web/src/components/MemoEditor/hooks/useFocusMode.ts index 05ece711c..7a78f6954 100644 --- a/web/src/components/MemoEditor/hooks/useFocusMode.ts +++ b/web/src/components/MemoEditor/hooks/useFocusMode.ts @@ -1,8 +1,5 @@ import { useEffect } from "react"; -/** - * Hook to lock body scroll when focus mode is active - */ export function useFocusMode(isFocusMode: boolean): void { useEffect(() => { document.body.style.overflow = isFocusMode ? "hidden" : ""; diff --git a/web/src/components/MemoEditor/hooks/useLocalFileManager.ts b/web/src/components/MemoEditor/hooks/useLocalFileManager.ts index fc19adf7b..91297f842 100644 --- a/web/src/components/MemoEditor/hooks/useLocalFileManager.ts +++ b/web/src/components/MemoEditor/hooks/useLocalFileManager.ts @@ -2,33 +2,10 @@ import { useState } from "react"; import type { LocalFile } from "@/components/memo-metadata"; import { useBlobUrls } from "./useBlobUrls"; -/** - * Custom hook for managing local file uploads with preview - * Handles file state, blob URL creation, and cleanup - * - * @returns Object with file state and management functions - * - * @example - * ```tsx - * const { localFiles, addFiles, removeFile, clearFiles } = useLocalFileManager(); - * - * // Add files from input or drag-drop - * addFiles(fileList); - * - * // Remove specific file - * removeFile(previewUrl); - * - * // Clear all (e.g., after successful upload) - * clearFiles(); - * ``` - */ export function useLocalFileManager() { const [localFiles, setLocalFiles] = useState([]); const { createBlobUrl, revokeBlobUrl } = useBlobUrls(); - /** - * Adds files to local state with blob URL previews - */ const addFiles = (files: FileList | File[]): void => { const fileArray = Array.from(files); const newLocalFiles: LocalFile[] = fileArray.map((file) => ({ @@ -38,9 +15,6 @@ export function useLocalFileManager() { setLocalFiles((prev) => [...prev, ...newLocalFiles]); }; - /** - * Removes a specific file by preview URL - */ const removeFile = (previewUrl: string): void => { setLocalFiles((prev) => { const toRemove = prev.find((f) => f.previewUrl === previewUrl); @@ -51,9 +25,6 @@ export function useLocalFileManager() { }); }; - /** - * Clears all files and revokes their blob URLs - */ const clearFiles = (): void => { localFiles.forEach(({ previewUrl }) => revokeBlobUrl(previewUrl)); setLocalFiles([]); diff --git a/web/src/components/MemoEditor/hooks/useMemoEditorHandlers.ts b/web/src/components/MemoEditor/hooks/useMemoEditorHandlers.ts index d07a2a7c0..7de9cc657 100644 --- a/web/src/components/MemoEditor/hooks/useMemoEditorHandlers.ts +++ b/web/src/components/MemoEditor/hooks/useMemoEditorHandlers.ts @@ -17,10 +17,6 @@ export interface UseMemoEditorHandlersReturn { handleEditorFocus: () => void; } -/** - * Hook for managing MemoEditor event handlers - * Centralizes composition, paste, and focus handling - */ export const useMemoEditorHandlers = (options: UseMemoEditorHandlersOptions): UseMemoEditorHandlersReturn => { const { editorRef, onFilesAdded, setComposing } = options; diff --git a/web/src/components/MemoEditor/hooks/useMemoEditorInit.ts b/web/src/components/MemoEditor/hooks/useMemoEditorInit.ts index 3f1eae60c..367297de1 100644 --- a/web/src/components/MemoEditor/hooks/useMemoEditorInit.ts +++ b/web/src/components/MemoEditor/hooks/useMemoEditorInit.ts @@ -27,10 +27,6 @@ export interface UseMemoEditorInitReturn { setUpdateTime: (time: Date | undefined) => void; } -/** - * Hook for initializing MemoEditor state - * Handles loading existing memo data and setting initial visibility - */ export const useMemoEditorInit = (options: UseMemoEditorInitOptions): UseMemoEditorInitReturn => { const { editorRef, diff --git a/web/src/components/MemoEditor/hooks/useMemoEditorKeyboard.ts b/web/src/components/MemoEditor/hooks/useMemoEditorKeyboard.ts index 2d346be0f..baad7eb46 100644 --- a/web/src/components/MemoEditor/hooks/useMemoEditorKeyboard.ts +++ b/web/src/components/MemoEditor/hooks/useMemoEditorKeyboard.ts @@ -12,10 +12,6 @@ export interface UseMemoEditorKeyboardOptions { onToggleFocusMode: () => void; } -/** - * Hook for handling keyboard shortcuts in MemoEditor - * Centralizes all keyboard event handling logic - */ export const useMemoEditorKeyboard = (options: UseMemoEditorKeyboardOptions) => { const { editorRef, isFocusMode, isComposing, onSave, onToggleFocusMode } = options; diff --git a/web/src/components/MemoEditor/hooks/useMemoEditorState.ts b/web/src/components/MemoEditor/hooks/useMemoEditorState.ts index e34c4a7b7..1dd267f4f 100644 --- a/web/src/components/MemoEditor/hooks/useMemoEditorState.ts +++ b/web/src/components/MemoEditor/hooks/useMemoEditorState.ts @@ -15,9 +15,6 @@ interface MemoEditorState { isDraggingFile: boolean; } -/** - * Hook for managing MemoEditor state - */ export const useMemoEditorState = (initialVisibility: Visibility = Visibility.PRIVATE) => { const [state, setState] = useState({ memoVisibility: initialVisibility, diff --git a/web/src/components/MemoEditor/hooks/useMemoSave.ts b/web/src/components/MemoEditor/hooks/useMemoSave.ts index 16f3731e3..b3b8ed8c1 100644 --- a/web/src/components/MemoEditor/hooks/useMemoSave.ts +++ b/web/src/components/MemoEditor/hooks/useMemoSave.ts @@ -9,44 +9,26 @@ import type { Location, Memo, MemoRelation, Visibility } from "@/types/proto/api import type { Translations } from "@/utils/i18n"; interface MemoSaveContext { - /** Current memo name (for update mode) */ memoName?: string; - /** Parent memo name (for comment mode) */ parentMemoName?: string; - /** Current visibility setting */ visibility: Visibility; - /** Current attachments */ attachmentList: Attachment[]; - /** Current relations */ relationList: MemoRelation[]; - /** Current location */ location?: Location; - /** Local files pending upload */ localFiles: LocalFile[]; - /** Create time override */ createTime?: Date; - /** Update time override */ updateTime?: Date; } interface MemoSaveCallbacks { - /** Called when upload state changes */ onUploadingChange: (uploading: boolean) => void; - /** Called when request state changes */ onRequestingChange: (requesting: boolean) => void; - /** Called on successful save */ onSuccess: (memoName: string) => void; - /** Called on cancellation (no changes) */ onCancel: () => void; - /** Called to reset after save */ onReset: () => void; - /** Translation function */ t: (key: Translations, params?: Record) => string; } -/** - * Uploads local files and creates attachments - */ async function uploadLocalFiles(localFiles: LocalFile[], onUploadingChange: (uploading: boolean) => void): Promise { if (localFiles.length === 0) return []; @@ -72,9 +54,6 @@ async function uploadLocalFiles(localFiles: LocalFile[], onUploadingChange: (upl } } -/** - * Builds an update mask by comparing memo properties - */ function buildUpdateMask( prevMemo: Memo, content: string, @@ -126,10 +105,6 @@ function buildUpdateMask( return { mask, patch }; } -/** - * Hook for saving/updating memos - * Extracts complex save logic from MemoEditor - */ export function useMemoSave(callbacks: MemoSaveCallbacks) { const { onUploadingChange, onRequestingChange, onSuccess, onCancel, onReset, t } = callbacks; diff --git a/web/src/components/MemoEditor/types/context.ts b/web/src/components/MemoEditor/types/context.ts index e4cec4528..b6e2b4673 100644 --- a/web/src/components/MemoEditor/types/context.ts +++ b/web/src/components/MemoEditor/types/context.ts @@ -3,26 +3,14 @@ import type { Attachment } from "@/types/proto/api/v1/attachment_service"; import type { MemoRelation } from "@/types/proto/api/v1/memo_service"; import type { LocalFile } from "../../memo-metadata"; -/** - * Context interface for MemoEditor - * Provides access to editor state and actions for child components - */ export interface MemoEditorContextValue { - /** List of uploaded attachments */ attachmentList: Attachment[]; - /** List of memo relations/links */ relationList: MemoRelation[]; - /** Update the attachment list */ setAttachmentList: (attachmentList: Attachment[]) => void; - /** Update the relation list */ setRelationList: (relationList: MemoRelation[]) => void; - /** Name of memo being edited (undefined for new memos) */ memoName?: string; - /** Add local files for upload preview */ addLocalFiles?: (files: LocalFile[]) => void; - /** Remove a local file by preview URL */ removeLocalFile?: (previewUrl: string) => void; - /** List of local files pending upload */ localFiles?: LocalFile[]; } diff --git a/web/src/components/MemoExplorer/MemoExplorer.tsx b/web/src/components/MemoExplorer/MemoExplorer.tsx index 19fafedf0..8b0fc9134 100644 --- a/web/src/components/MemoExplorer/MemoExplorer.tsx +++ b/web/src/components/MemoExplorer/MemoExplorer.tsx @@ -10,66 +10,21 @@ import TagsSection from "./TagsSection"; export type MemoExplorerContext = "home" | "explore" | "archived" | "profile"; export interface MemoExplorerFeatures { - /** - * Show search bar at the top - * Default: true - */ search?: boolean; - - /** - * Show statistics section (activity calendar + stat cards) - * Default: true - */ statistics?: boolean; - - /** - * Show shortcuts section (user-defined filter shortcuts) - * Default: true for authenticated users on home/profile, false for explore - */ shortcuts?: boolean; - - /** - * Show tags section - * Default: true - */ tags?: boolean; - - /** - * Context for statistics view (affects which stats to show) - * Default: "user" - */ statisticsContext?: MemoExplorerContext; } interface Props { className?: string; - - /** - * Context for the explorer (determines default features) - */ context?: MemoExplorerContext; - - /** - * Feature configuration (overrides context defaults) - */ features?: MemoExplorerFeatures; - - /** - * Statistics data computed from filtered memos - * Should be computed using useFilteredMemoStats with the same filter as the memo list - */ statisticsData: StatisticsData; - - /** - * Tag counts computed from filtered memos - * Should be computed using useFilteredMemoStats with the same filter as the memo list - */ tagCount: Record; } -/** - * Default features based on context - */ const getDefaultFeatures = (context: MemoExplorerContext): MemoExplorerFeatures => { switch (context) { case "explore": diff --git a/web/src/components/MemoExplorer/MemoExplorerDrawer.tsx b/web/src/components/MemoExplorer/MemoExplorerDrawer.tsx index fb96b6284..1b086fe44 100644 --- a/web/src/components/MemoExplorer/MemoExplorerDrawer.tsx +++ b/web/src/components/MemoExplorer/MemoExplorerDrawer.tsx @@ -7,24 +7,9 @@ import type { StatisticsData } from "@/types/statistics"; import MemoExplorer, { MemoExplorerContext, MemoExplorerFeatures } from "./MemoExplorer"; interface Props { - /** - * Context for the explorer - */ context?: MemoExplorerContext; - - /** - * Feature configuration - */ features?: MemoExplorerFeatures; - - /** - * Statistics data computed from filtered memos - */ statisticsData: StatisticsData; - - /** - * Tag counts computed from filtered memos - */ tagCount: Record; } diff --git a/web/src/components/MemoExplorer/TagsSection.tsx b/web/src/components/MemoExplorer/TagsSection.tsx index 6317f57e0..244be2c30 100644 --- a/web/src/components/MemoExplorer/TagsSection.tsx +++ b/web/src/components/MemoExplorer/TagsSection.tsx @@ -10,10 +10,6 @@ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; interface Props { readonly?: boolean; - /** - * Tag count computed from filtered memos - * Should be provided by parent component using useFilteredMemoStats - */ tagCount: Record; } diff --git a/web/src/components/MemoView/index.ts b/web/src/components/MemoView/index.ts index b6e6a0bf5..b9e832f8c 100644 --- a/web/src/components/MemoView/index.ts +++ b/web/src/components/MemoView/index.ts @@ -1,13 +1,3 @@ -/** - * MemoView component and related exports - * - * This module provides a fully refactored MemoView component with: - * - Separation of concerns via custom hooks - * - Smaller, focused sub-components - * - Proper TypeScript types - * - Better maintainability and testability - */ - export { MemoBody, MemoHeader } from "./components"; export * from "./constants"; export { default, default as MemoView } from "./MemoView"; diff --git a/web/src/components/MemoView/types.ts b/web/src/components/MemoView/types.ts new file mode 100644 index 000000000..6a1852294 --- /dev/null +++ b/web/src/components/MemoView/types.ts @@ -0,0 +1,70 @@ +import type { Memo } from "@/types/proto/api/v1/memo_service"; +import type { User } from "@/types/proto/api/v1/user_service"; + +export interface MemoViewProps { + memo: Memo; + compact?: boolean; + showCreator?: boolean; + showVisibility?: boolean; + showPinned?: boolean; + showNsfwContent?: boolean; + className?: string; + parentPage?: string; +} + +export interface MemoHeaderProps { + // Display options + showCreator?: boolean; + showVisibility?: boolean; + showPinned?: boolean; + // Callbacks + onEdit: () => void; + onGotoDetail: () => void; + onUnpin: () => void; + onToggleNsfwVisibility?: () => void; + // Reaction state + reactionSelectorOpen: boolean; + onReactionSelectorOpenChange: (open: boolean) => void; +} + +export interface MemoBodyProps { + // Display options + compact?: boolean; + // Callbacks + onContentClick: (e: React.MouseEvent) => void; + onContentDoubleClick: (e: React.MouseEvent) => void; + onToggleNsfwVisibility: () => void; +} + +export interface ImagePreviewState { + open: boolean; + urls: string[]; + index: number; +} + +export interface UseMemoActionsReturn { + archiveMemo: () => Promise; + unpinMemo: () => Promise; +} + +export interface UseKeyboardShortcutsOptions { + enabled: boolean; + readonly: boolean; + showEditor: boolean; + isArchived: boolean; + onEdit: () => void; + onArchive: () => Promise; +} + +export interface UseNsfwContentReturn { + nsfw: boolean; + showNSFWContent: boolean; + toggleNsfwVisibility: () => void; +} + +export interface UseImagePreviewReturn { + previewState: ImagePreviewState; + openPreview: (url: string) => void; + closePreview: () => void; + setPreviewOpen: (open: boolean) => void; +} diff --git a/web/src/components/Settings/SettingGroup.tsx b/web/src/components/Settings/SettingGroup.tsx index 7aac58c1c..7629aa3dc 100644 --- a/web/src/components/Settings/SettingGroup.tsx +++ b/web/src/components/Settings/SettingGroup.tsx @@ -10,10 +10,6 @@ interface SettingGroupProps { showSeparator?: boolean; } -/** - * Groups related settings together with optional title and separator - * Use this to organize multiple SettingRows under a common category - */ const SettingGroup: React.FC = ({ title, description, children, className, showSeparator = false }) => { return ( <> diff --git a/web/src/components/Settings/SettingRow.tsx b/web/src/components/Settings/SettingRow.tsx index f71f50c0f..3e3a72c84 100644 --- a/web/src/components/Settings/SettingRow.tsx +++ b/web/src/components/Settings/SettingRow.tsx @@ -12,10 +12,6 @@ interface SettingRowProps { vertical?: boolean; } -/** - * Standardized row component for individual settings - * Provides consistent label/control layout with optional tooltip - */ const SettingRow: React.FC = ({ label, description, tooltip, children, className, vertical = false }) => { return (

diff --git a/web/src/components/Settings/SettingSection.tsx b/web/src/components/Settings/SettingSection.tsx index ec3a35f5a..4b56051c0 100644 --- a/web/src/components/Settings/SettingSection.tsx +++ b/web/src/components/Settings/SettingSection.tsx @@ -9,10 +9,6 @@ interface SettingSectionProps { actions?: React.ReactNode; } -/** - * Wrapper component for consistent section layout in settings pages - * Provides standardized spacing, titles, and descriptions - */ const SettingSection: React.FC = ({ title, description, children, className, actions }) => { return (
diff --git a/web/src/components/Settings/SettingTable.tsx b/web/src/components/Settings/SettingTable.tsx index 26367a466..3fd5e5ecc 100644 --- a/web/src/components/Settings/SettingTable.tsx +++ b/web/src/components/Settings/SettingTable.tsx @@ -16,10 +16,6 @@ interface SettingTableProps { getRowKey?: (row: any, index: number) => string; } -/** - * Standardized table component for settings data lists - * Provides consistent styling for tables in settings pages - */ const SettingTable: React.FC = ({ columns, data, emptyMessage = "No data", className, getRowKey }) => { return (
diff --git a/web/src/components/StatisticsView/StatisticsView.tsx b/web/src/components/StatisticsView/StatisticsView.tsx index 8efcf1f1a..59091107a 100644 --- a/web/src/components/StatisticsView/StatisticsView.tsx +++ b/web/src/components/StatisticsView/StatisticsView.tsx @@ -9,17 +9,9 @@ import { MonthNavigator } from "./MonthNavigator"; export type StatisticsViewContext = "home" | "explore" | "archived" | "profile"; interface Props { - /** - * Context for the statistics view - * Affects which stat cards are shown - * Default: "home" - */ + // Context for the statistics view (affects which stat cards are shown) context?: StatisticsViewContext; - - /** - * Statistics data computed from filtered memos - * Should be provided by parent component using useFilteredMemoStats - */ + // Statistics data computed from filtered memos (use useFilteredMemoStats) statisticsData: StatisticsData; } diff --git a/web/src/components/memo-metadata/AttachmentCard.tsx b/web/src/components/memo-metadata/AttachmentCard.tsx index 37d4e76e6..62a8faf2f 100644 --- a/web/src/components/memo-metadata/AttachmentCard.tsx +++ b/web/src/components/memo-metadata/AttachmentCard.tsx @@ -3,7 +3,6 @@ import { cn } from "@/lib/utils"; import type { AttachmentItem, DisplayMode } from "./types"; interface AttachmentCardProps { - /** Unified attachment item (uploaded or local file) */ item: AttachmentItem; mode: DisplayMode; onRemove?: () => void; @@ -12,10 +11,6 @@ interface AttachmentCardProps { showThumbnail?: boolean; } -/** - * Unified attachment card component for all file types - * Renders differently based on mode (edit/view) and file category - */ const AttachmentCard = ({ item, mode, onRemove, onClick, className, showThumbnail = true }: AttachmentCardProps) => { const { category, filename, thumbnailUrl, sourceUrl } = item; const isMedia = category === "image" || category === "video"; diff --git a/web/src/components/memo-metadata/AttachmentList.tsx b/web/src/components/memo-metadata/AttachmentList.tsx index dfdd7b069..f660dfe6b 100644 --- a/web/src/components/memo-metadata/AttachmentList.tsx +++ b/web/src/components/memo-metadata/AttachmentList.tsx @@ -17,20 +17,6 @@ interface AttachmentListProps extends BaseMetadataProps { onRemoveLocalFile?: (previewUrl: string) => void; } -/** - * Unified AttachmentList component for both editor and view modes - * - * Editor mode: - * - Shows all attachments as sortable badges with thumbnails - * - Supports drag-and-drop reordering - * - Shows remove buttons - * - Shows pending files (not yet uploaded) with preview - * - * View mode: - * - Separates media (images/videos) from other files - * - Shows media in gallery layout with preview - * - Shows other files as clickable cards - */ const AttachmentList = ({ attachments, mode, onAttachmentsChange, localFiles = [], onRemoveLocalFile }: AttachmentListProps) => { const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor)); const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({ diff --git a/web/src/components/memo-metadata/MetadataCard.tsx b/web/src/components/memo-metadata/MetadataCard.tsx index 3c66d2dd3..cd4a63fa5 100644 --- a/web/src/components/memo-metadata/MetadataCard.tsx +++ b/web/src/components/memo-metadata/MetadataCard.tsx @@ -6,10 +6,6 @@ interface MetadataCardProps { className?: string; } -/** - * Shared card component for structured metadata (Relations, Comments, etc.) - * Provides consistent card styling across editor and view modes - */ const MetadataCard = ({ children, className }: MetadataCardProps) => { return (
{ const memoId = extractMemoIdFromName(memo.name); diff --git a/web/src/components/memo-metadata/RelationList.tsx b/web/src/components/memo-metadata/RelationList.tsx index 1c75a6d05..2ebb1738e 100644 --- a/web/src/components/memo-metadata/RelationList.tsx +++ b/web/src/components/memo-metadata/RelationList.tsx @@ -16,20 +16,6 @@ interface RelationListProps extends BaseMetadataProps { parentPage?: string; } -/** - * Unified RelationList component for both editor and view modes - * - * Editor mode: - * - Shows only outgoing relations (referencing) - * - Badge-style display with remove buttons - * - Compact inline layout - * - * View mode: - * - Shows bidirectional relations in tabbed card - * - "Referencing" tab: Memos this memo links to - * - "Referenced by" tab: Memos that link to this memo - * - Navigable links with memo IDs - */ const RelationList = observer(({ relations, currentMemoName, mode, onRelationsChange, parentPage, className }: RelationListProps) => { const t = useTranslate(); const [referencingMemos, setReferencingMemos] = useState([]); diff --git a/web/src/components/memo-metadata/index.ts b/web/src/components/memo-metadata/index.ts index 1f30cbf1d..525d4411f 100644 --- a/web/src/components/memo-metadata/index.ts +++ b/web/src/components/memo-metadata/index.ts @@ -1,8 +1,3 @@ -/** - * Unified memo metadata components - * Provides consistent styling and behavior across editor and view modes - */ - export { default as AttachmentCard } from "./AttachmentCard"; export { default as AttachmentList } from "./AttachmentList"; export { default as LocationDisplay } from "./LocationDisplay"; diff --git a/web/src/components/memo-metadata/types.ts b/web/src/components/memo-metadata/types.ts index 92ae12395..67f66fd90 100644 --- a/web/src/components/memo-metadata/types.ts +++ b/web/src/components/memo-metadata/types.ts @@ -1,7 +1,3 @@ -/** - * Common types for memo metadata components - */ - import type { Attachment } from "@/types/proto/api/v1/attachment_service"; import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; @@ -12,47 +8,26 @@ export interface BaseMetadataProps { className?: string; } -/** - * File type categories for consistent handling across components - */ export type FileCategory = "image" | "video" | "document"; -/** - * Pure view model for rendering attachments and local files - * Contains only presentation data needed by UI components - * Does not store references to original domain objects for cleaner architecture - */ +// Pure view model for rendering attachments and local files export interface AttachmentItem { - /** Unique identifier - stable across renders */ readonly id: string; - /** Display name for the file */ readonly filename: string; - /** Categorized file type */ readonly category: FileCategory; - /** MIME type for detailed handling if needed */ readonly mimeType: string; - /** URL for thumbnail/preview display */ readonly thumbnailUrl: string; - /** URL for full file access */ readonly sourceUrl: string; - /** Size in bytes (optional) */ readonly size?: number; - /** Whether this represents a local file not yet uploaded */ readonly isLocal: boolean; } -/** - * Determine file category from MIME type - */ function categorizeFile(mimeType: string): FileCategory { if (mimeType.startsWith("image/")) return "image"; if (mimeType.startsWith("video/")) return "video"; return "document"; } -/** - * Convert an uploaded Attachment to AttachmentItem view model - */ export function attachmentToItem(attachment: Attachment): AttachmentItem { const attachmentType = getAttachmentType(attachment); const sourceUrl = getAttachmentUrl(attachment); @@ -69,9 +44,6 @@ export function attachmentToItem(attachment: Attachment): AttachmentItem { }; } -/** - * Convert a local File with blob URL to AttachmentItem view model - */ export function fileToItem(file: File, blobUrl: string): AttachmentItem { return { id: blobUrl, // Use blob URL as unique ID since we don't have a server ID yet @@ -85,34 +57,20 @@ export function fileToItem(file: File, blobUrl: string): AttachmentItem { }; } -/** - * Simple container for local files with their blob URLs - * Kept minimal to avoid unnecessary abstraction - */ export interface LocalFile { readonly file: File; readonly previewUrl: string; } -/** - * Batch convert attachments and local files to AttachmentItems - * Returns items in order: uploaded first, then local - */ export function toAttachmentItems(attachments: Attachment[], localFiles: LocalFile[] = []): AttachmentItem[] { return [...attachments.map(attachmentToItem), ...localFiles.map(({ file, previewUrl }) => fileToItem(file, previewUrl))]; } -/** - * Filter items by category for specialized rendering - */ export function filterByCategory(items: AttachmentItem[], categories: FileCategory[]): AttachmentItem[] { const categorySet = new Set(categories); return items.filter((item) => categorySet.has(item.category)); } -/** - * Separate items into media (image/video) and documents - */ export function separateMediaAndDocs(items: AttachmentItem[]): { media: AttachmentItem[]; docs: AttachmentItem[] } { const media: AttachmentItem[] = []; const docs: AttachmentItem[] = []; diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx index 9dd549b16..9181909e6 100644 --- a/web/src/components/ui/dialog.tsx +++ b/web/src/components/ui/dialog.tsx @@ -28,22 +28,6 @@ const DialogOverlay = React.forwardRef< )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; -/** - * Dialog content variants with improved mobile responsiveness. - * - * Mobile behavior: - * - Mobile phones (< 640px): Uses calc(100% - 2rem) width with better 1rem margin on each side - * - Small tablets (≥ 640px): Uses calc(100% - 3rem) width with 1.5rem margin on each side - * - Medium screens and up (≥ 768px): Uses fixed max-widths based on size variant - * - * Size variants: - * - sm: max-w-sm (384px) for compact dialogs - * - default: max-w-md (448px) for standard dialogs - * - lg: max-w-lg (512px) for larger forms - * - xl: max-w-xl (576px) for detailed content - * - 2xl: max-w-2xl (672px) for wide layouts - * - full: Takes available width with margins - */ const dialogContentVariants = cva( "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-2rem)] sm:max-h-[calc(100vh-3rem)] md:max-h-[calc(100vh-4rem)]", { diff --git a/web/src/hooks/useDialog.ts b/web/src/hooks/useDialog.ts index 84010b1f8..f2c12d90d 100644 --- a/web/src/hooks/useDialog.ts +++ b/web/src/hooks/useDialog.ts @@ -1,24 +1,5 @@ import { useCallback, useState } from "react"; -/** - * Hook for managing dialog state with a clean API - * - * @returns Object with dialog state and handlers - * - * @example - * const dialog = useDialog(); - * - * return ( - * <> - * - * - * - * ); - */ export function useDialog(defaultOpen = false) { const [isOpen, setIsOpen] = useState(defaultOpen); @@ -35,30 +16,6 @@ export function useDialog(defaultOpen = false) { }; } -/** - * Hook for managing multiple dialogs with named keys - * - * @returns Object with dialog management functions - * - * @example - * const dialogs = useDialogs(); - * - * return ( - * <> - * - * - * - * dialogs.setOpen('create', open)} - * /> - * dialogs.setOpen('edit', open)} - * /> - * - * ); - */ export function useDialogs() { const [openDialogs, setOpenDialogs] = useState>(new Set()); diff --git a/web/src/hooks/useFilteredMemoStats.ts b/web/src/hooks/useFilteredMemoStats.ts index cf0abe990..a1272fca2 100644 --- a/web/src/hooks/useFilteredMemoStats.ts +++ b/web/src/hooks/useFilteredMemoStats.ts @@ -10,50 +10,14 @@ export interface FilteredMemoStats { loading: boolean; } -/** - * Convert user name to user stats key. - * Backend returns UserStats with name "users/{id}/stats" but we pass "users/{id}" - * @param userName - User name in format "users/{id}" - * @returns Stats key in format "users/{id}/stats" - */ const getUserStatsKey = (userName: string): string => { return `${userName}/stats`; }; export interface UseFilteredMemoStatsOptions { - /** - * User name to fetch stats for (e.g., "users/123") - * - * When provided: - * - Fetches backend user stats via GetUserStats API - * - Returns unfiltered tags and activity (all NORMAL memos for that user) - * - Tags remain stable even when memo filters are applied - * - * When undefined: - * - Computes stats from cached memos in the store - * - Reflects current filters (useful for Explore/Archived pages) - * - * IMPORTANT: Backend user stats only include NORMAL (non-archived) memos. - * Do NOT use for Archived page context. - */ userName?: string; } -/** - * Hook to compute statistics and tags for the sidebar. - * - * Data sources by context: - * - **Home/Profile**: Uses backend UserStats API (unfiltered, normal memos only) - * - **Archived/Explore**: Computes from cached memos (filtered by page context) - * - * Benefits of using backend stats: - * - Tag list remains stable when memo filters are applied - * - Activity calendar shows full history, not just filtered results - * - Prevents "disappearing tags" issue when filtering by tag - * - * @param options - Configuration options - * @returns Object with statistics data, tag counts, and loading state - */ export const useFilteredMemoStats = (options: UseFilteredMemoStatsOptions = {}): FilteredMemoStats => { const { userName } = options; const [data, setData] = useState({ diff --git a/web/src/hooks/useMemoFilters.ts b/web/src/hooks/useMemoFilters.ts index 2f459dd85..434d4372a 100644 --- a/web/src/hooks/useMemoFilters.ts +++ b/web/src/hooks/useMemoFilters.ts @@ -5,81 +5,18 @@ import memoFilterStore from "@/store/memoFilter"; import { InstanceSetting_Key } from "@/types/proto/api/v1/instance_service"; import { Visibility } from "@/types/proto/api/v1/memo_service"; -// Helper function to extract shortcut ID from resource name -// Format: users/{user}/shortcuts/{shortcut} const getShortcutId = (name: string): string => { const parts = name.split("/"); return parts.length === 4 ? parts[3] : ""; }; export interface UseMemoFiltersOptions { - /** - * User name to scope memos to (e.g., "users/123") - * If undefined, no creator filter is applied (useful for Explore page) - */ creatorName?: string; - - /** - * Whether to include shortcut filter from memoFilterStore - * Default: false - */ includeShortcuts?: boolean; - - /** - * Whether to include pinned filter from memoFilterStore - * Default: false - */ includePinned?: boolean; - - /** - * Visibility levels to filter by (for Explore page) - * If provided, adds visibility filter to show only specified visibility levels - * Default: undefined (no visibility filter) - * - * **Security Note**: This filter is enforced at the API level. The backend is responsible - * for respecting visibility permissions when: - * - Returning memo lists (filtered by this parameter) - * - Calculating statistics (should only count visible memos) - * - Aggregating tags (should only include tags from visible memos) - * - * This ensures that private memo data never leaks to unauthorized users through - * stats, tags, or direct memo access. - * - * @example - * // For logged-in users on Explore - * visibilities: [Visibility.PUBLIC, Visibility.PROTECTED] - * - * @example - * // For visitors on Explore - * visibilities: [Visibility.PUBLIC] - */ visibilities?: Visibility[]; } -/** - * Hook to build memo filter string based on active filters and options. - * - * This hook consolidates filter building logic that was previously duplicated - * across Home, Explore, Archived, and UserProfile pages. - * - * @param options - Configuration for filter building - * @returns Filter string to pass to API, or undefined if no filters - * - * @example - * // Home page - include everything - * const filter = useMemoFilters({ - * creatorName: user.name, - * includeShortcuts: true, - * includePinned: true - * }); - * - * @example - * // Explore page - no creator scoping - * const filter = useMemoFilters({ - * includeShortcuts: false, - * includePinned: false - * }); - */ export const useMemoFilters = (options: UseMemoFiltersOptions = {}): string | undefined => { const { creatorName, includeShortcuts = false, includePinned = false, visibilities } = options; diff --git a/web/src/hooks/useMemoSorting.ts b/web/src/hooks/useMemoSorting.ts index 842ed1981..1b2aa70c1 100644 --- a/web/src/hooks/useMemoSorting.ts +++ b/web/src/hooks/useMemoSorting.ts @@ -5,54 +5,15 @@ import { State } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; export interface UseMemoSortingOptions { - /** - * Whether to sort pinned memos first - * Default: false - */ pinnedFirst?: boolean; - - /** - * State to filter memos by (NORMAL, ARCHIVED, etc.) - * Default: State.NORMAL - */ state?: State; } export interface UseMemoSortingResult { - /** - * Sort function to pass to PagedMemoList's listSort prop - */ listSort: (memos: Memo[]) => Memo[]; - - /** - * Order by string to pass to PagedMemoList's orderBy prop - */ orderBy: string; } -/** - * Hook to generate memo sorting logic based on options. - * - * This hook consolidates sorting logic that was previously duplicated - * across Home, Explore, Archived, and UserProfile pages. - * - * @param options - Configuration for sorting - * @returns Object with listSort function and orderBy string - * - * @example - * // Home page - pinned first, then by time - * const { listSort, orderBy } = useMemoSorting({ - * pinnedFirst: true, - * state: State.NORMAL - * }); - * - * @example - * // Explore page - only by time - * const { listSort, orderBy } = useMemoSorting({ - * pinnedFirst: false, - * state: State.NORMAL - * }); - */ export const useMemoSorting = (options: UseMemoSortingOptions = {}): UseMemoSortingResult => { const { pinnedFirst = false, state = State.NORMAL } = options; diff --git a/web/src/pages/Attachments.tsx b/web/src/pages/Attachments.tsx index f8b818b38..6dc806a47 100644 --- a/web/src/pages/Attachments.tsx +++ b/web/src/pages/Attachments.tsx @@ -22,9 +22,6 @@ import { useTranslate } from "@/utils/i18n"; const PAGE_SIZE = 50; -/** - * Groups attachments by month for organized display - */ const groupAttachmentsByDate = (attachments: Attachment[]): Map => { const grouped = new Map(); const sorted = [...attachments].sort((a, b) => dayjs(b.createTime).unix() - dayjs(a.createTime).unix()); @@ -39,18 +36,12 @@ const groupAttachmentsByDate = (attachments: Attachment[]): Map { if (!searchQuery.trim()) return attachments; const query = searchQuery.toLowerCase(); return attachments.filter((attachment) => attachment.filename.toLowerCase().includes(query)); }; -/** - * Individual attachment item component - */ interface AttachmentItemProps { attachment: Attachment; } diff --git a/web/src/store/attachment.ts b/web/src/store/attachment.ts index a118f0878..2d53d73cb 100644 --- a/web/src/store/attachment.ts +++ b/web/src/store/attachment.ts @@ -1,23 +1,12 @@ -/** - * Attachment Store - * - * Manages file attachment state including uploads and metadata. - * This is a server state store that fetches and caches attachment data. - */ +// Attachment Store - manages file attachment state including uploads and metadata import { computed, makeObservable, observable } from "mobx"; import { attachmentServiceClient } from "@/grpcweb"; import { Attachment, CreateAttachmentRequest, UpdateAttachmentRequest } from "@/types/proto/api/v1/attachment_service"; import { createServerStore, StandardState } from "./base-store"; import { createRequestKey } from "./store-utils"; -/** - * Attachment store state - * Uses a name-based map for efficient lookups - */ class AttachmentState extends StandardState { - /** - * Map of attachments indexed by resource name (e.g., "attachments/123") - */ + // Map of attachments indexed by resource name (e.g., "attachments/123") attachmentMapByName: Record = {}; constructor() { @@ -29,24 +18,15 @@ class AttachmentState extends StandardState { }); } - /** - * Computed getter for all attachments as an array - */ get attachments(): Attachment[] { return Object.values(this.attachmentMapByName); } - /** - * Get attachment count - */ get size(): number { return Object.keys(this.attachmentMapByName).length; } } -/** - * Attachment store instance - */ const attachmentStore = (() => { const base = createServerStore(new AttachmentState(), { name: "attachment", @@ -55,13 +35,6 @@ const attachmentStore = (() => { const { state, executeRequest } = base; - /** - * Fetch attachment by resource name - * Results are cached in the store - * - * @param name - Resource name (e.g., "attachments/123") - * @returns The attachment object - */ const fetchAttachmentByName = async (name: string): Promise => { const requestKey = createRequestKey("fetchAttachment", { name }); @@ -84,24 +57,10 @@ const attachmentStore = (() => { ); }; - /** - * Get attachment from cache by resource name - * Does not trigger a fetch if not found - * - * @param name - Resource name - * @returns The cached attachment or undefined - */ const getAttachmentByName = (name: string): Attachment | undefined => { return state.attachmentMapByName[name]; }; - /** - * Get or fetch attachment by name - * Checks cache first, fetches if not found - * - * @param name - Resource name - * @returns The attachment object - */ const getOrFetchAttachmentByName = async (name: string): Promise => { const cached = getAttachmentByName(name); if (cached) { @@ -110,12 +69,6 @@ const attachmentStore = (() => { return fetchAttachmentByName(name); }; - /** - * Create a new attachment - * - * @param request - Attachment creation request - * @returns The created attachment - */ const createAttachment = async (request: CreateAttachmentRequest): Promise => { return executeRequest( "", // No deduplication for creates @@ -136,12 +89,6 @@ const attachmentStore = (() => { ); }; - /** - * Update an existing attachment - * - * @param request - Attachment update request - * @returns The updated attachment - */ const updateAttachment = async (request: UpdateAttachmentRequest): Promise => { return executeRequest( "", // No deduplication for updates @@ -162,11 +109,6 @@ const attachmentStore = (() => { ); }; - /** - * Delete an attachment - * - * @param name - Resource name of the attachment to delete - */ const deleteAttachment = async (name: string): Promise => { return executeRequest( "", // No deduplication for deletes @@ -182,9 +124,6 @@ const attachmentStore = (() => { ); }; - /** - * Clear all cached attachments - */ const clearCache = (): void => { state.setPartial({ attachmentMapByName: {} }); }; diff --git a/web/src/store/base-store.ts b/web/src/store/base-store.ts index ceb984f1c..20da1cace 100644 --- a/web/src/store/base-store.ts +++ b/web/src/store/base-store.ts @@ -1,56 +1,18 @@ -/** - * Base store classes and utilities for consistent store patterns - * - * This module provides: - * - BaseServerStore: For stores that fetch data from APIs - * - BaseClientStore: For stores that manage UI/client state - * - Common patterns for all stores - */ +// Base store classes and utilities for consistent store patterns +// - BaseServerStore: For stores that fetch data from APIs +// - BaseClientStore: For stores that manage UI/client state import { action, makeObservable } from "mobx"; import { RequestDeduplicator, StoreError } from "./store-utils"; -/** - * Base interface for all store states - * Ensures all stores have a consistent setPartial method - */ export interface BaseState { setPartial(partial: Partial): void; } -/** - * Base class for server state stores (data fetching) - * - * Server stores: - * - Fetch data from APIs - * - Cache responses in memory - * - Handle errors with StoreError - * - Support request deduplication - * - * @example - * class MemoState implements BaseState { - * memoMapByName: Record = {}; - * constructor() { makeAutoObservable(this); } - * setPartial(partial: Partial) { Object.assign(this, partial); } - * } - * - * const store = createServerStore(new MemoState()); - */ export interface ServerStoreConfig { - /** - * Enable request deduplication - * Prevents multiple identical requests from running simultaneously - */ enableDeduplication?: boolean; - - /** - * Store name for debugging and error messages - */ name: string; } -/** - * Create a server store with built-in utilities - */ export function createServerStore(state: TState, config: ServerStoreConfig) { const deduplicator = config.enableDeduplication !== false ? new RequestDeduplicator() : null; @@ -59,9 +21,6 @@ export function createServerStore(state: TState, confi deduplicator, name: config.name, - /** - * Wrap an async operation with error handling and optional deduplication - */ async executeRequest(key: string, operation: () => Promise, errorCode?: string): Promise { try { if (deduplicator && key) { @@ -70,7 +29,7 @@ export function createServerStore(state: TState, confi return await operation(); } catch (error) { if (StoreError.isAbortError(error)) { - throw error; // Re-throw abort errors as-is + throw error; } throw StoreError.wrap(errorCode || `${config.name.toUpperCase()}_OPERATION_FAILED`, error); } @@ -78,35 +37,8 @@ export function createServerStore(state: TState, confi }; } -/** - * Base class for client state stores (UI state) - * - * Client stores: - * - Manage UI preferences and transient state - * - May persist to localStorage or URL - * - No API calls - * - Instant updates - * - * @example - * class ViewState implements BaseState { - * orderByTimeAsc = false; - * layout: "LIST" | "MASONRY" = "LIST"; - * constructor() { makeAutoObservable(this); } - * setPartial(partial: Partial) { - * Object.assign(this, partial); - * localStorage.setItem("view", JSON.stringify(this)); - * } - * } - */ export interface ClientStoreConfig { - /** - * Store name for debugging - */ name: string; - - /** - * Enable localStorage persistence - */ persistence?: { key: string; serialize?: (state: any) => string; @@ -114,9 +46,6 @@ export interface ClientStoreConfig { }; } -/** - * Create a client store with optional persistence - */ export function createClientStore(state: TState, config: ClientStoreConfig) { // Load from localStorage if enabled if (config.persistence) { @@ -135,9 +64,6 @@ export function createClientStore(state: TState, confi state, name: config.name, - /** - * Save state to localStorage if persistence is enabled - */ persist(): void { if (config.persistence) { try { @@ -149,9 +75,6 @@ export function createClientStore(state: TState, confi } }, - /** - * Clear persisted state - */ clearPersistence(): void { if (config.persistence) { localStorage.removeItem(config.persistence.key); @@ -160,10 +83,6 @@ export function createClientStore(state: TState, confi }; } -/** - * Standard state class implementation - * Use this as a base for your state classes - */ export abstract class StandardState implements BaseState { constructor() { makeObservable(this, { diff --git a/web/src/store/config.ts b/web/src/store/config.ts index 6d8f705f8..7ea3baa13 100644 --- a/web/src/store/config.ts +++ b/web/src/store/config.ts @@ -1,72 +1,32 @@ -/** - * MobX configuration for strict state management - * - * This configuration enforces best practices to prevent common mistakes: - * - All state changes must happen in actions (prevents accidental mutations) - * - Computed values cannot have side effects (ensures purity) - * - Observables must be accessed within reactions (helps catch missing observers) - * - * This file is imported early in the application lifecycle to configure MobX - * before any stores are created. - */ +// MobX configuration for strict state management +// Enforces best practices: state changes must happen in actions, computed values cannot have side effects import { configure } from "mobx"; -/** - * Configure MobX with production-safe settings - * This runs immediately when the module is imported - */ configure({ - /** - * Enforce that all state mutations happen within actions - * Since we use makeAutoObservable, all methods are automatically actions - * This prevents bugs from direct mutations like: - * store.state.value = 5 // ERROR: This will throw - * - * Instead, you must use action methods: - * store.state.setPartial({ value: 5 }) // Correct - */ - enforceActions: "never", // Start with "never", can be upgraded to "observed" or "always" - - /** - * Use Proxies for better performance and ES6 compatibility - * makeAutoObservable requires this to be enabled - */ + // Enforce that all state mutations happen within actions (start permissive, can upgrade later) + enforceActions: "never", + // Use Proxies for better performance and ES6 compatibility (required for makeAutoObservable) useProxies: "always", - - /** - * Isolate global state to prevent accidental sharing between tests - */ + // Isolate global state to prevent accidental sharing between tests isolateGlobalState: true, - - /** - * Disable error boundaries so errors propagate normally - * This ensures React error boundaries can catch store errors - */ + // Disable error boundaries so errors propagate normally disableErrorBoundaries: false, }); -/** - * Enable strict mode for development - * Call this in main.tsx if you want stricter checking - */ export function enableStrictMode() { if (import.meta.env.DEV) { configure({ - enforceActions: "observed", // Enforce actions only for observed values - computedRequiresReaction: false, // Don't warn about computed access - reactionRequiresObservable: false, // Don't warn about reactions + enforceActions: "observed", + computedRequiresReaction: false, + reactionRequiresObservable: false, }); console.info("✓ MobX strict mode enabled"); } } -/** - * Enable production mode for maximum performance - * This is automatically called in production builds - */ export function enableProductionMode() { configure({ - enforceActions: "never", // No runtime checks for performance + enforceActions: "never", disableErrorBoundaries: false, }); } diff --git a/web/src/store/index.ts b/web/src/store/index.ts index a9c5d5e0b..03ef04013 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -1,50 +1,6 @@ -/** - * Store Module - * - * This module exports all application stores and their types. - * - * ## Store Architecture - * - * Stores are divided into two categories: - * - * ### Server State Stores (Data Fetching) - * These stores fetch and cache data from the backend API: - * - **memoStore**: Memo CRUD operations - * - **userStore**: User authentication and settings - * - **instanceStore**: Instance configuration - * - **attachmentStore**: File attachment management - * - * Features: - * - Request deduplication - * - Error handling with StoreError - * - Optimistic updates (memo updates) - * - Computed property memoization - * - * ### Client State Stores (UI State) - * These stores manage UI preferences and transient state: - * - **viewStore**: Display preferences (sort order, layout) - * - **memoFilterStore**: Active search filters - * - * Features: - * - localStorage persistence (viewStore) - * - URL synchronization (memoFilterStore) - * - No API calls - * - * ## Usage - * - * ```typescript - * import { memoStore, userStore, viewStore } from "@/store"; - * import { observer } from "mobx-react-lite"; - * - * const MyComponent = observer(() => { - * const memos = memoStore.state.memos; - * const user = userStore.state.currentUser; - * - * return
...
; - * }); - * ``` - */ -// Server State Stores +// Store Module - exports all application stores and their types +// Server State Stores (fetch/cache backend data): memoStore, userStore, instanceStore, attachmentStore +// Client State Stores (UI preferences): viewStore, memoFilterStore import attachmentStore from "./attachment"; import instanceStore from "./instance"; import memoStore from "./memo"; @@ -89,9 +45,6 @@ export { viewStore, }; -/** - * All stores grouped by category for convenience - */ export const stores = { // Server state server: { diff --git a/web/src/store/instance.ts b/web/src/store/instance.ts index 11636250b..19f896832 100644 --- a/web/src/store/instance.ts +++ b/web/src/store/instance.ts @@ -1,9 +1,4 @@ -/** - * Instance Store - * - * Manages instance-level configuration and settings. - * This is a server state store that fetches instance profile and settings. - */ +// Instance Store - manages instance-level configuration and settings import { uniqBy } from "lodash-es"; import { computed } from "mobx"; import { instanceServiceClient } from "@/grpcweb"; @@ -19,48 +14,20 @@ import { createServerStore, StandardState } from "./base-store"; import { instanceSettingNamePrefix } from "./common"; import { createRequestKey } from "./store-utils"; -/** - * Valid theme options - */ const VALID_THEMES = ["system", "default", "default-dark", "midnight", "paper", "whitewall"] as const; export type Theme = (typeof VALID_THEMES)[number]; -/** - * Check if a string is a valid theme - */ export function isValidTheme(theme: string): theme is Theme { return VALID_THEMES.includes(theme as Theme); } -/** - * Instance store state - */ class InstanceState extends StandardState { - /** - * Current locale (e.g., "en", "zh", "ja") - */ locale: string = "en"; - - /** - * Current theme - * Note: Accepts string for flexibility, but validates to Theme - */ theme: Theme | string = "system"; - - /** - * Instance profile containing owner and metadata - */ profile: InstanceProfile = InstanceProfile.fromPartial({}); - - /** - * Array of instance settings - */ settings: InstanceSetting[] = []; - /** - * Computed property for general settings - * Memoized for performance - */ + // Computed property for general settings (memoized) get generalSetting(): InstanceSetting_GeneralSetting { return computed(() => { const setting = this.settings.find((s) => s.name === `${instanceSettingNamePrefix}${InstanceSetting_Key.GENERAL}`); @@ -68,10 +35,7 @@ class InstanceState extends StandardState { }).get(); } - /** - * Computed property for memo-related settings - * Memoized for performance - */ + // Computed property for memo-related settings (memoized) get memoRelatedSetting(): InstanceSetting_MemoRelatedSetting { return computed(() => { const setting = this.settings.find((s) => s.name === `${instanceSettingNamePrefix}${InstanceSetting_Key.MEMO_RELATED}`); @@ -79,9 +43,6 @@ class InstanceState extends StandardState { }).get(); } - /** - * Override setPartial to validate locale and theme - */ setPartial(partial: Partial): void { const finalState = { ...this, ...partial }; @@ -106,9 +67,6 @@ class InstanceState extends StandardState { } } -/** - * Instance store instance - */ const instanceStore = (() => { const base = createServerStore(new InstanceState(), { name: "instance", @@ -117,11 +75,6 @@ const instanceStore = (() => { const { state, executeRequest } = base; - /** - * Fetch a specific instance setting by key - * - * @param settingKey - The setting key to fetch - */ const fetchInstanceSetting = async (settingKey: InstanceSetting_Key): Promise => { const requestKey = createRequestKey("fetchInstanceSetting", { key: settingKey }); @@ -141,11 +94,6 @@ const instanceStore = (() => { ); }; - /** - * Update or create an instance setting - * - * @param setting - The setting to upsert - */ const upsertInstanceSetting = async (setting: InstanceSetting): Promise => { return executeRequest( "", // No deduplication for updates @@ -161,24 +109,11 @@ const instanceStore = (() => { ); }; - /** - * Get an instance setting from cache by key - * Does not trigger a fetch - * - * @param settingKey - The setting key - * @returns The cached setting or an empty setting - */ const getInstanceSettingByKey = (settingKey: InstanceSetting_Key): InstanceSetting => { const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}${settingKey}`); return setting || InstanceSetting.fromPartial({}); }; - /** - * Set the instance theme - * Updates both local state and persists to server - * - * @param theme - The theme to set - */ const setTheme = async (theme: string): Promise => { // Validate theme if (!isValidTheme(theme)) { @@ -206,9 +141,6 @@ const instanceStore = (() => { ); }; - /** - * Fetch instance profile - */ const fetchInstanceProfile = async (): Promise => { const requestKey = createRequestKey("fetchInstanceProfile"); @@ -233,12 +165,7 @@ const instanceStore = (() => { }; })(); -/** - * Initialize the instance store - * Called once at app startup to load instance profile and settings - * - * @throws Never - errors are logged but not thrown - */ +// Initialize the instance store - called once at app startup export const initialInstanceStore = async (): Promise => { try { // Fetch instance profile diff --git a/web/src/store/memoFilter.ts b/web/src/store/memoFilter.ts index 1720599fc..eb860d898 100644 --- a/web/src/store/memoFilter.ts +++ b/web/src/store/memoFilter.ts @@ -1,49 +1,26 @@ -/** - * Memo Filter Store - * - * Manages active memo filters and search state. - * This is a client state store that syncs with URL query parameters. - * - * Filters are URL-driven and shareable - copying the URL preserves the filter state. - */ +// Memo Filter Store - manages active memo filters and search state +// This is a client state store that syncs with URL query parameters import { uniqBy } from "lodash-es"; import { action, computed, makeObservable, observable } from "mobx"; import { StandardState } from "./base-store"; -/** - * Filter factor types - * Defines what aspect of a memo to filter by - */ export type FilterFactor = - | "tagSearch" // Filter by tag name - | "visibility" // Filter by visibility (public/private) - | "contentSearch" // Search in memo content - | "displayTime" // Filter by date - | "pinned" // Show only pinned memos - | "property.hasLink" // Memos containing links - | "property.hasTaskList" // Memos with task lists - | "property.hasCode"; // Memos with code blocks + | "tagSearch" + | "visibility" + | "contentSearch" + | "displayTime" + | "pinned" + | "property.hasLink" + | "property.hasTaskList" + | "property.hasCode"; -/** - * Memo filter object - */ export interface MemoFilter { factor: FilterFactor; value: string; } -/** - * Generate a unique key for a filter - * Used for deduplication - */ export const getMemoFilterKey = (filter: MemoFilter): string => `${filter.factor}:${filter.value}`; -/** - * Parse filter query string from URL into filter objects - * - * @param query - URL query string (e.g., "tagSearch:work,pinned:true") - * @returns Array of filter objects - */ export const parseFilterQuery = (query: string | null): MemoFilter[] => { if (!query) return []; @@ -61,34 +38,14 @@ export const parseFilterQuery = (query: string | null): MemoFilter[] => { } }; -/** - * Convert filter objects into URL query string - * - * @param filters - Array of filter objects - * @returns URL-encoded query string - */ export const stringifyFilters = (filters: MemoFilter[]): string => { return filters.map((filter) => `${filter.factor}:${encodeURIComponent(filter.value)}`).join(","); }; -/** - * Memo filter store state - */ class MemoFilterState extends StandardState { - /** - * Active filters - */ filters: MemoFilter[] = []; - - /** - * Currently selected shortcut ID - * Shortcuts are predefined filter combinations - */ shortcut?: string = undefined; - /** - * Initialize from URL on construction - */ constructor() { super(); makeObservable(this, { @@ -104,9 +61,6 @@ class MemoFilterState extends StandardState { this.initFromURL(); } - /** - * Load filters from current URL query parameters - */ private initFromURL(): void { try { const searchParams = new URLSearchParams(window.location.search); @@ -117,144 +71,60 @@ class MemoFilterState extends StandardState { } } - /** - * Get all filters for a specific factor - * - * @param factor - The filter factor to query - * @returns Array of matching filters - */ getFiltersByFactor(factor: FilterFactor): MemoFilter[] { return this.filters.filter((f) => f.factor === factor); } - /** - * Add a filter (deduplicates automatically) - * - * @param filter - The filter to add - */ addFilter(filter: MemoFilter): void { this.filters = uniqBy([...this.filters, filter], getMemoFilterKey); } - /** - * Remove filters matching the predicate - * - * @param predicate - Function that returns true for filters to remove - */ removeFilter(predicate: (f: MemoFilter) => boolean): void { this.filters = this.filters.filter((f) => !predicate(f)); } - /** - * Remove all filters for a specific factor - * - * @param factor - The filter factor to remove - */ removeFiltersByFactor(factor: FilterFactor): void { this.filters = this.filters.filter((f) => f.factor !== factor); } - /** - * Clear all filters - */ clearAllFilters(): void { this.filters = []; this.shortcut = undefined; } - /** - * Set the current shortcut - * - * @param shortcut - Shortcut ID or undefined to clear - */ setShortcut(shortcut?: string): void { this.shortcut = shortcut; } - /** - * Check if a specific filter is active - * - * @param filter - The filter to check - * @returns True if the filter is active - */ hasFilter(filter: MemoFilter): boolean { return this.filters.some((f) => getMemoFilterKey(f) === getMemoFilterKey(filter)); } - /** - * Check if any filters are active - */ get hasActiveFilters(): boolean { return this.filters.length > 0 || this.shortcut !== undefined; } } -/** - * Memo filter store instance - */ const memoFilterStore = (() => { const state = new MemoFilterState(); return { - /** - * Direct access to state for observers - */ state, - - /** - * Get all active filters - */ get filters(): MemoFilter[] { return state.filters; }, - - /** - * Get current shortcut ID - */ get shortcut(): string | undefined { return state.shortcut; }, - - /** - * Check if any filters are active - */ get hasActiveFilters(): boolean { return state.hasActiveFilters; }, - - /** - * Get filters by factor - */ getFiltersByFactor: (factor: FilterFactor): MemoFilter[] => state.getFiltersByFactor(factor), - - /** - * Add a filter - */ addFilter: (filter: MemoFilter): void => state.addFilter(filter), - - /** - * Remove filters matching predicate - */ removeFilter: (predicate: (f: MemoFilter) => boolean): void => state.removeFilter(predicate), - - /** - * Remove all filters for a factor - */ removeFiltersByFactor: (factor: FilterFactor): void => state.removeFiltersByFactor(factor), - - /** - * Clear all filters - */ clearAllFilters: (): void => state.clearAllFilters(), - - /** - * Set current shortcut - */ setShortcut: (shortcut?: string): void => state.setShortcut(shortcut), - - /** - * Check if a filter is active - */ hasFilter: (filter: MemoFilter): boolean => state.hasFilter(filter), }; })(); diff --git a/web/src/store/store-utils.ts b/web/src/store/store-utils.ts index b9e328123..dff7c3d47 100644 --- a/web/src/store/store-utils.ts +++ b/web/src/store/store-utils.ts @@ -1,12 +1,6 @@ -/** - * Store utilities for MobX stores - * Provides request deduplication, error handling, and other common patterns - */ +// Store utilities for MobX stores +// Provides request deduplication, error handling, and other common patterns -/** - * Custom error class for store operations - * Provides structured error information for better debugging and error handling - */ export class StoreError extends Error { constructor( public readonly code: string, @@ -17,16 +11,10 @@ export class StoreError extends Error { this.name = "StoreError"; } - /** - * Check if an error is an AbortError from a cancelled request - */ static isAbortError(error: unknown): boolean { return error instanceof Error && error.name === "AbortError"; } - /** - * Wrap an unknown error in a StoreError for consistent error handling - */ static wrap(code: string, error: unknown, customMessage?: string): StoreError { if (error instanceof StoreError) { return error; @@ -37,21 +25,10 @@ export class StoreError extends Error { } } -/** - * Request deduplication manager - * Prevents multiple identical requests from being made simultaneously - */ +// Request deduplication manager - prevents multiple identical requests export class RequestDeduplicator { private pendingRequests = new Map>(); - /** - * Execute a request with deduplication - * If the same request key is already pending, returns the existing promise - * - * @param key - Unique identifier for this request (e.g., JSON.stringify(params)) - * @param requestFn - Function that executes the actual request - * @returns Promise that resolves with the request result - */ async execute(key: string, requestFn: () => Promise): Promise { // Check if this request is already pending if (this.pendingRequests.has(key)) { @@ -70,32 +47,19 @@ export class RequestDeduplicator { return promise; } - /** - * Cancel all pending requests - */ clear(): void { this.pendingRequests.clear(); } - /** - * Check if a request with the given key is pending - */ isPending(key: string): boolean { return this.pendingRequests.has(key); } - /** - * Get the number of pending requests - */ get size(): number { return this.pendingRequests.size; } } -/** - * Create a request key from parameters - * Useful for generating consistent keys for request deduplication - */ export function createRequestKey(prefix: string, params?: Record): string { if (!params) { return prefix; @@ -115,23 +79,13 @@ export function createRequestKey(prefix: string, params?: Record): return `${prefix}:${JSON.stringify(sortedParams)}`; } -/** - * Optimistic update helper - * Handles optimistic updates with rollback on error - */ +// Optimistic update helper with rollback on error export class OptimisticUpdate { constructor( private getCurrentState: () => T, private setState: (state: T) => void, ) {} - /** - * Execute an update with optimistic UI updates - * - * @param optimisticState - State to apply immediately - * @param updateFn - Async function that performs the actual update - * @returns Promise that resolves with the update result - */ async execute(optimisticState: T, updateFn: () => Promise): Promise { const previousState = this.getCurrentState(); diff --git a/web/src/store/user.ts b/web/src/store/user.ts index b73f15c34..d86bf173b 100644 --- a/web/src/store/user.ts +++ b/web/src/store/user.ts @@ -31,11 +31,7 @@ class LocalState { // The state id of user stats map. statsStateId = uniqueId(); - /** - * Computed property that aggregates tag counts across all users. - * Uses @computed to memoize the result and only recalculate when userStatsByName changes. - * This prevents unnecessary recalculations on every access. - */ + // Computed property that aggregates tag counts across all users (memoized) get tagCount() { return computed(() => { const tagCount: Record = {}; @@ -306,17 +302,11 @@ const userStore = (() => { }; })(); -/** - * Initializes the user store with proper sequencing to avoid temporal coupling. - * - * Initialization steps (order is critical): - * 1. Fetch current authenticated user session - * 2. Set current user in store (required for subsequent calls) - * 3. Fetch user settings (depends on currentUser being set) - * 4. Apply user preferences to instance store - * - * @throws Never - errors are handled internally with fallback behavior - */ +// Initializes the user store with proper sequencing: +// 1. Fetch current authenticated user session +// 2. Set current user in store (required for subsequent calls) +// 3. Fetch user settings (depends on currentUser being set) +// 4. Apply user preferences to instance store export const initialUserStore = async () => { try { // Step 1: Authenticate and get current user diff --git a/web/src/store/view.ts b/web/src/store/view.ts index 065f004b8..9c72f5954 100644 --- a/web/src/store/view.ts +++ b/web/src/store/view.ts @@ -1,34 +1,14 @@ -/** - * View Store - * - * Manages UI display preferences and layout settings. - * This is a client state store that persists to localStorage. - */ import { makeObservable, observable } from "mobx"; import { StandardState } from "./base-store"; const LOCAL_STORAGE_KEY = "memos-view-setting"; -/** - * Layout mode options - */ export type LayoutMode = "LIST" | "MASONRY"; -/** - * View store state - * Contains UI preferences for displaying memos - */ class ViewState extends StandardState { - /** - * Sort order: true = ascending (oldest first), false = descending (newest first) - */ + // Sort order: true = ascending (oldest first), false = descending (newest first) orderByTimeAsc: boolean = false; - - /** - * Display layout mode - * - LIST: Traditional vertical list - * - MASONRY: Pinterest-style grid layout - */ + // Display layout mode: LIST (vertical list) or MASONRY (Pinterest-style grid) layout: LayoutMode = "LIST"; constructor() { @@ -39,9 +19,6 @@ class ViewState extends StandardState { }); } - /** - * Override setPartial to persist to localStorage - */ setPartial(partial: Partial): void { // Validate layout if provided if (partial.layout !== undefined && !["LIST", "MASONRY"].includes(partial.layout)) { @@ -66,9 +43,6 @@ class ViewState extends StandardState { } } -/** - * View store instance - */ const viewStore = (() => { const state = new ViewState(); @@ -92,25 +66,14 @@ const viewStore = (() => { console.warn("Failed to load view settings from localStorage:", error); } - /** - * Toggle sort order between ascending and descending - */ const toggleSortOrder = (): void => { state.setPartial({ orderByTimeAsc: !state.orderByTimeAsc }); }; - /** - * Set the layout mode - * - * @param layout - The layout mode to set - */ const setLayout = (layout: LayoutMode): void => { state.setPartial({ layout }); }; - /** - * Reset to default settings - */ const resetToDefaults = (): void => { state.setPartial({ orderByTimeAsc: false, @@ -118,9 +81,6 @@ const viewStore = (() => { }); }; - /** - * Clear persisted settings - */ const clearStorage = (): void => { localStorage.removeItem(LOCAL_STORAGE_KEY); }; diff --git a/web/src/utils/i18n.ts b/web/src/utils/i18n.ts index 99ccb84d4..fc4e2f9e1 100644 --- a/web/src/utils/i18n.ts +++ b/web/src/utils/i18n.ts @@ -52,11 +52,7 @@ export const isValidateLocale = (locale: string | undefined | null): boolean => return locales.includes(locale); }; -/** - * Get the display name for a locale in its native language - * @param locale - The locale code (e.g., "en", "zh-Hans", "fr") - * @returns The display name with capitalized first letter, or the locale code if display name is unavailable - */ +// Get the display name for a locale in its native language export const getLocaleDisplayName = (locale: string): string => { try { const displayName = new Intl.DisplayNames([locale], { type: "language" }).of(locale); diff --git a/web/src/utils/markdown-list-detection.ts b/web/src/utils/markdown-list-detection.ts index f143adaa6..cd771ccb5 100644 --- a/web/src/utils/markdown-list-detection.ts +++ b/web/src/utils/markdown-list-detection.ts @@ -1,9 +1,3 @@ -/** - * Utilities for detecting list patterns in markdown text - * - * Used by the editor for auto-continuation of lists when user presses Enter - */ - export interface ListItemInfo { type: "task" | "unordered" | "ordered" | null; symbol?: string; // For task/unordered lists: "- ", "* ", "+ " @@ -11,12 +5,7 @@ export interface ListItemInfo { indent?: string; // Leading whitespace } -/** - * Detect the list item type of the last line before cursor - * - * @param contentBeforeCursor - Markdown content from start to cursor position - * @returns List item information, or null if not a list item - */ +// Detect the list item type of the last line before cursor export function detectLastListItem(contentBeforeCursor: string): ListItemInfo { const lines = contentBeforeCursor.split("\n"); const lastLine = lines[lines.length - 1]; @@ -61,12 +50,7 @@ export function detectLastListItem(contentBeforeCursor: string): ListItemInfo { }; } -/** - * Generate the text to insert when pressing Enter on a list item - * - * @param listInfo - Information about the current list item - * @returns Text to insert at cursor - */ +// Generate the text to insert when pressing Enter on a list item export function generateListContinuation(listInfo: ListItemInfo): string { const indent = listInfo.indent || ""; diff --git a/web/src/utils/markdown-manipulation.ts b/web/src/utils/markdown-manipulation.ts index 72ed684e4..6ae68719e 100644 --- a/web/src/utils/markdown-manipulation.ts +++ b/web/src/utils/markdown-manipulation.ts @@ -1,18 +1,6 @@ -/** - * Utilities for manipulating markdown strings (GitHub-style approach) - * - * These functions modify the raw markdown text directly without parsing to AST. - * This is the same approach GitHub uses for task list updates. - */ +// Utilities for manipulating markdown strings (GitHub-style approach) +// These functions modify the raw markdown text directly without parsing to AST -/** - * Toggle a task checkbox at a specific line number - * - * @param markdown - The full markdown content - * @param lineNumber - Zero-based line number - * @param checked - New checked state - * @returns Updated markdown string - */ export function toggleTaskAtLine(markdown: string, lineNumber: number, checked: boolean): string { const lines = markdown.split("\n"); @@ -27,7 +15,6 @@ export function toggleTaskAtLine(markdown: string, lineNumber: number, checked: const match = line.match(taskPattern); if (!match) { - // Not a task list item return markdown; } @@ -38,14 +25,6 @@ export function toggleTaskAtLine(markdown: string, lineNumber: number, checked: return lines.join("\n"); } -/** - * Toggle a task checkbox by its index (nth task in the document) - * - * @param markdown - The full markdown content - * @param taskIndex - Zero-based index of the task (0 = first task, 1 = second task, etc.) - * @param checked - New checked state - * @returns Updated markdown string - */ export function toggleTaskAtIndex(markdown: string, taskIndex: number, checked: boolean): string { const lines = markdown.split("\n"); const taskPattern = /^(\s*[-*+]\s+)\[([ xX])\](\s+.*)$/; @@ -70,12 +49,6 @@ export function toggleTaskAtIndex(markdown: string, taskIndex: number, checked: return lines.join("\n"); } -/** - * Remove all completed tasks from markdown - * - * @param markdown - The full markdown content - * @returns Markdown with completed tasks removed - */ export function removeCompletedTasks(markdown: string): string { const lines = markdown.split("\n"); const completedTaskPattern = /^(\s*[-*+]\s+)\[([xX])\](\s+.*)$/; @@ -88,7 +61,7 @@ export function removeCompletedTasks(markdown: string): string { if (completedTaskPattern.test(line)) { // Also skip the following line if it's empty (preserve spacing) if (i + 1 < lines.length && lines[i + 1].trim() === "") { - i++; // Skip next line + i++; } continue; } @@ -99,12 +72,6 @@ export function removeCompletedTasks(markdown: string): string { return result.join("\n"); } -/** - * Count tasks in markdown - * - * @param markdown - The full markdown content - * @returns Object with task counts - */ export function countTasks(markdown: string): { total: number; completed: number; @@ -134,24 +101,11 @@ export function countTasks(markdown: string): { }; } -/** - * Check if markdown has any completed tasks - * - * @param markdown - The full markdown content - * @returns True if there are completed tasks - */ export function hasCompletedTasks(markdown: string): boolean { const completedTaskPattern = /^(\s*[-*+]\s+)\[([xX])\](\s+.*)$/m; return completedTaskPattern.test(markdown); } -/** - * Get the line number of the nth task - * - * @param markdown - The full markdown content - * @param taskIndex - Zero-based task index - * @returns Line number, or -1 if not found - */ export function getTaskLineNumber(markdown: string, taskIndex: number): number { const lines = markdown.split("\n"); const taskPattern = /^(\s*[-*+]\s+)\[([ xX])\](\s+.*)$/; @@ -170,12 +124,6 @@ export function getTaskLineNumber(markdown: string, taskIndex: number): number { return -1; } -/** - * Extract all task items with their metadata - * - * @param markdown - The full markdown content - * @returns Array of task metadata - */ export interface TaskItem { lineNumber: number; taskIndex: number; diff --git a/web/src/utils/oauth.ts b/web/src/utils/oauth.ts index 045fddc82..a538e6fde 100644 --- a/web/src/utils/oauth.ts +++ b/web/src/utils/oauth.ts @@ -1,9 +1,3 @@ -/** - * OAuth state management utilities - * Implements secure state parameter handling following Auth0 best practices - * @see https://auth0.com/docs/secure/attack-protection/state-parameters - */ - const STATE_STORAGE_KEY = "oauth_state"; const STATE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes @@ -14,20 +8,14 @@ interface OAuthState { returnUrl?: string; } -/** - * Generate a cryptographically secure random state value - * Uses Web Crypto API for strong randomness - */ +// Generate a cryptographically secure random state value function generateSecureState(): string { const array = new Uint8Array(32); crypto.getRandomValues(array); return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(""); } -/** - * Store OAuth state in sessionStorage with metadata - * State is stored temporarily and will be validated on callback - */ +// Store OAuth state in sessionStorage export function storeOAuthState(identityProviderId: number, returnUrl?: string): string { const state = generateSecureState(); const stateData: OAuthState = { @@ -47,11 +35,7 @@ export function storeOAuthState(identityProviderId: number, returnUrl?: string): return state; } -/** - * Validate and retrieve OAuth state from storage - * Implements CSRF protection by verifying state matches - * Cleans up expired or used states - */ +// Validate and retrieve OAuth state from storage (CSRF protection) export function validateOAuthState(stateParam: string): { identityProviderId: number; returnUrl?: string } | null { try { const storedData = sessionStorage.getItem(STATE_STORAGE_KEY); @@ -89,10 +73,7 @@ export function validateOAuthState(stateParam: string): { identityProviderId: nu } } -/** - * Clean up expired OAuth states - * Should be called on app initialization - */ +// Clean up expired OAuth states (call on app init) export function cleanupExpiredOAuthState(): void { try { const storedData = sessionStorage.getItem(STATE_STORAGE_KEY); diff --git a/web/src/utils/remark-plugins/remark-preserve-type.ts b/web/src/utils/remark-plugins/remark-preserve-type.ts index 83e7d8207..cdf15151d 100644 --- a/web/src/utils/remark-plugins/remark-preserve-type.ts +++ b/web/src/utils/remark-plugins/remark-preserve-type.ts @@ -1,15 +1,7 @@ import type { Root } from "mdast"; import { visit } from "unist-util-visit"; -/** - * Remark plugin to preserve original mdast node types in the data field - * - * This allows us to check the original node type even after - * transformation to hast (HTML AST). - * - * The original type is stored in data.mdastType and will be available - * in the hast node as data.mdastType. - */ +// Remark plugin to preserve original mdast node types in the data field export const remarkPreserveType = () => { return (tree: Root) => { visit(tree, (node: any) => { diff --git a/web/src/utils/remark-plugins/remark-tag.ts b/web/src/utils/remark-plugins/remark-tag.ts index b21e5a9a4..fcb3575fb 100644 --- a/web/src/utils/remark-plugins/remark-tag.ts +++ b/web/src/utils/remark-plugins/remark-tag.ts @@ -1,38 +1,9 @@ import type { Root, Text } from "mdast"; import { visit } from "unist-util-visit"; -/** - * Custom remark plugin for #tag syntax - * - * Parses #tag patterns in text nodes and converts them to HTML nodes. - * This matches the goldmark backend TagNode implementation. - * - * Examples: - * #work → #work - * #2024_plans → #2024_plans - * #work-notes → #work-notes - * #tag1/subtag/subtag2 → #tag1/subtag/subtag2 - * - * Rules: - * - Tag must start with # followed by valid tag characters - * - Valid characters: Unicode letters, Unicode digits, underscore (_), hyphen (-), forward slash (/) - * - Maximum length: 100 characters - * - Stops at: whitespace, punctuation, or other invalid characters - * - Tags at start of line after ## are headings, not tags - */ - const MAX_TAG_LENGTH = 100; -/** - * Check if character is valid for tag content using Unicode categories. - * Uses Unicode property escapes for proper international character support. - * - * Valid characters: - * - \p{L}: Unicode letters (any script: Latin, CJK, Arabic, Cyrillic, etc.) - * - \p{N}: Unicode numbers/digits - * - \p{S}: Unicode symbols (includes emoji) - * - Special symbols: underscore (_), hyphen (-), forward slash (/) - */ +// Check if character is valid for tag content (Unicode letters, digits, symbols, _, -, /) function isTagChar(char: string): boolean { // Allow Unicode letters (any script) if (/\p{L}/u.test(char)) { @@ -62,9 +33,7 @@ function isTagChar(char: string): boolean { return false; } -/** - * Parse tags from text and return segments - */ +// Parse tags from text and return segments function parseTagsFromText(text: string): Array<{ type: "text" | "tag"; value: string }> { const segments: Array<{ type: "text" | "tag"; value: string }> = []; let i = 0; @@ -111,9 +80,7 @@ function parseTagsFromText(text: string): Array<{ type: "text" | "tag"; value: s return segments; } -/** - * Remark plugin to parse #tag syntax - */ +// Remark plugin to parse #tag syntax export const remarkTag = () => { return (tree: Root) => { // Process text nodes in all node types (paragraphs, headings, etc.) diff --git a/web/src/utils/theme.ts b/web/src/utils/theme.ts index dcb562361..ee61b77cc 100644 --- a/web/src/utils/theme.ts +++ b/web/src/utils/theme.ts @@ -33,9 +33,6 @@ const validateTheme = (theme: string): ValidTheme => { return VALID_THEMES.includes(theme as ValidTheme) ? (theme as ValidTheme) : "default"; }; -/** - * Detects system theme preference - */ export const getSystemTheme = (): "default" | "default-dark" => { if (typeof window !== "undefined" && window.matchMedia) { return window.matchMedia("(prefers-color-scheme: dark)").matches ? "default-dark" : "default"; @@ -43,10 +40,7 @@ export const getSystemTheme = (): "default" | "default-dark" => { return "default"; }; -/** - * Resolves the actual theme to apply based on user preference - * If theme is "system", returns the system preference, otherwise returns the theme as-is - */ +// Resolves "system" to actual theme based on OS preference export const resolveTheme = (theme: string): "default" | "default-dark" | "midnight" | "paper" | "whitewall" => { if (theme === "system") { return getSystemTheme(); @@ -55,12 +49,9 @@ export const resolveTheme = (theme: string): "default" | "default-dark" | "midni return validTheme === "system" ? getSystemTheme() : validTheme; }; -/** - * Gets the theme that should be applied on initial load - * Priority: stored user preference -> system preference -> default - */ +// Gets the theme that should be applied on initial load export const getInitialTheme = (): ValidTheme => { - // Try to get stored theme from localStorage (where user settings might be cached) + // Try to get stored theme from localStorage try { const storedTheme = localStorage.getItem("memos-theme"); if (storedTheme && VALID_THEMES.includes(storedTheme as ValidTheme)) { @@ -70,13 +61,10 @@ export const getInitialTheme = (): ValidTheme => { // localStorage might not be available } - // Fall back to system preference (return "system" to enable auto-switching) return "system"; }; -/** - * Applies the theme early to prevent flash of wrong theme - */ +// Applies the theme early to prevent flash of wrong theme export const applyThemeEarly = (): void => { const theme = getInitialTheme(); loadTheme(theme); @@ -113,10 +101,7 @@ export const loadTheme = (themeName: string): void => { } }; -/** - * Sets up a listener for system theme preference changes - * Returns a cleanup function to remove the listener - */ +// Sets up a listener for system theme preference changes export const setupSystemThemeListener = (onThemeChange: () => void): (() => void) => { if (typeof window === "undefined" || !window.matchMedia) { return () => {}; // No-op cleanup