@rip-lang/ui 0.3.19 → 0.3.21

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 (61) hide show
  1. package/README.md +443 -576
  2. package/accordion.rip +113 -0
  3. package/alert-dialog.rip +96 -0
  4. package/autocomplete.rip +141 -0
  5. package/avatar.rip +37 -0
  6. package/badge.rip +15 -0
  7. package/breadcrumb.rip +46 -0
  8. package/button-group.rip +26 -0
  9. package/button.rip +23 -0
  10. package/card.rip +25 -0
  11. package/carousel.rip +110 -0
  12. package/checkbox-group.rip +65 -0
  13. package/checkbox.rip +33 -0
  14. package/collapsible.rip +50 -0
  15. package/combobox.rip +155 -0
  16. package/context-menu.rip +105 -0
  17. package/date-picker.rip +214 -0
  18. package/dialog.rip +107 -0
  19. package/drawer.rip +79 -0
  20. package/editable-value.rip +80 -0
  21. package/field.rip +53 -0
  22. package/fieldset.rip +22 -0
  23. package/form.rip +39 -0
  24. package/grid.rip +901 -0
  25. package/index.rip +16 -0
  26. package/input-group.rip +28 -0
  27. package/input.rip +36 -0
  28. package/label.rip +16 -0
  29. package/menu.rip +162 -0
  30. package/menubar.rip +155 -0
  31. package/meter.rip +36 -0
  32. package/multi-select.rip +158 -0
  33. package/native-select.rip +32 -0
  34. package/nav-menu.rip +129 -0
  35. package/number-field.rip +162 -0
  36. package/otp-field.rip +89 -0
  37. package/package.json +18 -27
  38. package/pagination.rip +123 -0
  39. package/popover.rip +143 -0
  40. package/preview-card.rip +73 -0
  41. package/progress.rip +25 -0
  42. package/radio-group.rip +67 -0
  43. package/resizable.rip +123 -0
  44. package/scroll-area.rip +145 -0
  45. package/select.rip +184 -0
  46. package/separator.rip +17 -0
  47. package/skeleton.rip +22 -0
  48. package/slider.rip +165 -0
  49. package/spinner.rip +17 -0
  50. package/table.rip +27 -0
  51. package/tabs.rip +124 -0
  52. package/textarea.rip +48 -0
  53. package/toast.rip +87 -0
  54. package/toggle-group.rip +78 -0
  55. package/toggle.rip +24 -0
  56. package/toolbar.rip +46 -0
  57. package/tooltip.rip +115 -0
  58. package/dist/rip-ui.min.js +0 -524
  59. package/dist/rip-ui.min.js.br +0 -0
  60. package/serve.rip +0 -92
  61. package/ui.rip +0 -964
package/accordion.rip ADDED
@@ -0,0 +1,113 @@
1
+ # Accordion — accessible headless expand/collapse widget
2
+ #
3
+ # Supports single or multiple expanded sections. Keyboard: Enter/Space to
4
+ # toggle, ArrowDown/Up to move between triggers. Exposes $open on items.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Accordion multiple: false
9
+ # div $item: "a"
10
+ # button $trigger: true, "Section A"
11
+ # div $content: true
12
+ # p "Content A"
13
+ # div $item: "b"
14
+ # button $trigger: true, "Section B"
15
+ # div $content: true
16
+ # p "Content B"
17
+
18
+ export Accordion = component
19
+ @multiple := false
20
+
21
+ openItems := new Set()
22
+ _ready := false
23
+ _id =! "acc-#{Math.random().toString(36).slice(2, 8)}"
24
+
25
+ mounted: ->
26
+ _ready = true
27
+ @_content?.querySelectorAll('[data-trigger]').forEach (trigger) =>
28
+ item = trigger.closest('[data-item]')
29
+ return unless item
30
+ id = item.dataset.item
31
+ trigger.addEventListener 'click', =>
32
+ return if item.hasAttribute('data-disabled')
33
+ @toggle(id)
34
+ trigger.addEventListener 'keydown', (e) => @onTriggerKeydown(e, id)
35
+
36
+ ~>
37
+ return unless _ready
38
+ @_content?.querySelectorAll('[data-item]').forEach (item) =>
39
+ id = item.dataset.item
40
+ isOpen = openItems.has(id)
41
+ item.toggleAttribute 'data-open', isOpen
42
+ trigger = item.querySelector('[data-trigger]')
43
+ content = item.querySelector('[data-content]')
44
+ triggerId = "#{_id}-trigger-#{id}"
45
+ panelId = "#{_id}-panel-#{id}"
46
+ if trigger
47
+ isDisabled = item.hasAttribute('data-disabled')
48
+ trigger.id = triggerId
49
+ trigger.setAttribute 'aria-expanded', isOpen
50
+ trigger.setAttribute 'aria-controls', panelId
51
+ trigger.setAttribute 'aria-disabled', isDisabled if isDisabled
52
+ trigger.tabIndex = if isDisabled then -1 else 0
53
+ if content
54
+ content.id = panelId
55
+ content.hidden = if isOpen then false else 'until-found'
56
+ content.setAttribute 'role', 'region'
57
+ content.setAttribute 'aria-labelledby', triggerId
58
+ if isOpen
59
+ rect = content.getBoundingClientRect()
60
+ content.style.setProperty '--accordion-panel-height', "#{rect.height}px"
61
+ content.style.setProperty '--accordion-panel-width', "#{rect.width}px"
62
+
63
+ toggle: (id) ->
64
+ if openItems.has(id)
65
+ openItems.delete(id)
66
+ else
67
+ openItems.clear() unless @multiple
68
+ openItems.add(id)
69
+ openItems = new Set(openItems)
70
+ @emit 'change', Array.from(openItems)
71
+
72
+ isOpen: (id) ->
73
+ openItems.has(id)
74
+
75
+ onTriggerKeydown: (e, id) ->
76
+ item = e.currentTarget.closest('[data-item]')
77
+ return if item?.hasAttribute('data-disabled') and e.key in ['Enter', ' ']
78
+ switch e.key
79
+ when 'Enter', ' '
80
+ e.preventDefault()
81
+ @toggle(id)
82
+ when 'ArrowDown'
83
+ e.preventDefault()
84
+ @_focusNext(1)
85
+ when 'ArrowUp'
86
+ e.preventDefault()
87
+ @_focusNext(-1)
88
+ when 'Home'
89
+ e.preventDefault()
90
+ @_focusTrigger(0)
91
+ when 'End'
92
+ e.preventDefault()
93
+ @_focusTrigger(-1)
94
+
95
+ _triggers: ->
96
+ return [] unless @_content
97
+ Array.from(@_content.querySelectorAll('[data-trigger]'))
98
+
99
+ _focusNext: (dir) ->
100
+ triggers = @_triggers()
101
+ idx = triggers.indexOf(document.activeElement)
102
+ return if idx is -1
103
+ next = (idx + dir) %% triggers.length
104
+ triggers[next]?.focus()
105
+
106
+ _focusTrigger: (idx) ->
107
+ triggers = @_triggers()
108
+ target = if idx < 0 then triggers[triggers.length - 1] else triggers[idx]
109
+ target?.focus()
110
+
111
+ render
112
+ div ref: "_content"
113
+ slot
@@ -0,0 +1,96 @@
1
+ # AlertDialog — accessible headless non-dismissable modal
2
+ #
3
+ # A Dialog variant that requires explicit user action to close.
4
+ # Cannot be dismissed by clicking outside or pressing Escape.
5
+ # Use for destructive confirmations, unsaved changes, etc.
6
+ # Ships zero CSS.
7
+ #
8
+ # Usage:
9
+ # AlertDialog open <=> showConfirm
10
+ # h2 "Delete account?"
11
+ # p "This action cannot be undone."
12
+ # button @click: (=> showConfirm = false), "Cancel"
13
+ # button @click: handleDelete, "Delete"
14
+
15
+ alertDialogStack = []
16
+
17
+ export AlertDialog = component
18
+ @open := false
19
+ @initialFocus := null
20
+
21
+ _prevFocus = null
22
+ _cleanupTrap = null
23
+ _scrollY = 0
24
+ _id =! "adlg-#{Math.random().toString(36).slice(2, 8)}"
25
+
26
+ _wireAria: ->
27
+ panel = @_panel
28
+ return unless panel
29
+ heading = panel.querySelector('h1,h2,h3,h4,h5,h6')
30
+ if heading
31
+ heading.id ?= "#{_id}-title"
32
+ panel.setAttribute 'aria-labelledby', heading.id
33
+ desc = panel.querySelector('p')
34
+ if desc
35
+ desc.id ?= "#{_id}-desc"
36
+ panel.setAttribute 'aria-describedby', desc.id
37
+
38
+ ~>
39
+ if @open
40
+ _prevFocus = document.activeElement
41
+ _scrollY = window.scrollY
42
+ alertDialogStack.push this
43
+ document.body.style.position = 'fixed'
44
+ document.body.style.top = "-#{_scrollY}px"
45
+ document.body.style.width = '100%'
46
+
47
+ setTimeout =>
48
+ panel = @_panel
49
+ if panel
50
+ @_wireAria()
51
+ if @initialFocus
52
+ target = if typeof @initialFocus is 'string' then panel.querySelector(@initialFocus) else @initialFocus
53
+ target?.focus()
54
+ else
55
+ focusable = panel.querySelectorAll 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
56
+ focusable[0]?.focus()
57
+ _cleanupTrap = (e) ->
58
+ return unless e.key is 'Tab'
59
+ list = Array.from(panel.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter (f) -> f.offsetParent isnt null
60
+ return unless list.length
61
+ first = list[0]
62
+ last = list[list.length - 1]
63
+ if e.shiftKey
64
+ if document.activeElement is first then (e.preventDefault(); last.focus())
65
+ else
66
+ if document.activeElement is last then (e.preventDefault(); first.focus())
67
+ panel.addEventListener 'keydown', _cleanupTrap
68
+ , 0
69
+
70
+ return ->
71
+ idx = alertDialogStack.indexOf this
72
+ alertDialogStack.splice(idx, 1) if idx >= 0
73
+ document.body.style.position = '' unless alertDialogStack.length
74
+ document.body.style.top = '' unless alertDialogStack.length
75
+ document.body.style.width = '' unless alertDialogStack.length
76
+ window.scrollTo 0, _scrollY unless alertDialogStack.length
77
+ _prevFocus?.focus()
78
+ else
79
+ idx = alertDialogStack.indexOf this
80
+ alertDialogStack.splice(idx, 1) if idx >= 0
81
+ unless alertDialogStack.length
82
+ document.body.style.position = ''
83
+ document.body.style.top = ''
84
+ document.body.style.width = ''
85
+ window.scrollTo 0, _scrollY
86
+ _prevFocus?.focus()
87
+
88
+ close: ->
89
+ @open = false
90
+ @emit 'close'
91
+
92
+ render
93
+ if @open
94
+ div ref: "_backdrop", $open: true
95
+ div ref: "_panel", role: "alertdialog", aria-modal: "true", tabindex: "-1"
96
+ slot
@@ -0,0 +1,141 @@
1
+ # Autocomplete — accessible headless suggestion input
2
+ #
3
+ # Like Combobox but the input value IS the value (no selection from a list).
4
+ # Suggestions are shown as the user types; selecting a suggestion fills the input.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Autocomplete value <=> city, items: cities, @filter: filterCities
9
+
10
+ acCollator = new Intl.Collator(undefined, { sensitivity: 'base' })
11
+
12
+ export Autocomplete = component
13
+ @value := ''
14
+ @items := []
15
+ @placeholder := 'Type to search...'
16
+ @disabled := false
17
+
18
+ open := false
19
+
20
+ filteredItems ~=
21
+ q = @value.trim()
22
+ return @items unless q
23
+ @items.filter (item) ->
24
+ label = if typeof item is 'string' then item else (item.label or item.name or String(item))
25
+ acCollator.compare(label.slice(0, q.length), q) is 0
26
+
27
+ _listId =! "ac-list-#{Math.random().toString(36).slice(2, 8)}"
28
+
29
+ _getItems: ->
30
+ return [] unless @_list
31
+ Array.from(@_list.querySelectorAll('[role="option"]'))
32
+
33
+ _updateHighlight: ->
34
+ idx = @_hlIdx
35
+ opts = @_getItems()
36
+ opts.forEach (el, ndx) ->
37
+ el.id = "#{@_listId}-opt-#{ndx}" unless el.id
38
+ el.toggleAttribute 'data-highlighted', ndx is idx
39
+ activeId = if idx >= 0 and opts[idx] then opts[idx].id else undefined
40
+ if @_input
41
+ if activeId then @_input.setAttribute 'aria-activedescendant', activeId
42
+ else @_input.removeAttribute 'aria-activedescendant'
43
+ opts[idx]?.scrollIntoView({ block: 'nearest' })
44
+
45
+ openMenu: ->
46
+ open = true
47
+ @_hlIdx = -1
48
+ setTimeout => @_position(), 0
49
+
50
+ close: ->
51
+ open = false
52
+ @_hlIdx = -1
53
+
54
+ _position: ->
55
+ return unless @_input and @_list
56
+ tr = @_input.getBoundingClientRect()
57
+ @_list.style.position = 'fixed'
58
+ @_list.style.left = "#{tr.left}px"
59
+ @_list.style.top = "#{tr.bottom + 2}px"
60
+ @_list.style.minWidth = "#{tr.width}px"
61
+ fl = @_list.getBoundingClientRect()
62
+ if fl.bottom > window.innerHeight
63
+ @_list.style.top = "#{tr.top - fl.height - 2}px"
64
+
65
+ selectIndex: (idx) ->
66
+ item = filteredItems[idx]
67
+ return unless item
68
+ label = if typeof item is 'string' then item else (item.label or item.name or String(item))
69
+ @value = label
70
+ @_input?.value = label
71
+ @emit 'select', item
72
+ @close()
73
+
74
+ onInput: (e) ->
75
+ newVal = e.target.value
76
+ return if newVal is @value
77
+ @value = newVal
78
+ open = true
79
+ @_hlIdx = if filteredItems.length > 0 then 0 else -1
80
+ setTimeout =>
81
+ @_position()
82
+ @_updateHighlight()
83
+ , 0
84
+
85
+ onKeydown: (e) ->
86
+ len = filteredItems.length
87
+ switch e.key
88
+ when 'ArrowDown'
89
+ e.preventDefault()
90
+ @openMenu() unless open
91
+ if len
92
+ @_hlIdx = (@_hlIdx + 1) %% len
93
+ @_updateHighlight()
94
+ when 'ArrowUp'
95
+ e.preventDefault()
96
+ @openMenu() unless open
97
+ if len
98
+ @_hlIdx = if @_hlIdx <= 0 then len - 1 else @_hlIdx - 1
99
+ @_updateHighlight()
100
+ when 'Enter'
101
+ e.preventDefault()
102
+ @selectIndex(@_hlIdx) if @_hlIdx >= 0
103
+ when 'Escape'
104
+ e.preventDefault()
105
+ @close()
106
+ when 'Tab'
107
+ @close()
108
+
109
+ ~>
110
+ if open
111
+ onDown = (e) =>
112
+ unless @_input?.contains(e.target) or @_list?.contains(e.target)
113
+ @close()
114
+ document.addEventListener 'mousedown', onDown
115
+ return -> document.removeEventListener 'mousedown', onDown
116
+
117
+ mounted: ->
118
+ @_hlIdx = -1
119
+ @_input.value = @value if @_input and @value
120
+
121
+ render
122
+ . $open: open?!
123
+
124
+ input ref: "_input", role: "combobox", type: "text"
125
+ autocomplete: "off"
126
+ aria-expanded: !!open
127
+ aria-haspopup: "listbox"
128
+ aria-autocomplete: "list"
129
+ aria-controls: open ? _listId : undefined
130
+ $disabled: @disabled?!
131
+ disabled: @disabled
132
+ placeholder: @placeholder
133
+ @input: @onInput
134
+
135
+ if open and filteredItems.length > 0
136
+ div ref: "_list", id: _listId, role: "listbox", $open: true, style: "position:fixed"
137
+ for item, idx in filteredItems
138
+ div role: "option", tabindex: "-1"
139
+ @click: (=> @selectIndex(idx))
140
+ @mouseenter: (=> @_hlIdx = idx; @_updateHighlight())
141
+ "#{if typeof item is 'string' then item else (item.label or item.name or String(item))}"
package/avatar.rip ADDED
@@ -0,0 +1,37 @@
1
+ # Avatar — accessible headless avatar
2
+ #
3
+ # Shows an image, falls back to initials or a generic icon placeholder.
4
+ # Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # Avatar src: user.photoUrl, alt: user.name, fallback: "AC"
8
+ # Avatar fallback: "JD"
9
+ # Avatar
10
+
11
+ export Avatar = component
12
+ @src := ''
13
+ @alt := ''
14
+ @fallback := ''
15
+
16
+ imgError := false
17
+
18
+ _onError: -> imgError = true
19
+
20
+ _initials ~=
21
+ return @fallback if @fallback
22
+ return '' unless @alt
23
+ parts = @alt.trim().split(/\s+/)
24
+ chars = parts.map (p) -> p[0]?.toUpperCase() or ''
25
+ chars.slice(0, 2).join('')
26
+
27
+ render
28
+ span role: "img", aria-label: @alt or 'Avatar'
29
+ $status: if @src and not imgError then 'image' else if _initials then 'fallback' else 'placeholder'
30
+ if @src and not imgError
31
+ img src: @src, alt: @alt, @error: @_onError
32
+ else if _initials
33
+ span $initials: true
34
+ _initials
35
+ else
36
+ span $placeholder: true
37
+ "?"
package/badge.rip ADDED
@@ -0,0 +1,15 @@
1
+ # Badge — accessible headless inline label
2
+ #
3
+ # Decorative label for status, counts, or categories.
4
+ # Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # Badge "New"
8
+ # Badge variant: "outline", "Beta"
9
+
10
+ export Badge = component
11
+ @variant := 'solid'
12
+
13
+ render
14
+ span $variant: @variant
15
+ slot
package/breadcrumb.rip ADDED
@@ -0,0 +1,46 @@
1
+ # Breadcrumb — accessible headless navigation breadcrumb
2
+ #
3
+ # Renders a navigation trail with separator between items.
4
+ # The last item is automatically marked as the current page.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Breadcrumb
9
+ # a $item: true, href: "/", "Home"
10
+ # a $item: true, href: "/products", "Products"
11
+ # span $item: true, "Widget Pro"
12
+ #
13
+ # Breadcrumb separator: ">"
14
+ # a $item: true, href: "/", "Home"
15
+ # span $item: true, "Settings"
16
+
17
+ export Breadcrumb = component
18
+ @separator := '/'
19
+ @label := 'Breadcrumb'
20
+
21
+ _ready := false
22
+
23
+ mounted: -> _ready = true
24
+
25
+ _items ~=
26
+ return [] unless _ready
27
+ return [] unless @_content
28
+ Array.from(@_content.querySelectorAll('[data-item]') or [])
29
+
30
+ ~>
31
+ return unless _ready
32
+ items = _items
33
+ return unless items.length
34
+ items.forEach (el, idx) =>
35
+ isLast = idx is items.length - 1
36
+ if isLast
37
+ el.setAttribute 'aria-current', 'page'
38
+ el.toggleAttribute 'data-current', true
39
+ else
40
+ el.removeAttribute 'aria-current'
41
+ el.removeAttribute 'data-current'
42
+
43
+ render
44
+ nav aria-label: @label
45
+ ol ref: "_content"
46
+ slot
@@ -0,0 +1,26 @@
1
+ # ButtonGroup — accessible headless button group
2
+ #
3
+ # Groups related buttons with proper ARIA semantics.
4
+ # Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # ButtonGroup
8
+ # Button "Cut"
9
+ # Button "Copy"
10
+ # Button "Paste"
11
+ # ButtonGroup orientation: "vertical", label: "Text formatting"
12
+ # Toggle pressed <=> isBold, "Bold"
13
+ # Toggle pressed <=> isItalic, "Italic"
14
+
15
+ export ButtonGroup = component
16
+ @orientation := 'horizontal'
17
+ @disabled := false
18
+ @label := ''
19
+
20
+ render
21
+ div role: "group"
22
+ aria-label: @label?!
23
+ aria-orientation: @orientation
24
+ $orientation: @orientation
25
+ $disabled: @disabled?!
26
+ slot
package/button.rip ADDED
@@ -0,0 +1,23 @@
1
+ # Button — accessible headless button
2
+ #
3
+ # Handles disabled-but-focusable pattern and pressed state.
4
+ # Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # Button @click: handleClick
8
+ # "Save"
9
+ # Button disabled: true
10
+ # "Unavailable"
11
+
12
+ export Button = component
13
+ @disabled := false
14
+
15
+ onClick: ->
16
+ return if @disabled
17
+ @emit 'press'
18
+
19
+ render
20
+ button disabled: @disabled
21
+ aria-disabled: @disabled?!
22
+ $disabled: @disabled?!
23
+ slot
package/card.rip ADDED
@@ -0,0 +1,25 @@
1
+ # Card — accessible headless content container
2
+ #
3
+ # Structured container with optional header, content, and footer sections.
4
+ # Use $header, $content, $footer on children to mark sections.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Card
9
+ # div $header: true
10
+ # h3 "Title"
11
+ # div $content: true
12
+ # p "Body text"
13
+ # div $footer: true
14
+ # Button "Action"
15
+ #
16
+ # Card interactive: true, @click: handleClick
17
+ # p "Clickable card"
18
+
19
+ export Card = component
20
+ @interactive := false
21
+
22
+ render
23
+ div tabindex: (if @interactive then "0" else undefined)
24
+ $interactive: @interactive?!
25
+ slot
package/carousel.rip ADDED
@@ -0,0 +1,110 @@
1
+ # Carousel — accessible headless slide carousel
2
+ #
3
+ # Displays one slide at a time with arrow key navigation, optional
4
+ # autoplay, and loop mode. Discovers slides from [data-slide] children.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Carousel loop: true
9
+ # div $slide: true
10
+ # img src: "slide1.jpg"
11
+ # div $slide: true
12
+ # img src: "slide2.jpg"
13
+ # div $slide: true
14
+ # img src: "slide3.jpg"
15
+ #
16
+ # Carousel autoplay: true, interval: 5000, @change: handleSlide
17
+ # div $slide: true, "Slide A"
18
+ # div $slide: true, "Slide B"
19
+
20
+ export Carousel = component
21
+ @orientation := 'horizontal'
22
+ @loop := false
23
+ @autoplay := false
24
+ @interval := 4000
25
+ @label := 'Carousel'
26
+
27
+ activeIndex := 0
28
+ _ready := false
29
+ _timer = null
30
+
31
+ _slides ~=
32
+ return [] unless _ready
33
+ return [] unless @_content
34
+ Array.from(@_content.querySelectorAll('[data-slide]') or [])
35
+
36
+ totalSlides ~= _slides.length
37
+
38
+ mounted: ->
39
+ _ready = true
40
+ @_startAutoplay() if @autoplay
41
+
42
+ beforeUnmount: ->
43
+ @_stopAutoplay()
44
+
45
+ _startAutoplay: ->
46
+ @_stopAutoplay()
47
+ _timer = setInterval (=> @next()), @interval
48
+
49
+ _stopAutoplay: ->
50
+ clearInterval _timer if _timer
51
+ _timer = null
52
+
53
+ goto: (idx) ->
54
+ count = totalSlides
55
+ return unless count
56
+ if @loop
57
+ idx = idx %% count
58
+ else
59
+ idx = Math.max(0, Math.min(idx, count - 1))
60
+ activeIndex = idx
61
+ @emit 'change', activeIndex
62
+
63
+ next: -> @goto(activeIndex + 1)
64
+ prev: -> @goto(activeIndex - 1)
65
+
66
+ onKeydown: (e) ->
67
+ horiz = @orientation is 'horizontal'
68
+ switch e.key
69
+ when (if horiz then 'ArrowRight' else 'ArrowDown')
70
+ e.preventDefault()
71
+ @next()
72
+ when (if horiz then 'ArrowLeft' else 'ArrowUp')
73
+ e.preventDefault()
74
+ @prev()
75
+ when 'Home'
76
+ e.preventDefault()
77
+ @goto(0)
78
+ when 'End'
79
+ e.preventDefault()
80
+ @goto(totalSlides - 1)
81
+
82
+ ~>
83
+ return unless _ready
84
+ _slides.forEach (el, idx) =>
85
+ isActive = idx is activeIndex
86
+ el.hidden = not isActive
87
+ el.toggleAttribute 'data-active', isActive
88
+ el.setAttribute 'role', 'tabpanel'
89
+ el.setAttribute 'aria-roledescription', 'slide'
90
+ el.setAttribute 'aria-label', "Slide #{idx + 1} of #{totalSlides}"
91
+
92
+ onMouseenter: -> @_stopAutoplay() if @autoplay
93
+ onMouseleave: -> @_startAutoplay() if @autoplay
94
+
95
+ render
96
+ div role: "region", aria-roledescription: "carousel", aria-label: @label, tabindex: "0"
97
+ $orientation: @orientation
98
+ @keydown: @onKeydown
99
+ @mouseenter: @onMouseenter
100
+ @mouseleave: @onMouseleave
101
+ button $prev: true, aria-label: "Previous slide"
102
+ disabled: not @loop and activeIndex <= 0
103
+ $disabled: (not @loop and activeIndex <= 0)?!
104
+ @click: (=> @prev())
105
+ div ref: "_content"
106
+ slot
107
+ button $next: true, aria-label: "Next slide"
108
+ disabled: not @loop and activeIndex >= totalSlides - 1
109
+ $disabled: (not @loop and activeIndex >= totalSlides - 1)?!
110
+ @click: (=> @next())