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 seeCLAUDE_BUILD=1 pnpm build
in the commands.
IMPORTANT: Run
pnpm check:fix
andpnpm 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 theCLAUDE_BUILD=1 pnpm build
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
- 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
AWS Amplify hosting with CDK infrastructure:
- Production: main branch → https://screen.cam
- Staging: stage branch → https://stage.screen.cam
- Key Amplify Config:
.npmrc
withnode-linker=hoisted
required for pnpm workspace builds - Dependencies: In packages/web, types moved from devDependencies to dependencies for Amplify build compatibility
# 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
- 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)
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.
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.
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
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>
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 },
],
})
Framework-agnostic i18n architecture with dependency inversion pattern.
i18n-shared
: ProvidesTranslator
interface anduseT()
hookcore
: Components depend only on the abstractionweb
: Adapts next-i18next to the interface viauseTranslationAdapter()
packages/web/public/locales/en/
├── common.json # Navigation, actions, meta content
└── components.json # Component-specific strings
// 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)
}
// 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)
}
}
// 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"])),
},
})
- 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
// 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()
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,
}))
- 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 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
- 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
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
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:
- Create
<experiment-name>/index.tsx
with all components in one file - Iterate until the concept is stable and working
- Break components into separate files once happy with functionality
- Convert
index.tsx
to an export file for the main component - Add entry to LabSidebar
- Create
NEVER work around fundamental technical limitations with hacks or fallbacks when in learning mode. When encountering a limitation:
- Stop and understand WHY it doesn't work at a fundamental level
- Accept the limitation as valuable learning - knowing what doesn't work is as important as knowing what does
- Document the proper solution even if it can't be implemented with current tools
- 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.