# 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 its own `.ripple` file extension that fully supports TypeScript. **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 Ripple-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 defined using the `component` keyword (not functions that return JSX): ```ripple component Button(props: { text: string, onClick: () => void }) { } // Usage export component App() { } ``` **IMPORTANT:** You must use `track()` with lazy destructuring `&[...]` to create reactive variables: ```ripple import { track } from 'ripple'; // CORRECT: let &[count] = track(0); // Create with track() and &[] destructuring count++; // Read/write directly // Access the tracked ref as the second element: let &[count, countTracked] = track(0); ``` #### Accessing Tracked Values with `.value` As an alternative to lazy destructuring, you can read and write a tracked value directly using the `.value` property on the `Tracked` object: ```ripple import { track } from 'ripple'; const count = track(0); // Read the current value console.log(count.value); // 0 // Write a new value count.value++; console.log(count.value); // 1 ``` Using `&[...]` lazy destructuring is preferred in most cases because it produces cleaner, more readable code. However, `.value` is useful when you need to keep the `Tracked` object around — for example, when storing tracked values in data structures, passing them as props typed as `Tracked`, or when you need both the tracked object and its value in different contexts: ```ripple import { track } from 'ripple'; // Storing tracked values in an array — use .value to read/write const items = [track(1), track(2), track(3)]; items[0].value++; // reactively updates // Passing tracked objects as props — the child receives the Tracked object component Child(props: { count: Tracked }) {
{props.count.value}
} // Using &[value, trackedValue] gives you both: let &[count, countTracked] = track(0); count++; // convenient direct access via lazy destructuring console.log(countTracked.value); // equivalent: read via .value on the tracked object ``` Objects can also contain tracked values — use lazy destructuring to access them: ```ripple import { track } from 'ripple'; let counter = { current: track(0) }; let &[current] = counter.current; current++; // Triggers reactivity ``` Tracked derived values are also `Tracked` objects, except you pass a function to `track` rather than a value: ```ripple import { track } from 'ripple'; let &[count] = track(0); let &[double] = track(() => count * 2); let &[quadruple] = track(() => double * 2); console.log(quadruple); ``` If you want to use a tracked value inside a reactive context, such as an effect but you don't want that value to be a tracked dependency, you can use `untrack`: ```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)); }) ``` > Note: you cannot create `Tracked` objects in module/global scope, they have to be created on access from an active component context. #### track with get / set The optional get and set parameters of the `track` function let you customize how a tracked value is read or written, similar to property accessors but expressed as pure functions. The get function receives the current stored value and its return value is exposed when the tracked value is read via `&[]` destructuring. The set function should return the value that will actually be stored and receives two parameters: the first is the one being assigned and the second with the previous value. The get and set functions may be useful for tasks such as logging, validating, or transforming values before they are exposed or stored. ```ripple import { track } from 'ripple'; export component App() { let &[count] = track(0, (current) => { console.log(current); return current; }, (next, prev) => { console.log(prev); if (typeof next === 'string') { next = Number(next); } return next; } ); } ``` > Note: If no value is returned from either `get` or `set`, `undefined` is either exposed (for get) or stored (for set). Also, if only supplying the `set`, the `get` parameter must be set to `undefined`. #### Lazy Destructuring (`&{...}` / `&[...]`) Lazy destructuring uses the `&` prefix directly before `{` or `[` in a destructuring pattern. Instead of eagerly pulling values out of the source object, lazy destructuring compiles each variable access to a deferred property/index lookup on the source. This preserves reactivity for reactive props and other tracked objects. ```ripple // Lazy object destructuring const &{ a, b } = props; // Compiles to: a → props.a, b → props.b (accessed lazily) // Lazy array destructuring const &[first, second] = items; // Compiles to: first → items[0], second → items[1] // With default values const &{ x = 10 } = props; // Compiles to: x → _$_.fallback(props.x, 10) // With rest patterns const &{ a, ...rest } = props; // Compiles to: a → props.a, rest → { ...props, a: undefined } (lazily) ``` **Component props** — use `&{...}` to lazily destructure props, preserving reactivity: ```ripple component Child(&{ count, className, children }: Props) { // count, className, children are lazily read from __props
{`Count is: ${count}`}
} ``` **Function parameters** — works in regular functions too: ```ripple function process(&{ x, y }: Point) { return x + y; // lazily reads from the parameter object } ``` **Variable declarations** — lazy destructuring works with `const`, `let`, and `var`: ```ripple const &{ a, b } = someObject; // read-only lazy access let &{ x, y } = mutableObject; // supports assignment: x = 5 writes back to mutableObject.x ``` > **When to use lazy destructuring**: Use `&{...}` whenever you destructure reactive props or tracked objects and need the variables to remain reactive. Regular destructuring (`{ a, b } = obj`) eagerly copies values and loses reactivity. ### Transporting Reactivity **Critical Concept**: Ripple doesn't constrain reactivity to components only. `Tracked` objects can simply be passed by reference between boundaries to improve expressivity and co-location. #### Basic Transport Pattern ```ripple import { track, effect } from 'ripple'; function createDouble(&[count]) { const &[double] = track(() => count * 2); effect(() => { console.log('Count:', count) }); return double; } export component App() { let &[count, countTracked] = track(0); const &[double] = createDouble(countTracked);
{'Double: ' + double}
} ``` #### Dynamic Component Transport Pattern Ripple has built-in support for dynamic components, a way to render different components based on reactive state. Instead of hardcoding which component to show, you can store a component in a `Tracked` via `track()`, and update it at runtime. When the tracked value changes, Ripple automatically unmounts the previous component and mounts the new one. Dynamic components are written with the `<@Component />` tag, where `@` is a special marker that tells the compiler the component or element is dynamic — it does not dereference or unwrap the value. The expression after `@` (e.g., `swapMe` in `<@swapMe />`) is treated as a `Tracked` value, and the runtime handles unwrapping it internally. This makes it straightforward to pass components as props or swap them directly within a component, enabling flexible, state-driven UIs with minimal boilerplate. ```ripple import { track } from 'ripple'; export component App() { let &[swapMe, swapMeTracked] = track(() => Child1); } component Child(&{ swapMe }: {swapMe: Tracked}) { <@swapMe /> } component Child1(props) {
{'I am child 1'}
} component Child2(props) {
{'I am child 2'}
} ``` **Transport Rules:** - Reactive state must be connected to a component - Cannot be global or created at module/global scope - Use arrays `[ trackedVar ]` or objects `{ trackedVar }` to transport reactivity - Functions can accept and return reactive state using these patterns - This enables composable reactive logic outside of component boundaries ### Control Flow #### If Statements ```ripple component Conditional({ isVisible }) {
if (isVisible) { {"Visible content"} } else { {"Hidden state"} }
} ``` #### Switch Statements Switch statements in Ripple provide a powerful way to conditionally render content based on the value of an expression. They are fully reactive and integrate seamlessly with Ripple's templating syntax. **Key Features:** - **Reactivity:** Works with both static and reactive (`Tracked`) values. - **Control Flow:** Full support of standard JS with `break` statements and fall-through's. - **Template Integration:** `case` blocks can contain any valid Ripple template content, including other components, elements, and logic. **Basic Usage:** The `switch` statement evaluates an expression and matches its value against a series of `case` clauses. ```ripple component StatusIndicator({ status }) { // The switch statement evaluates the 'status' prop switch (status) { case: 'init': // fall-through to the next case 'loading':

{'Loading...'}

break; // break is mandatory case 'success':

{'Success!'}

break; case 'error':

{'Error!'}

break; default:

{'Unknown status'}

// No break needed for default } } ``` `switch` statements can also react to changes in `Tracked` variables. When the tracked variable changes, the `switch` statement will re-evaluate and render the appropriate `case`. ```ripple import { track } from 'ripple'; component InteractiveStatus() { let &[status] = track('loading'); // Reactive state // This switch block will update automatically when 'status' changes switch (status) { case 'init':

{'Status: Init'}

// fall-through to the next case 'loading':

{'Status: Loading...'}

break; case 'success':

{'Status: Success!'}

break; case 'error':

{'Status: Error!'}

break; default:

{'Status: Unknown'}

} } ``` #### For Loops ```ripple component List({ items }) {
    for (const item of items) {
  • {item.text}
  • }
} ``` #### For Loops with index ```ripple component ListView({ title, items }) {

{title}

    for (const item of items; index i) {
  • {item.text}{' at index '}{i}
  • }
} ``` #### For Loops with key ```ripple component ListView({ title, items }) {

{title}

    for (const item of items; index i; key item.id) {
  • {item.text}{' at index '}{i}
  • }
} ``` **Key Usage Guidelines:** - **Arrays with `RippleObject` instances**: Keys are usually unnecessary - object identity and reactivity handle updates automatically. Identity-based loops are more efficient with less bookkeeping. - **Arrays with plain objects**: Keys are needed when object reference isn't sufficient for identification. Use stable identifiers: `key item.id`. #### Dynamic Elements ```ripple import { track } from 'ripple'; export component App() { let &[tag] = track('div'); <@tag class="dynamic">{'Hello World'} } ``` #### Try-Catch (Error Boundaries) ```ripple component ErrorBoundary() {
try { } catch (e) {
{"Error: "}{e.message}
}
} ``` ### Children Components Use the `children` prop for component composition. Use the `Children` type to accept one or more children. If you need to pass additional child components such as `Header`, `Footer`, or `InlineComp`, define them in scope and pass them as explicit props on the component element. Do not declare `component Foo() {}` directly inside another component's child content. ```ripple import type { Children, Component } from 'ripple'; component Card(props: { children: Children; Footer?: Component }) {
{props.children} if (props.Footer) { }
} component Footer() { } // Usage

{"Card content here"}

``` ### Events #### Attribute Event Handling Events follow React-style naming (`onClick`, `onPointerMove`, etc.): ```ripple import { track } from 'ripple'; component EventExample() { let &[message] = track("");
message = e.target.value} />

{message}

} ``` For capture phase events, add `Capture` suffix: - `onClickCapture` - `onPointerDownCapture` #### Direct Event Handling Use function `on` to attach events to window, document or any other element instead of addEventListener. This method guarantees the proper execution order with respect to attribute-based handlers such as `onClick`, and similarly optimized through event delegation for those events that support it. ```ripple import { on, effect } from 'ripple'; export component App() { effect(() => { // on component mount const removeListener = on(window, 'resize', () => { console.log('Window resized!'); }); // return the removeListener when the component unmounts return removeListener; }); } ``` ### Styling Components support scoped CSS with ` } ``` #### Style Scoping Styles defined in a ` } component Child() {
{"I won't be red"}
} ``` #### Global Styles with `:global()` To escape scoping and apply styles globally (or to reach into child components), use the `:global()` modifier: ```ripple component Parent() {
} ``` The `:global()` modifier can wrap: - A single selector: `:global(.class-name)` - Multiple selectors: `:global(.foo, .bar)` - Part of a selector chain: `.scoped :global(.unscoped) .also-scoped` ``` #### Dynamic Classes In Ripple, the `class` attribute can accept more than just a string — it also supports objects and arrays. Truthy values are included as class names, while falsy values are omitted. This behavior is powered by the `clsx` library. Examples: ```ripple import { track } from 'ripple'; let &[includeBaz] = track(true);
// becomes: class="foo baz"
// becomes: class="foo bat" let &[count] = track(3);
2}, count > 3 && 'bat']}>
// becomes: class="foo bar" ``` #### Dynamic Inline Styles Sometimes you might need to dynamically set inline styles. For this, you can use the `style` attribute, passing either a string or an object to it: ```ripple import { track } from 'ripple'; let &[color] = track('red');
const style = { color, fontWeight: 'bold', 'background-color': 'gray', }; // using object spread
// using object directly
``` Both examples above will render the same inline styles, however, it's recommended to use the object notation as it's typically more performance optimized. > Note: When passing an object to the `style` attribute, you can use either camelCase or kebab-case for CSS property names. ### DOM References (Refs) Use `{ref fn}` syntax to capture DOM element references: ```ripple import { track } from 'ripple'; export component App() { let &[div] = track(); const divRef = (node) => { div = node; console.log("mounted", node); return () => { div = undefined; console.log("unmounted", node); }; };
{"Hello world"}
} ``` Inline refs: ```ripple
console.log(node)}>{"Content"}
``` ## 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.ripple'; 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.ripple'; 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.ripple'; // Render to string const html = render(App, { props: { title: 'Hello world!' } }); ``` For streaming SSR: ```typescript import { renderToStream } from 'ripple/server'; import { App } from './App.ripple'; const stream = renderToStream(App, { props: { title: 'Hello world!' } }); ``` ### Effects ```ripple import { track, effect } from 'ripple'; export component App() { let &[count] = track(0); effect(() => { console.log("Count changed:", count); }); } ``` ### 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 component App() { let &[count] = track(0); effect(() => { count; if (count === 0) { console.log('initial run, skipping'); return; } tick().then(() => { console.log('after the update'); }); }); } ``` ### 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 component 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); // 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); component Child() { // Context is read in the Child component const value = MyContext.get(); // value is "Hello from context!" console.log(value); } component 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!"); } ``` ### 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 component App() { const items = new RippleArray(1, 2, 3);

{"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 component App() { const obj = new RippleObject({a: 0}) obj.a = 0;
{'obj.a is: '}{obj.a}
{'obj.b is: '}{obj.b}
} ``` #### Reactive Set ```ripple import { RippleSet } from 'ripple'; component SetExample() { const mySet = new RippleSet([1, 2, 3]);

{"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 component App() { const map = new RippleMap([[1,1], [2,2], [3,3], [4,4]]); // 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 component App() { const date = new RippleDate(2025, 0, 1, 12, 0, 0); // 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 ### React Compatibility Ripple provides a compatibility layer for integrating with React applications. This allows you to: - Embed React components inside Ripple applications - Embed Ripple components inside React applications - Share React Context and React Suspense between React and Ripple components **Note**: React SSR is not currently supported. The compatibility layer is client-side only. #### Installation ```bash pnpm add @ripple-ts/compat-react ``` #### Using React Components in Ripple (tsx:react) The `` block allows you to embed React JSX directly inside Ripple components. React components inside these blocks use React's JSX semantics (e.g., `className` instead of `class`). ```ripple import { Suspense } from 'react'; component App() {

{"Ripple App"}

{/* Embed React components using tsx:react */}
This is React JSX!
} ``` #### Setting Up React Compat with mount() To use `` blocks, you must configure the React compatibility layer when mounting your Ripple app: ```typescript // main.ts import { mount } from 'ripple'; import { createReactCompat } from '@ripple-ts/compat-react'; import { App } from './App.ripple'; mount(App, { target: document.getElementById('app')!, compat: { react: createReactCompat(), }, }); ``` #### Using Ripple Components in React (RippleRoot + Ripple) To embed Ripple components inside a React application, wrap your React app with `` and use the `` component to render Ripple components: ```tsx // App.tsx - React application import { createRoot } from 'react-dom/client'; import { RippleRoot, Ripple } from '@ripple-ts/compat-react'; import { MyRippleComponent } from './MyComponent.ripple'; function App() { return (

Hello from React!

); } const root = createRoot(document.getElementById('root')!); root.render( ); ``` The `` component accepts: - `component`: The Ripple component to render - `props` (optional): Props to pass to the Ripple component #### React Context Integration React Context works seamlessly across the Ripple/React boundary. Context providers in React are accessible from Ripple components embedded via ``, and vice versa. ```ripple import { createContext, useContext } from 'react'; // Create a React context const ThemeContext = createContext('light'); // React component that uses context function ThemedButton() { const theme = useContext(ThemeContext); return ; } // Ripple component that provides and consumes React context component App() { } ``` #### Error Boundaries Ripple's `try/catch` blocks can catch errors thrown by React components inside `` blocks: ```ripple function BuggyReactComponent() { throw new Error('Something went wrong!'); } component App() { try { } catch (error) {
{"An error occurred in the React component"}
} } ``` #### Common Mistakes with React Compatibility **❌ WRONG: Using React JSX syntax outside tsx:react blocks** ```ripple component App() { // Wrong: className is React syntax, use class in Ripple
{"Hello"}
} ``` **✅ CORRECT: Use Ripple syntax outside tsx:react, React syntax inside** ```ripple component App() {
{"Hello"}
React content
} ``` **❌ WRONG: Defining React components with JSX inside .ripple files** ```ripple // Wrong: JSX in a .ripple file is Ripple syntax, not React syntax function ReactChild() { return
Child
; // This is Ripple JSX, not React JSX! } component App() { } ``` **✅ CORRECT: Define React components in separate .tsx files** ```tsx // ReactChild.tsx - React component in its own file export function ReactChild() { return
Child
; // This is React JSX } ``` ```ripple // App.ripple - Import and use the React component import { ReactChild } from './ReactChild.tsx'; component App() { } ``` **✅ CORRECT: Or use jsx/jsxs directly in .ripple files** ```ripple import { jsx } from 'react/jsx-runtime'; // React component using jsx() instead of JSX syntax function ReactChild() { return jsx('div', { children: 'Child' }); } component App() { } ``` **❌ WRONG: Forgetting to wrap React app with RippleRoot** ```tsx // Wrong: Ripple component won't work without RippleRoot root.render(); ``` **✅ CORRECT: Always wrap with RippleRoot when using Ripple in React** ```tsx root.render( ); ``` **❌ WRONG: Forgetting createReactCompat() when using tsx:react in Ripple** ```typescript // Wrong: tsx:react blocks won't render mount(App, { target: document.getElementById('app')! }); ``` **✅ CORRECT: Always configure compat when using tsx:react** ```typescript mount(App, { target: document.getElementById('app')!, compat: { react: createReactCompat(), }, }); ``` ### 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 component App() {

{'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 `{html}` directive. ```ripple export component App() { let source = `

My Blog Post

Hi! I like JS and Ripple.

`
{html source}
} ``` ### Explicit Text By default, a `{expression}` in a template can render either text or a fragment. If you know the expression will always be text, use the `{text}` directive to make that explicit: ```ripple export component Frame({ children }) {
{text 'before'} {children} {text 'after'}
} ``` The `{text}` directive guarantees the expression is treated as text content. Like regular expressions, the value is HTML-escaped. Unlike `{html}`, the content is never parsed as HTML. The compiler can optimize `{text}` expressions more efficiently than general expressions that might need to handle component rendering. ```ripple export component App() { const markup = 'Not HTML'; // Renders the literal string "Not HTML" as text
{text markup}
} ``` Note: `text` is a reserved keyword in Ripple expressions. You cannot use `text` as a variable name inside `{braces}`. ## TypeScript Integration ### Component Types ```typescript import type { Component } from 'ripple'; interface Props { value: string; label: string; children?: Component; } component MyComponent(props: Props) { // Component implementation } ``` ### Context Types ```typescript import { Context } from 'ripple'; type Theme = 'light' | 'dark'; const ThemeContext = new Context('light'); ``` ## File Structure ``` src/ App.ripple # Main app component components/ Button.ripple # Reusable components Card.ripple 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": ["@ripple-ts/prettier-plugin"] } ``` ## Key Differences from Other Frameworks ### vs React - No JSX functions/returns - components use statement-based templates - 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 - `.ripple` extension instead of `.svelte` - Similar reactivity concepts but different syntax ### vs Solid - Component definition with `component` keyword - Built-in collections (RippleArray, RippleSet) - Different templating approach within component bodies ## Best Practices 1. **Reactivity**: Use `track()` (imported from `'ripple'`) with `&[]` lazy destructuring to create reactive variables 2. **Strings**: Wrap string literals in `{"string"}` within templates 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 `