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,57 +0,0 @@
1
- # RadioGroup — accessible headless radio group
2
- #
3
- # Exactly one option can be selected. Arrow keys move focus and selection.
4
- # Ships zero CSS.
5
- #
6
- # Usage:
7
- # RadioGroup value <=> size
8
- # div $value: "sm", "Small"
9
- # div $value: "md", "Medium"
10
- # div $value: "lg", "Large"
11
-
12
- export RadioGroup = component
13
- @value:: any := null
14
- @disabled:: boolean := false
15
- @orientation:: "horizontal" | "vertical" := "vertical"
16
- @name:: string := ""
17
-
18
- _options ~=
19
- return [] unless @_slot
20
- Array.from(@_slot.querySelectorAll('[data-value]') or [])
21
-
22
- _select: (val) ->
23
- return if @disabled
24
- @value = val
25
- @emit 'change', @value
26
-
27
- onKeydown: (e) ->
28
- radios = @_root?.querySelectorAll('[role="radio"]')
29
- return unless radios?.length
30
- focused = Array.from(radios).indexOf(document.activeElement)
31
- return if focused < 0
32
- len = radios.length
33
- move = (idx) => radios[idx]?.focus(); @_select(_options[idx]?.dataset.value)
34
- ARIA.rovingNav e, {
35
- next: => move((focused + 1) %% len)
36
- prev: => move((focused - 1) %% len)
37
- first: => move(0)
38
- last: => move(len - 1)
39
- }, 'both'
40
-
41
- render
42
- div ref: "_root", role: "radiogroup", aria-orientation: @orientation
43
- $orientation: @orientation
44
- $disabled: @disabled?!
45
-
46
- . ref: "_slot", style: "display:none"
47
- slot
48
-
49
- for opt, idx in _options
50
- button role: "radio"
51
- tabindex: (if (opt.dataset.value is @value) or (@value is null and idx is 0) then "0" else "-1")
52
- aria-checked: opt.dataset.value is @value
53
- $checked: (opt.dataset.value is @value)?!
54
- $disabled: @disabled?!
55
- $value: opt.dataset.value
56
- @click: (=> @_select(opt.dataset.value))
57
- = opt.textContent
@@ -1,123 +0,0 @@
1
- # Resizable — accessible headless resizable panels
2
- #
3
- # Container with draggable handles between panels for resizing.
4
- # Panel sizes are stored as percentages and exposed via CSS
5
- # custom properties. Place [data-handle] elements between [data-panel]
6
- # elements. Ships zero CSS.
7
- #
8
- # Usage:
9
- # Resizable
10
- # div $panel: true
11
- # p "Left panel"
12
- # div $handle: true
13
- # div $panel: true
14
- # p "Right panel"
15
- #
16
- # Resizable orientation: "vertical", minSize: 20
17
- # div $panel: true, "Top"
18
- # div $handle: true
19
- # div $panel: true, "Bottom"
20
-
21
- export Resizable = component
22
- @orientation:: "horizontal" | "vertical" := "horizontal"
23
- @minSize:: number := 10
24
- @maxSize:: number := 90
25
-
26
- _ready := false
27
- _dragging = null
28
- _startPos = 0
29
- _startSizes = []
30
- sizes := []
31
-
32
- _panels: ->
33
- return [] unless @_root
34
- Array.from(@_root.querySelectorAll(':scope > [data-panel]') or [])
35
-
36
- _handles: ->
37
- return [] unless @_root
38
- Array.from(@_root.querySelectorAll(':scope > [data-handle]') or [])
39
-
40
- mounted: ->
41
- _ready = true
42
- panels = @_panels()
43
- count = panels.length
44
- if count and not sizes.length
45
- even = 100 / count
46
- sizes = Array.from {length: count}, -> even
47
-
48
- @_handles().forEach (handle, idx) =>
49
- handle.setAttribute 'role', 'separator'
50
- handle.setAttribute 'tabindex', '0'
51
- handle.addEventListener 'pointerdown', (e) => @_onPointerDown(idx, e)
52
- handle.addEventListener 'keydown', (e) => @_onKeydown(idx, e)
53
-
54
- _getPos: (e) ->
55
- if @orientation is 'horizontal' then e.clientX else e.clientY
56
-
57
- _getContainerSize: ->
58
- rect = @_root?.getBoundingClientRect()
59
- return 0 unless rect
60
- if @orientation is 'horizontal' then rect.width else rect.height
61
-
62
- _onPointerDown: (handleIdx, e) ->
63
- e.preventDefault()
64
- _dragging = handleIdx
65
- _startPos = @_getPos(e)
66
- _startSizes = [...sizes]
67
- e.target.setPointerCapture(e.pointerId)
68
- e.target.toggleAttribute 'data-dragging', true
69
-
70
- _onPointerMove: (e) ->
71
- return unless _dragging?
72
- total = @_getContainerSize()
73
- return unless total
74
- delta = @_getPos(e) - _startPos
75
- pctDelta = (delta / total) * 100
76
- @_applyResize(_dragging, _startSizes[_dragging] + pctDelta, _startSizes[_dragging + 1] - pctDelta)
77
-
78
- _onPointerUp: (e) ->
79
- return unless _dragging?
80
- handle = @_handles()[_dragging]
81
- handle?.removeAttribute 'data-dragging'
82
- _dragging = null
83
- @emit 'resize', sizes
84
-
85
- _applyResize: (idx, newLeft, newRight) ->
86
- newLeft = Math.max(@minSize, Math.min(@maxSize, newLeft))
87
- newRight = Math.max(@minSize, Math.min(@maxSize, newRight))
88
- combined = sizes[idx] + sizes[idx + 1]
89
- newRight = combined - newLeft
90
- return if newRight < @minSize or newRight > @maxSize
91
- updated = [...sizes]
92
- updated[idx] = newLeft
93
- updated[idx + 1] = newRight
94
- sizes = updated
95
-
96
- _onKeydown: (handleIdx, e) ->
97
- step = 10
98
- horiz = @orientation is 'horizontal'
99
- delta = switch e.key
100
- when (if horiz then 'ArrowRight' else 'ArrowDown') then step
101
- when (if horiz then 'ArrowLeft' else 'ArrowUp') then -step
102
- else null
103
- return unless delta?
104
- e.preventDefault()
105
- @_applyResize(handleIdx, sizes[handleIdx] + delta, sizes[handleIdx + 1] - delta)
106
- @emit 'resize', sizes
107
-
108
- ~>
109
- return unless _ready
110
- @_panels().forEach (el, idx) =>
111
- pct = sizes[idx] or 0
112
- el.style.setProperty '--panel-size', "#{pct}%"
113
- el.style.flexBasis = "#{pct}%"
114
- @_handles().forEach (handle, idx) =>
115
- handle.setAttribute 'aria-valuenow', Math.round(sizes[idx] or 0)
116
- handle.setAttribute 'aria-orientation', @orientation
117
-
118
- render
119
- div ref: "_root", $orientation: @orientation
120
- @pointermove: @_onPointerMove
121
- @pointerup: @_onPointerUp
122
- style: "display:flex; flex-direction:#{if @orientation is 'horizontal' then 'row' else 'column'}"
123
- slot
@@ -1,145 +0,0 @@
1
- # ScrollArea — accessible headless custom scrollbar
2
- #
3
- # Renders custom scrollbar thumb that tracks scroll position. Thumb is
4
- # draggable and the track is clickable. Auto-hides when not scrolling.
5
- # Ships zero CSS.
6
- #
7
- # Usage:
8
- # ScrollArea
9
- # div "Long scrollable content..."
10
-
11
- MIN_THUMB = 20
12
-
13
- export ScrollArea = component
14
- @orientation:: "horizontal" | "vertical" := "vertical"
15
-
16
- hovering := false
17
- scrolling := false
18
- _scrollTimer = null
19
- _dragStart = 0
20
- _dragScrollStart = 0
21
- _dragging = false
22
- _ready := false
23
-
24
- _updateThumb: ->
25
- vp = @_viewport
26
- sb = @_scrollbar
27
- th = @_thumb
28
- return unless vp and sb and th
29
- vert = @orientation is 'vertical'
30
-
31
- vpSize = if vert then vp.clientHeight else vp.clientWidth
32
- scSize = if vert then vp.scrollHeight else vp.scrollWidth
33
- sbSize = if vert then sb.clientHeight else sb.clientWidth
34
- scrollPos = if vert then vp.scrollTop else vp.scrollLeft
35
-
36
- ratio = vpSize / (scSize or 1)
37
- if ratio >= 1
38
- th.style.display = 'none'
39
- return
40
- th.style.display = ''
41
-
42
- thumbPx = Math.max(MIN_THUMB, sbSize * ratio)
43
- scrollRange = scSize - vpSize
44
- maxOffset = sbSize - thumbPx
45
- posPx = if scrollRange > 0 then Math.min(maxOffset, Math.max(0, (scrollPos / scrollRange) * maxOffset)) else 0
46
-
47
- if vert
48
- th.style.height = "#{thumbPx}px"
49
- th.style.transform = "translate3d(0,#{posPx}px,0)"
50
- else
51
- th.style.width = "#{thumbPx}px"
52
- th.style.transform = "translate3d(#{posPx}px,0,0)"
53
-
54
- _onScroll: ->
55
- scrolling = true
56
- clearTimeout _scrollTimer if _scrollTimer
57
- _scrollTimer = setTimeout (-> scrolling = false), 800
58
- @_updateThumb()
59
-
60
- _onTrackClick: (e) ->
61
- return if e.target is @_thumb
62
- vp = @_viewport
63
- sb = @_scrollbar
64
- th = @_thumb
65
- return unless vp and sb and th
66
- vert = @orientation is 'vertical'
67
- rect = sb.getBoundingClientRect()
68
- thumbPx = if vert then th.offsetHeight else th.offsetWidth
69
-
70
- if vert
71
- clickPos = e.clientY - rect.top - thumbPx / 2
72
- maxOffset = rect.height - thumbPx
73
- ratio = Math.max(0, Math.min(1, clickPos / maxOffset))
74
- vp.scrollTop = ratio * (vp.scrollHeight - vp.clientHeight)
75
- else
76
- clickPos = e.clientX - rect.left - thumbPx / 2
77
- maxOffset = rect.width - thumbPx
78
- ratio = Math.max(0, Math.min(1, clickPos / maxOffset))
79
- vp.scrollLeft = ratio * (vp.scrollWidth - vp.clientWidth)
80
-
81
- _onThumbDown: (e) ->
82
- return if e.button isnt 0
83
- e.preventDefault()
84
- e.stopPropagation()
85
- _dragging = true
86
- if @orientation is 'vertical'
87
- _dragStart = e.clientY
88
- _dragScrollStart = @_viewport.scrollTop
89
- else
90
- _dragStart = e.clientX
91
- _dragScrollStart = @_viewport.scrollLeft
92
- @_thumb.setPointerCapture e.pointerId
93
-
94
- _onThumbMove: (e) ->
95
- return unless _dragging
96
- vp = @_viewport
97
- sb = @_scrollbar
98
- th = @_thumb
99
- return unless vp and sb and th
100
- vert = @orientation is 'vertical'
101
- thumbPx = if vert then th.offsetHeight else th.offsetWidth
102
- sbSize = if vert then sb.clientHeight else sb.clientWidth
103
- maxOffset = sbSize - thumbPx
104
- return if maxOffset <= 0
105
-
106
- delta = if vert then e.clientY - _dragStart else e.clientX - _dragStart
107
- scrollRange = if vert then vp.scrollHeight - vp.clientHeight else vp.scrollWidth - vp.clientWidth
108
- newPos = _dragScrollStart + (delta / maxOffset) * scrollRange
109
-
110
- if vert then vp.scrollTop = newPos else vp.scrollLeft = newPos
111
-
112
- _onThumbUp: (e) ->
113
- _dragging = false
114
- @_thumb.releasePointerCapture e.pointerId
115
-
116
- mounted: ->
117
- _ready = true
118
- requestAnimationFrame => @_updateThumb()
119
- if @_viewport
120
- @_resizeObs = new ResizeObserver => @_updateThumb()
121
- @_resizeObs.observe @_viewport
122
- @_resizeObs.observe @_viewport.firstElementChild if @_viewport.firstElementChild
123
-
124
- beforeUnmount: ->
125
- @_resizeObs?.disconnect()
126
-
127
- render
128
- div $orientation: @orientation
129
- $hovering: hovering?!
130
- $scrolling: scrolling?!
131
- $dragging: _dragging?!
132
- @mouseenter: (=> hovering = true)
133
- @mouseleave: (=> hovering = false)
134
-
135
- div ref: "_viewport", $viewport: true
136
- style: "overflow:scroll;scrollbar-width:none"
137
- @scroll: @_onScroll
138
- slot
139
-
140
- div ref: "_scrollbar", $scrollbar: true
141
- @click: @_onTrackClick
142
- div ref: "_thumb", $thumb: true
143
- @pointerdown: @_onThumbDown
144
- @pointermove: @_onThumbMove
145
- @pointerup: @_onThumbUp
@@ -1,151 +0,0 @@
1
- # Select — accessible headless select widget
2
- #
3
- # Keyboard: ArrowDown/Up to navigate, Enter/Space to select, Escape to close,
4
- # Home/End for first/last, typeahead to jump by character.
5
- #
6
- # Exposes $open and $placeholder on button, $highlighted and $selected on options.
7
- # Uses native `popover="auto"` + anchor positioning.
8
- # Ships zero CSS — style entirely via attribute selectors in your stylesheet.
9
- #
10
- # Usage:
11
- # Select value <=> selectedValue, @change: (=> handle(event.detail))
12
- # option value: "a", "Option A"
13
- # option value: "b", "Option B"
14
-
15
- export Select = component
16
- @value:: any := null
17
- @placeholder:: string := "Select..."
18
- @disabled:: boolean := false
19
-
20
- open := false
21
- highlightedIndex := -1
22
- typeaheadBuffer := ''
23
- typeaheadTimer := null
24
- _listId =! "sel-#{Math.random().toString(36).slice(2, 8)}"
25
-
26
- getOpt: (o) -> o.dataset.value ?? o.value
27
-
28
- options ~=
29
- return [] unless @_slot
30
- Array.from(@_slot.querySelectorAll('[data-value], option[value]') or [])
31
-
32
- selectedLabel ~=
33
- if @value?
34
- el = options.find (o) -> @getOpt(o) is String(@value)
35
- el?.textContent?.trim() or String(@value)
36
- else
37
- @placeholder
38
-
39
- toggle: ->
40
- return if @disabled
41
- if open then @close() else @openMenu()
42
-
43
- openMenu: ->
44
- open = true
45
- highlightedIndex = Math.max(0, options.findIndex (o) -> @getOpt(o) is String(@value))
46
- requestAnimationFrame => @_focusHighlighted()
47
-
48
- close: ->
49
- open = false
50
- highlightedIndex = -1
51
- @_trigger?.focus()
52
-
53
- isDisabled: (opt) -> opt?.hasAttribute?('data-disabled') or opt?.disabled
54
-
55
- selectIndex: (idx) ->
56
- opt = options[idx]
57
- return unless opt
58
- return if @isDisabled(opt)
59
- @value = @getOpt(opt)
60
- @emit 'change', @value
61
- @close()
62
-
63
- onTriggerKeydown: (e) ->
64
- return if @disabled
65
- switch e.key
66
- when 'ArrowDown', 'ArrowUp', 'Enter', ' '
67
- e.preventDefault()
68
- @openMenu()
69
- when 'Escape'
70
- e.preventDefault()
71
- @close() if open
72
-
73
- _nextEnabled: (from, dir) ->
74
- len = options.length
75
- i = from
76
- loop len
77
- i = (i + dir) %% len
78
- return i unless @isDisabled(options[i])
79
- from
80
-
81
- onListKeydown: (e) ->
82
- return unless options.length
83
- ARIA.listNav e,
84
- next: => highlightedIndex = @_nextEnabled(highlightedIndex, 1); @_focusHighlighted()
85
- prev: => highlightedIndex = @_nextEnabled(highlightedIndex, -1); @_focusHighlighted()
86
- first: => highlightedIndex = 0; @_focusHighlighted()
87
- last: => highlightedIndex = options.length - 1; @_focusHighlighted()
88
- select: => @selectIndex(highlightedIndex)
89
- dismiss: => @close()
90
- tab: => @close()
91
- char: => @_typeahead(e.key)
92
-
93
- _typeahead: (char) ->
94
- clearTimeout typeaheadTimer if typeaheadTimer
95
- typeaheadBuffer += char.toLowerCase()
96
- typeaheadTimer = setTimeout (-> typeaheadBuffer = ''), 500
97
- idx = options.findIndex (o) -> o.textContent.trim().toLowerCase().startsWith(typeaheadBuffer)
98
- if idx >= 0
99
- highlightedIndex = idx
100
- @_focusHighlighted()
101
-
102
- _focusHighlighted: ->
103
- el = @_list?.querySelectorAll('[role="option"]')?.[highlightedIndex]
104
- el?.focus()
105
- el?.scrollIntoView { block: 'nearest' }
106
-
107
- _applyPlacement: ->
108
- ARIA.position @_trigger, @_list, placement: 'bottom start', offset: 4, matchWidth: true
109
-
110
- ~>
111
- if @_list
112
- @_list.setAttribute 'popover', 'auto'
113
- @_applyPlacement()
114
- ARIA.bindPopover open, (=> @_list), ((isOpen) => open = isOpen), (=> @_trigger)
115
-
116
- render
117
- .
118
-
119
- # Button
120
- button ref: "_trigger", role: "combobox"
121
- aria-expanded: !!open
122
- aria-haspopup: "listbox"
123
- aria-controls: open ? _listId : undefined
124
- $open: open?!
125
- $placeholder: (!@value)?!
126
- $disabled: @disabled?!
127
- disabled: @disabled
128
- @click: @toggle
129
- @keydown: @onTriggerKeydown
130
- span selectedLabel
131
-
132
- # Hidden slot for reading option definitions
133
- . ref: "_slot", style: "display:none"
134
- slot
135
-
136
- # Dropdown listbox
137
- div ref: "_list", id: _listId, role: "listbox", style: "position:fixed;margin:0;inset:auto"
138
- $open: open?!
139
- @keydown: @onListKeydown
140
- for opt, idx in options
141
- div role: "option"
142
- tabindex: "-1"
143
- $value: @getOpt(opt)
144
- $highlighted: (idx is highlightedIndex)?!
145
- $selected: (@getOpt(opt) is String(@value))?!
146
- $disabled: @isDisabled(opt)?!
147
- aria-selected: @getOpt(opt) is String(@value)
148
- aria-disabled: @isDisabled(opt)?!
149
- @click: (=> @selectIndex(idx))
150
- @mouseenter: (=> highlightedIndex = idx)
151
- = opt.textContent
@@ -1,17 +0,0 @@
1
- # Separator — accessible headless visual divider
2
- #
3
- # Decorative or semantic separator between content sections.
4
- # Ships zero CSS.
5
- #
6
- # Usage:
7
- # Separator
8
- # Separator orientation: "vertical"
9
-
10
- export Separator = component
11
- @orientation:: "horizontal" | "vertical" := "horizontal"
12
- @decorative:: boolean := true
13
-
14
- render
15
- div role: (if @decorative then 'none' else 'separator')
16
- aria-orientation: (if @orientation is 'vertical' then 'vertical' else undefined)
17
- $orientation: @orientation
@@ -1,22 +0,0 @@
1
- # Skeleton — accessible headless loading placeholder
2
- #
3
- # Placeholder element shown while content is loading.
4
- # Exposes dimensions as CSS custom properties for styling.
5
- # Ships zero CSS.
6
- #
7
- # Usage:
8
- # Skeleton
9
- # Skeleton width: "200px", height: "1em"
10
- # Skeleton circle: true, width: "48px"
11
-
12
- export Skeleton = component
13
- @width:: any := null
14
- @height:: any := null
15
- @circle:: boolean := false
16
- @label:: string := "Loading"
17
-
18
- render
19
- div role: "status", aria-busy: "true", aria-label: @label
20
- style: "--skeleton-width: #{@width or 'auto'}; --skeleton-height: #{@height or 'auto'}"
21
- $circle: @circle?!
22
- slot
@@ -1,165 +0,0 @@
1
- # Slider — accessible headless range slider
2
- #
3
- # Supports single and range (multi-thumb) modes, pointer drag with capture,
4
- # keyboard stepping, and CSS custom properties for thumb/indicator positioning.
5
- # Ships zero CSS.
6
- #
7
- # Usage:
8
- # Slider value <=> volume
9
- # Slider value <=> volume, min: 0, max: 100, step: 5
10
- # Slider value <=> range, min: 0, max: 100 (pass array for range mode)
11
-
12
- export Slider = component
13
- @value:: number := 0
14
- @min:: number := 0
15
- @max:: number := 100
16
- @step:: number := 1
17
- @largeStep:: number := 10
18
- @orientation:: "horizontal" | "vertical" := "horizontal"
19
- @disabled:: boolean := false
20
- @name:: any := null
21
- @valueText:: any := null
22
-
23
- dragging := false
24
- activeThumb := -1
25
- _thumbOffset = 0
26
- _id =! "sld-#{Math.random().toString(36).slice(2, 8)}"
27
-
28
- isRange ~= Array.isArray(@value)
29
- values ~= if isRange then @value else [@value]
30
- horiz ~= @orientation is 'horizontal'
31
-
32
- _clamp: (v) -> Math.min(@max, Math.max(@min, v))
33
-
34
- _roundToStep: (v) ->
35
- rounded = Math.round((v - @min) / @step) * @step + @min
36
- precision = String(@step).split('.')[1]?.length or 0
37
- parseFloat rounded.toFixed(precision)
38
-
39
- _percentOf: (v) -> ((@_clamp(v) - @min) / (@max - @min)) * 100
40
-
41
- _valueFromPointer: (e) ->
42
- rect = @_track.getBoundingClientRect()
43
- if horiz
44
- ratio = (e.clientX - _thumbOffset - rect.left) / rect.width
45
- else
46
- ratio = 1 - (e.clientY - _thumbOffset - rect.top) / rect.height
47
- ratio = Math.max(0, Math.min(1, ratio))
48
- @_roundToStep(@min + ratio * (@max - @min))
49
-
50
- _closestThumb: (e) ->
51
- return 0 unless isRange
52
- rect = @_track.getBoundingClientRect()
53
- pos = if horiz then (e.clientX - rect.left) / rect.width else 1 - (e.clientY - rect.top) / rect.height
54
- best = 0
55
- bestDist = Infinity
56
- for v, i in values
57
- pct = @_percentOf(v) / 100
58
- dist = Math.abs(pos - pct)
59
- if dist < bestDist
60
- bestDist = dist
61
- best = i
62
- best
63
-
64
- _setValue: (idx, val) ->
65
- val = @_clamp(@_roundToStep(val))
66
- if isRange
67
- arr = [...values]
68
- arr[idx] = val
69
- arr.sort (a, b) -> a - b
70
- @value = arr
71
- else
72
- @value = val
73
- @emit 'input', @value
74
-
75
- _commitValue: ->
76
- @emit 'change', @value
77
-
78
- _onPointerDown: (e) ->
79
- return if @disabled or e.button isnt 0
80
- e.preventDefault()
81
- idx = @_closestThumb(e)
82
- activeThumb = idx
83
- dragging = true
84
-
85
- thumb = @_track.querySelectorAll('[data-thumb]')[idx]
86
- if thumb
87
- tr = thumb.getBoundingClientRect()
88
- if horiz
89
- _thumbOffset = e.clientX - (tr.left + tr.width / 2)
90
- else
91
- _thumbOffset = e.clientY - (tr.top + tr.height / 2)
92
- else
93
- _thumbOffset = 0
94
-
95
- newVal = @_valueFromPointer(e)
96
- @_setValue idx, newVal
97
-
98
- @_track.setPointerCapture e.pointerId
99
-
100
- _onPointerMove: (e) ->
101
- return unless dragging
102
- newVal = @_valueFromPointer(e)
103
- @_setValue activeThumb, newVal
104
-
105
- _onPointerUp: (e) ->
106
- return unless dragging
107
- dragging = false
108
- activeThumb = -1
109
- _thumbOffset = 0
110
- @_track.releasePointerCapture e.pointerId
111
- @_commitValue()
112
-
113
- _onKeydown: (e, idx) ->
114
- s = if e.shiftKey then @largeStep else @step
115
- v = values[idx]
116
- newVal = switch e.key
117
- when 'ArrowRight', 'ArrowUp' then v + s
118
- when 'ArrowLeft', 'ArrowDown' then v - s
119
- when 'PageUp' then v + @largeStep
120
- when 'PageDown' then v - @largeStep
121
- when 'Home' then @min
122
- when 'End' then @max
123
- else null
124
- if newVal?
125
- e.preventDefault()
126
- @_setValue idx, newVal
127
- @_commitValue()
128
-
129
- render
130
- div role: "group", $orientation: @orientation, $disabled: @disabled?!, $dragging: dragging?!
131
- style: "--slider-min: #{@min}; --slider-max: #{@max}"
132
-
133
- # Track
134
- div ref: "_track", $track: true
135
- style: "position:relative"
136
- @pointerdown: @_onPointerDown
137
- @pointermove: @_onPointerMove
138
- @pointerup: @_onPointerUp
139
-
140
- # Indicator (filled portion)
141
- if isRange
142
- div $indicator: true
143
- style: "position:absolute; #{if horiz then 'left' else 'bottom'}: #{@_percentOf(values[0])}%; #{if horiz then 'width' else 'height'}: #{@_percentOf(values[1]) - @_percentOf(values[0])}%"
144
- else
145
- div $indicator: true
146
- style: "position:absolute; #{if horiz then 'left: 0; width' else 'bottom: 0; height'}: #{@_percentOf(values[0])}%"
147
-
148
- # Thumbs
149
- for val, idx in values
150
- div $thumb: true, $active: (idx is activeThumb)?!
151
- style: "position:absolute; #{if horiz then 'left' else 'bottom'}: #{@_percentOf(val)}%; z-index: #{if idx is activeThumb then 2 else 1}"
152
- @keydown: (e) => @_onKeydown(e, idx)
153
- input type: "range", style: "position:absolute;opacity:0;width:0;height:0;pointer-events:none"
154
- id: "#{_id}-thumb-#{idx}"
155
- name: @name?!
156
- min: @min, max: @max, step: @step
157
- value: val
158
- aria-valuenow: val
159
- aria-valuemin: @min
160
- aria-valuemax: @max
161
- aria-valuetext: if @valueText then @valueText(val, idx) else undefined
162
- aria-orientation: @orientation
163
- aria-disabled: @disabled?!
164
-
165
- slot
@@ -1,17 +0,0 @@
1
- # Spinner — accessible headless loading indicator
2
- #
3
- # Announces loading state to screen readers via role="status".
4
- # Exposes size as a CSS custom property for styling.
5
- # Ships zero CSS.
6
- #
7
- # Usage:
8
- # Spinner
9
- # Spinner label: "Saving...", size: "24px"
10
-
11
- export Spinner = component
12
- @label:: string := "Loading"
13
- @size:: any := null
14
-
15
- render
16
- div role: "status", aria-label: @label
17
- style: if @size then "--spinner-size: #{@size}" else undefined