rip-lang 3.13.92 → 3.13.94
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/CHANGELOG.md +2 -2
- package/README.md +3 -3
- package/bin/rip +11 -1
- package/docs/AGENTS.md +43 -0
- package/docs/RIP-LANG.md +3 -3
- package/docs/RIP-TYPES.md +72 -91
- package/docs/charts.html +15 -15
- 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/docs/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +456 -0
- package/src/lexer.js +1 -2
- package/src/typecheck.js +188 -6
- package/src/types.js +63 -38
- 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/ui/combobox.rip
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
# Combobox — accessible headless combobox (input + filterable listbox)
|
|
2
|
-
#
|
|
3
|
-
# Keyboard: ArrowDown/Up to navigate, Enter to select, Escape to close,
|
|
4
|
-
# typing filters the list via the @filter callback.
|
|
5
|
-
#
|
|
6
|
-
# Exposes $open on the wrapper, $highlighted on options.
|
|
7
|
-
# Ships zero CSS — style entirely via attribute selectors in your stylesheet.
|
|
8
|
-
#
|
|
9
|
-
# Usage:
|
|
10
|
-
# Combobox query <=> searchText, items: fruits, @select: handleSelect, @filter: filterFn
|
|
11
|
-
|
|
12
|
-
export Combobox = component
|
|
13
|
-
@query := ''
|
|
14
|
-
@items := []
|
|
15
|
-
@placeholder := 'Search...'
|
|
16
|
-
@disabled := false
|
|
17
|
-
@autoHighlight := true
|
|
18
|
-
|
|
19
|
-
open := false
|
|
20
|
-
highlightedIndex := -1
|
|
21
|
-
_listId =! "cb-#{Math.random().toString(36).slice(2, 8)}"
|
|
22
|
-
|
|
23
|
-
getItems: ->
|
|
24
|
-
return [] unless @_list
|
|
25
|
-
Array.from(@_list.querySelectorAll('[role="option"]'))
|
|
26
|
-
|
|
27
|
-
_scrollToItem: ->
|
|
28
|
-
@getItems()[highlightedIndex]?.scrollIntoView({ block: 'nearest' })
|
|
29
|
-
|
|
30
|
-
clear: ->
|
|
31
|
-
@query = ''
|
|
32
|
-
highlightedIndex = -1
|
|
33
|
-
@emit 'filter', ''
|
|
34
|
-
|
|
35
|
-
onInput: (e) ->
|
|
36
|
-
@query = e.target.value
|
|
37
|
-
open = true
|
|
38
|
-
highlightedIndex = if @autoHighlight and @items.length > 0 then 0 else -1
|
|
39
|
-
@emit 'filter', @query
|
|
40
|
-
|
|
41
|
-
onFocusin: -> @openMenu()
|
|
42
|
-
|
|
43
|
-
onFocusout: (e) ->
|
|
44
|
-
unless @_content?.contains(e.relatedTarget)
|
|
45
|
-
@close()
|
|
46
|
-
|
|
47
|
-
openMenu: ->
|
|
48
|
-
open = true
|
|
49
|
-
highlightedIndex = -1
|
|
50
|
-
setTimeout => @_position(), 0
|
|
51
|
-
|
|
52
|
-
close: ->
|
|
53
|
-
open = false
|
|
54
|
-
highlightedIndex = -1
|
|
55
|
-
|
|
56
|
-
_position: ->
|
|
57
|
-
return unless @_input and @_list
|
|
58
|
-
tr = @_input.getBoundingClientRect()
|
|
59
|
-
@_list.style.left = "#{tr.left}px"
|
|
60
|
-
@_list.style.top = "#{tr.bottom + 2}px"
|
|
61
|
-
@_list.style.minWidth = "#{tr.width}px"
|
|
62
|
-
fl = @_list.getBoundingClientRect()
|
|
63
|
-
if fl.bottom > window.innerHeight
|
|
64
|
-
@_list.style.top = "#{tr.top - fl.height - 2}px"
|
|
65
|
-
|
|
66
|
-
isDisabled: (item) -> item?.hasAttribute?('data-disabled')
|
|
67
|
-
|
|
68
|
-
selectIndex: (idx) ->
|
|
69
|
-
item = @getItems()[idx]
|
|
70
|
-
return unless item
|
|
71
|
-
return if @isDisabled(item)
|
|
72
|
-
val = item.dataset.value ?? item.textContent.trim()
|
|
73
|
-
@query = val
|
|
74
|
-
@emit 'select', val
|
|
75
|
-
@close()
|
|
76
|
-
@_input?.blur()
|
|
77
|
-
|
|
78
|
-
_nextEnabled: (from, dir) ->
|
|
79
|
-
opts = @getItems()
|
|
80
|
-
len = opts.length
|
|
81
|
-
i = from
|
|
82
|
-
loop len
|
|
83
|
-
i = (i + dir) %% len
|
|
84
|
-
return i unless @isDisabled(opts[i])
|
|
85
|
-
from
|
|
86
|
-
|
|
87
|
-
_onKeydown: (e) ->
|
|
88
|
-
len = @getItems().length
|
|
89
|
-
switch e.key
|
|
90
|
-
when 'ArrowDown'
|
|
91
|
-
e.preventDefault()
|
|
92
|
-
@openMenu() unless open
|
|
93
|
-
highlightedIndex = @_nextEnabled(highlightedIndex, 1)
|
|
94
|
-
@_scrollToItem()
|
|
95
|
-
when 'ArrowUp'
|
|
96
|
-
e.preventDefault()
|
|
97
|
-
highlightedIndex = @_nextEnabled(highlightedIndex, -1)
|
|
98
|
-
@_scrollToItem()
|
|
99
|
-
when 'Enter'
|
|
100
|
-
e.preventDefault()
|
|
101
|
-
if highlightedIndex >= 0
|
|
102
|
-
@selectIndex(highlightedIndex)
|
|
103
|
-
else if len is 1
|
|
104
|
-
@selectIndex(0)
|
|
105
|
-
when 'Escape'
|
|
106
|
-
e.preventDefault()
|
|
107
|
-
if open then @close() else @query = ''
|
|
108
|
-
when 'Tab'
|
|
109
|
-
@close()
|
|
110
|
-
|
|
111
|
-
~>
|
|
112
|
-
if open
|
|
113
|
-
onDown = (e) =>
|
|
114
|
-
root = @_content
|
|
115
|
-
unless root?.contains(e.target)
|
|
116
|
-
@close()
|
|
117
|
-
document.addEventListener 'mousedown', onDown
|
|
118
|
-
return -> document.removeEventListener 'mousedown', onDown
|
|
119
|
-
|
|
120
|
-
render
|
|
121
|
-
. ref: "_content", $open: open?!
|
|
122
|
-
|
|
123
|
-
# Text input
|
|
124
|
-
. style: "position:relative;display:inline-flex;align-items:center"
|
|
125
|
-
input ref: "_input", role: "combobox"
|
|
126
|
-
type: "text"
|
|
127
|
-
autocomplete: "off"
|
|
128
|
-
aria-expanded: !!open
|
|
129
|
-
aria-haspopup: "listbox"
|
|
130
|
-
aria-autocomplete: "list"
|
|
131
|
-
aria-controls: if open then _listId else undefined
|
|
132
|
-
aria-activedescendant: if highlightedIndex >= 0 then "#{_listId}-#{highlightedIndex}" else undefined
|
|
133
|
-
$disabled: @disabled?!
|
|
134
|
-
disabled: @disabled
|
|
135
|
-
placeholder: @placeholder
|
|
136
|
-
value: @query
|
|
137
|
-
@keydown: @_onKeydown
|
|
138
|
-
if @query
|
|
139
|
-
button aria-label: "Clear", $clear: true, @click: @clear
|
|
140
|
-
"✕"
|
|
141
|
-
|
|
142
|
-
# Listbox — conditionally rendered (like Select)
|
|
143
|
-
if open and @items.length > 0
|
|
144
|
-
div ref: "_list", id: _listId, role: "listbox", $open: true
|
|
145
|
-
style: "position:fixed"
|
|
146
|
-
for item, idx in @items
|
|
147
|
-
div role: "option", tabindex: "-1", id: "#{_listId}-#{idx}"
|
|
148
|
-
$value: item
|
|
149
|
-
$highlighted: (idx is highlightedIndex)?!
|
|
150
|
-
@click: (=> @selectIndex(idx))
|
|
151
|
-
@mouseenter: (=> highlightedIndex = idx)
|
|
152
|
-
"#{item}"
|
|
153
|
-
if open and @items.length is 0 and @query
|
|
154
|
-
div role: "status", aria-live: "polite", $empty: true
|
|
155
|
-
"No results"
|
package/docs/ui/context-menu.rip
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# ContextMenu — accessible headless right-click menu
|
|
2
|
-
#
|
|
3
|
-
# Opens on contextmenu event (right-click) over the trigger area.
|
|
4
|
-
# Keyboard navigation matches Menu. Ships zero CSS.
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# ContextMenu @select: handleAction
|
|
8
|
-
# div $trigger: true
|
|
9
|
-
# p "Right-click this area"
|
|
10
|
-
# div $item: "cut", "Cut"
|
|
11
|
-
# div $item: "copy", "Copy"
|
|
12
|
-
# div $item: "paste", "Paste"
|
|
13
|
-
|
|
14
|
-
export ContextMenu = component
|
|
15
|
-
@disabled := false
|
|
16
|
-
|
|
17
|
-
open := false
|
|
18
|
-
highlightedIndex := -1
|
|
19
|
-
posX := 0
|
|
20
|
-
posY := 0
|
|
21
|
-
|
|
22
|
-
_menuItems ~=
|
|
23
|
-
return [] unless @_slot
|
|
24
|
-
Array.from(@_slot.querySelectorAll('[data-item]') or [])
|
|
25
|
-
|
|
26
|
-
_triggerEl ~=
|
|
27
|
-
return null unless @_slot
|
|
28
|
-
@_slot.querySelector('[data-trigger]')
|
|
29
|
-
|
|
30
|
-
_onContextMenu: (e) ->
|
|
31
|
-
return if @disabled
|
|
32
|
-
e.preventDefault()
|
|
33
|
-
posX = e.clientX
|
|
34
|
-
posY = e.clientY
|
|
35
|
-
open = true
|
|
36
|
-
highlightedIndex = 0
|
|
37
|
-
requestAnimationFrame =>
|
|
38
|
-
@_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
|
|
39
|
-
|
|
40
|
-
close: ->
|
|
41
|
-
open = false
|
|
42
|
-
highlightedIndex = -1
|
|
43
|
-
|
|
44
|
-
selectIndex: (idx) ->
|
|
45
|
-
item = _menuItems[idx]
|
|
46
|
-
return unless item
|
|
47
|
-
return if item.dataset.disabled?
|
|
48
|
-
@emit 'select', item.dataset.item
|
|
49
|
-
@close()
|
|
50
|
-
|
|
51
|
-
_onKeydown: (e) ->
|
|
52
|
-
len = _menuItems.length
|
|
53
|
-
return unless len
|
|
54
|
-
switch e.key
|
|
55
|
-
when 'ArrowDown'
|
|
56
|
-
e.preventDefault()
|
|
57
|
-
highlightedIndex = (highlightedIndex + 1) %% len
|
|
58
|
-
@_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
|
|
59
|
-
when 'ArrowUp'
|
|
60
|
-
e.preventDefault()
|
|
61
|
-
highlightedIndex = (highlightedIndex - 1) %% len
|
|
62
|
-
@_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
|
|
63
|
-
when 'Home'
|
|
64
|
-
e.preventDefault()
|
|
65
|
-
highlightedIndex = 0
|
|
66
|
-
@_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
|
|
67
|
-
when 'End'
|
|
68
|
-
e.preventDefault()
|
|
69
|
-
highlightedIndex = len - 1
|
|
70
|
-
@_list?.querySelectorAll('[role="menuitem"]')[len - 1]?.focus()
|
|
71
|
-
when 'Enter', ' '
|
|
72
|
-
e.preventDefault()
|
|
73
|
-
@selectIndex(highlightedIndex)
|
|
74
|
-
when 'Escape', 'Tab'
|
|
75
|
-
e.preventDefault() if e.key is 'Escape'
|
|
76
|
-
@close()
|
|
77
|
-
|
|
78
|
-
~>
|
|
79
|
-
if open
|
|
80
|
-
onDown = (e) =>
|
|
81
|
-
unless @_list?.contains(e.target)
|
|
82
|
-
@close()
|
|
83
|
-
document.addEventListener 'mousedown', onDown
|
|
84
|
-
return -> document.removeEventListener 'mousedown', onDown
|
|
85
|
-
|
|
86
|
-
render
|
|
87
|
-
. @contextmenu: @_onContextMenu
|
|
88
|
-
|
|
89
|
-
. ref: "_slot", style: "display:none"
|
|
90
|
-
slot
|
|
91
|
-
|
|
92
|
-
if _triggerEl
|
|
93
|
-
. innerHTML: _triggerEl.innerHTML
|
|
94
|
-
|
|
95
|
-
if open
|
|
96
|
-
div ref: "_list", role: "menu", $open: true, @keydown: @_onKeydown
|
|
97
|
-
style: "position:fixed;left:#{posX}px;top:#{posY}px;z-index:50"
|
|
98
|
-
for item, idx in _menuItems
|
|
99
|
-
div role: "menuitem", tabindex: "-1"
|
|
100
|
-
$highlighted: (idx is highlightedIndex)?!
|
|
101
|
-
$disabled: item.dataset.disabled?!
|
|
102
|
-
$value: item.dataset.item
|
|
103
|
-
@click: (=> @selectIndex(idx))
|
|
104
|
-
@mouseenter: (=> highlightedIndex = idx)
|
|
105
|
-
= item.textContent
|
package/docs/ui/date-picker.rip
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
# DatePicker — accessible headless date picker with calendar
|
|
2
|
-
#
|
|
3
|
-
# A popover calendar for selecting a single date or a date range.
|
|
4
|
-
# Set @range to true for range selection (value becomes [from, to]).
|
|
5
|
-
# Keyboard: Arrow keys navigate days, Enter selects, Escape closes.
|
|
6
|
-
# Ships zero CSS — style entirely via attribute selectors in your stylesheet.
|
|
7
|
-
#
|
|
8
|
-
# Usage:
|
|
9
|
-
# DatePicker value <=> selectedDate, placeholder: "Pick a date"
|
|
10
|
-
# DatePicker value <=> dateRange, range: true
|
|
11
|
-
|
|
12
|
-
dpFmt = (d) ->
|
|
13
|
-
return '' unless d
|
|
14
|
-
m = String(d.getMonth() + 1).padStart(2, '0')
|
|
15
|
-
day = String(d.getDate()).padStart(2, '0')
|
|
16
|
-
"#{m}/#{day}/#{d.getFullYear()}"
|
|
17
|
-
|
|
18
|
-
dpParse = (str) ->
|
|
19
|
-
return null unless str?.length is 10
|
|
20
|
-
parts = str.split('/')
|
|
21
|
-
return null unless parts.length is 3
|
|
22
|
-
[m, d, y] = parts.map Number
|
|
23
|
-
return null if isNaN(m) or isNaN(d) or isNaN(y)
|
|
24
|
-
dt = new Date(y, m - 1, d)
|
|
25
|
-
return null if dt.getMonth() isnt m - 1
|
|
26
|
-
dt
|
|
27
|
-
|
|
28
|
-
dpSameDay = (a, b) ->
|
|
29
|
-
return false unless a and b
|
|
30
|
-
a.getFullYear() is b.getFullYear() and a.getMonth() is b.getMonth() and a.getDate() is b.getDate()
|
|
31
|
-
|
|
32
|
-
dpInRange = (day, from, to) ->
|
|
33
|
-
return false unless day and from and to
|
|
34
|
-
t = day.getTime()
|
|
35
|
-
lo = Math.min(from.getTime(), to.getTime())
|
|
36
|
-
hi = Math.max(from.getTime(), to.getTime())
|
|
37
|
-
t >= lo and t <= hi
|
|
38
|
-
|
|
39
|
-
export DatePicker = component
|
|
40
|
-
@value := null
|
|
41
|
-
@placeholder := 'mm/dd/yyyy'
|
|
42
|
-
@disabled := false
|
|
43
|
-
@range := false
|
|
44
|
-
@firstDayOfWeek := 0
|
|
45
|
-
|
|
46
|
-
open := false
|
|
47
|
-
viewMonth := new Date()
|
|
48
|
-
_rangeStart := null
|
|
49
|
-
_hoveredDay := null
|
|
50
|
-
_inputText := ''
|
|
51
|
-
_id =! "dp-#{Math.random().toString(36).slice(2, 8)}"
|
|
52
|
-
|
|
53
|
-
_dayNames =! ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
|
|
54
|
-
|
|
55
|
-
_daysInView ~=
|
|
56
|
-
yr = viewMonth.getFullYear()
|
|
57
|
-
mo = viewMonth.getMonth()
|
|
58
|
-
firstOfMonth = new Date(yr, mo, 1)
|
|
59
|
-
startDay = firstOfMonth.getDay()
|
|
60
|
-
offset = (startDay - @firstDayOfWeek + 7) %% 7
|
|
61
|
-
daysInMonth = new Date(yr, mo + 1, 0).getDate()
|
|
62
|
-
prevMonthDays = new Date(yr, mo, 0).getDate()
|
|
63
|
-
dayList = []
|
|
64
|
-
for n in [0...offset]
|
|
65
|
-
dayList.push { date: new Date(yr, mo - 1, prevMonthDays - offset + n + 1), outside: true }
|
|
66
|
-
for n in [1..daysInMonth]
|
|
67
|
-
dayList.push { date: new Date(yr, mo, n), outside: false }
|
|
68
|
-
trailing = (7 - dayList.length %% 7) %% 7
|
|
69
|
-
for n in [1..trailing]
|
|
70
|
-
dayList.push { date: new Date(yr, mo + 1, n), outside: true }
|
|
71
|
-
dayList
|
|
72
|
-
|
|
73
|
-
_displayText ~=
|
|
74
|
-
if @range
|
|
75
|
-
if Array.isArray(@value) and @value[0]
|
|
76
|
-
from = dpFmt(@value[0])
|
|
77
|
-
to = if @value[1] then dpFmt(@value[1]) else '...'
|
|
78
|
-
"#{from} – #{to}"
|
|
79
|
-
else
|
|
80
|
-
@placeholder
|
|
81
|
-
else
|
|
82
|
-
if @value then dpFmt(@value) else @placeholder
|
|
83
|
-
|
|
84
|
-
_monthLabel ~=
|
|
85
|
-
viewMonth.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
|
|
86
|
-
|
|
87
|
-
_today =! new Date()
|
|
88
|
-
|
|
89
|
-
_prevMonth: ->
|
|
90
|
-
viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() - 1, 1)
|
|
91
|
-
|
|
92
|
-
_nextMonth: ->
|
|
93
|
-
viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1)
|
|
94
|
-
|
|
95
|
-
_selectDay: (day) ->
|
|
96
|
-
return if @disabled
|
|
97
|
-
if @range
|
|
98
|
-
if _rangeStart and not dpSameDay(_rangeStart, day)
|
|
99
|
-
from = if _rangeStart < day then _rangeStart else day
|
|
100
|
-
to = if _rangeStart < day then day else _rangeStart
|
|
101
|
-
@value = [from, to]
|
|
102
|
-
_rangeStart = null
|
|
103
|
-
@emit 'change', @value
|
|
104
|
-
open = false
|
|
105
|
-
else
|
|
106
|
-
_rangeStart = day
|
|
107
|
-
@value = [day, null]
|
|
108
|
-
else
|
|
109
|
-
@value = day
|
|
110
|
-
@emit 'change', @value
|
|
111
|
-
open = false
|
|
112
|
-
_inputText = _displayText
|
|
113
|
-
|
|
114
|
-
_onInputChange: (e) ->
|
|
115
|
-
raw = e.target.value.replace(/[^\d\/]/g, '')
|
|
116
|
-
if raw.length <= 10
|
|
117
|
-
_inputText = raw
|
|
118
|
-
if raw.length is 10
|
|
119
|
-
dt = dpParse(raw)
|
|
120
|
-
if dt
|
|
121
|
-
@value = dt
|
|
122
|
-
viewMonth = new Date(dt.getFullYear(), dt.getMonth(), 1)
|
|
123
|
-
@emit 'change', @value
|
|
124
|
-
|
|
125
|
-
toggle: ->
|
|
126
|
-
return if @disabled
|
|
127
|
-
if open then @close() else @openPicker()
|
|
128
|
-
|
|
129
|
-
openPicker: ->
|
|
130
|
-
open = true
|
|
131
|
-
if @value and not @range
|
|
132
|
-
viewMonth = new Date(@value.getFullYear(), @value.getMonth(), 1)
|
|
133
|
-
else if @range and Array.isArray(@value) and @value[0]
|
|
134
|
-
viewMonth = new Date(@value[0].getFullYear(), @value[0].getMonth(), 1)
|
|
135
|
-
_inputText = _displayText
|
|
136
|
-
setTimeout => @_position(), 0
|
|
137
|
-
|
|
138
|
-
close: ->
|
|
139
|
-
open = false
|
|
140
|
-
_rangeStart = null
|
|
141
|
-
_hoveredDay = null
|
|
142
|
-
|
|
143
|
-
_position: ->
|
|
144
|
-
return unless @_trigger and @_cal
|
|
145
|
-
tr = @_trigger.getBoundingClientRect()
|
|
146
|
-
@_cal.style.position = 'fixed'
|
|
147
|
-
@_cal.style.left = "#{tr.left}px"
|
|
148
|
-
@_cal.style.top = "#{tr.bottom + 4}px"
|
|
149
|
-
fl = @_cal.getBoundingClientRect()
|
|
150
|
-
if fl.bottom > window.innerHeight
|
|
151
|
-
@_cal.style.top = "#{tr.top - fl.height - 4}px"
|
|
152
|
-
|
|
153
|
-
_onKeydown: (e) ->
|
|
154
|
-
switch e.key
|
|
155
|
-
when 'Escape'
|
|
156
|
-
e.preventDefault()
|
|
157
|
-
@close()
|
|
158
|
-
@_trigger?.focus()
|
|
159
|
-
when 'Enter', ' '
|
|
160
|
-
e.preventDefault()
|
|
161
|
-
@toggle() unless open
|
|
162
|
-
|
|
163
|
-
~>
|
|
164
|
-
if open
|
|
165
|
-
onDown = (e) =>
|
|
166
|
-
unless @_trigger?.contains(e.target) or @_cal?.contains(e.target)
|
|
167
|
-
@close()
|
|
168
|
-
document.addEventListener 'mousedown', onDown
|
|
169
|
-
return -> document.removeEventListener 'mousedown', onDown
|
|
170
|
-
|
|
171
|
-
render
|
|
172
|
-
. $open: open?!, $disabled: @disabled?!, $range: @range?!
|
|
173
|
-
|
|
174
|
-
# Trigger
|
|
175
|
-
button ref: "_trigger", $trigger: true
|
|
176
|
-
aria-haspopup: "dialog"
|
|
177
|
-
aria-expanded: !!open
|
|
178
|
-
disabled: @disabled
|
|
179
|
-
@click: @toggle
|
|
180
|
-
@keydown: @_onKeydown
|
|
181
|
-
_displayText
|
|
182
|
-
|
|
183
|
-
# Calendar dropdown
|
|
184
|
-
if open
|
|
185
|
-
div ref: "_cal", role: "dialog", aria-label: "Date picker", $calendar: true
|
|
186
|
-
style: "position:fixed;z-index:50"
|
|
187
|
-
|
|
188
|
-
# Month navigation
|
|
189
|
-
. $header: true
|
|
190
|
-
button $prev: true, aria-label: "Previous month", @click: @_prevMonth
|
|
191
|
-
"‹"
|
|
192
|
-
span $month-label: true
|
|
193
|
-
_monthLabel
|
|
194
|
-
button $next: true, aria-label: "Next month", @click: @_nextMonth
|
|
195
|
-
"›"
|
|
196
|
-
|
|
197
|
-
# Day-of-week headers
|
|
198
|
-
. $weekdays: true
|
|
199
|
-
for dayName in _dayNames
|
|
200
|
-
span $weekday: true
|
|
201
|
-
dayName
|
|
202
|
-
|
|
203
|
-
# Day grid
|
|
204
|
-
. $days: true, role: "grid"
|
|
205
|
-
for entry, dIdx in _daysInView
|
|
206
|
-
button role: "gridcell", tabindex: "-1"
|
|
207
|
-
$outside: entry.outside?!
|
|
208
|
-
$today: dpSameDay(entry.date, _today)?!
|
|
209
|
-
$selected: (if @range then (Array.isArray(@value) and (dpSameDay(entry.date, @value[0]) or dpSameDay(entry.date, @value[1]))) else dpSameDay(entry.date, @value))?!
|
|
210
|
-
$in-range: (if @range then (if _rangeStart then dpInRange(entry.date, _rangeStart, _hoveredDay or _rangeStart) else if Array.isArray(@value) and @value[0] and @value[1] then dpInRange(entry.date, @value[0], @value[1]) else false) else false)?!
|
|
211
|
-
$range-start: (if @range and _rangeStart then dpSameDay(entry.date, _rangeStart) else false)?!
|
|
212
|
-
@click: (=> @_selectDay(entry.date))
|
|
213
|
-
@mouseenter: (=> _hoveredDay = entry.date)
|
|
214
|
-
entry.date.getDate()
|
package/docs/ui/dialog.rip
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# Dialog — accessible headless modal dialog
|
|
2
|
-
#
|
|
3
|
-
# Traps focus, locks scroll, dismisses on Escape or click outside.
|
|
4
|
-
# Restores focus to the previously focused element on close.
|
|
5
|
-
# Auto-wires aria-labelledby (first h1-h6) and aria-describedby (first p).
|
|
6
|
-
#
|
|
7
|
-
# Exposes $open on the backdrop. Ships zero CSS.
|
|
8
|
-
#
|
|
9
|
-
# Usage:
|
|
10
|
-
# Dialog open <=> showDialog, @close: handleClose
|
|
11
|
-
# h2 "Title"
|
|
12
|
-
# p "Content"
|
|
13
|
-
# button @click: (=> showDialog = false), "Close"
|
|
14
|
-
|
|
15
|
-
dialogStack = []
|
|
16
|
-
|
|
17
|
-
export Dialog = component
|
|
18
|
-
@open := false
|
|
19
|
-
@dismissable := true
|
|
20
|
-
@initialFocus := null
|
|
21
|
-
|
|
22
|
-
_prevFocus = null
|
|
23
|
-
_cleanupTrap = null
|
|
24
|
-
_scrollY = 0
|
|
25
|
-
_id =! "dlg-#{Math.random().toString(36).slice(2, 8)}"
|
|
26
|
-
|
|
27
|
-
_wireAria: ->
|
|
28
|
-
panel = @_panel
|
|
29
|
-
return unless panel
|
|
30
|
-
heading = panel.querySelector('h1,h2,h3,h4,h5,h6')
|
|
31
|
-
if heading
|
|
32
|
-
heading.id ?= "#{_id}-title"
|
|
33
|
-
panel.setAttribute 'aria-labelledby', heading.id
|
|
34
|
-
desc = panel.querySelector('p')
|
|
35
|
-
if desc
|
|
36
|
-
desc.id ?= "#{_id}-desc"
|
|
37
|
-
panel.setAttribute 'aria-describedby', desc.id
|
|
38
|
-
|
|
39
|
-
~>
|
|
40
|
-
if @open
|
|
41
|
-
_prevFocus = document.activeElement
|
|
42
|
-
_scrollY = window.scrollY
|
|
43
|
-
dialogStack.push this
|
|
44
|
-
document.body.style.position = 'fixed'
|
|
45
|
-
document.body.style.top = "-#{_scrollY}px"
|
|
46
|
-
document.body.style.width = '100%'
|
|
47
|
-
|
|
48
|
-
setTimeout =>
|
|
49
|
-
panel = @_panel
|
|
50
|
-
if panel
|
|
51
|
-
@_wireAria()
|
|
52
|
-
if @initialFocus
|
|
53
|
-
target = if typeof @initialFocus is 'string' then panel.querySelector(@initialFocus) else @initialFocus
|
|
54
|
-
target?.focus()
|
|
55
|
-
else
|
|
56
|
-
focusable = panel.querySelectorAll 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
|
|
57
|
-
focusable[0]?.focus()
|
|
58
|
-
_cleanupTrap = (e) ->
|
|
59
|
-
return unless e.key is 'Tab'
|
|
60
|
-
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
|
|
61
|
-
return unless list.length
|
|
62
|
-
first = list[0]
|
|
63
|
-
last = list[list.length - 1]
|
|
64
|
-
if e.shiftKey
|
|
65
|
-
if document.activeElement is first then (e.preventDefault(); last.focus())
|
|
66
|
-
else
|
|
67
|
-
if document.activeElement is last then (e.preventDefault(); first.focus())
|
|
68
|
-
panel.addEventListener 'keydown', _cleanupTrap
|
|
69
|
-
, 0
|
|
70
|
-
|
|
71
|
-
return ->
|
|
72
|
-
idx = dialogStack.indexOf this
|
|
73
|
-
dialogStack.splice(idx, 1) if idx >= 0
|
|
74
|
-
document.body.style.position = '' unless dialogStack.length
|
|
75
|
-
document.body.style.top = '' unless dialogStack.length
|
|
76
|
-
document.body.style.width = '' unless dialogStack.length
|
|
77
|
-
window.scrollTo 0, _scrollY unless dialogStack.length
|
|
78
|
-
_prevFocus?.focus()
|
|
79
|
-
else
|
|
80
|
-
idx = dialogStack.indexOf this
|
|
81
|
-
dialogStack.splice(idx, 1) if idx >= 0
|
|
82
|
-
unless dialogStack.length
|
|
83
|
-
document.body.style.position = ''
|
|
84
|
-
document.body.style.top = ''
|
|
85
|
-
document.body.style.width = ''
|
|
86
|
-
window.scrollTo 0, _scrollY
|
|
87
|
-
_prevFocus?.focus()
|
|
88
|
-
|
|
89
|
-
close: ->
|
|
90
|
-
@open = false
|
|
91
|
-
@emit 'close'
|
|
92
|
-
|
|
93
|
-
onKeydown: (e) ->
|
|
94
|
-
if e.key is 'Escape' and dialogStack[dialogStack.length - 1] is this
|
|
95
|
-
e.preventDefault()
|
|
96
|
-
@close() if @dismissable
|
|
97
|
-
|
|
98
|
-
onBackdropClick: (e) ->
|
|
99
|
-
@close() if e.target is e.currentTarget and @dismissable
|
|
100
|
-
|
|
101
|
-
render
|
|
102
|
-
if @open
|
|
103
|
-
div ref: "_backdrop", $open: true,
|
|
104
|
-
@click: @onBackdropClick,
|
|
105
|
-
@keydown: @onKeydown
|
|
106
|
-
div ref: "_panel", role: "dialog", aria-modal: "true", tabindex: "-1"
|
|
107
|
-
slot
|
package/docs/ui/drawer.rip
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# Drawer — accessible headless slide-out panel
|
|
2
|
-
#
|
|
3
|
-
# A Dialog variant that slides from an edge of the screen.
|
|
4
|
-
# Supports dismiss on escape, click-outside, and optional swipe-to-close.
|
|
5
|
-
# Ships zero CSS.
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# Drawer open <=> showDrawer, side: "right"
|
|
9
|
-
# h2 "Settings"
|
|
10
|
-
# p "Panel content here"
|
|
11
|
-
|
|
12
|
-
export Drawer = component
|
|
13
|
-
@open := false
|
|
14
|
-
@side := 'right'
|
|
15
|
-
@dismissable := true
|
|
16
|
-
|
|
17
|
-
_prevFocus = null
|
|
18
|
-
_scrollY = 0
|
|
19
|
-
_id =! "drw-#{Math.random().toString(36).slice(2, 8)}"
|
|
20
|
-
|
|
21
|
-
_wireAria: ->
|
|
22
|
-
panel = @_panel
|
|
23
|
-
return unless panel
|
|
24
|
-
heading = panel.querySelector('h1,h2,h3,h4,h5,h6')
|
|
25
|
-
if heading
|
|
26
|
-
heading.id ?= "#{_id}-title"
|
|
27
|
-
panel.setAttribute 'aria-labelledby', heading.id
|
|
28
|
-
desc = panel.querySelector('p')
|
|
29
|
-
if desc
|
|
30
|
-
desc.id ?= "#{_id}-desc"
|
|
31
|
-
panel.setAttribute 'aria-describedby', desc.id
|
|
32
|
-
|
|
33
|
-
~>
|
|
34
|
-
if @open
|
|
35
|
-
_prevFocus = document.activeElement
|
|
36
|
-
document.body.style.overflow = 'hidden'
|
|
37
|
-
|
|
38
|
-
setTimeout =>
|
|
39
|
-
panel = @_panel
|
|
40
|
-
if panel
|
|
41
|
-
@_wireAria()
|
|
42
|
-
focusable = panel.querySelectorAll 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
|
|
43
|
-
focusable[0]?.focus()
|
|
44
|
-
@_trapHandler = (e) ->
|
|
45
|
-
return unless e.key is 'Tab'
|
|
46
|
-
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
|
|
47
|
-
return unless list.length
|
|
48
|
-
first = list[0]
|
|
49
|
-
last = list[list.length - 1]
|
|
50
|
-
if e.shiftKey
|
|
51
|
-
if document.activeElement is first then (e.preventDefault(); last.focus())
|
|
52
|
-
else
|
|
53
|
-
if document.activeElement is last then (e.preventDefault(); first.focus())
|
|
54
|
-
panel.addEventListener 'keydown', @_trapHandler
|
|
55
|
-
, 0
|
|
56
|
-
else
|
|
57
|
-
document.body.style.overflow = ''
|
|
58
|
-
_prevFocus?.focus()
|
|
59
|
-
|
|
60
|
-
close: ->
|
|
61
|
-
@open = false
|
|
62
|
-
@emit 'close'
|
|
63
|
-
|
|
64
|
-
onKeydown: (e) ->
|
|
65
|
-
if e.key is 'Escape' and @dismissable
|
|
66
|
-
e.preventDefault()
|
|
67
|
-
@close()
|
|
68
|
-
|
|
69
|
-
onBackdropClick: (e) ->
|
|
70
|
-
@close() if e.target is e.currentTarget and @dismissable
|
|
71
|
-
|
|
72
|
-
render
|
|
73
|
-
if @open
|
|
74
|
-
div ref: "_backdrop", $open: true, $side: @side
|
|
75
|
-
@click: @onBackdropClick
|
|
76
|
-
@keydown: @onKeydown
|
|
77
|
-
div ref: "_panel", role: "dialog", aria-modal: "true", tabindex: "-1"
|
|
78
|
-
$side: @side
|
|
79
|
-
slot
|