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.
Files changed (112) hide show
  1. package/README.md +6 -4
  2. package/bin/rip +167 -12
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-APP.md +808 -0
  5. package/docs/RIP-DUCKDB.md +477 -0
  6. package/docs/RIP-INTRO.md +396 -0
  7. package/docs/RIP-LANG.md +59 -5
  8. package/docs/RIP-SCHEMA.md +191 -8
  9. package/docs/RIP-TYPES.md +74 -103
  10. package/docs/demo/README.md +4 -3
  11. package/docs/dist/rip.js +3627 -1470
  12. package/docs/dist/rip.min.js +671 -244
  13. package/docs/dist/rip.min.js.br +0 -0
  14. package/docs/example/index.json +7 -7
  15. package/docs/example/index.json.br +0 -0
  16. package/docs/extensions/duckdb/manifest.json +1 -1
  17. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  18. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  19. package/docs/extensions/vscode/print/index.html +2 -1
  20. package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
  21. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  22. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  23. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  24. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  25. package/docs/ui/bundle.json +61 -0
  26. package/docs/ui/bundle.json.br +0 -0
  27. package/docs/ui/hljs-rip.js +0 -7
  28. package/docs/ui/index.css +66 -23
  29. package/docs/ui/index.html +6 -6
  30. package/package.json +9 -3
  31. package/rip-loader.js +64 -2
  32. package/src/AGENTS.md +63 -36
  33. package/src/browser.js +96 -14
  34. package/src/compiler.js +960 -143
  35. package/src/components.js +794 -88
  36. package/src/{types-emit.js → dts.js} +181 -71
  37. package/src/grammar/README.md +1 -1
  38. package/src/grammar/grammar.rip +111 -97
  39. package/src/lexer.js +132 -18
  40. package/src/parser.js +203 -205
  41. package/src/repl.js +74 -6
  42. package/src/schema/runtime-orm.js +168 -4
  43. package/src/schema/runtime-validate.js +146 -2
  44. package/src/schema/runtime.generated.js +314 -6
  45. package/src/schema/schema.js +5 -5
  46. package/src/sourcemaps.js +277 -1
  47. package/src/stdlib.js +253 -0
  48. package/src/typecheck.js +2023 -106
  49. package/src/types.js +127 -7
  50. package/docs/ui/accordion.rip +0 -103
  51. package/docs/ui/alert-dialog.rip +0 -53
  52. package/docs/ui/autocomplete.rip +0 -115
  53. package/docs/ui/avatar.rip +0 -37
  54. package/docs/ui/badge.rip +0 -15
  55. package/docs/ui/breadcrumb.rip +0 -47
  56. package/docs/ui/button-group.rip +0 -26
  57. package/docs/ui/button.rip +0 -23
  58. package/docs/ui/card.rip +0 -25
  59. package/docs/ui/carousel.rip +0 -110
  60. package/docs/ui/checkbox-group.rip +0 -61
  61. package/docs/ui/checkbox.rip +0 -33
  62. package/docs/ui/collapsible.rip +0 -50
  63. package/docs/ui/combobox.rip +0 -130
  64. package/docs/ui/context-menu.rip +0 -88
  65. package/docs/ui/date-picker.rip +0 -206
  66. package/docs/ui/dialog.rip +0 -60
  67. package/docs/ui/drawer.rip +0 -58
  68. package/docs/ui/editable-value.rip +0 -82
  69. package/docs/ui/field.rip +0 -53
  70. package/docs/ui/fieldset.rip +0 -22
  71. package/docs/ui/form.rip +0 -39
  72. package/docs/ui/grid.rip +0 -901
  73. package/docs/ui/input-group.rip +0 -28
  74. package/docs/ui/input.rip +0 -36
  75. package/docs/ui/label.rip +0 -16
  76. package/docs/ui/menu.rip +0 -134
  77. package/docs/ui/menubar.rip +0 -151
  78. package/docs/ui/meter.rip +0 -36
  79. package/docs/ui/multi-select.rip +0 -203
  80. package/docs/ui/native-select.rip +0 -33
  81. package/docs/ui/nav-menu.rip +0 -126
  82. package/docs/ui/number-field.rip +0 -162
  83. package/docs/ui/otp-field.rip +0 -89
  84. package/docs/ui/pagination.rip +0 -123
  85. package/docs/ui/popover.rip +0 -93
  86. package/docs/ui/preview-card.rip +0 -75
  87. package/docs/ui/progress.rip +0 -25
  88. package/docs/ui/radio-group.rip +0 -57
  89. package/docs/ui/resizable.rip +0 -123
  90. package/docs/ui/scroll-area.rip +0 -145
  91. package/docs/ui/select.rip +0 -151
  92. package/docs/ui/separator.rip +0 -17
  93. package/docs/ui/skeleton.rip +0 -22
  94. package/docs/ui/slider.rip +0 -165
  95. package/docs/ui/spinner.rip +0 -17
  96. package/docs/ui/table.rip +0 -27
  97. package/docs/ui/tabs.rip +0 -113
  98. package/docs/ui/textarea.rip +0 -48
  99. package/docs/ui/toast.rip +0 -87
  100. package/docs/ui/toggle-group.rip +0 -71
  101. package/docs/ui/toggle.rip +0 -24
  102. package/docs/ui/toolbar.rip +0 -38
  103. package/docs/ui/tooltip.rip +0 -85
  104. package/src/app.rip +0 -1571
  105. package/src/sourcemap-merge.js +0 -287
  106. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  107. /package/docs/demo/{components → routes}/about.rip +0 -0
  108. /package/docs/demo/{components → routes}/card.rip +0 -0
  109. /package/docs/demo/{components → routes}/counter.rip +0 -0
  110. /package/docs/demo/{components → routes}/index.rip +0 -0
  111. /package/docs/demo/{components → routes}/todos.rip +0 -0
  112. /package/src/schema/{dts-emit.js → dts.js} +0 -0
@@ -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
@@ -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
@@ -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
@@ -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))
@@ -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
@@ -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
@@ -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