rip-lang 3.13.26 → 3.13.28
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 +25 -24
- package/bin/rip +29 -18
- package/docs/RIP-LANG.md +259 -4
- package/docs/RIP-TYPES.md +1 -1
- package/docs/dist/rip.js +414 -269
- package/docs/dist/rip.min.js +359 -178
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +1 -1
- package/src/app.rip +1 -1
- package/src/compiler.js +28 -2
- package/src/components.js +25 -13
- package/src/grammar/grammar.rip +6 -3
- package/src/lexer.js +15 -10
- package/src/parser.js +28 -27
- package/docs/RIP-INTERNALS.md +0 -593
- package/docs/WEB-FRAMEWORKS.md +0 -486
- package/docs/WEB-STYLING.md +0 -701
package/docs/WEB-STYLING.md
DELETED
|
@@ -1,701 +0,0 @@
|
|
|
1
|
-
# Styling & Components Guide
|
|
2
|
-
|
|
3
|
-
This document defines the styling architecture and component strategy for Rip
|
|
4
|
-
projects. These choices reflect Rip's core principles: elegance, minimalism,
|
|
5
|
-
zero dependencies, and letting the platform do the work.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Philosophy
|
|
10
|
-
|
|
11
|
-
Good styling follows the same rules as good code: say what you mean, don't
|
|
12
|
-
repeat yourself, and don't import machinery you don't need. CSS is a real
|
|
13
|
-
language. Modern CSS — with nesting, custom properties, cascade layers, and
|
|
14
|
-
container queries — is expressive enough to build any interface without
|
|
15
|
-
preprocessors, runtimes, or utility class vocabularies.
|
|
16
|
-
|
|
17
|
-
For interactive behavior — keyboard navigation, focus management, ARIA,
|
|
18
|
-
dismissal, positioning — we build our own headless components in Rip. The
|
|
19
|
-
patterns come from the WAI-ARIA Authoring Practices spec and Base UI's
|
|
20
|
-
reference implementations. The code is pure Rip, using the language's own
|
|
21
|
-
reactive primitives. Zero framework dependencies.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## The Stack
|
|
26
|
-
|
|
27
|
-
| Layer | Tool | Role |
|
|
28
|
-
|-------|------|------|
|
|
29
|
-
| **Behavior** | Rip Widgets | Accessible headless components — keyboard nav, ARIA, focus management |
|
|
30
|
-
| **Design Tokens** | Open Props | Consistent scales for spacing, color, shadow, radius, easing, typography |
|
|
31
|
-
| **Scoping** | CSS (scoped) | Component-scoped styles via CSS Modules or Rip UI's built-in scoping |
|
|
32
|
-
| **Platform** | Native CSS | Nesting, `@layer`, `data-*` selectors, `prefers-color-scheme` |
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Rip Widgets — Native Headless Components
|
|
37
|
-
|
|
38
|
-
Rip provides its own headless, accessible interactive components. These are
|
|
39
|
-
written in Rip using the language's reactive primitives (`:=`, `~=`, `~>`)
|
|
40
|
-
and compiled to JavaScript like everything else. No React. No framework
|
|
41
|
-
runtime. Just Rip.
|
|
42
|
-
|
|
43
|
-
### Why We Build Our Own
|
|
44
|
-
|
|
45
|
-
Base UI is the industry's best headless component library. But it requires
|
|
46
|
-
React — hooks, context, synthetic events, a virtual DOM reconciler. Shipping
|
|
47
|
-
React as a dependency contradicts Rip's zero-dependency philosophy, and
|
|
48
|
-
React's rendering model is fundamentally different from Rip UI's fine-grained
|
|
49
|
-
DOM updates.
|
|
50
|
-
|
|
51
|
-
Instead, we reimplement Base UI's proven behavioral patterns directly in Rip.
|
|
52
|
-
This is the same approach Rip took with CoffeeScript's syntax (reimplemented
|
|
53
|
-
better) and React's reactivity model (reimplemented as language-level
|
|
54
|
-
operators). The patterns are documented. The code is ours.
|
|
55
|
-
|
|
56
|
-
### How Rip's Primitives Map to Component Behavior
|
|
57
|
-
|
|
58
|
-
| Need | React (Base UI) | Rip |
|
|
59
|
-
|------|----------------|-----|
|
|
60
|
-
| Mutable state | `useState` | `:=` (reactive state) |
|
|
61
|
-
| Derived values | `useMemo` | `~=` (computed) |
|
|
62
|
-
| Side effects + cleanup | `useEffect` | `~>` (effect with cleanup) |
|
|
63
|
-
| DOM references | `useRef` | Direct DOM access — components own their elements |
|
|
64
|
-
| Shared state (compound components) | React Context | Component props / shared stash |
|
|
65
|
-
| Batched updates | `unstable_batchedUpdates` | `__batch` |
|
|
66
|
-
| Events | Synthetic event system | Native DOM events |
|
|
67
|
-
|
|
68
|
-
Rip's model is simpler. Fine-grained reactivity means no virtual DOM diffing,
|
|
69
|
-
no hook ordering rules, no dependency arrays. An effect that returns a function
|
|
70
|
-
automatically cleans up. State changes propagate to exactly the DOM nodes that
|
|
71
|
-
depend on them.
|
|
72
|
-
|
|
73
|
-
### The Components
|
|
74
|
-
|
|
75
|
-
These 10 components cover ~90% of real application needs:
|
|
76
|
-
|
|
77
|
-
| Component | What It Handles |
|
|
78
|
-
|-----------|----------------|
|
|
79
|
-
| **Dialog** | Focus trap, scroll lock, escape/click-outside dismiss, ARIA roles |
|
|
80
|
-
| **Popover** | Anchor positioning, flip/shift, dismiss behavior, ARIA |
|
|
81
|
-
| **Tooltip** | Show/hide with delay, anchor positioning, ARIA describedby |
|
|
82
|
-
| **Select** | Keyboard navigation, typeahead, ARIA listbox, positioning |
|
|
83
|
-
| **Menu** | Nested submenus, keyboard navigation, ARIA menu roles |
|
|
84
|
-
| **Tabs** | Arrow key navigation, ARIA tablist/tab/tabpanel |
|
|
85
|
-
| **Accordion** | Expand/collapse, single or multiple, ARIA |
|
|
86
|
-
| **Checkbox/Switch** | Toggle state, indeterminate, ARIA checked |
|
|
87
|
-
| **Combobox** | Input filtering, keyboard nav, ARIA combobox, positioning |
|
|
88
|
-
| **Toast** | Auto-dismiss timer, stacking, ARIA live region |
|
|
89
|
-
|
|
90
|
-
Each component:
|
|
91
|
-
- Handles all keyboard interactions per WAI-ARIA Authoring Practices
|
|
92
|
-
- Sets correct ARIA attributes automatically
|
|
93
|
-
- Exposes `data-*` attributes for CSS styling (`[data-open]`, `[data-selected]`, etc.)
|
|
94
|
-
- Ships zero CSS — styling is entirely in the user's stylesheets
|
|
95
|
-
- Uses Rip's reactive primitives for all state management
|
|
96
|
-
|
|
97
|
-
### Behavioral Primitives
|
|
98
|
-
|
|
99
|
-
The components are built from a small set of shared behavioral primitives:
|
|
100
|
-
|
|
101
|
-
**Focus Trap** — confines tab focus within a container (dialogs, modals):
|
|
102
|
-
|
|
103
|
-
```coffee
|
|
104
|
-
trapFocus = (el) ->
|
|
105
|
-
focusable = el.querySelectorAll 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
|
|
106
|
-
first = focusable[0]
|
|
107
|
-
last = focusable[focusable.length - 1]
|
|
108
|
-
first?.focus()
|
|
109
|
-
handler = (e) ->
|
|
110
|
-
return unless e.key is 'Tab'
|
|
111
|
-
if e.shiftKey
|
|
112
|
-
if document.activeElement is first
|
|
113
|
-
e.preventDefault()
|
|
114
|
-
last?.focus()
|
|
115
|
-
else
|
|
116
|
-
if document.activeElement is last
|
|
117
|
-
e.preventDefault()
|
|
118
|
-
first?.focus()
|
|
119
|
-
el.addEventListener 'keydown', handler
|
|
120
|
-
-> el.removeEventListener 'keydown', handler
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
**Scroll Lock** — prevents body scroll while a modal is open:
|
|
124
|
-
|
|
125
|
-
```coffee
|
|
126
|
-
lockScroll = ->
|
|
127
|
-
scrollY = window.scrollY
|
|
128
|
-
document.body.style.position = 'fixed'
|
|
129
|
-
document.body.style.top = "-#{scrollY}px"
|
|
130
|
-
document.body.style.width = '100%'
|
|
131
|
-
->
|
|
132
|
-
document.body.style.position = ''
|
|
133
|
-
document.body.style.top = ''
|
|
134
|
-
document.body.style.width = ''
|
|
135
|
-
window.scrollTo 0, scrollY
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
**Dismiss** — close on Escape key or click outside:
|
|
139
|
-
|
|
140
|
-
```coffee
|
|
141
|
-
onDismiss = (el, close) ->
|
|
142
|
-
onKey = (e) -> close() if e.key is 'Escape'
|
|
143
|
-
onClick = (e) -> close() unless el.contains(e.target)
|
|
144
|
-
document.addEventListener 'keydown', onKey
|
|
145
|
-
document.addEventListener 'pointerdown', onClick
|
|
146
|
-
->
|
|
147
|
-
document.removeEventListener 'keydown', onKey
|
|
148
|
-
document.removeEventListener 'pointerdown', onClick
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
**Keyboard Navigation** — arrow key movement through a list of items:
|
|
152
|
-
|
|
153
|
-
```coffee
|
|
154
|
-
navigateList = (el, opts = {}) ->
|
|
155
|
-
vertical = opts.vertical ? true
|
|
156
|
-
wrap = opts.wrap ? true
|
|
157
|
-
items = -> el.querySelectorAll('[role="option"]:not([aria-disabled="true"]), [role="menuitem"]:not([aria-disabled="true"])')
|
|
158
|
-
|
|
159
|
-
handler = (e) ->
|
|
160
|
-
list = Array.from items()
|
|
161
|
-
idx = list.indexOf document.activeElement
|
|
162
|
-
return if idx is -1
|
|
163
|
-
|
|
164
|
-
next = switch e.key
|
|
165
|
-
when (if vertical then 'ArrowDown' else 'ArrowRight')
|
|
166
|
-
if wrap then (idx + 1) %% list.length else Math.min(idx + 1, list.length - 1)
|
|
167
|
-
when (if vertical then 'ArrowUp' else 'ArrowLeft')
|
|
168
|
-
if wrap then (idx - 1) %% list.length else Math.max(idx - 1, 0)
|
|
169
|
-
when 'Home' then 0
|
|
170
|
-
when 'End' then list.length - 1
|
|
171
|
-
else null
|
|
172
|
-
|
|
173
|
-
if next?
|
|
174
|
-
e.preventDefault()
|
|
175
|
-
list[next].focus()
|
|
176
|
-
|
|
177
|
-
el.addEventListener 'keydown', handler
|
|
178
|
-
-> el.removeEventListener 'keydown', handler
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**Anchor Positioning** — position a floating element relative to a trigger:
|
|
182
|
-
|
|
183
|
-
```coffee
|
|
184
|
-
anchorPosition = (anchor, floating, opts = {}) ->
|
|
185
|
-
placement = opts.placement or 'bottom'
|
|
186
|
-
offset = opts.offset or 4
|
|
187
|
-
|
|
188
|
-
update = ->
|
|
189
|
-
ar = anchor.getBoundingClientRect()
|
|
190
|
-
fr = floating.getBoundingClientRect()
|
|
191
|
-
|
|
192
|
-
[side, align] = placement.split('-')
|
|
193
|
-
x = switch side
|
|
194
|
-
when 'bottom', 'top'
|
|
195
|
-
switch align
|
|
196
|
-
when 'start' then ar.left
|
|
197
|
-
when 'end' then ar.right - fr.width
|
|
198
|
-
else ar.left + (ar.width - fr.width) / 2
|
|
199
|
-
when 'right' then ar.right + offset
|
|
200
|
-
when 'left' then ar.left - fr.width - offset
|
|
201
|
-
|
|
202
|
-
y = switch side
|
|
203
|
-
when 'bottom' then ar.bottom + offset
|
|
204
|
-
when 'top' then ar.top - fr.height - offset
|
|
205
|
-
when 'left', 'right'
|
|
206
|
-
switch align
|
|
207
|
-
when 'start' then ar.top
|
|
208
|
-
when 'end' then ar.bottom - fr.height
|
|
209
|
-
else ar.top + (ar.height - fr.height) / 2
|
|
210
|
-
|
|
211
|
-
# Flip if off screen
|
|
212
|
-
if side is 'bottom' and y + fr.height > window.innerHeight
|
|
213
|
-
y = ar.top - fr.height - offset
|
|
214
|
-
if side is 'top' and y < 0
|
|
215
|
-
y = ar.bottom + offset
|
|
216
|
-
|
|
217
|
-
# Shift to stay in viewport
|
|
218
|
-
x = Math.max(4, Math.min(x, window.innerWidth - fr.width - 4))
|
|
219
|
-
|
|
220
|
-
floating.style.left = "#{x}px"
|
|
221
|
-
floating.style.top = "#{y}px"
|
|
222
|
-
|
|
223
|
-
update()
|
|
224
|
-
-> null
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### Example: Dialog Component
|
|
228
|
-
|
|
229
|
-
A complete headless dialog in Rip, showing how the primitives compose:
|
|
230
|
-
|
|
231
|
-
```coffee
|
|
232
|
-
component Dialog
|
|
233
|
-
@open := false
|
|
234
|
-
|
|
235
|
-
~>
|
|
236
|
-
if @open
|
|
237
|
-
prevFocus = document.activeElement
|
|
238
|
-
trapFocus @el
|
|
239
|
-
lockScroll()
|
|
240
|
-
->
|
|
241
|
-
releaseScroll()
|
|
242
|
-
prevFocus?.focus()
|
|
243
|
-
|
|
244
|
-
onKeydown: (e) ->
|
|
245
|
-
@open = false if e.key is 'Escape'
|
|
246
|
-
|
|
247
|
-
onBackdropClick: (e) ->
|
|
248
|
-
@open = false if e.target is e.currentTarget
|
|
249
|
-
|
|
250
|
-
render
|
|
251
|
-
div.backdrop @click: @onBackdropClick, data-open: @open
|
|
252
|
-
div.panel role: "dialog", aria-modal: "true"
|
|
253
|
-
slot
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
That's the entire behavioral core — focus trap, scroll lock, escape dismiss,
|
|
257
|
-
click-outside dismiss, ARIA attributes — in ~20 lines of Rip. The effect
|
|
258
|
-
cleanup handles teardown automatically when `@open` becomes false.
|
|
259
|
-
|
|
260
|
-
### Example: Tabs Component
|
|
261
|
-
|
|
262
|
-
```coffee
|
|
263
|
-
component Tabs
|
|
264
|
-
@active := @items?[0]?.id or ''
|
|
265
|
-
|
|
266
|
-
select: (id) -> @active = id
|
|
267
|
-
|
|
268
|
-
onKeydown: (e) ->
|
|
269
|
-
ids = @items.map -> it.id
|
|
270
|
-
idx = ids.indexOf @active
|
|
271
|
-
switch e.key
|
|
272
|
-
when 'ArrowRight' then @active = ids[(idx + 1) %% ids.length]
|
|
273
|
-
when 'ArrowLeft' then @active = ids[(idx - 1) %% ids.length]
|
|
274
|
-
when 'Home' then @active = ids[0]
|
|
275
|
-
when 'End' then @active = ids[-1]
|
|
276
|
-
|
|
277
|
-
render
|
|
278
|
-
div role: "tablist", @keydown: @onKeydown
|
|
279
|
-
for item in @items
|
|
280
|
-
button role: "tab",
|
|
281
|
-
aria-selected: item.id is @active,
|
|
282
|
-
tabindex: (if item.id is @active then 0 else -1),
|
|
283
|
-
data-active: item.id is @active,
|
|
284
|
-
@click: -> @select item.id
|
|
285
|
-
item.label
|
|
286
|
-
for item in @items
|
|
287
|
-
div role: "tabpanel",
|
|
288
|
-
data-active: item.id is @active,
|
|
289
|
-
hidden: item.id isnt @active
|
|
290
|
-
item.content
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Reference Material
|
|
294
|
-
|
|
295
|
-
Component behavior patterns follow:
|
|
296
|
-
- **WAI-ARIA Authoring Practices** — https://www.w3.org/WAI/ARIA/apg/patterns/
|
|
297
|
-
- **Base UI source** (MIT) — https://github.com/mui/base-ui
|
|
298
|
-
- **MDN ARIA documentation** — https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA
|
|
299
|
-
|
|
300
|
-
---
|
|
301
|
-
|
|
302
|
-
## Open Props — Design Tokens
|
|
303
|
-
|
|
304
|
-
Open Props is a set of CSS custom properties — design tokens — covering spacing,
|
|
305
|
-
color, shadow, radius, easing, typography, and animation. It ships as pure CSS
|
|
306
|
-
(4KB), has no runtime, no build step, and no opinions about how you use it.
|
|
307
|
-
|
|
308
|
-
We use Open Props as our design token foundation. It provides the consistent
|
|
309
|
-
scales that prevent ad-hoc magic numbers from creeping into stylesheets.
|
|
310
|
-
|
|
311
|
-
Import what you need:
|
|
312
|
-
|
|
313
|
-
```css
|
|
314
|
-
@import "open-props/sizes";
|
|
315
|
-
@import "open-props/colors";
|
|
316
|
-
@import "open-props/shadows";
|
|
317
|
-
@import "open-props/radii";
|
|
318
|
-
@import "open-props/easings";
|
|
319
|
-
@import "open-props/fonts";
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
Or import everything at once:
|
|
323
|
-
|
|
324
|
-
```css
|
|
325
|
-
@import "open-props/style";
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Override or extend any token by redefining the custom property:
|
|
329
|
-
|
|
330
|
-
```css
|
|
331
|
-
:root {
|
|
332
|
-
--color-primary: oklch(55% 0.25 260);
|
|
333
|
-
--radius-card: var(--radius-3);
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Token Categories
|
|
338
|
-
|
|
339
|
-
**Spacing** — `--size-1` through `--size-15` (0.25rem to 7.5rem). Use for
|
|
340
|
-
padding, margin, and gap.
|
|
341
|
-
|
|
342
|
-
**Colors** — Full palettes (`--blue-0` through `--blue-12`, etc.) plus
|
|
343
|
-
semantic surface tokens. Define project-level aliases:
|
|
344
|
-
|
|
345
|
-
```css
|
|
346
|
-
:root {
|
|
347
|
-
--color-primary: var(--indigo-7);
|
|
348
|
-
--color-danger: var(--red-7);
|
|
349
|
-
--color-success: var(--green-7);
|
|
350
|
-
--color-text: var(--gray-9);
|
|
351
|
-
--color-text-muted: var(--gray-6);
|
|
352
|
-
--surface-1: var(--gray-0);
|
|
353
|
-
--surface-2: var(--gray-1);
|
|
354
|
-
--surface-3: var(--gray-2);
|
|
355
|
-
}
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
**Shadows** — `--shadow-1` through `--shadow-6`, progressively stronger.
|
|
359
|
-
|
|
360
|
-
**Radii** — `--radius-1` through `--radius-6` plus `--radius-round`.
|
|
361
|
-
|
|
362
|
-
**Easing** — `--ease-1` through `--ease-5` (standard) and `--ease-spring-1`
|
|
363
|
-
through `--ease-spring-5` (spring).
|
|
364
|
-
|
|
365
|
-
**Typography** — `--font-size-0` through `--font-size-8`,
|
|
366
|
-
`--font-weight-1` through `--font-weight-9`,
|
|
367
|
-
`--font-lineheight-0` through `--font-lineheight-5`.
|
|
368
|
-
|
|
369
|
-
---
|
|
370
|
-
|
|
371
|
-
## CSS Architecture
|
|
372
|
-
|
|
373
|
-
### Native CSS Features
|
|
374
|
-
|
|
375
|
-
Modern CSS eliminates the need for preprocessors. Use these features directly:
|
|
376
|
-
|
|
377
|
-
**Nesting** — group related rules:
|
|
378
|
-
|
|
379
|
-
```css
|
|
380
|
-
.card {
|
|
381
|
-
padding: var(--size-4);
|
|
382
|
-
|
|
383
|
-
& .title {
|
|
384
|
-
font-size: var(--font-size-4);
|
|
385
|
-
font-weight: var(--font-weight-7);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
&:hover {
|
|
389
|
-
box-shadow: var(--shadow-3);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
**Cascade Layers** — control specificity:
|
|
395
|
-
|
|
396
|
-
```css
|
|
397
|
-
@layer base, components, overrides;
|
|
398
|
-
|
|
399
|
-
@layer base {
|
|
400
|
-
button { font: inherit; }
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
@layer components {
|
|
404
|
-
.dialog { border-radius: var(--radius-3); }
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
**Container Queries** — style based on the container, not the viewport:
|
|
409
|
-
|
|
410
|
-
```css
|
|
411
|
-
.sidebar {
|
|
412
|
-
container-type: inline-size;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
@container (min-width: 400px) {
|
|
416
|
-
.sidebar .nav { flex-direction: row; }
|
|
417
|
-
}
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
**`color-mix()`** — derive colors without Sass:
|
|
421
|
-
|
|
422
|
-
```css
|
|
423
|
-
.muted {
|
|
424
|
-
color: color-mix(in oklch, var(--color-text), transparent 40%);
|
|
425
|
-
}
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
### Styling Widget State
|
|
429
|
-
|
|
430
|
-
Rip widgets expose `data-*` attributes for all interactive states. Style them
|
|
431
|
-
with pure CSS attribute selectors:
|
|
432
|
-
|
|
433
|
-
```css
|
|
434
|
-
.dialog-backdrop[data-open] { ... }
|
|
435
|
-
.tab[data-active] { ... }
|
|
436
|
-
.option[data-highlighted] { ... }
|
|
437
|
-
.option[data-selected] { ... }
|
|
438
|
-
.switch[data-checked] { ... }
|
|
439
|
-
.input[data-invalid] { ... }
|
|
440
|
-
.button[data-disabled] { ... }
|
|
441
|
-
.tooltip[data-entering] { ... }
|
|
442
|
-
.tooltip[data-exiting] { ... }
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
No JavaScript styling logic. No className toggling. The component sets the
|
|
446
|
-
attribute; CSS handles the rest.
|
|
447
|
-
|
|
448
|
-
---
|
|
449
|
-
|
|
450
|
-
## Dark Mode
|
|
451
|
-
|
|
452
|
-
Use `prefers-color-scheme` with CSS variable swapping:
|
|
453
|
-
|
|
454
|
-
```css
|
|
455
|
-
:root {
|
|
456
|
-
color-scheme: light dark;
|
|
457
|
-
|
|
458
|
-
--surface-1: var(--gray-0);
|
|
459
|
-
--surface-2: var(--gray-1);
|
|
460
|
-
--color-text: var(--gray-9);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
@media (prefers-color-scheme: dark) {
|
|
464
|
-
:root {
|
|
465
|
-
--surface-1: var(--gray-11);
|
|
466
|
-
--surface-2: var(--gray-10);
|
|
467
|
-
--color-text: var(--gray-1);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
For a manual toggle, use a `[data-theme]` attribute on the root element:
|
|
473
|
-
|
|
474
|
-
```css
|
|
475
|
-
[data-theme="dark"] {
|
|
476
|
-
--surface-1: var(--gray-11);
|
|
477
|
-
--surface-2: var(--gray-10);
|
|
478
|
-
--color-text: var(--gray-1);
|
|
479
|
-
}
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
```js
|
|
483
|
-
document.documentElement.dataset.theme = 'dark'
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
---
|
|
487
|
-
|
|
488
|
-
## Common CSS Patterns
|
|
489
|
-
|
|
490
|
-
### Button
|
|
491
|
-
|
|
492
|
-
```css
|
|
493
|
-
.button {
|
|
494
|
-
display: inline-flex;
|
|
495
|
-
align-items: center;
|
|
496
|
-
gap: var(--size-2);
|
|
497
|
-
padding: var(--size-2) var(--size-4);
|
|
498
|
-
border: 1px solid var(--color-primary);
|
|
499
|
-
border-radius: var(--radius-2);
|
|
500
|
-
background: var(--color-primary);
|
|
501
|
-
color: white;
|
|
502
|
-
font-weight: var(--font-weight-6);
|
|
503
|
-
cursor: pointer;
|
|
504
|
-
transition: background 150ms var(--ease-2);
|
|
505
|
-
|
|
506
|
-
&:hover { background: color-mix(in oklch, var(--color-primary), black 15%); }
|
|
507
|
-
&:active { scale: 0.98; }
|
|
508
|
-
&[data-disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
.ghost {
|
|
512
|
-
background: transparent;
|
|
513
|
-
color: var(--color-primary);
|
|
514
|
-
|
|
515
|
-
&:hover { background: color-mix(in oklch, var(--color-primary), transparent 90%); }
|
|
516
|
-
}
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### Form Input
|
|
520
|
-
|
|
521
|
-
```css
|
|
522
|
-
.input {
|
|
523
|
-
padding: var(--size-2) var(--size-3);
|
|
524
|
-
border: 1px solid var(--gray-4);
|
|
525
|
-
border-radius: var(--radius-2);
|
|
526
|
-
font-size: var(--font-size-1);
|
|
527
|
-
background: var(--surface-1);
|
|
528
|
-
color: var(--color-text);
|
|
529
|
-
transition: border-color 150ms var(--ease-2);
|
|
530
|
-
|
|
531
|
-
&:focus {
|
|
532
|
-
outline: 2px solid var(--color-primary);
|
|
533
|
-
outline-offset: 1px;
|
|
534
|
-
border-color: var(--color-primary);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
&[data-invalid] { border-color: var(--color-danger); }
|
|
538
|
-
&[data-disabled] { opacity: 0.5; }
|
|
539
|
-
&::placeholder { color: var(--color-text-muted); }
|
|
540
|
-
}
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Card
|
|
544
|
-
|
|
545
|
-
```css
|
|
546
|
-
.card {
|
|
547
|
-
background: var(--surface-1);
|
|
548
|
-
border-radius: var(--radius-3);
|
|
549
|
-
padding: var(--size-5);
|
|
550
|
-
box-shadow: var(--shadow-2);
|
|
551
|
-
transition: box-shadow 200ms var(--ease-2);
|
|
552
|
-
|
|
553
|
-
&:hover { box-shadow: var(--shadow-3); }
|
|
554
|
-
|
|
555
|
-
& .title {
|
|
556
|
-
font-size: var(--font-size-3);
|
|
557
|
-
font-weight: var(--font-weight-7);
|
|
558
|
-
margin-block-end: var(--size-2);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
& .body {
|
|
562
|
-
color: var(--color-text-muted);
|
|
563
|
-
line-height: var(--font-lineheight-3);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
### Dialog
|
|
569
|
-
|
|
570
|
-
```css
|
|
571
|
-
.backdrop {
|
|
572
|
-
position: fixed;
|
|
573
|
-
inset: 0;
|
|
574
|
-
background: oklch(0% 0 0 / 40%);
|
|
575
|
-
display: grid;
|
|
576
|
-
place-items: center;
|
|
577
|
-
|
|
578
|
-
&[data-open] { animation: fade-in 150ms var(--ease-2); }
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
.panel {
|
|
582
|
-
background: var(--surface-1);
|
|
583
|
-
border-radius: var(--radius-3);
|
|
584
|
-
padding: var(--size-6);
|
|
585
|
-
box-shadow: var(--shadow-4);
|
|
586
|
-
max-width: min(90vw, 32rem);
|
|
587
|
-
width: 100%;
|
|
588
|
-
animation: slide-in-up 200ms var(--ease-spring-3);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
.panel .title {
|
|
592
|
-
font-size: var(--font-size-4);
|
|
593
|
-
font-weight: var(--font-weight-7);
|
|
594
|
-
margin-block-end: var(--size-2);
|
|
595
|
-
}
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
### Select
|
|
599
|
-
|
|
600
|
-
```css
|
|
601
|
-
.trigger {
|
|
602
|
-
display: inline-flex;
|
|
603
|
-
align-items: center;
|
|
604
|
-
justify-content: space-between;
|
|
605
|
-
gap: var(--size-2);
|
|
606
|
-
padding: var(--size-2) var(--size-3);
|
|
607
|
-
border: 1px solid var(--gray-4);
|
|
608
|
-
border-radius: var(--radius-2);
|
|
609
|
-
background: var(--surface-1);
|
|
610
|
-
cursor: pointer;
|
|
611
|
-
min-width: 10rem;
|
|
612
|
-
|
|
613
|
-
&[data-open] { border-color: var(--color-primary); }
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
.popup {
|
|
617
|
-
background: var(--surface-1);
|
|
618
|
-
border: 1px solid var(--gray-3);
|
|
619
|
-
border-radius: var(--radius-2);
|
|
620
|
-
box-shadow: var(--shadow-3);
|
|
621
|
-
padding: var(--size-1);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
.option {
|
|
625
|
-
padding: var(--size-2) var(--size-3);
|
|
626
|
-
border-radius: var(--radius-1);
|
|
627
|
-
cursor: pointer;
|
|
628
|
-
|
|
629
|
-
&[data-highlighted] { background: var(--surface-2); }
|
|
630
|
-
&[data-selected] { font-weight: var(--font-weight-6); color: var(--color-primary); }
|
|
631
|
-
}
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### Tooltip
|
|
635
|
-
|
|
636
|
-
```css
|
|
637
|
-
.tooltip {
|
|
638
|
-
background: var(--gray-10);
|
|
639
|
-
color: var(--gray-0);
|
|
640
|
-
font-size: var(--font-size-0);
|
|
641
|
-
padding: var(--size-1) var(--size-2);
|
|
642
|
-
border-radius: var(--radius-2);
|
|
643
|
-
max-width: 20rem;
|
|
644
|
-
|
|
645
|
-
&[data-entering] { animation: fade-in 100ms var(--ease-2); }
|
|
646
|
-
&[data-exiting] { animation: fade-out 75ms var(--ease-2); }
|
|
647
|
-
}
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
---
|
|
651
|
-
|
|
652
|
-
## What We Don't Use
|
|
653
|
-
|
|
654
|
-
**React or any framework runtime** — Rip widgets are written in Rip, compiled
|
|
655
|
-
to JavaScript, with zero runtime dependencies.
|
|
656
|
-
|
|
657
|
-
**Tailwind CSS** — utility classes in markup are write-only and semantically
|
|
658
|
-
empty. We write real CSS with real selectors.
|
|
659
|
-
|
|
660
|
-
**CSS-in-JS runtimes** (styled-components, Emotion) — runtime style injection
|
|
661
|
-
adds bundle size and creates hydration complexity.
|
|
662
|
-
|
|
663
|
-
**Sass / Less** — native CSS nesting, `color-mix()`, and custom properties
|
|
664
|
-
eliminate the need for preprocessors.
|
|
665
|
-
|
|
666
|
-
**Inline styles for layout** — the `style` prop is for truly dynamic values
|
|
667
|
-
(e.g., positioning from a calculation). Layout, spacing, color, and typography
|
|
668
|
-
go in CSS.
|
|
669
|
-
|
|
670
|
-
**Third-party headless libraries** (Base UI, Radix, Headless UI, Zag.js) —
|
|
671
|
-
we implement the same WAI-ARIA patterns natively in Rip. The patterns are
|
|
672
|
-
standard; the implementation is ours.
|
|
673
|
-
|
|
674
|
-
---
|
|
675
|
-
|
|
676
|
-
## Installation
|
|
677
|
-
|
|
678
|
-
Open Props is the only external styling dependency:
|
|
679
|
-
|
|
680
|
-
```bash
|
|
681
|
-
bun add open-props
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
Rip widgets ship as part of `rip-lang`. No additional installation needed.
|
|
685
|
-
|
|
686
|
-
---
|
|
687
|
-
|
|
688
|
-
## Summary
|
|
689
|
-
|
|
690
|
-
Write semantic CSS. Use Open Props for consistent design tokens. Use `data-*`
|
|
691
|
-
attributes for styling interactive states. Use native CSS features — nesting,
|
|
692
|
-
layers, container queries, `color-mix()` — for everything else.
|
|
693
|
-
|
|
694
|
-
Build accessible interactive components in Rip, following WAI-ARIA patterns
|
|
695
|
-
and Base UI's behavioral specifications. Rip's reactive primitives (`:=`,
|
|
696
|
-
`~=`, `~>`) handle all state, effects, and cleanup. Zero framework
|
|
697
|
-
dependencies. Zero runtime overhead.
|
|
698
|
-
|
|
699
|
-
The result: accessible components, consistent design tokens, scoped styles,
|
|
700
|
-
and clean readable code in both Rip and CSS. No class soup. No framework
|
|
701
|
-
lock-in. Just the platform.
|