@rip-lang/ui 0.3.0 → 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,208 +1,658 @@
1
- <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.png" 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
- > **Zero-build reactive web framework for the Rip language.**
7
+ Widgets are plain `.rip` source files no build step. The browser compiles
8
+ them on the fly via Rip UI's runtime. Include them in your app by adding the
9
+ widgets directory to your serve middleware:
6
10
 
7
- Load the Rip compiler in the browser. Write inline Rip. Launch your app.
8
- No build step, no bundler, no configuration.
11
+ ```coffee
12
+ use serve
13
+ dir: dir
14
+ components: ['components', '../../../packages/ui']
15
+ ```
16
+
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
23
+
24
+ ---
25
+
26
+ ## Overview
27
+
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 |
41
+
42
+ ---
43
+
44
+ ## Widgets
45
+
46
+ ### Select
47
+
48
+ Keyboard-navigable dropdown with typeahead. For provider selects, account
49
+ pickers, or any single-value choice from a list.
50
+
51
+ ```coffee
52
+ Select value <=> selectedRole, @change: handleChange
53
+ option value: "eng", "Engineer"
54
+ option value: "des", "Designer"
55
+ option value: "mgr", "Manager"
56
+ ```
57
+
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
63
+
64
+ ### Combobox
65
+
66
+ Filterable input + dropdown for search-as-you-type scenarios. For patient
67
+ search, autocomplete, or any large list that needs filtering.
68
+
69
+ ```coffee
70
+ Combobox query <=> searchText, @select: handleSelect, @filter: handleFilter
71
+ for item in filteredItems
72
+ div $value: item.id
73
+ span item.name
74
+ ```
75
+
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
81
+
82
+ ### Dialog
83
+
84
+ Modal dialog with focus trap, scroll lock, and escape/click-outside dismiss.
85
+ Restores focus to the previously focused element on close.
86
+
87
+ ```coffee
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"
93
+ ```
94
+
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.
101
+
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.
106
+
107
+ ```coffee
108
+ toasts := []
109
+
110
+ # Add — reactive assignment is the API
111
+ toasts = [...toasts, { message: "Saved!", type: "success" }]
112
+
113
+ # Dismiss — filter it out
114
+ toasts = toasts.filter (t) -> t isnt target
115
+
116
+ # Render
117
+ ToastViewport toasts <=> toasts
118
+ ```
119
+
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.
9
126
 
10
- ## Quick Start
127
+ ### Popover
11
128
 
12
- **`index.rip`** the server:
129
+ Floating content anchored to a trigger element. Positions itself with
130
+ flip/shift to stay in viewport.
13
131
 
14
132
  ```coffee
15
- import { get, use, start, notFound } from '@rip-lang/api'
16
- import { ripUI } from '@rip-lang/ui/serve'
133
+ Popover placement: "bottom-start"
134
+ button "Options"
135
+ div
136
+ p "Popover content here"
137
+ ```
138
+
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
142
+
143
+ ### Tooltip
17
144
 
18
- dir = import.meta.dir
19
- use ripUI dir: dir, 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
145
+ Hover/focus tooltip with configurable delay and positioning.
146
+
147
+ ```coffee
148
+ Tooltip text: "Save your changes", placement: "top"
149
+ button "Save"
23
150
  ```
24
151
 
25
- **`index.html`** the page:
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.
156
+
157
+ ### Tabs
158
+
159
+ Keyboard-navigable tab panel with roving tabindex.
26
160
 
27
- ```html
28
- <script type="module" src="/rip/browser.js"></script>
29
- <script type="text/rip">
30
- { launch } = importRip! '/rip/ui.rip'
31
- launch()
32
- </script>
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"
33
169
  ```
34
170
 
35
- **`components/index.rip`** — a component:
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
175
+
176
+ ### Accordion
177
+
178
+ Expand/collapse sections. Single or multiple mode.
36
179
 
37
180
  ```coffee
38
- export Home = component
39
- @count := 0
40
- render
41
- div
42
- h1 "Hello from Rip UI"
43
- button @click: (-> @count += 1), "Clicked #{@count} times"
181
+ Accordion multiple: false
182
+ div $item: "a"
183
+ button $trigger: true, "Section A"
184
+ div $content: true
185
+ p "Content A"
44
186
  ```
45
187
 
46
- Run `bun index.rip`, open `http://localhost:3000`.
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)`
192
+
193
+ ### Checkbox
47
194
 
48
- ## How It Works
195
+ Toggle with checkbox or switch semantics. Supports indeterminate state.
49
196
 
50
- The browser loads two things from the `/rip/` namespace:
197
+ ```coffee
198
+ Checkbox checked <=> isActive, @change: handleChange
199
+ span "Enable notifications"
51
200
 
52
- - `/rip/browser.js` the Rip compiler (~45KB gzip, cached forever)
53
- - `/rip/ui.rip` — the UI framework (compiled in the browser in ~10-20ms)
201
+ Checkbox checked <=> isDark, switch: true
202
+ span "Dark mode"
203
+ ```
54
204
 
55
- Then `launch()` fetches the app bundle, hydrates the stash, and renders.
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)
56
210
 
57
- ## The Stash
211
+ ### Menu
58
212
 
59
- Everything lives in one reactive tree:
213
+ Dropdown menu with keyboard navigation. For action menus, context menus.
60
214
 
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"
61
221
  ```
62
- app
63
- ├── components/ ← component source files
64
- │ ├── index.rip
65
- │ ├── counter.rip
66
- │ └── _layout.rip
67
- ├── routes ← navigation state (path, params, query, hash)
68
- └── data ← reactive app state (title, theme, user, etc.)
222
+
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
227
+
228
+ ### Grid
229
+
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.
233
+
234
+ ```coffee
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
69
314
  ```
70
315
 
71
- Writing to `app.data.theme` updates any component reading it. The stash
72
- uses Rip's built-in reactive primitives — the same signals that power
73
- `:=` and `~=` in components.
316
+ Import what you need:
74
317
 
75
- ## The App Bundle
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
+ ```
76
326
 
77
- The bundle is JSON served at `/{app}/bundle`. It populates the stash:
327
+ Or import everything:
78
328
 
79
- ```json
80
- {
81
- "components": {
82
- "components/index.rip": "export Home = component...",
83
- "components/counter.rip": "export Counter = component..."
84
- },
85
- "data": {
86
- "title": "My App",
87
- "theme": "light"
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);
339
+ }
340
+ ```
341
+
342
+ **Token categories:**
343
+
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`
350
+
351
+ Define project-level aliases:
352
+
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
+ }
364
+ ```
365
+
366
+ ### CSS Architecture
367
+
368
+ Modern CSS eliminates the need for preprocessors. Use these features directly:
369
+
370
+ **Nesting** — group related rules:
371
+
372
+ ```css
373
+ .card {
374
+ padding: var(--size-4);
375
+
376
+ & .title {
377
+ font-size: var(--font-size-4);
378
+ font-weight: var(--font-weight-7);
379
+ }
380
+
381
+ &:hover {
382
+ box-shadow: var(--shadow-3);
88
383
  }
89
384
  }
90
385
  ```
91
386
 
92
- ## Server Middleware
387
+ **Cascade Layers** — control specificity:
93
388
 
94
- The `ripUI` middleware registers routes for the framework files, the app
95
- bundle, and optional SSE hot-reload:
389
+ ```css
390
+ @layer base, components, overrides;
96
391
 
97
- ```coffee
98
- use ripUI app: '/demo', dir: dir, title: 'My App'
392
+ @layer base {
393
+ button { font: inherit; }
394
+ }
395
+
396
+ @layer components {
397
+ .dialog { border-radius: var(--radius-3); }
398
+ }
99
399
  ```
100
400
 
101
- | Option | Default | Description |
102
- |--------|---------|-------------|
103
- | `app` | `''` | URL mount point |
104
- | `dir` | `'.'` | App directory on disk |
105
- | `components` | `'components'` | Components subdirectory within `dir` |
106
- | `watch` | `false` | Enable SSE hot-reload |
107
- | `debounce` | `250` | Milliseconds to batch file change events |
108
- | `state` | `null` | Initial app state |
109
- | `title` | `null` | Document title |
401
+ **Container Queries** style based on the container, not the viewport:
110
402
 
111
- Routes registered:
403
+ ```css
404
+ .sidebar {
405
+ container-type: inline-size;
406
+ }
112
407
 
408
+ @container (min-width: 400px) {
409
+ .sidebar .nav { flex-direction: row; }
410
+ }
113
411
  ```
114
- /rip/browser.js — Rip compiler
115
- /rip/ui.rip UI framework
116
- /{app}/bundle — app bundle (components + data as JSON)
117
- /{app}/watch — SSE hot-reload stream (when watch: true)
118
- /{app}/components/* — individual component files (for hot-reload refetch)
412
+
413
+ **`color-mix()`** derive colors without Sass:
414
+
415
+ ```css
416
+ .muted {
417
+ color: color-mix(in oklch, var(--color-text), transparent 40%);
418
+ }
119
419
  ```
120
420
 
121
- ## State Preservation (Keep-Alive)
421
+ ### Dark Mode
122
422
 
123
- Components are cached when navigating away instead of destroyed. Navigate
124
- to `/counter`, increment the count, go to `/about`, come back — the count
125
- is preserved. Configurable via `cacheSize` (default 10).
423
+ Use `prefers-color-scheme` with CSS variable swapping:
126
424
 
127
- ## Data Loading
425
+ ```css
426
+ :root {
427
+ color-scheme: light dark;
128
428
 
129
- `createResource` manages async data with reactive `loading`, `error`, and
130
- `data` properties:
429
+ --surface-1: var(--gray-0);
430
+ --surface-2: var(--gray-1);
431
+ --color-text: var(--gray-9);
432
+ }
131
433
 
132
- ```coffee
133
- export UserPage = component
134
- user := createResource -> fetch!("/api/users/#{@params.id}").json!
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
+ }
441
+ ```
442
+
443
+ For a manual toggle, use a `[data-theme]` attribute on the root element:
135
444
 
136
- render
137
- if user.loading
138
- p "Loading..."
139
- else if user.error
140
- p "Error: #{user.error.message}"
141
- else
142
- h1 user.data.name
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
+ }
143
451
  ```
144
452
 
145
- ## Error Boundaries
453
+ ```js
454
+ document.documentElement.dataset.theme = 'dark'
455
+ ```
146
456
 
147
- Layouts with an `onError` method catch errors from child components:
457
+ ### Common Patterns
458
+
459
+ **Button:**
460
+
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);
474
+
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
+ }
148
479
 
149
- ```coffee
150
- export Layout = component
151
- errorMsg := null
480
+ .ghost {
481
+ background: transparent;
482
+ color: var(--color-primary);
152
483
 
153
- onError: (err) -> errorMsg = err.message
484
+ &:hover { background: color-mix(in oklch, var(--color-primary), transparent 90%); }
485
+ }
486
+ ```
487
+
488
+ **Form Input:**
489
+
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);
499
+
500
+ &:focus {
501
+ outline: 2px solid var(--color-primary);
502
+ outline-offset: 1px;
503
+ border-color: var(--color-primary);
504
+ }
154
505
 
155
- render
156
- .app-layout
157
- if errorMsg
158
- .error-banner "#{errorMsg}"
159
- #content
506
+ &[data-invalid] { border-color: var(--color-danger); }
507
+ &[data-disabled] { opacity: 0.5; }
508
+ &::placeholder { color: var(--color-text-muted); }
509
+ }
160
510
  ```
161
511
 
162
- ## Navigation Indicator
512
+ **Card:**
163
513
 
164
- `router.navigating` is a reactive signal — true while a route transition
165
- is in progress:
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);
166
521
 
167
- ```coffee
168
- if @router.navigating
169
- span "Loading..."
522
+ &:hover { box-shadow: var(--shadow-3); }
523
+
524
+ & .title {
525
+ font-size: var(--font-size-3);
526
+ font-weight: var(--font-weight-7);
527
+ margin-block-end: var(--size-2);
528
+ }
529
+
530
+ & .body {
531
+ color: var(--color-text-muted);
532
+ line-height: var(--font-lineheight-3);
533
+ }
534
+ }
170
535
  ```
171
536
 
172
- ## Multi-App Hosting
537
+ **Dialog:**
173
538
 
174
- Mount multiple apps under one server:
539
+ ```css
540
+ .backdrop {
541
+ position: fixed;
542
+ inset: 0;
543
+ background: oklch(0% 0 0 / 40%);
544
+ display: grid;
545
+ place-items: center;
175
546
 
176
- ```coffee
177
- import { get, start, notFound } from '@rip-lang/api'
178
- import { mount as demo } from './demo/index.rip'
179
- import { mount as labs } from './labs/index.rip'
547
+ &[data-open] { animation: fade-in 150ms var(--ease-2); }
548
+ }
549
+
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
+ }
180
559
 
181
- demo '/demo'
182
- labs '/labs'
183
- get '/', -> Response.redirect('/demo/', 302)
184
- start port: 3002
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
+ }
185
565
  ```
186
566
 
187
- Each app is a directory with `components/`, `css/`, `index.html`, and `index.rip`.
188
- The `/rip/` namespace is shared — all apps use the same compiler and framework.
567
+ **Select:**
568
+
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;
581
+
582
+ &[data-open] { border-color: var(--color-primary); }
583
+ }
584
+
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
+ }
189
592
 
190
- ## File Structure
593
+ .option {
594
+ padding: var(--size-2) var(--size-3);
595
+ border-radius: var(--radius-1);
596
+ cursor: pointer;
191
597
 
598
+ &[data-highlighted] { background: var(--surface-2); }
599
+ &[data-selected] { font-weight: var(--font-weight-6); color: var(--color-primary); }
600
+ }
192
601
  ```
193
- my-app/
194
- ├── index.rip # Server
195
- ├── index.html # HTML page
196
- ├── components/
197
- │ ├── _layout.rip # Root layout
198
- │ ├── index.rip # Home → /
199
- │ ├── about.rip # About → /about
200
- │ └── users/
201
- │ └── [id].rip # User profile → /users/:id
202
- └── css/
203
- └── styles.css # Styles
602
+
603
+ **Tooltip:**
604
+
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;
613
+
614
+ &[data-entering] { animation: fade-in 100ms var(--ease-2); }
615
+ &[data-exiting] { animation: fade-out 75ms var(--ease-2); }
616
+ }
204
617
  ```
205
618
 
206
- ## License
619
+ ### What We Don't Use
620
+
621
+ **React or any framework runtime** — Rip widgets are written in Rip, compiled
622
+ to JavaScript, with zero runtime dependencies.
623
+
624
+ **Tailwind CSS** — utility classes in markup are write-only and semantically
625
+ empty. We write real CSS with real selectors.
626
+
627
+ **CSS-in-JS runtimes** (styled-components, Emotion) — runtime style injection
628
+ adds bundle size and creates hydration complexity.
629
+
630
+ **Sass / Less** — native CSS nesting, `color-mix()`, and custom properties
631
+ eliminate the need for preprocessors.
632
+
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.
636
+
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.
640
+
641
+ ---
642
+
643
+ ## File Summary
207
644
 
208
- MIT
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** | |