@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.
- package/README.md +443 -576
- package/accordion.rip +113 -0
- package/alert-dialog.rip +96 -0
- package/autocomplete.rip +141 -0
- package/avatar.rip +37 -0
- package/badge.rip +15 -0
- package/breadcrumb.rip +46 -0
- package/button-group.rip +26 -0
- package/button.rip +23 -0
- package/card.rip +25 -0
- package/carousel.rip +110 -0
- package/checkbox-group.rip +65 -0
- package/checkbox.rip +33 -0
- package/collapsible.rip +50 -0
- package/combobox.rip +155 -0
- package/context-menu.rip +105 -0
- package/date-picker.rip +214 -0
- package/dialog.rip +107 -0
- package/drawer.rip +79 -0
- package/editable-value.rip +80 -0
- package/field.rip +53 -0
- package/fieldset.rip +22 -0
- package/form.rip +39 -0
- package/grid.rip +901 -0
- package/index.rip +16 -0
- package/input-group.rip +28 -0
- package/input.rip +36 -0
- package/label.rip +16 -0
- package/menu.rip +162 -0
- package/menubar.rip +155 -0
- package/meter.rip +36 -0
- package/multi-select.rip +158 -0
- package/native-select.rip +32 -0
- package/nav-menu.rip +129 -0
- package/number-field.rip +162 -0
- package/otp-field.rip +89 -0
- package/package.json +18 -27
- package/pagination.rip +123 -0
- package/popover.rip +143 -0
- package/preview-card.rip +73 -0
- package/progress.rip +25 -0
- package/radio-group.rip +67 -0
- package/resizable.rip +123 -0
- package/scroll-area.rip +145 -0
- package/select.rip +184 -0
- package/separator.rip +17 -0
- package/skeleton.rip +22 -0
- package/slider.rip +165 -0
- package/spinner.rip +17 -0
- package/table.rip +27 -0
- package/tabs.rip +124 -0
- package/textarea.rip +48 -0
- package/toast.rip +87 -0
- package/toggle-group.rip +78 -0
- package/toggle.rip +24 -0
- package/toolbar.rip +46 -0
- package/tooltip.rip +115 -0
- package/dist/rip-ui.min.js +0 -524
- package/dist/rip-ui.min.js.br +0 -0
- package/serve.rip +0 -92
- package/ui.rip +0 -964
package/index.rip
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Widget Gallery — dev server for testing all widgets
|
|
2
|
+
|
|
3
|
+
import { get, use, start, notFound } from '@rip-lang/server'
|
|
4
|
+
import { serve } from '@rip-lang/server/middleware'
|
|
5
|
+
|
|
6
|
+
dir = import.meta.dir
|
|
7
|
+
|
|
8
|
+
use serve dir: dir, components: ['.'], watch: true
|
|
9
|
+
|
|
10
|
+
get '/*.rip', -> @send "#{dir}/#{@req.path.slice(1)}"
|
|
11
|
+
get '/*.css', -> @send "#{dir}/#{@req.path.slice(1)}", 'text/css; charset=UTF-8'
|
|
12
|
+
get '/hljs-rip.js', -> @send "#{dir}/../print/hljs-rip.js", 'text/javascript; charset=UTF-8'
|
|
13
|
+
|
|
14
|
+
notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
|
|
15
|
+
|
|
16
|
+
start port: 3005
|
package/input-group.rip
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# InputGroup — accessible headless input with prefix/suffix
|
|
2
|
+
#
|
|
3
|
+
# Wraps a form control with optional prefix and suffix elements.
|
|
4
|
+
# Use $prefix and $suffix on children to mark addon positions.
|
|
5
|
+
# Tracks child input focus for styling. Ships zero CSS.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# InputGroup
|
|
9
|
+
# span $prefix: true, "$"
|
|
10
|
+
# Input value <=> amount, type: "number"
|
|
11
|
+
# InputGroup
|
|
12
|
+
# Input value <=> search, placeholder: "Search..."
|
|
13
|
+
# button $suffix: true, @click: doSearch, "Go"
|
|
14
|
+
|
|
15
|
+
export InputGroup = component
|
|
16
|
+
@disabled := false
|
|
17
|
+
|
|
18
|
+
focused := false
|
|
19
|
+
|
|
20
|
+
mounted: ->
|
|
21
|
+
ctrl = @_root?.querySelector('input, select, textarea')
|
|
22
|
+
return unless ctrl
|
|
23
|
+
ctrl.addEventListener 'focusin', => focused = true
|
|
24
|
+
ctrl.addEventListener 'focusout', => focused = false
|
|
25
|
+
|
|
26
|
+
render
|
|
27
|
+
div ref: "_root", $disabled: @disabled?!, $focused: focused?!
|
|
28
|
+
slot
|
package/input.rip
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Input — accessible headless input wrapper
|
|
2
|
+
#
|
|
3
|
+
# Tracks focus, validation, and disabled state via data attributes.
|
|
4
|
+
# Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# Input value <=> name, placeholder: "Enter name"
|
|
8
|
+
# Input value <=> email, type: "email", required: true
|
|
9
|
+
|
|
10
|
+
export Input = component
|
|
11
|
+
@value := ''
|
|
12
|
+
@placeholder := ''
|
|
13
|
+
@type := 'text'
|
|
14
|
+
@disabled := false
|
|
15
|
+
@required := false
|
|
16
|
+
|
|
17
|
+
focused := false
|
|
18
|
+
touched := false
|
|
19
|
+
|
|
20
|
+
onInput: (e) -> @value = e.target.value
|
|
21
|
+
onFocus: -> focused = true
|
|
22
|
+
onBlur: ->
|
|
23
|
+
focused = false
|
|
24
|
+
touched = true
|
|
25
|
+
|
|
26
|
+
render
|
|
27
|
+
input type: @type, value: @value, placeholder: @placeholder
|
|
28
|
+
disabled: @disabled
|
|
29
|
+
required: @required
|
|
30
|
+
aria-disabled: @disabled?!
|
|
31
|
+
aria-required: @required?!
|
|
32
|
+
$disabled: @disabled?!
|
|
33
|
+
$focused: focused?!
|
|
34
|
+
$touched: touched?!
|
|
35
|
+
@focusin: @onFocus
|
|
36
|
+
@focusout: @onBlur
|
package/label.rip
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Label — accessible headless form label
|
|
2
|
+
#
|
|
3
|
+
# Standalone label element that associates with a form control via @for.
|
|
4
|
+
# Companions Field but works independently. Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# Label for: "email-input", "Email address"
|
|
8
|
+
# Label required: true, "Username"
|
|
9
|
+
|
|
10
|
+
export Label = component
|
|
11
|
+
@for := null
|
|
12
|
+
@required := false
|
|
13
|
+
|
|
14
|
+
render
|
|
15
|
+
label for: @for?!, $required: @required?!
|
|
16
|
+
slot
|
package/menu.rip
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Menu — accessible headless dropdown menu
|
|
2
|
+
#
|
|
3
|
+
# Keyboard: ArrowDown/Up to navigate, Enter/Space to select, Escape to close,
|
|
4
|
+
# Home/End for first/last. Exposes $open on menu, $highlighted on items.
|
|
5
|
+
# Ships zero CSS.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# Menu
|
|
9
|
+
# span "Actions"
|
|
10
|
+
# div $item: "edit", "Edit"
|
|
11
|
+
# div $item: "delete", "Delete"
|
|
12
|
+
# div $item: "archive", "Archive"
|
|
13
|
+
|
|
14
|
+
export Menu = component
|
|
15
|
+
@disabled := false
|
|
16
|
+
|
|
17
|
+
open := false
|
|
18
|
+
highlightedIndex := -1
|
|
19
|
+
typeaheadBuffer := ''
|
|
20
|
+
typeaheadTimer := null
|
|
21
|
+
|
|
22
|
+
items ~=
|
|
23
|
+
return [] unless @_slot
|
|
24
|
+
Array.from(@_slot.querySelectorAll('[data-item]') or [])
|
|
25
|
+
|
|
26
|
+
triggerLabel ~=
|
|
27
|
+
return '' unless @_slot
|
|
28
|
+
el = @_slot.querySelector(':not([data-item])')
|
|
29
|
+
el?.textContent?.trim() or ''
|
|
30
|
+
|
|
31
|
+
toggle: ->
|
|
32
|
+
return if @disabled
|
|
33
|
+
if open then @close() else @openMenu()
|
|
34
|
+
|
|
35
|
+
openMenu: ->
|
|
36
|
+
open = true
|
|
37
|
+
highlightedIndex = 0
|
|
38
|
+
requestAnimationFrame =>
|
|
39
|
+
@_position()
|
|
40
|
+
@_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
|
|
41
|
+
|
|
42
|
+
close: ->
|
|
43
|
+
open = false
|
|
44
|
+
highlightedIndex = -1
|
|
45
|
+
@_trigger?.focus()
|
|
46
|
+
|
|
47
|
+
selectIndex: (idx) ->
|
|
48
|
+
item = items[idx]
|
|
49
|
+
return unless item
|
|
50
|
+
return if item.dataset.disabled?
|
|
51
|
+
role = item.getAttribute('role')
|
|
52
|
+
if role is 'menuitemcheckbox'
|
|
53
|
+
checked = item.getAttribute('aria-checked') is 'true'
|
|
54
|
+
item.setAttribute 'aria-checked', not checked
|
|
55
|
+
@emit 'select', { id: item.dataset.item, checked: not checked }
|
|
56
|
+
else if role is 'menuitemradio'
|
|
57
|
+
group = item.closest('[data-radio-group]')
|
|
58
|
+
if group
|
|
59
|
+
group.querySelectorAll('[role="menuitemradio"]').forEach (r) ->
|
|
60
|
+
r.setAttribute 'aria-checked', false
|
|
61
|
+
item.setAttribute 'aria-checked', true
|
|
62
|
+
@emit 'select', { id: item.dataset.item, value: item.dataset.item }
|
|
63
|
+
else
|
|
64
|
+
@emit 'select', item.dataset.item
|
|
65
|
+
@close()
|
|
66
|
+
|
|
67
|
+
_typeahead: (char) ->
|
|
68
|
+
clearTimeout typeaheadTimer if typeaheadTimer
|
|
69
|
+
typeaheadBuffer += char.toLowerCase()
|
|
70
|
+
typeaheadTimer = setTimeout (-> typeaheadBuffer = ''), 500
|
|
71
|
+
idx = items.findIndex (o) -> o.textContent.trim().toLowerCase().startsWith(typeaheadBuffer)
|
|
72
|
+
if idx >= 0
|
|
73
|
+
highlightedIndex = idx
|
|
74
|
+
@_list?.querySelectorAll('[role="menuitem"]')[idx]?.focus()
|
|
75
|
+
|
|
76
|
+
_position: ->
|
|
77
|
+
return unless @_trigger and @_list
|
|
78
|
+
tr = @_trigger.getBoundingClientRect()
|
|
79
|
+
@_list.style.position = 'fixed'
|
|
80
|
+
@_list.style.left = "#{tr.left}px"
|
|
81
|
+
@_list.style.top = "#{tr.bottom + 4}px"
|
|
82
|
+
@_list.style.minWidth = "#{tr.width}px"
|
|
83
|
+
fl = @_list.getBoundingClientRect()
|
|
84
|
+
if fl.bottom > window.innerHeight
|
|
85
|
+
@_list.style.top = "#{tr.top - fl.height - 4}px"
|
|
86
|
+
if fl.right > window.innerWidth
|
|
87
|
+
@_list.style.left = "#{window.innerWidth - fl.width - 4}px"
|
|
88
|
+
@_list.style.visibility = 'visible'
|
|
89
|
+
|
|
90
|
+
onTriggerKeydown: (e) ->
|
|
91
|
+
return if @disabled
|
|
92
|
+
if e.key in ['ArrowDown', 'Enter', ' ']
|
|
93
|
+
e.preventDefault()
|
|
94
|
+
@openMenu()
|
|
95
|
+
|
|
96
|
+
onMenuKeydown: (e) ->
|
|
97
|
+
len = items.length
|
|
98
|
+
return unless len
|
|
99
|
+
|
|
100
|
+
switch e.key
|
|
101
|
+
when 'ArrowDown'
|
|
102
|
+
e.preventDefault()
|
|
103
|
+
highlightedIndex = (highlightedIndex + 1) %% len
|
|
104
|
+
@_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
|
|
105
|
+
when 'ArrowUp'
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
highlightedIndex = (highlightedIndex - 1) %% len
|
|
108
|
+
@_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
|
|
109
|
+
when 'Home'
|
|
110
|
+
e.preventDefault()
|
|
111
|
+
highlightedIndex = 0
|
|
112
|
+
@_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
|
|
113
|
+
when 'End'
|
|
114
|
+
e.preventDefault()
|
|
115
|
+
highlightedIndex = len - 1
|
|
116
|
+
@_list?.querySelectorAll('[role="menuitem"]')[len - 1]?.focus()
|
|
117
|
+
when 'Enter', ' '
|
|
118
|
+
e.preventDefault()
|
|
119
|
+
@selectIndex(highlightedIndex)
|
|
120
|
+
when 'Escape'
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
@close()
|
|
123
|
+
when 'Tab'
|
|
124
|
+
@close()
|
|
125
|
+
else
|
|
126
|
+
if e.key.length is 1
|
|
127
|
+
@_typeahead(e.key)
|
|
128
|
+
|
|
129
|
+
~>
|
|
130
|
+
if open
|
|
131
|
+
onDown = (e) =>
|
|
132
|
+
unless @_trigger?.contains(e.target) or @_list?.contains(e.target)
|
|
133
|
+
@close()
|
|
134
|
+
document.addEventListener 'mousedown', onDown
|
|
135
|
+
return -> document.removeEventListener 'mousedown', onDown
|
|
136
|
+
|
|
137
|
+
render
|
|
138
|
+
.
|
|
139
|
+
button ref: "_trigger"
|
|
140
|
+
aria-haspopup: "menu"
|
|
141
|
+
aria-expanded: !!open
|
|
142
|
+
$open: open?!
|
|
143
|
+
$disabled: @disabled?!
|
|
144
|
+
disabled: @disabled
|
|
145
|
+
@click: @toggle
|
|
146
|
+
@keydown: @onTriggerKeydown
|
|
147
|
+
triggerLabel
|
|
148
|
+
|
|
149
|
+
. ref: "_slot", style: "display:none"
|
|
150
|
+
slot
|
|
151
|
+
|
|
152
|
+
if open
|
|
153
|
+
div ref: "_list", role: "menu", $open: true, style: "position:fixed;visibility:hidden", @keydown: @onMenuKeydown
|
|
154
|
+
for item, idx in items
|
|
155
|
+
div role: item.getAttribute('role') or 'menuitem'
|
|
156
|
+
tabindex: "-1"
|
|
157
|
+
aria-checked: item.getAttribute('aria-checked')?!
|
|
158
|
+
$highlighted: (idx is highlightedIndex)?!
|
|
159
|
+
$disabled: item.dataset.disabled?!
|
|
160
|
+
@click: (=> @selectIndex(idx))
|
|
161
|
+
@mouseenter: (=> highlightedIndex = idx)
|
|
162
|
+
= item.textContent
|
package/menubar.rip
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Menubar — accessible headless horizontal menu bar
|
|
2
|
+
#
|
|
3
|
+
# A horizontal bar of menu triggers. Each trigger opens a dropdown menu.
|
|
4
|
+
# Arrow keys navigate between triggers; open menus close when moving
|
|
5
|
+
# to the next trigger. Ships zero CSS.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# Menubar
|
|
9
|
+
# div $menu: "file"
|
|
10
|
+
# div $item: "new", "New"
|
|
11
|
+
# div $item: "open", "Open"
|
|
12
|
+
# div $item: "save", "Save"
|
|
13
|
+
# div $menu: "edit"
|
|
14
|
+
# div $item: "undo", "Undo"
|
|
15
|
+
# div $item: "redo", "Redo"
|
|
16
|
+
|
|
17
|
+
export Menubar = component
|
|
18
|
+
@disabled := false
|
|
19
|
+
|
|
20
|
+
activeMenu := null
|
|
21
|
+
highlightedIndex := -1
|
|
22
|
+
|
|
23
|
+
_menus ~=
|
|
24
|
+
return [] unless @_slot
|
|
25
|
+
Array.from(@_slot.querySelectorAll('[data-menu]') or [])
|
|
26
|
+
|
|
27
|
+
_menuItemsFor: (menu) ->
|
|
28
|
+
return [] unless menu
|
|
29
|
+
Array.from(menu.querySelectorAll('[data-item]') or [])
|
|
30
|
+
|
|
31
|
+
_openMenu: (menuId) ->
|
|
32
|
+
return if @disabled
|
|
33
|
+
activeMenu = menuId
|
|
34
|
+
highlightedIndex = 0
|
|
35
|
+
requestAnimationFrame =>
|
|
36
|
+
@_position(menuId)
|
|
37
|
+
@_root?.querySelector("[data-menu-list=\"#{menuId}\"] [role=\"menuitem\"]")?.focus()
|
|
38
|
+
|
|
39
|
+
_closeMenu: ->
|
|
40
|
+
activeMenu = null
|
|
41
|
+
highlightedIndex = -1
|
|
42
|
+
|
|
43
|
+
_position: (menuId) ->
|
|
44
|
+
trigger = @_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]")
|
|
45
|
+
list = @_root?.querySelector("[data-menu-list=\"#{menuId}\"]")
|
|
46
|
+
return unless trigger and list
|
|
47
|
+
tr = trigger.getBoundingClientRect()
|
|
48
|
+
list.style.position = 'fixed'
|
|
49
|
+
list.style.left = "#{tr.left}px"
|
|
50
|
+
list.style.top = "#{tr.bottom + 2}px"
|
|
51
|
+
list.style.minWidth = "#{tr.width}px"
|
|
52
|
+
|
|
53
|
+
selectItem: (menuId, itemId) ->
|
|
54
|
+
@emit 'select', { menu: menuId, item: itemId }
|
|
55
|
+
@_closeMenu()
|
|
56
|
+
@_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]")?.focus()
|
|
57
|
+
|
|
58
|
+
_onBarKeydown: (e) ->
|
|
59
|
+
triggers = @_root?.querySelectorAll('[data-menu-trigger]')
|
|
60
|
+
return unless triggers?.length
|
|
61
|
+
focused = Array.from(triggers).indexOf(document.activeElement)
|
|
62
|
+
return if focused < 0
|
|
63
|
+
len = triggers.length
|
|
64
|
+
switch e.key
|
|
65
|
+
when 'ArrowRight'
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
next = (focused + 1) %% len
|
|
68
|
+
triggers[next]?.focus()
|
|
69
|
+
if activeMenu
|
|
70
|
+
@_openMenu(triggers[next]?.dataset.menuTrigger)
|
|
71
|
+
when 'ArrowLeft'
|
|
72
|
+
e.preventDefault()
|
|
73
|
+
prev = (focused - 1) %% len
|
|
74
|
+
triggers[prev]?.focus()
|
|
75
|
+
if activeMenu
|
|
76
|
+
@_openMenu(triggers[prev]?.dataset.menuTrigger)
|
|
77
|
+
when 'ArrowDown', 'Enter', ' '
|
|
78
|
+
e.preventDefault()
|
|
79
|
+
menuId = triggers[focused]?.dataset.menuTrigger
|
|
80
|
+
@_openMenu(menuId) if menuId
|
|
81
|
+
when 'Escape'
|
|
82
|
+
@_closeMenu()
|
|
83
|
+
|
|
84
|
+
_onMenuKeydown: (e, menuId, menuItems) ->
|
|
85
|
+
len = menuItems.length
|
|
86
|
+
return unless len
|
|
87
|
+
switch e.key
|
|
88
|
+
when 'ArrowDown'
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
highlightedIndex = (highlightedIndex + 1) %% len
|
|
91
|
+
@_root?.querySelector("[data-menu-list=\"#{menuId}\"]")?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
|
|
92
|
+
when 'ArrowUp'
|
|
93
|
+
e.preventDefault()
|
|
94
|
+
highlightedIndex = (highlightedIndex - 1) %% len
|
|
95
|
+
@_root?.querySelector("[data-menu-list=\"#{menuId}\"]")?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
|
|
96
|
+
when 'Enter', ' '
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
item = menuItems[highlightedIndex]
|
|
99
|
+
@selectItem(menuId, item?.dataset.item) if item
|
|
100
|
+
when 'Escape', 'Tab'
|
|
101
|
+
e.preventDefault() if e.key is 'Escape'
|
|
102
|
+
@_closeMenu()
|
|
103
|
+
@_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]")?.focus()
|
|
104
|
+
when 'ArrowRight'
|
|
105
|
+
e.preventDefault()
|
|
106
|
+
@_closeMenu()
|
|
107
|
+
triggers = @_root?.querySelectorAll('[data-menu-trigger]')
|
|
108
|
+
focused = Array.from(triggers).findIndex (t) -> t.dataset.menuTrigger is menuId
|
|
109
|
+
next = (focused + 1) %% triggers.length
|
|
110
|
+
triggers[next]?.focus()
|
|
111
|
+
@_openMenu(triggers[next]?.dataset.menuTrigger)
|
|
112
|
+
when 'ArrowLeft'
|
|
113
|
+
e.preventDefault()
|
|
114
|
+
@_closeMenu()
|
|
115
|
+
triggers = @_root?.querySelectorAll('[data-menu-trigger]')
|
|
116
|
+
focused = Array.from(triggers).findIndex (t) -> t.dataset.menuTrigger is menuId
|
|
117
|
+
prev = (focused - 1) %% triggers.length
|
|
118
|
+
triggers[prev]?.focus()
|
|
119
|
+
@_openMenu(triggers[prev]?.dataset.menuTrigger)
|
|
120
|
+
|
|
121
|
+
~>
|
|
122
|
+
if activeMenu
|
|
123
|
+
onDown = (e) =>
|
|
124
|
+
unless @_root?.contains(e.target)
|
|
125
|
+
@_closeMenu()
|
|
126
|
+
document.addEventListener 'mousedown', onDown
|
|
127
|
+
return -> document.removeEventListener 'mousedown', onDown
|
|
128
|
+
|
|
129
|
+
render
|
|
130
|
+
div ref: "_root", role: "menubar", $disabled: @disabled?!
|
|
131
|
+
|
|
132
|
+
. ref: "_slot", style: "display:none"
|
|
133
|
+
slot
|
|
134
|
+
|
|
135
|
+
for menu in _menus
|
|
136
|
+
button role: "menuitem", tabindex: "0"
|
|
137
|
+
"data-menu-trigger": menu.dataset.menu
|
|
138
|
+
aria-haspopup: "menu"
|
|
139
|
+
aria-expanded: activeMenu is menu.dataset.menu
|
|
140
|
+
$open: (activeMenu is menu.dataset.menu)?!
|
|
141
|
+
@click: (=> if activeMenu is menu.dataset.menu then @_closeMenu() else @_openMenu(menu.dataset.menu))
|
|
142
|
+
@keydown: @_onBarKeydown
|
|
143
|
+
= menu.dataset.menu
|
|
144
|
+
|
|
145
|
+
if activeMenu is menu.dataset.menu
|
|
146
|
+
div role: "menu", $open: true, style: "position:fixed"
|
|
147
|
+
"data-menu-list": menu.dataset.menu
|
|
148
|
+
@keydown: (e) => @_onMenuKeydown(e, menu.dataset.menu, @_menuItemsFor(menu))
|
|
149
|
+
for item, idx in @_menuItemsFor(menu)
|
|
150
|
+
div role: "menuitem", tabindex: "-1"
|
|
151
|
+
$highlighted: (idx is highlightedIndex)?!
|
|
152
|
+
$value: item.dataset.item
|
|
153
|
+
@click: (=> @selectItem(menu.dataset.menu, item.dataset.item))
|
|
154
|
+
@mouseenter: (=> highlightedIndex = idx)
|
|
155
|
+
= item.textContent
|
package/meter.rip
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Meter — accessible headless meter (gauge)
|
|
2
|
+
#
|
|
3
|
+
# For known-range measurements (disk usage, password strength, etc.).
|
|
4
|
+
# Exposes value and thresholds as CSS custom properties.
|
|
5
|
+
# Ships zero CSS.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# Meter value: 0.7, low: 0.3, high: 0.8, optimum: 0.5
|
|
9
|
+
# Meter value: 75, min: 0, max: 100
|
|
10
|
+
|
|
11
|
+
export Meter = component
|
|
12
|
+
@value := 0
|
|
13
|
+
@min := 0
|
|
14
|
+
@max := 1
|
|
15
|
+
@low := null
|
|
16
|
+
@high := null
|
|
17
|
+
@optimum := null
|
|
18
|
+
@label := null
|
|
19
|
+
|
|
20
|
+
percent ~= Math.min(100, Math.max(0, ((@value - @min) / (@max - @min)) * 100))
|
|
21
|
+
|
|
22
|
+
level ~=
|
|
23
|
+
return 'optimum' unless @low? and @high?
|
|
24
|
+
if @value <= @low then 'low'
|
|
25
|
+
else if @value >= @high then 'high'
|
|
26
|
+
else 'optimum'
|
|
27
|
+
|
|
28
|
+
render
|
|
29
|
+
div role: "meter"
|
|
30
|
+
aria-valuenow: @value
|
|
31
|
+
aria-valuemin: @min
|
|
32
|
+
aria-valuemax: @max
|
|
33
|
+
aria-label: @label?!
|
|
34
|
+
style: "--meter-value: #{@value}; --meter-percent: #{percent}%"
|
|
35
|
+
$level: level
|
|
36
|
+
slot
|
package/multi-select.rip
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# MultiSelect — accessible headless multi-select with chips
|
|
2
|
+
#
|
|
3
|
+
# Filterable dropdown where multiple items can be selected. Selected items
|
|
4
|
+
# appear as removable chips. Type to filter, click or arrow+Enter to toggle.
|
|
5
|
+
# Ships zero CSS — style entirely via attribute selectors in your stylesheet.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# MultiSelect value <=> selectedColors, items: colors, placeholder: "Choose colors..."
|
|
9
|
+
|
|
10
|
+
export MultiSelect = component
|
|
11
|
+
@value := []
|
|
12
|
+
@items := []
|
|
13
|
+
@placeholder := 'Select...'
|
|
14
|
+
@disabled := false
|
|
15
|
+
|
|
16
|
+
open := false
|
|
17
|
+
query := ''
|
|
18
|
+
highlightedIndex := -1
|
|
19
|
+
_listId =! "ms-#{Math.random().toString(36).slice(2, 8)}"
|
|
20
|
+
|
|
21
|
+
filtered ~=
|
|
22
|
+
q = query.trim().toLowerCase()
|
|
23
|
+
return @items unless q
|
|
24
|
+
@items.filter (item) ->
|
|
25
|
+
label = if typeof item is 'string' then item else (item.label or item.name or String(item))
|
|
26
|
+
label.toLowerCase().includes(q)
|
|
27
|
+
|
|
28
|
+
_label: (item) ->
|
|
29
|
+
if typeof item is 'string' then item else (item.label or item.name or String(item))
|
|
30
|
+
|
|
31
|
+
_val: (item) ->
|
|
32
|
+
if typeof item is 'string' then item else (item.value or item.id or String(item))
|
|
33
|
+
|
|
34
|
+
_isSelected: (item) ->
|
|
35
|
+
v = @_val(item)
|
|
36
|
+
Array.isArray(@value) and v in @value
|
|
37
|
+
|
|
38
|
+
_toggleItem: (item) ->
|
|
39
|
+
return if @disabled
|
|
40
|
+
v = @_val(item)
|
|
41
|
+
arr = if Array.isArray(@value) then [...@value] else []
|
|
42
|
+
if v in arr
|
|
43
|
+
arr = arr.filter (x) -> x isnt v
|
|
44
|
+
else
|
|
45
|
+
arr.push v
|
|
46
|
+
@value = arr
|
|
47
|
+
@emit 'change', @value
|
|
48
|
+
|
|
49
|
+
_removeChip: (v) ->
|
|
50
|
+
return if @disabled
|
|
51
|
+
@value = @value.filter (x) -> x isnt v
|
|
52
|
+
@emit 'change', @value
|
|
53
|
+
|
|
54
|
+
clearAll: ->
|
|
55
|
+
return if @disabled
|
|
56
|
+
@value = []
|
|
57
|
+
query = ''
|
|
58
|
+
@emit 'change', @value
|
|
59
|
+
|
|
60
|
+
_onInput: (e) ->
|
|
61
|
+
query = e.target.value
|
|
62
|
+
open = true
|
|
63
|
+
highlightedIndex = if filtered.length > 0 then 0 else -1
|
|
64
|
+
|
|
65
|
+
onFocusin: -> open = true
|
|
66
|
+
|
|
67
|
+
onFocusout: (e) ->
|
|
68
|
+
unless @_content?.contains(e.relatedTarget)
|
|
69
|
+
open = false
|
|
70
|
+
query = ''
|
|
71
|
+
highlightedIndex = -1
|
|
72
|
+
|
|
73
|
+
_position: ->
|
|
74
|
+
return unless @_input and @_list
|
|
75
|
+
tr = @_input.getBoundingClientRect()
|
|
76
|
+
@_list.style.left = "#{tr.left}px"
|
|
77
|
+
@_list.style.top = "#{tr.bottom + 2}px"
|
|
78
|
+
@_list.style.minWidth = "#{tr.width}px"
|
|
79
|
+
fl = @_list.getBoundingClientRect()
|
|
80
|
+
if fl.bottom > window.innerHeight
|
|
81
|
+
@_list.style.top = "#{tr.top - fl.height - 2}px"
|
|
82
|
+
|
|
83
|
+
_onKeydown: (e) ->
|
|
84
|
+
len = filtered.length
|
|
85
|
+
switch e.key
|
|
86
|
+
when 'ArrowDown'
|
|
87
|
+
e.preventDefault()
|
|
88
|
+
open = true
|
|
89
|
+
highlightedIndex = (highlightedIndex + 1) %% len if len
|
|
90
|
+
setTimeout (=> @_position()), 0
|
|
91
|
+
when 'ArrowUp'
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
highlightedIndex = (highlightedIndex - 1) %% len if len
|
|
94
|
+
when 'Enter'
|
|
95
|
+
e.preventDefault()
|
|
96
|
+
if highlightedIndex >= 0 and highlightedIndex < len
|
|
97
|
+
@_toggleItem(filtered[highlightedIndex])
|
|
98
|
+
when 'Escape'
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
open = false
|
|
101
|
+
query = ''
|
|
102
|
+
when 'Backspace'
|
|
103
|
+
if not query and @value.length > 0
|
|
104
|
+
@value = @value.slice(0, -1)
|
|
105
|
+
@emit 'change', @value
|
|
106
|
+
when 'Tab'
|
|
107
|
+
open = false
|
|
108
|
+
query = ''
|
|
109
|
+
|
|
110
|
+
~>
|
|
111
|
+
if open
|
|
112
|
+
setTimeout (=> @_position()), 0
|
|
113
|
+
onDown = (e) =>
|
|
114
|
+
unless @_content?.contains(e.target)
|
|
115
|
+
open = false
|
|
116
|
+
query = ''
|
|
117
|
+
highlightedIndex = -1
|
|
118
|
+
document.addEventListener 'mousedown', onDown
|
|
119
|
+
return -> document.removeEventListener 'mousedown', onDown
|
|
120
|
+
|
|
121
|
+
render
|
|
122
|
+
. ref: "_content", $open: open?!, $disabled: @disabled?!
|
|
123
|
+
|
|
124
|
+
# Chip area + input
|
|
125
|
+
. $chips: true, @click: (=> @_input?.focus())
|
|
126
|
+
for chip in @value
|
|
127
|
+
span $chip: true
|
|
128
|
+
"#{chip}"
|
|
129
|
+
button $remove: true, aria-label: "Remove #{chip}", @click: (=> @_removeChip(chip))
|
|
130
|
+
"✕"
|
|
131
|
+
input ref: "_input", type: "text", autocomplete: "off"
|
|
132
|
+
role: "combobox"
|
|
133
|
+
aria-expanded: !!open
|
|
134
|
+
aria-haspopup: "listbox"
|
|
135
|
+
aria-controls: if open then _listId else undefined
|
|
136
|
+
$disabled: @disabled?!
|
|
137
|
+
disabled: @disabled
|
|
138
|
+
placeholder: if @value.length is 0 then @placeholder else ''
|
|
139
|
+
value: query
|
|
140
|
+
@input: @_onInput
|
|
141
|
+
@keydown: @_onKeydown
|
|
142
|
+
if @value.length > 0
|
|
143
|
+
button $clear: true, aria-label: "Clear all", @click: @clearAll
|
|
144
|
+
"✕"
|
|
145
|
+
|
|
146
|
+
# Dropdown
|
|
147
|
+
if open and filtered.length > 0
|
|
148
|
+
div ref: "_list", id: _listId, role: "listbox", $open: true, aria-multiselectable: "true"
|
|
149
|
+
style: "position:fixed"
|
|
150
|
+
for item, idx in filtered
|
|
151
|
+
div role: "option", tabindex: "-1", id: "#{_listId}-#{idx}"
|
|
152
|
+
$value: @_val(item)
|
|
153
|
+
$selected: @_isSelected(item)?!
|
|
154
|
+
$highlighted: (idx is highlightedIndex)?!
|
|
155
|
+
aria-selected: !!@_isSelected(item)
|
|
156
|
+
@click: (=> @_toggleItem(item))
|
|
157
|
+
@mouseenter: (=> highlightedIndex = idx)
|
|
158
|
+
@_label(item)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# NativeSelect — accessible headless native select wrapper
|
|
2
|
+
#
|
|
3
|
+
# Wraps a native <select> element with state tracking via data attributes.
|
|
4
|
+
# Use when the browser's built-in dropdown is preferred. Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# NativeSelect value <=> role, @change: handleChange
|
|
8
|
+
# option value: "", "Choose a role..."
|
|
9
|
+
# option value: "admin", "Admin"
|
|
10
|
+
# option value: "user", "User"
|
|
11
|
+
|
|
12
|
+
export NativeSelect = component
|
|
13
|
+
@value := ''
|
|
14
|
+
@disabled := false
|
|
15
|
+
@required := false
|
|
16
|
+
|
|
17
|
+
focused := false
|
|
18
|
+
|
|
19
|
+
onChange: (e) ->
|
|
20
|
+
@value = e.target.value
|
|
21
|
+
@emit 'change', @value
|
|
22
|
+
|
|
23
|
+
render
|
|
24
|
+
select value: @value, disabled: @disabled, required: @required
|
|
25
|
+
aria-disabled: @disabled?!
|
|
26
|
+
aria-required: @required?!
|
|
27
|
+
$disabled: @disabled?!
|
|
28
|
+
$focused: focused?!
|
|
29
|
+
@change: @onChange
|
|
30
|
+
@focusin: (=> focused = true)
|
|
31
|
+
@focusout: (=> focused = false)
|
|
32
|
+
slot
|