@rip-lang/ui 0.3.19 → 0.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +443 -576
  2. package/accordion.rip +113 -0
  3. package/alert-dialog.rip +96 -0
  4. package/autocomplete.rip +141 -0
  5. package/avatar.rip +37 -0
  6. package/badge.rip +15 -0
  7. package/breadcrumb.rip +46 -0
  8. package/button-group.rip +26 -0
  9. package/button.rip +23 -0
  10. package/card.rip +25 -0
  11. package/carousel.rip +110 -0
  12. package/checkbox-group.rip +65 -0
  13. package/checkbox.rip +33 -0
  14. package/collapsible.rip +50 -0
  15. package/combobox.rip +155 -0
  16. package/context-menu.rip +105 -0
  17. package/date-picker.rip +214 -0
  18. package/dialog.rip +107 -0
  19. package/drawer.rip +79 -0
  20. package/editable-value.rip +80 -0
  21. package/field.rip +53 -0
  22. package/fieldset.rip +22 -0
  23. package/form.rip +39 -0
  24. package/grid.rip +901 -0
  25. package/index.rip +16 -0
  26. package/input-group.rip +28 -0
  27. package/input.rip +36 -0
  28. package/label.rip +16 -0
  29. package/menu.rip +162 -0
  30. package/menubar.rip +155 -0
  31. package/meter.rip +36 -0
  32. package/multi-select.rip +158 -0
  33. package/native-select.rip +32 -0
  34. package/nav-menu.rip +129 -0
  35. package/number-field.rip +162 -0
  36. package/otp-field.rip +89 -0
  37. package/package.json +18 -27
  38. package/pagination.rip +123 -0
  39. package/popover.rip +143 -0
  40. package/preview-card.rip +73 -0
  41. package/progress.rip +25 -0
  42. package/radio-group.rip +67 -0
  43. package/resizable.rip +123 -0
  44. package/scroll-area.rip +145 -0
  45. package/select.rip +184 -0
  46. package/separator.rip +17 -0
  47. package/skeleton.rip +22 -0
  48. package/slider.rip +165 -0
  49. package/spinner.rip +17 -0
  50. package/table.rip +27 -0
  51. package/tabs.rip +124 -0
  52. package/textarea.rip +48 -0
  53. package/toast.rip +87 -0
  54. package/toggle-group.rip +78 -0
  55. package/toggle.rip +24 -0
  56. package/toolbar.rip +46 -0
  57. package/tooltip.rip +115 -0
  58. package/dist/rip-ui.min.js +0 -524
  59. package/dist/rip-ui.min.js.br +0 -0
  60. package/serve.rip +0 -92
  61. package/ui.rip +0 -964
package/README.md CHANGED
@@ -1,723 +1,590 @@
1
- <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.png" style="width:50px" /> <br>
1
+ # Rip UI
2
2
 
3
- # Rip UI - @rip-lang/ui
3
+ Headless, accessible UI components written in Rip. Zero dependencies. Zero CSS.
4
+ Every widget exposes `$` attributes (compiled to `data-*`) for styling and
5
+ handles keyboard interactions per WAI-ARIA Authoring Practices.
4
6
 
5
- > **Zero-build reactive web framework for the Rip language.**
7
+ Components are plain `.rip` source files no build step. The browser compiles
8
+ them on the fly.
6
9
 
7
- Load the Rip compiler in the browser. Write inline Rip. Launch your app.
8
- No build step, no bundler, no configuration.
10
+ ---
9
11
 
10
12
  ## Quick Start
11
13
 
12
- **`index.rip`** the server:
14
+ Add the components directory to your serve middleware:
13
15
 
14
16
  ```coffee
15
- import { get, use, start, notFound } from '@rip-lang/api'
16
- import { ripUI } from '@rip-lang/ui/serve'
17
-
18
- dir = import.meta.dir
19
- use ripUI dir: dir, components: 'routes', includes: ['ui'], watch: true, title: 'My App'
20
- get '/css/*', -> @send "#{dir}/css/#{@req.path.slice(5)}"
21
- notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
22
- start port: 3000
17
+ use serve
18
+ dir: dir
19
+ components: ['components', '../../../packages/ui']
23
20
  ```
24
21
 
25
- **`index.html`** the page:
22
+ All widgets become available by name (`Select`, `Dialog`, `Grid`, etc.) in the
23
+ shared scope — no imports needed.
26
24
 
27
- ```html
28
- <script type="module" src="/rip/rip-ui.min.js"></script>
29
- <script type="text/rip">
30
- { launch } = importRip! 'ui.rip'
31
- launch()
32
- </script>
25
+ ```bash
26
+ cd packages/ui
27
+ rip server
33
28
  ```
34
29
 
35
- **`pages/index.rip`** — a page component:
30
+ Every widget:
31
+ - Handles all keyboard interactions per WAI-ARIA Authoring Practices
32
+ - Sets correct ARIA attributes automatically
33
+ - Exposes state via `$` sigil (`$open`, `$selected`) for CSS styling
34
+ - Ships zero CSS — styling is entirely yours
35
+ - Uses Rip's reactive primitives for all state management
36
36
 
37
- ```coffee
38
- export Home = component
39
- @count := 0
40
- render
41
- .
42
- h1 "Hello from Rip UI"
43
- button @click: (-> @count += 1), "Clicked #{@count} times"
44
- ```
45
-
46
- Run `bun index.rip`, open `http://localhost:3000`.
47
-
48
- ## The Two Keywords
49
-
50
- Rip UI adds two keywords to the language: `component` and `render`. Each
51
- serves a distinct role, and together they form a complete reactive UI model.
52
-
53
- ### `component` — the model
54
-
55
- Raw Rip Lang has no concept of a self-contained, reusable UI unit. The
56
- `component` keyword adds everything needed to manage interactive state:
57
-
58
- - **Reactive state** (`:=`) — assignments create signals that trigger
59
- updates automatically. `count := 0` is not a plain variable; changing
60
- it updates the DOM.
61
- - **Computed values** (`~=`) — derived values that recalculate when their
62
- dependencies change. `remaining ~= todos.filter((t) -> not t.done).length`
63
- - **Effects** (`~>`) — side effects that run whenever reactive dependencies
64
- change. `~> @app.data.count = count`
65
- - **Props** (`@` prefix, `=!` for readonly) — a public API for parent
66
- components to pass data in, with signal passthrough for shared reactivity.
67
- - **Lifecycle hooks** — `beforeMount`, `mounted`, `updated`, `beforeUnmount`,
68
- `unmounted` for running code at specific points in a component's life.
69
- - **Context API** — `setContext` and `getContext` for ancestor-to-descendant
70
- data sharing without prop drilling.
71
- - **Mount/unmount mechanics** — attaching to the DOM, cascading teardown
72
- to children, and keep-alive caching across navigation.
73
- - **Encapsulation** — each component is a class with its own scope, state,
74
- and methods. No global variable collisions, no leaking internals.
75
-
76
- A component without a render block can still hold state, run effects, and
77
- participate in the component tree — it just has no visual output.
78
-
79
- ### `render` — the view
80
-
81
- The `render` keyword provides a declarative template DSL for describing DOM
82
- structure. It has its own lexer pass and syntax rules distinct from regular
83
- Rip code:
84
-
85
- - **Element creation** — tag names become DOM nodes: `div`, `h1`, `button`
86
- - **CSS-selector shortcuts** — `div.card.active`, `#main`, `.card` (implicit `div`)
87
- - **Dynamic classes** — `div.('card', active && 'active')` with CLSX semantics
88
- - **Event handlers** — `@click: handler` compiles to `addEventListener`
89
- - **Two-way binding** — `value <=> username` wires reactive read and write
90
- (see [Two-Way Binding](#two-way-binding--the--operator) below)
91
- - **Conditionals and loops** — `if`/`else` and `for item in items` with
92
- anchor-based DOM insertion and keyed reconciliation
93
- - **Children/slots** — `@children` receives child nodes, `#content` marks
94
- layout insertion points
95
- - **Component instantiation** — PascalCase names like `Card title: "Hello"`
96
- resolve to components automatically, no imports needed
97
-
98
- Render compiles to two methods: `_create()` builds the DOM tree once, and
99
- `_setup()` wires reactive effects for fine-grained updates. There is no
100
- virtual DOM — each reactive binding creates a direct DOM effect that updates
101
- the specific text node or attribute that depends on it.
102
-
103
- A render block can only exist inside a component. It needs the component's
104
- signals, computed values, and lifecycle to have something to render and
105
- react to.
106
-
107
- ### Together
108
-
109
- `component` provides the **model** — state, reactivity, lifecycle, identity.
110
- `render` provides the **view** — a concise way to describe what the DOM
111
- should look like and how it stays in sync with that state. One defines
112
- behavior, the other defines structure. Neither is useful without the other
113
- in practice, but they are separate concerns with separate syntax.
114
-
115
- ## Component Composition
116
-
117
- Page components in `pages/` map to routes via file-based routing. Shared
118
- components in `ui/` (or any `includes` directory) are available by PascalCase
119
- name. No imports needed:
120
-
121
- ```coffee
122
- # ui/card.rip
123
- export Card = component
124
- title =! ""
125
- render
126
- .card
127
- if title
128
- h3 "#{title}"
129
- @children
37
+ ---
130
38
 
131
- # pages/about.rip
132
- export About = component
133
- render
134
- .
135
- h1 "About"
136
- Card title: "The Idea"
137
- p "Components compose naturally."
138
- Card title: "Architecture"
139
- p "PascalCase resolution, signal passthrough, children blocks."
140
- ```
39
+ ## Rip in 60 Seconds
141
40
 
142
- Reactive props via `:=` signal passthrough. Readonly props via `=!`.
143
- Children blocks passed as DOM nodes via `@children`.
41
+ If you're coming from React or another framework, here's the Rip you need
42
+ to know to use these widgets:
144
43
 
145
- ## Props The `@` Contract
44
+ | Syntax | Name | What It Does |
45
+ |--------|------|-------------|
46
+ | `:=` | State | `count := 0` — reactive state (like `useState`) |
47
+ | `~=` | Computed | `doubled ~= count * 2` — derived value (like `useMemo`, but auto-tracked) |
48
+ | `~>` | Effect | `~> document.title = "#{count}"` — side effect (like `useEffect`, but auto-tracked) |
49
+ | `<=>` | Bind | `value <=> @name` — two-way binding between parent and child |
50
+ | `@prop` | Prop | `@checked`, `@disabled` — component props (reactive) |
51
+ | `$attr` | Data attr | `$open`, `$selected` — compiles to `data-open`, `data-selected` in HTML |
52
+ | `@emit` | Event | `@emit 'change', value` — dispatches a CustomEvent |
53
+ | `ref:` | DOM ref | `ref: "_panel"` — saves DOM element reference |
54
+ | `slot` | Children | Projects parent-provided content into the component |
55
+ | `offer` / `accept` | Context | Share reactive state between ancestor and descendant components |
146
56
 
147
- The `@` prefix on a member declaration marks it as a **public prop** — settable
148
- by a parent component. Members without `@` are **private state** and ignore
149
- any value a parent tries to pass in.
57
+ Two-way binding example React vs Rip:
150
58
 
151
59
  ```coffee
152
- export Drawer = component
153
- @open := false # public — parent can pass `open: true`
154
- @breakpoint := 480 # public — parent can pass `breakpoint: 768`
155
- isRight := false # private — always starts as false
156
- closing := false # private always starts as false
157
-
158
- render
159
- if open
160
- div "Drawer is open"
161
- ```
162
-
163
- The compiler enforces the boundary:
164
-
165
- ```javascript
166
- // @open := false → accepts parent value
167
- this.open = __state(props.open ?? false);
60
+ # React: 4 lines per binding
61
+ const [show, setShow] = useState(false)
62
+ <Dialog open={show} onOpenChange={setShow} />
63
+ const [name, setName] = useState('')
64
+ <input value={name} onChange={e => setName(e.target.value)} />
168
65
 
169
- // isRight := false → ignores parent, always uses default
170
- this.isRight = __state(false);
66
+ # Rip: 1 line per binding
67
+ Dialog open <=> show
68
+ input value <=> @name
171
69
  ```
172
70
 
173
- This works for all member types:
71
+ ---
174
72
 
175
- | Declaration | Visibility | Meaning |
176
- |-------------|-----------|---------|
177
- | `@title := 'Hello'` | Public | Reactive state, settable by parent |
178
- | `@label =! 'Default'` | Public | Readonly prop, settable by parent |
179
- | `count := 0` | Private | Reactive state, internal only |
180
- | `cache =! null` | Private | Readonly, internal only |
181
- | `total ~= items.length` | — | Computed (always derived, never a prop) |
73
+ ## Why Rip UI
182
74
 
183
- A parent passes props as key-value pairs when using a component:
75
+ | | ShadCN / Radix | Rip UI |
76
+ |--|---------------|--------|
77
+ | Runtime dependency | React (~42KB gz) + ReactDOM | None |
78
+ | Component count | ~40 | 54 |
79
+ | Total source | ShadCN wrappers (~3K LOC) atop Radix (~20K+ LOC) | 5,191 SLOC — everything included |
80
+ | Build step | Required (Next.js, Vite, etc.) | None — browser compiles `.rip` source |
81
+ | Styling | Pre-wired Tailwind (ShadCN) or unstyled (Radix) | Zero CSS — `data-*` contract, any methodology |
82
+ | Controlled components | `value` + `onChange` callback pair | `<=>` two-way binding |
83
+ | Shared state | React Context + Provider wrappers | `offer` / `accept` keywords |
84
+ | Reactivity | `useState` + `useEffect` + dependency arrays | `:=` / `~=` / `~>` — language-level |
85
+ | Virtual DOM | Yes (diffing on every render) | No — fine-grained updates to exact nodes |
86
+ | Data grid | Not included | 901 SLOC — 100K+ rows at 60fps |
184
87
 
185
- ```coffee
186
- Drawer open: showDrawer, breakpoint: 768
187
- div "Content here"
188
- ```
88
+ ### Architecture
189
89
 
190
- The `@` declarations at the top of a component are its public API. Everything
191
- else is an implementation detail. No separate type files, no prop validation
192
- boilerplate one character that says "this is settable from outside."
90
+ **Fine-grained reactivity.** When `count` changes, only the text node
91
+ displaying `count` updates. No tree diffing, no wasted renders, no
92
+ memoization needed. Same model as SolidJS and Svelte 5's runes, but built
93
+ into the language.
193
94
 
194
- ## Render Block Syntax
95
+ **Components compile to JavaScript.** The `component` keyword, `render`
96
+ block, and reactive operators resolve at compile time into ES2022 classes
97
+ with direct DOM operations. Source maps point back to `.rip` source for
98
+ debugging.
195
99
 
196
- Inside a `render` block, elements are declared by tag name. Classes, attributes,
197
- and children can be expressed inline or across multiple indented lines.
100
+ **No build pipeline.** The browser loads the Rip compiler (~50KB) once and
101
+ compiles `.rip` files on the fly. For production, pre-compile. For
102
+ development, save and see — SSE-based hot reload.
198
103
 
199
- ### Classes with `.(...)`
104
+ **Source as distribution.** Components are served as `.rip` source files.
105
+ Read them, understand them, modify them.
200
106
 
201
- The `.()` helper applies classes using CLSX semantics — strings are included
202
- directly, and object keys are conditionally included based on their values:
107
+ ### Why We Build Our Own
203
108
 
204
- ```coffee
205
- button.('px-4 py-2 rounded-full') "Click"
206
- button.('px-4 py-2', active: isActive) "Click"
207
- ```
109
+ Radix and Base UI implement proven WAI-ARIA patterns, but they require
110
+ React. Rip reimplements the same behavioral patterns using its own
111
+ primitives:
208
112
 
209
- Arguments can span multiple lines, just like a normal function call:
113
+ | Capability | React | Rip |
114
+ |-----------|-------|-----|
115
+ | Child projection | No equivalent | `slot` |
116
+ | DOM ownership | Virtual DOM abstraction | Direct DOM + `ref:` |
117
+ | State sharing | Context + Provider wrappers | `offer` / `accept` |
118
+ | Two-way binding | `value` + `onChange` pair | `<=>` operator |
119
+ | Reactivity | Hooks + dependency arrays | `:=` / `~=` / `~>` |
210
120
 
211
- ```coffee
212
- input.(
213
- 'block w-full rounded-lg border border-primary',
214
- 'text-sm-plus text-tertiary shadow-xs'
215
- )
216
- ```
121
+ This lets Rip use the **right pattern for each widget**: single-component
122
+ for data-driven widgets (Select, Combobox, Menu), compositional via
123
+ `offer`/`accept` when children contain complex content the parent shouldn't
124
+ own.
217
125
 
218
- ### Indented Attributes
126
+ ---
219
127
 
220
- Attributes can be placed on separate indented lines after the element:
221
-
222
- ```coffee
223
- input.('rounded-lg border px-3.5 py-2.5')
224
- type: "email"
225
- value: user.email
226
- disabled: true
227
- ```
128
+ ## Styling
228
129
 
229
- This is equivalent to the inline form:
130
+ All widgets ship zero CSS. The contract between behavior and styling is
131
+ `data-*` attributes:
230
132
 
231
133
  ```coffee
232
- input.('rounded-lg border px-3.5 py-2.5') type: "email", value: user.email, disabled: true
134
+ # Widget exposes semantic state
135
+ button $open: open?!, $disabled: @disabled?!
233
136
  ```
234
137
 
235
- ### The `class:` Attribute
236
-
237
- The `class:` attribute works like `.()` and merges cumulatively with any
238
- existing `.()` classes on the same element:
239
-
240
- ```coffee
241
- input.('block w-full rounded-lg')
242
- class: 'text-sm text-tertiary'
243
- type: "email"
138
+ ```css
139
+ /* You write the styles */
140
+ [data-open] { border-color: var(--color-primary); }
141
+ [data-disabled] { opacity: 0.5; cursor: not-allowed; }
244
142
  ```
245
143
 
246
- This produces a single combined class expression: `block w-full rounded-lg text-sm text-tertiary`.
144
+ Any CSS methodology works vanilla CSS, Tailwind, Open Props, a custom
145
+ design system. The widgets don't care.
247
146
 
248
- The `class:` value also supports `.()` syntax for conditional classes:
147
+ For our recommended approach design tokens, CSS architecture, dark mode,
148
+ and the rationale behind it — see **[STYLING.md](STYLING.md)**.
249
149
 
250
- ```coffee
251
- div.('mt-4 p-4')
252
- class: .('ring-1', highlighted: isHighlighted)
253
- span "Content"
254
- ```
150
+ ---
255
151
 
256
- ### Attributes and Children Together
152
+ ## Code Density
257
153
 
258
- Attributes and children can coexist at the same indentation level. Attributes
259
- (key-value pairs) are listed first, followed by child elements:
154
+ ### Checkbox 18 Lines
260
155
 
261
156
  ```coffee
262
- button.('flex items-center rounded-lg')
263
- type: "submit"
264
- disabled: saving
157
+ export Checkbox = component
158
+ @checked := false
159
+ @disabled := false
160
+ @indeterminate := false
161
+ @switch := false
162
+
163
+ onClick: ->
164
+ return if @disabled
165
+ @indeterminate = false
166
+ @checked = not @checked
167
+ @emit 'change', @checked
265
168
 
266
- span.('font-bold') "Submit"
267
- span.('text-sm text-secondary') "or press Enter"
169
+ render
170
+ button role: @switch ? 'switch' : 'checkbox'
171
+ aria-checked: @indeterminate ? 'mixed' : !!@checked
172
+ aria-disabled: @disabled?!
173
+ $checked: @checked?!
174
+ $indeterminate: @indeterminate?!
175
+ $disabled: @disabled?!
176
+ slot
268
177
  ```
269
178
 
270
- Blank lines between attributes and children are fine they don't break the
271
- structure.
272
-
273
- ## Two-Way Binding — The `<=>` Operator
274
-
275
- The `<=>` operator is one of Rip UI's most powerful features. It creates a
276
- bidirectional reactive binding between a parent's state and a child element
277
- or component — changes flow in both directions automatically.
179
+ Full ARIA. Checkbox and switch modes. Indeterminate state. Data attributes
180
+ for styling. 18 lines, complete.
278
181
 
279
- ### The Problem It Solves
182
+ ### Dialog Effect-Based Lifecycle
280
183
 
281
- In React, wiring state to interactive elements requires explicit value props
282
- and callback handlers for every bindable property:
184
+ Focus trap, scroll lock, escape dismiss, click-outside dismiss, focus
185
+ restore all in one reactive effect with automatic cleanup:
283
186
 
284
- ```jsx
285
- // React: verbose, repetitive ceremony
286
- const [name, setName] = useState('');
287
- const [role, setRole] = useState('viewer');
288
- const [notify, setNotify] = useState(true);
289
- const [showConfirm, setShowConfirm] = useState(false);
187
+ ```coffee
188
+ ~>
189
+ if @open
190
+ _prevFocus = document.activeElement
191
+ # lock scroll, trap focus, wire ARIA ...
192
+ return ->
193
+ # cleanup runs automatically when @open becomes false
194
+ ```
195
+
196
+ No `useEffect`. No dependency array. No cleanup that captures stale state.
197
+
198
+ ### Grid — 901 Lines
199
+
200
+ No equivalent in ShadCN, Radix, or Headless UI. Virtual scrolling, DOM
201
+ recycling, Sheets-style selection, full keyboard nav, inline editing,
202
+ multi-column sort, column resizing, clipboard (Ctrl+C/V/X as TSV — interop
203
+ with Excel, Google Sheets, Numbers). 901 lines vs 50,000+ for AG Grid.
204
+
205
+ ---
206
+
207
+ ## Component Overview
208
+
209
+ 54 headless components across 10 categories — 5,191 lines total.
210
+
211
+ ### Selection
212
+
213
+ | Widget | Description | Key Props | Events |
214
+ |--------|-------------|-----------|--------|
215
+ | **Select** | Dropdown with typeahead, ARIA listbox | `@value`, `@placeholder`, `@disabled` | `@change` |
216
+ | **Combobox** | Filterable input + listbox | `@query`, `@placeholder`, `@disabled` | `@select`, `@filter` |
217
+ | **MultiSelect** | Multi-select with chips and filtering | `@value`, `@query`, `@placeholder` | `@change` |
218
+ | **Autocomplete** | Type to filter, select to fill | `@value`, `@query`, `@placeholder` | `@change` |
219
+
220
+ ### Toggle
221
+
222
+ | Widget | Description | Key Props | Events |
223
+ |--------|-------------|-----------|--------|
224
+ | **Checkbox** | Toggle with checkbox/switch semantics | `@checked`, `@disabled`, `@switch` | `@change` |
225
+ | **Toggle** | Two-state toggle button | `@pressed`, `@disabled` | `@change` |
226
+ | **ToggleGroup** | Single or multi-select toggles | `@value`, `@multiple` | `@change` |
227
+ | **RadioGroup** | Exactly one selected, arrow nav | `@value`, `@disabled` | `@change` |
228
+ | **CheckboxGroup** | Multiple checked independently | `@value`, `@disabled` | `@change` |
229
+
230
+ ### Input
231
+
232
+ | Widget | Description | Key Props | Events |
233
+ |--------|-------------|-----------|--------|
234
+ | **Input** | Focus, touch, and validation tracking | `@value`, `@type`, `@placeholder` | `@change` |
235
+ | **Textarea** | Auto-resizing text area | `@value`, `@autoResize`, `@rows` | `@change` |
236
+ | **NumberField** | Stepper buttons, hold-to-repeat | `@value`, `@min`, `@max`, `@step` | `@change` |
237
+ | **Slider** | Drag with pointer capture + keyboard | `@value`, `@min`, `@max`, `@step` | `@change` |
238
+ | **OTPField** | Multi-digit code, auto-advance + paste | `@value`, `@length` | `@complete` |
239
+ | **DatePicker** | Calendar dropdown, single or range | `@value`, `@min`, `@max`, `@range` | `@change` |
240
+ | **EditableValue** | Click-to-edit inline value | `@value`, `@placeholder` | `@change` |
241
+ | **NativeSelect** | Styled native `<select>` wrapper | `@value`, `@disabled` | `@change` |
242
+ | **InputGroup** | Input with prefix/suffix addons | `@disabled` | — |
243
+
244
+ ### Navigation
245
+
246
+ | Widget | Description | Key Props | Events |
247
+ |--------|-------------|-----------|--------|
248
+ | **Tabs** | Arrow key nav, roving tabindex | `@active`, `@orientation` | `@change` |
249
+ | **Menu** | Dropdown action menu | `@disabled` | `@select` |
250
+ | **ContextMenu** | Right-click context menu | `@disabled` | `@select` |
251
+ | **Menubar** | Horizontal menu bar with dropdowns | — | `@select` |
252
+ | **NavMenu** | Site nav with hover/click panels | — | — |
253
+ | **Toolbar** | Grouped controls, roving tabindex | `@orientation`, `@label` | — |
254
+ | **Breadcrumb** | Navigation trail with separator | `@separator`, `@label` | — |
255
+
256
+ ### Overlay
257
+
258
+ | Widget | Description | Key Props | Events |
259
+ |--------|-------------|-----------|--------|
260
+ | **Dialog** | Focus trap, scroll lock, ARIA modal | `@open` | `@close` |
261
+ | **AlertDialog** | Non-dismissable modal | `@open`, `@initialFocus` | `@close` |
262
+ | **Drawer** | Slide-out panel with focus trap | `@open`, `@side` | `@close` |
263
+ | **Popover** | Anchored floating with flip/shift | `@placement`, `@offset` | — |
264
+ | **Tooltip** | Hover/focus with delay | `@text`, `@placement`, `@delay` | — |
265
+ | **PreviewCard** | Hover/focus preview card | `@delay`, `@placement` | — |
266
+ | **Toast** | Auto-dismiss, ARIA live region | `@toast` (object) | `@dismiss` |
267
+
268
+ ### Display
269
+
270
+ | Widget | Description | Key Props |
271
+ |--------|-------------|-----------|
272
+ | **Button** | Disabled-but-focusable pattern | `@disabled` |
273
+ | **Badge** | Inline label (solid/outline/subtle) | `@variant` |
274
+ | **Card** | Container with header/content/footer | `@interactive` |
275
+ | **Separator** | Decorative or semantic divider | `@orientation`, `@decorative` |
276
+ | **Progress** | Progress bar via CSS custom prop | `@value`, `@max` |
277
+ | **Meter** | Gauge with thresholds | `@value`, `@min`, `@max`, `@low`, `@high` |
278
+ | **Spinner** | Loading indicator | `@label`, `@size` |
279
+ | **Skeleton** | Loading placeholder with shimmer | `@width`, `@height`, `@circle` |
280
+ | **Avatar** | Image with fallback to initials | `@src`, `@alt`, `@fallback` |
281
+ | **Label** | Accessible form label | `@for`, `@required` |
282
+ | **ScrollArea** | Custom scrollbar, draggable thumb | `@orientation` |
283
+
284
+ ### Form
285
+
286
+ | Widget | Description | Key Props |
287
+ |--------|-------------|-----------|
288
+ | **Field** | Label + description + error wrapper | `@label`, `@error`, `@required` |
289
+ | **Fieldset** | Grouped fields with cascading disable | `@legend`, `@disabled` |
290
+ | **Form** | Submit handling + validation state | `@onSubmit` |
291
+ | **ButtonGroup** | Grouped buttons, ARIA semantics | `@orientation`, `@disabled` |
292
+
293
+ ### Data
294
+
295
+ | Widget | Description | Key Props |
296
+ |--------|-------------|-----------|
297
+ | **Grid** | Virtual scroll, 100K+ rows at 60fps | `@data`, `@columns`, `@rowHeight` |
298
+ | **Accordion** | Expand/collapse, single or multiple | `@multiple` |
299
+ | **Table** | Semantic table wrapper | `@caption`, `@striped` |
300
+
301
+ ### Interactive
302
+
303
+ | Widget | Description | Key Props | Events |
304
+ |--------|-------------|-----------|--------|
305
+ | **Collapsible** | Animated expand/collapse | `@open`, `@disabled` | `@change` |
306
+ | **Pagination** | Page nav with ellipsis gaps | `@page`, `@total`, `@perPage` | `@change` |
307
+ | **Carousel** | Slide with autoplay + loop | `@loop`, `@autoplay`, `@interval` | `@change` |
308
+ | **Resizable** | Draggable resize handles | `@orientation`, `@minSize` | `@resize` |
309
+
310
+ ---
311
+
312
+ ## Widget Reference
313
+
314
+ ### Select
290
315
 
291
- <input value={name} onChange={e => setName(e.target.value)} />
292
- <Select value={role} onValueChange={setRole} />
293
- <Switch checked={notify} onCheckedChange={setNotify} />
294
- <Dialog open={showConfirm} onOpenChange={setShowConfirm} />
316
+ ```coffee
317
+ Select value <=> selectedRole, @change: handleChange
318
+ option value: "eng", "Engineer"
319
+ option value: "des", "Designer"
320
+ option value: "mgr", "Manager"
295
321
  ```
296
322
 
297
- Every bindable property needs a state declaration AND a setter callback.
298
- This is the single most tedious pattern in React development.
323
+ **Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close, Home/End, type-ahead
324
+ **Data attributes:** `$open`, `$highlighted`, `$selected`, `$disabled`
299
325
 
300
- ### The Rip Way
301
-
302
- In Rip, `<=>` replaces all of that with a single operator:
326
+ ### Combobox
303
327
 
304
328
  ```coffee
305
- export UserForm = component
306
- @name := ''
307
- @role := 'viewer'
308
- @notify := true
309
- @showConfirm := false
310
-
311
- render
312
- input value <=> @name
313
- Select value <=> @role
314
- Option value: "viewer", "Viewer"
315
- Option value: "editor", "Editor"
316
- Option value: "admin", "Admin"
317
- Switch checked <=> @notify
318
- Dialog open <=> @showConfirm
319
- p "Save changes?"
329
+ Combobox query <=> searchText, @select: handleSelect, @filter: handleFilter
330
+ for item in filteredItems
331
+ div $value: item.id
332
+ span item.name
320
333
  ```
321
334
 
322
- No `onChange`. No `onValueChange`. No `onOpenChange`. No `setName`, `setRole`,
323
- `setNotify`, `setShowConfirm`. The reactive system handles everything — state
324
- flows down, user interactions flow back up.
325
-
326
- ### How It Works
327
-
328
- `value <=> username` compiles to two things:
329
-
330
- 1. **State → DOM** (reactive effect): `__effect(() => { el.value = username; })`
331
- 2. **DOM → State** (event listener): `el.addEventListener('input', (e) => { username = e.target.value; })`
335
+ **Keyboard:** ArrowDown/Up navigate, Enter select, Escape close/clear, Tab close
336
+ **Data attributes:** `$open`, `$highlighted`
332
337
 
333
- The compiler is smart about types:
334
- - `value <=>` on text inputs uses the `input` event and `e.target.value`
335
- - `value <=>` on number/range inputs uses `e.target.valueAsNumber`
336
- - `checked <=>` uses the `change` event and `e.target.checked`
337
-
338
- For custom components, `<=>` passes the reactive signal itself, enabling the
339
- child to both read and write the parent's state directly — no callback
340
- indirection.
341
-
342
- ### Auto-Detection
343
-
344
- Even without `<=>`, the compiler auto-detects when `value:` or `checked:` is
345
- bound to a reactive expression and generates two-way binding automatically:
338
+ ### Dialog
346
339
 
347
340
  ```coffee
348
- # These are equivalent:
349
- input value <=> @name # explicit two-way binding
350
- input value: @name # auto-detected (name is reactive)
341
+ Dialog open <=> showDialog, @close: handleClose
342
+ h2 "Confirm Action"
343
+ p "Are you sure?"
344
+ button @click: (=> showDialog = false), "Cancel"
345
+ button @click: handleConfirm, "Confirm"
351
346
  ```
352
347
 
353
- ### Why This Matters
354
-
355
- Two-way binding is what Vue has with `v-model`, what Svelte has with `bind:`,
356
- and what Angular has with `[(ngModel)]`. React is the only major framework
357
- that deliberately omits it, forcing the verbose controlled component pattern
358
- instead.
359
-
360
- Rip's `<=>` goes further than Vue or Svelte — it works uniformly across HTML
361
- elements and custom components with the same syntax. A `Dialog open <=> show`
362
- and an `input value <=> name` use the same operator, the same mental model,
363
- and the same compilation strategy. This makes headless interactive components
364
- dramatically cleaner to use than their React equivalents.
348
+ **Keyboard:** Escape to close, Tab trapped within dialog
349
+ **Data attributes:** `$open`
350
+ **Behavior:** Focus trap, body scroll lock, focus restore on close
365
351
 
366
- ## How It Works
352
+ ### AlertDialog
367
353
 
368
- The browser loads one file — `rip-ui.min.js` (~52KB Brotli) — which bundles the
369
- Rip compiler and the pre-compiled UI framework. No runtime compilation of the
370
- framework, no extra network requests.
371
-
372
- Then `launch()` loads component sources (from a server bundle, static files, or
373
- inline DOM), hydrates the stash, and renders.
374
-
375
- ### Browser Execution Contexts
376
-
377
- Rip provides full async/await support across every browser context — no other
378
- compile-to-JS language has this:
379
-
380
- | Context | How async works | Returns value? |
381
- |---------|-----------------|----------------|
382
- | `<script type="text/rip">` | Async IIFE wrapper | No (fire-and-forget) |
383
- | Playground "Run" button | Async IIFE wrapper | No (use console.log) |
384
- | `rip()` console REPL | Rip `do ->` block | Yes (sync direct, async via Promise) |
385
- | `.rip` files via `importRip()` | ES module import | Yes (module exports) |
386
-
387
- The `!` postfix compiles to `await`. Inline scripts are wrapped in an async IIFE
388
- automatically. The `rip()` console function wraps user code in a `do ->` block
389
- so the Rip compiler handles implicit return and auto-async natively.
390
-
391
- ### globalThis Exports
392
-
393
- When `rip-ui.min.js` loads, it registers these on `globalThis`:
394
-
395
- | Function | Purpose |
396
- |----------|---------|
397
- | `rip(code)` | Console REPL — compile and execute Rip code |
398
- | `importRip(url)` | Fetch, compile, and import a `.rip` file as an ES module |
399
- | `compileToJS(code)` | Compile Rip source to JavaScript |
400
- | `__rip` | Reactive runtime — `__state`, `__computed`, `__effect`, `__batch` |
401
- | `__ripComponent` | Component runtime — `__Component`, `__clsx`, `__fragment` |
402
- | `__ripExports` | All compiler exports — `compile`, `formatSExpr`, `VERSION`, etc. |
354
+ ```coffee
355
+ AlertDialog open <=> showConfirm
356
+ h2 "Delete account?"
357
+ p "This action cannot be undone."
358
+ button @click: (=> showConfirm = false), "Cancel"
359
+ button @click: handleDelete, "Delete"
360
+ ```
403
361
 
404
- ## The Stash
362
+ Like Dialog but cannot be closed by Escape or click outside.
363
+ **ARIA:** `role="alertdialog"`, auto-wired `aria-labelledby`/`aria-describedby`
405
364
 
406
- App state lives in one reactive tree:
365
+ ### Toast
407
366
 
408
- ```
409
- app
410
- ├── routes ← navigation state (path, params, query, hash)
411
- └── data ← reactive app state (title, theme, user, etc.)
412
- ```
413
-
414
- Writing to `app.data.theme` updates any component reading it. The stash
415
- uses Rip's built-in reactive primitives — the same signals that power
416
- `:=` and `~=` in components.
417
-
418
- ## The App Bundle
419
-
420
- The bundle is JSON served at `/{app}/bundle`:
421
-
422
- ```json
423
- {
424
- "components": {
425
- "components/index.rip": "export Home = component...",
426
- "components/counter.rip": "export Counter = component...",
427
- "components/_lib/card.rip": "export Card = component..."
428
- },
429
- "data": {
430
- "title": "My App",
431
- "theme": "light"
432
- }
433
- }
367
+ ```coffee
368
+ toasts := []
369
+ toasts = [...toasts, { message: "Saved!", type: "success" }]
370
+ toasts = toasts.filter (t) -> t isnt target
371
+ ToastViewport toasts <=> toasts
434
372
  ```
435
373
 
436
- On disk you organize your app into `pages/` and `ui/`. The middleware
437
- maps them into a flat `components/` namespace in the bundle — pages go
438
- under `components/`, shared components under `components/_lib/`. The `_`
439
- prefix tells the router to skip `_lib/` entries when generating routes.
374
+ **Props:** `@toasts`, `@placement` (bottom-right, top-right, etc.)
375
+ **Per-toast:** `message`, `type`, `duration` (default 4000ms), `title`, `action`
376
+ **Data attributes:** `$type`, `$leaving`
377
+ **Behavior:** Timer pauses on hover, resumes on leave
440
378
 
441
- ## Component Loading Modes
379
+ ### Tabs
442
380
 
443
- `launch()` supports three ways to load component sources, checked in priority
444
- order. All three produce the same internal bundle format — everything downstream
445
- (compilation, routing, rendering) works identically regardless of source.
381
+ ```coffee
382
+ Tabs active <=> currentTab
383
+ div $tab: "one", "Tab One"
384
+ div $tab: "two", "Tab Two"
385
+ div $panel: "one"
386
+ p "Content for tab one"
387
+ div $panel: "two"
388
+ p "Content for tab two"
389
+ ```
446
390
 
447
- ### 1. Static File URLs — `launch components: [...]`
391
+ **Keyboard:** ArrowLeft/Right navigate, Home/End jump
392
+ **Data attributes:** `$active`
448
393
 
449
- Fetch individual `.rip` files as plain text from any static server:
394
+ ### Accordion
450
395
 
451
396
  ```coffee
452
- launch components: [
453
- 'components/index.rip'
454
- 'components/dashboard.rip'
455
- 'components/line-chart.rip'
456
- ]
397
+ Accordion multiple: false
398
+ div $item: "a"
399
+ button $trigger: true, "Section A"
400
+ div $content: true
401
+ p "Content A"
457
402
  ```
458
403
 
459
- No server middleware needed. Serve `.rip` files as static text from any HTTP
460
- server, CDN, or `file://` path. Each URL is fetched individually and compiled
461
- in the browser.
462
-
463
- ### 2. Inline DOM — `<script type="text/rip" data-name="...">`
464
-
465
- Embed component source directly in the HTML page:
404
+ **Keyboard:** Enter/Space toggle, ArrowDown/Up between triggers, Home/End
405
+ **Methods:** `toggle(id)`, `isOpen(id)`
466
406
 
467
- ```html
468
- <script type="text/rip" data-name="index">
469
- export Home = component
470
- render
471
- h1 "Hello from inline"
472
- </script>
407
+ ### Checkbox
473
408
 
474
- <script type="text/rip" data-name="counter">
475
- export Counter = component
476
- @count := 0
477
- render
478
- button @click: (-> count += 1), "#{count}"
479
- </script>
409
+ ```coffee
410
+ Checkbox checked <=> isActive, @change: handleChange
411
+ span "Enable notifications"
480
412
 
481
- <script type="text/rip">
482
- { launch } = importRip! '/rip/ui.rip'
483
- launch()
484
- </script>
413
+ Checkbox checked <=> isDark, switch: true
414
+ span "Dark mode"
485
415
  ```
486
416
 
487
- The `data-name` attribute maps to the component filename (`.rip` extension is
488
- added automatically if omitted). Scripts with `data-name` are collected as
489
- component sources and are not executed as top-level code.
490
-
491
- ### 3. Server Bundle (default)
417
+ **ARIA:** `role="checkbox"` or `role="switch"`, `aria-checked` (true/false/mixed)
418
+ **Data attributes:** `$checked`, `$indeterminate`, `$disabled`
492
419
 
493
- When neither `components` nor inline `data-name` scripts are present, `launch()`
494
- fetches the app bundle from the server at `/{app}/bundle`. This is the default
495
- mode when using the `ripUI` server middleware.
420
+ ### Menu
496
421
 
497
422
  ```coffee
498
- launch() # fetches /bundle automatically
423
+ Menu @select: handleAction
424
+ button $trigger: true, "Actions"
425
+ div $item: "edit", "Edit"
426
+ div $item: "delete", "Delete"
499
427
  ```
500
428
 
501
- ## Server Middleware
429
+ **Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close
430
+ **Data attributes:** `$open`, `$highlighted`
502
431
 
503
- The `ripUI` middleware registers routes for the framework files, the app
504
- bundle, and optional SSE hot-reload:
432
+ ### Popover
505
433
 
506
434
  ```coffee
507
- use ripUI dir: dir, components: 'routes', includes: ['ui'], watch: true, title: 'My App'
435
+ Popover placement: "bottom-start"
436
+ button "Options"
437
+ div
438
+ p "Popover content here"
508
439
  ```
509
440
 
510
- | Option | Default | Description |
511
- |--------|---------|-------------|
512
- | `app` | `''` | URL mount point |
513
- | `dir` | `'.'` | App directory on disk |
514
- | `components` | `'components'` | Directory for page components (file-based routing) |
515
- | `includes` | `[]` | Directories for shared components (no routes) |
516
- | `watch` | `false` | Enable SSE hot-reload |
517
- | `debounce` | `250` | Milliseconds to batch file change events |
518
- | `state` | `null` | Initial app state |
519
- | `title` | `null` | Document title |
441
+ **Keyboard:** Enter/Space/ArrowDown toggle, Escape close
442
+ **Data attributes:** `$open`, `$placement`
520
443
 
521
- Routes registered:
444
+ ### Tooltip
522
445
 
446
+ ```coffee
447
+ Tooltip text: "Save your changes", placement: "top"
448
+ button "Save"
523
449
  ```
524
- /rip/rip-ui.min.js — Rip compiler + pre-compiled UI framework
525
- /{app}/bundle — app bundle (components + data as JSON)
526
- /{app}/watch — SSE hot-reload stream (when watch: true)
527
- /{app}/components/* — individual component files (for hot-reload refetch)
528
- ```
529
-
530
- ## State Preservation (Keep-Alive)
531
450
 
532
- Components are cached when navigating away instead of destroyed. Navigate
533
- to `/counter`, increment the count, go to `/about`, come back — the count
534
- is preserved. Configurable via `cacheSize` (default 10).
451
+ **Data attributes:** `$open`, `$entering`, `$exiting`, `$placement`
452
+ **Behavior:** Shows after delay on hover/focus, uses `aria-describedby`
535
453
 
536
- ## Data Loading
537
-
538
- `createResource` manages async data with reactive `loading`, `error`, and
539
- `data` properties:
454
+ ### Grid
540
455
 
541
456
  ```coffee
542
- export UserPage = component
543
- user := createResource -> fetch!("/api/users/#{@params.id}").json!
544
-
545
- render
546
- if user.loading
547
- p "Loading..."
548
- else if user.error
549
- p "Error: #{user.error.message}"
550
- else
551
- h1 user.data.name
457
+ Grid
458
+ data: employees
459
+ columns: [
460
+ { key: 'name', title: 'Name', width: 200 }
461
+ { key: 'age', title: 'Age', width: 80, align: 'right' }
462
+ { key: 'role', title: 'Role', width: 150, type: 'select', source: roles }
463
+ { key: 'active', title: 'Active', width: 60, type: 'checkbox' }
464
+ ]
465
+ rowHeight: 32
466
+ striped: true
552
467
  ```
553
468
 
554
- ## Error Boundaries
469
+ **Column types:** `text`, `number`, `checkbox`, `select`
470
+ **Methods:** `getCell`, `setCell`, `getData`, `setData`, `sort`, `scrollToRow`, `copySelection`, `cutSelection`, `pasteAtActive`
471
+ **Keyboard:** Arrows, Tab, Enter/F2 edit, Escape cancel, Ctrl+arrows jump, PageUp/Down, Ctrl+A, Ctrl+C/V/X, Delete, Space (checkboxes), type-to-edit
472
+ **Sorting:** Click header (asc/desc/none), Shift+click for multi-column
473
+ **Clipboard:** TSV format — interop with Excel, Sheets, Numbers
474
+ **Data attributes:** `$active`, `$selected`, `$sorted`, `$editing`, `$selecting`
555
475
 
556
- Layouts with an `onError` method catch errors from child components:
476
+ ### Collapsible
557
477
 
558
478
  ```coffee
559
- export Layout = component
560
- errorMsg := null
561
-
562
- onError: (err) -> errorMsg = err.message
563
-
564
- render
565
- .app-layout
566
- if errorMsg
567
- .error-banner "#{errorMsg}"
568
- #content
479
+ Collapsible open <=> isOpen
480
+ button $trigger: true, "Show details"
481
+ div $content: true
482
+ p "Hidden content here"
569
483
  ```
570
484
 
571
- ## Navigation Indicator
485
+ **Methods:** `toggle()`
486
+ **Data attributes:** `$open`, `$disabled`
487
+ **CSS custom properties:** `--collapsible-height`, `--collapsible-width`
572
488
 
573
- `router.navigating` is a reactive signal — true while a route transition
574
- is in progress:
489
+ ### Pagination
575
490
 
576
491
  ```coffee
577
- if @router.navigating
578
- span "Loading..."
492
+ Pagination page <=> currentPage, total: 100, perPage: 10
579
493
  ```
580
494
 
581
- ## Multi-App Hosting
495
+ **Keyboard:** ArrowLeft/Right, Home/End
496
+ **Data attributes:** `$active`, `$disabled`, `$ellipsis`
582
497
 
583
- Mount multiple apps under one server:
498
+ ### Carousel
584
499
 
585
500
  ```coffee
586
- import { get, start, notFound } from '@rip-lang/api'
587
- import { mount as demo } from './demo/index.rip'
588
- import { mount as labs } from './labs/index.rip'
589
-
590
- demo '/demo'
591
- labs '/labs'
592
- get '/', -> Response.redirect('/demo/', 302)
593
- start port: 3002
501
+ Carousel loop: true
502
+ div $slide: true, "Slide 1"
503
+ div $slide: true, "Slide 2"
504
+ div $slide: true, "Slide 3"
594
505
  ```
595
506
 
596
- The `/rip/` namespace is shared — all apps use the same compiler and framework.
507
+ **Methods:** `goto(index)`, `next()`, `prev()`
508
+ **Behavior:** Autoplay pauses on hover
597
509
 
598
- ## File Structure
510
+ ### Drawer
599
511
 
600
- ```
601
- my-app/
602
- ├── index.rip # Server
603
- ├── index.html # HTML page
604
- ├── pages/ # Page components (file-based routing)
605
- │ ├── _layout.rip # Root layout
606
- │ ├── index.rip # Home → /
607
- │ ├── about.rip # About → /about
608
- │ └── users/
609
- │ └── [id].rip # User profile → /users/:id
610
- ├── ui/ # Shared components (no routes)
611
- │ └── card.rip # Card → available as Card
612
- └── css/
613
- └── styles.css # Styles
512
+ ```coffee
513
+ Drawer open <=> showDrawer, side: "left"
514
+ nav "Sidebar content"
614
515
  ```
615
516
 
616
- Files starting with `_` don't generate routes (`_layout.rip` is a layout,
617
- not a page). Directories starting with `_` are also excluded, which is how
618
- shared components from `includes` stay out of the router.
517
+ **Props:** `@open`, `@side` (top/right/bottom/left), `@dismissable`
518
+ **Behavior:** Focus trap, scroll lock, Escape to close
619
519
 
620
- ## Hash Routing
621
-
622
- For static hosting (GitHub Pages, S3, etc.) where the server can't handle
623
- SPA fallback routing, use hash-based URLs:
520
+ ### Breadcrumb
624
521
 
625
522
  ```coffee
626
- launch '/app', hash: true
523
+ Breadcrumb
524
+ a $item: true, href: "/", "Home"
525
+ a $item: true, href: "/products", "Products"
526
+ span $item: true, "Widget Pro"
627
527
  ```
628
528
 
629
- This switches from `/about` to `page.html#/about`. Back/forward navigation,
630
- direct URL loading, and `href="#/path"` links all work correctly.
631
-
632
- ## Static Deployment
633
-
634
- For zero-server deployment, use inline `data-name` scripts or a `components`
635
- URL list. Both work with `rip-ui.min.js` (~52KB Brotli) from a CDN — no
636
- server middleware needed.
529
+ **ARIA:** `aria-current="page"` on last item
637
530
 
638
- **Inline mode** — everything in one HTML file:
531
+ ### Resizable
639
532
 
640
- ```html
641
- <script type="module" src="dist/rip-ui.min.js"></script>
642
-
643
- <script type="text/rip" data-name="index">
644
- export Home = component
645
- render
646
- h1 "Hello"
647
- </script>
533
+ ```coffee
534
+ Resizable
535
+ div $panel: true, "Left"
536
+ div $panel: true, "Right"
537
+ ```
648
538
 
649
- <script type="text/rip" data-name="about">
650
- export About = component
651
- render
652
- h1 "About"
653
- </script>
539
+ **ARIA:** `role="separator"` on handles
540
+ **CSS custom properties:** `--panel-size` on each panel
654
541
 
655
- <script type="text/rip">
656
- { launch } = importRip! 'ui.rip'
657
- launch hash: true
658
- </script>
659
- ```
542
+ ### Context Sharing: `offer` / `accept`
660
543
 
661
- **Static files mode** `.rip` files served from any HTTP server or CDN:
544
+ For compound components where descendants need shared state:
662
545
 
663
- ```html
664
- <script type="module" src="dist/rip-ui.min.js"></script>
665
- <script type="text/rip">
666
- { launch } = importRip! 'ui.rip'
667
- launch components: ['components/index.rip', 'components/about.rip'], hash: true
668
- </script>
669
- ```
546
+ ```coffee
547
+ # Parent offers reactive state to all descendants
548
+ export Tabs = component
549
+ offer active := 'overview'
670
550
 
671
- **Explicit bundle** pass a bundle object directly:
672
-
673
- ```html
674
- <script type="module" src="dist/rip-ui.min.js"></script>
675
- <script type="text/rip">
676
- { launch } = importRip! 'ui.rip'
677
-
678
- launch bundle:
679
- '/': '''
680
- export Home = component
681
- render
682
- h1 "Hello"
683
- '''
684
- '/about': '''
685
- export About = component
686
- render
687
- h1 "About"
688
- '''
689
- , hash: true
690
- </script>
551
+ # Child accepts the shared signal
552
+ export TabContent = component
553
+ accept active
554
+ render
555
+ div hidden: active isnt @value
556
+ slot
691
557
  ```
692
558
 
693
- See `docs/demo.html` for a complete example the full Rip UI Demo app
694
- (6 components, router, reactive state, persistence) in 337 lines of
695
- static HTML.
559
+ Parent and child share the same reactive object mutations in either
560
+ direction are instantly visible. No Provider wrappers, no string keys.
696
561
 
697
- ## Tailwind CSS Autocompletion
562
+ ---
698
563
 
699
- To get Tailwind class autocompletion inside `.()` CLSX helpers in render
700
- templates, install the
701
- [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)
702
- extension and add these to your VS Code / Cursor settings:
564
+ ## File Summary
703
565
 
704
- ```json
705
- {
706
- "tailwindCSS.includeLanguages": { "rip": "html" },
707
- "tailwindCSS.experimental.classRegex": [
708
- ["\\.\\(([\\s\\S]*?)\\)", "'([^']*)'"]
709
- ]
710
- }
711
- ```
566
+ | Category | Files | Lines |
567
+ |----------|-------|-------|
568
+ | Selection | 4 | 638 |
569
+ | Toggle | 5 | 267 |
570
+ | Input | 9 | 854 |
571
+ | Navigation | 7 | 767 |
572
+ | Overlay | 7 | 700 |
573
+ | Display | 11 | 378 |
574
+ | Form | 4 | 140 |
575
+ | Data | 3 | 1,041 |
576
+ | Interactive | 4 | 406 |
577
+ | **Total** | **54** | **5,191** |
712
578
 
713
- This gives you autocompletion, hover previews, and linting for Tailwind
714
- classes in expressions like:
579
+ ---
715
580
 
716
- ```coffee
717
- h1.('text-3xl font-semibold') "Hello"
718
- button.('flex items-center px-4 py-2 rounded-full') "Click"
719
- ```
581
+ ## Status
720
582
 
721
- ## License
583
+ The reactive model, headless contract, and performance architecture are
584
+ proven. The compiler has 1,436 tests. The widget suite is comprehensive
585
+ but still maturing — tests are being added, and a few widgets have known
586
+ structural issues being resolved (see [CONTRIBUTING.md](CONTRIBUTING.md)
587
+ for details).
722
588
 
723
- MIT
589
+ For widget authoring patterns, implementation notes, known issues, and the
590
+ development roadmap, see **[CONTRIBUTING.md](CONTRIBUTING.md)**.