Skip to content

Instantly share code, notes, and snippets.

@johnwheeler
Last active June 4, 2025 05:33
Show Gist options
  • Save johnwheeler/c2a5839efca589a342b93c64ed65112f to your computer and use it in GitHub Desktop.
Save johnwheeler/c2a5839efca589a342b93c64ed65112f to your computer and use it in GitHub Desktop.
CLAUDE.md

screen.cam Project

A modern pnpm workspace project with NextJS and ChakraUI, structured for component-driven development with enterprise-grade code quality tools.

Repository: https://github.com/johnwheeler/screencam (Private)

IMPORTANT: This documentation should be kept concise and focused on essential information for development. Avoid adding comprehensive documentation unless it provides clear value for daily development workflow. When adding new sections, evaluate their importance relative to existing content.

IMPORTANT: The CLAUDE_BUILD environment variable is set by Claude Code to build to /tmp instead of .next, preventing interference with the dev server. This is why you see CLAUDE_BUILD=1 pnpm build in the commands.

IMPORTANT: Run pnpm check:fix and pnpm test:run after every source code change to ensure consistent formatting and catch linting issues early. When done with a large change, or periodically, run the CLAUDE_BUILD=1 pnpm build

Project Structure

screencam/
├── packages/
│   ├── web/          # NextJS 15.3.3 web application (Pages Router)
│   ├── core/         # Shared components and design system with ChakraUI 3.19.1
│   └── i18n-shared/  # Framework-agnostic i18n abstraction
│   └── lab/          # Proving ground, prototypes, experiments
├── infrastructure/   # AWS CDK infrastructure (Amplify hosting)
├── .vscode/          # VS Code settings for BiomeJS integration
├── biome.json        # BiomeJS configuration (formatting + linting)
├── package.json      # Root workspace configuration
├── pnpm-workspace.yaml
├── .npmrc            # Required for Amplify builds (node-linker=hoisted)
└── CLAUDE.md

Workspaces

  • web: Main NextJS application using pages router
  • core: Shared component library with ChakraUI design system
  • i18n-shared: Framework-agnostic internationalization abstraction
  • infrastructure: AWS CDK infrastructure for Amplify deployment

Deployment

AWS Amplify hosting with CDK infrastructure:

  • Production: main branch → https://screen.cam
  • Staging: stage branch → https://stage.screen.cam
  • Key Amplify Config: .npmrc with node-linker=hoisted required for pnpm workspace builds
  • Dependencies: In packages/web, types moved from devDependencies to dependencies for Amplify build compatibility

Development Commands

# Install dependencies
pnpm install

# Start development server
pnpm dev

# Build the project
pnpm build

# Code quality checks (BiomeJS)
pnpm check                  # Check formatting and linting

# Type checking
pnpm type-check

# Testing
pnpm test                   # Run tests in watch mode
pnpm test:run               # Run tests once
pnpm test:ui                # Run tests with UI interface

# Code formatting and linting with BiomeJS
pnpm format                 # Format code
pnpm check                  # Check formatting and linting
pnpm check:fix              # Auto-fix all issues
pnpm ci                     # CI mode (no fixes, exit 1 on issues)

# Infrastructure
pnpm cdk:deploy             # Deploy AWS infrastructure (CDK)

# Claude Code Integration
CLAUDE_BUILD=1 pnpm build   # Build to /tmp to avoid clobbering dev server

Technology Stack

  • Framework: NextJS 15.3.3 with Pages Router
  • UI Library: ChakraUI 3.19.1
  • Package Manager: pnpm with workspaces
  • Language: TypeScript
  • Icons: Lucide React (clean, customizable SVG icons)
  • Fonts: Google Fonts (Geist Sans & Geist Mono)
  • Styling: ChakraUI custom design system
  • Internationalization: Framework-agnostic with next-i18next adapter
  • Testing: Vitest 3.1.4 + React Testing Library + ChakraUI test utilities
  • Code Quality: BiomeJS (formatting + linting + import organization)

Design System

The core package includes:

  • Custom theme configuration with brand colors
  • Typography scale and spacing tokens (including page-x for consistent page-level horizontal padding)
  • Component recipes
  • HelloWorld component with full feature showcase

Design Token Usage: Always use semantic tokens (e.g., px="lg", py="md") instead of raw values (e.g., px={5}). Search existing tokens in packages/core/src/theme.ts before creating new ones to maintain consistency.

Chakra UI CLI Snippets

Use Chakra UI CLI snippets for consistent, production-ready components with proper TypeScript types, accessibility features, and Portal architecture.

# List all available snippets
npx @chakra-ui/cli snippet list

# Add a specific snippet
npx @chakra-ui/cli snippet add <snippet-name>

# Example: Add dialog components
npx @chakra-ui/cli snippet add dialog

Usage: Check for existing snippets before implementing custom components. If using ChakraUI components with Root elements (e.g. Menu.Root), verify if snippets exist.

Component System

Toast Notifications

ChakraUI snippet-based toaster with global configuration and i18n integration. The <Toaster /> component is rendered globally in _app.tsx with Portal rendering.

import { toaster } from "@screencam/core"

// Basic usage
toaster.create({
  title: "Action completed",
  type: "success",
})

// With i18n
const t = useT()
toaster.create({
  title: t("success.title"),
  description: t("success.message"),
  type: "success",
})

Configuration: bottom-end placement, auto-dismiss with pause on idle, closable, Portal rendering

Dialog System (Declarative)

ChakraUI snippet-based dialog components with Portal rendering.

import {
  DialogRoot,
  DialogContent,
  DialogHeader,
  DialogBody,
  DialogFooter,
  DialogTitle,
  DialogTrigger,
  DialogCloseTrigger,
  DialogActionTrigger,
} from "@screencam/core"
;<DialogRoot>
  <DialogTrigger asChild>
    <Button>Open Dialog</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>{t("dialog.title")}</DialogTitle>
    </DialogHeader>
    <DialogBody>
      <Text>{t("dialog.message")}</Text>
    </DialogBody>
    <DialogFooter>
      <DialogActionTrigger asChild>
        <Button variant="outline">{t("dialog.cancel")}</Button>
      </DialogActionTrigger>
      <DialogActionTrigger asChild>
        <Button colorPalette="brand">{t("dialog.confirm")}</Button>
      </DialogActionTrigger>
    </DialogFooter>
    <DialogCloseTrigger />
  </DialogContent>
</DialogRoot>

Dialog System (Imperative)

Custom hook providing toast-like imperative API for dialogs. The <DialogProvider /> is rendered globally in _app.tsx.

import { useDialog } from "@screencam/core"

const dialog = useDialog()

// Alert dialog (single OK button)
dialog.alert({
  title: t("alert.title"),
  message: t("alert.message"),
})

// Confirm dialog (Cancel/Confirm buttons)
dialog.confirm({
  title: t("confirm.title"),
  message: t("confirm.message"),
  confirmLabel: t("confirm.yes"),
  cancelLabel: t("confirm.cancel"),
  onConfirm: () => handleConfirm(),
  onCancel: () => handleCancel(),
})

// Custom dialog (flexible content and actions)
dialog.show({
  title: t("custom.title"),
  content: <MyCustomContent />,
  actions: [
    { label: t("cancel"), variant: "outline" },
    { label: t("save"), colorPalette: "brand", onClick: handleSave },
  ],
})

Internationalization System

Framework-agnostic i18n architecture with dependency inversion pattern.

Architecture

  • i18n-shared: Provides Translator interface and useT() hook
  • core: Components depend only on the abstraction
  • web: Adapts next-i18next to the interface via useTranslationAdapter()

Translation Files

packages/web/public/locales/en/
├── common.json      # Navigation, actions, meta content
└── components.json  # Component-specific strings

Framework-Agnostic Interface

// packages/i18n-shared/src/index.tsx
export interface Translator {
  (key: string, options?: Record<string, unknown>): string
}

export function I18nProvider({ t, children }: { t: Translator; children: React.ReactNode }) {
  return <I18nContext.Provider value={t}>{children}</I18nContext.Provider>
}

export function useT(): Translator {
  return React.useContext(I18nContext)
}

Translation Adapter

// packages/web/src/lib/translation-adapter.ts
export function useTranslationAdapter(): Translator {
  const { t: globalT } = useTranslation()

  return (key: string, options?: Record<string, unknown>) => {
    if (key.includes(".")) {
      const [namespace, ...keyParts] = key.split(".")
      return globalT(`${namespace}:${keyParts.join(".")}`, options)
    }
    return globalT(key, options)
  }
}

Usage Pattern

// Core component
import { useT } from "@screencam/i18n-shared"

export function HelloWorld() {
  const t = useT()
  return <Text>{t("components.helloWorld.title")}</Text>
}

// Web adapter (_app.tsx)
import { useTranslationAdapter } from "../lib"

function App({ Component, pageProps }: AppProps) {
  const t = useTranslationAdapter()
  return (
    <I18nProvider t={t}>
      <Component {...pageProps} />
    </I18nProvider>
  )
}

// Web page (getStaticProps)
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
  props: {
    ...(await serverSideTranslations(locale ?? "en", ["common", "components"])),
  },
})

Testing Setup

  • Framework: Vitest 3.1.4 with jsdom environment
  • Testing Library: React Testing Library + ChakraUI test utilities
  • Custom utilities: renderWithProvider with ChakraUI + I18n providers
  • Coverage: Component rendering, user interaction, i18n integration, accessibility, snapshots

Testing Pattern

// packages/core/src/test-utils.tsx
import { I18nProvider } from "@screencam/i18n-shared"

const mockT = (key: string) => {
  const translations: Record<string, string> = {
    "components.helloWorld.title": "TEST_HELLO_WORLD_TITLE",
    "components.helloWorld.welcomeMessage": "TEST_HELLO_WORLD_WELCOME_MESSAGE",
  }
  return translations[key] || key
}

export function renderWithProvider(ui: React.ReactElement) {
  return render(
    <I18nProvider t={mockT}>
      <ChakraProvider value={system}>{ui}</ChakraProvider>
    </I18nProvider>
  )
}

// Test usage
expect(screen.getByText("TEST_HELLO_WORLD_TITLE")).toBeInTheDocument()

Component Testing

Mock patterns for testing:

// Mock toaster
vi.mock("@screencam/core", () => ({
  toaster: { create: vi.fn() },
}))

// Mock dialog
const mockDialog = {
  alert: vi.fn(),
  confirm: vi.fn(),
  show: vi.fn(),
  close: vi.fn(),
}

vi.mock("@screencam/core", () => ({
  useDialog: () => mockDialog,
}))

Code Quality Setup

  • BiomeJS 1.9.4: All-in-one formatter, linter, and import organizer
  • Style: Double quotes, semicolons, 2-space indentation
  • Pre-commit hooks: BiomeJS runs automatically on git commit via Husky + lint-staged
  • VS Code integration: Auto-format on save with BiomeJS extension

Git Hooks Status

  • git commit → BiomeJS runs automatically (Husky + lint-staged)
  • VS Code → Auto-format on save always works
  • CI/CD → Use pnpm ci for strict checking without fixes

Known Issues & Workarounds

  • jj hooks: Jujutsu doesn't support git hooks natively - use manual pnpm pre-jj workflow
  • ChakraUI v3: Required specific configuration to work with NextJS 15 Pages Router
  • Vitest + React 19: Required specific JSX runtime configuration and optimized dependencies
  • Next-i18next: Uses CommonJS config file (next-i18next.config.js) for NextJS integration

File Structure

packages/web/
├── src/lib/translation-adapter.ts  # i18n bridge
├── src/pages/_app.tsx             # Provider composition
├── public/locales/en/             # Translation files
└── next.config.ts                 # NextJS + i18n config

packages/core/
├── src/test-utils.tsx            # Testing utilities with providers
└── vitest.config.ts              # Test configuration

packages/i18n-shared/
└── src/index.tsx                 # Framework-agnostic interface

packages/lab/
├── src/
│   ├── video-capture/            # WebCodecs video capture experiments
│   │   ├── webgl/               # WebGL rendering components
│   │   │   ├── WebGLCard.tsx    # Refactored with useWebGLInitialization hook
│   │   │   └── ...
│   │   └── ...
│   ├── encode-gl-context/        # WebGL encoding experiments
│   │   ├── webgl/               # WebGL components with fit modes
│   │   │   ├── WebGLCard.tsx    # Video fit modes implementation
│   │   │   ├── WebGLFitModeMenu.tsx
│   │   │   └── ...
│   │   └── ...
│   └── ...
└── package.json

Learning Mode

When the user says "Learning Mode" or is working in the 'lab' PNPM workspace:

  • Skip enterprise requirements: Ignore i18n, comprehensive testing, and deployment considerations
  • No internationalization: Use hardcoded strings instead of the i18n system
  • Minimal boilerplate: Focus on the core learning objectives without enterprise patterns
  • Single file start: Begin with everything in <experiment-name>/index.tsx to focus on learning concepts
  • Component evolution workflow:
    1. Create <experiment-name>/index.tsx with all components in one file
    2. Iterate until the concept is stable and working
    3. Break components into separate files once happy with functionality
    4. Convert index.tsx to an export file for the main component
    5. Add entry to LabSidebar

Critical Learning Mode Principle: Don't Hack Around Limitations

NEVER work around fundamental technical limitations with hacks or fallbacks when in learning mode. When encountering a limitation:

  1. Stop and understand WHY it doesn't work at a fundamental level
  2. Accept the limitation as valuable learning - knowing what doesn't work is as important as knowing what does
  3. Document the proper solution even if it can't be implemented with current tools
  4. Ask the user if they want to pivot to learning something else or explore the limitation deeper

Examples of unacceptable workarounds in learning mode:

  • Falling back to WebGL when WebGPU doesn't support something
  • Using screenshot APIs when direct frame capture doesn't work
  • Switching to a different library that "just works" without understanding why

The goal is LEARNING, not shipping features. A failed experiment that teaches fundamental concepts is more valuable than a working hack that obscures the underlying technology.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment