This file provides frontend-specific development guidance for the Glean project.
For general project information, see ../CLAUDE.md.
frontend/
├── apps/
│ ├── web/ # Main React app (port 3000)
│ │ ├── components/
│ │ ├── pages/
│ │ ├── hooks/
│ │ └── stores/ # Zustand state stores
│ └── admin/ # Admin dashboard (port 3001)
├── packages/
│ ├── ui/ # Shared components (COSS UI based)
│ ├── api-client/ # TypeScript API client SDK
│ ├── types/ # Shared TypeScript types
│ └── logger/ # Unified logging (loglevel based)
- Language: TypeScript (strict)
- Framework: React 18 + Vite
- State/Cache: Zustand + TanStack Query
- Styling: Tailwind CSS
- Package Manager: pnpm + Turborepo
- Linting: ESLint + Prettier
This project uses COSS UI for components.
To add a new component:
- Visit
https://coss.com/ui/r/{component-name}.json - Copy to
frontend/packages/ui/src/components/ - Export from
frontend/packages/ui/src/components/index.ts
IMPORTANT: DO NOT modify anything within frontend/packages/ui/src/components/ unless explicitly asked.
When using AlertDialogClose or DialogClose from base-ui, do not use the render prop with <Button /> as it requires ref forwarding which the Button component doesn't support. Instead, use buttonVariants to apply button styling directly:
// ❌ Bad - causes "Function components cannot be given refs" warning
import { AlertDialogClose, Button } from '@glean/ui'
<AlertDialogClose render={<Button variant="ghost" />}>
Cancel
</AlertDialogClose>
// ✅ Good - use buttonVariants for styling
import { AlertDialogClose, buttonVariants } from '@glean/ui'
<AlertDialogClose className={buttonVariants({ variant: 'ghost' })}>
Cancel
</AlertDialogClose>
// For default button style
<AlertDialogClose className={buttonVariants()}>
OK
</AlertDialogClose>
// For destructive actions
<AlertDialogClose className={buttonVariants({ variant: 'destructive' })}>
Delete
</AlertDialogClose>The same pattern applies to DialogClose.
- Prettier with Tailwind plugin
- Import order: React → third-party → workspace packages → relative
ESLint + Prettier (configured in frontend/eslint.config.js and .prettierrc):
- No semicolons
- Single quotes
- 2-space indentation
- 100 character print width
- Trailing commas (ES5 style)
- Tailwind class sorting (via prettier-plugin-tailwindcss)
TypeScript:
- Strict mode enabled
- Unused variables: error (prefix with
_to ignore) - All exports should be typed
React-specific Rules:
- Use
react-refresh/only-export-componentsfor HMR compatibility - React hooks rules enforced
Follow these rules for choosing between window and globalThis:
// ✅ Use window for browser-specific APIs (DOM/BOM)
window.addEventListener('resize', handleResize)
window.removeEventListener('resize', handleResize)
window.dispatchEvent(new CustomEvent('myEvent'))
window.location.href = '/login'
window.location.reload()
window.isSecureContext
window.innerWidth
window.matchMedia('(prefers-color-scheme: dark)')
window.electronAPI // Custom browser API
// ✅ Use globalThis only when code might run in multiple environments
// (e.g., shared code between browser and Node.js)
if (typeof globalThis !== 'undefined') {
// Cross-platform code
}
// ❌ Bad - Don't use globalThis for browser-specific APIs
globalThis.location.reload() // Use window.location
globalThis.addEventListener(...) // Use window.addEventListenerBest Practice:
- Default to
window: When using browser-specific APIs (DOM, BOM, Web APIs) - Use
globalThis: Only when code needs to run in multiple environments (browser + Node.js) - Type safety:
windowprovides better TypeScript support for browser APIs
Rationale:
- This is a browser-only frontend application
windowis the idiomatic and correct choice for browser APIswindowprovides proper TypeScript DOM typesglobalThisshould be reserved for truly cross-platform code
import { logger, createNamedLogger } from '@glean/logger'
logger.info('Message', { context: 'data' })- Configure via
VITE_LOG_LEVEL(debug in dev, error in prod)
This project uses react-i18next for internationalization. Always use translation keys instead of hardcoded text.
// ❌ Bad - hardcoded text
<button>Save</button>
<h1>Settings</h1>
// ✅ Good - using i18n
import { useTranslation } from '@glean/i18n'
function MyComponent() {
const { t } = useTranslation('common') // or 'auth', 'settings', etc.
return (
<>
<button>{t('actions.save')}</button>
<h1>{t('settings:title')}</h1>
</>
)
}common: Shared UI text (buttons, states, actions)auth: Authentication pages (login, register)settings: Settings pagereader: Reading interfacebookmarks: Bookmark managementfeeds: Feed management, folders, OPMLui: UI component-level text
-
Add key-value pairs to the appropriate namespace JSON files:
frontend/packages/i18n/src/locales/en/{namespace}.jsonfrontend/packages/i18n/src/locales/zh-CN/{namespace}.json
-
Use the translation in your component:
const { t } = useTranslation('namespace') t('your.new.key')
// feeds.json
{
"count": "{{count}} feeds"
}t('feeds:count', { count: 5 }) // Output: "5 feeds"import { formatRelativeTime } from '@glean/i18n/utils/date-formatter'
import { useTranslation } from '@glean/i18n'
function MyComponent({ date }) {
const { i18n } = useTranslation()
return <time>{formatRelativeTime(date, i18n.language)}</time>
}Users can change language in Settings → Appearance → Language. The selection is persisted to localStorage and auto-detected on first visit.
The web app uses a three-column layout:
┌──────────────────────────────────────────────────────────────────┐
│ Header (optional) │
├──────────┬─────────────────┬─────────────────────────────────────┤
│ │ │ │
│ Sidebar │ Entry List │ Reading Pane │
│ (72-256) │ (280-500) │ (flexible) │
│ │ │ │
│ - Feeds │ - Entry cards │ - Article title │
│ - Folders│ - Filters │ - Content (prose) │
│ - Tags │ - Skeleton │ - Actions (like, bookmark, share) │
│ │ │ │
└──────────┴─────────────────┴─────────────────────────────────────┘
- Sidebar: Collapsible (72px ↔ 256px), contains navigation and feed list
- Entry List: Resizable (280-500px), shows article previews with filters
- Reading Pane: Flexible width, displays full article content
See @docs/design.md for more details.
| Principle | Description |
|---|---|
| Warm Dark Theme | Default theme with amber primary (hsl(38 92% 50%)) |
| Reading-First | Optimized typography and spacing for long-form content |
| Subtle Animations | Meaningful feedback without distraction (fade, slide, glow) |
| Glassmorphism | Modern blur effects for overlays and cards |
Always use CSS variables, never hard-coded colors:
// Correct
className = 'bg-primary text-primary-foreground'
className = 'text-muted-foreground hover:text-foreground'
// Incorrect
className = 'bg-amber-500 text-slate-900'Key semantic colors:
--primary: Amber accent--secondary: Teal accent--background/--foreground: Main page colors--card/--muted: Surface colors--destructive/--success/--warning: Semantic states
| Usage | Font Family | Example Class |
|---|---|---|
| Headings/UI | DM Sans | font-display text-2xl font-bold |
| Article Content | Crimson Pro | prose font-reading |
| Code | Monospace | Built-in prose styling |
// Glass effect for overlays
<div className="glass">...</div>
// Interactive cards
<div className="card-hover">...</div>
// Primary action buttons with glow
<Button className="btn-glow">...</Button>
// Animations
<div className="animate-fade-in">...</div>
<ul className="stagger-children">{items}</ul>- Buttons: Primary (glow on hover), Ghost (transparent), Outline (bordered)
- Cards: Subtle lift on hover (
translateY(-2px)) - Focus States: 4px ring in primary color
- Loading: Skeleton placeholders matching content layout
- Transitions: Fast (150ms) for hover, Standard (200ms) for state changes
Refer to docs/design.md for complete color palettes, spacing scales, and detailed component specifications.
cd frontend/apps/web && pnpm testWhen debugging frontend issues, use chrome-devtools MCP to help with:
- Taking snapshots and screenshots
- Inspecting network requests and console messages
- Interacting with page elements
# Auto-fix ESLint issues
cd frontend && pnpm lint --fix
# Auto-format with Prettier
cd frontend && pnpm format
# Type check
cd frontend && pnpm typecheck
# Build
cd frontend && pnpm build| Error | Solution |
|---|---|
ESLint: no-unused-vars |
Remove variable or prefix with _ |
TypeScript: implicit any |
Add explicit type annotation |
Prettier: formatting |
Run pnpm format |
# Type check all packages
pnpm typecheck
# Type check specific package
pnpm --filter=@glean/web typecheck
# Build specific package
pnpm --filter=@glean/web build
# Start dev server
pnpm --filter=@glean/web dev
# Run tests
pnpm --filter=@glean/web test- This project uses monorepo structure - always check your current working directory
- You don't have to create documentation unless explicitly asked
- Always write code comments in English
- DO NOT modify anything within
frontend/packages/ui/src/components/unless explicitly asked