diff --git a/.claude/agents/recipe-dev-guide.md b/.claude/agents/recipe-dev-guide.md index 1f2d84909..be86c17ec 100644 --- a/.claude/agents/recipe-dev-guide.md +++ b/.claude/agents/recipe-dev-guide.md @@ -7,7 +7,7 @@ color: orange You are an expert CommonTools recipe development guide specializing in helping users create, modify, and network recipes using the ct utility. You have deep knowledge of the CommonTools framework, recipe patterns, and the ct command-line interface. **Critical Prerequisites**: -- You MUST first read `.claude/commands/common/ct.md` for CT binary setup instructions +- You MUST first read `docs/common/CT.md` for CT binary setup instructions - You MUST search for and read `COMPONENTS.md` and `RECIPES.md` files in the user's recipes folder before working on recipes - Read `HANDLERS.md` when encountering event handler errors - The user should have already run the space setup script or have an existing space diff --git a/.claude/agents/research-specialist.md b/.claude/agents/research-specialist.md index b7c92403c..d9fcf4844 100644 --- a/.claude/agents/research-specialist.md +++ b/.claude/agents/research-specialist.md @@ -7,7 +7,7 @@ color: pink You are a research specialist with expertise in systematic codebase investigation and technical analysis. Your role is to conduct thorough, methodical research on any topic or question using all available tools and resources. -**CRITICAL FIRST STEP**: Before doing anything else, you MUST read .claude/commands/common/ct.md to understand how to use the CommonTools system properly. +**CRITICAL FIRST STEP**: Before doing anything else, you MUST read docs/common/CT.md to understand how to use the CommonTools system properly. **Your Research Methodology**: diff --git a/.claude/commands/imagine-recipe.md b/.claude/commands/imagine-recipe.md index 45433c587..8c0406193 100644 --- a/.claude/commands/imagine-recipe.md +++ b/.claude/commands/imagine-recipe.md @@ -6,10 +6,10 @@ This script guides Claude through creating new CommonTools recipes based on user **Before starting recipe imagination:** - User should have an existing space or have run the space setup script -- Claude MUST read the common CT setup instructions in `.claude/commands/common/ct.md` +- Claude MUST read the common CT setup instructions in `docs/common/CT.md` **Recipe Documentation Reference:** -Before working on recipes, search for these documentation files in the user's `recipes` folder: +Before working on recipes, search for these documentation files in the `docs/common` folder: - `RECIPES.md` - Core recipe development patterns and examples - `COMPONENTS.md` - Available UI components and usage patterns - `HANDLERS.md` - Event handler patterns and troubleshooting @@ -21,7 +21,7 @@ The user provides an initial prompt describing what they want their recipe to do ### STEP 1: Initial Setup and Context **Read common setup instructions:** -- First, read `.claude/commands/common/ct.md` for shared CT binary setup +- First, read `docs/common/CT.md` for shared CT binary setup - Follow those instructions for CT binary check, identity management, environment setup - Collect required parameters (API URL, space name, recipe path, identity file) diff --git a/.claude/commands/onboarding.md b/.claude/commands/onboarding.md index dc094d8c2..902cfd32f 100644 --- a/.claude/commands/onboarding.md +++ b/.claude/commands/onboarding.md @@ -14,7 +14,7 @@ This command provides an interactive tour of the Common Tools platform repositor "Now that you have the basic idea, what would you like to know more about: - **Programs that run in this platform** (recipes, charms, UI components) -- **The runtime that enables information flow analysis and storage** (runner, builder, storage) +- **The runtime that enables information flow analysis and storage** (runner, builder, storage) - **The application layer that users access the platform through** (toolshed, shell, CLI) - **The LLM tooling layer** (Claude commands, subagents, and development workflows) @@ -53,7 +53,7 @@ Based on the user's choice in Step 2, follow these focused exploration paths: **Explore in order:** 1. **Toolshed backend** - Quote from `packages/toolshed/README.md` about hosted platform 2. **Shell frontend** - Quote from `packages/shell/README.md` about user interface -3. **CT CLI** - Overview from `.claude/commands/common/ct.md` +3. **CT CLI** - Overview from `docs/common/CT.md` 4. **How they work together** - Integration points and data flow 5. **Development workflow** - Running local development environment @@ -71,7 +71,7 @@ Based on the user's choice in Step 2, follow these focused exploration paths: **Throughout any path, users can access:** - **Available commands**: List `.claude/commands/` and read `.claude/commands/README.md` -- **Integration setup**: Review `deps.md` for tools and MCP integrations +- **Integration setup**: Review `deps.md` for tools and MCP integrations - **Development guidelines**: Reference `CLAUDE.md` for coding standards - **Research commands**: Use `/research` to dive deeper into specific areas @@ -79,7 +79,7 @@ Based on the user's choice in Step 2, follow these focused exploration paths: **Users can switch paths or dive deeper:** - From Recipe Development → explore Runtime internals -- From Runtime → understand Application layer integration +- From Runtime → understand Application layer integration - From Application layer → try Recipe development - Or combine multiple paths based on curiosity @@ -96,7 +96,7 @@ Based on the user's choice in Step 2, follow these focused exploration paths: **Critical: This is a guided discovery experience, not a fire-hose lecture:** - **Start tiny and build** - Don't read multiple files at once. Start with one small section -- **Quote small chunks** - Show 2-3 sentences from files, not entire sections +- **Quote small chunks** - Show 2-3 sentences from files, not entire sections - **Wait for their reaction** - After each quote, ask what they think and WAIT for response - **Follow their energy** - Only show more based on what sparked their curiosity - **Never charge ahead** - Resist the urge to show everything; be patient and responsive @@ -104,7 +104,7 @@ Based on the user's choice in Step 2, follow these focused exploration paths: **Example flow:** 1. Read just the "What is Common Tools?" section from README.md -2. Quote the first paragraph: "Common Tools is a new distributed computing platform..." +2. Quote the first paragraph: "Common Tools is a new distributed computing platform..." 3. Ask: "What's your first reaction to this?" 4. Wait for user response 5. Based on their response, show ONLY the next relevant small piece @@ -115,4 +115,4 @@ Based on the user's choice in Step 2, follow these focused exploration paths: - Don't summarize what you found after reading files - Don't offer too many paths; the four focused paths are sufficient based on their actual interest -**Key principle:** The user should feel like they're discovering things themselves with Claude as a helpful guide, not receiving a presentation. \ No newline at end of file +**Key principle:** The user should feel like they're discovering things themselves with Claude as a helpful guide, not receiving a presentation. diff --git a/.claude/commands/recipe-dev.md b/.claude/commands/recipe-dev.md index 9f6ddd136..dff78ce4a 100644 --- a/.claude/commands/recipe-dev.md +++ b/.claude/commands/recipe-dev.md @@ -6,7 +6,7 @@ This script guides Claude through recipe development with the `ct` utility after **Before starting recipe development:** - User should have already run the space setup script or have an existing space -- Claude MUST read the common CT setup instructions in `.claude/commands/common/ct.md` +- Claude MUST read the common CT setup instructions in `docs/common/CT.md` ## Script Flow for Claude @@ -27,7 +27,7 @@ This script guides Claude through recipe development with the `ct` utility after ### STEP 1: Initial Setup and Context (ONLY if no config provided) **Read common setup instructions:** -- First, read `.claude/commands/common/ct.md` for shared CT binary setup +- First, read `docs/common/CT.md` for shared CT binary setup - Follow those instructions for: - CT binary check - Identity management @@ -42,7 +42,7 @@ This script guides Claude through recipe development with the `ct` utility after ### STEP 2: Recipe Development Workflows -Before working on recipes it is recommended to search for `COMPONENTS.md` and `RECIPES.md` files in the `recipes` folder provided by the user. +Before working on recipes it is recommended to search for `COMPONENTS.md` and `RECIPES.md` files in the `docs/common` folder. Read `HANDLERS.md` when confused about event handler errors. diff --git a/.claude/commands/research.md b/.claude/commands/research.md index b1599a77a..cf30db840 100644 --- a/.claude/commands/research.md +++ b/.claude/commands/research.md @@ -15,7 +15,7 @@ Task: Research [topic/question] You are a research specialist. Conduct thorough investigation of the topic using all available tools. -**First, learn how to use ct:** Read .claude/commands/common/ct.md to understand how to use the CommonTools system. +**First, learn how to use ct:** Read docs/common/CT.md to understand how to use the CommonTools system. **Your Task:** 1. **Consult the wiki first** - Read .claude/commands/search-wiki.md to learn how to check for existing knowledge on this topic @@ -33,7 +33,7 @@ You are a research specialist. Conduct thorough investigation of the topic using ## Research Methodology ### Core Steps -- **Learn ct usage first** - Read .claude/commands/common/ct.md to understand CommonTools +- **Learn ct usage first** - Read docs/common/CT.md to understand CommonTools - **Start with wiki search** to avoid duplicating previous research - Use Task tool for systematic codebase exploration - Check recent git history and commits @@ -55,7 +55,7 @@ You are a research specialist. Conduct thorough investigation of the topic using After research is complete, you MUST ask: "Would you like me to deploy this as a CommonTools research report?" -If yes, use the .claude/commands/deploy-research.md command. Make sure to read .claude/commands/common/ct.md first to understand how to use the CommonTools system properly. +If yes, use the .claude/commands/deploy-research.md command. Make sure to read docs/common/CT.md first to understand how to use the CommonTools system properly. ## When to Use diff --git a/.claude/commands/search-wiki.md b/.claude/commands/search-wiki.md index 22662cd06..a4ca13478 100644 --- a/.claude/commands/search-wiki.md +++ b/.claude/commands/search-wiki.md @@ -18,7 +18,7 @@ You are a wiki search specialist. Your job is to search the project wiki for rel - Wiki Charm ID: baedreigkqfmhscbwwfhkjxicogsw3m66nxbetlhlnjkscgbs56hsqjrmkq **Your Task:** -0. **First, learn how to use ct:** Read .claude/commands/common/ct.md to understand how to use the CommonTools system. +0. **First, learn how to use ct:** Read docs/common/CT.md to understand how to use the CommonTools system. 1. Get all wiki content: `./dist/ct charm get --identity claude.key --api-url https://toolshed.saga-castor.ts.net/ --space 2025-wiki --charm baedreigkqfmhscbwwfhkjxicogsw3m66nxbetlhlnjkscgbs56hsqjrmkq wiki` diff --git a/.claude/commands/setup-space.md b/.claude/commands/setup-space.md index 1d8015478..a0ea967f2 100644 --- a/.claude/commands/setup-space.md +++ b/.claude/commands/setup-space.md @@ -7,7 +7,7 @@ This script guides Claude through setting up a complete space with the `ct` util ### STEP 1: Initial Setup Check and Preparation **Read common setup instructions:** -- First, read `.claude/commands/common/ct.md` for shared CT binary setup and configuration +- First, read `docs/common/CT.md` for shared CT binary setup and configuration - Follow those instructions for: - Checking if user is in the right directory (should be in `labs`) - CT binary check and build if needed @@ -78,7 +78,7 @@ This script guides Claude through setting up a complete space with the `ct` util ### Error Handling **General error handling:** -- Refer to error handling section in `.claude/commands/common/ct.md` for common issues +- Refer to error handling section in `docs/common/CT.md` for common issues - Don't continue to dependent steps until current step works **Space setup specific errors:** @@ -149,7 +149,7 @@ echo '[{"name": "Item 1"}, {"name": "Item 2"}]' | ./dist/ct charm set --identity ## Reference Information for Claude ### Key Commands and Linking Concepts: -- See `.claude/commands/common/ct.md` for: +- See `docs/common/CT.md` for: - Complete list of CT commands - Understanding linking syntax and concepts - Examples of charm-to-charm and well-known ID linking @@ -164,7 +164,7 @@ echo '[{"name": "Item 1"}, {"name": "Item 2"}]' | ./dist/ct charm set --identity - Charms list in any space: `baedreiahv63wxwgaem4hzjkizl4qncfgvca7pj5cvdon7cukumfon3ioye` ### Space Setup Specific Notes: -- See `.claude/commands/common/ct.md` for general troubleshooting +- See `docs/common/CT.md` for general troubleshooting - Recipe files needed for initial setup: - simple-list.tsx - gmail.tsx diff --git a/.claude/commands/update-wiki.md b/.claude/commands/update-wiki.md index 380e37e70..b4d629f44 100644 --- a/.claude/commands/update-wiki.md +++ b/.claude/commands/update-wiki.md @@ -14,7 +14,7 @@ When you need to update the wiki, follow these steps: **Process:** -0. **First, learn how to use ct:** Read .claude/commands/common/ct.md to understand how to use the CommonTools system. +0. **First, learn how to use ct:** Read docs/common/CT.md to understand how to use the CommonTools system. 1. Choose appropriate page key following naming conventions: - Solutions: `[problem-type]-solution` or `fix-[specific-issue]` diff --git a/AGENTS.md b/AGENTS.md index 36678c049..181d9ccd0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ The instructions in this document apply to the entire repository. ### `ct` & Common Tools Framework -Before ever calling `ct` you MUST read `.claude/commands/common/ct.md`. +Before ever calling `ct` you MUST read `docs/common/CT.md`. ### Recipe Development diff --git a/README.md b/README.md index d30fe6b34..82f79edc0 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,7 @@ written in Deno2, that provides the distributed runtime and storage. Lit Web Components for interacting with CommonTools spaces. **CLI (CT Binary)**: Command-line interface for managing charms, linking -recipes, and deploying to spaces. See -[CT Usage Guide](./.claude/commands/common/ct.md). +recipes, and deploying to spaces. See [CT Usage Guide](./docs/common/CT.md). **UI Components ([packages/ui](./packages/ui))**: Custom VDOM layer and `ct-` prefixed components for recipe UIs. @@ -55,7 +54,8 @@ prefixed components for recipe UIs. recipes and common patterns for building with CommonTools. **Recipe Development**: Recipes can be developed using LLM assistance with -commands like `/imagine-recipe`, `/recipe-dev`, and `/explore-recipe`. +commands like `/imagine-recipe`, `/recipe-dev`, and `/explore-recipe`. See +[Recipe Documentation](./docs/common/) for patterns, components, and handlers. ## Development & Integrations diff --git a/docs/common/COMPONENTS.md b/docs/common/COMPONENTS.md new file mode 100644 index 000000000..5e5059bd9 --- /dev/null +++ b/docs/common/COMPONENTS.md @@ -0,0 +1,186 @@ +# ct-button + +A styled button component matching the regular `button` API. Pass a handler to the `onClick` prop to bind it. + +```tsx +type InputSchema = { count: Cell }; +type OutputSchema = { count: Cell }; + +const MyRecipe = recipe("MyRecipe", ({ count }) => { + const handleClick = handler }>((_event, { count }) => { + count.set(count.get() + 1); + }); + + return { + [UI]: , + count, + }; +}); +``` + +Notice how handlers are bound to the cell from the input schema _in_ the VDOM declaration? That's partial application of the state, the rest of the state (the actual event) comes through as the (unused) `_event` in the handler. This way you can merge the discrete updates from events with the reactive cells that are always changing values. + +(For even more detail, see `HANDLERS.md`) + +# ct-input + +Some components can work with a cell directly, as well as using handlers. + +```tsx +type InputSchema = { value: Cell }; +type OutputSchema = { value: Cell }; + +const MyRecipe = recipe("MyRecipe", ({ value }) => { + // Here we know the type of the event and can use the `detail` to get the value + const handleChange = handler<{ detail: { value: string } }, { value: Cell }>((event, { value }) => { + value.set(event.detail.value); + }); + + return { + [UI]:
+ + +
, + }; +}); +``` + +These two inputs are functionally equivalent. They both update the cell with the value from the input event, providing a cell via the `$` prefix allows the component to call `.set()` on the cell internally - reducing boilerplate. + +### Validation + +One reason to prefer a handler is validation of the value. This is a good idea, but you can _also_ consider using two cells. A raw input cell and a validated cell derived from the former, e.g. + +```tsx +type InputSchema = { rawValue: Cell }; +type OutputSchema = { validatedValue: string | null }; + +const MyRecipe = recipe("MyRecipe", ({ rawValue }) => { + // Example 1: Using full event object + // Here we destructure the event for convenience/brevity + const handleChange = handler<{ detail: { value: string } }, { rawValue: Cell }>(({ detail: { value } }, { rawValue }) => { + rawValue.set(value); + }); + + // Example 2: Destructuring specific event properties (often cleaner) + const handleChangeDestructured = handler<{ detail: { value: string } }, { rawValue: Cell }>(({ detail: { value } }, { rawValue }) => { + rawValue.set(value); + }); + + // Example 3: When event data isn't needed + const handleReset = handler }>((_ , { rawValue }) => { + rawValue.set(""); + }); + + const validatedValue = derive(rawValue, v => v.length > 0 ? v : null); + + return { + [UI]:
+ + Reset +
, + validatedValue + }; +}); +``` + +# ct-list + +When working with a list of objects, of any kind, if they have `title` properties you can display and manage them via a `ct-list` component. + +```tsx +type Item = { title: string }; +type ListSchema = { items: Cell }; + +const MyRecipe = recipe("MyRecipe", ({ items }) => { + return { + [UI]:
+ +
, + items, + }; +}); +``` + +# ct-message-input + +This component bundles an input and a button to 'send a message' or 'add an item to a list' which is a common pattern. You don't need to worry about the value until submission time. + +```tsx +const addItem = handler< + { detail: { message: string } }, + { list: { title: string; items: Cell } } +>(({ detail: { message } }, { list }) => { + const item = message?.trim(); + if (item) list.items.push({ title: item }); +}); + +// ... + + +```` + +# ct-outliner + +`ct-outliner` is conceptually similar to `ct-list` except it works on a tree data structure. Below is a demonstration of the minimal use, see `page.tsx` for a more complete example. + +This example also demonstrates verbose specification of more complex types. + +```tsx +import { + h, + derive, + handler, + ifElse, + NAME, + recipe, + str, + UI, + OpaqueRef, + Cell, + Default, + Opaque, +} from "commontools"; + +type Charm = any; + +type OutlinerNode = { + body: Default; + children: Default; + attachments: Default[], []>; +}; + +type Outliner = { + root: OutlinerNode; +}; + +type PageResult = { + outline: Cell< + Default + >; +}; + +export type PageInput = { + outline: Cell; +}; + +export default recipe( + "Outliner Page", + ({ outline }) => { + return { + [NAME]: "Outliner", + [UI]: ( +
+ + +
+ ), + outline, + }; + }, +); +``` diff --git a/.claude/commands/common/ct.md b/docs/common/CT.md similarity index 100% rename from .claude/commands/common/ct.md rename to docs/common/CT.md diff --git a/docs/common/HANDLERS.md b/docs/common/HANDLERS.md new file mode 100644 index 000000000..651d6bd7a --- /dev/null +++ b/docs/common/HANDLERS.md @@ -0,0 +1,332 @@ +# CommonTools Handler Patterns Guide + +## Handler Function Structure + +Handlers in CommonTools follow a specific three-parameter pattern: + +```typescript +const myHandler = handler( + eventSchema, // What data comes from UI events + stateSchema, // What data the handler needs to operate on + handlerFunction // The actual function that executes +); + +// or + +type EventSchema = ... +type StateSchema = ... +const myHandler = handler(handlerFunction); +``` + +## Common Handler Patterns + +### 1. Simple Click Handler (No Event Data) + +```typescript +const increment = handler, { count: Cell }>( + (_event, { count }) => { + count.set(count.get() + 1); + } +); + +// Usage in UI + + +1 + +``` + +### 2. Event Data Handler (Form Input, etc.) + +```typescript +const updateTitle = handler< + { detail: { value: string } }, + { title: Cell } +>( + ({ detail }, { title }) => { + title.set(detail.value); + } +); + +// Usage in UI + + +// OR, bind directly to cell (see component docs for when this is available) + + +``` + +## Common TypeScript Issues & Fixes + +### Handler Function Parameters +- **First parameter**: Event data (from UI interactions) +- **Second parameter**: State data (destructured from state schema) +- **Parameter naming**: Use descriptive names or `_` prefix for unused parameters + +Notice how handlers are bound to the cell from the input schema _in_ the VDOM declaration? That's partial application of the state, the rest of the state (the actual event) comes through as `e` in the handler. This way you can merge the discrete updates from events with the reactive cells that are always changing values. + +#### Event Parameter Patterns + +You can handle the event parameter in different ways depending on your needs: + +```typescript +// Option 1: Destructure specific event properties (most common) +const updateTitle = handler< + { detail: { value: string } }, + { title: Cell } +>( + ({ detail }, { title }) => { + title.set(detail.value); + } +); + +// Option 2: Use full event object when you need multiple properties +const handleComplexEvent = handler< + { detail: { value: string }, target: HTMLElement, timestamp: number }, + { data: Cell } +>( + (e, { data }) => { + // Access multiple event properties + console.log('Event timestamp:', e.timestamp); + console.log('Target element:', e.target); + data.set({ value: e.detail.value, element: e.target.tagName }); + } +); + +// Option 3: Use underscore when event data isn't needed +const simpleIncrement = handler, { count: Cell }>( + (_, { count }) => { + count.set(count.get() + 1); + } +); +``` + +## Handler Invocation Patterns + +### State Passing +When invoking handlers in UI, pass an object that matches your state schema: + +```typescript +// Handler definition state schema +{ + items: any[], + currentPage: string +} + +// Handler invocation - must match state schema +onClick={myHandler({ items: itemsArray, currentPage: currentPageString })} +``` + +### Event vs Props Confusion +- **Event type**: Describes data coming FROM the UI event +- **State type**: Describes data the handler needs to ACCESS +- **Invocation object**: Must match the state type, NOT the event type + +## Debugging Handler Issues + +### Type Mismatches +1. Check that handler invocation object matches state type +2. Verify event type matches actual UI event structure +3. Ensure destructuring in handler function matches types + +### Runtime Issues +1. Use `console.log` in handler functions to debug +2. Check that state objects have expected properties +3. Verify UI events are firing correctly + +## Best Practices + +1. **Use meaningful parameter names**: `(formData, { items, title })` not `(event, state)` +2. **Keep event types minimal**: Often `Record` for simple clicks +3. **Make state types explicit**: Always define the exact properties needed +4. **Match invocation to state type**: The object passed to handler() should match state type exactly +5. **Prefer descriptive handler names**: `updateItemTitle` not `handleUpdate` + +## Examples by Use Case + +### Counter/Simple State +```typescript +const increment = handler, { count: Cell }>( + (_, { count }) => count.set(count.get() + 1) +); +``` + +### Form/Input Updates +```typescript +const updateField = handler< + { detail: { value: string } }, + { fieldValue: Cell } +>( + ({ detail }, { fieldValue }) => fieldValue.set(detail.value) +); +``` + +### List/Array Operations +```typescript +const addItem = handler< + { message: string }, + { items: Cell> } +>( + ({ message }, { items }) => + items.push({ title: message, done: false }) +); +``` + +### Complex State Updates +```typescript +const updatePageContent = handler< + { detail: { value: string } }, + { pages: Record, currentPage: string } +>( + ({ detail }, { pages, currentPage }) => { + pages[currentPage] = detail.value; + } +); +``` + +## Advanced Patterns + +### Handler Composition +```typescript +// Base handlers for reuse +const createTimestamp = () => Date.now(); + +const addItemWithTimestamp = handler< + { title: string }, + { items: Cell> } +>( + ({ title }, { items }) => { + items.push({ + title, + done: false, + createdAt: createTimestamp() + }); + } +); +``` + +### Conditional State Updates +```typescript +const toggleItem = handler< + Record, + { item: { id: string }, items: Cell> } +>( + (_, { item, items }) => { + const index = items.get().findIndex(i => i.id === item.id); + if (index !== -1) { + items.get()[index].done = !items.get()[index].done; + } + } +); +``` + +#### Improving Event Typing + +While `Record` works for simple handlers that don't use event data, better event typing improves development experience and catches errors: + +```typescript +// Better: Define specific event interfaces when you need event data +interface ClickEvent { + target: HTMLElement; + shiftKey?: boolean; + metaKey?: boolean; +} + +interface CustomEvent { + detail: T; + target: HTMLElement; +} + +// Use specific types for better IntelliSense and error catching +const handleItemClick = handler< + ClickEvent, + { items: Cell> } +>( + ({ target, shiftKey }, { items }) => { + const itemId = target.getAttribute('data-item-id'); + if (itemId) { + // Type-safe access to event properties + const shouldSelectMultiple = shiftKey; + // ... handler logic + } + } +); + +// For custom events with specific detail shapes +const handleFormSubmit = handler< + CustomEvent<{ formData: Record }>, + { submissions: Cell } +>( + ({ detail }, { submissions }) => { + // detail.formData is properly typed + submissions.push(detail.formData); + } +); +``` + +## Common Pitfalls + +### 1. Type Mismatch +```typescript +// ❌ Wrong: State type doesn't match invocation +const handler = handler, { count: number }>( + (_, { count }) => { ... } +); + +// Invocation passes wrong shape +onClick={handler({ value: 5 })} // Should be { count: 5 } +``` + +### 2. Event Type Over-specification +```typescript +// ❌ Wrong: Over-complicated event type for simple clicks +const handler = handler<{ + target: object, + currentTarget: object +}, any>( + (ev, state) => { ... } +); + +// ✅ Better: Simple clicks rarely need event data +const handler = handler, any>( + (ev, state) => { ... } +); +``` + +### 3. Mutation vs Immutability +```typescript +// ❌ Wrong: Direct assignment to non-cell state +(_, { title }) => { + title = newValue; // This won't work +} + +// ✅ Right: Use cell methods for reactive state +(_, { title }) => { + title.set(newValue); // For cells +} + +// ✅ Right: Mutate arrays/objects directly for non-cell state +(_, { items }) => { + items.push(newItem); // For regular arrays +} +``` + +## Debugging Handlers + +### Console-based Debugging +```typescript +// In recipes, use console logging for debugging +const addItem = handler< + { detail: { value: string } }, + { items: Cell } +>( + ({ detail }, { items }) => { + console.log("Adding item:", detail.value); + console.log("Current items:", items.get()); + items.push({ title: detail.value, done: false }); + console.log("Updated items:", items.get()); + } +); +``` diff --git a/docs/common/RECIPES.md b/docs/common/RECIPES.md new file mode 100644 index 000000000..0d472b20f --- /dev/null +++ b/docs/common/RECIPES.md @@ -0,0 +1,659 @@ +# Recipe Framework Documentation + +## Overview + +The Recipe Framework is a declarative, reactive system for building +integrations and data transformations. It uses a component-based architecture +where recipes are autonomous modules that can import, process, and export data. + +## Core Concepts + +### Recipe + +A recipe is the fundamental building block, defined using the `recipe('My Recipe', (input) => {})` +function. It takes two types and two parameters: + +- Types: define Input and Output relationships for composition with other recipes + - Properties such as `[UI]` and `[NAME]` do not have to be explicitly included in the output type +- Parameters: a descriptive name and a function that receives the inputs and returns + outputs + +### Types and Runtime Safety + +The framework uses a TypeScript-first approach for defining types in recipes, handlers, and lifted functions: + +```typescript +const myHandler = handler( + (input, state) => {/* ... */}, +); +``` + +This TypeScript-first approach provides several benefits: + +- Full TypeScript type inference and checking +- Clean, readable type definitions +- Integration with IDE tooling +- Express Cell vs readonly value requirements directly in types + +Importantly, the framework automatically handles type validation and serialization using the CTS (Common Tools TypeScript) system. This gives you runtime validation, self-documentation, and serialization support. You can express your desire for Cells vs readonly values directly in TypeScript types, and the system will fulfill the values by reflecting on the types. + +### Data Flow + +The framework uses a reactive programming model: + +- `cell`: Represents a reactive state container that can be updated and observed +- `derive`: Creates a derived value that updates when its dependencies change +- `lift`: Similar to derive, but lifts a regular function to work on reactive values + - `derive(param, function)` is an alias to `lift(function)(param)` +- `handler`: Creates an event handler that always fires with up-to-date dependencies (possibly mutating them) + - takes two parameters, an event and state (bound variables) + - e.g. `` + +### Handlers vs Reactive Functions + +There are important differences between the types of functions in the framework: + +#### Handlers + +(For even more detail, see `HANDLERS.md`) + +Handlers are functions that declare node types in the reactive graph that +respond to events: + +- Created with `handler()` function +- Use `Cell<>` to indicate you want a reactive value (for mutation, usually): + + ```typescript + const updateCounter = handler }>( + (input, { count }) => { + // Now count is a Cell instance + count.set(count.get() + 1); + }, + ); + ``` + +- Instantiated in recipes by passing parameters: + + ```typescript + const stream = definedHandler({ cell1, cell2 }); + ``` + +- Return a stream that can be: + - Passed to JSX components as event handlers (e.g., `onClick={stream}`) + - Returned by a recipe for external consumption + - Passed to another handler which can call `.send(...)` on it to generate + events +- Can update cells and trigger side effects +- Support async operations for data processing +- React to outside events (user interactions, API responses) +- Cannot directly call built-in functions like `llm` + +#### Reactive Functions (lift/derive) + +- `lift`: Declares a reactive node type that transforms data in the reactive + graph + + ```typescript + const transformData = lift( + ({ value, multiplier }: { value: number, multiplier: number }) => value * multiplier, + ); + ``` + + - When instantiated, it inserts a reactive node in the graph: + + ```typescript + const newCell = liftedFunction({ cell1, cell2 }); + ``` + + - The result is a proxy cell that can be further referenced: + + ```typescript + const compound = { data: newCell.field }; + ``` + +- `derive`: A convenience wrapper around lift: + + ```typescript + // These are equivalent: + const result1 = derive({ x, y }, ({ x, y }) => x + y); + const result2 = lift(({ x, y }) => x + y)({ x, y }); + ``` + +- React to data changes within the reactive graph +- Cannot directly call built-in functions like `llm` + +### Data as Futures + +Within recipes, functions cannot directly read values - they can only pass +references to other nodes. Think of the data passed to a recipe as "futures" - +promises of values that will be available when the program runs. + +The system allows accessing fields using the dot notation (e.g., `cell.field`), +but this doesn't actually read values - it's creating new references to future +data. + +```tsx +// This doesn't read values, it creates references: +const data = { + firstName: user.firstName, + lastName: user.lastName, +}; +``` + +### UI Components + +Recipes can include UI components using JSX syntax: + +- Common components like `ct-input`, `ct-hstack`, `ct-vstack` +- Integration-specific components like `common-google-oauth` +- Custom components can be created as needed via `const MyComponent = recipe(...)` `` + +### Built-in Functions + +Several utility functions are available: + +- `llm`: Makes calls to language models with parameters for system prompt, user + prompt, etc. +- `fetchData`: Fetches data from URLs +- `streamData`: Streams data from URLs +- `ifElse`: Conditional logic for reactive flows + + ```typescript + // Creates a reactive value that changes based on the condition + const message = ifElse( + user.isLoggedIn, + str`Welcome back, ${user.name}!`, + "Please log in to continue", + ); + ``` + +- `str`: Template literal for string interpolation with reactive values, + creating reactive strings + + ```typescript + // Creates a reactive string that updates when cells change + const greeting = + str`Hello, ${user.name}! You have ${notifications.count} new messages.`; + ``` + +**Important**: These built-in functions can only be called from within a recipe +function, not from handlers, lift, or derive functions. They create nodes in the +reactive graph and cannot be awaited directly. + +## JSX and Reactive Arrays + +The Recipe Framework has an interesting approach to handling arrays with JSX: + +```typescript +{ + items.map((item) => ); +} +``` + +While this looks like regular JSX mapping, in the Recipe framework, this +actually creates mini-recipes for each item in the array, constructing a +reactive graph. Each mapped item becomes a reactive node that updates when the +source data changes. + +In the todo-list example, this pattern is used to create draggable todo items, +where each item has its own encapsulated recipe: + +```typescript +{items.map((item: TodoItem) => ( + ({ + [UI]: ( + + ), + })), + )} + > + + +))} +``` + +This approach allows for efficient updates and encapsulation of item-specific +logic. + +## Best Practices + +1. **Use CTS TypeScript Types**: Define clear TypeScript interfaces for your + input and output schemas. This provides runtime validation, self-documentation, + and compatibility with framework tooling through the CTS (Common Tools TypeScript) system. + +2. **Use `Cell<>` for Handler State**: When defining handler state types, use `Cell<>` for properties that need to be updated. This gives you direct access to the Cell methods like `.set()` and `.get()`. + +3. **Avoid All Direct Conditionals in Recipes**: Never use direct if statements, + ternary operators, or any other conditionals inside a recipe function - they + won't work properly because they immediately evaluate data instead of + creating reactive nodes: + + ```typescript + // DON'T DO THIS - if statements don't work in recipes + const result = emails.map((email) => { + if (email.hasContent) { // This won't work! + return processEmail(email); + } else { + return { email, empty: true }; + } + }); + + // DON'T DO THIS EITHER - ternary operators also don't work + const tableHeader = ( + + Name + {settings.showDetails ? Details : null} // This won't work! + + ); + + // DON'T DO THIS - ternaries in string templates don't work + const prompt = str` + Process this data + ${ + settings.includeTimestamp ? "Include timestamps" : "No timestamps" + } // This won't work! + `; + + // DO THIS INSTEAD - use ifElse function for conditionals in data flow + const result = emails.map((email) => + ifElse( + email.hasContent, + () => processEmail(email), + () => ({ email, empty: true }), + ) + ); + + // USE ifElse IN JSX TOO + const tableHeader = ( + + Name + {ifElse(settings.showDetails, Details, null)} + + ); + + // USE ifElse IN STRING TEMPLATES + const includeTimestampText = ifElse( + settings.includeTimestamp, + "Include timestamps", + "No timestamps", + ); + const prompt = str` + Process this data + ${includeTimestampText} + `; + + // WHEN APPROPRIATE - skip conditionals entirely + // and let LLM handle edge cases: + const result = emails.map((email) => { + const processed = processWithLLM(email); + return { email, result: processed }; + }); + ``` + +4. **Reference Data Instead of Copying**: When transforming data, reference the + original objects rather than copying all their properties. This maintains + reactivity and creates cleaner code: + + ```typescript + // DO THIS: Reference the original data + const processedItems = items.map((item) => ({ + originalItem: item, // Direct reference + processed: processItem(item), + })); + + // NOT THIS: Spread/copy all properties + const processedItems = items.map((item) => ({ + id: item.id, // Copying each field + name: item.name, // breaks the reactive + date: item.date, // connection to the + // ... more fields // original data + processed: processItem(item), + })); + ``` + +5. **Use Reactive String Templates**: Use the `str` template literal to create + reactive strings that update when their inputs change: + + ```typescript + const message = + str`Hello ${user.name}, you have ${notifications.count} notifications`; + ``` + +6. **Keep Logic Inside Recipes**: Place as much logic as possible inside recipe + functions or the `map` function. This creates a cleaner reactive system where + data flow is transparent. + +7. **Leverage Framework Reactivity**: Let the framework track changes and + updates. Avoid manually tracking which items have been processed or creating + complex state management patterns. + +8. **Composition**: Build complex flows by composing smaller recipes. + +9. **Minimize Side Effects**: Side effects should be managed through handlers + rather than directly in recipes. + +10. **Type Reuse**: Define types once and reuse them across recipes, handlers, and lifted functions to maintain consistency. + +## Type Best Practices + +When defining types in the Recipe Framework, follow these guidelines for best +results: + +1. **Define Types as Reusable Interfaces**: Declare types as interfaces or type aliases for reuse and + reference: + + ```typescript + type User = { + id: string; + name: string; + email?: string; + }; + ``` + +2. **Use Descriptive Type Names**: Always use clear, descriptive names that explain what the type represents: + + ```typescript + // DO THIS + type UserPreferences = { + theme: "light" | "dark" | "system"; + fontSize: number; + notifications: boolean; + }; + + // NOT THIS + type Config = { theme: string; size: number; alerts: boolean }; + ``` + +3. **Use Default<> for Sensible Defaults**: Where possible, provide sensible default values using the CTS Default<> type: + + ```typescript + type UserSettings = { + theme: Default<"light" | "dark" | "system", "system">; + fontSize: Default; + notifications: Default; + }; + ``` + +4. **Compose Types Instead of Duplicating**: For complex objects, compose existing types: + + ```typescript + type User = { + id: string; + name: string; + email: string; + }; + + type Post = { + author: User; // Reference the existing type + content: string; + timestamp: Date; + }; + ``` + +5. **Document Types with Comments**: Add JSDoc comments to types for better self-documentation: + + ```typescript + /** User's primary email address used for notifications */ + type Email = string; + + type User = { + id: string; + name: string; + /** User's primary email address used for notifications */ + email?: Email; + }; + ``` + +6. **Use Cell<> for Reactive State**: In handler state types, use `Cell<>` for properties that need direct access to Cell methods: + + ```typescript + type HandlerState = { + /** Counter value that can be directly manipulated */ + counter: Cell; + readonlyValue: string; // Regular values are readonly + }; + ``` + +7. **Make Optional vs Required Explicit**: Use TypeScript's optional properties (?) to clearly indicate what's required: + + ```typescript + type User = { + id: string; // Required + name: string; // Required + email?: string; // Optional + }; + ``` + +8. **Use Type Composition**: Break down complex types into smaller, reusable parts: + + ```typescript + type Address = { + street: string; + city: string; + zipCode: string; + }; + + type Contact = { + phone?: string; + email: string; + }; + + type User = { + // Basic info + id: string; + name: string; + // Composed types + address: Address; + contact: Contact; + }; + ``` + +9. **Use Union Types for Enums**: Define enums with union types and const assertions: + + ```typescript + const StatusValues = ["pending", "active", "suspended", "deleted"] as const; + type Status = typeof StatusValues[number]; // "pending" | "active" | "suspended" | "deleted" + + type User = { + id: string; + name: string; + status: Default; // Provide a reasonable default + }; + ``` + +## Advanced Type Concepts + +### TypeScript to Runtime Schema + +The CTS framework automatically handles your TypeScript types at runtime: + +```typescript +// Define TypeScript types +type Person = { + name: string; + age?: number; +}; + +// The framework automatically processes this TypeScript type +// No manual schema definition needed - it's all handled by CTS reflection +``` + +### Cell Type vs Value Type + +When working with handlers, it's important to understand the distinction: + +```typescript +// Regular state property (value type) +{ count: number } // Handler receives the number directly (readonly) + +// Cell-typed property +{ count: Cell } // Handler receives Cell for mutation +``` + +With Cell-typed properties, the handler function receives actual Cell instances +with methods: + +- `cell.get()`: Get the current value +- `cell.set(newValue)`: Set a new value +- `cell.update(fn)`: Update using a function + +This allows for more control over state updates, including: + +- Batching multiple updates +- Conditional updates based on current value +- Handling complex state transitions + +## Framework Goals + +The primary goal of this framework is to generate "low taint code" that enables +effective data flow analysis. In this system: + +- Recipes are transparent to data flow analysis +- Functions passed to `handler` or `lift` aren't transparent (they're "tainted") +- TypeScript types provide clean abstractions that are automatically converted to runtime validation +- The `Cell<>` pattern maintains reactivity while allowing direct manipulation + +By following these principles, applications built with this framework can +achieve predictable data flow, easier testing, and better security through data +flow isolation. + +## Integration Process + +1. Define schemas for inputs and outputs +2. Create cells to hold state +3. Implement handlers for user interactions +4. Return an object with processed data and UI components + +## Example Pattern + +```typescript +export default recipe( + InputSchema, + OutputSchema, + ({ input1, input2 }) => { + const state = cell([]); + + // Define handlers and side effects + + return { + [NAME]: "Recipe Name", + [UI]: ( + // JSX component + ), + outputField1: state, + outputField2: derivedValue + }; + } +); +``` + +## Integration Between Recipes + +Recipes can be composed together, where the output of one recipe serves as the +input to another. This is done by: + +1. Defining a common data schema between recipes +2. Exporting data from the source recipe +3. Importing that data as input in the consuming recipe + +For example, our Email Summarizer recipe takes emails from the Gmail recipe as +input: + +- Gmail recipe exports an array of emails +- Email Summarizer recipe consumes these emails and processes them with LLM + +## LLM Integration + +The framework provides integration with language models through the `llm` +function: + +```typescript +const result = llm({ + system: "System prompt here", // Instructions for the LLM + prompt: "User prompt here", // Content to process + // Optional parameters + stop: "custom stop sequence", // Stop generation at this sequence + max_tokens: 1000, // Max tokens to generate +}); +``` + +**Important restrictions**: + +1. The `llm` function can only be called directly within a recipe function, not + in handlers, lift, or derive functions +2. You cannot `await` the result directly, as it's a node in the reactive graph +3. To use LLM results, you need to access them through the reactive graph (e.g., + via derive) + +The result object includes: + +- `result`: The generated text +- `partial`: Streaming partial results +- `pending`: Boolean indicating if the request is still processing +- `error`: Any error that occurred + +## Reactive Processing Pattern + +A common pattern in recipes is: + +1. Initialize state using `cell()` +2. Create derived values with `derive()` +3. Define handlers for UI events and data processing +4. Create async functions for complex operations +5. Return a recipe object with UI and exported values + +## Example Pattern for Data Transformation Recipes + +```typescript +export default recipe( + "Recipe Name", + ({ inputData, settings }) => { + // Initialize state + const processedData = cell([]); + + // Process data with LLM (directly in recipe) + // Notice we call map() directly on the cell - inputData is a cell + const processedItems = inputData.map(item => { + return { + originalItem: item, + llmResult: llm({ + system: "System prompt", + prompt: `Process this: ${item.content}`, + }) + }; + }); + + // Create derived value from LLM results + const summaries = derive(processedItems, items => + items.map(item => ({ + id: item.originalItem.id, + summary: item.llmResult.result || "Processing...", + })) + ); + + // Handler for user interactions + const refreshData = handler }>( + (_, state) => { + // Update state based on user action + // Note: Cannot call llm() directly here + } + ); + + return { + [NAME]: "Recipe Name", + [UI]: ( + // JSX UI component + ), + processedData, + }; + } +); +``` diff --git a/packages/cli/commands/init.ts b/packages/cli/commands/init.ts index edd21bc3c..23e59e31d 100644 --- a/packages/cli/commands/init.ts +++ b/packages/cli/commands/init.ts @@ -3,6 +3,7 @@ import { Engine } from "@commontools/runner"; import { join } from "@std/path/join"; import { getCompilerOptions } from "@commontools/js-runtime/typescript"; import { StaticCache } from "@commontools/static"; +import { dirname } from "@std/path/dirname"; export const init = new Command() .name("init") @@ -75,6 +76,8 @@ function createTsConfig() { // containing types for the imported stdlib (`commontools`), the // implicitly loaded jsx-runtime (`react/jsx-runtime`) and the // environment types (`commontoolsenv`) loaded by the `tsconfig.json`. +// +// Also copies recipe documentation from docs/common to .ct-docs for reference. async function initWorkspace(cwd: string) { const cache = new StaticCache(); const runtimeModuleTypes = await Engine.getRuntimeModuleTypes( @@ -115,4 +118,37 @@ async function initWorkspace(cwd: string) { join(cwd, "tsconfig.json"), JSON.stringify(createTsConfig(), null, 2), ); + + // Copy recipe documentation files to .ct-docs folder + try { + const ctDocsPath = join(cwd, ".ct-docs"); + await Deno.mkdir(ctDocsPath, { recursive: true }); + + // In compiled binary, docs are bundled and accessible relative to the binary location + const currentFilePath = import.meta.url; + const currentDir = dirname(new URL(currentFilePath).pathname); + const docsCommonPath = join(currentDir, "..", "..", "..", "docs", "common"); + + // Copy each documentation file dynamically + try { + for await (const entry of Deno.readDir(docsCommonPath)) { + if (entry.isFile) { + const sourcePath = join(docsCommonPath, entry.name); + const targetPath = join(ctDocsPath, entry.name); + const content = await Deno.readTextFile(sourcePath); + await Deno.writeTextFile(targetPath, content); + } + } + } catch (dirError) { + console.warn( + "Warning: Could not read docs directory:", + dirError instanceof Error ? dirError.message : String(dirError), + ); + } + } catch (error) { + console.warn( + "Warning: Could not copy recipe documentation:", + error instanceof Error ? error.message : String(error), + ); + } } diff --git a/packages/patterns/chat.tsx b/packages/patterns/chat.tsx index 7f727e1d7..d2b8a4e7d 100644 --- a/packages/patterns/chat.tsx +++ b/packages/patterns/chat.tsx @@ -100,7 +100,7 @@ export default recipe( /> Clear Chat diff --git a/packages/ui/src/v2/components/ct-button/ct-button.ts b/packages/ui/src/v2/components/ct-button/ct-button.ts index f850d52d4..0f8ca7c5f 100644 --- a/packages/ui/src/v2/components/ct-button/ct-button.ts +++ b/packages/ui/src/v2/components/ct-button/ct-button.ts @@ -14,10 +14,8 @@ import { BaseElement } from "../../core/base-element.ts"; * * @slot - Default slot for button content * - * @fires ct-click - Fired on click with detail: { variant, size } - * * @example - * Click Me + * console.log('Button clicked')}>Click Me */ export type ButtonVariant = @@ -220,12 +218,6 @@ export class CTButton extends BaseElement { if (this.type !== "button") { return; } - - // Emit custom event - this.emit("ct-click", { - variant: this.variant, - size: this.size, - }); } } diff --git a/tasks/build-binaries.ts b/tasks/build-binaries.ts index 3bc25dd4f..38a6fcc53 100755 --- a/tasks/build-binaries.ts +++ b/tasks/build-binaries.ts @@ -92,6 +92,10 @@ class BuildConfig { return this.path("packages", "static", "assets", "types"); } + docsCommonPath() { + return this.path("docs", "common"); + } + cliEntryPath() { return this.path("packages", "cli", "mod.ts"); } @@ -285,6 +289,8 @@ async function buildCli(config: BuildConfig): Promise { "--allow-net", // for @db/sqlite lazy download "--include", config.staticTypesPath(), + "--include", + config.docsCommonPath(), config.cliEntryPath(), ], cwd: config.root,