@rip-lang/ui 0.1.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 ADDED
@@ -0,0 +1,694 @@
1
+ <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.svg" style="width:50px" /> <br>
2
+
3
+ # Rip UI - @rip-lang/ui
4
+
5
+ > **A zero-build reactive web framework — ship the compiler to the browser, compile on demand, render with fine-grained reactivity**
6
+
7
+ Rip UI inverts the traditional web development model. Instead of building,
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
+ Browser loads: rip.browser.js (40KB) + @rip-lang/ui (~8KB)
22
+
23
+ ┌────────────────┼────────────────┐
24
+ │ │ │
25
+ Reactive Stash Virtual FS Router
26
+ (app state) (file storage) (URL → VFS)
27
+ │ │ │
28
+ └────────────────┼────────────────┘
29
+
30
+ Renderer
31
+ (compiles + mounts)
32
+
33
+ DOM
34
+ ```
35
+
36
+ | Module | Size | Role |
37
+ |--------|------|------|
38
+ | `ui.js` | ~150 lines | `createApp` entry point, re-exports everything |
39
+ | `stash.js` | ~400 lines | Deep reactive state tree with path-based navigation |
40
+ | `vfs.js` | ~200 lines | Browser-local Virtual File System with watchers |
41
+ | `router.js` | ~300 lines | File-based router (URL ↔ VFS paths, History API) |
42
+ | `renderer.js` | ~250 lines | Component lifecycle, layouts, transitions |
43
+
44
+ ## The Idea
45
+
46
+ Modern web frameworks — React, Vue, Svelte, Solid — all share the same
47
+ fundamental assumption: code must be compiled and bundled on the developer's
48
+ machine, then shipped as static artifacts. This creates an entire ecosystem of
49
+ build tools (Vite, Webpack, Turbopack, esbuild), configuration files, dev
50
+ servers, hot module replacement protocols, and deployment pipelines. The
51
+ developer experience is powerful, but the machinery is enormous.
52
+
53
+ Rip UI asks: **what if the compiler ran in the browser?**
54
+
55
+ At 40KB, the Rip compiler is small enough to ship alongside your application.
56
+ Components arrive as `.rip` source files — plain text — and are compiled to
57
+ JavaScript on the client's machine. This eliminates the build step entirely.
58
+ There is no `dist/` folder, no source maps, no chunk splitting, no tree
59
+ shaking, no CI build minutes. You write a `.rip` file, the browser compiles it,
60
+ and it runs.
61
+
62
+ This is not a toy or a limitation. The compiler produces the same output it
63
+ would on a server. The reactive system is the same signal-based engine that
64
+ powers server-side Rip. The component model compiles to anonymous ES6 classes
65
+ with fine-grained DOM manipulation — no virtual DOM diffing.
66
+
67
+ ### How It Differs from Existing Frameworks
68
+
69
+ | | React/Vue/Svelte | Rip UI |
70
+ |---|---|---|
71
+ | **Build step** | Required (Vite, Webpack, etc.) | None — compiler runs in browser |
72
+ | **Bundle size** | 40-100KB+ framework + app bundle | 40KB compiler + ~8KB framework + raw source |
73
+ | **HMR** | Dev server ↔ browser WebSocket | Not needed — recompile in-place |
74
+ | **Deployment** | Build artifacts (`dist/`) | Source files served as-is |
75
+ | **Component format** | JSX, SFC, templates | Rip source (`.rip` files) |
76
+ | **Reactivity** | Library-specific (hooks, refs, signals) | Language-native (`:=`, `~=`, `~>`) |
77
+ | **DOM updates** | Virtual DOM diff or compiled transforms | Fine-grained effects, direct DOM mutation |
78
+ | **Routing** | Framework plugin (react-router, etc.) | Built-in file-based router over VFS |
79
+ | **State management** | External library (Redux, Pinia, etc.) | Built-in reactive stash with deep tracking |
80
+
81
+ ## Component Model
82
+
83
+ The component system adds two keywords to Rip: `component` and `render`. Everything
84
+ else — reactive state (`:=`), computed values (`~=`), effects (`~>`), methods,
85
+ lifecycle — uses standard Rip syntax. A component is an expression that evaluates
86
+ to an anonymous ES6 class.
87
+
88
+ ### Defining a Component
89
+
90
+ ```coffee
91
+ Counter = component
92
+ @count := 0 # reactive state (signal) — parent can override via props
93
+ @step = 1 # plain prop — parent can set, not reactive
94
+
95
+ doubled ~= @count * 2 # computed (derived, read-only)
96
+
97
+ increment: -> @count += @step
98
+ decrement: -> @count -= @step
99
+
100
+ mounted: ->
101
+ console.log "Counter mounted"
102
+
103
+ render
104
+ div.counter
105
+ h1 "Count: #{@count}"
106
+ p "Doubled: #{doubled}"
107
+ button @click: @increment, "+#{@step}"
108
+ button @click: @decrement, "-#{@step}"
109
+ ```
110
+
111
+ This compiles to an anonymous ES6 class expression:
112
+
113
+ ```javascript
114
+ Counter = class {
115
+ constructor(props = {}) {
116
+ this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
117
+ this.step = props.step ?? 1;
118
+ this.doubled = __computed(() => this.count.value * 2);
119
+ }
120
+ increment() { return this.count.value += this.step; }
121
+ decrement() { return this.count.value -= this.step; }
122
+ mounted() { return console.log("Counter mounted"); }
123
+ _create() { /* fine-grained DOM creation */ }
124
+ _setup() { /* reactive effect bindings */ }
125
+ mount(target) { /* ... */ }
126
+ unmount() { /* ... */ }
127
+ }
128
+ ```
129
+
130
+ ### The Two Keywords
131
+
132
+ **`component`** — Declares an anonymous class with component semantics:
133
+
134
+ - `@` properties become instance variables that the parent can set via props
135
+ - `:=` assignments create reactive signals (`__state`)
136
+ - `~=` assignments create computed values (`__computed`)
137
+ - `~>` assignments create effects (`__effect`)
138
+ - Plain `=` assignments with function values become methods
139
+ - `mounted`, `unmounted`, `updated` are lifecycle hooks called by the runtime
140
+ - Everything else is standard Rip class behavior
141
+
142
+ **`render`** — Defines the component's template using a Pug/Jade-like DSL:
143
+
144
+ - Tags are bare identifiers: `div`, `h1`, `button`, `span`
145
+ - Classes use dot notation: `div.card.active`, `button.btn.btn-primary`
146
+ - Dynamic classes use dot-parens: `div.("active" if @selected)` (CLSX-like)
147
+ - Attributes use object syntax: `input type: "text", placeholder: "..."`
148
+ - Events use `@` prefix: `button @click: @handleClick`
149
+ - Text content is a string argument: `h1 "Hello"`
150
+ - Interpolation works: `p "Count: #{@count}"`
151
+ - Children are indented below their parent
152
+ - `if`/`else` and `for...in` work inside templates
153
+
154
+ That's it. No special attribute syntax, no directive system, no template
155
+ compiler — just Rip's existing syntax applied to DOM construction.
156
+
157
+ ### Props
158
+
159
+ Every `@` property on a component is a prop that the parent can set. The child
160
+ owns the property; the parent can provide an initial value or pass a reactive
161
+ signal:
162
+
163
+ ```coffee
164
+ # Parent passes plain value — child wraps it in a signal
165
+ Counter {count: 10}
166
+
167
+ # Parent passes its own signal — child uses it directly (shared state)
168
+ Counter {count: parentCount}
169
+ ```
170
+
171
+ The `isSignal` check in the constructor handles this automatically:
172
+
173
+ ```javascript
174
+ this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
175
+ ```
176
+
177
+ If you don't want the parent to override a prop, don't use `@`:
178
+
179
+ ```coffee
180
+ MyComponent = component
181
+ active := false # internal state — not a prop, parent can't set it
182
+ @count := 0 # prop — parent can set or share a signal
183
+ ```
184
+
185
+ ### Required and Optional Props
186
+
187
+ ```coffee
188
+ UserCard = component
189
+ @name # required — no default, error if missing
190
+ @avatar = "/default.png" # optional — has a default
191
+ @bio? := "" # optional reactive — ? makes it explicitly optional
192
+ ```
193
+
194
+ ### Computed Values and Effects
195
+
196
+ ```coffee
197
+ TodoList = component
198
+ @todos := []
199
+ remaining ~= @todos.filter((t) -> not t.done).length
200
+ total ~= @todos.length
201
+
202
+ ~> console.log "#{remaining} of #{total} remaining"
203
+
204
+ render
205
+ div
206
+ h2 "Todos (#{remaining}/#{total})"
207
+ # ...
208
+ ```
209
+
210
+ ### Lifecycle
211
+
212
+ `mounted`, `unmounted`, and `updated` are just methods. No special syntax. The
213
+ runtime calls them at the appropriate times:
214
+
215
+ ```coffee
216
+ Timer = component
217
+ @elapsed := 0
218
+ @interval = null
219
+
220
+ mounted: ->
221
+ @interval = setInterval (=> @elapsed += 1), 1000
222
+
223
+ unmounted: ->
224
+ clearInterval @interval
225
+
226
+ render
227
+ p "#{@elapsed} seconds"
228
+ ```
229
+
230
+ ### Two-Way Binding
231
+
232
+ The `<=>` operator creates two-way bindings between form elements and reactive
233
+ state:
234
+
235
+ ```coffee
236
+ SearchBox = component
237
+ @query := ""
238
+
239
+ render
240
+ input type: "text", value <=> @query
241
+ p "Searching for: #{@query}"
242
+ ```
243
+
244
+ ### Child Components
245
+
246
+ Components can nest. Props are passed as object arguments:
247
+
248
+ ```coffee
249
+ App = component
250
+ @user := { name: "Alice" }
251
+
252
+ render
253
+ div
254
+ Header {title: "My App"}
255
+ UserCard {name: @user.name, avatar: "/alice.png"}
256
+ Footer
257
+ ```
258
+
259
+ ### Context
260
+
261
+ Components can share state down the tree without passing props at every level:
262
+
263
+ ```coffee
264
+ # In a parent component's constructor:
265
+ setContext 'theme', @theme
266
+
267
+ # In any descendant component:
268
+ theme = getContext 'theme'
269
+ ```
270
+
271
+ ### Multiple Components Per File
272
+
273
+ Because `component` is an expression (not a declaration), multiple components
274
+ can live in one file:
275
+
276
+ ```coffee
277
+ Button = component
278
+ @label = "Click"
279
+ @onClick = null
280
+ render
281
+ button.btn @click: @onClick, @label
282
+
283
+ Card = component
284
+ @title = ""
285
+ render
286
+ div.card
287
+ h3 @title
288
+ div.card-body
289
+ slot
290
+
291
+ Page = component
292
+ render
293
+ div
294
+ Card {title: "Welcome"}
295
+ Button {label: "Get Started", onClick: -> alert "Go!"}
296
+ ```
297
+
298
+ ## Virtual File System
299
+
300
+ The VFS is a browser-local file storage layer. Components are delivered as
301
+ source text and stored in memory. The compiler reads from the VFS, not from
302
+ disk.
303
+
304
+ ```javascript
305
+ import { vfs } from '@rip-lang/ui/vfs'
306
+
307
+ const fs = vfs()
308
+
309
+ // Write source files
310
+ fs.write('pages/index.rip', 'component Home\n render\n h1 "Hello"')
311
+ fs.write('pages/users/[id].rip', '...')
312
+
313
+ // Read
314
+ fs.read('pages/index.rip') // source string
315
+ fs.exists('pages/index.rip') // true
316
+ fs.list('pages/') // ['index.rip', 'users/']
317
+ fs.listAll('pages/') // all files recursively
318
+
319
+ // Watch for changes (triggers recompilation)
320
+ fs.watch('pages/', ({ event, path }) => {
321
+ console.log(`${event}: ${path}`)
322
+ })
323
+
324
+ // Fetch from server
325
+ await fs.fetch('pages/index.rip', '/api/pages/index.rip')
326
+ await fs.fetchManifest([
327
+ 'pages/index.rip',
328
+ 'pages/about.rip',
329
+ 'pages/counter.rip'
330
+ ])
331
+ ```
332
+
333
+ ### Why a VFS?
334
+
335
+ Traditional frameworks read from the server's file system during the build step
336
+ and produce static bundles. Rip UI has no build step, so it needs somewhere to
337
+ store source files in the browser. The VFS provides:
338
+
339
+ - **Addressable storage** — components are referenced by path, just like files
340
+ - **File watching** — the renderer re-compiles when a file changes
341
+ - **Lazy loading** — pages can be fetched on demand as the user navigates
342
+ - **Hot update** — write a new version of a file and the component re-renders
343
+ - **Manifest loading** — bulk-fetch an app's files in one call
344
+
345
+ The VFS is not IndexedDB or localStorage — it's a plain in-memory Map. Fast,
346
+ simple, ephemeral. For persistence, the server delivers files on page load.
347
+
348
+ ## File-Based Router
349
+
350
+ URLs map to VFS paths. The routing conventions match Next.js / SvelteKit:
351
+
352
+ ```javascript
353
+ import { createRouter } from '@rip-lang/ui/router'
354
+
355
+ const router = createRouter(fs, { root: 'pages' })
356
+ ```
357
+
358
+ | VFS Path | URL Pattern | Example |
359
+ |----------|-------------|---------|
360
+ | `pages/index.rip` | `/` | Home page |
361
+ | `pages/about.rip` | `/about` | Static page |
362
+ | `pages/users/index.rip` | `/users` | User list |
363
+ | `pages/users/[id].rip` | `/users/:id` | Dynamic segment |
364
+ | `pages/blog/[...slug].rip` | `/blog/*slug` | Catch-all |
365
+ | `pages/_layout.rip` | — | Root layout (wraps all pages) |
366
+ | `pages/users/_layout.rip` | — | Nested layout (wraps `/users/*`) |
367
+
368
+ ```javascript
369
+ // Navigate
370
+ router.push('/users/42')
371
+ router.replace('/login')
372
+ router.back()
373
+
374
+ // Reactive route state
375
+ effect(() => {
376
+ console.log(router.path) // '/users/42'
377
+ console.log(router.params) // { id: '42' }
378
+ })
379
+ ```
380
+
381
+ The router intercepts `<a>` clicks automatically for SPA navigation. External
382
+ links and modified clicks (ctrl+click, etc.) pass through normally.
383
+
384
+ ## Reactive Stash
385
+
386
+ Deep reactive state tree with path-based navigation. Every nested property is
387
+ automatically tracked — changing any value triggers fine-grained updates.
388
+
389
+ ```javascript
390
+ import { stash, effect } from '@rip-lang/ui/stash'
391
+
392
+ const app = stash({
393
+ user: { name: 'Alice', prefs: { theme: 'dark' } },
394
+ cart: { items: [], total: 0 }
395
+ })
396
+
397
+ // Direct property access (tracked)
398
+ app.user.name // 'Alice'
399
+ app.user.prefs.theme = 'light' // triggers updates
400
+
401
+ // Path-based access
402
+ app.get('user.prefs.theme') // 'light'
403
+ app.set('cart.items[0]', { id: 1 }) // deep set with auto-creation
404
+ app.has('user.name') // true
405
+ app.del('cart.items[0]') // delete
406
+ app.inc('cart.total', 9.99) // increment
407
+ app.merge({ user: { role: 'admin' }}) // shallow merge
408
+ app.keys('user') // ['name', 'prefs', 'role']
409
+
410
+ // Reactive effects
411
+ effect(() => {
412
+ console.log(`Theme: ${app.user.prefs.theme}`)
413
+ // Re-runs whenever theme changes
414
+ })
415
+ ```
416
+
417
+ ### State Tiers
418
+
419
+ Three levels of reactive state, each scoped appropriately:
420
+
421
+ | Tier | Scope | Lifetime | Access |
422
+ |------|-------|----------|--------|
423
+ | **App** | Global | Entire session | `app.user`, `app.theme` |
424
+ | **Route** | Per-route | Until navigation | `router.params`, `router.query` |
425
+ | **Component** | Per-instance | Until unmount | `:=` reactive state |
426
+
427
+ App state lives in the stash. Route state lives in the router. Component state
428
+ lives in the component instance as signals. All three are reactive — changes at
429
+ any level trigger the appropriate DOM updates.
430
+
431
+ ## Component Renderer
432
+
433
+ The renderer is the bridge between the router and the DOM. When the route
434
+ changes, the renderer:
435
+
436
+ 1. Resolves the VFS path for the new route
437
+ 2. Reads the `.rip` source from the VFS
438
+ 3. Compiles it to JavaScript using the Rip compiler
439
+ 4. Evaluates the compiled code to get a component class
440
+ 5. Instantiates the component, passing route params as props
441
+ 6. Wraps it in any applicable layout components
442
+ 7. Mounts the result into the DOM target
443
+ 8. Runs transition animations (if configured)
444
+ 9. Unmounts the previous component
445
+
446
+ ```javascript
447
+ import { createRenderer } from '@rip-lang/ui/renderer'
448
+
449
+ const renderer = createRenderer({
450
+ router,
451
+ fs,
452
+ stash: appState,
453
+ compile: compileToJS,
454
+ target: '#app',
455
+ transition: { duration: 200 }
456
+ })
457
+
458
+ renderer.start() // Watch for route changes, mount components
459
+ renderer.stop() // Unmount everything, clean up
460
+ ```
461
+
462
+ ### Compilation Cache
463
+
464
+ Compiled components are cached by VFS path. A file is only recompiled when it
465
+ changes. The VFS watcher triggers cache invalidation, so updating a file in the
466
+ VFS automatically causes the next render to use the new version.
467
+
468
+ ## Quick Start
469
+
470
+ ### Minimal HTML Shell
471
+
472
+ ```html
473
+ <!DOCTYPE html>
474
+ <html>
475
+ <head><title>My App</title></head>
476
+ <body>
477
+ <div id="app"></div>
478
+ <script type="module">
479
+ import { compileToJS } from './rip.browser.js'
480
+ import { createApp } from './ui.js'
481
+
482
+ const app = createApp({
483
+ target: '#app',
484
+ compile: compileToJS,
485
+ state: { theme: 'light' }
486
+ })
487
+
488
+ // Load pages into the VFS
489
+ await app.load([
490
+ 'pages/_layout.rip',
491
+ 'pages/index.rip',
492
+ 'pages/about.rip'
493
+ ])
494
+
495
+ // Start routing and rendering
496
+ app.start()
497
+ </script>
498
+ </body>
499
+ </html>
500
+ ```
501
+
502
+ ### Inline Components (No Server)
503
+
504
+ ```html
505
+ <script type="module">
506
+ import { compileToJS } from './rip.browser.js'
507
+ import { createApp } from './ui.js'
508
+
509
+ createApp({
510
+ target: '#app',
511
+ compile: compileToJS,
512
+ files: {
513
+ 'pages/index.rip': `
514
+ Home = component
515
+ render
516
+ h1 "Hello, World"
517
+ p "This was compiled in your browser."
518
+ `
519
+ }
520
+ }).start()
521
+ </script>
522
+ ```
523
+
524
+ ### File Structure
525
+
526
+ ```
527
+ my-app/
528
+ ├── index.html # HTML shell (the only "build" artifact)
529
+ ├── rip.browser.js # Rip compiler (40KB)
530
+ ├── ui.js # Framework entry point
531
+ ├── stash.js # Reactive state
532
+ ├── vfs.js # Virtual File System
533
+ ├── router.js # File-based router
534
+ ├── renderer.js # Component renderer
535
+ ├── pages/
536
+ │ ├── _layout.rip # Root layout (nav, footer)
537
+ │ ├── index.rip # Home page → /
538
+ │ ├── about.rip # About page → /about
539
+ │ └── users/
540
+ │ ├── _layout.rip # Users layout → wraps /users/*
541
+ │ ├── index.rip # User list → /users
542
+ │ └── [id].rip # User profile → /users/:id
543
+ └── css/
544
+ └── styles.css # Tailwind or plain CSS
545
+ ```
546
+
547
+ ## Render Template Syntax
548
+
549
+ The `render` block uses a concise, indentation-based template syntax:
550
+
551
+ ### Tags and Classes
552
+
553
+ ```coffee
554
+ render
555
+ div # <div></div>
556
+ div.card # <div class="card"></div>
557
+ div.card.active # <div class="card active"></div>
558
+ button.btn.btn-primary # <button class="btn btn-primary"></button>
559
+ ```
560
+
561
+ ### Dynamic Classes (CLSX)
562
+
563
+ ```coffee
564
+ render
565
+ div.("active" if @selected) # conditional class
566
+ div.("bg-red" if error, "bg-green" if ok) # multiple conditions
567
+ div.card.("highlighted" if @featured) # static + dynamic
568
+ ```
569
+
570
+ Dynamic class expressions are evaluated at runtime. Falsy values are filtered
571
+ out. This provides native CLSX-like behavior without a library.
572
+
573
+ ### Attributes and Events
574
+
575
+ ```coffee
576
+ render
577
+ input type: "text", placeholder: "Search..."
578
+ button @click: @handleClick, "Submit"
579
+ a href: "/about", "About Us"
580
+ img src: @imageUrl, alt: "Photo"
581
+ ```
582
+
583
+ ### Text and Interpolation
584
+
585
+ ```coffee
586
+ render
587
+ h1 "Static text"
588
+ p "Hello, #{@name}"
589
+ span "Count: #{@count}"
590
+ ```
591
+
592
+ ### Conditionals
593
+
594
+ ```coffee
595
+ render
596
+ if @loggedIn
597
+ p "Welcome back, #{@name}"
598
+ else
599
+ p "Please log in"
600
+ ```
601
+
602
+ ### Loops
603
+
604
+ ```coffee
605
+ render
606
+ ul
607
+ for item in @items
608
+ li item.name
609
+ ```
610
+
611
+ ### Nesting
612
+
613
+ Indentation defines parent-child relationships:
614
+
615
+ ```coffee
616
+ render
617
+ div.app
618
+ header.app-header
619
+ h1 "My App"
620
+ nav
621
+ a href: "/", "Home"
622
+ a href: "/about", "About"
623
+ main.app-body
624
+ p "Content goes here"
625
+ footer
626
+ p "Footer"
627
+ ```
628
+
629
+ ## API Reference
630
+
631
+ ### `createApp(options)`
632
+
633
+ | Option | Type | Default | Description |
634
+ |--------|------|---------|-------------|
635
+ | `target` | `string\|Element` | `'#app'` | DOM mount target |
636
+ | `state` | `object` | `{}` | Initial app state (becomes reactive stash) |
637
+ | `files` | `object` | `{}` | Initial VFS files `{ path: content }` |
638
+ | `root` | `string` | `'pages'` | Pages directory in VFS |
639
+ | `compile` | `function` | — | Rip compiler (`compileToJS`) |
640
+ | `transition` | `object` | — | Route transition `{ duration }` |
641
+ | `onError` | `function` | — | Error handler |
642
+ | `onNavigate` | `function` | — | Navigation callback |
643
+
644
+ Returns: `{ app, fs, router, renderer, start, stop, load, go, addPage, get, set }`
645
+
646
+ ### `stash(data)`
647
+
648
+ Creates a deeply reactive proxy around `data`. Every property read is tracked,
649
+ every write triggers effects.
650
+
651
+ ### `effect(fn)`
652
+
653
+ Creates a side effect that re-runs whenever its tracked dependencies change.
654
+
655
+ ### `computed(fn)`
656
+
657
+ Creates a lazy computed value that caches until dependencies change.
658
+
659
+ ### `batch(fn)`
660
+
661
+ Groups multiple state updates — effects only fire once at the end.
662
+
663
+ ## Design Principles
664
+
665
+ **No build step.** The compiler is small enough to ship. Source files are the
666
+ deployment artifact.
667
+
668
+ **Language-native reactivity.** `:=` for state, `~=` for computed, `~>` for
669
+ effects. These are Rip language features, not framework APIs.
670
+
671
+ **Fine-grained DOM updates.** No virtual DOM. Each reactive binding creates a
672
+ direct effect that updates exactly the DOM nodes it touches.
673
+
674
+ **Components are classes.** `component` produces an anonymous ES6 class.
675
+ Methods, lifecycle hooks, and state are ordinary class members. No hooks API, no
676
+ composition functions, no magic — just a class with a `render` method.
677
+
678
+ **Props are instance variables.** `@count := 0` defines a reactive prop. The
679
+ parent can set it, ignore it, or share a signal. The child owns it.
680
+
681
+ **File-based everything.** Components live in the VFS. Routes map to VFS paths.
682
+ Layouts are `_layout.rip` files in the directory tree. The file system is the
683
+ API.
684
+
685
+ ## License
686
+
687
+ MIT
688
+
689
+ ## Links
690
+
691
+ - [Rip Language](https://github.com/shreeve/rip-lang)
692
+ - [@rip-lang/api](../api/README.md)
693
+ - [@rip-lang/server](../server/README.md)
694
+ - [Report Issues](https://github.com/shreeve/rip-lang/issues)