@rip-lang/ui 0.1.3 → 0.3.2
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 +209 -770
- package/package.json +7 -18
- package/serve.rip +98 -79
- package/ui.rip +964 -0
- package/renderer.js +0 -397
- package/router.js +0 -325
- package/stash.js +0 -413
- package/ui.js +0 -208
- package/vfs.js +0 -215
package/README.md
CHANGED
|
@@ -1,877 +1,316 @@
|
|
|
1
|
-
<img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.
|
|
1
|
+
<img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.png" style="width:50px" /> <br>
|
|
2
2
|
|
|
3
3
|
# Rip UI - @rip-lang/ui
|
|
4
4
|
|
|
5
|
-
> **
|
|
5
|
+
> **Zero-build reactive web framework for the Rip language.**
|
|
6
6
|
|
|
7
|
-
Rip
|
|
8
|
-
|
|
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.
|
|
7
|
+
Load the Rip compiler in the browser. Write inline Rip. Launch your app.
|
|
8
|
+
No build step, no bundler, no configuration.
|
|
13
9
|
|
|
14
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
```coffee
|
|
106
|
-
Counter = component
|
|
107
|
-
@count := 0 # reactive state (signal) — parent can override via props
|
|
108
|
-
@step = 1 # plain prop — parent can set, not reactive
|
|
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}"
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
This compiles to an anonymous ES6 class expression:
|
|
127
|
-
|
|
128
|
-
```javascript
|
|
129
|
-
Counter = class {
|
|
130
|
-
constructor(props = {}) {
|
|
131
|
-
this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
|
|
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
|
-
```
|
|
144
|
-
|
|
145
|
-
### The Two Keywords
|
|
146
|
-
|
|
147
|
-
**`component`** — Declares an anonymous class with component semantics:
|
|
148
|
-
|
|
149
|
-
- `@` properties become instance variables that the parent can set via props
|
|
150
|
-
- `:=` assignments create reactive signals (`__state`)
|
|
151
|
-
- `~=` assignments create computed values (`__computed`)
|
|
152
|
-
- `~>` assignments create effects (`__effect`)
|
|
153
|
-
- Plain `=` assignments with function values become methods
|
|
154
|
-
- `mounted`, `unmounted`, `updated` are lifecycle hooks called by the runtime
|
|
155
|
-
- Everything else is standard Rip class behavior
|
|
156
|
-
|
|
157
|
-
**`render`** — Defines the component's template using a Pug/Jade-like DSL:
|
|
158
|
-
|
|
159
|
-
- Tags are bare identifiers: `div`, `h1`, `button`, `span`
|
|
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
|
|
168
|
-
|
|
169
|
-
That's it. No special attribute syntax, no directive system, no template
|
|
170
|
-
compiler — just Rip's existing syntax applied to DOM construction.
|
|
171
|
-
|
|
172
|
-
### Props
|
|
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:
|
|
177
|
-
|
|
178
|
-
```coffee
|
|
179
|
-
# Parent passes plain value — child wraps it in a signal
|
|
180
|
-
Counter {count: 10}
|
|
181
|
-
|
|
182
|
-
# Parent passes its own signal — child uses it directly (shared state)
|
|
183
|
-
Counter {count: parentCount}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
The `isSignal` check in the constructor handles this automatically:
|
|
187
|
-
|
|
188
|
-
```javascript
|
|
189
|
-
this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
If you don't want the parent to override a prop, don't use `@`:
|
|
193
|
-
|
|
194
|
-
```coffee
|
|
195
|
-
MyComponent = component
|
|
196
|
-
active := false # internal state — not a prop, parent can't set it
|
|
197
|
-
@count := 0 # prop — parent can set or share a signal
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Required and Optional Props
|
|
201
|
-
|
|
202
|
-
```coffee
|
|
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
|
-
```
|
|
208
|
-
|
|
209
|
-
### Computed Values and Effects
|
|
210
|
-
|
|
211
|
-
```coffee
|
|
212
|
-
TodoList = component
|
|
213
|
-
@todos := []
|
|
214
|
-
remaining ~= @todos.filter((t) -> not t.done).length
|
|
215
|
-
total ~= @todos.length
|
|
216
|
-
|
|
217
|
-
~> console.log "#{remaining} of #{total} remaining"
|
|
218
|
-
|
|
219
|
-
render
|
|
220
|
-
div
|
|
221
|
-
h2 "Todos (#{remaining}/#{total})"
|
|
222
|
-
# ...
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Lifecycle
|
|
10
|
+
## Quick Start
|
|
226
11
|
|
|
227
|
-
|
|
228
|
-
runtime calls them at the appropriate times:
|
|
12
|
+
**`index.rip`** — the server:
|
|
229
13
|
|
|
230
14
|
```coffee
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
@interval = null
|
|
234
|
-
|
|
235
|
-
mounted: ->
|
|
236
|
-
@interval = setInterval (=> @elapsed += 1), 1000
|
|
237
|
-
|
|
238
|
-
unmounted: ->
|
|
239
|
-
clearInterval @interval
|
|
15
|
+
import { get, use, start, notFound } from '@rip-lang/api'
|
|
16
|
+
import { ripUI } from '@rip-lang/ui/serve'
|
|
240
17
|
|
|
241
|
-
|
|
242
|
-
|
|
18
|
+
dir = import.meta.dir
|
|
19
|
+
use ripUI dir: dir, components: 'pages', 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
|
|
243
23
|
```
|
|
244
24
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
The `<=>` operator creates two-way bindings between form elements and reactive
|
|
248
|
-
state:
|
|
249
|
-
|
|
250
|
-
```coffee
|
|
251
|
-
SearchBox = component
|
|
252
|
-
@query := ""
|
|
25
|
+
**`index.html`** — the page:
|
|
253
26
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
27
|
+
```html
|
|
28
|
+
<script type="module" src="/rip/rip-ui.min.js"></script>
|
|
29
|
+
<script type="text/rip">
|
|
30
|
+
{ launch } = importRip! 'ui.rip'
|
|
31
|
+
launch()
|
|
32
|
+
</script>
|
|
257
33
|
```
|
|
258
34
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Components can nest. Props are passed as object arguments:
|
|
35
|
+
**`pages/index.rip`** — a page component:
|
|
262
36
|
|
|
263
37
|
```coffee
|
|
264
|
-
|
|
265
|
-
@
|
|
266
|
-
|
|
38
|
+
export Home = component
|
|
39
|
+
@count := 0
|
|
267
40
|
render
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
Footer
|
|
41
|
+
.
|
|
42
|
+
h1 "Hello from Rip UI"
|
|
43
|
+
button @click: (-> @count += 1), "Clicked #{@count} times"
|
|
272
44
|
```
|
|
273
45
|
|
|
274
|
-
|
|
46
|
+
Run `bun index.rip`, open `http://localhost:3000`.
|
|
275
47
|
|
|
276
|
-
|
|
48
|
+
## Component Composition
|
|
277
49
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
# In any descendant component:
|
|
283
|
-
theme = getContext 'theme'
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Multiple Components Per File
|
|
287
|
-
|
|
288
|
-
Because `component` is an expression (not a declaration), multiple components
|
|
289
|
-
can live in one file:
|
|
50
|
+
Page components in `pages/` map to routes via file-based routing. Shared
|
|
51
|
+
components in `ui/` (or any `includes` directory) are available by PascalCase
|
|
52
|
+
name. No imports needed:
|
|
290
53
|
|
|
291
54
|
```coffee
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
55
|
+
# ui/card.rip
|
|
56
|
+
export Card = component
|
|
57
|
+
title =! ""
|
|
295
58
|
render
|
|
296
|
-
|
|
59
|
+
.card
|
|
60
|
+
if title
|
|
61
|
+
h3 "#{title}"
|
|
62
|
+
@children
|
|
297
63
|
|
|
298
|
-
|
|
299
|
-
|
|
64
|
+
# pages/about.rip
|
|
65
|
+
export About = component
|
|
300
66
|
render
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
render
|
|
308
|
-
div
|
|
309
|
-
Card {title: "Welcome"}
|
|
310
|
-
Button {label: "Get Started", onClick: -> alert "Go!"}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
## Virtual File System
|
|
314
|
-
|
|
315
|
-
The VFS is a browser-local file storage layer. Components are delivered as
|
|
316
|
-
source text and stored in memory. The compiler reads from the VFS, not from
|
|
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', '...')
|
|
327
|
-
|
|
328
|
-
// Read
|
|
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
|
|
333
|
-
|
|
334
|
-
// Watch for changes (triggers recompilation)
|
|
335
|
-
fs.watch('pages/', ({ event, path }) => {
|
|
336
|
-
console.log(`${event}: ${path}`)
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
// Fetch from server
|
|
340
|
-
await fs.fetch('pages/index.rip', '/api/pages/index.rip')
|
|
341
|
-
await fs.fetchManifest([
|
|
342
|
-
'pages/index.rip',
|
|
343
|
-
'pages/about.rip',
|
|
344
|
-
'pages/counter.rip'
|
|
345
|
-
])
|
|
67
|
+
.
|
|
68
|
+
h1 "About"
|
|
69
|
+
Card title: "The Idea"
|
|
70
|
+
p "Components compose naturally."
|
|
71
|
+
Card title: "Architecture"
|
|
72
|
+
p "PascalCase resolution, signal passthrough, children blocks."
|
|
346
73
|
```
|
|
347
74
|
|
|
348
|
-
|
|
75
|
+
Reactive props via `:=` signal passthrough. Readonly props via `=!`.
|
|
76
|
+
Children blocks passed as DOM nodes via `@children`.
|
|
349
77
|
|
|
350
|
-
|
|
351
|
-
and produce static bundles. Rip UI has no build step, so it needs somewhere to
|
|
352
|
-
store source files in the browser. The VFS provides:
|
|
78
|
+
## How It Works
|
|
353
79
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
80
|
+
The browser loads one file — `rip-ui.min.js` (~52KB Brotli) — which bundles the
|
|
81
|
+
Rip compiler and the pre-compiled UI framework. No runtime compilation of the
|
|
82
|
+
framework, no extra network requests.
|
|
359
83
|
|
|
360
|
-
|
|
361
|
-
simple, ephemeral. For persistence, the server delivers files on page load.
|
|
84
|
+
Then `launch()` fetches the app bundle, hydrates the stash, and renders.
|
|
362
85
|
|
|
363
|
-
|
|
86
|
+
### Browser Execution Contexts
|
|
364
87
|
|
|
365
|
-
|
|
88
|
+
Rip provides full async/await support across every browser context — no other
|
|
89
|
+
compile-to-JS language has this:
|
|
366
90
|
|
|
367
|
-
|
|
368
|
-
|
|
91
|
+
| Context | How async works | Returns value? |
|
|
92
|
+
|---------|-----------------|----------------|
|
|
93
|
+
| `<script type="text/rip">` | Async IIFE wrapper | No (fire-and-forget) |
|
|
94
|
+
| Playground "Run" button | Async IIFE wrapper | No (use console.log) |
|
|
95
|
+
| `rip()` console REPL | Rip `do ->` block | Yes (sync direct, async via Promise) |
|
|
96
|
+
| `.rip` files via `importRip()` | ES module import | Yes (module exports) |
|
|
369
97
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
| VFS Path | URL Pattern | Example |
|
|
374
|
-
|----------|-------------|---------|
|
|
375
|
-
| `pages/index.rip` | `/` | Home page |
|
|
376
|
-
| `pages/about.rip` | `/about` | Static page |
|
|
377
|
-
| `pages/users/index.rip` | `/users` | User list |
|
|
378
|
-
| `pages/users/[id].rip` | `/users/:id` | Dynamic segment |
|
|
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
|
-
})
|
|
394
|
-
```
|
|
98
|
+
The `!` postfix compiles to `await`. Inline scripts are wrapped in an async IIFE
|
|
99
|
+
automatically. The `rip()` console function wraps user code in a `do ->` block
|
|
100
|
+
so the Rip compiler handles implicit return and auto-async natively.
|
|
395
101
|
|
|
396
|
-
|
|
397
|
-
links and modified clicks (ctrl+click, etc.) pass through normally.
|
|
102
|
+
### globalThis Exports
|
|
398
103
|
|
|
399
|
-
|
|
104
|
+
When `rip-ui.min.js` loads, it registers these on `globalThis`:
|
|
400
105
|
|
|
401
|
-
|
|
402
|
-
|
|
106
|
+
| Function | Purpose |
|
|
107
|
+
|----------|---------|
|
|
108
|
+
| `rip(code)` | Console REPL — compile and execute Rip code |
|
|
109
|
+
| `importRip(url)` | Fetch, compile, and import a `.rip` file as an ES module |
|
|
110
|
+
| `compileToJS(code)` | Compile Rip source to JavaScript |
|
|
111
|
+
| `__rip` | Reactive runtime — `__state`, `__computed`, `__effect`, `__batch` |
|
|
112
|
+
| `__ripComponent` | Component runtime — `__Component`, `__clsx`, `__fragment` |
|
|
113
|
+
| `__ripExports` | All compiler exports — `compile`, `formatSExpr`, `VERSION`, etc. |
|
|
403
114
|
|
|
404
|
-
|
|
405
|
-
import { stash, effect } from '@rip-lang/ui/stash'
|
|
115
|
+
## The Stash
|
|
406
116
|
|
|
407
|
-
|
|
408
|
-
user: { name: 'Alice', prefs: { theme: 'dark' } },
|
|
409
|
-
cart: { items: [], total: 0 }
|
|
410
|
-
})
|
|
117
|
+
App state lives in one reactive tree:
|
|
411
118
|
|
|
412
|
-
// Direct property access (tracked)
|
|
413
|
-
app.user.name // 'Alice'
|
|
414
|
-
app.user.prefs.theme = 'light' // triggers updates
|
|
415
|
-
|
|
416
|
-
// Path-based access
|
|
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']
|
|
424
|
-
|
|
425
|
-
// Reactive effects
|
|
426
|
-
effect(() => {
|
|
427
|
-
console.log(`Theme: ${app.user.prefs.theme}`)
|
|
428
|
-
// Re-runs whenever theme changes
|
|
429
|
-
})
|
|
430
119
|
```
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
Three levels of reactive state, each scoped appropriately:
|
|
435
|
-
|
|
436
|
-
| Tier | Scope | Lifetime | Access |
|
|
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
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
### Compilation Cache
|
|
478
|
-
|
|
479
|
-
Compiled components are cached by VFS path. A file is only recompiled when it
|
|
480
|
-
changes. The VFS watcher triggers cache invalidation, so updating a file in the
|
|
481
|
-
VFS automatically causes the next render to use the new version.
|
|
482
|
-
|
|
483
|
-
## Server Integration
|
|
484
|
-
|
|
485
|
-
### `ripUI` Middleware
|
|
486
|
-
|
|
487
|
-
The `ripUI` export from `@rip-lang/ui/serve` is a setup function that registers
|
|
488
|
-
routes for serving framework files, auto-generated page manifests, and an SSE
|
|
489
|
-
hot-reload channel. It works with `@rip-lang/api`'s `use()`:
|
|
490
|
-
|
|
491
|
-
```coffee
|
|
492
|
-
import { use } from '@rip-lang/api'
|
|
493
|
-
import { ripUI } from '@rip-lang/ui/serve'
|
|
494
|
-
|
|
495
|
-
use ripUI pages: 'pages', watch: true
|
|
120
|
+
app
|
|
121
|
+
├── routes ← navigation state (path, params, query, hash)
|
|
122
|
+
└── data ← reactive app state (title, theme, user, etc.)
|
|
496
123
|
```
|
|
497
124
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|-------|-------------|
|
|
502
|
-
| `GET /rip-ui/ui.js` | Framework entry point |
|
|
503
|
-
| `GET /rip-ui/stash.js` | Reactive state module |
|
|
504
|
-
| `GET /rip-ui/vfs.js` | Virtual File System |
|
|
505
|
-
| `GET /rip-ui/router.js` | File-based router |
|
|
506
|
-
| `GET /rip-ui/renderer.js` | Component renderer |
|
|
507
|
-
| `GET /rip-ui/compiler.js` | Rip compiler (40KB) |
|
|
508
|
-
| `GET /rip-ui/manifest.json` | Auto-generated manifest of all `.rip` pages |
|
|
509
|
-
| `GET /rip-ui/watch` | SSE hot-reload endpoint (when `watch: true`) |
|
|
510
|
-
| `GET /pages/*` | Individual `.rip` page files (for hot-reload refetch) |
|
|
125
|
+
Writing to `app.data.theme` updates any component reading it. The stash
|
|
126
|
+
uses Rip's built-in reactive primitives — the same signals that power
|
|
127
|
+
`:=` and `~=` in components.
|
|
511
128
|
|
|
512
|
-
|
|
129
|
+
## The App Bundle
|
|
513
130
|
|
|
514
|
-
|
|
515
|
-
|--------|------|---------|-------------|
|
|
516
|
-
| `pages` | `string` | `'pages'` | Directory containing `.rip` page files |
|
|
517
|
-
| `base` | `string` | `'/rip-ui'` | URL prefix for framework files |
|
|
518
|
-
| `watch` | `boolean` | `false` | Enable SSE hot-reload endpoint |
|
|
519
|
-
| `debounce` | `number` | `250` | Milliseconds to batch filesystem change events |
|
|
520
|
-
|
|
521
|
-
### Page Manifest
|
|
522
|
-
|
|
523
|
-
The manifest endpoint (`/rip-ui/manifest.json`) auto-discovers every `.rip` file
|
|
524
|
-
in the `pages` directory and bundles them into a single JSON response:
|
|
131
|
+
The bundle is JSON served at `/{app}/bundle`:
|
|
525
132
|
|
|
526
133
|
```json
|
|
527
134
|
{
|
|
528
|
-
"
|
|
529
|
-
|
|
530
|
-
|
|
135
|
+
"components": {
|
|
136
|
+
"components/index.rip": "export Home = component...",
|
|
137
|
+
"components/counter.rip": "export Counter = component...",
|
|
138
|
+
"components/_lib/card.rip": "export Card = component..."
|
|
139
|
+
},
|
|
140
|
+
"data": {
|
|
141
|
+
"title": "My App",
|
|
142
|
+
"theme": "light"
|
|
143
|
+
}
|
|
531
144
|
}
|
|
532
145
|
```
|
|
533
146
|
|
|
534
|
-
|
|
535
|
-
|
|
147
|
+
On disk you organize your app into `pages/` and `ui/`. The middleware
|
|
148
|
+
maps them into a flat `components/` namespace in the bundle — pages go
|
|
149
|
+
under `components/`, shared components under `components/_lib/`. The `_`
|
|
150
|
+
prefix tells the router to skip `_lib/` entries when generating routes.
|
|
536
151
|
|
|
537
|
-
##
|
|
152
|
+
## Server Middleware
|
|
538
153
|
|
|
539
|
-
The
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
### How It Works
|
|
543
|
-
|
|
544
|
-
```
|
|
545
|
-
Developer saves a .rip file
|
|
546
|
-
│
|
|
547
|
-
▼
|
|
548
|
-
Server (fs.watch)
|
|
549
|
-
├── Debounce (250ms, batches rapid saves)
|
|
550
|
-
└── SSE "changed" event: { paths: ["pages/counter.rip"] }
|
|
551
|
-
│
|
|
552
|
-
▼
|
|
553
|
-
Browser (EventSource)
|
|
554
|
-
├── Invalidate VFS entries (fs.delete)
|
|
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)
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### Server Side
|
|
565
|
-
|
|
566
|
-
The `watch` option enables filesystem monitoring with debounced SSE
|
|
567
|
-
notifications. A heartbeat every 5 seconds keeps the connection alive:
|
|
154
|
+
The `ripUI` middleware registers routes for the framework files, the app
|
|
155
|
+
bundle, and optional SSE hot-reload:
|
|
568
156
|
|
|
569
157
|
```coffee
|
|
570
|
-
use ripUI
|
|
158
|
+
use ripUI dir: dir, components: 'pages', includes: ['ui'], watch: true, title: 'My App'
|
|
571
159
|
```
|
|
572
160
|
|
|
573
|
-
|
|
161
|
+
| Option | Default | Description |
|
|
162
|
+
|--------|---------|-------------|
|
|
163
|
+
| `app` | `''` | URL mount point |
|
|
164
|
+
| `dir` | `'.'` | App directory on disk |
|
|
165
|
+
| `components` | `'components'` | Directory for page components (file-based routing) |
|
|
166
|
+
| `includes` | `[]` | Directories for shared components (no routes) |
|
|
167
|
+
| `watch` | `false` | Enable SSE hot-reload |
|
|
168
|
+
| `debounce` | `250` | Milliseconds to batch file change events |
|
|
169
|
+
| `state` | `null` | Initial app state |
|
|
170
|
+
| `title` | `null` | Document title |
|
|
574
171
|
|
|
575
|
-
|
|
172
|
+
Routes registered:
|
|
576
173
|
|
|
577
|
-
```javascript
|
|
578
|
-
app.watch('/rip-ui/watch');
|
|
579
174
|
```
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
```javascript
|
|
585
|
-
app.watch('/rip-ui/watch', {
|
|
586
|
-
eager: ['pages/_layout.rip']
|
|
587
|
-
});
|
|
175
|
+
/rip/rip-ui.min.js — Rip compiler + pre-compiled UI framework
|
|
176
|
+
/{app}/bundle — app bundle (components + data as JSON)
|
|
177
|
+
/{app}/watch — SSE hot-reload stream (when watch: true)
|
|
178
|
+
/{app}/components/* — individual component files (for hot-reload refetch)
|
|
588
179
|
```
|
|
589
180
|
|
|
590
|
-
|
|
181
|
+
## State Preservation (Keep-Alive)
|
|
591
182
|
|
|
592
|
-
|
|
593
|
-
|
|
183
|
+
Components are cached when navigating away instead of destroyed. Navigate
|
|
184
|
+
to `/counter`, increment the count, go to `/about`, come back — the count
|
|
185
|
+
is preserved. Configurable via `cacheSize` (default 10).
|
|
594
186
|
|
|
595
|
-
|
|
596
|
-
await app.loadBundle('/rip-ui/manifest.json');
|
|
597
|
-
```
|
|
187
|
+
## Data Loading
|
|
598
188
|
|
|
599
|
-
|
|
189
|
+
`createResource` manages async data with reactive `loading`, `error`, and
|
|
190
|
+
`data` properties:
|
|
600
191
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
192
|
+
```coffee
|
|
193
|
+
export UserPage = component
|
|
194
|
+
user := createResource -> fetch!("/api/users/#{@params.id}").json!
|
|
604
195
|
|
|
605
|
-
|
|
606
|
-
|
|
196
|
+
render
|
|
197
|
+
if user.loading
|
|
198
|
+
p "Loading..."
|
|
199
|
+
else if user.error
|
|
200
|
+
p "Error: #{user.error.message}"
|
|
201
|
+
else
|
|
202
|
+
h1 user.data.name
|
|
607
203
|
```
|
|
608
204
|
|
|
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:
|
|
205
|
+
## Error Boundaries
|
|
616
206
|
|
|
617
|
-
|
|
207
|
+
Layouts with an `onError` method catch errors from child components:
|
|
618
208
|
|
|
619
209
|
```coffee
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
dir = import.meta.dir
|
|
624
|
-
|
|
625
|
-
use ripUI pages: "#{dir}/pages", watch: true
|
|
626
|
-
|
|
627
|
-
get '/css/*', -> @send "#{dir}/css/#{@req.path.slice(5)}"
|
|
210
|
+
export Layout = component
|
|
211
|
+
errorMsg := null
|
|
628
212
|
|
|
629
|
-
|
|
213
|
+
onError: (err) -> errorMsg = err.message
|
|
630
214
|
|
|
631
|
-
|
|
215
|
+
render
|
|
216
|
+
.app-layout
|
|
217
|
+
if errorMsg
|
|
218
|
+
.error-banner "#{errorMsg}"
|
|
219
|
+
#content
|
|
632
220
|
```
|
|
633
221
|
|
|
634
|
-
|
|
222
|
+
## Navigation Indicator
|
|
635
223
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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>
|
|
224
|
+
`router.navigating` is a reactive signal — true while a route transition
|
|
225
|
+
is in progress:
|
|
226
|
+
|
|
227
|
+
```coffee
|
|
228
|
+
if @router.navigating
|
|
229
|
+
span "Loading..."
|
|
658
230
|
```
|
|
659
231
|
|
|
660
|
-
|
|
661
|
-
file in the `pages/` directory is auto-discovered, served as a manifest, and
|
|
662
|
-
hot-reloaded on save.
|
|
232
|
+
## Multi-App Hosting
|
|
663
233
|
|
|
664
|
-
|
|
234
|
+
Mount multiple apps under one server:
|
|
665
235
|
|
|
666
|
-
|
|
236
|
+
```coffee
|
|
237
|
+
import { get, start, notFound } from '@rip-lang/api'
|
|
238
|
+
import { mount as demo } from './demo/index.rip'
|
|
239
|
+
import { mount as labs } from './labs/index.rip'
|
|
667
240
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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>
|
|
241
|
+
demo '/demo'
|
|
242
|
+
labs '/labs'
|
|
243
|
+
get '/', -> Response.redirect('/demo/', 302)
|
|
244
|
+
start port: 3002
|
|
686
245
|
```
|
|
687
246
|
|
|
688
|
-
|
|
247
|
+
The `/rip/` namespace is shared — all apps use the same compiler and framework.
|
|
248
|
+
|
|
249
|
+
## File Structure
|
|
689
250
|
|
|
690
251
|
```
|
|
691
252
|
my-app/
|
|
692
|
-
├── index.rip
|
|
693
|
-
├── index.html
|
|
694
|
-
├── pages/
|
|
695
|
-
│ ├── _layout.rip
|
|
696
|
-
│ ├── index.rip
|
|
697
|
-
│ ├── about.rip
|
|
253
|
+
├── index.rip # Server
|
|
254
|
+
├── index.html # HTML page
|
|
255
|
+
├── pages/ # Page components (file-based routing)
|
|
256
|
+
│ ├── _layout.rip # Root layout
|
|
257
|
+
│ ├── index.rip # Home → /
|
|
258
|
+
│ ├── about.rip # About → /about
|
|
698
259
|
│ └── users/
|
|
699
|
-
│
|
|
700
|
-
|
|
701
|
-
│
|
|
260
|
+
│ └── [id].rip # User profile → /users/:id
|
|
261
|
+
├── ui/ # Shared components (no routes)
|
|
262
|
+
│ └── card.rip # Card → available as Card
|
|
702
263
|
└── css/
|
|
703
|
-
└── styles.css
|
|
264
|
+
└── styles.css # Styles
|
|
704
265
|
```
|
|
705
266
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
## Render Template Syntax
|
|
267
|
+
Files starting with `_` don't generate routes (`_layout.rip` is a layout,
|
|
268
|
+
not a page). Directories starting with `_` are also excluded, which is how
|
|
269
|
+
shared components from `includes` stay out of the router.
|
|
711
270
|
|
|
712
|
-
|
|
271
|
+
## Hash Routing
|
|
713
272
|
|
|
714
|
-
|
|
273
|
+
For static hosting (GitHub Pages, S3, etc.) where the server can't handle
|
|
274
|
+
SPA fallback routing, use hash-based URLs:
|
|
715
275
|
|
|
716
276
|
```coffee
|
|
717
|
-
|
|
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>
|
|
277
|
+
launch '/app', hash: true
|
|
722
278
|
```
|
|
723
279
|
|
|
724
|
-
|
|
280
|
+
This switches from `/about` to `page.html#/about`. Back/forward navigation,
|
|
281
|
+
direct URL loading, and `href="#/path"` links all work correctly.
|
|
725
282
|
|
|
726
|
-
|
|
727
|
-
render
|
|
728
|
-
div.("active" if @selected) # conditional class
|
|
729
|
-
div.("bg-red" if error, "bg-green" if ok) # multiple conditions
|
|
730
|
-
div.card.("highlighted" if @featured) # static + dynamic
|
|
731
|
-
```
|
|
283
|
+
## Static Deployment — `launch bundle:`
|
|
732
284
|
|
|
733
|
-
|
|
734
|
-
|
|
285
|
+
Inline all components in a single HTML file for zero-server deployment.
|
|
286
|
+
Use `rip-ui.min.js` (~52KB Brotli) — a combined bundle with the compiler
|
|
287
|
+
and pre-compiled UI framework. No extra network requests, no runtime
|
|
288
|
+
compilation of the framework:
|
|
735
289
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
render
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
### Conditionals
|
|
756
|
-
|
|
757
|
-
```coffee
|
|
758
|
-
render
|
|
759
|
-
if @loggedIn
|
|
760
|
-
p "Welcome back, #{@name}"
|
|
761
|
-
else
|
|
762
|
-
p "Please log in"
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
### Loops
|
|
766
|
-
|
|
767
|
-
```coffee
|
|
768
|
-
render
|
|
769
|
-
ul
|
|
770
|
-
for item in @items
|
|
771
|
-
li item.name
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
### Nesting
|
|
775
|
-
|
|
776
|
-
Indentation defines parent-child relationships:
|
|
777
|
-
|
|
778
|
-
```coffee
|
|
779
|
-
render
|
|
780
|
-
div.app
|
|
781
|
-
header.app-header
|
|
782
|
-
h1 "My App"
|
|
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"
|
|
290
|
+
```html
|
|
291
|
+
<script type="module" src="dist/rip-ui.min.js"></script>
|
|
292
|
+
<script type="text/rip">
|
|
293
|
+
{ launch } = importRip! 'ui.rip'
|
|
294
|
+
|
|
295
|
+
launch bundle:
|
|
296
|
+
'/': '''
|
|
297
|
+
export Home = component
|
|
298
|
+
render
|
|
299
|
+
h1 "Hello"
|
|
300
|
+
'''
|
|
301
|
+
'/about': '''
|
|
302
|
+
export About = component
|
|
303
|
+
render
|
|
304
|
+
h1 "About"
|
|
305
|
+
'''
|
|
306
|
+
, hash: true
|
|
307
|
+
</script>
|
|
790
308
|
```
|
|
791
309
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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:
|
|
808
|
-
|
|
809
|
-
| Method | Description |
|
|
810
|
-
|--------|-------------|
|
|
811
|
-
| `start()` | Start routing and rendering |
|
|
812
|
-
| `stop()` | Unmount components, close SSE connection, clean up |
|
|
813
|
-
| `load(paths)` | Fetch `.rip` files from server into VFS |
|
|
814
|
-
| `loadBundle(url)` | Fetch a JSON manifest and bulk-load all pages into VFS |
|
|
815
|
-
| `watch(url, opts?)` | Connect to SSE hot-reload endpoint |
|
|
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 |
|
|
820
|
-
|
|
821
|
-
Also exposes: `app` (stash), `fs` (VFS), `router`, `renderer`
|
|
822
|
-
|
|
823
|
-
### `stash(data)`
|
|
824
|
-
|
|
825
|
-
Creates a deeply reactive proxy around `data`. Every property read is tracked,
|
|
826
|
-
every write triggers effects.
|
|
827
|
-
|
|
828
|
-
### `effect(fn)`
|
|
829
|
-
|
|
830
|
-
Creates a side effect that re-runs whenever its tracked dependencies change.
|
|
831
|
-
|
|
832
|
-
### `computed(fn)`
|
|
833
|
-
|
|
834
|
-
Creates a lazy computed value that caches until dependencies change.
|
|
835
|
-
|
|
836
|
-
### `batch(fn)`
|
|
837
|
-
|
|
838
|
-
Groups multiple state updates — effects only fire once at the end.
|
|
839
|
-
|
|
840
|
-
## Design Principles
|
|
841
|
-
|
|
842
|
-
**No build step.** The compiler is small enough to ship. Source files are the
|
|
843
|
-
deployment artifact.
|
|
844
|
-
|
|
845
|
-
**Language-native reactivity.** `:=` for state, `~=` for computed, `~>` for
|
|
846
|
-
effects. These are Rip language features, not framework APIs.
|
|
847
|
-
|
|
848
|
-
**Fine-grained DOM updates.** No virtual DOM. Each reactive binding creates a
|
|
849
|
-
direct effect that updates exactly the DOM nodes it touches.
|
|
850
|
-
|
|
851
|
-
**Components are classes.** `component` produces an anonymous ES6 class.
|
|
852
|
-
Methods, lifecycle hooks, and state are ordinary class members. No hooks API, no
|
|
853
|
-
composition functions, no magic — just a class with a `render` method.
|
|
854
|
-
|
|
855
|
-
**Props are instance variables.** `@count := 0` defines a reactive prop. The
|
|
856
|
-
parent can set it, ignore it, or share a signal. The child owns it.
|
|
857
|
-
|
|
858
|
-
**File-based everything.** Components live in the VFS. Routes map to VFS paths.
|
|
859
|
-
Layouts are `_layout.rip` files in the directory tree. The file system is the
|
|
860
|
-
API.
|
|
310
|
+
See `docs/demo.html` for a complete example — the full Rip UI Demo app
|
|
311
|
+
(6 components, router, reactive state, persistence) in 337 lines of
|
|
312
|
+
static HTML.
|
|
861
313
|
|
|
862
314
|
## License
|
|
863
315
|
|
|
864
316
|
MIT
|
|
865
|
-
|
|
866
|
-
## Requirements
|
|
867
|
-
|
|
868
|
-
- [Bun](https://bun.sh) runtime
|
|
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)
|
|
871
|
-
|
|
872
|
-
## Links
|
|
873
|
-
|
|
874
|
-
- [Rip Language](https://github.com/shreeve/rip-lang)
|
|
875
|
-
- [@rip-lang/api](../api/README.md) — API framework (routing, middleware, `@send`)
|
|
876
|
-
- [@rip-lang/server](../server/README.md) — Production server (HTTPS, mDNS, multi-worker)
|
|
877
|
-
- [Report Issues](https://github.com/shreeve/rip-lang/issues)
|