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