# Ripple TS Framework - AI/LLM Documentation ## Overview Ripple is a TypeScript UI framework that combines the best parts of React, Solid, and Svelte into one elegant package. Created by Dominic Gannaway ([@trueadm](https://github.com/trueadm)), Ripple is designed to be JS/TS-first with `.tsrx` as its default component file extension while preserving `.tsrx` compatibility. **Key Characteristics:** - **Performance**: Fine-grain rendering, with industry-leading performance, bundle-size and memory usage - **TypeScript-first**: Full TypeScript integration with type checking - **JSX-like syntax**: Familiar templating with TSRX-specific enhancements - **Reactive state**: Built-in reactivity with `track` and `&[]` lazy destructuring syntax - **Component-based**: Clean, reusable components with props and children ## Installation & Setup ```bash # Create new project from template npx degit Ripple-TS/ripple/templates/basic my-app cd my-app pnpm install && pnpm dev # Or install in existing project pnpm add ripple pnpm add -D '@ripple-ts/vite-plugin' # For Vite integration ``` ## Core Syntax & Concepts ### Component Definition Components are ordinary TypeScript functions that return TSRX. Use a capitalized function name, accept props as the first parameter, and return either a single TSRX element or a fragment. Direct calls keep ordinary helper semantics. A PascalCase helper such as `StatusCode()` or `FormatName()` is left as a normal function when called directly; component compilation applies to functions used as components or render entries, and to functions that return native TSRX without being directly called. ```ripple function Button(props: { text: string; onClick: () => void }) { return ; } export function App() { return ; } ``` Components receive `ref={...}` as a prop. Forward it explicitly or include it in a spread onto the host element that should be exposed. Named props such as `inputRef` are ordinary component API props; pass them into `ref={...}` inside the receiving component when you want to forward them. ```ripple function Field({ inputRef, ...rest }) { return } export function App() { let input: HTMLInputElement | null | undefined; return ; } ``` Use `createRefKey()` when refs need to be assembled programmatically and applied through object spread. ## Built-in APIs ### Core Functions ```typescript import { mount, // Mount component to DOM flushSync, // Synchronous state updates } from 'ripple'; ``` ### Mount API Use `mount()` to render a component to the DOM for client-side only applications: ```typescript import { mount } from 'ripple'; import { App } from './App.tsrx'; const cleanup = mount(App, { target: document.getElementById('root')!, props: { title: 'Hello world!' } }); // To unmount later: cleanup(); ``` ### Hydrate API Use `hydrate()` when your HTML was server-rendered and you need to make it interactive: ```typescript import { hydrate } from 'ripple'; import { App } from './App.tsrx'; const cleanup = hydrate(App, { target: document.getElementById('root')!, props: { title: 'Hello world!' } }); ``` **When to use each:** - `mount()`: Client-side only (SPA). Clears target and renders fresh DOM. - `hydrate()`: SSR apps. Adopts existing server-rendered HTML without re-creating elements. ### Server-Side Rendering On the server, use `render()` from `ripple/server`: ```typescript import { render } from 'ripple/server'; import { App } from './App.tsrx'; // Render to string const html = render(App, { props: { title: 'Hello world!' } }); ``` ### Effects ```ripple import { track, effect } from 'ripple'; export function App() { let &[count] = track(0); effect(() => { console.log("Count changed:", count); }); return ; } ``` ### After Update tick() The `tick()` function returns a Promise that resolves after all pending reactive updates have been applied to the DOM. This is useful when you need to ensure that DOM changes are complete before executing subsequent code, similar to Vue's `nextTick()` or Svelte's `tick()`. ```ripple import { tick, track, effect } from 'ripple'; export function App() { let &[count] = track(0); effect(() => { count; if (count === 0) { console.log('initial run, skipping'); return; } tick().then(() => { console.log('after the update'); }); }); return ; } ``` ### Context Ripple has the concept of `context` where a value or reactive object can be shared through the component tree – like in other frameworks. This all happens from the `Context` class that is imported from `ripple`. Creating contexts may take place anywhere. Contexts can contain anything including tracked values or objects. However, context cannot be read via `get` or written to via `set` inside an event handler or at the module level as it must happen within the context of a component. A good strategy is to assign the contents of a context to a variable via the `.get()` method during the component initialization and use this variable for reading and writing. When Child components overwrite a context's value via `.set()`, this new value will only be seen by its descendants. Components higher up in the tree will continue to see the original value. Example with tracked / reactive contents: ```ripple import { Context, track } from 'ripple'; // create context with an empty object const context = new Context({}); const context2 = new Context(); export function App() { // get reference to the object const obj = context.get(); // set your reactive value let &[objCount, objCountTracked] = track(0); obj.count = objCountTracked; // create another tracked variable let &[count2, count2Tracked] = track(0); // context2 now contains a tracked variable context2.set(count2Tracked); return <> // context's reactive property count gets updated
"Context: "{objCount}
"Context2: "{count2}
; } ``` Passing data between components: ```ripple import { Context } from 'ripple'; const MyContext = new Context(null); function Child() { // Context is read in the Child component const value = MyContext.get(); // value is "Hello from context!" console.log(value); return

"Value in Child: "{value}

; } function Parent() { const value = MyContext.get(); // Context is read in the Parent component, but hasn't yet // been set, so we fallback to the initial context value. // So the value is `null` console.log(value); // Context is set in the Parent component MyContext.set("Hello from context!"); return } ``` ### Reactive Collections #### Simple Reactive Array Just like objects, you can use the `Tracked` objects in any standard JavaScript object, like arrays: ```ripple import { track } from 'ripple'; let &[first, firstTracked] = track(0); let &[second, secondTracked] = track(0); const arr = [firstTracked, secondTracked]; const &[total] = track(() => arr.reduce((a, b) => a + b, 0)); console.log(total); ``` Like shown in the above example, you can compose normal arrays with reactivity and pass them through props or boundaries. However, if you need the entire array to be fully reactive, including when new elements get added, you should use the reactive array that Ripple provides. #### Fully Reactive Array `RippleArray` class from Ripple extends the standard JS `Array` class, and supports all of its methods and properties. Import `RippleArray` from `'ripple'` to use it. All elements existing or new of the `RippleArray` are reactive and respond to the various array operations such as push, pop, shift, unshift, etc. Even if you reference a non-existent element, once it added, the original reference will react to the change. ```ripple import { RippleArray } from 'ripple'; // using the constructor const arr = new RippleArray(1, 2, 3); // using static from method const arr = RippleArray.from([1, 2, 3]); // using static fromAsync method const arr = RippleArray.fromAsync([1, 2, 3]); // using static of method const arr = RippleArray.of(1, 2, 3); ``` Usage Example: ```ripple import { RippleArray } from 'ripple'; export function App() { const items = new RippleArray(1, 2, 3); return

"Length: "{items.length}

// Reactive length for (const item of items) {
{item}
}
; } ``` #### Reactive Object `RippleObject` class extends the standard JS `Object` class, and supports all of its methods and properties. Import `RippleObject` from `'ripple'` to use it. `RippleObject` fully supports shallow reactivity and any property on the root level is reactive. You can even reference non-existent properties and once added the original reference reacts to the change. ```ripple import { RippleObject } from 'ripple'; // using the constructor const arr = new RippleObject({a: 1, b: 2, c: 3}); ``` Usage Example: ```ripple import { RippleObject } from 'ripple'; export function App() { const obj = new RippleObject({a: 0}) obj.a = 0; return <>
"obj.a is: "{obj.a}
"obj.b is: "{obj.b}
; } ``` #### Reactive Set ```ripple import { RippleSet } from 'ripple'; function SetExample() { const mySet = new RippleSet([1, 2, 3]); return

"Size: "{mySet.size}

// Reactive size

"Has 2: "{mySet.has(2)}

; } ``` #### Reactive Map The `RippleMap` extends the standard JS `Map` class, and supports all of its methods and properties. ```ripple import { RippleMap } from 'ripple'; const map = new RippleMap([[1,1], [2,2], [3,3], [4,4]]); ``` RippleMap's reactive methods or properties can be used directly or assigned to reactive variables. ```ripple import { RippleMap, track } from 'ripple'; export function App() { const map = new RippleMap([[1,1], [2,2], [3,3], [4,4]]); return <> // direct usage

"Direct usage: map has an item with key 2: "{map.has(2)}

// reactive assignment let &[has] = track(() => map.has(2));

"Assigned usage: map has an item with key 2: "{has}

; } ``` #### Reactive Date The `RippleDate` extends the standard JS `Date` class, and supports all of its methods and properties. ```ripple import { RippleDate } from 'ripple'; const date = new RippleDate(2026, 0, 1); // January 1, 2026 ``` RippleDate's reactive methods or properties can be used directly or assigned to reactive variables. All getter methods (`getFullYear()`, `getMonth()`, `getDate()`, etc.) and formatting methods (`toISOString()`, `toDateString()`, etc.) are reactive and will update when the date is modified. ```ripple import { RippleDate, track } from 'ripple'; export function App() { const date = new RippleDate(2025, 0, 1, 12, 0, 0); return <> // direct usage

"Direct usage: Current year is "{date.getFullYear()}

"ISO String: "{date.toISOString()}

// reactive assignment let &[year] = track(() => date.getFullYear()); let &[month] = track(() => date.getMonth());

"Assigned usage: Year "{year}", Month "{month}

; } ``` ## Advanced Features ### Portal Component The `Portal` component allows you to render (teleport) content anywhere in the DOM tree, breaking out of the normal component hierarchy. This is particularly useful for modals, tooltips, and notifications. ```ripple import { Portal } from 'ripple'; export function App() { return

"My App"

{/* This will render inside document.body, not inside the .app div */}
; } ``` ### Untracking Reactivity ```ripple import { track, effect, untrack } from 'ripple'; let &[count] = track(0); let &[double] = track(() => count * 2); let &[quadruple] = track(() => double * 2); effect(() => { // This effect will never fire again, as we've untracked the only dependency it has console.log(untrack(() => quadruple)); }) ``` ### Prop Shortcuts ```ripple // Object spread
"Content"
// Shorthand props (when variable name matches prop name)
"Content"
// Equivalent to:
"Content"
``` ### Raw HTML All text nodes are escaped by default in Ripple. To render trusted raw HTML strings, use the native `innerHTML` prop: `
`. When raw markup must sit beside normal children, import `Fragment` from `ripple` and render ``. ```tsrx function Article({ markup }: { markup: string }) { return
; } ``` ### Text Expressions Direct double-quoted children are static escaped text. Dynamic text is just a normal `{expression}`. When you need explicit string coercion, write it in JavaScript with `String(value)`, `value + ''`, or a typed string value. ```ripple export function Frame({ children }) { return
"before" {children} "after"
; } ``` Regular text expressions are HTML-escaped by the target renderer. The content is never parsed as HTML unless you use the framework's raw HTML prop. ```ripple export function App() { const markup = 'Not HTML'; // Renders the literal string "Not HTML" as text return
{markup}
; } ``` ## TypeScript Integration ### Component Types ```typescript import type { Component } from 'ripple'; interface Props { value: string; label: string; children?: Component; } function MyComponent(props: Props) { return <> // Component implementation ; } ``` ### Context Types ```typescript import { Context } from 'ripple'; type Theme = 'light' | 'dark'; const ThemeContext = new Context('light'); ``` ## File Structure ``` src/ App.tsrx # Main app component components/ Button.tsrx # Reusable components Card.tsrx index.ts # Entry point with mount() ``` ## Development Tools ### VSCode Extension - **Name**: "Ripple for VS Code" - **ID**: `Ripple-TS.ripple-ts-vscode-plugin` - **Features**: Syntax highlighting, diagnostics, TypeScript integration, IntelliSense ### Vite Plugin ```typescript // vite.config.js import { defineConfig } from 'vite'; import ripple from '@ripple-ts/vite-plugin'; export default defineConfig({ plugins: [ripple()] }); ``` ### Prettier Plugin ```javascript // .prettierrc { "plugins": ["@tsrx/prettier-plugin"] } ``` ### TSRX MCP Server Use `@tsrx/mcp` when an AI coding tool supports Model Context Protocol (MCP) and needs target-aware TSRX help. The server exposes current TSRX documentation, target detection, project inspection, formatting, compilation, diagnostic analysis, and read-only file validation. Hosted endpoint: For hosted MCP apps and connectors, use: ```text https://mcp.tsrx.dev/mcp ``` This is the best starting point for ChatGPT web and other clients that connect to an HTTP MCP server instead of launching a local command. ChatGPT setup: Add `https://mcp.tsrx.dev/mcp` when creating a custom app or connector from ChatGPT developer mode, then select that app from the tools menu in a chat. Self-hosting: To self-host the endpoint, deploy the monorepo's `website-mcp` app anywhere that can run a Node HTTP server and connect remote MCP clients to its `/mcp` route. Local MCP server: For local repository work in Codex, Cursor, Gemini CLI, or Claude Code, install `@tsrx/mcp` as a stdio MCP server so the tool can inspect your project. Generic local MCP client config: ```json { "mcpServers": { "tsrx": { "command": "npx", "args": ["-y", "@tsrx/mcp"] } } } ``` Codex setup: ```toml # ~/.codex/config.toml [mcp_servers.tsrx] command = "npx" args = ["-y", "@tsrx/mcp"] ``` Gemini CLI setup: Put the generic `mcpServers` JSON in `.gemini/settings.json` for a project, or `~/.gemini/settings.json` globally. Cursor setup: Put the generic `mcpServers` JSON in `.cursor/mcp.json` for a project, or `~/.cursor/mcp.json` globally. Cursor Agent and `cursor-agent` discover the configured tools automatically. Claude Code setup: ```bash claude mcp add tsrx -- npx -y @tsrx/mcp ``` Recommended agent workflow: - Start with `inspect-project` to identify the active runtime target, installed TSRX packages, formatting/typecheck scripts, and likely project commands. - Use `detect-target` when only the runtime target is needed. - For generated source, run `format-tsrx`, then `compile-tsrx`. If compilation fails, run `analyze-tsrx`, apply the advice, format again, and compile again. - For existing `.tsrx` files, prefer `validate-tsrx-file`; it reads the file and runs formatting, compilation, and diagnostic advice in one read-only pass. - Fetch `tsrx://docs/{slug}.md` resources for target-neutral syntax details and `tsrx://targets/{target}.md` for runtime-specific handoff guidance. The MCP server is target-neutral. Runtime-specific imports, bundler setup, and framework semantics should come from the detected target package and its docs. ## Key Differences from Other Frameworks ### vs React - No wrapper components for control flow - returned templates support inline `if`, `for`, and `switch` - Built-in reactivity with `track` and `&[]` lazy destructuring syntax instead of useState/useEffect - Scoped CSS without CSS-in-JS libraries - No virtual DOM - fine-grained reactivity ### vs Svelte - TypeScript-first approach - JSX-like syntax instead of HTML templates - Default `.tsrx` extension instead of `.svelte` - Similar reactivity concepts but different syntax ### vs Solid - Components are ordinary functions that return TSRX - Built-in collections (RippleArray, RippleSet) - Returned templates support statement-style control flow ## Best Practices 1. **Reactivity**: Use `track()` (imported from `'ripple'`) with `&[]` lazy destructuring to create reactive variables 2. **Strings**: Use direct double-quoted children for static text, and `{...}` for JavaScript expressions 3. **Effects**: Use `effect()` (imported from `'ripple'`) for side effects, not direct reactive variable access 4. **Components**: Keep components focused and use TypeScript interfaces for props 5. **Styling**: Use scoped static `