Files
memos/frontend.txt
gugus bb402d4ccc
Some checks failed
Backend Tests / Static Checks (push) Has been cancelled
Backend Tests / Tests (other) (push) Has been cancelled
Backend Tests / Tests (plugin) (push) Has been cancelled
Backend Tests / Tests (server) (push) Has been cancelled
Backend Tests / Tests (store) (push) Has been cancelled
Build Canary Image / build-frontend (push) Has been cancelled
Build Canary Image / build-push (linux/amd64) (push) Has been cancelled
Build Canary Image / build-push (linux/arm64) (push) Has been cancelled
Build Canary Image / merge (push) Has been cancelled
Frontend Tests / Lint (push) Has been cancelled
Frontend Tests / Build (push) Has been cancelled
Proto Linter / Lint Protos (push) Has been cancelled
first commit
2026-03-04 06:30:47 +00:00

658 lines
18 KiB
Plaintext

# Memos Frontend Documentation
## Authentication and Middleware System
### Frontend Authentication Architecture
The frontend authentication system uses a combination of:
1. **React Context** for state management
2. **Route-based guards** for access control
3. **Connect RPC interceptors** for API authentication
4. **Layout-based protection** for UI components
### Core Authentication Components
#### 1. Auth Context (`web/src/contexts/AuthContext.tsx`)
```typescript
// Manages global authentication state
const AuthContext = createContext<AuthContextType>({
currentUser: undefined,
userGeneralSetting: undefined,
isInitialized: false,
isLoading: true,
initialize: async () => {},
logout: () => {},
refetchSettings: async () => {}
});
// Provides authentication state to entire app
function AuthProvider({ children }: { children: React.ReactNode }) {
// Handles token initialization, user fetching, logout logic
}
```
#### 2. Route Protection System
**Public Routes** (no auth required):
- `/auth` - Authentication pages
- `/auth/signup` - User registration
- `/auth/callback` - OAuth callback
- `/explore` - Public explore page
- `/u/:username` - User profiles
- `/memos/:uid` - Individual memo details
**Private Routes** (auth required):
- `/` (root) - Main dashboard
- `/attachments` - File attachments
- `/inbox` - Notifications
- `/archived` - Archived memos
- `/setting` - User settings
#### 3. Layout-Based Authentication Guards
**RootLayout** (`web/src/layouts/RootLayout.tsx`):
```typescript
const RootLayout = () => {
const currentUser = useCurrentUser();
useEffect(() => {
if (!currentUser) {
redirectOnAuthFailure(); // Redirects to /auth
}
}, [currentUser]);
// Renders navigation and main content
return (
<div className="w-full min-h-full flex flex-row">
<Navigation />
<main>
<Outlet />
</main>
</div>
);
};
```
**Route Configuration** (`web/src/router/index.tsx`):
```typescript
const router = createBrowserRouter([
{
path: "/",
element: <App />, // Handles instance initialization
children: [
{
path: "/auth", // Public routes
children: [
{ path: "", element: <SignIn /> },
{ path: "signup", element: <SignUp /> },
{ path: "callback", element: <AuthCallback /> }
]
},
{
path: "/", // Protected routes (wrapped in RootLayout)
element: <RootLayout />, // Auth guard here
children: [
{
element: <MainLayout />, // Main app layout
children: [
{ path: "", element: <Home /> },
{ path: "explore", element: <Explore /> }
// ... other protected routes
]
}
]
}
]
}
]);
```
### API Authentication Interceptors
#### Connect RPC Interceptor (`web/src/connect.ts`):
```typescript
const authInterceptor: Interceptor = (next) => async (req) => {
const isRetryAttempt = req.header.get(RETRY_HEADER) === RETRY_HEADER_VALUE;
const token = await getRequestToken();
setAuthorizationHeader(req, token);
try {
return await next(req);
} catch (error) {
// Handle 401 Unauthorized
if (error.code === Code.Unauthenticated && !isRetryAttempt) {
try {
// Attempt token refresh
const newToken = await refreshAndGetAccessToken();
setAuthorizationHeader(req, newToken);
req.header.set(RETRY_HEADER, RETRY_HEADER_VALUE);
return await next(req);
} catch (refreshError) {
redirectOnAuthFailure(); // Redirect to login
throw refreshError;
}
}
throw error;
}
};
```
#### Token Management
- **Storage**: Access tokens stored in memory (not localStorage)
- **Refresh**: Automatic token refresh on 401 errors
- **Expiration**: Proactive refresh on tab focus
- **Cleanup**: Tokens cleared on logout/auth failure
### Authentication Flow
#### 1. App Initialization (`web/src/main.tsx`)
```typescript
// Early theme/locale setup to prevent flash
applyThemeEarly();
applyLocaleEarly();
// Initialize auth and instance contexts
<AuthProvider>
<InstanceProvider>
<RouterProvider router={router} />
</InstanceProvider>
</AuthProvider>
```
#### 2. Authentication Check (`web/src/utils/auth-redirect.ts`)
```typescript
export function redirectOnAuthFailure(): void {
const currentPath = window.location.pathname;
// Allow public routes
if (isPublicRoute(currentPath)) return;
// Redirect private routes to auth
if (isPrivateRoute(currentPath)) {
clearAccessToken();
window.location.replace(ROUTES.AUTH);
}
}
```
#### 3. User Session Management
- Token refresh on window focus
- Automatic logout on token expiration
- Context cleanup on logout
- Query cache invalidation
### Key Security Features
- **Token Storage**: In-memory only (security best practice)
- **Automatic Refresh**: Handles token rotation seamlessly
- **Route Guards**: Prevent unauthorized access to protected routes
- **Context Isolation**: Auth state managed centrally
- **Error Handling**: Graceful degradation on auth failures
- **Session Cleanup**: Complete state reset on logout
### Related Files
- `web/src/contexts/AuthContext.tsx` - Authentication state management
- `web/src/layouts/RootLayout.tsx` - Main authentication guard
- `web/src/utils/auth-redirect.ts` - Route protection logic
- `web/src/connect.ts` - API authentication interceptors
- `web/src/router/index.tsx` - Route configuration
- `web/src/main.tsx` - App initialization
## Theme System Implementation
### Vite Build Process for Themes
The CSS themes are processed and built by **Vite** during the build process:
1. **Vite Configuration** (`web/vite.config.mts`)
- Uses `@tailwindcss/vite` plugin for CSS processing
- Tailwind CSS v4 handles theme token compilation
- Themes are bundled during `pnpm build` process
2. **CSS Processing Pipeline**
- Base styles imported in `web/src/index.css`
- Theme-specific CSS files located in `web/src/themes/`
- Vite processes `@theme inline` directives at build time
- Dynamic theme switching handled via JavaScript at runtime
### Theme Architecture
#### Core Theme Files
- `web/src/index.css` - Main CSS entry point
- `web/src/themes/default.css` - Base theme with Tailwind token mappings
- `web/src/themes/default-dark.css` - Dark theme variables
- `web/src/themes/paper.css` - Paper-style theme
- `web/src/utils/theme.ts` - Theme loading and management logic
#### How Themes Are Built
##### 1. Build Time Processing (Vite + Tailwind)
```css
/* In web/src/themes/default.css */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
/* ... other CSS variables */
}
```
- Tailwind compiles these into static CSS classes
- Shared across all themes
- Optimized during Vite build process
##### 2. Runtime Theme Switching
- JavaScript dynamically injects theme CSS
- Uses `?raw` import to get CSS as string
- Injects `<style>` elements into document head
- Controlled by `web/src/utils/theme.ts`
### Theme Loading Mechanism
In `web/src/utils/theme.ts`:
```typescript
import defaultDarkThemeContent from "../themes/default-dark.css?raw";
import paperThemeContent from "../themes/paper.css?raw";
const THEME_CONTENT: Record<ResolvedTheme, string | null> = {
default: null, // Uses base CSS
"default-dark": defaultDarkThemeContent,
paper: paperThemeContent,
};
// Dynamically injects theme CSS
const injectThemeStyle = (theme: ResolvedTheme): void => {
if (theme === "default") return; // Use base CSS
const css = THEME_CONTENT[theme];
if (css) {
const style = document.createElement("style");
style.id = "instance-theme";
style.textContent = css;
document.head.appendChild(style);
}
};
```
### Available Themes
1. **System** (`system`) - Follows OS preference
2. **Light** (`default`) - Default light theme
3. **Dark** (`default-dark`) - Dark mode theme
4. **Paper** (`paper`) - Paper-style theme
### Theme Selection Components
- `web/src/components/ThemeSelect.tsx` - Theme dropdown selector
- `web/src/pages/UserSetting.tsx` - User profile theme settings
- `web/src/contexts/InstanceContext.tsx` - Instance-wide theme defaults
### Build Process Commands
```bash
# Development
pnpm dev
# Production build (processes themes)
pnpm build
# The build process:
# 1. Vite processes Tailwind CSS
# 2. Theme CSS files are bundled
# 3. Dynamic theme loading code is included
# 4. Output goes to dist/ directory
```
### Key Technical Details
- **CSS-in-JS Approach**: Themes are injected as `<style>` elements
- **Tree Shaking**: Unused theme CSS is removed during build
- **Hot Reloading**: Theme changes reflect instantly in development
- **Performance**: Theme switching is instant (no page reload)
- **Storage**: Theme preference stored in localStorage
- **Fallback**: Defaults to "system" theme if none selected
The theme system leverages Vite's build optimization while maintaining runtime flexibility for dynamic theme switching.
## Menu and Navigation System
### Navigation Component Architecture
The menu/navigation system is implemented through the `Navigation` component which serves as the main sidebar navigation.
### Core Navigation Component (`web/src/components/Navigation.tsx`)
```typescript
interface NavLinkItem {
id: string;
path: string;
title: string;
icon: React.ReactNode;
}
const Navigation = (props: { collapsed?: boolean; className?: string }) => {
const currentUser = useCurrentUser();
const { data: notifications = [] } = useNotifications();
// Navigation items are defined as objects
const homeNavLink: NavLinkItem = {
id: "header-memos",
path: Routes.ROOT,
title: t("common.memos"),
icon: <LibraryIcon className="w-6 h-auto shrink-0" />
};
// Conditional navigation based on authentication state
const navLinks: NavLinkItem[] = currentUser
? [homeNavLink, exploreNavLink, attachmentsNavLink, inboxNavLink]
: [exploreNavLink, signInNavLink];
}
```
### Menu Items Configuration
#### For Authenticated Users
1. **Home** (`/`) - Main memos dashboard
- Icon: `LibraryIcon`
- Shows user's memos
2. **Explore** (`/explore`) - Public memos exploration
- Icon: `EarthIcon`
- Browse public content
3. **Attachments** (`/attachments`) - File management
- Icon: `PaperclipIcon`
- Manage uploaded files
4. **Inbox** (`/inbox`) - Notifications
- Icon: `BellIcon` with badge
- Shows unread notification count
#### For Unauthenticated Users
1. **Explore** (`/explore`) - Public content browsing
2. **Sign In** (`/auth`) - Authentication page
- Icon: `UserCircleIcon`
### Navigation Layout Structure
#### Desktop (>768px)
- Fixed sidebar on left (`w-16` when collapsed, wider when expanded)
- `RootLayout` renders `Navigation` component vertically
- Collapsed state shows icons only with tooltips
- Expanded state shows icons + text labels
#### Mobile (<768px)
- `NavigationDrawer` component in mobile header
- Slide-out drawer from left side
- Full-width navigation when opened
### Navigation Implementation
```typescript
// In RootLayout.tsx
{sm && (
<div className="fixed top-0 left-0 h-full w-16">
<Navigation className="py-4" collapsed={true} />
</div>
)}
// In MobileHeader.tsx
<NavigationDrawer /> // For mobile devices
```
### Key Features
#### 1. Conditional Rendering
- Different menus for authenticated vs unauthenticated users
- Notification badges for unread messages
- Responsive design for mobile/desktop
#### 2. Active State Management
```typescript
<NavLink
className={({ isActive }) =>
cn(
"px-2 py-2 rounded-2xl border flex flex-row",
isActive
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "border-transparent hover:bg-sidebar-accent"
)
}
to={navLink.path}
>
```
#### 3. Collapsed State
- Icons only with tooltip hints
- Space-efficient for narrow screens
- Smooth transitions
#### 4. User Menu Integration
- Bottom section shows `UserMenu` component
- Profile/avatar display
- Settings and logout options
### Related Components
- `web/src/components/Navigation.tsx` - Main navigation logic
- `web/src/components/NavigationDrawer.tsx` - Mobile drawer implementation
- `web/src/components/UserMenu.tsx` - User profile dropdown
- `web/src/layouts/RootLayout.tsx` - Desktop layout with sidebar
- `web/src/components/MobileHeader.tsx` - Mobile header with drawer
### Internationalization
- Menu titles translated via `useTranslate()` hook
- Supports multiple languages
- Dynamic text based on user locale
The navigation system provides a clean, responsive menu that adapts to user authentication state and screen size while maintaining consistent UX across devices.
## Layout System (Masonry vs List)
### View Context Architecture
The layout system is managed through the `ViewContext` which provides global state management for layout preferences and sorting options.
### Core View Context (`web/src/contexts/ViewContext.tsx`)
```typescript
export type LayoutMode = "LIST" | "MASONRY";
interface ViewContextValue {
orderByTimeAsc: boolean; // Sort order
layout: LayoutMode; // Current layout mode
toggleSortOrder: () => void;
setLayout: (layout: LayoutMode) => void;
}
// Persistent storage in localStorage
const LOCAL_STORAGE_KEY = "memos-view-setting";
// Default state
return { orderByTimeAsc: false, layout: "LIST" as LayoutMode };
```
### Layout Modes
#### 1. LIST Layout (`"LIST"`)
- **Description**: Traditional linear list view
- **Implementation**: Single column layout
- **Behavior**: Memos displayed vertically in chronological order
- **Use Case**: Reading-focused, sequential browsing
#### 2. MASONRY Layout (`"MASONRY"`)
- **Description**: Pinterest-style grid layout
- **Implementation**: Multi-column responsive grid
- **Behavior**: Memos distributed based on actual rendered heights
- **Use Case**: Visual browsing, efficient space utilization
### Masonry Layout Implementation
#### Core Components
1. **MasonryView** (`web/src/components/MasonryView/MasonryView.tsx`)
- Main container component
- Manages column distribution
- Uses CSS Grid for layout
2. **MasonryColumn** (`web/src/components/MasonryView/MasonryColumn.tsx`)
- Represents individual columns
- Contains assigned memos
- Handles prefix elements (like memo editor)
3. **MasonryItem** (`web/src/components/MasonryView/MasonryItem.tsx`)
- Wraps individual memos
- Measures actual rendered height
- Uses ResizeObserver for dynamic updates
4. **useMasonryLayout** Hook (`web/src/components/MasonryView/useMasonryLayout.ts`)
- Calculates optimal column count
- Distributes memos to columns
- Manages height measurements
#### Key Features
##### 1. Height-Based Distribution
```typescript
// Smart algorithm that assigns memos to shortest column
const shortestColumnIndex = columnHeights.reduce(
(minIndex, currentHeight, currentIndex) =>
(currentHeight < columnHeights[minIndex] ? currentIndex : minIndex),
0
);
```
##### 2. Dynamic Column Count
```typescript
const calculateColumns = useCallback(() => {
if (!containerRef.current || listMode) return 1;
const containerWidth = containerRef.current.offsetWidth;
const scale = containerWidth / MINIMUM_MEMO_VIEWPORT_WIDTH;
return scale >= 1.2 ? Math.ceil(scale) : 1;
}, [containerRef, listMode]);
```
##### 3. ResizeObserver Integration
```typescript
useEffect(() => {
const measureHeight = () => {
if (itemRef.current) {
const height = itemRef.current.offsetHeight;
onHeightChange(memo.name, height);
}
};
resizeObserverRef.current = new ResizeObserver(measureHeight);
resizeObserverRef.current.observe(itemRef.current);
}, [memo.name, onHeightChange]);
```
##### 4. Responsive Behavior
- Automatically adjusts columns based on viewport width
- Minimum width threshold for multi-column layout
- Smooth transitions during resizing
### Layout Switching
#### User Interface
```typescript
// MemoDisplaySettingMenu component
<Select value={layout} onValueChange={(value) => setLayout(value as "LIST" | "MASONRY")}>
<SelectItem value="LIST">{t("memo.list")}</SelectItem>
<SelectItem value="MASONRY">{t("memo.masonry")}</SelectItem>
</Select>
```
#### State Management
```typescript
const { layout, setLayout } = useView();
// Toggle between layouts
setLayout("MASONRY"); // or "LIST"
// Persistence
localStorage.setItem("memos-view-setting", JSON.stringify({ layout, orderByTimeAsc }));
```
### Integration with Memo Display
#### PagedMemoList Component
```typescript
const PagedMemoList = (props: Props) => {
const { layout } = useView(); // Get current layout preference
return (
<div className="flex flex-col justify-start items-start w-full max-w-full">
{layout === "MASONRY" ? (
<MasonryView
memoList={sortedMemoList}
renderer={props.renderer}
prefixElement={<MemoEditor />}
/>
) : (
// Traditional list view implementation
<ListView memoList={sortedMemoList} />
)}
</div>
);
};
```
### Performance Optimizations
1. **Debounced Redistribution**: Prevents excessive re-layout during rapid changes
2. **Memoized Calculations**: Optimized height calculations and distribution algorithms
3. **Efficient State Updates**: Only re-render when necessary
4. **ResizeObserver Cleanup**: Proper memory management
### Related Files
- `web/src/contexts/ViewContext.tsx` - Global layout state management
- `web/src/components/MasonryView/` - Masonry layout implementation
- `web/src/components/MemoDisplaySettingMenu.tsx` - Layout selection UI
- `web/src/components/PagedMemoList/PagedMemoList.tsx` - Memo list container
- `web/src/components/MasonryView/README.md` - Detailed technical documentation
### User Experience Benefits
- **Choice**: Users can choose preferred browsing style
- **Persistence**: Layout preference saved across sessions
- **Responsiveness**: Adapts to different screen sizes
- **Performance**: Optimized rendering for both modes
- **Consistency**: Same memo content, different presentation
The layout system provides flexible viewing options that cater to different user preferences and use cases while maintaining optimal performance and responsive design.