@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 CHANGED
@@ -1,877 +1,658 @@
1
- <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.svg" style="width:50px" /> <br>
1
+ # Rip UI
2
2
 
3
- # Rip UI - @rip-lang/ui
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
- > **A zero-build reactive web frameworkship 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
- ┌─────────────────────────────────────────┐
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 filesno 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
- 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}"
12
+ use serve
13
+ dir: dir
14
+ components: ['components', '../../../packages/ui']
124
15
  ```
125
16
 
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
- ```
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
- ### The Two Keywords
24
+ ---
146
25
 
147
- **`component`** — Declares an anonymous class with component semantics:
26
+ ## Overview
148
27
 
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
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
- **`render`** — Defines the component's template using a Pug/Jade-like DSL:
42
+ ---
158
43
 
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
44
+ ## Widgets
168
45
 
169
- That's it. No special attribute syntax, no directive system, no template
170
- compiler — just Rip's existing syntax applied to DOM construction.
46
+ ### Select
171
47
 
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:
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
- # 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}
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
- The `isSignal` check in the constructor handles this automatically:
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
- ```javascript
189
- this.count = isSignal(props.count) ? props.count : __state(props.count ?? 0);
190
- ```
64
+ ### Combobox
191
65
 
192
- If you don't want the parent to override a prop, don't use `@`:
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
- 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
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
- ### Required and Optional Props
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
- ```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
- ```
82
+ ### Dialog
208
83
 
209
- ### Computed Values and Effects
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
- 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
- # ...
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
- ### Lifecycle
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
- `mounted`, `unmounted`, and `updated` are just methods. No special syntax. The
228
- runtime calls them at the appropriate times:
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
- Timer = component
232
- @elapsed := 0
233
- @interval = null
108
+ toasts := []
234
109
 
235
- mounted: ->
236
- @interval = setInterval (=> @elapsed += 1), 1000
110
+ # Add — reactive assignment is the API
111
+ toasts = [...toasts, { message: "Saved!", type: "success" }]
237
112
 
238
- unmounted: ->
239
- clearInterval @interval
113
+ # Dismiss — filter it out
114
+ toasts = toasts.filter (t) -> t isnt target
240
115
 
241
- render
242
- p "#{@elapsed} seconds"
116
+ # Render
117
+ ToastViewport toasts <=> toasts
243
118
  ```
244
119
 
245
- ### Two-Way Binding
246
-
247
- The `<=>` operator creates two-way bindings between form elements and reactive
248
- state:
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
- ```coffee
251
- SearchBox = component
252
- @query := ""
127
+ ### Popover
253
128
 
254
- render
255
- input type: "text", value <=> @query
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
- App = component
265
- @user := { name: "Alice" }
266
-
267
- render
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
- ### Context
275
-
276
- Components can share state down the tree without passing props at every level:
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
- ```coffee
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
- Because `component` is an expression (not a declaration), multiple components
289
- can live in one file:
145
+ Hover/focus tooltip with configurable delay and positioning.
290
146
 
291
147
  ```coffee
292
- Button = component
293
- @label = "Click"
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
- ## 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', '...')
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
- // 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
157
+ ### Tabs
333
158
 
334
- // Watch for changes (triggers recompilation)
335
- fs.watch('pages/', ({ event, path }) => {
336
- console.log(`${event}: ${path}`)
337
- })
159
+ Keyboard-navigable tab panel with roving tabindex.
338
160
 
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
- ])
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
- ### Why a VFS?
349
-
350
- Traditional frameworks read from the server's file system during the build step
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:
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
- ## File-Based Router
176
+ ### Accordion
364
177
 
365
- URLs map to VFS paths. The routing conventions match Next.js / SvelteKit:
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
- | 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
- })
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
- The router intercepts `<a>` clicks automatically for SPA navigation. External
397
- links and modified clicks (ctrl+click, etc.) pass through normally.
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
- ## Reactive Stash
193
+ ### Checkbox
400
194
 
401
- Deep reactive state tree with path-based navigation. Every nested property is
402
- automatically tracked — changing any value triggers fine-grained updates.
195
+ Toggle with checkbox or switch semantics. Supports indeterminate state.
403
196
 
404
- ```javascript
405
- import { stash, effect } from '@rip-lang/ui/stash'
197
+ ```coffee
198
+ Checkbox checked <=> isActive, @change: handleChange
199
+ span "Enable notifications"
406
200
 
407
- const app = stash({
408
- user: { name: 'Alice', prefs: { theme: 'dark' } },
409
- cart: { items: [], total: 0 }
410
- })
201
+ Checkbox checked <=> isDark, switch: true
202
+ span "Dark mode"
203
+ ```
411
204
 
412
- // Direct property access (tracked)
413
- app.user.name // 'Alice'
414
- app.user.prefs.theme = 'light' // triggers updates
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
- // 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']
211
+ ### Menu
424
212
 
425
- // Reactive effects
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
- ### State Tiers
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
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
- ### 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
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
- ### `ripUI` Middleware
228
+ ### Grid
486
229
 
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()`:
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
- import { use } from '@rip-lang/api'
493
- import { ripUI } from '@rip-lang/ui/serve'
494
-
495
- use ripUI pages: 'pages', watch: true
496
- ```
497
-
498
- When called, `ripUI` registers the following routes:
499
-
500
- | Route | Description |
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) |
511
-
512
- ### Options
513
-
514
- | Option | Type | Default | Description |
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:
525
-
526
- ```json
527
- {
528
- "pages/index.rip": "Home = component\n render\n h1 \"Hello\"",
529
- "pages/about.rip": "About = component\n render\n h1 \"About\"",
530
- "pages/counter.rip": "..."
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
- The client loads this with `app.loadBundle('/rip-ui/manifest.json')`, which
535
- populates the VFS in a single request — no need to list pages manually.
536
-
537
- ## Hot Reload
342
+ **Token categories:**
538
343
 
539
- The hot-reload system uses a **notify-only** architecture the server tells the
540
- browser *which files changed*, then the browser decides what to refetch.
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
- ### How It Works
351
+ Define project-level aliases:
543
352
 
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)
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
- ### Server Side
366
+ ### CSS Architecture
565
367
 
566
- The `watch` option enables filesystem monitoring with debounced SSE
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
- ```coffee
570
- use ripUI pages: "#{dir}/pages", watch: true, debounce: 250
571
- ```
370
+ **Nesting** — group related rules:
572
371
 
573
- ### Client Side
372
+ ```css
373
+ .card {
374
+ padding: var(--size-4);
574
375
 
575
- Connect to the SSE endpoint after starting the app:
376
+ & .title {
377
+ font-size: var(--font-size-4);
378
+ font-weight: var(--font-weight-7);
379
+ }
576
380
 
577
- ```javascript
578
- app.watch('/rip-ui/watch');
381
+ &:hover {
382
+ box-shadow: var(--shadow-3);
383
+ }
384
+ }
579
385
  ```
580
386
 
581
- The `watch()` method accepts an optional `eager` array files that should always
582
- be refetched immediately, even if they're not the current route:
387
+ **Cascade Layers**control specificity:
583
388
 
584
- ```javascript
585
- app.watch('/rip-ui/watch', {
586
- eager: ['pages/_layout.rip']
587
- });
588
- ```
589
-
590
- ### `loadBundle(url)`
389
+ ```css
390
+ @layer base, components, overrides;
591
391
 
592
- Fetches a JSON manifest containing all page sources and loads them into the VFS
593
- in a single request. Returns the app instance (chainable).
392
+ @layer base {
393
+ button { font: inherit; }
394
+ }
594
395
 
595
- ```javascript
596
- await app.loadBundle('/rip-ui/manifest.json');
396
+ @layer components {
397
+ .dialog { border-radius: var(--radius-3); }
398
+ }
597
399
  ```
598
400
 
599
- ### `remount()`
401
+ **Container Queries** — style based on the container, not the viewport:
600
402
 
601
- The renderer's `remount()` method re-renders the current route. This is called
602
- automatically by `watch()` after refetching changed files, but it can also be
603
- called manually:
403
+ ```css
404
+ .sidebar {
405
+ container-type: inline-size;
406
+ }
604
407
 
605
- ```javascript
606
- app.renderer.remount();
408
+ @container (min-width: 400px) {
409
+ .sidebar .nav { flex-direction: row; }
410
+ }
607
411
  ```
608
412
 
609
- ## Quick Start
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
- **`index.rip`** — The complete server:
618
-
619
- ```coffee
620
- import { get, use, start, notFound } from '@rip-lang/api'
621
- import { ripUI } from '@rip-lang/ui/serve'
415
+ ```css
416
+ .muted {
417
+ color: color-mix(in oklch, var(--color-text), transparent 40%);
418
+ }
419
+ ```
622
420
 
623
- dir = import.meta.dir
421
+ ### Dark Mode
624
422
 
625
- use ripUI pages: "#{dir}/pages", watch: true
423
+ Use `prefers-color-scheme` with CSS variable swapping:
626
424
 
627
- get '/css/*', -> @send "#{dir}/css/#{@req.path.slice(5)}"
425
+ ```css
426
+ :root {
427
+ color-scheme: light dark;
628
428
 
629
- notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
429
+ --surface-1: var(--gray-0);
430
+ --surface-2: var(--gray-1);
431
+ --color-text: var(--gray-9);
432
+ }
630
433
 
631
- start port: 3000
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
- **`index.html`** The HTML shell:
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
- That's it. Run `bun index.rip` and open `http://localhost:3000`. Every `.rip`
661
- file in the `pages/` directory is auto-discovered, served as a manifest, and
662
- hot-reloaded on save.
663
-
664
- ### Standalone (No Server)
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
- ### File Structure
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
- ### Tags and Classes
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
- ### Dynamic Classes (CLSX)
459
+ **Button:**
725
460
 
726
- ```coffee
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
- ```
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
- Dynamic class expressions are evaluated at runtime. Falsy values are filtered
734
- out. This provides native CLSX-like behavior without a library.
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
- ### Attributes and Events
480
+ .ghost {
481
+ background: transparent;
482
+ color: var(--color-primary);
737
483
 
738
- ```coffee
739
- render
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
- ### Text and Interpolation
488
+ **Form Input:**
747
489
 
748
- ```coffee
749
- render
750
- h1 "Static text"
751
- p "Hello, #{@name}"
752
- span "Count: #{@count}"
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
- ### Conditionals
500
+ &:focus {
501
+ outline: 2px solid var(--color-primary);
502
+ outline-offset: 1px;
503
+ border-color: var(--color-primary);
504
+ }
756
505
 
757
- ```coffee
758
- render
759
- if @loggedIn
760
- p "Welcome back, #{@name}"
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
- ### Loops
512
+ **Card:**
766
513
 
767
- ```coffee
768
- render
769
- ul
770
- for item in @items
771
- li item.name
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
- ### Nesting
522
+ &:hover { box-shadow: var(--shadow-3); }
775
523
 
776
- Indentation defines parent-child relationships:
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
- ```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"
530
+ & .body {
531
+ color: var(--color-text-muted);
532
+ line-height: var(--font-lineheight-3);
533
+ }
534
+ }
790
535
  ```
791
536
 
792
- ## API Reference
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
- | 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 |
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
- Also exposes: `app` (stash), `fs` (VFS), `router`, `renderer`
547
+ &[data-open] { animation: fade-in 150ms var(--ease-2); }
548
+ }
822
549
 
823
- ### `stash(data)`
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
- Creates a deeply reactive proxy around `data`. Every property read is tracked,
826
- every write triggers effects.
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
- ### `effect(fn)`
567
+ **Select:**
829
568
 
830
- Creates a side effect that re-runs whenever its tracked dependencies change.
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
- ### `computed(fn)`
582
+ &[data-open] { border-color: var(--color-primary); }
583
+ }
833
584
 
834
- Creates a lazy computed value that caches until dependencies change.
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
- ### `batch(fn)`
593
+ .option {
594
+ padding: var(--size-2) var(--size-3);
595
+ border-radius: var(--radius-1);
596
+ cursor: pointer;
837
597
 
838
- Groups multiple state updates — effects only fire once at the end.
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
- ## Design Principles
603
+ **Tooltip:**
841
604
 
842
- **No build step.** The compiler is small enough to ship. Source files are the
843
- deployment artifact.
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
- **Language-native reactivity.** `:=` for state, `~=` for computed, `~>` for
846
- effects. These are Rip language features, not framework APIs.
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
- **Fine-grained DOM updates.** No virtual DOM. Each reactive binding creates a
849
- direct effect that updates exactly the DOM nodes it touches.
619
+ ### What We Don't Use
850
620
 
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.
621
+ **React or any framework runtime** Rip widgets are written in Rip, compiled
622
+ to JavaScript, with zero runtime dependencies.
854
623
 
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.
624
+ **Tailwind CSS** utility classes in markup are write-only and semantically
625
+ empty. We write real CSS with real selectors.
857
626
 
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.
627
+ **CSS-in-JS runtimes** (styled-components, Emotion) runtime style injection
628
+ adds bundle size and creates hydration complexity.
861
629
 
862
- ## License
630
+ **Sass / Less** — native CSS nesting, `color-mix()`, and custom properties
631
+ eliminate the need for preprocessors.
863
632
 
864
- MIT
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
- ## Requirements
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
- - [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)
641
+ ---
871
642
 
872
- ## Links
643
+ ## File Summary
873
644
 
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)
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** | |