This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
TimeLab is an offline-first web-based time series data labeling tool built with TypeScript, Vite, and modern web technologies. It features a clean architecture with a focus on code quality, performance, and developer experience.
- User uploads time series data (CSV, JSON, etc.) files via drag-and-drop or file picker, that is opened by a "Upload data" button in the toolbar.
- The data is parsed and stored in IndexedDB using a storage service.
- The main chart component visualizes the time series data using Apache ECharts. One time series chart is shown per data file, but a user can (1) switch axes in the "Chart configuration" panel, (2) use time stepper in the main toolbar to navigate through data files.
- Users can create label definitions (categories) in the "Labeling" panel and apply labels to time ranges on the chart using click-and-drag interactions. The Labeling is enabled by pressing "Toggle label drawing mode" button in the chart's toolbar.
- Labels are stored in IndexedDB and associated with specific time ranges, chart axes (selected columns) and data files.
- Users can manage projects, each containing its own set of data files and labels. Projects can be created, renamed, deleted, and switched between in the main toolbar from the projects dropdown.
- When done labeling, user can mark time series as labeled using a button in the main toolbar (#toggle-labeled).
npm run dev- Start development server on port 3000 (auto-opens browser)npm run build- TypeScript compilation + Vite build for productionnpm run preview- Preview production build locally on port 4173
npm run ci- Full CI pipeline: type-check + lint + test + buildnpm run type-check- TypeScript type checking without emitnpm run lint- ESLint with TypeScript strict rulesnpm run lint:fix- Auto-fix ESLint issuesnpm run format- Format code with Prettier
npm run test- Run tests once with Vitestnpm run test:watch- Run tests in watch modenpm run test:ui- Interactive testing with Vitest UInpm run test:coverage- Generate test coverage reportnpm run test:e2e- Run Cypress end-to-end testsnpm run test:e2e:open- Open Cypress interactive runner
TimeLab is a TypeScript time series data labeling tool with a clean, layered architecture:
- Entry Point:
src/main.tsorchestrates app initialization with loading screen integration - App Layer:
src/app/bootstrap.tsprovides service initialization and composition root - Services Layer:
src/services/contains dependency injection container and service registrycontainer.ts- Dependency injection container with lifecycle managementserviceRegistry.ts- Service registration and factory functionsprojectService.ts- Project management with injected storage dependencieslabelService.ts- Label management with cascade operations and cleanup
- Core Modules:
src/charts/- Time series chart implementations (main feature)src/data/- Data management, processing, and centralized data servicesrc/ui/- UI components and interactionssrc/platform/- Platform-specific functionality (storage, etc.)src/domain/- Pure business logic and domain modelssrc/shared/- Utility types, performance optimizations, and common functionality
Configured in vite.config.ts and vitest.config.ts:
@/→src/(primary alias)@app→src/app@domain→src/domain@platform→src/platform@charts→src/charts@ui→src/ui@styles→src/styles@shared→src/shared@workers→src/workers@services→src/services
Critical dual-directory structure for styling:
src/styles/ (Core theming system):
abstracts/tokens- Raw CSS custom properties (colors, spacing, typography)_semantic.scss- SCSS variables mapping to CSS properties for development_theme-variants.scss- Theme-specific overrides via[data-theme="theme-name"]abstracts/- Mixins and utility functions
styles/ (Application styles):
components/- Component-specific stylesheetsmain.scss- Global styles importing all components
Key Pattern: Always use semantic SCSS variables ($color-bg-primary) instead of CSS custom properties directly. Tokens are auto-injected via Vite's additionalData configuration.
- Themes controlled by
data-themeattribute on<html>element - Available themes: dark, oled, high-contrast, sepia, light, blue, green, purple, auto
- Auto theme respects
prefers-color-scheme - Runtime theme switching via CSS custom properties
In development mode, global functions are exposed:
window.__timeSeriesController- Main chart controller for debuggingwindow.__resetDatabase()- Reset IndexedDB storage
- Vite: Modern build tool with SCSS preprocessing and path aliases
- TypeScript: Strict configuration with separate configs for different contexts
- ESLint: Flat config (
eslint.config.js) with@typescript-eslint/strict-type-checked - Prettier: Code formatting integrated with ESLint
- Husky: Git hooks with lint-staged for pre-commit quality checks
- Vitest: Testing framework with jsdom environment
- Cypress: E2E testing with component testing support
- Coverage excludes config files and type definitions
- Global test utilities in
tests/setup.ts
- Use
@useinstead of@importfor SCSS modules - Include semantic imports:
@use '@/styles/semantic' as *; - Apply theme transitions for smooth theme switching
- Use elevation mixins for consistent shadows
- Strict TypeScript with
strictTypeCheckedESLint rules - Theme types use const assertions (
as const) - Import ordering enforced: builtin → external → internal
- Unused variables must be prefixed with
_
- Always run
npm run cibefore committing (type-check + lint + test + build) - Use sentence case for titles
- Modern JavaScript features (ESNext target)
- Source maps enabled for debugging
- Domain-First Design: Keep business logic in
src/domain/with no external dependencies - Dependency Injection: Use service registry and container for loose coupling and testability
- Result Pattern: Use
Result<T, E>for predictable error handling instead of throwing exceptions - Event-Driven Communication: Use typed custom events for component communication
- Memory Management: Always clean up event listeners and observers in destroy methods
- Type Safety: Use branded types for IDs, utility types for common patterns
- Performance Optimization: Leverage memoization, caching, and debouncing from
@shared/performance
- Use
Result<T, E>pattern for async operations that can fail - Create specific error types extending
TimeLabError - Handle promises explicitly - avoid floating promises in event handlers
- Log errors with structured information for debugging
- Use
ResizeObserverfor responsive components with proper cleanup - Implement lazy loading for large datasets
- Use defensive copying with
readonlytypes for immutable data - Leverage memoization with
memoize()andmemoizeAsync()from@shared - Use
debounce()andthrottle()for user input events - Apply
LRUCachefor expensive computations with size limits
- Write integration tests for key workflows
- Mock external dependencies properly
- Use dependency injection container for testable service isolation
- Leverage
resetServiceContainer()for test cleanup - Use branded types and factory functions for type-safe test data
TimeLab uses a sophisticated dependency injection system for loose coupling:
import { getServiceContainer, SERVICE_TOKENS } from '@services/container';
import { getProjectService, getLabelService, getDataService } from '@services/serviceRegistry';
// Services are registered as singletons with dependency injection
const projectService = getProjectService(); // Uses injected storage
const labelService = getLabelService(); // Uses injected data manager
const dataService = getDataService(); // Centralized data operations- Initialization:
startServices()initializes all services in dependency order - Registration: Services register with the container using tokens
- Injection: Dependencies are injected via constructor parameters
- Cleanup:
shutdownServices()properly disposes resources
- Create service interface and implementation
- Add service token to
SERVICE_TOKENS - Register in
serviceRegistry.tswith dependencies - Add getter function for easy access
- Include in startup/shutdown lifecycle
Use branded types to prevent ID mismatches:
import { ProjectId, LabelDefinitionId, createProjectId } from '@shared';
// Type-safe ID handling
const projectId: ProjectId = createProjectId('proj_123');
const labelId: LabelDefinitionId = createLabelDefinitionId('label_456');
// Compiler prevents mixing different ID types
function updateProject(id: ProjectId) {
/* ... */
}
updateProject(labelId); // ❌ TypeScript errorLeverage utility types for cleaner code:
import type { PartialExcept, WithRequired, DeepReadonly } from '@shared';
// Require specific fields while making others optional
type CreateProject = PartialExcept<Project, 'name' | 'isDefault'>;
// Make certain fields required
type ProjectWithMetadata = WithRequired<Project, 'createdAt' | 'updatedAt'>;
// Deep immutability
type ImmutableConfig = DeepReadonly<Configuration>;import { memoize, memoizeAsync } from '@shared/performance';
// Synchronous memoization
const expensiveCalculation = memoize(
(data: number[]) => {
return data.reduce((sum, val) => sum + Math.sqrt(val), 0);
},
{ maxSize: 100, ttl: 5000 }
);
// Async memoization with TTL
const loadUserData = memoizeAsync(
async (userId: string) => {
return await api.getUser(userId);
},
{ maxSize: 50, ttl: 30000 }
);import { debounce, throttle } from '@shared/performance';
// Debounced search input
const debouncedSearch = debounce((query: string) => {
performSearch(query);
}, 300);
// Throttled scroll handling
const throttledScroll = throttle(() => {
updateScrollPosition();
}, 16); // ~60fpsimport { LRUCache } from '@shared/performance';
const chartDataCache = new LRUCache<string, ChartData>(100);
function getChartData(sourceId: string): ChartData {
const cached = chartDataCache.get(sourceId);
if (cached) return cached;
const data = computeChartData(sourceId);
chartDataCache.set(sourceId, data);
return data;
}