rip-lang 3.13.93 → 3.13.95
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 +1 -1
- package/docs/dist/rip.js +142 -38
- package/docs/dist/rip.min.js +174 -174
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +1 -1
- package/src/ui.rip +65 -0
- package/docs/ui/accordion.rip +0 -113
- package/docs/ui/alert-dialog.rip +0 -96
- package/docs/ui/autocomplete.rip +0 -141
- package/docs/ui/avatar.rip +0 -37
- package/docs/ui/badge.rip +0 -15
- package/docs/ui/breadcrumb.rip +0 -46
- 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 -65
- package/docs/ui/checkbox.rip +0 -33
- package/docs/ui/collapsible.rip +0 -50
- package/docs/ui/combobox.rip +0 -155
- package/docs/ui/context-menu.rip +0 -105
- package/docs/ui/date-picker.rip +0 -214
- package/docs/ui/dialog.rip +0 -107
- package/docs/ui/drawer.rip +0 -79
- package/docs/ui/editable-value.rip +0 -80
- 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/hljs-rip.js +0 -209
- package/docs/ui/index.css +0 -1772
- package/docs/ui/index.html +0 -2433
- 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 -162
- package/docs/ui/menubar.rip +0 -155
- package/docs/ui/meter.rip +0 -36
- package/docs/ui/multi-select.rip +0 -158
- package/docs/ui/native-select.rip +0 -32
- package/docs/ui/nav-menu.rip +0 -129
- 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 -143
- package/docs/ui/preview-card.rip +0 -73
- package/docs/ui/progress.rip +0 -25
- package/docs/ui/radio-group.rip +0 -67
- package/docs/ui/resizable.rip +0 -123
- package/docs/ui/scroll-area.rip +0 -145
- package/docs/ui/select.rip +0 -184
- 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 -124
- package/docs/ui/textarea.rip +0 -48
- package/docs/ui/toast.rip +0 -87
- package/docs/ui/toggle-group.rip +0 -78
- package/docs/ui/toggle.rip +0 -24
- package/docs/ui/toolbar.rip +0 -46
- package/docs/ui/tooltip.rip +0 -115
package/docs/dist/rip.min.js.br
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/ui.rip
CHANGED
|
@@ -1013,3 +1013,68 @@ export launch = (appBase = '', opts = {}) ->
|
|
|
1013
1013
|
version: '0.3.0'
|
|
1014
1014
|
|
|
1015
1015
|
{ app, components: appComponents, router, renderer }
|
|
1016
|
+
|
|
1017
|
+
# ==============================================================================
|
|
1018
|
+
# ARIA — keyboard navigation and popup lifecycle utilities for UI components
|
|
1019
|
+
#
|
|
1020
|
+
# Provides the WAI-ARIA keyboard interaction patterns used by headless widgets.
|
|
1021
|
+
# Registered on globalThis so any component can use them without explicit imports.
|
|
1022
|
+
#
|
|
1023
|
+
# ARIA.listNav(e, handlers) — popup lists (listbox, menu, combobox)
|
|
1024
|
+
# ARIA.rovingNav(e, handlers, orient) — inline composites (radiogroup, tabs, toolbar)
|
|
1025
|
+
# ARIA.popupDismiss(open, popup, close, els) — close on outside click or scroll
|
|
1026
|
+
#
|
|
1027
|
+
# Both nav handlers:
|
|
1028
|
+
# - Guard against IME composition events (e.which === 229, CJK input)
|
|
1029
|
+
# - Call e.preventDefault() + e.stopPropagation() for handled keys
|
|
1030
|
+
# - Only invoke a handler if it is provided (all keys are optional)
|
|
1031
|
+
# - Alias PageUp/PageDown to first/last (fn+Up/Down on macOS)
|
|
1032
|
+
#
|
|
1033
|
+
# ARIA: https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/
|
|
1034
|
+
# ==============================================================================
|
|
1035
|
+
|
|
1036
|
+
_ariaNAV = (e, fn) ->
|
|
1037
|
+
return unless fn
|
|
1038
|
+
e.preventDefault()
|
|
1039
|
+
e.stopPropagation()
|
|
1040
|
+
fn()
|
|
1041
|
+
|
|
1042
|
+
_ariaListNav = (e, h) ->
|
|
1043
|
+
return if e.which is 229 # IME guard: suppress CJK composition events
|
|
1044
|
+
switch e.key
|
|
1045
|
+
when 'ArrowDown' then _ariaNAV e, h.next
|
|
1046
|
+
when 'ArrowUp' then _ariaNAV e, h.prev
|
|
1047
|
+
when 'Home', 'PageUp' then _ariaNAV e, h.first
|
|
1048
|
+
when 'End', 'PageDown' then _ariaNAV e, h.last
|
|
1049
|
+
when 'Enter', ' ' then _ariaNAV e, h.select
|
|
1050
|
+
when 'Escape' then _ariaNAV e, h.dismiss
|
|
1051
|
+
when 'Tab' then h.tab?() # no preventDefault: allow natural focus movement
|
|
1052
|
+
else h.char?(e.key) if e.key.length is 1 # printable chars: typeahead
|
|
1053
|
+
|
|
1054
|
+
_ariaPopupDismiss = (open, popup, close, els = []) ->
|
|
1055
|
+
return unless open
|
|
1056
|
+
inside = [popup, ...els]
|
|
1057
|
+
onDown = (e) => close() unless inside.some (el) -> el?.contains(e.target)
|
|
1058
|
+
onScroll = (e) => close() unless popup?.contains(e.target)
|
|
1059
|
+
document.addEventListener 'mousedown', onDown
|
|
1060
|
+
window.addEventListener 'scroll', onScroll, true
|
|
1061
|
+
->
|
|
1062
|
+
document.removeEventListener 'mousedown', onDown
|
|
1063
|
+
window.removeEventListener 'scroll', onScroll, true
|
|
1064
|
+
|
|
1065
|
+
_ariaRovingNav = (e, h, orientation = 'vertical') ->
|
|
1066
|
+
return if e.which is 229 # IME guard
|
|
1067
|
+
vert = orientation isnt 'horizontal'
|
|
1068
|
+
horz = orientation isnt 'vertical'
|
|
1069
|
+
switch e.key
|
|
1070
|
+
when 'ArrowDown' then _ariaNAV e, h.next if vert
|
|
1071
|
+
when 'ArrowUp' then _ariaNAV e, h.prev if vert
|
|
1072
|
+
when 'ArrowRight' then _ariaNAV e, h.next if horz
|
|
1073
|
+
when 'ArrowLeft' then _ariaNAV e, h.prev if horz
|
|
1074
|
+
when 'Home', 'PageUp' then _ariaNAV e, h.first
|
|
1075
|
+
when 'End', 'PageDown' then _ariaNAV e, h.last
|
|
1076
|
+
when 'Enter', ' ' then _ariaNAV e, h.select
|
|
1077
|
+
when 'Escape' then _ariaNAV e, h.dismiss
|
|
1078
|
+
|
|
1079
|
+
globalThis.__aria ??= { listNav: _ariaListNav, rovingNav: _ariaRovingNav, popupDismiss: _ariaPopupDismiss }
|
|
1080
|
+
globalThis.ARIA ??= globalThis.__aria
|
package/docs/ui/accordion.rip
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
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
|
package/docs/ui/alert-dialog.rip
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
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
|
package/docs/ui/autocomplete.rip
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
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/docs/ui/avatar.rip
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
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/docs/ui/badge.rip
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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/docs/ui/breadcrumb.rip
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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
|
package/docs/ui/button-group.rip
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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/docs/ui/button.rip
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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/docs/ui/card.rip
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|