@rip-lang/ui 0.3.20 → 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 +442 -572
  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 -522
  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,720 +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>
25
+ ```bash
26
+ cd packages/ui
27
+ rip server
29
28
  ```
30
29
 
31
- **`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
32
36
 
33
- ```coffee
34
- export Home = component
35
- @count := 0
36
- render
37
- .
38
- h1 "Hello from Rip UI"
39
- button @click: (-> @count += 1), "Clicked #{@count} times"
40
- ```
37
+ ---
41
38
 
42
- Run `bun index.rip`, open `http://localhost:3000`.
43
-
44
- ## The Two Keywords
45
-
46
- Rip UI adds two keywords to the language: `component` and `render`. Each
47
- serves a distinct role, and together they form a complete reactive UI model.
48
-
49
- ### `component` — the model
50
-
51
- Raw Rip Lang has no concept of a self-contained, reusable UI unit. The
52
- `component` keyword adds everything needed to manage interactive state:
53
-
54
- - **Reactive state** (`:=`) — assignments create signals that trigger
55
- updates automatically. `count := 0` is not a plain variable; changing
56
- it updates the DOM.
57
- - **Computed values** (`~=`) — derived values that recalculate when their
58
- dependencies change. `remaining ~= todos.filter((t) -> not t.done).length`
59
- - **Effects** (`~>`) — side effects that run whenever reactive dependencies
60
- change. `~> @app.data.count = count`
61
- - **Props** (`@` prefix, `=!` for readonly) — a public API for parent
62
- components to pass data in, with signal passthrough for shared reactivity.
63
- - **Lifecycle hooks** — `beforeMount`, `mounted`, `updated`, `beforeUnmount`,
64
- `unmounted` for running code at specific points in a component's life.
65
- - **Context API** — `setContext` and `getContext` for ancestor-to-descendant
66
- data sharing without prop drilling.
67
- - **Mount/unmount mechanics** — attaching to the DOM, cascading teardown
68
- to children, and keep-alive caching across navigation.
69
- - **Encapsulation** — each component is a class with its own scope, state,
70
- and methods. No global variable collisions, no leaking internals.
71
-
72
- A component without a render block can still hold state, run effects, and
73
- participate in the component tree — it just has no visual output.
74
-
75
- ### `render` — the view
76
-
77
- The `render` keyword provides a declarative template DSL for describing DOM
78
- structure. It has its own lexer pass and syntax rules distinct from regular
79
- Rip code:
80
-
81
- - **Element creation** — tag names become DOM nodes: `div`, `h1`, `button`
82
- - **CSS-selector shortcuts** — `div.card.active`, `#main`, `.card` (implicit `div`)
83
- - **Dynamic classes** — `div.('card', active && 'active')` with CLSX semantics
84
- - **Event handlers** — `@click: handler` compiles to `addEventListener`
85
- - **Two-way binding** — `value <=> username` wires reactive read and write
86
- (see [Two-Way Binding](#two-way-binding--the--operator) below)
87
- - **Conditionals and loops** — `if`/`else` and `for item in items` with
88
- anchor-based DOM insertion and keyed reconciliation
89
- - **Children/slots** — `@children` receives child nodes, `#content` marks
90
- layout insertion points
91
- - **Component instantiation** — PascalCase names like `Card title: "Hello"`
92
- resolve to components automatically, no imports needed
93
-
94
- Render compiles to two methods: `_create()` builds the DOM tree once, and
95
- `_setup()` wires reactive effects for fine-grained updates. There is no
96
- virtual DOM — each reactive binding creates a direct DOM effect that updates
97
- the specific text node or attribute that depends on it.
98
-
99
- A render block can only exist inside a component. It needs the component's
100
- signals, computed values, and lifecycle to have something to render and
101
- react to.
102
-
103
- ### Together
104
-
105
- `component` provides the **model** — state, reactivity, lifecycle, identity.
106
- `render` provides the **view** — a concise way to describe what the DOM
107
- should look like and how it stays in sync with that state. One defines
108
- behavior, the other defines structure. Neither is useful without the other
109
- in practice, but they are separate concerns with separate syntax.
110
-
111
- ## Component Composition
112
-
113
- Page components in `pages/` map to routes via file-based routing. Shared
114
- components in `ui/` (or any `includes` directory) are available by PascalCase
115
- name. No imports needed:
39
+ ## Rip in 60 Seconds
116
40
 
117
- ```coffee
118
- # ui/card.rip
119
- export Card = component
120
- title =! ""
121
- render
122
- .card
123
- if title
124
- h3 "#{title}"
125
- @children
41
+ If you're coming from React or another framework, here's the Rip you need
42
+ to know to use these widgets:
126
43
 
127
- # pages/about.rip
128
- export About = component
129
- render
130
- .
131
- h1 "About"
132
- Card title: "The Idea"
133
- p "Components compose naturally."
134
- Card title: "Architecture"
135
- p "PascalCase resolution, signal passthrough, children blocks."
136
- ```
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 |
137
56
 
138
- Reactive props via `:=` signal passthrough. Readonly props via `=!`.
139
- Children blocks passed as DOM nodes via `@children`.
140
-
141
- ## Props — The `@` Contract
142
-
143
- The `@` prefix on a member declaration marks it as a **public prop** — settable
144
- by a parent component. Members without `@` are **private state** and ignore
145
- any value a parent tries to pass in.
57
+ Two-way binding example React vs Rip:
146
58
 
147
59
  ```coffee
148
- export Drawer = component
149
- @open := false # public — parent can pass `open: true`
150
- @breakpoint := 480 # public — parent can pass `breakpoint: 768`
151
- isRight := false # private — always starts as false
152
- closing := false # private always starts as 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)} />
153
65
 
154
- render
155
- if open
156
- div "Drawer is open"
66
+ # Rip: 1 line per binding
67
+ Dialog open <=> show
68
+ input value <=> @name
157
69
  ```
158
70
 
159
- The compiler enforces the boundary:
71
+ ---
160
72
 
161
- ```javascript
162
- // @open := false → accepts parent value
163
- this.open = __state(props.open ?? false);
73
+ ## Why Rip UI
164
74
 
165
- // isRight := false → ignores parent, always uses default
166
- this.isRight = __state(false);
167
- ```
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 |
168
87
 
169
- This works for all member types:
88
+ ### Architecture
170
89
 
171
- | Declaration | Visibility | Meaning |
172
- |-------------|-----------|---------|
173
- | `@title := 'Hello'` | Public | Reactive state, settable by parent |
174
- | `@label =! 'Default'` | Public | Readonly prop, settable by parent |
175
- | `count := 0` | Private | Reactive state, internal only |
176
- | `cache =! null` | Private | Readonly, internal only |
177
- | `total ~= items.length` | — | Computed (always derived, never a prop) |
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.
178
94
 
179
- A parent passes props as key-value pairs when using a component:
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.
180
99
 
181
- ```coffee
182
- Drawer open: showDrawer, breakpoint: 768
183
- div "Content here"
184
- ```
185
-
186
- The `@` declarations at the top of a component are its public API. Everything
187
- else is an implementation detail. No separate type files, no prop validation
188
- boilerplate — one character that says "this is settable from outside."
189
-
190
- ## Render Block Syntax
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.
191
103
 
192
- Inside a `render` block, elements are declared by tag name. Classes, attributes,
193
- and children can be expressed inline or across multiple indented lines.
104
+ **Source as distribution.** Components are served as `.rip` source files.
105
+ Read them, understand them, modify them.
194
106
 
195
- ### Classes with `.(...)`
107
+ ### Why We Build Our Own
196
108
 
197
- The `.()` helper applies classes using CLSX semantics strings are included
198
- directly, and object keys are conditionally included based on their values:
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:
199
112
 
200
- ```coffee
201
- button.('px-4 py-2 rounded-full') "Click"
202
- button.('px-4 py-2', active: isActive) "Click"
203
- ```
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 | `:=` / `~=` / `~>` |
204
120
 
205
- Arguments can span multiple lines, just like a normal function call:
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.
206
125
 
207
- ```coffee
208
- input.(
209
- 'block w-full rounded-lg border border-primary',
210
- 'text-sm-plus text-tertiary shadow-xs'
211
- )
212
- ```
126
+ ---
213
127
 
214
- ### Indented Attributes
128
+ ## Styling
215
129
 
216
- Attributes can be placed on separate indented lines after the element:
130
+ All widgets ship zero CSS. The contract between behavior and styling is
131
+ `data-*` attributes:
217
132
 
218
133
  ```coffee
219
- input.('rounded-lg border px-3.5 py-2.5')
220
- type: "email"
221
- value: user.email
222
- disabled: true
134
+ # Widget exposes semantic state
135
+ button $open: open?!, $disabled: @disabled?!
223
136
  ```
224
137
 
225
- This is equivalent to the inline form:
226
-
227
- ```coffee
228
- input.('rounded-lg border px-3.5 py-2.5') type: "email", value: user.email, disabled: true
138
+ ```css
139
+ /* You write the styles */
140
+ [data-open] { border-color: var(--color-primary); }
141
+ [data-disabled] { opacity: 0.5; cursor: not-allowed; }
229
142
  ```
230
143
 
231
- ### The `class:` Attribute
144
+ Any CSS methodology works — vanilla CSS, Tailwind, Open Props, a custom
145
+ design system. The widgets don't care.
232
146
 
233
- The `class:` attribute works like `.()` and merges cumulatively with any
234
- existing `.()` classes on the same element:
147
+ For our recommended approach design tokens, CSS architecture, dark mode,
148
+ and the rationale behind it see **[STYLING.md](STYLING.md)**.
235
149
 
236
- ```coffee
237
- input.('block w-full rounded-lg')
238
- class: 'text-sm text-tertiary'
239
- type: "email"
240
- ```
150
+ ---
241
151
 
242
- This produces a single combined class expression: `block w-full rounded-lg text-sm text-tertiary`.
152
+ ## Code Density
243
153
 
244
- The `class:` value also supports `.()` syntax for conditional classes:
154
+ ### Checkbox 18 Lines
245
155
 
246
156
  ```coffee
247
- div.('mt-4 p-4')
248
- class: .('ring-1', highlighted: isHighlighted)
249
- span "Content"
250
- ```
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
251
168
 
252
- ### Attributes and Children Together
253
-
254
- Attributes and children can coexist at the same indentation level. Attributes
255
- (key-value pairs) are listed first, followed by child elements:
256
-
257
- ```coffee
258
- button.('flex items-center rounded-lg')
259
- type: "submit"
260
- disabled: saving
261
-
262
- span.('font-bold') "Submit"
263
- 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
264
177
  ```
265
178
 
266
- Blank lines between attributes and children are fine they don't break the
267
- structure.
268
-
269
- ## Two-Way Binding — The `<=>` Operator
179
+ Full ARIA. Checkbox and switch modes. Indeterminate state. Data attributes
180
+ for styling. 18 lines, complete.
270
181
 
271
- The `<=>` operator is one of Rip UI's most powerful features. It creates a
272
- bidirectional reactive binding between a parent's state and a child element
273
- or component — changes flow in both directions automatically.
182
+ ### Dialog Effect-Based Lifecycle
274
183
 
275
- ### The Problem It Solves
184
+ Focus trap, scroll lock, escape dismiss, click-outside dismiss, focus
185
+ restore — all in one reactive effect with automatic cleanup:
276
186
 
277
- In React, wiring state to interactive elements requires explicit value props
278
- and callback handlers for every bindable property:
279
-
280
- ```jsx
281
- // React: verbose, repetitive ceremony
282
- const [name, setName] = useState('');
283
- const [role, setRole] = useState('viewer');
284
- const [notify, setNotify] = useState(true);
285
- 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
286
315
 
287
- <input value={name} onChange={e => setName(e.target.value)} />
288
- <Select value={role} onValueChange={setRole} />
289
- <Switch checked={notify} onCheckedChange={setNotify} />
290
- <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"
291
321
  ```
292
322
 
293
- Every bindable property needs a state declaration AND a setter callback.
294
- This is the single most tedious pattern in React development.
295
-
296
- ### The Rip Way
323
+ **Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close, Home/End, type-ahead
324
+ **Data attributes:** `$open`, `$highlighted`, `$selected`, `$disabled`
297
325
 
298
- In Rip, `<=>` replaces all of that with a single operator:
326
+ ### Combobox
299
327
 
300
328
  ```coffee
301
- export UserForm = component
302
- @name := ''
303
- @role := 'viewer'
304
- @notify := true
305
- @showConfirm := false
306
-
307
- render
308
- input value <=> @name
309
- Select value <=> @role
310
- Option value: "viewer", "Viewer"
311
- Option value: "editor", "Editor"
312
- Option value: "admin", "Admin"
313
- Switch checked <=> @notify
314
- Dialog open <=> @showConfirm
315
- 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
316
333
  ```
317
334
 
318
- No `onChange`. No `onValueChange`. No `onOpenChange`. No `setName`, `setRole`,
319
- `setNotify`, `setShowConfirm`. The reactive system handles everything — state
320
- flows down, user interactions flow back up.
321
-
322
- ### How It Works
323
-
324
- `value <=> username` compiles to two things:
325
-
326
- 1. **State → DOM** (reactive effect): `__effect(() => { el.value = username; })`
327
- 2. **DOM → State** (event listener): `el.addEventListener('input', (e) => { username = e.target.value; })`
328
-
329
- The compiler is smart about types:
330
- - `value <=>` on text inputs uses the `input` event and `e.target.value`
331
- - `value <=>` on number/range inputs uses `e.target.valueAsNumber`
332
- - `checked <=>` uses the `change` event and `e.target.checked`
333
-
334
- For custom components, `<=>` passes the reactive signal itself, enabling the
335
- child to both read and write the parent's state directly — no callback
336
- indirection.
335
+ **Keyboard:** ArrowDown/Up navigate, Enter select, Escape close/clear, Tab close
336
+ **Data attributes:** `$open`, `$highlighted`
337
337
 
338
- ### Auto-Detection
339
-
340
- Even without `<=>`, the compiler auto-detects when `value:` or `checked:` is
341
- bound to a reactive expression and generates two-way binding automatically:
338
+ ### Dialog
342
339
 
343
340
  ```coffee
344
- # These are equivalent:
345
- input value <=> @name # explicit two-way binding
346
- 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"
347
346
  ```
348
347
 
349
- ### Why This Matters
350
-
351
- Two-way binding is what Vue has with `v-model`, what Svelte has with `bind:`,
352
- and what Angular has with `[(ngModel)]`. React is the only major framework
353
- that deliberately omits it, forcing the verbose controlled component pattern
354
- instead.
355
-
356
- Rip's `<=>` goes further than Vue or Svelte — it works uniformly across HTML
357
- elements and custom components with the same syntax. A `Dialog open <=> show`
358
- and an `input value <=> name` use the same operator, the same mental model,
359
- and the same compilation strategy. This makes headless interactive components
360
- dramatically cleaner to use than their React equivalents.
361
-
362
- ## How It Works
363
-
364
- The browser loads one file — `rip-ui.min.js` (~53KB Brotli) — which bundles the
365
- Rip compiler and the pre-compiled UI framework. No runtime compilation of the
366
- framework, no extra network requests.
367
-
368
- The runtime auto-detects `<script type="text/rip" data-name="...">` components
369
- on the page and calls `launch()` automatically with hash routing enabled by
370
- default. For server-rendered apps, `launch()` fetches the app bundle from the
371
- server. Either way, it hydrates the stash and renders.
348
+ **Keyboard:** Escape to close, Tab trapped within dialog
349
+ **Data attributes:** `$open`
350
+ **Behavior:** Focus trap, body scroll lock, focus restore on close
372
351
 
373
- ### Browser Execution Contexts
352
+ ### AlertDialog
374
353
 
375
- Rip provides full async/await support across every browser context — no other
376
- compile-to-JS language has this:
377
-
378
- | Context | How async works | Returns value? |
379
- |---------|-----------------|----------------|
380
- | `<script type="text/rip">` | Async IIFE wrapper | No (fire-and-forget) |
381
- | Playground "Run" button | Async IIFE wrapper | No (use console.log) |
382
- | `rip()` console REPL | Rip `do ->` block | Yes (sync direct, async via Promise) |
383
- | `.rip` files via `importRip()` | ES module import | Yes (module exports) |
384
-
385
- The `!` postfix compiles to `await`. Inline scripts are wrapped in an async IIFE
386
- automatically. The `rip()` console function wraps user code in a `do ->` block
387
- so the Rip compiler handles implicit return and auto-async natively.
388
-
389
- ### globalThis Exports
390
-
391
- When `rip-ui.min.js` loads, it registers these on `globalThis`:
392
-
393
- | Function | Purpose |
394
- |----------|---------|
395
- | `rip(code)` | Console REPL — compile and execute Rip code |
396
- | `importRip(url)` | Fetch, compile, and import a `.rip` file as an ES module |
397
- | `compileToJS(code)` | Compile Rip source to JavaScript |
398
- | `__rip` | Reactive runtime — `__state`, `__computed`, `__effect`, `__batch` |
399
- | `__ripComponent` | Component runtime — `__Component`, `__clsx`, `__fragment` |
400
- | `__ripExports` | All compiler exports — `compile`, `formatSExpr`, `VERSION`, etc. |
401
-
402
- ## The Stash
403
-
404
- App state lives in one reactive tree:
405
-
406
- ```
407
- app
408
- ├── routes ← navigation state (path, params, query, hash)
409
- └── data ← reactive app state (title, theme, user, etc.)
410
- ```
411
-
412
- Writing to `app.data.theme` updates any component reading it. The stash
413
- uses Rip's built-in reactive primitives — the same signals that power
414
- `:=` and `~=` in components.
415
-
416
- ## The App Bundle
417
-
418
- The bundle is JSON served at `/{app}/bundle`:
419
-
420
- ```json
421
- {
422
- "components": {
423
- "components/index.rip": "export Home = component...",
424
- "components/counter.rip": "export Counter = component...",
425
- "components/_lib/card.rip": "export Card = component..."
426
- },
427
- "data": {
428
- "title": "My App",
429
- "theme": "light"
430
- }
431
- }
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"
432
360
  ```
433
361
 
434
- On disk you organize your app into `pages/` and `ui/`. The middleware
435
- maps them into a flat `components/` namespace in the bundle — pages go
436
- under `components/`, shared components under `components/_lib/`. The `_`
437
- prefix tells the router to skip `_lib/` entries when generating routes.
438
-
439
- ## Component Loading Modes
362
+ Like Dialog but cannot be closed by Escape or click outside.
363
+ **ARIA:** `role="alertdialog"`, auto-wired `aria-labelledby`/`aria-describedby`
440
364
 
441
- `launch()` supports three ways to load component sources, checked in priority
442
- order. All three produce the same internal bundle format — everything downstream
443
- (compilation, routing, rendering) works identically regardless of source.
444
-
445
- ### 1. Static File URLs — `launch components: [...]`
446
-
447
- Fetch individual `.rip` files as plain text from any static server:
365
+ ### Toast
448
366
 
449
367
  ```coffee
450
- launch components: [
451
- 'components/index.rip'
452
- 'components/dashboard.rip'
453
- 'components/line-chart.rip'
454
- ]
368
+ toasts := []
369
+ toasts = [...toasts, { message: "Saved!", type: "success" }]
370
+ toasts = toasts.filter (t) -> t isnt target
371
+ ToastViewport toasts <=> toasts
455
372
  ```
456
373
 
457
- No server middleware needed. Serve `.rip` files as static text from any HTTP
458
- server, CDN, or `file://` path. Each URL is fetched individually and compiled
459
- in the browser.
460
-
461
- ### 2. Inline DOM — `<script type="text/rip" data-name="...">`
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
462
378
 
463
- Embed component source directly in the HTML page:
379
+ ### Tabs
464
380
 
465
- ```html
466
- <script type="module" src="rip-ui.min.js"></script>
467
-
468
- <script type="text/rip" data-name="index">
469
- export Home = component
470
- render
471
- h1 "Hello from inline"
472
- </script>
473
-
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>
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"
480
389
  ```
481
390
 
482
- The runtime auto-detects `data-name` scripts and launches automatically — no
483
- bootstrap script needed. The `data-name` attribute maps to the component
484
- filename (`.rip` extension is added automatically if omitted). Scripts with
485
- `data-name` are collected as component sources and are not executed as
486
- top-level code.
391
+ **Keyboard:** ArrowLeft/Right navigate, Home/End jump
392
+ **Data attributes:** `$active`
487
393
 
488
- ### 3. Server Bundle (default)
489
-
490
- When neither `components` nor inline `data-name` scripts are present, `launch()`
491
- fetches the app bundle from the server at `/{app}/bundle`. This is the default
492
- mode when using the `ripUI` server middleware.
394
+ ### Accordion
493
395
 
494
396
  ```coffee
495
- launch() # fetches /bundle automatically
397
+ Accordion multiple: false
398
+ div $item: "a"
399
+ button $trigger: true, "Section A"
400
+ div $content: true
401
+ p "Content A"
496
402
  ```
497
403
 
498
- ## Server Middleware
404
+ **Keyboard:** Enter/Space toggle, ArrowDown/Up between triggers, Home/End
405
+ **Methods:** `toggle(id)`, `isOpen(id)`
499
406
 
500
- The `ripUI` middleware registers routes for the framework files, the app
501
- bundle, and optional SSE hot-reload:
407
+ ### Checkbox
502
408
 
503
409
  ```coffee
504
- use ripUI dir: dir, components: 'routes', includes: ['ui'], watch: true, title: 'My App'
505
- ```
506
-
507
- | Option | Default | Description |
508
- |--------|---------|-------------|
509
- | `app` | `''` | URL mount point |
510
- | `dir` | `'.'` | App directory on disk |
511
- | `components` | `'components'` | Directory for page components (file-based routing) |
512
- | `includes` | `[]` | Directories for shared components (no routes) |
513
- | `watch` | `false` | Enable SSE hot-reload |
514
- | `debounce` | `250` | Milliseconds to batch file change events |
515
- | `state` | `null` | Initial app state |
516
- | `title` | `null` | Document title |
410
+ Checkbox checked <=> isActive, @change: handleChange
411
+ span "Enable notifications"
517
412
 
518
- Routes registered:
519
-
520
- ```
521
- /rip/rip-ui.min.js — Rip compiler + pre-compiled UI framework
522
- /{app}/bundle — app bundle (components + data as JSON)
523
- /{app}/watch — SSE hot-reload stream (when watch: true)
524
- /{app}/components/* — individual component files (for hot-reload refetch)
413
+ Checkbox checked <=> isDark, switch: true
414
+ span "Dark mode"
525
415
  ```
526
416
 
527
- ## State Preservation (Keep-Alive)
528
-
529
- Components are cached when navigating away instead of destroyed. Navigate
530
- to `/counter`, increment the count, go to `/about`, come back — the count
531
- is preserved. Configurable via `cacheSize` (default 10).
417
+ **ARIA:** `role="checkbox"` or `role="switch"`, `aria-checked` (true/false/mixed)
418
+ **Data attributes:** `$checked`, `$indeterminate`, `$disabled`
532
419
 
533
- ## Data Loading
534
-
535
- `createResource` manages async data with reactive `loading`, `error`, and
536
- `data` properties:
420
+ ### Menu
537
421
 
538
422
  ```coffee
539
- export UserPage = component
540
- user := createResource -> fetch!("/api/users/#{@params.id}").json!
541
-
542
- render
543
- if user.loading
544
- p "Loading..."
545
- else if user.error
546
- p "Error: #{user.error.message}"
547
- else
548
- h1 user.data.name
423
+ Menu @select: handleAction
424
+ button $trigger: true, "Actions"
425
+ div $item: "edit", "Edit"
426
+ div $item: "delete", "Delete"
549
427
  ```
550
428
 
551
- ## Error Boundaries
429
+ **Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close
430
+ **Data attributes:** `$open`, `$highlighted`
552
431
 
553
- Layouts with an `onError` method catch errors from child components:
432
+ ### Popover
554
433
 
555
434
  ```coffee
556
- export Layout = component
557
- errorMsg := null
558
-
559
- onError: (err) -> errorMsg = err.message
560
-
561
- render
562
- .app-layout
563
- if errorMsg
564
- .error-banner "#{errorMsg}"
565
- #content
435
+ Popover placement: "bottom-start"
436
+ button "Options"
437
+ div
438
+ p "Popover content here"
566
439
  ```
567
440
 
568
- ## Navigation Indicator
441
+ **Keyboard:** Enter/Space/ArrowDown toggle, Escape close
442
+ **Data attributes:** `$open`, `$placement`
569
443
 
570
- `router.navigating` is a reactive signal — true while a route transition
571
- is in progress:
444
+ ### Tooltip
572
445
 
573
446
  ```coffee
574
- if @router.navigating
575
- span "Loading..."
447
+ Tooltip text: "Save your changes", placement: "top"
448
+ button "Save"
576
449
  ```
577
450
 
578
- ## Multi-App Hosting
451
+ **Data attributes:** `$open`, `$entering`, `$exiting`, `$placement`
452
+ **Behavior:** Shows after delay on hover/focus, uses `aria-describedby`
579
453
 
580
- Mount multiple apps under one server:
454
+ ### Grid
581
455
 
582
456
  ```coffee
583
- import { get, start, notFound } from '@rip-lang/api'
584
- import { mount as demo } from './demo/index.rip'
585
- import { mount as labs } from './labs/index.rip'
586
-
587
- demo '/demo'
588
- labs '/labs'
589
- get '/', -> Response.redirect('/demo/', 302)
590
- start port: 3002
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
591
467
  ```
592
468
 
593
- The `/rip/` namespace is shared — all apps use the same compiler and framework.
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`
594
475
 
595
- ## File Structure
476
+ ### Collapsible
596
477
 
597
- ```
598
- my-app/
599
- ├── index.rip # Server
600
- ├── index.html # HTML page
601
- ├── pages/ # Page components (file-based routing)
602
- │ ├── _layout.rip # Root layout
603
- │ ├── index.rip # Home → /
604
- │ ├── about.rip # About → /about
605
- │ └── users/
606
- │ └── [id].rip # User profile → /users/:id
607
- ├── ui/ # Shared components (no routes)
608
- │ └── card.rip # Card → available as Card
609
- └── css/
610
- └── styles.css # Styles
478
+ ```coffee
479
+ Collapsible open <=> isOpen
480
+ button $trigger: true, "Show details"
481
+ div $content: true
482
+ p "Hidden content here"
611
483
  ```
612
484
 
613
- Files starting with `_` don't generate routes (`_layout.rip` is a layout,
614
- not a page). Directories starting with `_` are also excluded, which is how
615
- shared components from `includes` stay out of the router.
485
+ **Methods:** `toggle()`
486
+ **Data attributes:** `$open`, `$disabled`
487
+ **CSS custom properties:** `--collapsible-height`, `--collapsible-width`
616
488
 
617
- ## Hash Routing
489
+ ### Pagination
618
490
 
619
- Hash routing is **enabled by default** for auto-launched apps — ideal for
620
- static hosting (GitHub Pages, S3, etc.) where the server can't handle SPA
621
- fallback routing. URLs use `page.html#/about` instead of `/about`.
622
- Back/forward navigation, direct URL loading, and `href="#/path"` links all
623
- work correctly.
624
-
625
- To disable hash routing (e.g., for server-rendered apps with proper fallback):
626
-
627
- ```html
628
- <script type="module" src="rip-ui.min.js" data-hash="false"></script>
491
+ ```coffee
492
+ Pagination page <=> currentPage, total: 100, perPage: 10
629
493
  ```
630
494
 
631
- Or when calling `launch()` manually:
495
+ **Keyboard:** ArrowLeft/Right, Home/End
496
+ **Data attributes:** `$active`, `$disabled`, `$ellipsis`
497
+
498
+ ### Carousel
632
499
 
633
500
  ```coffee
634
- launch hash: false
501
+ Carousel loop: true
502
+ div $slide: true, "Slide 1"
503
+ div $slide: true, "Slide 2"
504
+ div $slide: true, "Slide 3"
635
505
  ```
636
506
 
637
- ## Static Deployment
507
+ **Methods:** `goto(index)`, `next()`, `prev()`
508
+ **Behavior:** Autoplay pauses on hover
638
509
 
639
- For zero-server deployment, use inline `data-name` scripts or a `components`
640
- URL list. Both work with `rip-ui.min.js` (~53KB Brotli) from a CDN — no
641
- server middleware needed, no bootstrap script needed.
510
+ ### Drawer
642
511
 
643
- **Inline mode** — everything in one HTML file:
512
+ ```coffee
513
+ Drawer open <=> showDrawer, side: "left"
514
+ nav "Sidebar content"
515
+ ```
644
516
 
645
- ```html
646
- <script type="module" src="rip-ui.min.js"></script>
517
+ **Props:** `@open`, `@side` (top/right/bottom/left), `@dismissable`
518
+ **Behavior:** Focus trap, scroll lock, Escape to close
647
519
 
648
- <script type="text/rip" data-name="index">
649
- export Home = component
650
- render
651
- h1 "Hello"
652
- </script>
520
+ ### Breadcrumb
653
521
 
654
- <script type="text/rip" data-name="about">
655
- export About = component
656
- render
657
- h1 "About"
658
- </script>
522
+ ```coffee
523
+ Breadcrumb
524
+ a $item: true, href: "/", "Home"
525
+ a $item: true, href: "/products", "Products"
526
+ span $item: true, "Widget Pro"
659
527
  ```
660
528
 
661
- The runtime auto-detects the `data-name` components and launches with hash
662
- routing. That's it — no bootstrap, no config.
529
+ **ARIA:** `aria-current="page"` on last item
663
530
 
664
- **Remote bundle** — fetch components from a URL:
531
+ ### Resizable
665
532
 
666
- ```html
667
- <script type="module" src="rip-ui.min.js" data-url="https://example.com/app/"></script>
533
+ ```coffee
534
+ Resizable
535
+ div $panel: true, "Left"
536
+ div $panel: true, "Right"
668
537
  ```
669
538
 
670
- The `data-url` attribute tells the runtime to fetch the app bundle from the
671
- given URL (appending `/bundle` to the path).
539
+ **ARIA:** `role="separator"` on handles
540
+ **CSS custom properties:** `--panel-size` on each panel
672
541
 
673
- **Manual launch** for full control, use a bare `<script type="text/rip">`:
542
+ ### Context Sharing: `offer` / `accept`
674
543
 
675
- ```html
676
- <script type="module" src="rip-ui.min.js"></script>
677
- <script type="text/rip">
678
- { launch } = importRip! 'ui.rip'
679
- launch components: ['components/index.rip', 'components/about.rip']
680
- </script>
681
- ```
544
+ For compound components where descendants need shared state:
682
545
 
683
- **Inline Rip** — run arbitrary Rip code alongside auto-launched apps:
546
+ ```coffee
547
+ # Parent offers reactive state to all descendants
548
+ export Tabs = component
549
+ offer active := 'overview'
684
550
 
685
- ```html
686
- <script type="text/rip">
687
- alert "Free cheese rollups for the girls!"
688
- </script>
551
+ # Child accepts the shared signal
552
+ export TabContent = component
553
+ accept active
554
+ render
555
+ div hidden: active isnt @value
556
+ slot
689
557
  ```
690
558
 
691
- See `docs/results/index.html` for a complete examplea full Lab Results
692
- brochure app with 7 components, SVG gauges, and inline CSS in one HTML file.
559
+ Parent and child share the same reactive object mutations in either
560
+ direction are instantly visible. No Provider wrappers, no string keys.
693
561
 
694
- ## Tailwind CSS Autocompletion
562
+ ---
695
563
 
696
- To get Tailwind class autocompletion inside `.()` CLSX helpers in render
697
- templates, install the
698
- [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)
699
- extension and add these to your VS Code / Cursor settings:
564
+ ## File Summary
700
565
 
701
- ```json
702
- {
703
- "tailwindCSS.includeLanguages": { "rip": "html" },
704
- "tailwindCSS.experimental.classRegex": [
705
- ["\\.\\(([\\s\\S]*?)\\)", "'([^']*)'"]
706
- ]
707
- }
708
- ```
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** |
709
578
 
710
- This gives you autocompletion, hover previews, and linting for Tailwind
711
- classes in expressions like:
579
+ ---
712
580
 
713
- ```coffee
714
- h1.('text-3xl font-semibold') "Hello"
715
- button.('flex items-center px-4 py-2 rounded-full') "Click"
716
- ```
581
+ ## Status
717
582
 
718
- ## 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).
719
588
 
720
- MIT
589
+ For widget authoring patterns, implementation notes, known issues, and the
590
+ development roadmap, see **[CONTRIBUTING.md](CONTRIBUTING.md)**.