@rip-lang/ui 0.1.3 → 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 +507 -726
- 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 -37
- 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/renderer.js +0 -397
- package/router.js +0 -325
- package/serve.rip +0 -140
- package/stash.js +0 -413
- package/ui.js +0 -208
- package/vfs.js +0 -215
package/toast.rip
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Toast — accessible headless toast notification system
|
|
2
|
+
#
|
|
3
|
+
# Managed toast system with stacking, timer pause on hover, and promise support.
|
|
4
|
+
# Uses ARIA live region for screen reader announcements. Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# toasts := []
|
|
8
|
+
#
|
|
9
|
+
# # Add a toast — reactive assignment is the API
|
|
10
|
+
# toasts = [...toasts, { message: "Saved!", type: "success" }]
|
|
11
|
+
#
|
|
12
|
+
# # Dismiss — filter it out
|
|
13
|
+
# toasts = toasts.filter (t) -> t isnt target
|
|
14
|
+
#
|
|
15
|
+
# # Clear all
|
|
16
|
+
# toasts = []
|
|
17
|
+
#
|
|
18
|
+
# # In render block
|
|
19
|
+
# ToastViewport toasts <=> toasts
|
|
20
|
+
|
|
21
|
+
export ToastViewport = component
|
|
22
|
+
@toasts := []
|
|
23
|
+
@placement := 'bottom-right'
|
|
24
|
+
|
|
25
|
+
onDismiss: (toast) ->
|
|
26
|
+
@toasts = @toasts.filter (t) -> t isnt toast
|
|
27
|
+
@emit 'dismiss', toast
|
|
28
|
+
|
|
29
|
+
render
|
|
30
|
+
div role: "region", aria-label: "Notifications", $placement: @placement
|
|
31
|
+
for toast in @toasts
|
|
32
|
+
Toast toast: toast, @dismiss: (e) => @onDismiss(e.detail)
|
|
33
|
+
|
|
34
|
+
export Toast = component
|
|
35
|
+
@toast := {}
|
|
36
|
+
|
|
37
|
+
leaving := false
|
|
38
|
+
_timer := null
|
|
39
|
+
_remaining = 0
|
|
40
|
+
_started = 0
|
|
41
|
+
|
|
42
|
+
_startTimer: ->
|
|
43
|
+
dur = @toast.duration ?? 4000
|
|
44
|
+
return unless dur > 0
|
|
45
|
+
_remaining = dur
|
|
46
|
+
_started = Date.now()
|
|
47
|
+
_timer = setTimeout => @dismiss(), dur
|
|
48
|
+
|
|
49
|
+
_pauseTimer: ->
|
|
50
|
+
return unless _timer
|
|
51
|
+
clearTimeout _timer
|
|
52
|
+
_remaining -= Date.now() - _started
|
|
53
|
+
_timer = null
|
|
54
|
+
|
|
55
|
+
_resumeTimer: ->
|
|
56
|
+
return if _timer or _remaining <= 0
|
|
57
|
+
_started = Date.now()
|
|
58
|
+
_timer = setTimeout => @dismiss(), _remaining
|
|
59
|
+
|
|
60
|
+
mounted: -> @_startTimer()
|
|
61
|
+
|
|
62
|
+
beforeUnmount: ->
|
|
63
|
+
clearTimeout _timer if _timer
|
|
64
|
+
|
|
65
|
+
dismiss: ->
|
|
66
|
+
leaving = true
|
|
67
|
+
setTimeout =>
|
|
68
|
+
@emit 'dismiss', @toast
|
|
69
|
+
, 200
|
|
70
|
+
|
|
71
|
+
render
|
|
72
|
+
div role: (if @toast.type is 'error' then 'alert' else 'status'),
|
|
73
|
+
aria-live: (if @toast.type is 'error' then 'assertive' else 'polite'),
|
|
74
|
+
$type: @toast.type ?? 'info',
|
|
75
|
+
$leaving: leaving?!,
|
|
76
|
+
@mouseenter: @_pauseTimer,
|
|
77
|
+
@mouseleave: @_resumeTimer,
|
|
78
|
+
@focusin: @_pauseTimer,
|
|
79
|
+
@focusout: @_resumeTimer
|
|
80
|
+
.
|
|
81
|
+
if @toast.title
|
|
82
|
+
strong @toast.title
|
|
83
|
+
span @toast.message
|
|
84
|
+
if @toast.action
|
|
85
|
+
button @click: @toast.action.onClick
|
|
86
|
+
@toast.action.label or 'Action'
|
|
87
|
+
button aria-label: "Dismiss", @click: @dismiss
|
|
88
|
+
"✕"
|
package/toggle-group.rip
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# ToggleGroup — accessible headless toggle group
|
|
2
|
+
#
|
|
3
|
+
# A set of two-state buttons where one or more can be pressed.
|
|
4
|
+
# Set @multiple to false for single-select (radio-like) behavior.
|
|
5
|
+
# Ships zero CSS.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ToggleGroup value <=> alignment
|
|
9
|
+
# div $value: "left", "Left"
|
|
10
|
+
# div $value: "center", "Center"
|
|
11
|
+
# div $value: "right", "Right"
|
|
12
|
+
|
|
13
|
+
export ToggleGroup = component
|
|
14
|
+
@value := null
|
|
15
|
+
@disabled := false
|
|
16
|
+
@multiple := false
|
|
17
|
+
@orientation := 'horizontal'
|
|
18
|
+
|
|
19
|
+
_items ~=
|
|
20
|
+
return [] unless @_slot
|
|
21
|
+
Array.from(@_slot.querySelectorAll('[data-value]') or [])
|
|
22
|
+
|
|
23
|
+
_isPressed: (item) ->
|
|
24
|
+
val = item.dataset.value
|
|
25
|
+
if @multiple
|
|
26
|
+
Array.isArray(@value) and val in @value
|
|
27
|
+
else
|
|
28
|
+
val is @value
|
|
29
|
+
|
|
30
|
+
_toggle: (val) ->
|
|
31
|
+
return if @disabled
|
|
32
|
+
if @multiple
|
|
33
|
+
arr = if Array.isArray(@value) then [...@value] else []
|
|
34
|
+
if val in arr
|
|
35
|
+
arr = arr.filter (v) -> v isnt val
|
|
36
|
+
else
|
|
37
|
+
arr.push val
|
|
38
|
+
@value = arr
|
|
39
|
+
else
|
|
40
|
+
@value = if val is @value then null else val
|
|
41
|
+
@emit 'change', @value
|
|
42
|
+
|
|
43
|
+
onKeydown: (e) ->
|
|
44
|
+
opts = @_root?.querySelectorAll('[data-value]')
|
|
45
|
+
return unless opts?.length
|
|
46
|
+
focused = Array.from(opts).indexOf(document.activeElement)
|
|
47
|
+
return if focused < 0
|
|
48
|
+
len = opts.length
|
|
49
|
+
switch e.key
|
|
50
|
+
when 'ArrowRight', 'ArrowDown'
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
opts[(focused + 1) %% len]?.focus()
|
|
53
|
+
when 'ArrowLeft', 'ArrowUp'
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
opts[(focused - 1) %% len]?.focus()
|
|
56
|
+
when 'Home'
|
|
57
|
+
e.preventDefault()
|
|
58
|
+
opts[0]?.focus()
|
|
59
|
+
when 'End'
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
opts[len - 1]?.focus()
|
|
62
|
+
|
|
63
|
+
render
|
|
64
|
+
div ref: "_root", role: "group", aria-orientation: @orientation
|
|
65
|
+
$orientation: @orientation
|
|
66
|
+
$disabled: @disabled?!
|
|
67
|
+
|
|
68
|
+
. ref: "_slot", style: "display:none"
|
|
69
|
+
slot
|
|
70
|
+
|
|
71
|
+
for item, idx in _items
|
|
72
|
+
button tabindex: (if idx is 0 then "0" else "-1")
|
|
73
|
+
aria-pressed: !!@_isPressed(item)
|
|
74
|
+
$pressed: @_isPressed(item)?!
|
|
75
|
+
$disabled: @disabled?!
|
|
76
|
+
$value: item.dataset.value
|
|
77
|
+
@click: (=> @_toggle(item.dataset.value))
|
|
78
|
+
item.textContent
|
package/toggle.rip
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Toggle — accessible headless toggle button
|
|
2
|
+
#
|
|
3
|
+
# Stateful button that toggles pressed state on click.
|
|
4
|
+
# Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# Toggle pressed <=> isBold
|
|
8
|
+
# "Bold"
|
|
9
|
+
|
|
10
|
+
export Toggle = component
|
|
11
|
+
@pressed := false
|
|
12
|
+
@disabled := false
|
|
13
|
+
|
|
14
|
+
onClick: ->
|
|
15
|
+
return if @disabled
|
|
16
|
+
@pressed = not @pressed
|
|
17
|
+
@emit 'change', @pressed
|
|
18
|
+
|
|
19
|
+
render
|
|
20
|
+
button aria-pressed: !!@pressed
|
|
21
|
+
aria-disabled: @disabled?!
|
|
22
|
+
$pressed: @pressed?!
|
|
23
|
+
$disabled: @disabled?!
|
|
24
|
+
slot
|
package/toolbar.rip
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Toolbar — accessible headless toolbar
|
|
2
|
+
#
|
|
3
|
+
# Groups interactive controls with roving tabindex keyboard navigation.
|
|
4
|
+
# Arrow keys move focus between focusable children. Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# Toolbar
|
|
8
|
+
# Button @click: save, "Save"
|
|
9
|
+
# Button @click: undo, "Undo"
|
|
10
|
+
# Separator orientation: "vertical"
|
|
11
|
+
# Toggle pressed <=> isBold, "Bold"
|
|
12
|
+
|
|
13
|
+
export Toolbar = component
|
|
14
|
+
@orientation := 'horizontal'
|
|
15
|
+
@label := ''
|
|
16
|
+
|
|
17
|
+
_getFocusable: ->
|
|
18
|
+
return [] unless @_root
|
|
19
|
+
Array.from(@_root.querySelectorAll('button, [tabindex], input, select, textarea')).filter (el) ->
|
|
20
|
+
not el.disabled and el.offsetParent isnt null
|
|
21
|
+
|
|
22
|
+
onKeydown: (e) ->
|
|
23
|
+
els = @_getFocusable()
|
|
24
|
+
return unless els.length
|
|
25
|
+
focused = els.indexOf(document.activeElement)
|
|
26
|
+
return if focused < 0
|
|
27
|
+
len = els.length
|
|
28
|
+
horiz = @orientation is 'horizontal'
|
|
29
|
+
switch e.key
|
|
30
|
+
when (if horiz then 'ArrowRight' else 'ArrowDown')
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
els[(focused + 1) %% len]?.focus()
|
|
33
|
+
when (if horiz then 'ArrowLeft' else 'ArrowUp')
|
|
34
|
+
e.preventDefault()
|
|
35
|
+
els[(focused - 1) %% len]?.focus()
|
|
36
|
+
when 'Home'
|
|
37
|
+
e.preventDefault()
|
|
38
|
+
els[0]?.focus()
|
|
39
|
+
when 'End'
|
|
40
|
+
e.preventDefault()
|
|
41
|
+
els[len - 1]?.focus()
|
|
42
|
+
|
|
43
|
+
render
|
|
44
|
+
div role: "toolbar", aria-label: @label or undefined, aria-orientation: @orientation
|
|
45
|
+
$orientation: @orientation
|
|
46
|
+
slot
|
package/tooltip.rip
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Tooltip — accessible headless tooltip with delay and positioning
|
|
2
|
+
#
|
|
3
|
+
# Shows on hover/focus with configurable delay. Uses aria-describedby.
|
|
4
|
+
# Exposes $open, $entering, $exiting. Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# Tooltip text: "Helpful info", placement: "top"
|
|
8
|
+
# button "Hover me"
|
|
9
|
+
|
|
10
|
+
lastCloseTime = 0
|
|
11
|
+
GROUP_TIMEOUT = 400
|
|
12
|
+
|
|
13
|
+
export Tooltip = component
|
|
14
|
+
@text := ''
|
|
15
|
+
@placement := 'top'
|
|
16
|
+
@delay := 300
|
|
17
|
+
@offset := 6
|
|
18
|
+
@hoverable := false
|
|
19
|
+
|
|
20
|
+
open := false
|
|
21
|
+
entering := false
|
|
22
|
+
exiting := false
|
|
23
|
+
_showTimer := null
|
|
24
|
+
_hideTimer := null
|
|
25
|
+
_id =! "tip-#{Math.random().toString(36).slice(2, 8)}"
|
|
26
|
+
|
|
27
|
+
show: ->
|
|
28
|
+
clearTimeout _hideTimer if _hideTimer
|
|
29
|
+
delay = if (Date.now() - lastCloseTime) < GROUP_TIMEOUT then 0 else @delay
|
|
30
|
+
_showTimer = setTimeout =>
|
|
31
|
+
open = true
|
|
32
|
+
entering = true
|
|
33
|
+
setTimeout =>
|
|
34
|
+
entering = false
|
|
35
|
+
@_position()
|
|
36
|
+
, 0
|
|
37
|
+
, delay
|
|
38
|
+
|
|
39
|
+
hide: ->
|
|
40
|
+
clearTimeout _showTimer if _showTimer
|
|
41
|
+
exiting = true
|
|
42
|
+
_hideTimer = setTimeout =>
|
|
43
|
+
open = false
|
|
44
|
+
exiting = false
|
|
45
|
+
lastCloseTime = Date.now()
|
|
46
|
+
, 150
|
|
47
|
+
|
|
48
|
+
_cancelHide: ->
|
|
49
|
+
clearTimeout _hideTimer if _hideTimer
|
|
50
|
+
exiting = false
|
|
51
|
+
|
|
52
|
+
_position: ->
|
|
53
|
+
return unless @_trigger and @_tip
|
|
54
|
+
tr = @_trigger.getBoundingClientRect()
|
|
55
|
+
fl = @_tip.getBoundingClientRect()
|
|
56
|
+
[side, align] = @placement.split('-')
|
|
57
|
+
align ?= 'center'
|
|
58
|
+
gap = @offset
|
|
59
|
+
|
|
60
|
+
x = switch side
|
|
61
|
+
when 'bottom', 'top'
|
|
62
|
+
switch align
|
|
63
|
+
when 'start' then tr.left
|
|
64
|
+
when 'end' then tr.right - fl.width
|
|
65
|
+
else tr.left + (tr.width - fl.width) / 2
|
|
66
|
+
when 'right' then tr.right + gap
|
|
67
|
+
when 'left' then tr.left - fl.width - gap
|
|
68
|
+
|
|
69
|
+
y = switch side
|
|
70
|
+
when 'bottom' then tr.bottom + gap
|
|
71
|
+
when 'top' then tr.top - fl.height - gap
|
|
72
|
+
when 'left', 'right'
|
|
73
|
+
switch align
|
|
74
|
+
when 'start' then tr.top
|
|
75
|
+
when 'end' then tr.bottom - fl.height
|
|
76
|
+
else tr.top + (tr.height - fl.height) / 2
|
|
77
|
+
|
|
78
|
+
if side is 'bottom' and y + fl.height > window.innerHeight
|
|
79
|
+
y = tr.top - fl.height - gap
|
|
80
|
+
if side is 'top' and y < 0
|
|
81
|
+
y = tr.bottom + gap
|
|
82
|
+
if side is 'right' and x + fl.width > window.innerWidth
|
|
83
|
+
x = tr.left - fl.width - gap
|
|
84
|
+
if side is 'left' and x < 0
|
|
85
|
+
x = tr.right + gap
|
|
86
|
+
|
|
87
|
+
x = Math.max(4, Math.min(x, window.innerWidth - fl.width - 4))
|
|
88
|
+
|
|
89
|
+
@_tip.style.position = 'fixed'
|
|
90
|
+
@_tip.style.left = "#{x}px"
|
|
91
|
+
@_tip.style.top = "#{y}px"
|
|
92
|
+
|
|
93
|
+
beforeUnmount: ->
|
|
94
|
+
clearTimeout _showTimer if _showTimer
|
|
95
|
+
clearTimeout _hideTimer if _hideTimer
|
|
96
|
+
|
|
97
|
+
render
|
|
98
|
+
.
|
|
99
|
+
div ref: "_trigger"
|
|
100
|
+
aria-describedby: open ? _id : undefined
|
|
101
|
+
@mouseenter: @show
|
|
102
|
+
@mouseleave: @hide
|
|
103
|
+
@focusin: @show
|
|
104
|
+
@focusout: @hide
|
|
105
|
+
slot
|
|
106
|
+
|
|
107
|
+
if open
|
|
108
|
+
div ref: "_tip", id: _id, role: "tooltip"
|
|
109
|
+
$open: true
|
|
110
|
+
$entering: entering?!
|
|
111
|
+
$exiting: exiting?!
|
|
112
|
+
$placement: @placement
|
|
113
|
+
@mouseenter: (=> @_cancelHide() if @hoverable)
|
|
114
|
+
@mouseleave: (=> @hide() if @hoverable)
|
|
115
|
+
@text
|