@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.
- package/README.md +446 -568
- package/accordion.rip +113 -0
- package/alert-dialog.rip +96 -0
- package/autocomplete.rip +141 -0
- package/avatar.rip +37 -0
- package/badge.rip +15 -0
- package/breadcrumb.rip +46 -0
- package/button-group.rip +26 -0
- package/button.rip +23 -0
- package/card.rip +25 -0
- package/carousel.rip +110 -0
- package/checkbox-group.rip +65 -0
- package/checkbox.rip +33 -0
- package/collapsible.rip +50 -0
- package/combobox.rip +155 -0
- package/context-menu.rip +105 -0
- package/date-picker.rip +214 -0
- package/dialog.rip +107 -0
- package/drawer.rip +79 -0
- package/editable-value.rip +80 -0
- package/field.rip +53 -0
- package/fieldset.rip +22 -0
- package/form.rip +39 -0
- package/grid.rip +901 -0
- package/index.rip +16 -0
- package/input-group.rip +28 -0
- package/input.rip +36 -0
- package/label.rip +16 -0
- package/menu.rip +162 -0
- package/menubar.rip +155 -0
- package/meter.rip +36 -0
- package/multi-select.rip +158 -0
- package/native-select.rip +32 -0
- package/nav-menu.rip +129 -0
- package/number-field.rip +162 -0
- package/otp-field.rip +89 -0
- package/package.json +18 -27
- package/pagination.rip +123 -0
- package/popover.rip +143 -0
- package/preview-card.rip +73 -0
- package/progress.rip +25 -0
- package/radio-group.rip +67 -0
- package/resizable.rip +123 -0
- package/scroll-area.rip +145 -0
- package/select.rip +184 -0
- package/separator.rip +17 -0
- package/skeleton.rip +22 -0
- package/slider.rip +165 -0
- package/spinner.rip +17 -0
- package/table.rip +27 -0
- package/tabs.rip +124 -0
- package/textarea.rip +48 -0
- package/toast.rip +87 -0
- package/toggle-group.rip +78 -0
- package/toggle.rip +24 -0
- package/toolbar.rip +46 -0
- package/tooltip.rip +115 -0
- package/dist/rip-ui.min.js +0 -522
- package/dist/rip-ui.min.js.br +0 -0
- package/serve.rip +0 -92
- package/ui.rip +0 -964
package/README.md
CHANGED
|
@@ -1,720 +1,598 @@
|
|
|
1
|
-
|
|
1
|
+
# Rip UI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
8
|
+
Components are plain `.rip` source files — no build step. The browser compiles
|
|
9
|
+
them on the fly.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
No build step, no bundler, no configuration.
|
|
11
|
+
---
|
|
9
12
|
|
|
10
13
|
## Quick Start
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
Add the components directory to your serve middleware:
|
|
13
16
|
|
|
14
17
|
```coffee
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
23
|
+
All widgets become available by name (`Select`, `Dialog`, `Grid`, etc.) in the
|
|
24
|
+
shared scope — no imports needed.
|
|
26
25
|
|
|
27
|
-
```
|
|
28
|
-
|
|
26
|
+
```bash
|
|
27
|
+
cd packages/ui
|
|
28
|
+
rip server
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
67
|
+
# Rip: 1 line per binding
|
|
68
|
+
Dialog open <=> show
|
|
69
|
+
input value <=> @name
|
|
157
70
|
```
|
|
158
71
|
|
|
159
|
-
|
|
72
|
+
---
|
|
160
73
|
|
|
161
|
-
|
|
162
|
-
// @open := false → accepts parent value
|
|
163
|
-
this.open = __state(props.open ?? false);
|
|
74
|
+
## Why Rip UI
|
|
164
75
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
89
|
+
### Architecture
|
|
170
90
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
105
|
+
**Source as distribution.** Components are served as `.rip` source files.
|
|
106
|
+
Read them, understand them, modify them.
|
|
191
107
|
|
|
192
|
-
|
|
193
|
-
and children can be expressed inline or across multiple indented lines.
|
|
108
|
+
### Why We Build Our Own
|
|
194
109
|
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
## Styling
|
|
215
130
|
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
value: user.email
|
|
222
|
-
disabled: true
|
|
135
|
+
# Widget exposes semantic state
|
|
136
|
+
button $open: open?!, $disabled: @disabled?!
|
|
223
137
|
```
|
|
224
138
|
|
|
225
|
-
|
|
139
|
+
Style with Tailwind's data attribute variants:
|
|
226
140
|
|
|
227
|
-
```
|
|
228
|
-
|
|
141
|
+
```html
|
|
142
|
+
<button class="data-[open]:border-blue-500 data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed">
|
|
229
143
|
```
|
|
230
144
|
|
|
231
|
-
|
|
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
|
-
```
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
152
|
+
Add Tailwind via CDN — no build step needed:
|
|
243
153
|
|
|
244
|
-
|
|
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
|
-
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Code Density
|
|
253
161
|
|
|
254
|
-
|
|
255
|
-
(key-value pairs) are listed first, followed by child elements:
|
|
162
|
+
### Checkbox — 18 Lines
|
|
256
163
|
|
|
257
164
|
```coffee
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
disabled
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
###
|
|
190
|
+
### Dialog — Effect-Based Lifecycle
|
|
276
191
|
|
|
277
|
-
|
|
278
|
-
|
|
192
|
+
Focus trap, scroll lock, escape dismiss, click-outside dismiss, focus
|
|
193
|
+
restore — all in one reactive effect with automatic cleanup:
|
|
279
194
|
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
294
|
-
|
|
331
|
+
**Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close, Home/End, type-ahead
|
|
332
|
+
**Data attributes:** `$open`, `$highlighted`, `$selected`, `$disabled`
|
|
295
333
|
|
|
296
|
-
###
|
|
297
|
-
|
|
298
|
-
In Rip, `<=>` replaces all of that with a single operator:
|
|
334
|
+
### Combobox
|
|
299
335
|
|
|
300
336
|
```coffee
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
376
|
-
compile-to-JS language has this:
|
|
360
|
+
### AlertDialog
|
|
377
361
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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
|
-
###
|
|
446
|
-
|
|
447
|
-
Fetch individual `.rip` files as plain text from any static server:
|
|
373
|
+
### Toast
|
|
448
374
|
|
|
449
375
|
```coffee
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
387
|
+
### Tabs
|
|
464
388
|
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
+
**Keyboard:** Enter/Space toggle, ArrowDown/Up between triggers, Home/End
|
|
413
|
+
**Methods:** `toggle(id)`, `isOpen(id)`
|
|
499
414
|
|
|
500
|
-
|
|
501
|
-
bundle, and optional SSE hot-reload:
|
|
415
|
+
### Checkbox
|
|
502
416
|
|
|
503
417
|
```coffee
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
`createResource` manages async data with reactive `loading`, `error`, and
|
|
536
|
-
`data` properties:
|
|
440
|
+
### Popover
|
|
537
441
|
|
|
538
442
|
```coffee
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
449
|
+
**Keyboard:** Enter/Space/ArrowDown toggle, Escape close
|
|
450
|
+
**Data attributes:** `$open`, `$placement`
|
|
552
451
|
|
|
553
|
-
|
|
452
|
+
### Tooltip
|
|
554
453
|
|
|
555
454
|
```coffee
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
459
|
+
**Data attributes:** `$open`, `$entering`, `$exiting`, `$placement`
|
|
460
|
+
**Behavior:** Shows after delay on hover/focus, uses `aria-describedby`
|
|
569
461
|
|
|
570
|
-
|
|
571
|
-
is in progress:
|
|
462
|
+
### Grid
|
|
572
463
|
|
|
573
464
|
```coffee
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
484
|
+
### Collapsible
|
|
581
485
|
|
|
582
486
|
```coffee
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
493
|
+
**Methods:** `toggle()`
|
|
494
|
+
**Data attributes:** `$open`, `$disabled`
|
|
495
|
+
**CSS custom properties:** `--collapsible-height`, `--collapsible-width`
|
|
594
496
|
|
|
595
|
-
|
|
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
|
-
|
|
503
|
+
**Keyboard:** ArrowLeft/Right, Home/End
|
|
504
|
+
**Data attributes:** `$active`, `$disabled`, `$ellipsis`
|
|
618
505
|
|
|
619
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
515
|
+
**Methods:** `goto(index)`, `next()`, `prev()`
|
|
516
|
+
**Behavior:** Autoplay pauses on hover
|
|
517
|
+
|
|
518
|
+
### Drawer
|
|
632
519
|
|
|
633
520
|
```coffee
|
|
634
|
-
|
|
521
|
+
Drawer open <=> showDrawer, side: "left"
|
|
522
|
+
nav "Sidebar content"
|
|
635
523
|
```
|
|
636
524
|
|
|
637
|
-
|
|
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
|
-
|
|
528
|
+
### Breadcrumb
|
|
644
529
|
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
662
|
-
routing. That's it — no bootstrap, no config.
|
|
537
|
+
**ARIA:** `aria-current="page"` on last item
|
|
663
538
|
|
|
664
|
-
|
|
539
|
+
### Resizable
|
|
665
540
|
|
|
666
|
-
```
|
|
667
|
-
|
|
541
|
+
```coffee
|
|
542
|
+
Resizable
|
|
543
|
+
div $panel: true, "Left"
|
|
544
|
+
div $panel: true, "Right"
|
|
668
545
|
```
|
|
669
546
|
|
|
670
|
-
|
|
671
|
-
|
|
547
|
+
**ARIA:** `role="separator"` on handles
|
|
548
|
+
**CSS custom properties:** `--panel-size` on each panel
|
|
672
549
|
|
|
673
|
-
|
|
550
|
+
### Context Sharing: `offer` / `accept`
|
|
674
551
|
|
|
675
|
-
|
|
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
|
-
|
|
554
|
+
```coffee
|
|
555
|
+
# Parent offers reactive state to all descendants
|
|
556
|
+
export Tabs = component
|
|
557
|
+
offer active := 'overview'
|
|
684
558
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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
|
-
|
|
570
|
+
---
|
|
695
571
|
|
|
696
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
711
|
-
classes in expressions like:
|
|
587
|
+
---
|
|
712
588
|
|
|
713
|
-
|
|
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
|
-
|
|
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
|
-
|
|
597
|
+
For widget authoring patterns, implementation notes, known issues, and the
|
|
598
|
+
development roadmap, see **[CONTRIBUTING.md](CONTRIBUTING.md)**.
|