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