# Ripple - AI/LLM Documentation ## Overview Ripple is a TypeScript-first UI framework and runtime for `.tsrx` files. TSRX is the shared source language; Ripple is the target that provides fine-grained reactivity, DOM rendering, server modules, hydration, context, portals, and reactive collections. When answering Ripple questions: - Prefer `.tsrx` component examples. - Return direct JSX for single-root components: `function Component(props) { return
; }`. - Use JSX statement containers (`@{...}`) when TypeScript setup belongs next to the rendered output. - Use JSX text for static text and `{expr}` for dynamic values. - When a statement container mixes TypeScript setup with rendered output, put setup first and finish it with one JSX element, JSX fragment, or JSX control-flow expression. It cannot finish with a bare expression container, and script statements cannot appear after the final output. - Use `@if`, `@for`, `@switch`, and `@try` for rendered control flow. Their bodies are implicit statement containers and must use `{...}` blocks. - Keep runtime API guidance Ripple-specific, but keep syntax guidance aligned with TSRX. For target-neutral TSRX syntax, see https://tsrx.dev/llms.txt. ## Install ```bash npm install ripple @ripple-ts/vite-plugin ``` ```ts // vite.config.ts import { defineConfig } from 'vite'; import ripple from '@ripple-ts/vite-plugin'; export default defineConfig({ plugins: [ripple()], }); ``` Mount client apps with `mount()`. ```ts import { mount } from 'ripple'; import { App } from './App.tsrx'; mount(App, { target: document.getElementById('root'), props: { title: 'Hello world' }, }); ``` `mount()` renders the app under a default `try`/`pending`/`catch` boundary. Pass `rootBoundary: { pending?, catch? }` to give it components, or `rootBoundary: false` to render without one: `trackAsync()` must then sit inside a user `@try` block, and errors that escape a `@try` propagate out of the flush. Use `hydrate()` for server-rendered HTML that should become interactive. ```ts import { hydrate } from 'ripple'; import { App } from './App.tsrx'; hydrate(App, { target: document.getElementById('root'), }); ``` ## Components Components are TypeScript functions. Return a JSX element directly when there is one root, use a fragment for real multiple-child output, and use a JSX statement container (`@{...}`) when setup statements belong next to the UI. ```tsrx export function Button({ text, onClick }: { text: string; onClick: () => void }) { return ; } export function App() { return } ``` If output needs multiple siblings, text, or expression containers after setup, wrap that output in a fragment. Plain text between tags is JSX text, not JavaScript. ```tsrx export function LiteralText() { return
x = 123
; } ``` ## Reactivity Create reactive state with `track()` and lazy destructuring. Reading a lazy binding subscribes to it; assigning to it updates the tracked value. ```tsrx import { effect, track, type Tracked } from 'ripple'; export function Counter() @{ let &[count, countTracked] = track(0); let &[double] = track(() => count * 2); effect(() => { console.log('count changed', count); }); <>

Count: {count}

Double: {double}

} function CounterValue({ count }: { count: Tracked }) { return

Shared value: {count.value}

; } ``` You can also keep the tracked object and read or write `.value` directly. ```tsrx import { track } from 'ripple'; export function Counter() @{ const count = track(0); } ``` Use `untrack(fn)` when an effect or derived value needs to read something without subscribing to it. Use `snapshot(value)` to take a plain, detached shallow copy of a reactive array or object. Values are read without subscribing, so it is safe to call inside an effect or derived. The copy is a plain array or object (not a proxy); nested values are shared by reference. ```tsrx import { RippleObject, snapshot } from 'ripple'; export function Settings() @{ const settings = new RippleObject({ theme: 'dark', fontSize: 14 }); } ``` ## Reactive Collections Use Ripple collection classes when collection operations should update rendered output. ```tsrx import { RippleArray, RippleMap, RippleObject, RippleSet } from 'ripple'; export function Inventory() @{ const products = new RippleArray( { id: 1, name: 'Jacket' }, { id: 2, name: 'Boots' }, ); const prices = new RippleMap([[1, 120], [2, 95]]); const selected = new RippleSet(); const totals = new RippleObject({ added: 0 }); <>
    @for (const product of products; key product.id) {
  • {product.name}: ${prices.get(product.id)}
  • }

Selected: {selected.size + totals.added}

} ``` Available collection APIs include `new RippleArray(...)`, `RippleArray.from(...)`, `RippleArray.of(...)`, `new RippleObject(...)`, `new RippleMap(...)`, and `new RippleSet(...)`. ## Template Control Flow Rendered control flow uses directive expressions. ```tsrx import { track } from 'ripple'; export function StatusBadge() @{ let &[status] = track<'idle' | 'loading' | 'done'>('idle'); <> @switch (status) { @case 'loading': {

Loading...

} @case 'done': {

Done

} @default: {

Idle

} } } ``` Use `@for (... of ...)` for rendered lists. Filter the iterable before rendering when some items should be skipped, and use `@empty { ... }` for the no-items fallback. Direct `continue`, `break`, and `return` statements are not allowed in `@for` template loop bodies, and are also invalid inside `@if` template branches. `@switch` cases use `@case` and `@default` clauses with isolated `{...}` blocks. They do not fall through, and do not use `break` or `return`. ```tsrx export function UserList({ users }: { users: User[] }) @{ const visibleUsers = users.filter((user) => !user.hidden);
    @for (const user of visibleUsers; index i; key user.id) {
  • {i + 1}. {user.name}
  • } @empty {
  • No users
  • }
} ``` Use ordinary TypeScript `return` for true component exits in setup code. ```tsrx export function Profile({ user }: { user: User | null }) @{ if (!user) { return null; }

{user.name}

} ``` `@try` provides pending and error UI. ```tsrx export function ProfileBoundary() @{ @try { } @pending {

Loading...

} @catch (error, reset) {

Error: {error.message}

} } ``` ## Events And Refs Events use JSX-style handler props. DOM refs can be callback refs, tracked refs, or local variables. ```tsrx import { track } from 'ripple'; export function SearchBox() @{ let &[query] = track(''); let input: HTMLInputElement | undefined; <> } ``` ## Dynamic Components And Elements Use the dynamic tag syntax `<{expression}>` when the element tag or component constructor is chosen at runtime. The expression can be a string tag name, a component, or a tracked variable holding either. The closing tag repeats the same expression: ``. Ripple host elements use `class`, not React's `className`. ```tsrx import { track } from 'ripple'; type Tag = 'section' | 'article'; function Summary() { return

Summary

; } function Details() { return
Details
; } export function DynamicPanel() @{ let &[tag] = track('section' as Tag); let &[Body] = track(() => Summary); <> <{tag} class="panel"> <{Body} /> } ``` The tag expression must resolve to an element name: an identifier, member access, static string, or a runtime expression composed of those. Calls, spreads, string concatenation, and string interpolation are not valid tag names. Do not use removed dynamic tag syntax such as `<@tag />`, `<@Component />`, or the imported `Dynamic` component with an `is` prop. Use `<{tag}>` instead. ## Styles A `

Card

} ``` - A block lives in the children list of an element or fragment, beside the output it styles. A `@{ ... }` body or an `@if`/`@for`/`@switch`/`@try` branch renders one output node and a block counts as one, so wrap the block and its markup in a fragment: `<>
`. - Raw CSS in ``, an ordinary element the compiler leaves alone. - Blocks among the same children share one hash class and one stylesheet. A nested element or fragment whose children hold a block is a nested scope with its own hash; elements carry the hash of every scope around them, outer first. Outer rules reach nested scopes; inner rules never reach out. - A block inside an `@if`/`@for` branch styles only the elements that branch renders, but its CSS always ships, because CSS is static. - A selector that matches nothing the block can reach, including a selector that only matches the containing element, is removed as unused and left as a `/* (unused) ... */` comment. - A scoped block accepts only the `ref` and `apply` attributes. - Use `:global(...)` to reach outside the scope: `:global(.x)` is page-wide, `.card :global(.x)` reaches only below your scoped `.card`, `:global(.dark) .card` reacts to an ancestor's class, `.card:global(.is-open)` matches a class another library toggles. `:global { ... }` does the same for every rule inside it. `:global` may only start or end a selector. Prefer passing a class from a theme to a child you own; keep `:global` for children you cannot change, with a scoped selector in front: | I want to ... | Use ... | | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Style my own elements | A `; // theme.$class -> 'tsrx-1a2b3c4d' // theme.dark -> 'tsrx-1a2b3c4d dark' export function Badge() { return New; } ``` - A block that is exported, applied, or whose `$class` is read is a **theme** and keeps every selector, element and descendant selectors included. A block only read through its class keys keeps only its standalone class selectors. - `$class` is reserved; a standalone `` applies the theme and declares the scope's own block in one tag. ```tsrx import { theme } from './theme.tsrx'; export function Panel() @{ <> Purple
Black: the local rule beats the theme's green
} ``` - `apply={[a, b]}` applies several themes; `const accent = `) .replace('', body); ``` ### Streaming SSR With `stream` set, `render` flushes a shell immediately: all synchronous content, each suspended `@try` boundary showing its `@pending` fallback, and all CSS registered so far. As each boundary's async work settles, its HTML streams as a self-contained chunk — carrying its own CSS, serialized `trackAsync` results, and `` content — and an inline runtime swaps it into place. Chunks arrive out of order, parents always before children. Catch-only boundaries stream an empty slot that later resolves to the body or the server-rendered `@catch` HTML. `hydrate()` handles both arrival orders: content swapped before hydration hydrates normally, and later chunks activate their boundary in place without re-rendering. ```ts import { render, createStream } from 'ripple/server'; const { stream, sink } = createStream(); render(App, { stream: sink, rootBoundary: { pending: Loading }, streamTemplate: { before: '', between: '
', after: '
', }, }); return new Response(stream, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); ``` Apps built on `@ripple-ts/vite-plugin` enable streaming for render routes in `ripple.config.ts` instead of calling `render` directly: ```ts export default { ssr: { streaming: true, }, }; ``` `index.html` must keep the `` and `` markers; the plugin falls back to buffered SSR with a warning when they are missing. ## Raw HTML Use `innerHTML={trustedHtml}` for trusted markup. Do not pass untrusted strings to raw HTML props. ```tsrx export function Article({ markup }: { markup: string }) { return
; } ``` ## Do And Do Not Do: - Use `.tsrx` for component files. - Return single-root components directly in examples. - Use `@{...}` when setup appears next to rendered output, and finish the statement container with one JSX element, JSX fragment, or JSX control-flow expression. - Use JSX text for literal children. - Use `@for` for rendered lists and `@if` for conditional rendering. - Use Ripple collections for reactive collection state. - Put a `