@rip-lang/ui 0.1.3 → 0.3.1
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 +507 -726
- package/accordion.rip +113 -0
- package/autocomplete.rip +141 -0
- package/avatar.rip +37 -0
- package/button.rip +23 -0
- package/checkbox-group.rip +65 -0
- package/checkbox.rip +33 -0
- package/combobox.rip +153 -0
- package/context-menu.rip +98 -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 +15 -0
- package/input.rip +35 -0
- package/menu.rip +162 -0
- package/menubar.rip +155 -0
- package/meter.rip +36 -0
- package/multi-select.rip +158 -0
- package/nav-menu.rip +132 -0
- package/number-field.rip +162 -0
- package/otp-field.rip +89 -0
- package/package.json +16 -37
- package/popover.rip +143 -0
- package/preview-card.rip +73 -0
- package/progress.rip +25 -0
- package/radio-group.rip +67 -0
- package/scroll-area.rip +145 -0
- package/select.rip +184 -0
- package/separator.rip +17 -0
- package/slider.rip +165 -0
- package/tabs.rip +124 -0
- package/toast.rip +88 -0
- package/toggle-group.rip +78 -0
- package/toggle.rip +24 -0
- package/toolbar.rip +46 -0
- package/tooltip.rip +115 -0
- package/renderer.js +0 -397
- package/router.js +0 -325
- package/serve.rip +0 -140
- package/stash.js +0 -413
- package/ui.js +0 -208
- package/vfs.js +0 -215
package/README.md
CHANGED
|
@@ -1,877 +1,658 @@
|
|
|
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 handles keyboard
|
|
5
|
+
interactions per WAI-ARIA Authoring Practices.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
bundling, and shipping static JavaScript artifacts to the browser, it ships the
|
|
9
|
-
40KB Rip compiler itself. Components are delivered as source files, stored in a
|
|
10
|
-
browser-local Virtual File System, compiled on demand, and rendered with
|
|
11
|
-
fine-grained DOM updates powered by Rip's built-in reactivity. No build step.
|
|
12
|
-
No bundler. No configuration files.
|
|
13
|
-
|
|
14
|
-
The component model adds exactly **two keywords** to the Rip language —
|
|
15
|
-
`component` and `render` — and reuses everything else (classes, reactivity,
|
|
16
|
-
functions, methods) that Rip already provides.
|
|
17
|
-
|
|
18
|
-
## Architecture
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
┌─────────────────────────────────────────┐
|
|
22
|
-
│ Server (Bun + @rip-lang/api) │
|
|
23
|
-
│ │
|
|
24
|
-
│ serve.rip (ripUI middleware) │
|
|
25
|
-
│ ├── /rip-ui/*.js → framework files │
|
|
26
|
-
│ ├── /rip-ui/manifest.json → all pages │
|
|
27
|
-
│ ├── /rip-ui/watch → SSE hot-reload │
|
|
28
|
-
│ └── /pages/*.rip → individual pages │
|
|
29
|
-
└──────────────┬──────────────────────────┘
|
|
30
|
-
│
|
|
31
|
-
┌──────────────────────────┼──────────────────────────┐
|
|
32
|
-
│ │ │
|
|
33
|
-
▼ ▼ ▼
|
|
34
|
-
rip.browser.js (40KB) @rip-lang/ui (~8KB) SSE EventSource
|
|
35
|
-
(Rip compiler) (framework modules) (hot-reload channel)
|
|
36
|
-
│ │ │
|
|
37
|
-
│ ┌────────────────┼────────────────┐ │
|
|
38
|
-
│ │ │ │ │
|
|
39
|
-
│ Reactive Stash Virtual FS Router │
|
|
40
|
-
│ (app state) (file storage) (URL → VFS) │
|
|
41
|
-
│ │ │ │ │
|
|
42
|
-
│ └────────────────┼────────────────┘ │
|
|
43
|
-
│ │ │
|
|
44
|
-
└─────────────────► Renderer ◄────────────────────┘
|
|
45
|
-
(compiles + mounts)
|
|
46
|
-
│
|
|
47
|
-
DOM
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
| Module | Size | Role |
|
|
51
|
-
|--------|------|------|
|
|
52
|
-
| `ui.js` | ~175 lines | `createApp` entry point with `loadBundle`, `watch`, re-exports |
|
|
53
|
-
| `stash.js` | ~400 lines | Deep reactive state tree with path-based navigation |
|
|
54
|
-
| `vfs.js` | ~200 lines | Browser-local Virtual File System with watchers |
|
|
55
|
-
| `router.js` | ~300 lines | File-based router (URL ↔ VFS paths, History API) |
|
|
56
|
-
| `renderer.js` | ~250 lines | Component lifecycle, layouts, transitions, `remount` |
|
|
57
|
-
| `serve.rip` | ~140 lines | Server middleware: framework files, manifest, SSE hot-reload |
|
|
58
|
-
|
|
59
|
-
## The Idea
|
|
60
|
-
|
|
61
|
-
Modern web frameworks — React, Vue, Svelte, Solid — all share the same
|
|
62
|
-
fundamental assumption: code must be compiled and bundled on the developer's
|
|
63
|
-
machine, then shipped as static artifacts. This creates an entire ecosystem of
|
|
64
|
-
build tools (Vite, Webpack, Turbopack, esbuild), configuration files, dev
|
|
65
|
-
servers, hot module replacement protocols, and deployment pipelines. The
|
|
66
|
-
developer experience is powerful, but the machinery is enormous.
|
|
67
|
-
|
|
68
|
-
Rip UI asks: **what if the compiler ran in the browser?**
|
|
69
|
-
|
|
70
|
-
At 40KB, the Rip compiler is small enough to ship alongside your application.
|
|
71
|
-
Components arrive as `.rip` source files — plain text — and are compiled to
|
|
72
|
-
JavaScript on the client's machine. This eliminates the build step entirely.
|
|
73
|
-
There is no `dist/` folder, no source maps, no chunk splitting, no tree
|
|
74
|
-
shaking, no CI build minutes. You write a `.rip` file, the browser compiles it,
|
|
75
|
-
and it runs.
|
|
76
|
-
|
|
77
|
-
This is not a toy or a limitation. The compiler produces the same output it
|
|
78
|
-
would on a server. The reactive system is the same signal-based engine that
|
|
79
|
-
powers server-side Rip. The component model compiles to anonymous ES6 classes
|
|
80
|
-
with fine-grained DOM manipulation — no virtual DOM diffing.
|
|
81
|
-
|
|
82
|
-
### How It Differs from Existing Frameworks
|
|
83
|
-
|
|
84
|
-
| | React/Vue/Svelte | Rip UI |
|
|
85
|
-
|---|---|---|
|
|
86
|
-
| **Build step** | Required (Vite, Webpack, etc.) | None — compiler runs in browser |
|
|
87
|
-
| **Bundle size** | 40-100KB+ framework + app bundle | 40KB compiler + ~8KB framework + raw source |
|
|
88
|
-
| **HMR** | Dev server ↔ browser WebSocket | SSE notify + VFS invalidation + recompile |
|
|
89
|
-
| **Deployment** | Build artifacts (`dist/`) | Source files served as-is |
|
|
90
|
-
| **Component format** | JSX, SFC, templates | Rip source (`.rip` files) |
|
|
91
|
-
| **Reactivity** | Library-specific (hooks, refs, signals) | Language-native (`:=`, `~=`, `~>`) |
|
|
92
|
-
| **DOM updates** | Virtual DOM diff or compiled transforms | Fine-grained effects, direct DOM mutation |
|
|
93
|
-
| **Routing** | Framework plugin (react-router, etc.) | Built-in file-based router over VFS |
|
|
94
|
-
| **State management** | External library (Redux, Pinia, etc.) | Built-in reactive stash with deep tracking |
|
|
95
|
-
|
|
96
|
-
## Component Model
|
|
97
|
-
|
|
98
|
-
The component system adds two keywords to Rip: `component` and `render`. Everything
|
|
99
|
-
else — reactive state (`:=`), computed values (`~=`), effects (`~>`), methods,
|
|
100
|
-
lifecycle — uses standard Rip syntax. A component is an expression that evaluates
|
|
101
|
-
to an anonymous ES6 class.
|
|
102
|
-
|
|
103
|
-
### Defining a Component
|
|
7
|
+
Widgets are plain `.rip` source files — no build step. The browser compiles
|
|
8
|
+
them on the fly via Rip UI's runtime. Include them in your app by adding the
|
|
9
|
+
widgets directory to your serve middleware:
|
|
104
10
|
|
|
105
11
|
```coffee
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
doubled ~= @count * 2 # computed (derived, read-only)
|
|
111
|
-
|
|
112
|
-
increment: -> @count += @step
|
|
113
|
-
decrement: -> @count -= @step
|
|
114
|
-
|
|
115
|
-
mounted: ->
|
|
116
|
-
console.log "Counter mounted"
|
|
117
|
-
|
|
118
|
-
render
|
|
119
|
-
div.counter
|
|
120
|
-
h1 "Count: #{@count}"
|
|
121
|
-
p "Doubled: #{doubled}"
|
|
122
|
-
button @click: @increment, "+#{@step}"
|
|
123
|
-
button @click: @decrement, "-#{@step}"
|
|
12
|
+
use serve
|
|
13
|
+
dir: dir
|
|
14
|
+
components: ['components', '../../../packages/ui']
|
|
124
15
|
```
|
|
125
16
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.step = props.step ?? 1;
|
|
133
|
-
this.doubled = __computed(() => this.count.value * 2);
|
|
134
|
-
}
|
|
135
|
-
increment() { return this.count.value += this.step; }
|
|
136
|
-
decrement() { return this.count.value -= this.step; }
|
|
137
|
-
mounted() { return console.log("Counter mounted"); }
|
|
138
|
-
_create() { /* fine-grained DOM creation */ }
|
|
139
|
-
_setup() { /* reactive effect bindings */ }
|
|
140
|
-
mount(target) { /* ... */ }
|
|
141
|
-
unmount() { /* ... */ }
|
|
142
|
-
}
|
|
143
|
-
```
|
|
17
|
+
Every widget:
|
|
18
|
+
- Handles all keyboard interactions per WAI-ARIA Authoring Practices
|
|
19
|
+
- Sets correct ARIA attributes automatically
|
|
20
|
+
- Exposes state via `$` sigil (`$open`, `$selected`) — compiles to `data-*` attributes for CSS
|
|
21
|
+
- Ships zero CSS — styling is entirely in the user's stylesheets
|
|
22
|
+
- Uses Rip's reactive primitives for all state management
|
|
144
23
|
|
|
145
|
-
|
|
24
|
+
---
|
|
146
25
|
|
|
147
|
-
|
|
26
|
+
## Overview
|
|
148
27
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
28
|
+
| Component | What It Handles |
|
|
29
|
+
|-----------|----------------|
|
|
30
|
+
| **Select** | Keyboard navigation, typeahead, ARIA listbox, positioning |
|
|
31
|
+
| **Combobox** | Input filtering, keyboard nav, ARIA combobox, positioning |
|
|
32
|
+
| **Dialog** | Focus trap, scroll lock, escape/click-outside dismiss, ARIA roles |
|
|
33
|
+
| **Toast** | Auto-dismiss timer, stacking, ARIA live region |
|
|
34
|
+
| **Popover** | Anchor positioning, flip/shift, dismiss behavior, ARIA |
|
|
35
|
+
| **Tooltip** | Show/hide with delay, anchor positioning, ARIA describedby |
|
|
36
|
+
| **Tabs** | Arrow key navigation, ARIA tablist/tab/tabpanel |
|
|
37
|
+
| **Accordion** | Expand/collapse, single or multiple, ARIA |
|
|
38
|
+
| **Checkbox** | Toggle state, indeterminate, ARIA checked |
|
|
39
|
+
| **Menu** | Keyboard navigation, ARIA menu roles |
|
|
40
|
+
| **Grid** | Virtual scrolling, DOM recycling, cell selection, inline editing, sorting, resizing, clipboard |
|
|
156
41
|
|
|
157
|
-
|
|
42
|
+
---
|
|
158
43
|
|
|
159
|
-
|
|
160
|
-
- Classes use dot notation: `div.card.active`, `button.btn.btn-primary`
|
|
161
|
-
- Dynamic classes use dot-parens: `div.("active" if @selected)` (CLSX-like)
|
|
162
|
-
- Attributes use object syntax: `input type: "text", placeholder: "..."`
|
|
163
|
-
- Events use `@` prefix: `button @click: @handleClick`
|
|
164
|
-
- Text content is a string argument: `h1 "Hello"`
|
|
165
|
-
- Interpolation works: `p "Count: #{@count}"`
|
|
166
|
-
- Children are indented below their parent
|
|
167
|
-
- `if`/`else` and `for...in` work inside templates
|
|
44
|
+
## Widgets
|
|
168
45
|
|
|
169
|
-
|
|
170
|
-
compiler — just Rip's existing syntax applied to DOM construction.
|
|
46
|
+
### Select
|
|
171
47
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
Every `@` property on a component is a prop that the parent can set. The child
|
|
175
|
-
owns the property; the parent can provide an initial value or pass a reactive
|
|
176
|
-
signal:
|
|
48
|
+
Keyboard-navigable dropdown with typeahead. For provider selects, account
|
|
49
|
+
pickers, or any single-value choice from a list.
|
|
177
50
|
|
|
178
51
|
```coffee
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Counter {count: parentCount}
|
|
52
|
+
Select value <=> selectedRole, @change: handleChange
|
|
53
|
+
option value: "eng", "Engineer"
|
|
54
|
+
option value: "des", "Designer"
|
|
55
|
+
option value: "mgr", "Manager"
|
|
184
56
|
```
|
|
185
57
|
|
|
186
|
-
|
|
58
|
+
**Props:** `@value`, `@placeholder`, `@disabled`
|
|
59
|
+
**Events:** `@change` (detail: selected value)
|
|
60
|
+
**Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close, Home/End
|
|
61
|
+
jump, type-ahead character matching
|
|
62
|
+
**Data attributes:** `$open` / `[data-open]` on trigger, `$highlighted` / `[data-highlighted]` and `$selected` / `[data-selected]` on options, `$disabled` / `[data-disabled]` on trigger
|
|
187
63
|
|
|
188
|
-
|
|
189
|
-
this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
|
|
190
|
-
```
|
|
64
|
+
### Combobox
|
|
191
65
|
|
|
192
|
-
|
|
66
|
+
Filterable input + dropdown for search-as-you-type scenarios. For patient
|
|
67
|
+
search, autocomplete, or any large list that needs filtering.
|
|
193
68
|
|
|
194
69
|
```coffee
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
70
|
+
Combobox query <=> searchText, @select: handleSelect, @filter: handleFilter
|
|
71
|
+
for item in filteredItems
|
|
72
|
+
div $value: item.id
|
|
73
|
+
span item.name
|
|
198
74
|
```
|
|
199
75
|
|
|
200
|
-
|
|
76
|
+
**Props:** `@query`, `@placeholder`, `@disabled`
|
|
77
|
+
**Events:** `@select` (detail: selected data-value), `@filter` (detail: query string)
|
|
78
|
+
**Keyboard:** ArrowDown/Up navigate, Enter select (or first if only one match),
|
|
79
|
+
Escape close/clear, Tab close
|
|
80
|
+
**Data attributes:** `$open` / `[data-open]` on wrapper, `$highlighted` / `[data-highlighted]` on items
|
|
201
81
|
|
|
202
|
-
|
|
203
|
-
UserCard = component
|
|
204
|
-
@name # required — no default, error if missing
|
|
205
|
-
@avatar = "/default.png" # optional — has a default
|
|
206
|
-
@bio? := "" # optional reactive — ? makes it explicitly optional
|
|
207
|
-
```
|
|
82
|
+
### Dialog
|
|
208
83
|
|
|
209
|
-
|
|
84
|
+
Modal dialog with focus trap, scroll lock, and escape/click-outside dismiss.
|
|
85
|
+
Restores focus to the previously focused element on close.
|
|
210
86
|
|
|
211
87
|
```coffee
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
~> console.log "#{remaining} of #{total} remaining"
|
|
218
|
-
|
|
219
|
-
render
|
|
220
|
-
div
|
|
221
|
-
h2 "Todos (#{remaining}/#{total})"
|
|
222
|
-
# ...
|
|
88
|
+
Dialog open <=> showDialog, @close: handleClose
|
|
89
|
+
h2 "Confirm Action"
|
|
90
|
+
p "Are you sure?"
|
|
91
|
+
button @click: (=> showDialog = false), "Cancel"
|
|
92
|
+
button @click: handleConfirm, "Confirm"
|
|
223
93
|
```
|
|
224
94
|
|
|
225
|
-
|
|
95
|
+
**Props:** `@open`
|
|
96
|
+
**Events:** `@close`
|
|
97
|
+
**Keyboard:** Escape to close, Tab trapped within dialog
|
|
98
|
+
**Data attributes:** `$open` / `[data-open]` on backdrop
|
|
99
|
+
**Behavior:** Focus trap confines Tab to dialog content. Body scroll is locked
|
|
100
|
+
while open. Previous focus is restored on close.
|
|
226
101
|
|
|
227
|
-
|
|
228
|
-
|
|
102
|
+
### Toast
|
|
103
|
+
|
|
104
|
+
Managed toast system with stacking and timer pause on hover. The state is
|
|
105
|
+
the array, the operation is assignment — no helpers needed.
|
|
229
106
|
|
|
230
107
|
```coffee
|
|
231
|
-
|
|
232
|
-
@elapsed := 0
|
|
233
|
-
@interval = null
|
|
108
|
+
toasts := []
|
|
234
109
|
|
|
235
|
-
|
|
236
|
-
|
|
110
|
+
# Add — reactive assignment is the API
|
|
111
|
+
toasts = [...toasts, { message: "Saved!", type: "success" }]
|
|
237
112
|
|
|
238
|
-
|
|
239
|
-
|
|
113
|
+
# Dismiss — filter it out
|
|
114
|
+
toasts = toasts.filter (t) -> t isnt target
|
|
240
115
|
|
|
241
|
-
|
|
242
|
-
|
|
116
|
+
# Render
|
|
117
|
+
ToastViewport toasts <=> toasts
|
|
243
118
|
```
|
|
244
119
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
120
|
+
**ToastViewport props:** `@toasts`, `@placement` (bottom-right, top-right, etc.)
|
|
121
|
+
**Toast props:** `@toast` (object with `message`, `type`, `duration`, `title`, `action`)
|
|
122
|
+
**Toast defaults:** `duration` = 4000ms, `type` = 'info'
|
|
123
|
+
**Events:** `@dismiss` (detail: toast object)
|
|
124
|
+
**Data attributes:** `$type` / `[data-type]`, `$leaving` / `[data-leaving]` (during exit animation)
|
|
125
|
+
**Behavior:** Timer pauses on hover and keyboard focus, resumes on leave.
|
|
249
126
|
|
|
250
|
-
|
|
251
|
-
SearchBox = component
|
|
252
|
-
@query := ""
|
|
127
|
+
### Popover
|
|
253
128
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
p "Searching for: #{@query}"
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Child Components
|
|
260
|
-
|
|
261
|
-
Components can nest. Props are passed as object arguments:
|
|
129
|
+
Floating content anchored to a trigger element. Positions itself with
|
|
130
|
+
flip/shift to stay in viewport.
|
|
262
131
|
|
|
263
132
|
```coffee
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
div
|
|
269
|
-
Header {title: "My App"}
|
|
270
|
-
UserCard {name: @user.name, avatar: "/alice.png"}
|
|
271
|
-
Footer
|
|
133
|
+
Popover placement: "bottom-start"
|
|
134
|
+
button "Options"
|
|
135
|
+
div
|
|
136
|
+
p "Popover content here"
|
|
272
137
|
```
|
|
273
138
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
139
|
+
**Props:** `@placement`, `@offset`, `@disabled`
|
|
140
|
+
**Keyboard:** Enter/Space/ArrowDown toggle, Escape close
|
|
141
|
+
**Data attributes:** `$open` / `[data-open]`, `$placement` / `[data-placement]` on floating element
|
|
277
142
|
|
|
278
|
-
|
|
279
|
-
# In a parent component's constructor:
|
|
280
|
-
setContext 'theme', @theme
|
|
281
|
-
|
|
282
|
-
# In any descendant component:
|
|
283
|
-
theme = getContext 'theme'
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Multiple Components Per File
|
|
143
|
+
### Tooltip
|
|
287
144
|
|
|
288
|
-
|
|
289
|
-
can live in one file:
|
|
145
|
+
Hover/focus tooltip with configurable delay and positioning.
|
|
290
146
|
|
|
291
147
|
```coffee
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
@onClick = null
|
|
295
|
-
render
|
|
296
|
-
button.btn @click: @onClick, @label
|
|
297
|
-
|
|
298
|
-
Card = component
|
|
299
|
-
@title = ""
|
|
300
|
-
render
|
|
301
|
-
div.card
|
|
302
|
-
h3 @title
|
|
303
|
-
div.card-body
|
|
304
|
-
slot
|
|
305
|
-
|
|
306
|
-
Page = component
|
|
307
|
-
render
|
|
308
|
-
div
|
|
309
|
-
Card {title: "Welcome"}
|
|
310
|
-
Button {label: "Get Started", onClick: -> alert "Go!"}
|
|
148
|
+
Tooltip text: "Save your changes", placement: "top"
|
|
149
|
+
button "Save"
|
|
311
150
|
```
|
|
312
151
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
disk.
|
|
318
|
-
|
|
319
|
-
```javascript
|
|
320
|
-
import { vfs } from '@rip-lang/ui/vfs'
|
|
321
|
-
|
|
322
|
-
const fs = vfs()
|
|
323
|
-
|
|
324
|
-
// Write source files
|
|
325
|
-
fs.write('pages/index.rip', 'component Home\n render\n h1 "Hello"')
|
|
326
|
-
fs.write('pages/users/[id].rip', '...')
|
|
152
|
+
**Props:** `@text`, `@placement`, `@delay` (ms), `@offset`
|
|
153
|
+
**Data attributes:** `$open` / `[data-open]`, `$entering` / `[data-entering]`, `$exiting` / `[data-exiting]`, `$placement` / `[data-placement]`
|
|
154
|
+
**Behavior:** Shows on mouseenter/focusin after delay, hides on
|
|
155
|
+
mouseleave/focusout. Uses `aria-describedby` for accessibility.
|
|
327
156
|
|
|
328
|
-
|
|
329
|
-
fs.read('pages/index.rip') // source string
|
|
330
|
-
fs.exists('pages/index.rip') // true
|
|
331
|
-
fs.list('pages/') // ['index.rip', 'users/']
|
|
332
|
-
fs.listAll('pages/') // all files recursively
|
|
157
|
+
### Tabs
|
|
333
158
|
|
|
334
|
-
|
|
335
|
-
fs.watch('pages/', ({ event, path }) => {
|
|
336
|
-
console.log(`${event}: ${path}`)
|
|
337
|
-
})
|
|
159
|
+
Keyboard-navigable tab panel with roving tabindex.
|
|
338
160
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
161
|
+
```coffee
|
|
162
|
+
Tabs active <=> currentTab
|
|
163
|
+
div $tab: "one", "Tab One"
|
|
164
|
+
div $tab: "two", "Tab Two"
|
|
165
|
+
div $panel: "one"
|
|
166
|
+
p "Content for tab one"
|
|
167
|
+
div $panel: "two"
|
|
168
|
+
p "Content for tab two"
|
|
346
169
|
```
|
|
347
170
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
store source files in the browser. The VFS provides:
|
|
353
|
-
|
|
354
|
-
- **Addressable storage** — components are referenced by path, just like files
|
|
355
|
-
- **File watching** — the renderer re-compiles when a file changes
|
|
356
|
-
- **Lazy loading** — pages can be fetched on demand as the user navigates
|
|
357
|
-
- **Hot update** — write a new version of a file and the component re-renders
|
|
358
|
-
- **Manifest loading** — bulk-fetch an app's files in one call
|
|
359
|
-
|
|
360
|
-
The VFS is not IndexedDB or localStorage — it's a plain in-memory Map. Fast,
|
|
361
|
-
simple, ephemeral. For persistence, the server delivers files on page load.
|
|
171
|
+
**Props:** `@active`
|
|
172
|
+
**Events:** `@change` (detail: tab id)
|
|
173
|
+
**Keyboard:** ArrowLeft/Right navigate tabs, Home/End jump
|
|
174
|
+
**Data attributes:** `$active` / `[data-active]` on active tab and panel
|
|
362
175
|
|
|
363
|
-
|
|
176
|
+
### Accordion
|
|
364
177
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
```javascript
|
|
368
|
-
import { createRouter } from '@rip-lang/ui/router'
|
|
369
|
-
|
|
370
|
-
const router = createRouter(fs, { root: 'pages' })
|
|
371
|
-
```
|
|
178
|
+
Expand/collapse sections. Single or multiple mode.
|
|
372
179
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
| `pages/blog/[...slug].rip` | `/blog/*slug` | Catch-all |
|
|
380
|
-
| `pages/_layout.rip` | — | Root layout (wraps all pages) |
|
|
381
|
-
| `pages/users/_layout.rip` | — | Nested layout (wraps `/users/*`) |
|
|
382
|
-
|
|
383
|
-
```javascript
|
|
384
|
-
// Navigate
|
|
385
|
-
router.push('/users/42')
|
|
386
|
-
router.replace('/login')
|
|
387
|
-
router.back()
|
|
388
|
-
|
|
389
|
-
// Reactive route state
|
|
390
|
-
effect(() => {
|
|
391
|
-
console.log(router.path) // '/users/42'
|
|
392
|
-
console.log(router.params) // { id: '42' }
|
|
393
|
-
})
|
|
180
|
+
```coffee
|
|
181
|
+
Accordion multiple: false
|
|
182
|
+
div $item: "a"
|
|
183
|
+
button $trigger: true, "Section A"
|
|
184
|
+
div $content: true
|
|
185
|
+
p "Content A"
|
|
394
186
|
```
|
|
395
187
|
|
|
396
|
-
|
|
397
|
-
|
|
188
|
+
**Props:** `@multiple`
|
|
189
|
+
**Events:** `@change` (detail: array of open item ids)
|
|
190
|
+
**Keyboard:** Enter/Space toggle, ArrowDown/Up move between triggers, Home/End
|
|
191
|
+
**Methods:** `toggle(id)`, `isOpen(id)`
|
|
398
192
|
|
|
399
|
-
|
|
193
|
+
### Checkbox
|
|
400
194
|
|
|
401
|
-
|
|
402
|
-
automatically tracked — changing any value triggers fine-grained updates.
|
|
195
|
+
Toggle with checkbox or switch semantics. Supports indeterminate state.
|
|
403
196
|
|
|
404
|
-
```
|
|
405
|
-
|
|
197
|
+
```coffee
|
|
198
|
+
Checkbox checked <=> isActive, @change: handleChange
|
|
199
|
+
span "Enable notifications"
|
|
406
200
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
})
|
|
201
|
+
Checkbox checked <=> isDark, switch: true
|
|
202
|
+
span "Dark mode"
|
|
203
|
+
```
|
|
411
204
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
205
|
+
**Props:** `@checked`, `@disabled`, `@indeterminate`, `@switch`
|
|
206
|
+
**Events:** `@change` (detail: boolean)
|
|
207
|
+
**Keyboard:** Enter/Space toggle
|
|
208
|
+
**Data attributes:** `$checked` / `[data-checked]`, `$indeterminate` / `[data-indeterminate]`, `$disabled` / `[data-disabled]`
|
|
209
|
+
**ARIA:** `role="checkbox"` or `role="switch"`, `aria-checked` (true/false/mixed)
|
|
415
210
|
|
|
416
|
-
|
|
417
|
-
app.get('user.prefs.theme') // 'light'
|
|
418
|
-
app.set('cart.items[0]', { id: 1 }) // deep set with auto-creation
|
|
419
|
-
app.has('user.name') // true
|
|
420
|
-
app.del('cart.items[0]') // delete
|
|
421
|
-
app.inc('cart.total', 9.99) // increment
|
|
422
|
-
app.merge({ user: { role: 'admin' }}) // shallow merge
|
|
423
|
-
app.keys('user') // ['name', 'prefs', 'role']
|
|
211
|
+
### Menu
|
|
424
212
|
|
|
425
|
-
|
|
426
|
-
effect(() => {
|
|
427
|
-
console.log(`Theme: ${app.user.prefs.theme}`)
|
|
428
|
-
// Re-runs whenever theme changes
|
|
429
|
-
})
|
|
430
|
-
```
|
|
213
|
+
Dropdown menu with keyboard navigation. For action menus, context menus.
|
|
431
214
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
| **App** | Global | Entire session | `app.user`, `app.theme` |
|
|
439
|
-
| **Route** | Per-route | Until navigation | `router.params`, `router.query` |
|
|
440
|
-
| **Component** | Per-instance | Until unmount | `:=` reactive state |
|
|
441
|
-
|
|
442
|
-
App state lives in the stash. Route state lives in the router. Component state
|
|
443
|
-
lives in the component instance as signals. All three are reactive — changes at
|
|
444
|
-
any level trigger the appropriate DOM updates.
|
|
445
|
-
|
|
446
|
-
## Component Renderer
|
|
447
|
-
|
|
448
|
-
The renderer is the bridge between the router and the DOM. When the route
|
|
449
|
-
changes, the renderer:
|
|
450
|
-
|
|
451
|
-
1. Resolves the VFS path for the new route
|
|
452
|
-
2. Reads the `.rip` source from the VFS
|
|
453
|
-
3. Compiles it to JavaScript using the Rip compiler
|
|
454
|
-
4. Evaluates the compiled code to get a component class
|
|
455
|
-
5. Instantiates the component, passing route params as props
|
|
456
|
-
6. Wraps it in any applicable layout components
|
|
457
|
-
7. Mounts the result into the DOM target
|
|
458
|
-
8. Runs transition animations (if configured)
|
|
459
|
-
9. Unmounts the previous component
|
|
460
|
-
|
|
461
|
-
```javascript
|
|
462
|
-
import { createRenderer } from '@rip-lang/ui/renderer'
|
|
463
|
-
|
|
464
|
-
const renderer = createRenderer({
|
|
465
|
-
router,
|
|
466
|
-
fs,
|
|
467
|
-
stash: appState,
|
|
468
|
-
compile: compileToJS,
|
|
469
|
-
target: '#app',
|
|
470
|
-
transition: { duration: 200 }
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
renderer.start() // Watch for route changes, mount components
|
|
474
|
-
renderer.stop() // Unmount everything, clean up
|
|
215
|
+
```coffee
|
|
216
|
+
Menu @select: handleAction
|
|
217
|
+
button $trigger: true, "Actions"
|
|
218
|
+
div $item: "edit", "Edit"
|
|
219
|
+
div $item: "delete", "Delete"
|
|
220
|
+
div $item: "archive", "Archive"
|
|
475
221
|
```
|
|
476
222
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
VFS automatically causes the next render to use the new version.
|
|
482
|
-
|
|
483
|
-
## Server Integration
|
|
223
|
+
**Props:** `@disabled`
|
|
224
|
+
**Events:** `@select` (detail: item id)
|
|
225
|
+
**Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close, Home/End
|
|
226
|
+
**Data attributes:** `$open` / `[data-open]` on trigger, `$highlighted` / `[data-highlighted]` on items
|
|
484
227
|
|
|
485
|
-
###
|
|
228
|
+
### Grid
|
|
486
229
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
230
|
+
High-performance data grid with virtual scrolling, DOM recycling, cell
|
|
231
|
+
selection, inline editing, column sorting, and column resizing. Renders 100K+
|
|
232
|
+
rows at 60fps.
|
|
490
233
|
|
|
491
234
|
```coffee
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
235
|
+
Grid
|
|
236
|
+
data: employees
|
|
237
|
+
columns: [
|
|
238
|
+
{ key: 'name', title: 'Name', width: 200 }
|
|
239
|
+
{ key: 'age', title: 'Age', width: 80, align: 'right' }
|
|
240
|
+
{ key: 'role', title: 'Role', width: 150, type: 'select', source: roles }
|
|
241
|
+
{ key: 'active', title: 'Active', width: 60, type: 'checkbox' }
|
|
242
|
+
]
|
|
243
|
+
rowHeight: 32
|
|
244
|
+
overscan: 5
|
|
245
|
+
striped: true
|
|
246
|
+
@beforeEdit: validator
|
|
247
|
+
@afterEdit: saveHandler
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Props:** `@data`, `@columns`, `@rowHeight`, `@headerHeight`, `@overscan`,
|
|
251
|
+
`@striped`, `@beforeEdit`, `@afterEdit`
|
|
252
|
+
**Column properties:** `key`, `title`, `width`, `align`, `type` (text/number/
|
|
253
|
+
checkbox/select), `source` (for select type)
|
|
254
|
+
**Methods:** `getCell(row, col)`, `setCell(row, col, value)`, `getData()`,
|
|
255
|
+
`setData(data)`, `sort(col, direction)`, `scrollToRow(index)`,
|
|
256
|
+
`copySelection()`, `cutSelection()`, `pasteAtActive()`
|
|
257
|
+
**Keyboard:** Arrows navigate, Tab/Shift+Tab move cells, Enter/F2 edit,
|
|
258
|
+
Escape cancel, Home/End, Ctrl+arrows jump to edge, PageUp/Down, Ctrl+A
|
|
259
|
+
select all, Ctrl+C copy, Ctrl+V paste, Ctrl+X cut, Delete/Backspace clear,
|
|
260
|
+
Space toggle checkboxes, type-to-edit
|
|
261
|
+
**Data attributes:** `$active` / `[data-active]` and `$selected` / `[data-selected]` on cells, `$sorted` / `[data-sorted]` on headers, `$editing` / `[data-editing]` and `$selecting` / `[data-selecting]` on container
|
|
262
|
+
**Sorting:** Click header to sort (asc/desc/none cycle), Shift+click for
|
|
263
|
+
multi-column sort
|
|
264
|
+
**Editing:** Double-click, Enter, F2, or start typing to edit. Enter/Tab
|
|
265
|
+
commit, Escape cancel. Checkbox cells toggle on click/Space.
|
|
266
|
+
**Clipboard:** Ctrl+C copies the selection as TSV (tab-separated values) —
|
|
267
|
+
the universal spreadsheet interchange format. Ctrl+V pastes TSV from
|
|
268
|
+
clipboard starting at the active cell, respecting column types and format
|
|
269
|
+
parsers. Ctrl+X copies then clears the selection. Full interop with Excel,
|
|
270
|
+
Google Sheets, and Numbers.
|
|
271
|
+
**CSS theming:** Uses `--grid-*` custom properties (see `GRID.md` in
|
|
272
|
+
`packages/grid/` for the full property list and dark mode example)
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Styling
|
|
277
|
+
|
|
278
|
+
All widgets ship zero CSS. Write `$name` in Rip (compiles to `data-name` in HTML), then style with `[data-name]` selectors in CSS:
|
|
279
|
+
|
|
280
|
+
```css
|
|
281
|
+
.select-trigger[data-open] { border-color: var(--color-primary); }
|
|
282
|
+
.option[data-highlighted] { background: var(--surface-2); }
|
|
283
|
+
.option[data-selected] { font-weight: 600; color: var(--color-primary); }
|
|
284
|
+
.dialog-backdrop[data-open] { animation: fade-in 150ms; }
|
|
285
|
+
.toast[data-type="success"] { border-left: 3px solid green; }
|
|
286
|
+
.toast[data-leaving] { animation: fade-out 200ms; }
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
No JavaScript styling logic. No className toggling. Write `$open` in Rip,
|
|
290
|
+
style `[data-open]` in CSS. Any CSS methodology works — vanilla, Tailwind,
|
|
291
|
+
Open Props, a custom design system. The widgets don't care.
|
|
292
|
+
|
|
293
|
+
### Why Not CSS-in-JS?
|
|
294
|
+
|
|
295
|
+
Libraries like Emotion and styled-components parse CSS strings at runtime,
|
|
296
|
+
generate class names in JavaScript, and inject `<style>` tags into the
|
|
297
|
+
document. This bundles styling into the component's JS, adds a runtime
|
|
298
|
+
dependency, and locks consumers into the library's API. You can't restyle
|
|
299
|
+
a component without modifying its source or fighting specificity.
|
|
300
|
+
|
|
301
|
+
Our approach separates concerns: **behavior lives in Rip, styling lives in
|
|
302
|
+
CSS, and `data-*` attributes are the interface between them.** The result
|
|
303
|
+
is faster (no CSS parsing in JS), smaller (no styling runtime), and more
|
|
304
|
+
flexible (swap your entire design system without touching widget code).
|
|
305
|
+
|
|
306
|
+
### Open Props — Design Tokens
|
|
307
|
+
|
|
308
|
+
[Open Props](https://open-props.style/) provides consistent scales for spacing,
|
|
309
|
+
color, shadow, radius, easing, and typography as CSS custom properties. Pure CSS
|
|
310
|
+
(4KB), no runtime, no build step.
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
bun add open-props
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Import what you need:
|
|
317
|
+
|
|
318
|
+
```css
|
|
319
|
+
@import "open-props/sizes";
|
|
320
|
+
@import "open-props/colors";
|
|
321
|
+
@import "open-props/shadows";
|
|
322
|
+
@import "open-props/radii";
|
|
323
|
+
@import "open-props/easings";
|
|
324
|
+
@import "open-props/fonts";
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Or import everything:
|
|
328
|
+
|
|
329
|
+
```css
|
|
330
|
+
@import "open-props/style";
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Override or extend any token:
|
|
334
|
+
|
|
335
|
+
```css
|
|
336
|
+
:root {
|
|
337
|
+
--color-primary: oklch(55% 0.25 260);
|
|
338
|
+
--radius-card: var(--radius-3);
|
|
531
339
|
}
|
|
532
340
|
```
|
|
533
341
|
|
|
534
|
-
|
|
535
|
-
populates the VFS in a single request — no need to list pages manually.
|
|
536
|
-
|
|
537
|
-
## Hot Reload
|
|
342
|
+
**Token categories:**
|
|
538
343
|
|
|
539
|
-
|
|
540
|
-
|
|
344
|
+
- **Spacing** — `--size-1` through `--size-15` (0.25rem to 7.5rem)
|
|
345
|
+
- **Colors** — Full palettes (`--blue-0` through `--blue-12`, etc.) plus semantic surface tokens
|
|
346
|
+
- **Shadows** — `--shadow-1` through `--shadow-6`, progressively stronger
|
|
347
|
+
- **Radii** — `--radius-1` through `--radius-6` plus `--radius-round`
|
|
348
|
+
- **Easing** — `--ease-1` through `--ease-5` (standard) and `--ease-spring-1` through `--ease-spring-5`
|
|
349
|
+
- **Typography** — `--font-size-0` through `--font-size-8`, `--font-weight-1` through `--font-weight-9`, `--font-lineheight-0` through `--font-lineheight-5`
|
|
541
350
|
|
|
542
|
-
|
|
351
|
+
Define project-level aliases:
|
|
543
352
|
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
├── Rebuild router (router.rebuild)
|
|
556
|
-
├── Smart refetch:
|
|
557
|
-
│ ├── Current page file? → fetch immediately
|
|
558
|
-
│ ├── Active layout? → fetch immediately
|
|
559
|
-
│ ├── Eager file? → fetch immediately
|
|
560
|
-
│ └── Other pages? → fetch lazily on next navigation
|
|
561
|
-
└── Re-render (renderer.remount)
|
|
353
|
+
```css
|
|
354
|
+
:root {
|
|
355
|
+
--color-primary: var(--indigo-7);
|
|
356
|
+
--color-danger: var(--red-7);
|
|
357
|
+
--color-success: var(--green-7);
|
|
358
|
+
--color-text: var(--gray-9);
|
|
359
|
+
--color-text-muted: var(--gray-6);
|
|
360
|
+
--surface-1: var(--gray-0);
|
|
361
|
+
--surface-2: var(--gray-1);
|
|
362
|
+
--surface-3: var(--gray-2);
|
|
363
|
+
}
|
|
562
364
|
```
|
|
563
365
|
|
|
564
|
-
###
|
|
366
|
+
### CSS Architecture
|
|
565
367
|
|
|
566
|
-
|
|
567
|
-
notifications. A heartbeat every 5 seconds keeps the connection alive:
|
|
368
|
+
Modern CSS eliminates the need for preprocessors. Use these features directly:
|
|
568
369
|
|
|
569
|
-
|
|
570
|
-
use ripUI pages: "#{dir}/pages", watch: true, debounce: 250
|
|
571
|
-
```
|
|
370
|
+
**Nesting** — group related rules:
|
|
572
371
|
|
|
573
|
-
|
|
372
|
+
```css
|
|
373
|
+
.card {
|
|
374
|
+
padding: var(--size-4);
|
|
574
375
|
|
|
575
|
-
|
|
376
|
+
& .title {
|
|
377
|
+
font-size: var(--font-size-4);
|
|
378
|
+
font-weight: var(--font-weight-7);
|
|
379
|
+
}
|
|
576
380
|
|
|
577
|
-
|
|
578
|
-
|
|
381
|
+
&:hover {
|
|
382
|
+
box-shadow: var(--shadow-3);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
579
385
|
```
|
|
580
386
|
|
|
581
|
-
|
|
582
|
-
be refetched immediately, even if they're not the current route:
|
|
387
|
+
**Cascade Layers** — control specificity:
|
|
583
388
|
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
eager: ['pages/_layout.rip']
|
|
587
|
-
});
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### `loadBundle(url)`
|
|
389
|
+
```css
|
|
390
|
+
@layer base, components, overrides;
|
|
591
391
|
|
|
592
|
-
|
|
593
|
-
|
|
392
|
+
@layer base {
|
|
393
|
+
button { font: inherit; }
|
|
394
|
+
}
|
|
594
395
|
|
|
595
|
-
|
|
596
|
-
|
|
396
|
+
@layer components {
|
|
397
|
+
.dialog { border-radius: var(--radius-3); }
|
|
398
|
+
}
|
|
597
399
|
```
|
|
598
400
|
|
|
599
|
-
|
|
401
|
+
**Container Queries** — style based on the container, not the viewport:
|
|
600
402
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
403
|
+
```css
|
|
404
|
+
.sidebar {
|
|
405
|
+
container-type: inline-size;
|
|
406
|
+
}
|
|
604
407
|
|
|
605
|
-
|
|
606
|
-
|
|
408
|
+
@container (min-width: 400px) {
|
|
409
|
+
.sidebar .nav { flex-direction: row; }
|
|
410
|
+
}
|
|
607
411
|
```
|
|
608
412
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
### With Server Integration (Recommended)
|
|
612
|
-
|
|
613
|
-
The fastest way to get started is with `@rip-lang/api` and the `ripUI` middleware.
|
|
614
|
-
The middleware serves all framework files, auto-generates a page manifest, and
|
|
615
|
-
provides hot-reload — your server is just a few lines:
|
|
413
|
+
**`color-mix()`** — derive colors without Sass:
|
|
616
414
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
415
|
+
```css
|
|
416
|
+
.muted {
|
|
417
|
+
color: color-mix(in oklch, var(--color-text), transparent 40%);
|
|
418
|
+
}
|
|
419
|
+
```
|
|
622
420
|
|
|
623
|
-
|
|
421
|
+
### Dark Mode
|
|
624
422
|
|
|
625
|
-
|
|
423
|
+
Use `prefers-color-scheme` with CSS variable swapping:
|
|
626
424
|
|
|
627
|
-
|
|
425
|
+
```css
|
|
426
|
+
:root {
|
|
427
|
+
color-scheme: light dark;
|
|
628
428
|
|
|
629
|
-
|
|
429
|
+
--surface-1: var(--gray-0);
|
|
430
|
+
--surface-2: var(--gray-1);
|
|
431
|
+
--color-text: var(--gray-9);
|
|
432
|
+
}
|
|
630
433
|
|
|
631
|
-
|
|
434
|
+
@media (prefers-color-scheme: dark) {
|
|
435
|
+
:root {
|
|
436
|
+
--surface-1: var(--gray-11);
|
|
437
|
+
--surface-2: var(--gray-10);
|
|
438
|
+
--color-text: var(--gray-1);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
632
441
|
```
|
|
633
442
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
```html
|
|
637
|
-
<!DOCTYPE html>
|
|
638
|
-
<html>
|
|
639
|
-
<head><title>My App</title></head>
|
|
640
|
-
<body>
|
|
641
|
-
<div id="app"></div>
|
|
642
|
-
<script type="module">
|
|
643
|
-
import { compileToJS } from '/rip-ui/compiler.js';
|
|
644
|
-
import { createApp } from '/rip-ui/ui.js';
|
|
645
|
-
|
|
646
|
-
const app = createApp({
|
|
647
|
-
target: '#app',
|
|
648
|
-
compile: compileToJS,
|
|
649
|
-
state: { theme: 'light' }
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
await app.loadBundle('/rip-ui/manifest.json');
|
|
653
|
-
app.start();
|
|
654
|
-
app.watch('/rip-ui/watch');
|
|
655
|
-
</script>
|
|
656
|
-
</body>
|
|
657
|
-
</html>
|
|
658
|
-
```
|
|
443
|
+
For a manual toggle, use a `[data-theme]` attribute on the root element:
|
|
659
444
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
For static deployments or quick prototyping, you can inline components directly:
|
|
667
|
-
|
|
668
|
-
```html
|
|
669
|
-
<script type="module">
|
|
670
|
-
import { compileToJS } from './rip.browser.js'
|
|
671
|
-
import { createApp } from './ui.js'
|
|
672
|
-
|
|
673
|
-
createApp({
|
|
674
|
-
target: '#app',
|
|
675
|
-
compile: compileToJS,
|
|
676
|
-
files: {
|
|
677
|
-
'pages/index.rip': `
|
|
678
|
-
Home = component
|
|
679
|
-
render
|
|
680
|
-
h1 "Hello, World"
|
|
681
|
-
p "This was compiled in your browser."
|
|
682
|
-
`
|
|
683
|
-
}
|
|
684
|
-
}).start()
|
|
685
|
-
</script>
|
|
445
|
+
```css
|
|
446
|
+
[data-theme="dark"] {
|
|
447
|
+
--surface-1: var(--gray-11);
|
|
448
|
+
--surface-2: var(--gray-10);
|
|
449
|
+
--color-text: var(--gray-1);
|
|
450
|
+
}
|
|
686
451
|
```
|
|
687
452
|
|
|
688
|
-
|
|
689
|
-
|
|
453
|
+
```js
|
|
454
|
+
document.documentElement.dataset.theme = 'dark'
|
|
690
455
|
```
|
|
691
|
-
my-app/
|
|
692
|
-
├── index.rip # Server (uses @rip-lang/api + ripUI middleware)
|
|
693
|
-
├── index.html # HTML shell
|
|
694
|
-
├── pages/
|
|
695
|
-
│ ├── _layout.rip # Root layout (nav, footer)
|
|
696
|
-
│ ├── index.rip # Home page → /
|
|
697
|
-
│ ├── about.rip # About page → /about
|
|
698
|
-
│ └── users/
|
|
699
|
-
│ ├── _layout.rip # Users layout → wraps /users/*
|
|
700
|
-
│ ├── index.rip # User list → /users
|
|
701
|
-
│ └── [id].rip # User profile → /users/:id
|
|
702
|
-
└── css/
|
|
703
|
-
└── styles.css # Tailwind or plain CSS
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
> **Note:** Framework files (`ui.js`, `stash.js`, `router.js`, etc.) are served
|
|
707
|
-
> automatically by the `ripUI` middleware from the installed `@rip-lang/ui`
|
|
708
|
-
> package — you don't need to copy them into your project.
|
|
709
|
-
|
|
710
|
-
## Render Template Syntax
|
|
711
|
-
|
|
712
|
-
The `render` block uses a concise, indentation-based template syntax:
|
|
713
456
|
|
|
714
|
-
###
|
|
715
|
-
|
|
716
|
-
```coffee
|
|
717
|
-
render
|
|
718
|
-
div # <div></div>
|
|
719
|
-
div.card # <div class="card"></div>
|
|
720
|
-
div.card.active # <div class="card active"></div>
|
|
721
|
-
button.btn.btn-primary # <button class="btn btn-primary"></button>
|
|
722
|
-
```
|
|
457
|
+
### Common Patterns
|
|
723
458
|
|
|
724
|
-
|
|
459
|
+
**Button:**
|
|
725
460
|
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
461
|
+
```css
|
|
462
|
+
.button {
|
|
463
|
+
display: inline-flex;
|
|
464
|
+
align-items: center;
|
|
465
|
+
gap: var(--size-2);
|
|
466
|
+
padding: var(--size-2) var(--size-4);
|
|
467
|
+
border: 1px solid var(--color-primary);
|
|
468
|
+
border-radius: var(--radius-2);
|
|
469
|
+
background: var(--color-primary);
|
|
470
|
+
color: white;
|
|
471
|
+
font-weight: var(--font-weight-6);
|
|
472
|
+
cursor: pointer;
|
|
473
|
+
transition: background 150ms var(--ease-2);
|
|
732
474
|
|
|
733
|
-
|
|
734
|
-
|
|
475
|
+
&:hover { background: color-mix(in oklch, var(--color-primary), black 15%); }
|
|
476
|
+
&:active { scale: 0.98; }
|
|
477
|
+
&[data-disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
478
|
+
}
|
|
735
479
|
|
|
736
|
-
|
|
480
|
+
.ghost {
|
|
481
|
+
background: transparent;
|
|
482
|
+
color: var(--color-primary);
|
|
737
483
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
input type: "text", placeholder: "Search..."
|
|
741
|
-
button @click: @handleClick, "Submit"
|
|
742
|
-
a href: "/about", "About Us"
|
|
743
|
-
img src: @imageUrl, alt: "Photo"
|
|
484
|
+
&:hover { background: color-mix(in oklch, var(--color-primary), transparent 90%); }
|
|
485
|
+
}
|
|
744
486
|
```
|
|
745
487
|
|
|
746
|
-
|
|
488
|
+
**Form Input:**
|
|
747
489
|
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
490
|
+
```css
|
|
491
|
+
.input {
|
|
492
|
+
padding: var(--size-2) var(--size-3);
|
|
493
|
+
border: 1px solid var(--gray-4);
|
|
494
|
+
border-radius: var(--radius-2);
|
|
495
|
+
font-size: var(--font-size-1);
|
|
496
|
+
background: var(--surface-1);
|
|
497
|
+
color: var(--color-text);
|
|
498
|
+
transition: border-color 150ms var(--ease-2);
|
|
754
499
|
|
|
755
|
-
|
|
500
|
+
&:focus {
|
|
501
|
+
outline: 2px solid var(--color-primary);
|
|
502
|
+
outline-offset: 1px;
|
|
503
|
+
border-color: var(--color-primary);
|
|
504
|
+
}
|
|
756
505
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
else
|
|
762
|
-
p "Please log in"
|
|
506
|
+
&[data-invalid] { border-color: var(--color-danger); }
|
|
507
|
+
&[data-disabled] { opacity: 0.5; }
|
|
508
|
+
&::placeholder { color: var(--color-text-muted); }
|
|
509
|
+
}
|
|
763
510
|
```
|
|
764
511
|
|
|
765
|
-
|
|
512
|
+
**Card:**
|
|
766
513
|
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
514
|
+
```css
|
|
515
|
+
.card {
|
|
516
|
+
background: var(--surface-1);
|
|
517
|
+
border-radius: var(--radius-3);
|
|
518
|
+
padding: var(--size-5);
|
|
519
|
+
box-shadow: var(--shadow-2);
|
|
520
|
+
transition: box-shadow 200ms var(--ease-2);
|
|
773
521
|
|
|
774
|
-
|
|
522
|
+
&:hover { box-shadow: var(--shadow-3); }
|
|
775
523
|
|
|
776
|
-
|
|
524
|
+
& .title {
|
|
525
|
+
font-size: var(--font-size-3);
|
|
526
|
+
font-weight: var(--font-weight-7);
|
|
527
|
+
margin-block-end: var(--size-2);
|
|
528
|
+
}
|
|
777
529
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
nav
|
|
784
|
-
a href: "/", "Home"
|
|
785
|
-
a href: "/about", "About"
|
|
786
|
-
main.app-body
|
|
787
|
-
p "Content goes here"
|
|
788
|
-
footer
|
|
789
|
-
p "Footer"
|
|
530
|
+
& .body {
|
|
531
|
+
color: var(--color-text-muted);
|
|
532
|
+
line-height: var(--font-lineheight-3);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
790
535
|
```
|
|
791
536
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
### `createApp(options)`
|
|
795
|
-
|
|
796
|
-
| Option | Type | Default | Description |
|
|
797
|
-
|--------|------|---------|-------------|
|
|
798
|
-
| `target` | `string\|Element` | `'#app'` | DOM mount target |
|
|
799
|
-
| `state` | `object` | `{}` | Initial app state (becomes reactive stash) |
|
|
800
|
-
| `files` | `object` | `{}` | Initial VFS files `{ path: content }` |
|
|
801
|
-
| `root` | `string` | `'pages'` | Pages directory in VFS |
|
|
802
|
-
| `compile` | `function` | — | Rip compiler (`compileToJS`) |
|
|
803
|
-
| `transition` | `object` | — | Route transition `{ duration }` |
|
|
804
|
-
| `onError` | `function` | — | Error handler |
|
|
805
|
-
| `onNavigate` | `function` | — | Navigation callback |
|
|
806
|
-
|
|
807
|
-
Returns an instance with these methods:
|
|
537
|
+
**Dialog:**
|
|
808
538
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
| `go(path)` | Navigate to a route |
|
|
817
|
-
| `addPage(path, source)` | Add a page to the VFS |
|
|
818
|
-
| `get(key)` | Get app state value |
|
|
819
|
-
| `set(key, value)` | Set app state value |
|
|
539
|
+
```css
|
|
540
|
+
.backdrop {
|
|
541
|
+
position: fixed;
|
|
542
|
+
inset: 0;
|
|
543
|
+
background: oklch(0% 0 0 / 40%);
|
|
544
|
+
display: grid;
|
|
545
|
+
place-items: center;
|
|
820
546
|
|
|
821
|
-
|
|
547
|
+
&[data-open] { animation: fade-in 150ms var(--ease-2); }
|
|
548
|
+
}
|
|
822
549
|
|
|
823
|
-
|
|
550
|
+
.panel {
|
|
551
|
+
background: var(--surface-1);
|
|
552
|
+
border-radius: var(--radius-3);
|
|
553
|
+
padding: var(--size-6);
|
|
554
|
+
box-shadow: var(--shadow-4);
|
|
555
|
+
max-width: min(90vw, 32rem);
|
|
556
|
+
width: 100%;
|
|
557
|
+
animation: slide-in-up 200ms var(--ease-spring-3);
|
|
558
|
+
}
|
|
824
559
|
|
|
825
|
-
|
|
826
|
-
|
|
560
|
+
.panel .title {
|
|
561
|
+
font-size: var(--font-size-4);
|
|
562
|
+
font-weight: var(--font-weight-7);
|
|
563
|
+
margin-block-end: var(--size-2);
|
|
564
|
+
}
|
|
565
|
+
```
|
|
827
566
|
|
|
828
|
-
|
|
567
|
+
**Select:**
|
|
829
568
|
|
|
830
|
-
|
|
569
|
+
```css
|
|
570
|
+
.trigger {
|
|
571
|
+
display: inline-flex;
|
|
572
|
+
align-items: center;
|
|
573
|
+
justify-content: space-between;
|
|
574
|
+
gap: var(--size-2);
|
|
575
|
+
padding: var(--size-2) var(--size-3);
|
|
576
|
+
border: 1px solid var(--gray-4);
|
|
577
|
+
border-radius: var(--radius-2);
|
|
578
|
+
background: var(--surface-1);
|
|
579
|
+
cursor: pointer;
|
|
580
|
+
min-width: 10rem;
|
|
831
581
|
|
|
832
|
-
|
|
582
|
+
&[data-open] { border-color: var(--color-primary); }
|
|
583
|
+
}
|
|
833
584
|
|
|
834
|
-
|
|
585
|
+
.popup {
|
|
586
|
+
background: var(--surface-1);
|
|
587
|
+
border: 1px solid var(--gray-3);
|
|
588
|
+
border-radius: var(--radius-2);
|
|
589
|
+
box-shadow: var(--shadow-3);
|
|
590
|
+
padding: var(--size-1);
|
|
591
|
+
}
|
|
835
592
|
|
|
836
|
-
|
|
593
|
+
.option {
|
|
594
|
+
padding: var(--size-2) var(--size-3);
|
|
595
|
+
border-radius: var(--radius-1);
|
|
596
|
+
cursor: pointer;
|
|
837
597
|
|
|
838
|
-
|
|
598
|
+
&[data-highlighted] { background: var(--surface-2); }
|
|
599
|
+
&[data-selected] { font-weight: var(--font-weight-6); color: var(--color-primary); }
|
|
600
|
+
}
|
|
601
|
+
```
|
|
839
602
|
|
|
840
|
-
|
|
603
|
+
**Tooltip:**
|
|
841
604
|
|
|
842
|
-
|
|
843
|
-
|
|
605
|
+
```css
|
|
606
|
+
.tooltip {
|
|
607
|
+
background: var(--gray-10);
|
|
608
|
+
color: var(--gray-0);
|
|
609
|
+
font-size: var(--font-size-0);
|
|
610
|
+
padding: var(--size-1) var(--size-2);
|
|
611
|
+
border-radius: var(--radius-2);
|
|
612
|
+
max-width: 20rem;
|
|
844
613
|
|
|
845
|
-
|
|
846
|
-
|
|
614
|
+
&[data-entering] { animation: fade-in 100ms var(--ease-2); }
|
|
615
|
+
&[data-exiting] { animation: fade-out 75ms var(--ease-2); }
|
|
616
|
+
}
|
|
617
|
+
```
|
|
847
618
|
|
|
848
|
-
|
|
849
|
-
direct effect that updates exactly the DOM nodes it touches.
|
|
619
|
+
### What We Don't Use
|
|
850
620
|
|
|
851
|
-
**
|
|
852
|
-
|
|
853
|
-
composition functions, no magic — just a class with a `render` method.
|
|
621
|
+
**React or any framework runtime** — Rip widgets are written in Rip, compiled
|
|
622
|
+
to JavaScript, with zero runtime dependencies.
|
|
854
623
|
|
|
855
|
-
**
|
|
856
|
-
|
|
624
|
+
**Tailwind CSS** — utility classes in markup are write-only and semantically
|
|
625
|
+
empty. We write real CSS with real selectors.
|
|
857
626
|
|
|
858
|
-
**
|
|
859
|
-
|
|
860
|
-
API.
|
|
627
|
+
**CSS-in-JS runtimes** (styled-components, Emotion) — runtime style injection
|
|
628
|
+
adds bundle size and creates hydration complexity.
|
|
861
629
|
|
|
862
|
-
|
|
630
|
+
**Sass / Less** — native CSS nesting, `color-mix()`, and custom properties
|
|
631
|
+
eliminate the need for preprocessors.
|
|
863
632
|
|
|
864
|
-
|
|
633
|
+
**Inline styles for layout** — the `style` prop is for truly dynamic values
|
|
634
|
+
(e.g., positioning from a calculation). Layout, spacing, color, and typography
|
|
635
|
+
go in CSS.
|
|
865
636
|
|
|
866
|
-
|
|
637
|
+
**Third-party headless libraries** (Base UI, Radix, Headless UI, Zag.js) —
|
|
638
|
+
we implement the same WAI-ARIA patterns natively in Rip. The patterns are
|
|
639
|
+
standard; the implementation is ours.
|
|
867
640
|
|
|
868
|
-
|
|
869
|
-
- `@rip-lang/api` ≥ 1.1.4 (for `@send` file serving used by `ripUI` middleware)
|
|
870
|
-
- `rip-lang` ≥ 3.1.1 (Rip compiler with browser build)
|
|
641
|
+
---
|
|
871
642
|
|
|
872
|
-
##
|
|
643
|
+
## File Summary
|
|
873
644
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
645
|
+
| File | Lines | Description |
|
|
646
|
+
|------|-------|-------------|
|
|
647
|
+
| `select.rip` | 182 | Dropdown select with typeahead |
|
|
648
|
+
| `combobox.rip` | 152 | Filterable input + listbox |
|
|
649
|
+
| `dialog.rip` | 93 | Modal with focus trap and scroll lock |
|
|
650
|
+
| `toast.rip` | 44 | Auto-dismiss notification |
|
|
651
|
+
| `popover.rip` | 116 | Anchored floating content |
|
|
652
|
+
| `tooltip.rip` | 99 | Hover/focus tooltip |
|
|
653
|
+
| `tabs.rip` | 92 | Tab panel with roving tabindex |
|
|
654
|
+
| `accordion.rip` | 92 | Expand/collapse sections |
|
|
655
|
+
| `checkbox.rip` | 33 | Checkbox and switch toggle |
|
|
656
|
+
| `menu.rip` | 132 | Dropdown action menu |
|
|
657
|
+
| `grid.rip` | 901 | Virtual-scrolling data grid with clipboard |
|
|
658
|
+
| **Total** | **1,936** | |
|