@rip-lang/ui 0.3.20 → 0.3.22

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 +446 -568
  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,598 @@
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.
4
+ Every widget exposes `$` attributes (compiled to `data-*`) for styling and
5
+ handles keyboard interactions per WAI-ARIA Authoring Practices. Style with
6
+ Tailwind, vanilla CSS, or any methodology you prefer.
4
7
 
5
- > **Zero-build reactive web framework for the Rip language.**
8
+ Components are plain `.rip` source files no build step. The browser compiles
9
+ them on the fly.
6
10
 
7
- Load the Rip compiler in the browser. Write inline Rip. Launch your app.
8
- No build step, no bundler, no configuration.
11
+ ---
9
12
 
10
13
  ## Quick Start
11
14
 
12
- **`index.rip`** the server:
15
+ Add the components directory to your serve middleware:
13
16
 
14
17
  ```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
18
+ use serve
19
+ dir: dir
20
+ components: ['components', '../../../packages/ui']
23
21
  ```
24
22
 
25
- **`index.html`** the page:
23
+ All widgets become available by name (`Select`, `Dialog`, `Grid`, etc.) in the
24
+ shared scope — no imports needed.
26
25
 
27
- ```html
28
- <script type="module" src="/rip/rip-ui.min.js"></script>
26
+ ```bash
27
+ cd packages/ui
28
+ rip server
29
29
  ```
30
30
 
31
- **`pages/index.rip`** — a page component:
31
+ Every widget:
32
+ - Handles all keyboard interactions per WAI-ARIA Authoring Practices
33
+ - Sets correct ARIA attributes automatically
34
+ - Exposes state via `$` sigil (`$open`, `$selected`) for CSS styling
35
+ - Ships no CSS — style with Tailwind or any methodology
36
+ - Uses Rip's reactive primitives for all state management
32
37
 
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
- ```
38
+ ---
41
39
 
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:
40
+ ## Rip in 60 Seconds
116
41
 
117
- ```coffee
118
- # ui/card.rip
119
- export Card = component
120
- title =! ""
121
- render
122
- .card
123
- if title
124
- h3 "#{title}"
125
- @children
42
+ If you're coming from React or another framework, here's the Rip you need
43
+ to know to use these widgets:
126
44
 
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
- ```
137
-
138
- Reactive props via `:=` signal passthrough. Readonly props via `=!`.
139
- Children blocks passed as DOM nodes via `@children`.
140
-
141
- ## Props — The `@` Contract
45
+ | Syntax | Name | What It Does |
46
+ |--------|------|-------------|
47
+ | `:=` | State | `count := 0` — reactive state (like `useState`) |
48
+ | `~=` | Computed | `doubled ~= count * 2` — derived value (like `useMemo`, but auto-tracked) |
49
+ | `~>` | Effect | `~> document.title = "#{count}"` — side effect (like `useEffect`, but auto-tracked) |
50
+ | `<=>` | Bind | `value <=> @name` — two-way binding between parent and child |
51
+ | `@prop` | Prop | `@checked`, `@disabled` — component props (reactive) |
52
+ | `$attr` | Data attr | `$open`, `$selected` — compiles to `data-open`, `data-selected` in HTML |
53
+ | `@emit` | Event | `@emit 'change', value` — dispatches a CustomEvent |
54
+ | `ref:` | DOM ref | `ref: "_panel"` — saves DOM element reference |
55
+ | `slot` | Children | Projects parent-provided content into the component |
56
+ | `offer` / `accept` | Context | Share reactive state between ancestor and descendant components |
142
57
 
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.
58
+ Two-way binding example React vs Rip:
146
59
 
147
60
  ```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
61
+ # React: 4 lines per binding
62
+ const [show, setShow] = useState(false)
63
+ <Dialog open={show} onOpenChange={setShow} />
64
+ const [name, setName] = useState('')
65
+ <input value={name} onChange={e => setName(e.target.value)} />
153
66
 
154
- render
155
- if open
156
- div "Drawer is open"
67
+ # Rip: 1 line per binding
68
+ Dialog open <=> show
69
+ input value <=> @name
157
70
  ```
158
71
 
159
- The compiler enforces the boundary:
72
+ ---
160
73
 
161
- ```javascript
162
- // @open := false → accepts parent value
163
- this.open = __state(props.open ?? false);
74
+ ## Why Rip UI
164
75
 
165
- // isRight := false → ignores parent, always uses default
166
- this.isRight = __state(false);
167
- ```
76
+ | | ShadCN / Radix | Rip UI |
77
+ |--|---------------|--------|
78
+ | Runtime dependency | React (~42KB gz) + ReactDOM | None |
79
+ | Component count | ~40 | 54 |
80
+ | Total source | ShadCN wrappers (~3K LOC) atop Radix (~20K+ LOC) | 5,191 SLOC — everything included |
81
+ | Build step | Required (Next.js, Vite, etc.) | None — browser compiles `.rip` source |
82
+ | Styling | Pre-wired Tailwind (ShadCN) or unstyled (Radix) | Headless — `data-*` contract, style with Tailwind or any CSS |
83
+ | Controlled components | `value` + `onChange` callback pair | `<=>` two-way binding |
84
+ | Shared state | React Context + Provider wrappers | `offer` / `accept` keywords |
85
+ | Reactivity | `useState` + `useEffect` + dependency arrays | `:=` / `~=` / `~>` — language-level |
86
+ | Virtual DOM | Yes (diffing on every render) | No — fine-grained updates to exact nodes |
87
+ | Data grid | Not included | 901 SLOC — 100K+ rows at 60fps |
168
88
 
169
- This works for all member types:
89
+ ### Architecture
170
90
 
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) |
91
+ **Fine-grained reactivity.** When `count` changes, only the text node
92
+ displaying `count` updates. No tree diffing, no wasted renders, no
93
+ memoization needed. Same model as SolidJS and Svelte 5's runes, but built
94
+ into the language.
178
95
 
179
- A parent passes props as key-value pairs when using a component:
96
+ **Components compile to JavaScript.** The `component` keyword, `render`
97
+ block, and reactive operators resolve at compile time into ES2022 classes
98
+ with direct DOM operations. Source maps point back to `.rip` source for
99
+ debugging.
180
100
 
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."
101
+ **No build pipeline.** The browser loads the Rip compiler (~50KB) once and
102
+ compiles `.rip` files on the fly. For production, pre-compile. For
103
+ development, save and see — SSE-based hot reload.
189
104
 
190
- ## Render Block Syntax
105
+ **Source as distribution.** Components are served as `.rip` source files.
106
+ Read them, understand them, modify them.
191
107
 
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.
108
+ ### Why We Build Our Own
194
109
 
195
- ### Classes with `.(...)`
110
+ Radix and Base UI implement proven WAI-ARIA patterns, but they require
111
+ React. Rip reimplements the same behavioral patterns using its own
112
+ primitives:
196
113
 
197
- The `.()` helper applies classes using CLSX semantics — strings are included
198
- directly, and object keys are conditionally included based on their values:
199
-
200
- ```coffee
201
- button.('px-4 py-2 rounded-full') "Click"
202
- button.('px-4 py-2', active: isActive) "Click"
203
- ```
114
+ | Capability | React | Rip |
115
+ |-----------|-------|-----|
116
+ | Child projection | No equivalent | `slot` |
117
+ | DOM ownership | Virtual DOM abstraction | Direct DOM + `ref:` |
118
+ | State sharing | Context + Provider wrappers | `offer` / `accept` |
119
+ | Two-way binding | `value` + `onChange` pair | `<=>` operator |
120
+ | Reactivity | Hooks + dependency arrays | `:=` / `~=` / `~>` |
204
121
 
205
- Arguments can span multiple lines, just like a normal function call:
122
+ This lets Rip use the **right pattern for each widget**: single-component
123
+ for data-driven widgets (Select, Combobox, Menu), compositional via
124
+ `offer`/`accept` when children contain complex content the parent shouldn't
125
+ own.
206
126
 
207
- ```coffee
208
- input.(
209
- 'block w-full rounded-lg border border-primary',
210
- 'text-sm-plus text-tertiary shadow-xs'
211
- )
212
- ```
127
+ ---
213
128
 
214
- ### Indented Attributes
129
+ ## Styling
215
130
 
216
- Attributes can be placed on separate indented lines after the element:
131
+ All widgets are headless they ship no CSS. The contract between behavior
132
+ and styling is `data-*` attributes:
217
133
 
218
134
  ```coffee
219
- input.('rounded-lg border px-3.5 py-2.5')
220
- type: "email"
221
- value: user.email
222
- disabled: true
135
+ # Widget exposes semantic state
136
+ button $open: open?!, $disabled: @disabled?!
223
137
  ```
224
138
 
225
- This is equivalent to the inline form:
139
+ Style with Tailwind's data attribute variants:
226
140
 
227
- ```coffee
228
- input.('rounded-lg border px-3.5 py-2.5') type: "email", value: user.email, disabled: true
141
+ ```html
142
+ <button class="data-[open]:border-blue-500 data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed">
229
143
  ```
230
144
 
231
- ### The `class:` Attribute
232
-
233
- The `class:` attribute works like `.()` and merges cumulatively with any
234
- existing `.()` classes on the same element:
145
+ Or vanilla CSS:
235
146
 
236
- ```coffee
237
- input.('block w-full rounded-lg')
238
- class: 'text-sm text-tertiary'
239
- type: "email"
147
+ ```css
148
+ [data-open] { border-color: theme('colors.blue.500'); }
149
+ [data-disabled] { opacity: 0.5; cursor: not-allowed; }
240
150
  ```
241
151
 
242
- This produces a single combined class expression: `block w-full rounded-lg text-sm text-tertiary`.
152
+ Add Tailwind via CDN no build step needed:
243
153
 
244
- The `class:` value also supports `.()` syntax for conditional classes:
245
-
246
- ```coffee
247
- div.('mt-4 p-4')
248
- class: .('ring-1', highlighted: isHighlighted)
249
- span "Content"
154
+ ```html
155
+ <script src="https://cdn.tailwindcss.com"></script>
250
156
  ```
251
157
 
252
- ### Attributes and Children Together
158
+ ---
159
+
160
+ ## Code Density
253
161
 
254
- Attributes and children can coexist at the same indentation level. Attributes
255
- (key-value pairs) are listed first, followed by child elements:
162
+ ### Checkbox 18 Lines
256
163
 
257
164
  ```coffee
258
- button.('flex items-center rounded-lg')
259
- type: "submit"
260
- disabled: saving
165
+ export Checkbox = component
166
+ @checked := false
167
+ @disabled := false
168
+ @indeterminate := false
169
+ @switch := false
170
+
171
+ onClick: ->
172
+ return if @disabled
173
+ @indeterminate = false
174
+ @checked = not @checked
175
+ @emit 'change', @checked
261
176
 
262
- span.('font-bold') "Submit"
263
- span.('text-sm text-secondary') "or press Enter"
177
+ render
178
+ button role: @switch ? 'switch' : 'checkbox'
179
+ aria-checked: @indeterminate ? 'mixed' : !!@checked
180
+ aria-disabled: @disabled?!
181
+ $checked: @checked?!
182
+ $indeterminate: @indeterminate?!
183
+ $disabled: @disabled?!
184
+ slot
264
185
  ```
265
186
 
266
- Blank lines between attributes and children are fine they don't break the
267
- structure.
268
-
269
- ## Two-Way Binding — The `<=>` Operator
270
-
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.
187
+ Full ARIA. Checkbox and switch modes. Indeterminate state. Data attributes
188
+ for styling. 18 lines, complete.
274
189
 
275
- ### The Problem It Solves
190
+ ### Dialog Effect-Based Lifecycle
276
191
 
277
- In React, wiring state to interactive elements requires explicit value props
278
- and callback handlers for every bindable property:
192
+ Focus trap, scroll lock, escape dismiss, click-outside dismiss, focus
193
+ restore all in one reactive effect with automatic cleanup:
279
194
 
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);
195
+ ```coffee
196
+ ~>
197
+ if @open
198
+ _prevFocus = document.activeElement
199
+ # lock scroll, trap focus, wire ARIA ...
200
+ return ->
201
+ # cleanup runs automatically when @open becomes false
202
+ ```
203
+
204
+ No `useEffect`. No dependency array. No cleanup that captures stale state.
205
+
206
+ ### Grid — 901 Lines
207
+
208
+ No equivalent in ShadCN, Radix, or Headless UI. Virtual scrolling, DOM
209
+ recycling, Sheets-style selection, full keyboard nav, inline editing,
210
+ multi-column sort, column resizing, clipboard (Ctrl+C/V/X as TSV — interop
211
+ with Excel, Google Sheets, Numbers). 901 lines vs 50,000+ for AG Grid.
212
+
213
+ ---
214
+
215
+ ## Component Overview
216
+
217
+ 54 headless components across 10 categories — 5,191 lines total.
218
+
219
+ ### Selection
220
+
221
+ | Widget | Description | Key Props | Events |
222
+ |--------|-------------|-----------|--------|
223
+ | **Select** | Dropdown with typeahead, ARIA listbox | `@value`, `@placeholder`, `@disabled` | `@change` |
224
+ | **Combobox** | Filterable input + listbox | `@query`, `@placeholder`, `@disabled` | `@select`, `@filter` |
225
+ | **MultiSelect** | Multi-select with chips and filtering | `@value`, `@query`, `@placeholder` | `@change` |
226
+ | **Autocomplete** | Type to filter, select to fill | `@value`, `@query`, `@placeholder` | `@change` |
227
+
228
+ ### Toggle
229
+
230
+ | Widget | Description | Key Props | Events |
231
+ |--------|-------------|-----------|--------|
232
+ | **Checkbox** | Toggle with checkbox/switch semantics | `@checked`, `@disabled`, `@switch` | `@change` |
233
+ | **Toggle** | Two-state toggle button | `@pressed`, `@disabled` | `@change` |
234
+ | **ToggleGroup** | Single or multi-select toggles | `@value`, `@multiple` | `@change` |
235
+ | **RadioGroup** | Exactly one selected, arrow nav | `@value`, `@disabled` | `@change` |
236
+ | **CheckboxGroup** | Multiple checked independently | `@value`, `@disabled` | `@change` |
237
+
238
+ ### Input
239
+
240
+ | Widget | Description | Key Props | Events |
241
+ |--------|-------------|-----------|--------|
242
+ | **Input** | Focus, touch, and validation tracking | `@value`, `@type`, `@placeholder` | `@change` |
243
+ | **Textarea** | Auto-resizing text area | `@value`, `@autoResize`, `@rows` | `@change` |
244
+ | **NumberField** | Stepper buttons, hold-to-repeat | `@value`, `@min`, `@max`, `@step` | `@change` |
245
+ | **Slider** | Drag with pointer capture + keyboard | `@value`, `@min`, `@max`, `@step` | `@change` |
246
+ | **OTPField** | Multi-digit code, auto-advance + paste | `@value`, `@length` | `@complete` |
247
+ | **DatePicker** | Calendar dropdown, single or range | `@value`, `@min`, `@max`, `@range` | `@change` |
248
+ | **EditableValue** | Click-to-edit inline value | `@value`, `@placeholder` | `@change` |
249
+ | **NativeSelect** | Styled native `<select>` wrapper | `@value`, `@disabled` | `@change` |
250
+ | **InputGroup** | Input with prefix/suffix addons | `@disabled` | — |
251
+
252
+ ### Navigation
253
+
254
+ | Widget | Description | Key Props | Events |
255
+ |--------|-------------|-----------|--------|
256
+ | **Tabs** | Arrow key nav, roving tabindex | `@active`, `@orientation` | `@change` |
257
+ | **Menu** | Dropdown action menu | `@disabled` | `@select` |
258
+ | **ContextMenu** | Right-click context menu | `@disabled` | `@select` |
259
+ | **Menubar** | Horizontal menu bar with dropdowns | — | `@select` |
260
+ | **NavMenu** | Site nav with hover/click panels | — | — |
261
+ | **Toolbar** | Grouped controls, roving tabindex | `@orientation`, `@label` | — |
262
+ | **Breadcrumb** | Navigation trail with separator | `@separator`, `@label` | — |
263
+
264
+ ### Overlay
265
+
266
+ | Widget | Description | Key Props | Events |
267
+ |--------|-------------|-----------|--------|
268
+ | **Dialog** | Focus trap, scroll lock, ARIA modal | `@open` | `@close` |
269
+ | **AlertDialog** | Non-dismissable modal | `@open`, `@initialFocus` | `@close` |
270
+ | **Drawer** | Slide-out panel with focus trap | `@open`, `@side` | `@close` |
271
+ | **Popover** | Anchored floating with flip/shift | `@placement`, `@offset` | — |
272
+ | **Tooltip** | Hover/focus with delay | `@text`, `@placement`, `@delay` | — |
273
+ | **PreviewCard** | Hover/focus preview card | `@delay`, `@placement` | — |
274
+ | **Toast** | Auto-dismiss, ARIA live region | `@toast` (object) | `@dismiss` |
275
+
276
+ ### Display
277
+
278
+ | Widget | Description | Key Props |
279
+ |--------|-------------|-----------|
280
+ | **Button** | Disabled-but-focusable pattern | `@disabled` |
281
+ | **Badge** | Inline label (solid/outline/subtle) | `@variant` |
282
+ | **Card** | Container with header/content/footer | `@interactive` |
283
+ | **Separator** | Decorative or semantic divider | `@orientation`, `@decorative` |
284
+ | **Progress** | Progress bar via CSS custom prop | `@value`, `@max` |
285
+ | **Meter** | Gauge with thresholds | `@value`, `@min`, `@max`, `@low`, `@high` |
286
+ | **Spinner** | Loading indicator | `@label`, `@size` |
287
+ | **Skeleton** | Loading placeholder with shimmer | `@width`, `@height`, `@circle` |
288
+ | **Avatar** | Image with fallback to initials | `@src`, `@alt`, `@fallback` |
289
+ | **Label** | Accessible form label | `@for`, `@required` |
290
+ | **ScrollArea** | Custom scrollbar, draggable thumb | `@orientation` |
291
+
292
+ ### Form
293
+
294
+ | Widget | Description | Key Props |
295
+ |--------|-------------|-----------|
296
+ | **Field** | Label + description + error wrapper | `@label`, `@error`, `@required` |
297
+ | **Fieldset** | Grouped fields with cascading disable | `@legend`, `@disabled` |
298
+ | **Form** | Submit handling + validation state | `@onSubmit` |
299
+ | **ButtonGroup** | Grouped buttons, ARIA semantics | `@orientation`, `@disabled` |
300
+
301
+ ### Data
302
+
303
+ | Widget | Description | Key Props |
304
+ |--------|-------------|-----------|
305
+ | **Grid** | Virtual scroll, 100K+ rows at 60fps | `@data`, `@columns`, `@rowHeight` |
306
+ | **Accordion** | Expand/collapse, single or multiple | `@multiple` |
307
+ | **Table** | Semantic table wrapper | `@caption`, `@striped` |
308
+
309
+ ### Interactive
310
+
311
+ | Widget | Description | Key Props | Events |
312
+ |--------|-------------|-----------|--------|
313
+ | **Collapsible** | Animated expand/collapse | `@open`, `@disabled` | `@change` |
314
+ | **Pagination** | Page nav with ellipsis gaps | `@page`, `@total`, `@perPage` | `@change` |
315
+ | **Carousel** | Slide with autoplay + loop | `@loop`, `@autoplay`, `@interval` | `@change` |
316
+ | **Resizable** | Draggable resize handles | `@orientation`, `@minSize` | `@resize` |
317
+
318
+ ---
319
+
320
+ ## Widget Reference
321
+
322
+ ### Select
286
323
 
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} />
324
+ ```coffee
325
+ Select value <=> selectedRole, @change: handleChange
326
+ option value: "eng", "Engineer"
327
+ option value: "des", "Designer"
328
+ option value: "mgr", "Manager"
291
329
  ```
292
330
 
293
- Every bindable property needs a state declaration AND a setter callback.
294
- This is the single most tedious pattern in React development.
331
+ **Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close, Home/End, type-ahead
332
+ **Data attributes:** `$open`, `$highlighted`, `$selected`, `$disabled`
295
333
 
296
- ### The Rip Way
297
-
298
- In Rip, `<=>` replaces all of that with a single operator:
334
+ ### Combobox
299
335
 
300
336
  ```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?"
337
+ Combobox query <=> searchText, @select: handleSelect, @filter: handleFilter
338
+ for item in filteredItems
339
+ div $value: item.id
340
+ span item.name
316
341
  ```
317
342
 
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.
343
+ **Keyboard:** ArrowDown/Up navigate, Enter select, Escape close/clear, Tab close
344
+ **Data attributes:** `$open`, `$highlighted`
321
345
 
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.
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:
346
+ ### Dialog
342
347
 
343
348
  ```coffee
344
- # These are equivalent:
345
- input value <=> @name # explicit two-way binding
346
- input value: @name # auto-detected (name is reactive)
349
+ Dialog open <=> showDialog, @close: handleClose
350
+ h2 "Confirm Action"
351
+ p "Are you sure?"
352
+ button @click: (=> showDialog = false), "Cancel"
353
+ button @click: handleConfirm, "Confirm"
347
354
  ```
348
355
 
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.
372
-
373
- ### Browser Execution Contexts
356
+ **Keyboard:** Escape to close, Tab trapped within dialog
357
+ **Data attributes:** `$open`
358
+ **Behavior:** Focus trap, body scroll lock, focus restore on close
374
359
 
375
- Rip provides full async/await support across every browser context — no other
376
- compile-to-JS language has this:
360
+ ### AlertDialog
377
361
 
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
- }
362
+ ```coffee
363
+ AlertDialog open <=> showConfirm
364
+ h2 "Delete account?"
365
+ p "This action cannot be undone."
366
+ button @click: (=> showConfirm = false), "Cancel"
367
+ button @click: handleDelete, "Delete"
432
368
  ```
433
369
 
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
440
-
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.
370
+ Like Dialog but cannot be closed by Escape or click outside.
371
+ **ARIA:** `role="alertdialog"`, auto-wired `aria-labelledby`/`aria-describedby`
444
372
 
445
- ### 1. Static File URLs — `launch components: [...]`
446
-
447
- Fetch individual `.rip` files as plain text from any static server:
373
+ ### Toast
448
374
 
449
375
  ```coffee
450
- launch components: [
451
- 'components/index.rip'
452
- 'components/dashboard.rip'
453
- 'components/line-chart.rip'
454
- ]
376
+ toasts := []
377
+ toasts = [...toasts, { message: "Saved!", type: "success" }]
378
+ toasts = toasts.filter (t) -> t isnt target
379
+ ToastViewport toasts <=> toasts
455
380
  ```
456
381
 
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="...">`
382
+ **Props:** `@toasts`, `@placement` (bottom-right, top-right, etc.)
383
+ **Per-toast:** `message`, `type`, `duration` (default 4000ms), `title`, `action`
384
+ **Data attributes:** `$type`, `$leaving`
385
+ **Behavior:** Timer pauses on hover, resumes on leave
462
386
 
463
- Embed component source directly in the HTML page:
387
+ ### Tabs
464
388
 
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>
389
+ ```coffee
390
+ Tabs active <=> currentTab
391
+ div $tab: "one", "Tab One"
392
+ div $tab: "two", "Tab Two"
393
+ div $panel: "one"
394
+ p "Content for tab one"
395
+ div $panel: "two"
396
+ p "Content for tab two"
480
397
  ```
481
398
 
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.
487
-
488
- ### 3. Server Bundle (default)
399
+ **Keyboard:** ArrowLeft/Right navigate, Home/End jump
400
+ **Data attributes:** `$active`
489
401
 
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.
402
+ ### Accordion
493
403
 
494
404
  ```coffee
495
- launch() # fetches /bundle automatically
405
+ Accordion multiple: false
406
+ div $item: "a"
407
+ button $trigger: true, "Section A"
408
+ div $content: true
409
+ p "Content A"
496
410
  ```
497
411
 
498
- ## Server Middleware
412
+ **Keyboard:** Enter/Space toggle, ArrowDown/Up between triggers, Home/End
413
+ **Methods:** `toggle(id)`, `isOpen(id)`
499
414
 
500
- The `ripUI` middleware registers routes for the framework files, the app
501
- bundle, and optional SSE hot-reload:
415
+ ### Checkbox
502
416
 
503
417
  ```coffee
504
- use ripUI dir: dir, components: 'routes', includes: ['ui'], watch: true, title: 'My App'
418
+ Checkbox checked <=> isActive, @change: handleChange
419
+ span "Enable notifications"
420
+
421
+ Checkbox checked <=> isDark, switch: true
422
+ span "Dark mode"
505
423
  ```
506
424
 
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 |
425
+ **ARIA:** `role="checkbox"` or `role="switch"`, `aria-checked` (true/false/mixed)
426
+ **Data attributes:** `$checked`, `$indeterminate`, `$disabled`
517
427
 
518
- Routes registered:
428
+ ### Menu
519
429
 
430
+ ```coffee
431
+ Menu @select: handleAction
432
+ button $trigger: true, "Actions"
433
+ div $item: "edit", "Edit"
434
+ div $item: "delete", "Delete"
520
435
  ```
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)
525
- ```
526
-
527
- ## State Preservation (Keep-Alive)
528
436
 
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).
437
+ **Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close
438
+ **Data attributes:** `$open`, `$highlighted`
532
439
 
533
- ## Data Loading
534
-
535
- `createResource` manages async data with reactive `loading`, `error`, and
536
- `data` properties:
440
+ ### Popover
537
441
 
538
442
  ```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
443
+ Popover placement: "bottom-start"
444
+ button "Options"
445
+ div
446
+ p "Popover content here"
549
447
  ```
550
448
 
551
- ## Error Boundaries
449
+ **Keyboard:** Enter/Space/ArrowDown toggle, Escape close
450
+ **Data attributes:** `$open`, `$placement`
552
451
 
553
- Layouts with an `onError` method catch errors from child components:
452
+ ### Tooltip
554
453
 
555
454
  ```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
455
+ Tooltip text: "Save your changes", placement: "top"
456
+ button "Save"
566
457
  ```
567
458
 
568
- ## Navigation Indicator
459
+ **Data attributes:** `$open`, `$entering`, `$exiting`, `$placement`
460
+ **Behavior:** Shows after delay on hover/focus, uses `aria-describedby`
569
461
 
570
- `router.navigating` is a reactive signal — true while a route transition
571
- is in progress:
462
+ ### Grid
572
463
 
573
464
  ```coffee
574
- if @router.navigating
575
- span "Loading..."
465
+ Grid
466
+ data: employees
467
+ columns: [
468
+ { key: 'name', title: 'Name', width: 200 }
469
+ { key: 'age', title: 'Age', width: 80, align: 'right' }
470
+ { key: 'role', title: 'Role', width: 150, type: 'select', source: roles }
471
+ { key: 'active', title: 'Active', width: 60, type: 'checkbox' }
472
+ ]
473
+ rowHeight: 32
474
+ striped: true
576
475
  ```
577
476
 
578
- ## Multi-App Hosting
477
+ **Column types:** `text`, `number`, `checkbox`, `select`
478
+ **Methods:** `getCell`, `setCell`, `getData`, `setData`, `sort`, `scrollToRow`, `copySelection`, `cutSelection`, `pasteAtActive`
479
+ **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
480
+ **Sorting:** Click header (asc/desc/none), Shift+click for multi-column
481
+ **Clipboard:** TSV format — interop with Excel, Sheets, Numbers
482
+ **Data attributes:** `$active`, `$selected`, `$sorted`, `$editing`, `$selecting`
579
483
 
580
- Mount multiple apps under one server:
484
+ ### Collapsible
581
485
 
582
486
  ```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
487
+ Collapsible open <=> isOpen
488
+ button $trigger: true, "Show details"
489
+ div $content: true
490
+ p "Hidden content here"
591
491
  ```
592
492
 
593
- The `/rip/` namespace is shared — all apps use the same compiler and framework.
493
+ **Methods:** `toggle()`
494
+ **Data attributes:** `$open`, `$disabled`
495
+ **CSS custom properties:** `--collapsible-height`, `--collapsible-width`
594
496
 
595
- ## File Structure
497
+ ### Pagination
596
498
 
499
+ ```coffee
500
+ Pagination page <=> currentPage, total: 100, perPage: 10
597
501
  ```
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
611
- ```
612
-
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.
616
502
 
617
- ## Hash Routing
503
+ **Keyboard:** ArrowLeft/Right, Home/End
504
+ **Data attributes:** `$active`, `$disabled`, `$ellipsis`
618
505
 
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.
506
+ ### Carousel
624
507
 
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>
508
+ ```coffee
509
+ Carousel loop: true
510
+ div $slide: true, "Slide 1"
511
+ div $slide: true, "Slide 2"
512
+ div $slide: true, "Slide 3"
629
513
  ```
630
514
 
631
- Or when calling `launch()` manually:
515
+ **Methods:** `goto(index)`, `next()`, `prev()`
516
+ **Behavior:** Autoplay pauses on hover
517
+
518
+ ### Drawer
632
519
 
633
520
  ```coffee
634
- launch hash: false
521
+ Drawer open <=> showDrawer, side: "left"
522
+ nav "Sidebar content"
635
523
  ```
636
524
 
637
- ## Static Deployment
638
-
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.
525
+ **Props:** `@open`, `@side` (top/right/bottom/left), `@dismissable`
526
+ **Behavior:** Focus trap, scroll lock, Escape to close
642
527
 
643
- **Inline mode** — everything in one HTML file:
528
+ ### Breadcrumb
644
529
 
645
- ```html
646
- <script type="module" src="rip-ui.min.js"></script>
647
-
648
- <script type="text/rip" data-name="index">
649
- export Home = component
650
- render
651
- h1 "Hello"
652
- </script>
653
-
654
- <script type="text/rip" data-name="about">
655
- export About = component
656
- render
657
- h1 "About"
658
- </script>
530
+ ```coffee
531
+ Breadcrumb
532
+ a $item: true, href: "/", "Home"
533
+ a $item: true, href: "/products", "Products"
534
+ span $item: true, "Widget Pro"
659
535
  ```
660
536
 
661
- The runtime auto-detects the `data-name` components and launches with hash
662
- routing. That's it — no bootstrap, no config.
537
+ **ARIA:** `aria-current="page"` on last item
663
538
 
664
- **Remote bundle** — fetch components from a URL:
539
+ ### Resizable
665
540
 
666
- ```html
667
- <script type="module" src="rip-ui.min.js" data-url="https://example.com/app/"></script>
541
+ ```coffee
542
+ Resizable
543
+ div $panel: true, "Left"
544
+ div $panel: true, "Right"
668
545
  ```
669
546
 
670
- The `data-url` attribute tells the runtime to fetch the app bundle from the
671
- given URL (appending `/bundle` to the path).
547
+ **ARIA:** `role="separator"` on handles
548
+ **CSS custom properties:** `--panel-size` on each panel
672
549
 
673
- **Manual launch** for full control, use a bare `<script type="text/rip">`:
550
+ ### Context Sharing: `offer` / `accept`
674
551
 
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
- ```
552
+ For compound components where descendants need shared state:
682
553
 
683
- **Inline Rip** — run arbitrary Rip code alongside auto-launched apps:
554
+ ```coffee
555
+ # Parent offers reactive state to all descendants
556
+ export Tabs = component
557
+ offer active := 'overview'
684
558
 
685
- ```html
686
- <script type="text/rip">
687
- alert "Free cheese rollups for the girls!"
688
- </script>
559
+ # Child accepts the shared signal
560
+ export TabContent = component
561
+ accept active
562
+ render
563
+ div hidden: active isnt @value
564
+ slot
689
565
  ```
690
566
 
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.
567
+ Parent and child share the same reactive object mutations in either
568
+ direction are instantly visible. No Provider wrappers, no string keys.
693
569
 
694
- ## Tailwind CSS Autocompletion
570
+ ---
695
571
 
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:
572
+ ## File Summary
700
573
 
701
- ```json
702
- {
703
- "tailwindCSS.includeLanguages": { "rip": "html" },
704
- "tailwindCSS.experimental.classRegex": [
705
- ["\\.\\(([\\s\\S]*?)\\)", "'([^']*)'"]
706
- ]
707
- }
708
- ```
574
+ | Category | Files | Lines |
575
+ |----------|-------|-------|
576
+ | Selection | 4 | 638 |
577
+ | Toggle | 5 | 267 |
578
+ | Input | 9 | 854 |
579
+ | Navigation | 7 | 767 |
580
+ | Overlay | 7 | 700 |
581
+ | Display | 11 | 378 |
582
+ | Form | 4 | 140 |
583
+ | Data | 3 | 1,041 |
584
+ | Interactive | 4 | 406 |
585
+ | **Total** | **54** | **5,191** |
709
586
 
710
- This gives you autocompletion, hover previews, and linting for Tailwind
711
- classes in expressions like:
587
+ ---
712
588
 
713
- ```coffee
714
- h1.('text-3xl font-semibold') "Hello"
715
- button.('flex items-center px-4 py-2 rounded-full') "Click"
716
- ```
589
+ ## Status
717
590
 
718
- ## License
591
+ The reactive model, headless contract, and performance architecture are
592
+ proven. The compiler has 1,436 tests. The widget suite is comprehensive
593
+ but still maturing — tests are being added, and a few widgets have known
594
+ structural issues being resolved (see [CONTRIBUTING.md](CONTRIBUTING.md)
595
+ for details).
719
596
 
720
- MIT
597
+ For widget authoring patterns, implementation notes, known issues, and the
598
+ development roadmap, see **[CONTRIBUTING.md](CONTRIBUTING.md)**.