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.
@@ -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.