@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 +587 -137
- package/accordion.rip +113 -0
- package/autocomplete.rip +141 -0
- package/avatar.rip +37 -0
- package/button.rip +23 -0
- package/checkbox-group.rip +65 -0
- package/checkbox.rip +33 -0
- package/combobox.rip +153 -0
- package/context-menu.rip +98 -0
- package/date-picker.rip +214 -0
- package/dialog.rip +107 -0
- package/drawer.rip +79 -0
- package/editable-value.rip +80 -0
- package/field.rip +53 -0
- package/fieldset.rip +22 -0
- package/form.rip +39 -0
- package/grid.rip +901 -0
- package/index.rip +15 -0
- package/input.rip +35 -0
- package/menu.rip +162 -0
- package/menubar.rip +155 -0
- package/meter.rip +36 -0
- package/multi-select.rip +158 -0
- package/nav-menu.rip +132 -0
- package/number-field.rip +162 -0
- package/otp-field.rip +89 -0
- package/package.json +16 -26
- package/popover.rip +143 -0
- package/preview-card.rip +73 -0
- package/progress.rip +25 -0
- package/radio-group.rip +67 -0
- package/scroll-area.rip +145 -0
- package/select.rip +184 -0
- package/separator.rip +17 -0
- package/slider.rip +165 -0
- package/tabs.rip +124 -0
- package/toast.rip +88 -0
- package/toggle-group.rip +78 -0
- package/toggle.rip +24 -0
- package/toolbar.rip +46 -0
- package/tooltip.rip +115 -0
- package/serve.rip +0 -140
- package/ui.rip +0 -935
package/README.md
CHANGED
|
@@ -1,208 +1,658 @@
|
|
|
1
|
-
|
|
1
|
+
# Rip UI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
127
|
+
### Popover
|
|
11
128
|
|
|
12
|
-
|
|
129
|
+
Floating content anchored to a trigger element. Positions itself with
|
|
130
|
+
flip/shift to stay in viewport.
|
|
13
131
|
|
|
14
132
|
```coffee
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
div
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
+
Toggle with checkbox or switch semantics. Supports indeterminate state.
|
|
49
196
|
|
|
50
|
-
|
|
197
|
+
```coffee
|
|
198
|
+
Checkbox checked <=> isActive, @change: handleChange
|
|
199
|
+
span "Enable notifications"
|
|
51
200
|
|
|
52
|
-
|
|
53
|
-
|
|
201
|
+
Checkbox checked <=> isDark, switch: true
|
|
202
|
+
span "Dark mode"
|
|
203
|
+
```
|
|
54
204
|
|
|
55
|
-
|
|
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
|
-
|
|
211
|
+
### Menu
|
|
58
212
|
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
+
Or import everything:
|
|
78
328
|
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
387
|
+
**Cascade Layers** — control specificity:
|
|
93
388
|
|
|
94
|
-
|
|
95
|
-
|
|
389
|
+
```css
|
|
390
|
+
@layer base, components, overrides;
|
|
96
391
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
421
|
+
### Dark Mode
|
|
122
422
|
|
|
123
|
-
|
|
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
|
-
|
|
425
|
+
```css
|
|
426
|
+
:root {
|
|
427
|
+
color-scheme: light dark;
|
|
128
428
|
|
|
129
|
-
|
|
130
|
-
|
|
429
|
+
--surface-1: var(--gray-0);
|
|
430
|
+
--surface-2: var(--gray-1);
|
|
431
|
+
--color-text: var(--gray-9);
|
|
432
|
+
}
|
|
131
433
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
453
|
+
```js
|
|
454
|
+
document.documentElement.dataset.theme = 'dark'
|
|
455
|
+
```
|
|
146
456
|
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
480
|
+
.ghost {
|
|
481
|
+
background: transparent;
|
|
482
|
+
color: var(--color-primary);
|
|
152
483
|
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
512
|
+
**Card:**
|
|
163
513
|
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
537
|
+
**Dialog:**
|
|
173
538
|
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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** | |
|