@rip-lang/ui 0.3.67 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/AGENTS.md +93 -0
  2. package/README.md +22 -625
  3. package/browser/AGENTS.md +213 -0
  4. package/browser/CONTRIBUTING.md +375 -0
  5. package/browser/README.md +11 -0
  6. package/browser/TESTING.md +59 -0
  7. package/browser/browser.rip +56 -0
  8. package/{components → browser/components}/accordion.rip +1 -1
  9. package/{components → browser/components}/alert-dialog.rip +6 -3
  10. package/{components → browser/components}/autocomplete.rip +27 -21
  11. package/{components → browser/components}/avatar.rip +3 -3
  12. package/{components → browser/components}/badge.rip +1 -1
  13. package/{components → browser/components}/breadcrumb.rip +2 -2
  14. package/{components → browser/components}/button-group.rip +3 -3
  15. package/{components → browser/components}/button.rip +2 -2
  16. package/{components → browser/components}/card.rip +1 -1
  17. package/{components → browser/components}/carousel.rip +5 -5
  18. package/{components → browser/components}/checkbox-group.rip +40 -11
  19. package/{components → browser/components}/checkbox.rip +4 -4
  20. package/{components → browser/components}/collapsible.rip +2 -2
  21. package/{components → browser/components}/combobox.rip +36 -23
  22. package/{components → browser/components}/context-menu.rip +1 -1
  23. package/{components → browser/components}/date-picker.rip +5 -5
  24. package/{components → browser/components}/dialog.rip +8 -4
  25. package/{components → browser/components}/drawer.rip +8 -4
  26. package/{components → browser/components}/editable-value.rip +7 -1
  27. package/{components → browser/components}/field.rip +5 -5
  28. package/{components → browser/components}/fieldset.rip +2 -2
  29. package/{components → browser/components}/form.rip +1 -1
  30. package/{components → browser/components}/grid.rip +8 -8
  31. package/{components → browser/components}/input-group.rip +1 -1
  32. package/{components → browser/components}/input.rip +6 -6
  33. package/{components → browser/components}/label.rip +2 -2
  34. package/{components → browser/components}/menu.rip +17 -10
  35. package/{components → browser/components}/menubar.rip +1 -1
  36. package/{components → browser/components}/meter.rip +7 -7
  37. package/{components → browser/components}/multi-select.rip +76 -33
  38. package/{components → browser/components}/native-select.rip +3 -3
  39. package/{components → browser/components}/nav-menu.rip +3 -3
  40. package/{components → browser/components}/number-field.rip +11 -11
  41. package/{components → browser/components}/otp-field.rip +4 -4
  42. package/{components → browser/components}/pagination.rip +4 -4
  43. package/{components → browser/components}/popover.rip +11 -24
  44. package/{components → browser/components}/preview-card.rip +7 -11
  45. package/{components → browser/components}/progress.rip +3 -3
  46. package/{components → browser/components}/radio-group.rip +4 -4
  47. package/{components → browser/components}/resizable.rip +3 -3
  48. package/{components → browser/components}/scroll-area.rip +1 -1
  49. package/{components → browser/components}/select.rip +55 -27
  50. package/{components → browser/components}/separator.rip +2 -2
  51. package/{components → browser/components}/skeleton.rip +4 -4
  52. package/{components → browser/components}/slider.rip +15 -10
  53. package/{components → browser/components}/spinner.rip +2 -2
  54. package/{components → browser/components}/table.rip +2 -2
  55. package/{components → browser/components}/tabs.rip +12 -7
  56. package/{components → browser/components}/textarea.rip +8 -8
  57. package/{components → browser/components}/toast.rip +3 -3
  58. package/{components → browser/components}/toggle-group.rip +42 -11
  59. package/{components → browser/components}/toggle.rip +2 -2
  60. package/{components → browser/components}/toolbar.rip +2 -2
  61. package/{components → browser/components}/tooltip.rip +19 -23
  62. package/browser/hljs-rip.js +209 -0
  63. package/browser/playwright.config.mjs +31 -0
  64. package/browser/tests/overlays.js +352 -0
  65. package/email/AGENTS.md +16 -0
  66. package/email/README.md +55 -0
  67. package/email/benchmarks/benchmark.rip +94 -0
  68. package/email/benchmarks/samples.rip +104 -0
  69. package/email/compat.rip +129 -0
  70. package/email/components.rip +371 -0
  71. package/email/dom.rip +330 -0
  72. package/email/email.rip +10 -0
  73. package/email/render.rip +82 -0
  74. package/package.json +29 -39
  75. package/shared/README.md +3 -0
  76. package/shared/styles.rip +17 -0
  77. package/tailwind/AGENTS.md +3 -0
  78. package/tailwind/README.md +27 -0
  79. package/tailwind/engine.js +107 -0
  80. package/tailwind/inline.js +215 -0
  81. package/tailwind/serve.js +6 -0
  82. package/tailwind/tailwind.rip +13 -0
  83. package/ui.rip +3 -0
package/README.md CHANGED
@@ -1,642 +1,39 @@
1
- # Rip UI
1
+ <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/assets/rip.png" style="width:50px" /> <br>
2
2
 
3
- Headless, accessible UI components written in Rip. Zero dependencies.
4
- Every widget exposes `$` attributes (compiled to `data-*`) for styling and
5
- handles keyboard interactions per WAI-ARIA Authoring Practices. Style with
6
- Tailwind using `data-[attr]:` variants.
3
+ # Rip UI - @rip-lang/ui
7
4
 
8
- Components are plain `.rip` source files — no build step. The browser compiles
9
- them on the fly.
5
+ > **Unified UI system for Rip.**
10
6
 
11
- ---
7
+ `@rip-lang/ui` is the umbrella package for:
8
+ - `@rip-lang/ui/browser` — headless interactive browser widgets
9
+ - `@rip-lang/ui/email` — curated PascalCase email components
10
+ - `@rip-lang/ui/shared` — shared render/styling helpers
11
+ - `@rip-lang/ui/tailwind` — Tailwind compiler/inlining integration
12
12
 
13
- ## Quick Start
14
-
15
- Add the components directory to your serve middleware:
16
-
17
- ```coffee
18
- use serve
19
- dir: dir
20
- bundle: ['components', '../../../packages/ui']
21
- ```
22
-
23
- All widgets become available by name (`Select`, `Dialog`, `Grid`, etc.) in the
24
- shared scope — no imports needed.
25
-
26
- ```bash
27
- cd packages/ui
28
- rip server
29
- ```
30
-
31
- For browser quality checks and overlay regression tests, see
32
- [TESTING.md](TESTING.md).
33
-
34
- Every widget:
35
- - Handles all keyboard interactions per WAI-ARIA Authoring Practices
36
- - Sets correct ARIA attributes automatically
37
- - Exposes state via `$` sigil (`$open`, `$selected`) — style with Tailwind's `data-[attr]:` variants
38
- - Ships no CSS — you bring Tailwind classes
39
- - Uses Rip's reactive primitives for all state management
40
-
41
- ---
42
-
43
- ## Rip in 60 Seconds
44
-
45
- If you're coming from React or another framework, here's the Rip you need
46
- to know to use these widgets:
47
-
48
- | Syntax | Name | What It Does |
49
- |--------|------|-------------|
50
- | `:=` | State | `count := 0` — reactive state (like `useState`) |
51
- | `~=` | Computed | `doubled ~= count * 2` — derived value (like `useMemo`, but auto-tracked) |
52
- | `~>` | Effect | `~> document.title = "#{count}"` — side effect (like `useEffect`, but auto-tracked) |
53
- | `<=>` | Bind | `value <=> @name` — two-way binding between parent and child |
54
- | `@prop` | Prop | `@checked`, `@disabled` — component props (reactive) |
55
- | `$attr` | Data attr | `$open`, `$selected` — compiles to `data-open`, `data-selected` in HTML |
56
- | `@emit` | Event | `@emit 'change', value` — dispatches a CustomEvent |
57
- | `ref:` | DOM ref | `ref: "_panel"` — saves DOM element reference |
58
- | `slot` | Children | Projects parent-provided content into the component |
59
- | `offer` / `accept` | Context | Share reactive state between ancestor and descendant components |
60
- | `::` | Type | `@variant:: string := "default"` — typed prop (enables IDE completions + diagnostics) |
61
-
62
- Two-way binding example — React vs Rip:
63
-
64
- ```coffee
65
- # React: 4 lines per binding
66
- const [show, setShow] = useState(false)
67
- <Dialog open={show} onOpenChange={setShow} />
68
- const [name, setName] = useState('')
69
- <input value={name} onChange={e => setName(e.target.value)} />
70
-
71
- # Rip: 1 line per binding
72
- Dialog open <=> show
73
- input value <=> @name
74
- ```
75
-
76
- ---
77
-
78
- ## Why Rip UI
79
-
80
- | | ShadCN / Radix | Rip UI |
81
- |--|---------------|--------|
82
- | Runtime dependency | React (~42KB gz) + ReactDOM | None |
83
- | Component count | ~40 | 54 |
84
- | Total source | ShadCN wrappers (~3K LOC) atop Radix (~20K+ LOC) | 5,191 SLOC — everything included |
85
- | Build step | Required (Next.js, Vite, etc.) | None — browser compiles `.rip` source |
86
- | Styling | Pre-wired Tailwind (ShadCN) or unstyled (Radix) | Headless — `data-*` contract, styled with Tailwind |
87
- | Controlled components | `value` + `onChange` callback pair | `<=>` two-way binding |
88
- | Shared state | React Context + Provider wrappers | `offer` / `accept` keywords |
89
- | Reactivity | `useState` + `useEffect` + dependency arrays | `:=` / `~=` / `~>` — language-level |
90
- | Virtual DOM | Yes (diffing on every render) | No — fine-grained updates to exact nodes |
91
- | Data grid | Not included | 901 SLOC — 100K+ rows at 60fps |
92
-
93
- ### Architecture
94
-
95
- **Fine-grained reactivity.** When `count` changes, only the text node
96
- displaying `count` updates. No tree diffing, no wasted renders, no
97
- memoization needed. Same model as SolidJS and Svelte 5's runes, but built
98
- into the language.
99
-
100
- **Components compile to JavaScript.** The `component` keyword, `render`
101
- block, and reactive operators resolve at compile time into ES2022 classes
102
- with direct DOM operations. Source maps point back to `.rip` source for
103
- debugging.
104
-
105
- **No build pipeline.** The browser loads the Rip compiler (~50KB) once and
106
- compiles `.rip` files on the fly. For production, pre-compile. For
107
- development, save and see — SSE-based hot reload.
108
-
109
- **Source as distribution.** Components are served as `.rip` source files.
110
- Read them, understand them, modify them.
111
-
112
- ### Component Primitives
113
-
114
- | Capability | React | Rip |
115
- |-----------|-------|-----|
116
- | Child projection | No equivalent | `slot` |
117
- | DOM ownership | Virtual DOM abstraction | Direct DOM + `ref:` |
118
- | State sharing | Context + Provider wrappers | `offer` / `accept` |
119
- | Two-way binding | `value` + `onChange` pair | `<=>` operator |
120
- | Reactivity | Hooks + dependency arrays | `:=` / `~=` / `~>` |
121
-
122
- ---
123
-
124
- ## Styling with Tailwind
125
-
126
- Widgets are headless — they ship no CSS. Each widget exposes semantic state
127
- through `$` attributes that compile to `data-*` in HTML. Style them with
128
- Tailwind's data attribute variants.
129
-
130
- ### Setup
131
-
132
- ```html
133
- <script src="https://cdn.tailwindcss.com"></script>
134
- ```
135
-
136
- ### The `data-*` Contract
137
-
138
- Widgets set `data-*` attributes to reflect their state. Tailwind targets
139
- these with `data-[attr]:` variants:
140
-
141
- ```coffee
142
- # Widget source — behavior only, zero styling
143
- button $open: open?!, $disabled: @disabled?!
144
- div $highlighted: (idx is highlightedIndex)?!
145
- div $selected: (@value is current)?!
146
- ```
147
-
148
- ```html
149
- <!-- Your markup — Tailwind classes -->
150
- <button class="border border-gray-300 rounded-lg px-4 py-2
151
- data-[open]:border-blue-500 data-[open]:ring-2 data-[open]:ring-blue-200
152
- data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed">
153
- ```
154
-
155
- ### Common Patterns
156
-
157
- **Button:**
158
-
159
- ```html
160
- <button class="inline-flex items-center gap-2 px-4 py-2 rounded-lg
161
- bg-blue-600 text-white font-medium
162
- hover:bg-blue-700 active:scale-[0.98] transition
163
- data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed">
164
- ```
165
-
166
- **Select trigger:**
167
-
168
- ```html
169
- <button class="inline-flex items-center justify-between w-full px-3 py-2
170
- border border-gray-300 rounded-lg bg-white
171
- data-[open]:border-blue-500 data-[open]:ring-2 data-[open]:ring-blue-200">
172
- ```
173
-
174
- **Select option:**
175
-
176
- ```html
177
- <div class="px-3 py-2 rounded cursor-pointer
178
- data-[highlighted]:bg-gray-100
179
- data-[selected]:font-semibold data-[selected]:text-blue-600">
180
- ```
181
-
182
- **Dialog overlay:**
183
-
184
- ```html
185
- <div class="fixed inset-0 bg-black/50 flex items-center justify-center">
186
- <div class="bg-white rounded-xl shadow-xl p-6 max-w-md w-full">
187
- ```
188
-
189
- ### Dark Mode
190
-
191
- Use Tailwind's `dark:` variant with a class-based toggle:
192
-
193
- ```html
194
- <html class="dark">
195
- <!-- dark:bg-gray-900 dark:text-gray-100 etc. -->
196
- ```
197
-
198
- Or define semantic color tokens in your Tailwind config and reference them
199
- throughout — `bg-surface`, `text-primary`, `border-muted` — so dark mode
200
- is a single token swap, not per-element `dark:` classes.
201
-
202
- ---
203
-
204
- ## Code Density
205
-
206
- ### Checkbox — 18 Lines
207
-
208
- ```coffee
209
- export Checkbox = component
210
- @checked := false
211
- @disabled := false
212
- @indeterminate := false
213
- @switch := false
214
-
215
- onClick: ->
216
- return if @disabled
217
- @indeterminate = false
218
- @checked = not @checked
219
- @emit 'change', @checked
220
-
221
- render
222
- button role: @switch ? 'switch' : 'checkbox'
223
- aria-checked: @indeterminate ? 'mixed' : !!@checked
224
- aria-disabled: @disabled?!
225
- $checked: @checked?!
226
- $indeterminate: @indeterminate?!
227
- $disabled: @disabled?!
228
- slot
229
- ```
230
-
231
- Full ARIA. Checkbox and switch modes. Indeterminate state. Data attributes
232
- for styling. 18 lines, complete.
233
-
234
- ### Dialog — Effect-Based Lifecycle
235
-
236
- Focus trap, scroll lock, escape dismiss, click-outside dismiss, focus
237
- restore — all in one reactive effect with automatic cleanup:
13
+ ## Import Paths
238
14
 
239
15
  ```coffee
240
- ~>
241
- if @open
242
- _prevFocus = document.activeElement
243
- # lock scroll, trap focus, wire ARIA ...
244
- return ->
245
- # cleanup runs automatically when @open becomes false
16
+ import { Select, Dialog, Tabs } from '@rip-lang/ui/browser'
17
+ import { toHTML, Email, Head, Body, Text, Button } from '@rip-lang/ui/email'
18
+ import { compile, inlineEmailTree, generateBrowserCss } from '@rip-lang/ui/tailwind'
246
19
  ```
247
20
 
248
- No `useEffect`. No dependency array. No cleanup that captures stale state.
249
-
250
- ### Grid — 901 Lines
251
-
252
- No equivalent in ShadCN, Radix, or Headless UI. Virtual scrolling, DOM
253
- recycling, Sheets-style selection, full keyboard nav, inline editing,
254
- multi-column sort, column resizing, clipboard (Ctrl+C/V/X as TSV — interop
255
- with Excel, Google Sheets, Numbers). 901 lines vs 50,000+ for AG Grid.
256
-
257
- ---
258
-
259
- ## Component Overview
260
-
261
- 54 headless components across 10 categories — 5,191 lines total.
262
-
263
- ### Selection
264
-
265
- | Widget | Description | Key Props | Events |
266
- |--------|-------------|-----------|--------|
267
- | **Select** | Dropdown with typeahead, ARIA listbox | `@value`, `@placeholder`, `@disabled` | `@change` |
268
- | **Combobox** | Filterable input + listbox | `@query`, `@placeholder`, `@disabled` | `@select`, `@filter` |
269
- | **MultiSelect** | Multi-select with chips and filtering | `@value`, `@query`, `@placeholder` | `@change` |
270
- | **Autocomplete** | Type to filter, select to fill | `@value`, `@query`, `@placeholder` | `@change` |
271
-
272
- ### Toggle
273
-
274
- | Widget | Description | Key Props | Events |
275
- |--------|-------------|-----------|--------|
276
- | **Checkbox** | Toggle with checkbox/switch semantics | `@checked`, `@disabled`, `@switch` | `@change` |
277
- | **Toggle** | Two-state toggle button | `@pressed`, `@disabled` | `@change` |
278
- | **ToggleGroup** | Single or multi-select toggles | `@value`, `@multiple` | `@change` |
279
- | **RadioGroup** | Exactly one selected, arrow nav | `@value`, `@disabled` | `@change` |
280
- | **CheckboxGroup** | Multiple checked independently | `@value`, `@disabled` | `@change` |
281
-
282
- ### Input
283
-
284
- | Widget | Description | Key Props | Events |
285
- |--------|-------------|-----------|--------|
286
- | **Input** | Focus, touch, and validation tracking | `@value`, `@type`, `@placeholder` | `@change` |
287
- | **Textarea** | Auto-resizing text area | `@value`, `@autoResize`, `@rows` | `@change` |
288
- | **NumberField** | Stepper buttons, hold-to-repeat | `@value`, `@min`, `@max`, `@step` | `@change` |
289
- | **Slider** | Drag with pointer capture + keyboard | `@value`, `@min`, `@max`, `@step` | `@change` |
290
- | **OTPField** | Multi-digit code, auto-advance + paste | `@value`, `@length` | `@complete` |
291
- | **DatePicker** | Calendar dropdown, single or range | `@value`, `@min`, `@max`, `@range` | `@change` |
292
- | **EditableValue** | Click-to-edit inline value | `@value`, `@placeholder` | `@change` |
293
- | **NativeSelect** | Styled native `<select>` wrapper | `@value`, `@disabled` | `@change` |
294
- | **InputGroup** | Input with prefix/suffix addons | `@disabled` | — |
295
-
296
- ### Navigation
297
-
298
- | Widget | Description | Key Props | Events |
299
- |--------|-------------|-----------|--------|
300
- | **Tabs** | Arrow key nav, roving tabindex | `@active`, `@orientation` | `@change` |
301
- | **Menu** | Dropdown action menu | `@disabled` | `@select` |
302
- | **ContextMenu** | Right-click context menu | `@disabled` | `@select` |
303
- | **Menubar** | Horizontal menu bar with dropdowns | — | `@select` |
304
- | **NavMenu** | Site nav with hover/click panels | — | — |
305
- | **Toolbar** | Grouped controls, roving tabindex | `@orientation`, `@label` | — |
306
- | **Breadcrumb** | Navigation trail with separator | `@separator`, `@label` | — |
307
-
308
- ### Overlay
309
-
310
- | Widget | Description | Key Props | Events |
311
- |--------|-------------|-----------|--------|
312
- | **Dialog** | Focus trap, scroll lock, ARIA modal | `@open` | `@close` |
313
- | **AlertDialog** | Non-dismissable modal | `@open`, `@initialFocus` | `@close` |
314
- | **Drawer** | Slide-out panel with focus trap | `@open`, `@side` | `@close` |
315
- | **Popover** | Anchored floating with flip/shift | `@placement`, `@offset` | — |
316
- | **Tooltip** | Hover/focus with delay | `@text`, `@placement`, `@delay` | — |
317
- | **PreviewCard** | Hover/focus preview card | `@delay`, `@placement` | — |
318
- | **Toast** | Auto-dismiss, ARIA live region | `@toast` (object) | `@dismiss` |
319
-
320
- ### Display
321
-
322
- | Widget | Description | Key Props |
323
- |--------|-------------|-----------|
324
- | **Button** | Disabled-but-focusable pattern | `@disabled` |
325
- | **Badge** | Inline label (solid/outline/subtle) | `@variant` |
326
- | **Card** | Container with header/content/footer | `@interactive` |
327
- | **Separator** | Decorative or semantic divider | `@orientation`, `@decorative` |
328
- | **Progress** | Progress bar via CSS custom prop | `@value`, `@max` |
329
- | **Meter** | Gauge with thresholds | `@value`, `@min`, `@max`, `@low`, `@high` |
330
- | **Spinner** | Loading indicator | `@label`, `@size` |
331
- | **Skeleton** | Loading placeholder with shimmer | `@width`, `@height`, `@circle` |
332
- | **Avatar** | Image with fallback to initials | `@src`, `@alt`, `@fallback` |
333
- | **Label** | Accessible form label | `@for`, `@required` |
334
- | **ScrollArea** | Custom scrollbar, draggable thumb | `@orientation` |
335
-
336
- ### Form
337
-
338
- | Widget | Description | Key Props |
339
- |--------|-------------|-----------|
340
- | **Field** | Label + description + error wrapper | `@label`, `@error`, `@required` |
341
- | **Fieldset** | Grouped fields with cascading disable | `@legend`, `@disabled` |
342
- | **Form** | Submit handling + validation state | `@onSubmit` |
343
- | **ButtonGroup** | Grouped buttons, ARIA semantics | `@orientation`, `@disabled` |
344
-
345
- ### Data
346
-
347
- | Widget | Description | Key Props |
348
- |--------|-------------|-----------|
349
- | **Grid** | Virtual scroll, 100K+ rows at 60fps | `@data`, `@columns`, `@rowHeight` |
350
- | **Accordion** | Expand/collapse, single or multiple | `@multiple` |
351
- | **Table** | Semantic table wrapper | `@caption`, `@striped` |
352
-
353
- ### Interactive
354
-
355
- | Widget | Description | Key Props | Events |
356
- |--------|-------------|-----------|--------|
357
- | **Collapsible** | Animated expand/collapse | `@open`, `@disabled` | `@change` |
358
- | **Pagination** | Page nav with ellipsis gaps | `@page`, `@total`, `@perPage` | `@change` |
359
- | **Carousel** | Slide with autoplay + loop | `@loop`, `@autoplay`, `@interval` | `@change` |
360
- | **Resizable** | Draggable resize handles | `@orientation`, `@minSize` | `@resize` |
361
-
362
- ---
363
-
364
- ## Widget Reference
365
-
366
- ### Select
21
+ The root package is intentionally minimal and does not flatten browser + email names into one namespace.
367
22
 
368
- ```coffee
369
- Select value <=> selectedRole, @change: handleChange
370
- option value: "eng", "Engineer"
371
- option value: "des", "Designer"
372
- option value: "mgr", "Manager"
373
- ```
374
-
375
- **Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close, Home/End, type-ahead
376
- **Data attributes:** `$open`, `$highlighted`, `$selected`, `$disabled`
377
-
378
- ### Combobox
379
-
380
- ```coffee
381
- Combobox query <=> searchText, @select: handleSelect, @filter: handleFilter
382
- for item in filteredItems
383
- div $value: item.id
384
- span item.name
385
- ```
386
-
387
- **Keyboard:** ArrowDown/Up navigate, Enter select, Escape close/clear, Tab close
388
- **Data attributes:** `$open`, `$highlighted`
389
-
390
- ### Dialog
391
-
392
- ```coffee
393
- Dialog open <=> showDialog, @close: handleClose
394
- h2 "Confirm Action"
395
- p "Are you sure?"
396
- button @click: (=> showDialog = false), "Cancel"
397
- button @click: handleConfirm, "Confirm"
398
- ```
399
-
400
- **Keyboard:** Escape to close, Tab trapped within dialog
401
- **Data attributes:** `$open`
402
- **Behavior:** Focus trap, body scroll lock, focus restore on close
403
-
404
- ### AlertDialog
405
-
406
- ```coffee
407
- AlertDialog open <=> showConfirm
408
- h2 "Delete account?"
409
- p "This action cannot be undone."
410
- button @click: (=> showConfirm = false), "Cancel"
411
- button @click: handleDelete, "Delete"
412
- ```
413
-
414
- Like Dialog but cannot be closed by Escape or click outside.
415
- **ARIA:** `role="alertdialog"`, auto-wired `aria-labelledby`/`aria-describedby`
416
-
417
- ### Toast
418
-
419
- ```coffee
420
- toasts := []
421
- toasts = [...toasts, { message: "Saved!", type: "success" }]
422
- toasts = toasts.filter (t) -> t isnt target
423
- ToastViewport toasts <=> toasts
424
- ```
425
-
426
- **Props:** `@toasts`, `@placement` (bottom-right, top-right, etc.)
427
- **Per-toast:** `message`, `type`, `duration` (default 4000ms), `title`, `action`
428
- **Data attributes:** `$type`, `$leaving`
429
- **Behavior:** Timer pauses on hover, resumes on leave
430
-
431
- ### Tabs
432
-
433
- ```coffee
434
- Tabs active <=> currentTab
435
- div $tab: "one", "Tab One"
436
- div $tab: "two", "Tab Two"
437
- div $panel: "one"
438
- p "Content for tab one"
439
- div $panel: "two"
440
- p "Content for tab two"
441
- ```
442
-
443
- **Keyboard:** ArrowLeft/Right navigate, Home/End jump
444
- **Data attributes:** `$active`
445
-
446
- ### Accordion
447
-
448
- ```coffee
449
- Accordion multiple: false
450
- div $item: "a"
451
- button $trigger: true, "Section A"
452
- div $content: true
453
- p "Content A"
454
- ```
455
-
456
- **Keyboard:** Enter/Space toggle, ArrowDown/Up between triggers, Home/End
457
- **Methods:** `toggle(id)`, `isOpen(id)`
458
-
459
- ### Checkbox
460
-
461
- ```coffee
462
- Checkbox checked <=> isActive, @change: handleChange
463
- span "Enable notifications"
464
-
465
- Checkbox checked <=> isDark, switch: true
466
- span "Dark mode"
467
- ```
468
-
469
- **ARIA:** `role="checkbox"` or `role="switch"`, `aria-checked` (true/false/mixed)
470
- **Data attributes:** `$checked`, `$indeterminate`, `$disabled`
471
-
472
- ### Menu
473
-
474
- ```coffee
475
- Menu @select: handleAction
476
- button $trigger: true, "Actions"
477
- div $item: "edit", "Edit"
478
- div $item: "delete", "Delete"
479
- ```
480
-
481
- **Keyboard:** ArrowDown/Up navigate, Enter/Space select, Escape close
482
- **Data attributes:** `$open`, `$highlighted`
483
-
484
- ### Popover
485
-
486
- ```coffee
487
- Popover placement: "bottom-start"
488
- button "Options"
489
- div
490
- p "Popover content here"
491
- ```
492
-
493
- **Keyboard:** Enter/Space/ArrowDown toggle, Escape close
494
- **Data attributes:** `$open`, `$placement`
495
-
496
- ### Tooltip
497
-
498
- ```coffee
499
- Tooltip text: "Save your changes", placement: "top"
500
- button "Save"
501
- ```
502
-
503
- **Data attributes:** `$open`, `$entering`, `$exiting`, `$placement`
504
- **Behavior:** Shows after delay on hover/focus, uses `aria-describedby`
505
-
506
- ### Grid
507
-
508
- ```coffee
509
- Grid
510
- data: employees
511
- columns: [
512
- { key: 'name', title: 'Name', width: 200 }
513
- { key: 'age', title: 'Age', width: 80, align: 'right' }
514
- { key: 'role', title: 'Role', width: 150, type: 'select', source: roles }
515
- { key: 'active', title: 'Active', width: 60, type: 'checkbox' }
516
- ]
517
- rowHeight: 32
518
- striped: true
519
- ```
520
-
521
- **Column types:** `text`, `number`, `checkbox`, `select`
522
- **Methods:** `getCell`, `setCell`, `getData`, `setData`, `sort`, `scrollToRow`, `copySelection`, `cutSelection`, `pasteAtActive`
523
- **Keyboard:** Arrows, Tab, Enter/F2 edit, Escape cancel, Ctrl+arrows jump, PageUp/Down, Ctrl+A, Ctrl+C/V/X, Delete, Space (checkboxes), type-to-edit
524
- **Sorting:** Click header (asc/desc/none), Shift+click for multi-column
525
- **Clipboard:** TSV format — interop with Excel, Sheets, Numbers
526
- **Data attributes:** `$active`, `$selected`, `$sorted`, `$editing`, `$selecting`
527
-
528
- ### Collapsible
529
-
530
- ```coffee
531
- Collapsible open <=> isOpen
532
- button $trigger: true, "Show details"
533
- div $content: true
534
- p "Hidden content here"
535
- ```
536
-
537
- **Methods:** `toggle()`
538
- **Data attributes:** `$open`, `$disabled`
539
- **CSS custom properties:** `--collapsible-height`, `--collapsible-width`
540
-
541
- ### Pagination
542
-
543
- ```coffee
544
- Pagination page <=> currentPage, total: 100, perPage: 10
545
- ```
546
-
547
- **Keyboard:** ArrowLeft/Right, Home/End
548
- **Data attributes:** `$active`, `$disabled`, `$ellipsis`
549
-
550
- ### Carousel
551
-
552
- ```coffee
553
- Carousel loop: true
554
- div $slide: true, "Slide 1"
555
- div $slide: true, "Slide 2"
556
- div $slide: true, "Slide 3"
557
- ```
558
-
559
- **Methods:** `goto(index)`, `next()`, `prev()`
560
- **Behavior:** Autoplay pauses on hover
561
-
562
- ### Drawer
563
-
564
- ```coffee
565
- Drawer open <=> showDrawer, side: "left"
566
- nav "Sidebar content"
567
- ```
568
-
569
- **Props:** `@open`, `@side` (top/right/bottom/left), `@dismissable`
570
- **Behavior:** Focus trap, scroll lock, Escape to close
571
-
572
- ### Breadcrumb
573
-
574
- ```coffee
575
- Breadcrumb
576
- a $item: true, href: "/", "Home"
577
- a $item: true, href: "/products", "Products"
578
- span $item: true, "Widget Pro"
579
- ```
580
-
581
- **ARIA:** `aria-current="page"` on last item
582
-
583
- ### Resizable
584
-
585
- ```coffee
586
- Resizable
587
- div $panel: true, "Left"
588
- div $panel: true, "Right"
589
- ```
590
-
591
- **ARIA:** `role="separator"` on handles
592
- **CSS custom properties:** `--panel-size` on each panel
593
-
594
- ### Context Sharing: `offer` / `accept`
595
-
596
- For compound components where descendants need shared state:
597
-
598
- ```coffee
599
- # Parent offers reactive state to all descendants
600
- export Tabs = component
601
- offer active := 'overview'
602
-
603
- # Child accepts the shared signal
604
- export TabContent = component
605
- accept active
606
- render
607
- div hidden: active isnt @value
608
- slot
609
- ```
23
+ ## Domains
610
24
 
611
- Parent and child share the same reactive object — mutations in either
612
- direction are instantly visible. No Provider wrappers, no string keys.
25
+ ### Browser
613
26
 
614
- ---
27
+ Headless, accessible browser widgets authored in Rip. They remain source-first and expose state via `$` / `data-*` attributes for external styling.
615
28
 
616
- ## File Summary
29
+ ### Email
617
30
 
618
- | Category | Files | Lines |
619
- |----------|-------|-------|
620
- | Selection | 4 | 638 |
621
- | Toggle | 5 | 267 |
622
- | Input | 9 | 854 |
623
- | Navigation | 7 | 767 |
624
- | Overlay | 7 | 700 |
625
- | Display | 11 | 378 |
626
- | Form | 4 | 140 |
627
- | Data | 3 | 1,041 |
628
- | Interactive | 4 | 406 |
629
- | **Total** | **54** | **5,191** |
31
+ Curated PascalCase email components with `toHTML`, `toText`, and `renderEmail` helpers, built on a server-side DOM shim and serializer.
630
32
 
631
- ---
33
+ ### Shared
632
34
 
633
- ## Status
35
+ Cross-domain style and utility helpers such as `joinStyles()` and `withMargin()`.
634
36
 
635
- The reactive model, headless contract, and performance architecture are
636
- proven. The compiler has 1,436 tests. The widget suite is comprehensive
637
- but still maturing — tests are being added, and a few widgets have known
638
- structural issues being resolved (see [CONTRIBUTING.md](CONTRIBUTING.md)
639
- for details).
37
+ ### Tailwind
640
38
 
641
- For widget authoring patterns, implementation notes, known issues, and the
642
- development roadmap, see **[CONTRIBUTING.md](CONTRIBUTING.md)**.
39
+ The one dependency-bearing domain. It invokes the real Tailwind compiler and provides CSS generation for browser use and CSS injection/inlining helpers for email.