rip-lang 3.15.4 → 3.16.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 +6 -4
- package/bin/rip +167 -12
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +808 -0
- package/docs/RIP-DUCKDB.md +477 -0
- package/docs/RIP-INTRO.md +396 -0
- package/docs/RIP-LANG.md +59 -5
- package/docs/RIP-SCHEMA.md +191 -8
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +3627 -1470
- package/docs/dist/rip.min.js +671 -244
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/vscode/print/index.html +2 -1
- package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/ui/bundle.json +61 -0
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +0 -7
- package/docs/ui/index.css +66 -23
- package/docs/ui/index.html +6 -6
- package/package.json +9 -3
- package/rip-loader.js +64 -2
- package/src/AGENTS.md +63 -36
- package/src/browser.js +96 -14
- package/src/compiler.js +960 -143
- package/src/components.js +794 -88
- package/src/{types-emit.js → dts.js} +181 -71
- package/src/grammar/README.md +1 -1
- package/src/grammar/grammar.rip +111 -97
- package/src/lexer.js +132 -18
- package/src/parser.js +203 -205
- package/src/repl.js +74 -6
- package/src/schema/runtime-orm.js +168 -4
- package/src/schema/runtime-validate.js +146 -2
- package/src/schema/runtime.generated.js +314 -6
- package/src/schema/schema.js +5 -5
- package/src/sourcemaps.js +277 -1
- package/src/stdlib.js +253 -0
- package/src/typecheck.js +2023 -106
- package/src/types.js +127 -7
- package/docs/ui/accordion.rip +0 -103
- package/docs/ui/alert-dialog.rip +0 -53
- package/docs/ui/autocomplete.rip +0 -115
- package/docs/ui/avatar.rip +0 -37
- package/docs/ui/badge.rip +0 -15
- package/docs/ui/breadcrumb.rip +0 -47
- package/docs/ui/button-group.rip +0 -26
- package/docs/ui/button.rip +0 -23
- package/docs/ui/card.rip +0 -25
- package/docs/ui/carousel.rip +0 -110
- package/docs/ui/checkbox-group.rip +0 -61
- package/docs/ui/checkbox.rip +0 -33
- package/docs/ui/collapsible.rip +0 -50
- package/docs/ui/combobox.rip +0 -130
- package/docs/ui/context-menu.rip +0 -88
- package/docs/ui/date-picker.rip +0 -206
- package/docs/ui/dialog.rip +0 -60
- package/docs/ui/drawer.rip +0 -58
- package/docs/ui/editable-value.rip +0 -82
- package/docs/ui/field.rip +0 -53
- package/docs/ui/fieldset.rip +0 -22
- package/docs/ui/form.rip +0 -39
- package/docs/ui/grid.rip +0 -901
- package/docs/ui/input-group.rip +0 -28
- package/docs/ui/input.rip +0 -36
- package/docs/ui/label.rip +0 -16
- package/docs/ui/menu.rip +0 -134
- package/docs/ui/menubar.rip +0 -151
- package/docs/ui/meter.rip +0 -36
- package/docs/ui/multi-select.rip +0 -203
- package/docs/ui/native-select.rip +0 -33
- package/docs/ui/nav-menu.rip +0 -126
- package/docs/ui/number-field.rip +0 -162
- package/docs/ui/otp-field.rip +0 -89
- package/docs/ui/pagination.rip +0 -123
- package/docs/ui/popover.rip +0 -93
- package/docs/ui/preview-card.rip +0 -75
- package/docs/ui/progress.rip +0 -25
- package/docs/ui/radio-group.rip +0 -57
- package/docs/ui/resizable.rip +0 -123
- package/docs/ui/scroll-area.rip +0 -145
- package/docs/ui/select.rip +0 -151
- package/docs/ui/separator.rip +0 -17
- package/docs/ui/skeleton.rip +0 -22
- package/docs/ui/slider.rip +0 -165
- package/docs/ui/spinner.rip +0 -17
- package/docs/ui/table.rip +0 -27
- package/docs/ui/tabs.rip +0 -113
- package/docs/ui/textarea.rip +0 -48
- package/docs/ui/toast.rip +0 -87
- package/docs/ui/toggle-group.rip +0 -71
- package/docs/ui/toggle.rip +0 -24
- package/docs/ui/toolbar.rip +0 -38
- package/docs/ui/tooltip.rip +0 -85
- package/src/app.rip +0 -1571
- package/src/sourcemap-merge.js +0 -287
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
- /package/src/schema/{dts-emit.js → dts.js} +0 -0
package/docs/ui/nav-menu.rip
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# NavigationMenu — accessible headless site navigation
|
|
2
|
-
#
|
|
3
|
-
# Horizontal navigation with optional dropdown sub-menus. Triggers show
|
|
4
|
-
# content on hover or click. Ships zero CSS.
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# NavigationMenu
|
|
8
|
-
# a $link: true, href: "/", "Home"
|
|
9
|
-
# div $trigger: "products"
|
|
10
|
-
# div $panel: true
|
|
11
|
-
# a href: "/ui", "ui"
|
|
12
|
-
# a href: "/tools", "Tools"
|
|
13
|
-
# a $link: true, href: "/about", "About"
|
|
14
|
-
|
|
15
|
-
export NavigationMenu = component
|
|
16
|
-
@orientation:: "horizontal" | "vertical" := "horizontal"
|
|
17
|
-
@hoverDelay:: number := 200
|
|
18
|
-
@hoverCloseDelay:: number := 300
|
|
19
|
-
|
|
20
|
-
activePanel := null
|
|
21
|
-
_ready := false
|
|
22
|
-
_hoverTimer := null
|
|
23
|
-
_closeTimer := null
|
|
24
|
-
|
|
25
|
-
_navItems ~=
|
|
26
|
-
return [] unless @_slot
|
|
27
|
-
Array.from(@_slot.children).filter (el) ->
|
|
28
|
-
el.dataset?.link? or el.dataset?.trigger?
|
|
29
|
-
|
|
30
|
-
mounted: -> _ready = true
|
|
31
|
-
|
|
32
|
-
beforeUnmount: ->
|
|
33
|
-
clearTimeout _hoverTimer if _hoverTimer
|
|
34
|
-
clearTimeout _closeTimer if _closeTimer
|
|
35
|
-
|
|
36
|
-
_openPanel: (id) ->
|
|
37
|
-
clearTimeout _closeTimer if _closeTimer
|
|
38
|
-
activePanel = id
|
|
39
|
-
requestAnimationFrame => @_position(id)
|
|
40
|
-
|
|
41
|
-
_closePanel: ->
|
|
42
|
-
activePanel = null
|
|
43
|
-
|
|
44
|
-
_scheduleOpen: (id) ->
|
|
45
|
-
clearTimeout _closeTimer if _closeTimer
|
|
46
|
-
_hoverTimer = setTimeout (=> @_openPanel(id)), @hoverDelay
|
|
47
|
-
|
|
48
|
-
_scheduleClose: ->
|
|
49
|
-
clearTimeout _hoverTimer if _hoverTimer
|
|
50
|
-
_closeTimer = setTimeout (=> @_closePanel()), @hoverCloseDelay
|
|
51
|
-
|
|
52
|
-
_cancelClose: ->
|
|
53
|
-
clearTimeout _closeTimer if _closeTimer
|
|
54
|
-
|
|
55
|
-
_position: (id) ->
|
|
56
|
-
ARIA.positionBelow @_root?.querySelector("[data-nav-trigger=\"#{id}\"]"),
|
|
57
|
-
@_root?.querySelector("[data-nav-panel=\"#{id}\"]"), 2, false
|
|
58
|
-
|
|
59
|
-
_onKeydown: (e) ->
|
|
60
|
-
navBtns = @_root?.querySelectorAll('[data-nav-trigger], [data-nav-link]')
|
|
61
|
-
return unless navBtns?.length
|
|
62
|
-
focused = Array.from(navBtns).indexOf(document.activeElement)
|
|
63
|
-
return if focused < 0
|
|
64
|
-
len = navBtns.length
|
|
65
|
-
switch e.key
|
|
66
|
-
when 'ArrowRight'
|
|
67
|
-
e.preventDefault()
|
|
68
|
-
navBtns[(focused + 1) %% len]?.focus()
|
|
69
|
-
when 'ArrowLeft'
|
|
70
|
-
e.preventDefault()
|
|
71
|
-
navBtns[(focused - 1) %% len]?.focus()
|
|
72
|
-
when 'ArrowDown'
|
|
73
|
-
triggerId = document.activeElement?.dataset?.navTrigger
|
|
74
|
-
if triggerId
|
|
75
|
-
e.preventDefault()
|
|
76
|
-
@_openPanel(triggerId)
|
|
77
|
-
@_root?.querySelector("[data-nav-panel=\"#{triggerId}\"] a, [data-nav-panel=\"#{triggerId}\"] button")?.focus()
|
|
78
|
-
when 'Escape'
|
|
79
|
-
@_closePanel()
|
|
80
|
-
|
|
81
|
-
~>
|
|
82
|
-
return unless _ready
|
|
83
|
-
if activePanel
|
|
84
|
-
onDown = (e) => @_closePanel() unless @_root?.contains(e.target)
|
|
85
|
-
onScroll = => @_position(activePanel)
|
|
86
|
-
document.addEventListener 'mousedown', onDown
|
|
87
|
-
window.addEventListener 'scroll', onScroll, true
|
|
88
|
-
return ->
|
|
89
|
-
document.removeEventListener 'mousedown', onDown
|
|
90
|
-
window.removeEventListener 'scroll', onScroll, true
|
|
91
|
-
|
|
92
|
-
render
|
|
93
|
-
nav ref: "_root", role: "navigation", aria-orientation: @orientation
|
|
94
|
-
$orientation: @orientation
|
|
95
|
-
|
|
96
|
-
. ref: "_slot", style: "display:none"
|
|
97
|
-
slot
|
|
98
|
-
|
|
99
|
-
for navItem, nIdx in _navItems
|
|
100
|
-
if navItem.dataset.link?
|
|
101
|
-
a $nav-link: true, href: navItem.getAttribute('href') or '#', tabindex: "0"
|
|
102
|
-
@keydown: @_onKeydown
|
|
103
|
-
= navItem.textContent
|
|
104
|
-
else if navItem.dataset.trigger?
|
|
105
|
-
. style: "display:inline-block;position:relative"
|
|
106
|
-
button $nav-trigger: navItem.dataset.trigger, tabindex: "0"
|
|
107
|
-
aria-expanded: activePanel is navItem.dataset.trigger
|
|
108
|
-
$open: (activePanel is navItem.dataset.trigger)?!
|
|
109
|
-
@click: (=> if activePanel is navItem.dataset.trigger then @_closePanel() else @_openPanel(navItem.dataset.trigger))
|
|
110
|
-
@mouseenter: (=> @_scheduleOpen(navItem.dataset.trigger))
|
|
111
|
-
@mouseleave: (=> @_scheduleClose())
|
|
112
|
-
@keydown: @_onKeydown
|
|
113
|
-
= navItem.dataset.trigger
|
|
114
|
-
|
|
115
|
-
if activePanel is navItem.dataset.trigger
|
|
116
|
-
div $nav-panel: navItem.dataset.trigger, $open: true
|
|
117
|
-
style: "position:fixed;z-index:50"
|
|
118
|
-
@mouseenter: (=> @_cancelClose())
|
|
119
|
-
@mouseleave: (=> @_scheduleClose())
|
|
120
|
-
for link, lIdx in Array.from(navItem.querySelectorAll('a, [data-link]'))
|
|
121
|
-
a href: link.getAttribute('href') or '#', tabindex: "0"
|
|
122
|
-
@keydown: (e) =>
|
|
123
|
-
if e.key is 'Escape'
|
|
124
|
-
@_closePanel()
|
|
125
|
-
@_root?.querySelector("[data-nav-trigger=\"#{navItem.dataset.trigger}\"]")?.focus()
|
|
126
|
-
= link.textContent
|
package/docs/ui/number-field.rip
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
# NumberField — accessible headless number input with stepper buttons
|
|
2
|
-
#
|
|
3
|
-
# Increment/decrement with click, hold-to-repeat, and keyboard.
|
|
4
|
-
# Supports min/max/step clamping and Shift/Alt step modifiers.
|
|
5
|
-
# Ships zero CSS.
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# NumberField value <=> quantity
|
|
9
|
-
# NumberField value <=> price, min: 0, max: 1000, step: 0.01
|
|
10
|
-
|
|
11
|
-
START_DELAY = 400
|
|
12
|
-
TICK_DELAY = 60
|
|
13
|
-
|
|
14
|
-
export NumberField = component
|
|
15
|
-
@value:: number := 0
|
|
16
|
-
@min:: any := null
|
|
17
|
-
@max:: any := null
|
|
18
|
-
@step:: number := 1
|
|
19
|
-
@smallStep:: number := 0.1
|
|
20
|
-
@largeStep:: number := 10
|
|
21
|
-
@disabled:: boolean := false
|
|
22
|
-
@readOnly:: boolean := false
|
|
23
|
-
@name:: any := null
|
|
24
|
-
|
|
25
|
-
_timer = null
|
|
26
|
-
_interval = null
|
|
27
|
-
_id =! "nf-#{Math.random().toString(36).slice(2, 8)}"
|
|
28
|
-
|
|
29
|
-
_clamp: (v) ->
|
|
30
|
-
v = Math.max(@min, v) if @min?
|
|
31
|
-
v = Math.min(@max, v) if @max?
|
|
32
|
-
v
|
|
33
|
-
|
|
34
|
-
_roundToStep: (v) ->
|
|
35
|
-
base = @min ?? 0
|
|
36
|
-
rounded = Math.round((v - base) / @step) * @step + base
|
|
37
|
-
precision = String(@step).split('.')[1]?.length or 0
|
|
38
|
-
parseFloat rounded.toFixed(precision)
|
|
39
|
-
|
|
40
|
-
_stepAmount: (e) ->
|
|
41
|
-
if e?.altKey then @smallStep
|
|
42
|
-
else if e?.shiftKey then @largeStep
|
|
43
|
-
else @step
|
|
44
|
-
|
|
45
|
-
increment: (amount) ->
|
|
46
|
-
return if @disabled or @readOnly
|
|
47
|
-
@value = @_clamp(@_roundToStep(+@value + amount))
|
|
48
|
-
@emit 'input', @value
|
|
49
|
-
|
|
50
|
-
decrement: (amount) ->
|
|
51
|
-
return if @disabled or @readOnly
|
|
52
|
-
@value = @_clamp(@_roundToStep(+@value - amount))
|
|
53
|
-
@emit 'input', @value
|
|
54
|
-
|
|
55
|
-
_startRepeat: (dir, e) ->
|
|
56
|
-
amount = @_stepAmount(e)
|
|
57
|
-
tick = => if dir > 0 then @increment(amount) else @decrement(amount)
|
|
58
|
-
tick()
|
|
59
|
-
_timer = setTimeout =>
|
|
60
|
-
_interval = setInterval tick, TICK_DELAY
|
|
61
|
-
, START_DELAY
|
|
62
|
-
|
|
63
|
-
_stopRepeat: ->
|
|
64
|
-
clearTimeout _timer if _timer
|
|
65
|
-
clearInterval _interval if _interval
|
|
66
|
-
_timer = null
|
|
67
|
-
_interval = null
|
|
68
|
-
@emit 'change', @value
|
|
69
|
-
|
|
70
|
-
_onIncDown: (e) ->
|
|
71
|
-
return if @disabled or @readOnly or e.button isnt 0
|
|
72
|
-
e.preventDefault()
|
|
73
|
-
@_input?.focus()
|
|
74
|
-
@_startRepeat 1, e
|
|
75
|
-
onUp = =>
|
|
76
|
-
@_stopRepeat()
|
|
77
|
-
document.removeEventListener 'pointerup', onUp
|
|
78
|
-
document.addEventListener 'pointerup', onUp
|
|
79
|
-
|
|
80
|
-
_onDecDown: (e) ->
|
|
81
|
-
return if @disabled or @readOnly or e.button isnt 0
|
|
82
|
-
e.preventDefault()
|
|
83
|
-
@_input?.focus()
|
|
84
|
-
@_startRepeat -1, e
|
|
85
|
-
onUp = =>
|
|
86
|
-
@_stopRepeat()
|
|
87
|
-
document.removeEventListener 'pointerup', onUp
|
|
88
|
-
document.addEventListener 'pointerup', onUp
|
|
89
|
-
|
|
90
|
-
onKeydown: (e) ->
|
|
91
|
-
return if @disabled or @readOnly
|
|
92
|
-
amount = @_stepAmount(e)
|
|
93
|
-
switch e.key
|
|
94
|
-
when 'ArrowUp'
|
|
95
|
-
e.preventDefault()
|
|
96
|
-
@increment(amount)
|
|
97
|
-
@emit 'change', @value
|
|
98
|
-
when 'ArrowDown'
|
|
99
|
-
e.preventDefault()
|
|
100
|
-
@decrement(amount)
|
|
101
|
-
@emit 'change', @value
|
|
102
|
-
when 'PageUp'
|
|
103
|
-
e.preventDefault()
|
|
104
|
-
@increment(@largeStep)
|
|
105
|
-
@emit 'change', @value
|
|
106
|
-
when 'PageDown'
|
|
107
|
-
e.preventDefault()
|
|
108
|
-
@decrement(@largeStep)
|
|
109
|
-
@emit 'change', @value
|
|
110
|
-
when 'Home'
|
|
111
|
-
if @min?
|
|
112
|
-
e.preventDefault()
|
|
113
|
-
@value = @min
|
|
114
|
-
@emit 'change', @value
|
|
115
|
-
when 'End'
|
|
116
|
-
if @max?
|
|
117
|
-
e.preventDefault()
|
|
118
|
-
@value = @max
|
|
119
|
-
@emit 'change', @value
|
|
120
|
-
|
|
121
|
-
_onBlur: ->
|
|
122
|
-
val = parseFloat @_input?.value
|
|
123
|
-
unless isNaN(val)
|
|
124
|
-
@value = @_clamp(@_roundToStep(val))
|
|
125
|
-
@emit 'change', @value
|
|
126
|
-
|
|
127
|
-
_ready := false
|
|
128
|
-
|
|
129
|
-
mounted: -> _ready = true
|
|
130
|
-
|
|
131
|
-
~>
|
|
132
|
-
return unless _ready
|
|
133
|
-
@_input?.value = String(@value)
|
|
134
|
-
|
|
135
|
-
beforeUnmount: -> @_stopRepeat()
|
|
136
|
-
|
|
137
|
-
render
|
|
138
|
-
div role: "group", $disabled: @disabled?!, $readonly: @readOnly?!
|
|
139
|
-
button aria-label: "Decrease", tabindex: "-1"
|
|
140
|
-
$decrement: true
|
|
141
|
-
aria-controls: _id
|
|
142
|
-
disabled: @disabled or (@min? and @value <= @min)
|
|
143
|
-
@pointerdown: @_onDecDown
|
|
144
|
-
|
|
145
|
-
input ref: "_input", id: _id, type: "text", inputmode: "numeric"
|
|
146
|
-
name: @name?!
|
|
147
|
-
aria-roledescription: "Number field"
|
|
148
|
-
aria-valuenow: @value
|
|
149
|
-
aria-valuemin: @min?!
|
|
150
|
-
aria-valuemax: @max?!
|
|
151
|
-
aria-disabled: @disabled?!
|
|
152
|
-
aria-readonly: @readOnly?!
|
|
153
|
-
disabled: @disabled
|
|
154
|
-
readonly: @readOnly
|
|
155
|
-
@keydown: @onKeydown
|
|
156
|
-
@blur: @_onBlur
|
|
157
|
-
|
|
158
|
-
button aria-label: "Increase", tabindex: "-1"
|
|
159
|
-
$increment: true
|
|
160
|
-
aria-controls: _id
|
|
161
|
-
disabled: @disabled or (@max? and @value >= @max)
|
|
162
|
-
@pointerdown: @_onIncDown
|
package/docs/ui/otp-field.rip
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# OTPField — accessible headless one-time password input
|
|
2
|
-
#
|
|
3
|
-
# Multi-digit code input with auto-advance, backspace navigation, and
|
|
4
|
-
# paste support. Each digit gets its own input box. Ships zero CSS.
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# OTPField length: 6, value <=> code, @complete: handleVerify
|
|
8
|
-
|
|
9
|
-
export OTPField = component
|
|
10
|
-
@length:: number := 6
|
|
11
|
-
@value:: string := ""
|
|
12
|
-
@disabled:: boolean := false
|
|
13
|
-
@mask:: boolean := false
|
|
14
|
-
|
|
15
|
-
_id =! "otp-#{Math.random().toString(36).slice(2, 8)}"
|
|
16
|
-
|
|
17
|
-
_getInputs: ->
|
|
18
|
-
return [] unless @_root
|
|
19
|
-
Array.from(@_root.querySelectorAll('input'))
|
|
20
|
-
|
|
21
|
-
_focusAt: (idx) ->
|
|
22
|
-
inputs = @_getInputs()
|
|
23
|
-
inputs[idx]?.focus()
|
|
24
|
-
inputs[idx]?.select()
|
|
25
|
-
|
|
26
|
-
_updateValue: ->
|
|
27
|
-
inputs = @_getInputs()
|
|
28
|
-
digits = inputs.map (el) -> el.value
|
|
29
|
-
@value = digits.join('')
|
|
30
|
-
@emit 'input', @value
|
|
31
|
-
if @value.length is @length and digits.every (d) -> d.length is 1
|
|
32
|
-
@emit 'complete', @value
|
|
33
|
-
|
|
34
|
-
_onInput: (e, idx) ->
|
|
35
|
-
ch = e.target.value.slice(-1)
|
|
36
|
-
e.target.value = ch
|
|
37
|
-
@_updateValue()
|
|
38
|
-
@_focusAt(idx + 1) if ch and idx < @length - 1
|
|
39
|
-
|
|
40
|
-
_onKeydown: (e, idx) ->
|
|
41
|
-
switch e.key
|
|
42
|
-
when 'Backspace'
|
|
43
|
-
if not e.target.value and idx > 0
|
|
44
|
-
@_focusAt(idx - 1)
|
|
45
|
-
inputs = @_getInputs()
|
|
46
|
-
inputs[idx - 1]?.value = ''
|
|
47
|
-
@_updateValue()
|
|
48
|
-
when 'ArrowLeft'
|
|
49
|
-
e.preventDefault()
|
|
50
|
-
@_focusAt(idx - 1) if idx > 0
|
|
51
|
-
when 'ArrowRight'
|
|
52
|
-
e.preventDefault()
|
|
53
|
-
@_focusAt(idx + 1) if idx < @length - 1
|
|
54
|
-
when 'Home'
|
|
55
|
-
e.preventDefault()
|
|
56
|
-
@_focusAt(0)
|
|
57
|
-
when 'End'
|
|
58
|
-
e.preventDefault()
|
|
59
|
-
@_focusAt(@length - 1)
|
|
60
|
-
|
|
61
|
-
_onPaste: (e) ->
|
|
62
|
-
e.preventDefault()
|
|
63
|
-
text = (e.clipboardData?.getData('text') or '').replace(/\D/g, '').slice(0, @length)
|
|
64
|
-
return unless text
|
|
65
|
-
inputs = @_getInputs()
|
|
66
|
-
for ch, idx in text.split('')
|
|
67
|
-
inputs[idx]?.value = ch
|
|
68
|
-
@_updateValue()
|
|
69
|
-
@_focusAt(Math.min(text.length, @length - 1))
|
|
70
|
-
|
|
71
|
-
_onFocus: (e) -> e.target.select()
|
|
72
|
-
|
|
73
|
-
render
|
|
74
|
-
div ref: "_root", role: "group", aria-label: "One-time password"
|
|
75
|
-
$disabled: @disabled?!
|
|
76
|
-
$complete: (@value.length is @length)?!
|
|
77
|
-
for idx in [0...@length]
|
|
78
|
-
input id: "#{_id}-#{idx}"
|
|
79
|
-
type: if @mask then "password" else "text"
|
|
80
|
-
inputmode: "numeric"
|
|
81
|
-
autocomplete: "one-time-code"
|
|
82
|
-
maxlength: "1"
|
|
83
|
-
aria-label: "Digit #{idx + 1} of #{@length}"
|
|
84
|
-
disabled: @disabled
|
|
85
|
-
$filled: (@value[idx])?!
|
|
86
|
-
@input: (e) => @_onInput(e, idx)
|
|
87
|
-
@keydown: (e) => @_onKeydown(e, idx)
|
|
88
|
-
@paste: @_onPaste
|
|
89
|
-
@focus: @_onFocus
|
package/docs/ui/pagination.rip
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# Pagination — accessible headless page navigation
|
|
2
|
-
#
|
|
3
|
-
# Renders page buttons with prev/next and ellipsis gaps.
|
|
4
|
-
# Ships zero CSS.
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# Pagination page <=> currentPage, total: 100, perPage: 10
|
|
8
|
-
# Pagination page <=> currentPage, total: 500, perPage: 20, siblingCount: 2
|
|
9
|
-
|
|
10
|
-
export Pagination = component
|
|
11
|
-
@page:: number := 1
|
|
12
|
-
@total:: number := 0
|
|
13
|
-
@perPage:: number := 10
|
|
14
|
-
@siblingCount:: number := 1
|
|
15
|
-
|
|
16
|
-
totalPages ~= Math.max(1, Math.ceil(@total / @perPage))
|
|
17
|
-
_ready := false
|
|
18
|
-
|
|
19
|
-
_range: (start, fin) ->
|
|
20
|
-
len = fin - start + 1
|
|
21
|
-
Array.from {length: len}, (_, i) -> start + i
|
|
22
|
-
|
|
23
|
-
visiblePages ~=
|
|
24
|
-
tp = totalPages
|
|
25
|
-
sibs = @siblingCount
|
|
26
|
-
current = @page
|
|
27
|
-
|
|
28
|
-
totalNumbers = sibs * 2 + 5
|
|
29
|
-
return @_range(1, tp) if tp <= totalNumbers
|
|
30
|
-
|
|
31
|
-
leftSib = Math.max(current - sibs, 1)
|
|
32
|
-
rightSib = Math.min(current + sibs, tp)
|
|
33
|
-
|
|
34
|
-
showLeftDots = leftSib > 2
|
|
35
|
-
showRightDots = rightSib < tp - 1
|
|
36
|
-
|
|
37
|
-
if not showLeftDots and showRightDots
|
|
38
|
-
leftCount = 3 + 2 * sibs
|
|
39
|
-
leftRange = @_range(1, leftCount)
|
|
40
|
-
return [...leftRange, -1, tp]
|
|
41
|
-
|
|
42
|
-
if showLeftDots and not showRightDots
|
|
43
|
-
rightCount = 3 + 2 * sibs
|
|
44
|
-
rightRange = @_range(tp - rightCount + 1, tp)
|
|
45
|
-
return [1, -2, ...rightRange]
|
|
46
|
-
|
|
47
|
-
midRange = @_range(leftSib, rightSib)
|
|
48
|
-
[1, -2, ...midRange, -1, tp]
|
|
49
|
-
|
|
50
|
-
goto: (pg) ->
|
|
51
|
-
pg = Math.max(1, Math.min(pg, totalPages))
|
|
52
|
-
return if pg is @page
|
|
53
|
-
@page = pg
|
|
54
|
-
@emit 'change', @page
|
|
55
|
-
|
|
56
|
-
onKeydown: (e) ->
|
|
57
|
-
switch e.key
|
|
58
|
-
when 'ArrowLeft'
|
|
59
|
-
e.preventDefault()
|
|
60
|
-
@goto(@page - 1)
|
|
61
|
-
when 'ArrowRight'
|
|
62
|
-
e.preventDefault()
|
|
63
|
-
@goto(@page + 1)
|
|
64
|
-
when 'Home'
|
|
65
|
-
e.preventDefault()
|
|
66
|
-
@goto(1)
|
|
67
|
-
when 'End'
|
|
68
|
-
e.preventDefault()
|
|
69
|
-
@goto(totalPages)
|
|
70
|
-
|
|
71
|
-
mounted: ->
|
|
72
|
-
_ready = true
|
|
73
|
-
|
|
74
|
-
_prevPages = null
|
|
75
|
-
|
|
76
|
-
_rebuild: (inner) ->
|
|
77
|
-
frag = document.createDocumentFragment()
|
|
78
|
-
for pg in visiblePages
|
|
79
|
-
if pg < 0
|
|
80
|
-
el = document.createElement 'span'
|
|
81
|
-
el.setAttribute 'data-ellipsis', ''
|
|
82
|
-
el.textContent = '...'
|
|
83
|
-
else
|
|
84
|
-
el = document.createElement 'button'
|
|
85
|
-
el.setAttribute 'aria-label', "Page #{pg}"
|
|
86
|
-
el.setAttribute 'data-page', ''
|
|
87
|
-
el.textContent = "#{pg}"
|
|
88
|
-
el.addEventListener 'click', => @goto(pg)
|
|
89
|
-
frag.appendChild el
|
|
90
|
-
inner.replaceChildren frag
|
|
91
|
-
_prevPages = visiblePages.join ','
|
|
92
|
-
|
|
93
|
-
_syncActive: (inner) ->
|
|
94
|
-
for btn in inner.querySelectorAll('[data-page]')
|
|
95
|
-
pg = parseInt btn.textContent
|
|
96
|
-
if pg is @page
|
|
97
|
-
btn.setAttribute 'aria-current', 'page'
|
|
98
|
-
btn.setAttribute 'data-active', ''
|
|
99
|
-
else
|
|
100
|
-
btn.removeAttribute 'aria-current'
|
|
101
|
-
btn.removeAttribute 'data-active'
|
|
102
|
-
|
|
103
|
-
~>
|
|
104
|
-
return unless _ready
|
|
105
|
-
inner = @_nav?.querySelector('[data-pages]')
|
|
106
|
-
return unless inner
|
|
107
|
-
|
|
108
|
-
key = visiblePages.join ','
|
|
109
|
-
if key isnt _prevPages
|
|
110
|
-
@_rebuild inner
|
|
111
|
-
@_syncActive inner
|
|
112
|
-
|
|
113
|
-
render
|
|
114
|
-
nav ref: "_nav", aria-label: "Pagination", @keydown: @onKeydown
|
|
115
|
-
button $prev: true, aria-label: "Previous page"
|
|
116
|
-
disabled: @page <= 1
|
|
117
|
-
$disabled: (@page <= 1)?!
|
|
118
|
-
@click: (=> @goto(@page - 1))
|
|
119
|
-
. $pages: true
|
|
120
|
-
button $next: true, aria-label: "Next page"
|
|
121
|
-
disabled: @page >= totalPages
|
|
122
|
-
$disabled: (@page >= totalPages)?!
|
|
123
|
-
@click: (=> @goto(@page + 1))
|
package/docs/ui/popover.rip
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Popover — accessible headless popover with anchor positioning
|
|
2
|
-
#
|
|
3
|
-
# Uses the native Popover API (top-layer + light-dismiss) and CSS anchor
|
|
4
|
-
# positioning. Exposes $open, $placement on content. Ships zero CSS.
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# Popover placement: "bottom-start"
|
|
8
|
-
# button $trigger: true, "Click me"
|
|
9
|
-
# div $content: true
|
|
10
|
-
# p "Popover content"
|
|
11
|
-
|
|
12
|
-
export Popover = component
|
|
13
|
-
@placement:: "top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "right" := "bottom-start"
|
|
14
|
-
@offset:: number := 4
|
|
15
|
-
@disabled:: boolean := false
|
|
16
|
-
@openOnHover:: boolean := false
|
|
17
|
-
@hoverDelay:: number := 300
|
|
18
|
-
@hoverCloseDelay:: number := 200
|
|
19
|
-
|
|
20
|
-
open := false
|
|
21
|
-
_ready := false
|
|
22
|
-
_hoverTimer := null
|
|
23
|
-
_hoverCloseTimer := null
|
|
24
|
-
_id =! "pop-#{Math.random().toString(36).slice(2, 8)}"
|
|
25
|
-
|
|
26
|
-
_applyPlacement: ->
|
|
27
|
-
trigger = @_content?.querySelector('[data-trigger]')
|
|
28
|
-
floating = @_content?.querySelector('[data-content]')
|
|
29
|
-
[side, align] = @placement.split('-')
|
|
30
|
-
align ??= 'center'
|
|
31
|
-
ARIA.position trigger, floating, placement: "#{side} #{align}", offset: @offset
|
|
32
|
-
|
|
33
|
-
mounted: ->
|
|
34
|
-
_ready = true
|
|
35
|
-
trigger = @_content?.querySelector('[data-trigger]')
|
|
36
|
-
floating = @_content?.querySelector('[data-content]')
|
|
37
|
-
if trigger and floating
|
|
38
|
-
floating.id = _id
|
|
39
|
-
floating.setAttribute 'popover', 'auto'
|
|
40
|
-
trigger.setAttribute 'aria-expanded', false
|
|
41
|
-
trigger.setAttribute 'aria-haspopup', 'dialog'
|
|
42
|
-
trigger.setAttribute 'aria-controls', _id
|
|
43
|
-
trigger.addEventListener 'click', =>
|
|
44
|
-
return if @disabled
|
|
45
|
-
open = not open
|
|
46
|
-
trigger.addEventListener 'keydown', (e) =>
|
|
47
|
-
if e.key in ['Enter', ' ', 'ArrowDown']
|
|
48
|
-
e.preventDefault()
|
|
49
|
-
return if @disabled
|
|
50
|
-
open = not open
|
|
51
|
-
if @openOnHover
|
|
52
|
-
trigger.addEventListener 'mouseenter', =>
|
|
53
|
-
clearTimeout _hoverCloseTimer if _hoverCloseTimer
|
|
54
|
-
_hoverTimer = setTimeout (=> open = true), @hoverDelay
|
|
55
|
-
trigger.addEventListener 'mouseleave', =>
|
|
56
|
-
clearTimeout _hoverTimer if _hoverTimer
|
|
57
|
-
_hoverCloseTimer = setTimeout (=> open = false), @hoverCloseDelay
|
|
58
|
-
@_applyPlacement()
|
|
59
|
-
|
|
60
|
-
toggle: ->
|
|
61
|
-
return if @disabled
|
|
62
|
-
open = not open
|
|
63
|
-
|
|
64
|
-
openPopover: ->
|
|
65
|
-
open = true
|
|
66
|
-
|
|
67
|
-
close: ->
|
|
68
|
-
open = false
|
|
69
|
-
|
|
70
|
-
~>
|
|
71
|
-
return unless _ready
|
|
72
|
-
trigger = @_content?.querySelector('[data-trigger]')
|
|
73
|
-
floating = @_content?.querySelector('[data-content]')
|
|
74
|
-
if trigger
|
|
75
|
-
trigger.setAttribute 'aria-expanded', !!open
|
|
76
|
-
if floating
|
|
77
|
-
floating.setAttribute 'data-placement', @placement
|
|
78
|
-
if open then floating.setAttribute 'data-open', '' else floating.removeAttribute 'data-open'
|
|
79
|
-
ARIA.wireAria floating, _id
|
|
80
|
-
@_applyPlacement()
|
|
81
|
-
|
|
82
|
-
~>
|
|
83
|
-
return unless _ready
|
|
84
|
-
ARIA.bindPopover open, (=> @_content?.querySelector('[data-content]')), ((isOpen) => open = isOpen), (=> @_content?.querySelector('[data-trigger]'))
|
|
85
|
-
|
|
86
|
-
onKeydown: (e) ->
|
|
87
|
-
if e.key is 'Escape' and open
|
|
88
|
-
e.preventDefault()
|
|
89
|
-
@close()
|
|
90
|
-
|
|
91
|
-
render
|
|
92
|
-
div ref: "_content"
|
|
93
|
-
slot
|
package/docs/ui/preview-card.rip
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# PreviewCard — accessible headless hover preview card
|
|
2
|
-
#
|
|
3
|
-
# Shows a floating card on hover/focus of a trigger element. Dismisses
|
|
4
|
-
# on mouse leave or blur. Uses native `popover="hint"` for top-layer behavior.
|
|
5
|
-
# Ships zero CSS.
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# PreviewCard delay: 400
|
|
9
|
-
# a $trigger: true, href: "/user/42", "View Profile"
|
|
10
|
-
# div $content: true
|
|
11
|
-
# p "User details here..."
|
|
12
|
-
|
|
13
|
-
export PreviewCard = component
|
|
14
|
-
@delay:: number := 400
|
|
15
|
-
@closeDelay:: number := 200
|
|
16
|
-
|
|
17
|
-
open := false
|
|
18
|
-
_ready := false
|
|
19
|
-
_openTimer := null
|
|
20
|
-
_closeTimer := null
|
|
21
|
-
_id =! "pc-#{Math.random().toString(36).slice(2, 8)}"
|
|
22
|
-
|
|
23
|
-
beforeUnmount: ->
|
|
24
|
-
clearTimeout _openTimer if _openTimer
|
|
25
|
-
clearTimeout _closeTimer if _closeTimer
|
|
26
|
-
|
|
27
|
-
mounted: ->
|
|
28
|
-
_ready = true
|
|
29
|
-
trigger = @_root?.querySelector('[data-trigger]')
|
|
30
|
-
floating = @_root?.querySelector('[data-content]')
|
|
31
|
-
return unless trigger and floating
|
|
32
|
-
floating.id = _id
|
|
33
|
-
floating.setAttribute 'popover', 'hint'
|
|
34
|
-
trigger.setAttribute 'aria-controls', _id
|
|
35
|
-
trigger.setAttribute 'aria-expanded', false
|
|
36
|
-
trigger.addEventListener 'mouseenter', =>
|
|
37
|
-
clearTimeout _closeTimer if _closeTimer
|
|
38
|
-
_openTimer = setTimeout (=> open = true), @delay
|
|
39
|
-
trigger.addEventListener 'mouseleave', =>
|
|
40
|
-
clearTimeout _openTimer if _openTimer
|
|
41
|
-
_closeTimer = setTimeout (=> open = false), @closeDelay
|
|
42
|
-
@_applyPlacement()
|
|
43
|
-
trigger.addEventListener 'focus', =>
|
|
44
|
-
clearTimeout _closeTimer if _closeTimer
|
|
45
|
-
_openTimer = setTimeout (=> open = true; @_position()), @delay
|
|
46
|
-
trigger.addEventListener 'blur', =>
|
|
47
|
-
clearTimeout _openTimer if _openTimer
|
|
48
|
-
_closeTimer = setTimeout (=> open = false), @closeDelay
|
|
49
|
-
|
|
50
|
-
_applyPlacement: ->
|
|
51
|
-
trigger = @_root?.querySelector('[data-trigger]')
|
|
52
|
-
floating = @_root?.querySelector('[data-content]')
|
|
53
|
-
ARIA.position trigger, floating, placement: 'bottom start', offset: 4
|
|
54
|
-
|
|
55
|
-
~>
|
|
56
|
-
return unless _ready
|
|
57
|
-
trigger = @_root?.querySelector('[data-trigger]')
|
|
58
|
-
floating = @_root?.querySelector('[data-content]')
|
|
59
|
-
return unless floating and trigger
|
|
60
|
-
trigger.setAttribute 'aria-expanded', !!open
|
|
61
|
-
@_applyPlacement()
|
|
62
|
-
if open then floating.setAttribute('data-open', '') else floating.removeAttribute('data-open')
|
|
63
|
-
ARIA.bindPopover open, (=> floating), ((isOpen) => open = isOpen), (=> trigger)
|
|
64
|
-
if open
|
|
65
|
-
onEnter = => clearTimeout _closeTimer if _closeTimer
|
|
66
|
-
onLeave = => _closeTimer = setTimeout (=> open = false), @closeDelay
|
|
67
|
-
floating.addEventListener 'mouseenter', onEnter
|
|
68
|
-
floating.addEventListener 'mouseleave', onLeave
|
|
69
|
-
return ->
|
|
70
|
-
floating.removeEventListener 'mouseenter', onEnter
|
|
71
|
-
floating.removeEventListener 'mouseleave', onLeave
|
|
72
|
-
|
|
73
|
-
render
|
|
74
|
-
div ref: "_root"
|
|
75
|
-
slot
|
package/docs/ui/progress.rip
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Progress — accessible headless progress bar
|
|
2
|
-
#
|
|
3
|
-
# Exposes progress value as CSS custom property for styling.
|
|
4
|
-
# Ships zero CSS.
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# Progress value: 0.65
|
|
8
|
-
# Progress value: 42, max: 100
|
|
9
|
-
|
|
10
|
-
export Progress = component
|
|
11
|
-
@value:: number := 0
|
|
12
|
-
@max:: number := 1
|
|
13
|
-
@label:: any := null
|
|
14
|
-
|
|
15
|
-
percent ~= Math.min(100, Math.max(0, (@value / @max) * 100))
|
|
16
|
-
|
|
17
|
-
render
|
|
18
|
-
div role: "progressbar"
|
|
19
|
-
aria-valuenow: @value
|
|
20
|
-
aria-valuemin: 0
|
|
21
|
-
aria-valuemax: @max
|
|
22
|
-
aria-label: @label?!
|
|
23
|
-
style: "--progress-value: #{@value}; --progress-percent: #{percent}%"
|
|
24
|
-
$complete: (percent >= 100)?!
|
|
25
|
-
slot
|