rip-lang 3.15.4 → 3.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +6 -4
  2. package/bin/rip +167 -12
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-APP.md +808 -0
  5. package/docs/RIP-DUCKDB.md +477 -0
  6. package/docs/RIP-INTRO.md +396 -0
  7. package/docs/RIP-LANG.md +59 -5
  8. package/docs/RIP-SCHEMA.md +191 -8
  9. package/docs/RIP-TYPES.md +74 -103
  10. package/docs/demo/README.md +4 -3
  11. package/docs/dist/rip.js +3627 -1470
  12. package/docs/dist/rip.min.js +671 -244
  13. package/docs/dist/rip.min.js.br +0 -0
  14. package/docs/example/index.json +7 -7
  15. package/docs/example/index.json.br +0 -0
  16. package/docs/extensions/duckdb/manifest.json +1 -1
  17. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  18. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  19. package/docs/extensions/vscode/print/index.html +2 -1
  20. package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
  21. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  22. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  23. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  24. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  25. package/docs/ui/bundle.json +61 -0
  26. package/docs/ui/bundle.json.br +0 -0
  27. package/docs/ui/hljs-rip.js +0 -7
  28. package/docs/ui/index.css +66 -23
  29. package/docs/ui/index.html +6 -6
  30. package/package.json +9 -3
  31. package/rip-loader.js +64 -2
  32. package/src/AGENTS.md +63 -36
  33. package/src/browser.js +96 -14
  34. package/src/compiler.js +960 -143
  35. package/src/components.js +794 -88
  36. package/src/{types-emit.js → dts.js} +181 -71
  37. package/src/grammar/README.md +1 -1
  38. package/src/grammar/grammar.rip +111 -97
  39. package/src/lexer.js +132 -18
  40. package/src/parser.js +203 -205
  41. package/src/repl.js +74 -6
  42. package/src/schema/runtime-orm.js +168 -4
  43. package/src/schema/runtime-validate.js +146 -2
  44. package/src/schema/runtime.generated.js +314 -6
  45. package/src/schema/schema.js +5 -5
  46. package/src/sourcemaps.js +277 -1
  47. package/src/stdlib.js +253 -0
  48. package/src/typecheck.js +2023 -106
  49. package/src/types.js +127 -7
  50. package/docs/ui/accordion.rip +0 -103
  51. package/docs/ui/alert-dialog.rip +0 -53
  52. package/docs/ui/autocomplete.rip +0 -115
  53. package/docs/ui/avatar.rip +0 -37
  54. package/docs/ui/badge.rip +0 -15
  55. package/docs/ui/breadcrumb.rip +0 -47
  56. package/docs/ui/button-group.rip +0 -26
  57. package/docs/ui/button.rip +0 -23
  58. package/docs/ui/card.rip +0 -25
  59. package/docs/ui/carousel.rip +0 -110
  60. package/docs/ui/checkbox-group.rip +0 -61
  61. package/docs/ui/checkbox.rip +0 -33
  62. package/docs/ui/collapsible.rip +0 -50
  63. package/docs/ui/combobox.rip +0 -130
  64. package/docs/ui/context-menu.rip +0 -88
  65. package/docs/ui/date-picker.rip +0 -206
  66. package/docs/ui/dialog.rip +0 -60
  67. package/docs/ui/drawer.rip +0 -58
  68. package/docs/ui/editable-value.rip +0 -82
  69. package/docs/ui/field.rip +0 -53
  70. package/docs/ui/fieldset.rip +0 -22
  71. package/docs/ui/form.rip +0 -39
  72. package/docs/ui/grid.rip +0 -901
  73. package/docs/ui/input-group.rip +0 -28
  74. package/docs/ui/input.rip +0 -36
  75. package/docs/ui/label.rip +0 -16
  76. package/docs/ui/menu.rip +0 -134
  77. package/docs/ui/menubar.rip +0 -151
  78. package/docs/ui/meter.rip +0 -36
  79. package/docs/ui/multi-select.rip +0 -203
  80. package/docs/ui/native-select.rip +0 -33
  81. package/docs/ui/nav-menu.rip +0 -126
  82. package/docs/ui/number-field.rip +0 -162
  83. package/docs/ui/otp-field.rip +0 -89
  84. package/docs/ui/pagination.rip +0 -123
  85. package/docs/ui/popover.rip +0 -93
  86. package/docs/ui/preview-card.rip +0 -75
  87. package/docs/ui/progress.rip +0 -25
  88. package/docs/ui/radio-group.rip +0 -57
  89. package/docs/ui/resizable.rip +0 -123
  90. package/docs/ui/scroll-area.rip +0 -145
  91. package/docs/ui/select.rip +0 -151
  92. package/docs/ui/separator.rip +0 -17
  93. package/docs/ui/skeleton.rip +0 -22
  94. package/docs/ui/slider.rip +0 -165
  95. package/docs/ui/spinner.rip +0 -17
  96. package/docs/ui/table.rip +0 -27
  97. package/docs/ui/tabs.rip +0 -113
  98. package/docs/ui/textarea.rip +0 -48
  99. package/docs/ui/toast.rip +0 -87
  100. package/docs/ui/toggle-group.rip +0 -71
  101. package/docs/ui/toggle.rip +0 -24
  102. package/docs/ui/toolbar.rip +0 -38
  103. package/docs/ui/tooltip.rip +0 -85
  104. package/src/app.rip +0 -1571
  105. package/src/sourcemap-merge.js +0 -287
  106. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  107. /package/docs/demo/{components → routes}/about.rip +0 -0
  108. /package/docs/demo/{components → routes}/card.rip +0 -0
  109. /package/docs/demo/{components → routes}/counter.rip +0 -0
  110. /package/docs/demo/{components → routes}/index.rip +0 -0
  111. /package/docs/demo/{components → routes}/todos.rip +0 -0
  112. /package/src/schema/{dts-emit.js → dts.js} +0 -0
@@ -1,28 +0,0 @@
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:: boolean := 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/docs/ui/input.rip DELETED
@@ -1,36 +0,0 @@
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:: string := ""
12
- @placeholder:: string := ""
13
- @type:: string := "text"
14
- @disabled:: boolean := false
15
- @required:: boolean := 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/docs/ui/label.rip DELETED
@@ -1,16 +0,0 @@
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:: any := null
12
- @required:: boolean := false
13
-
14
- render
15
- label for: @for?!, $required: @required?!
16
- slot
package/docs/ui/menu.rip DELETED
@@ -1,134 +0,0 @@
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
- # Uses native `popover="auto"` + anchor positioning. 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:: boolean := false
16
-
17
- open := false
18
- highlightedIndex := -1
19
- typeaheadBuffer := ''
20
- typeaheadTimer := null
21
- _id =! "menu-#{Math.random().toString(36).slice(2, 8)}"
22
-
23
- items ~=
24
- return [] unless @_slot
25
- Array.from(@_slot.querySelectorAll('[data-item]') or [])
26
-
27
- triggerLabel ~=
28
- return '' unless @_slot
29
- el = @_slot.querySelector(':not([data-item])')
30
- el?.textContent?.trim() or ''
31
-
32
- toggle: ->
33
- return if @disabled
34
- if open then @close() else @openMenu()
35
-
36
- openMenu: ->
37
- open = true
38
- highlightedIndex = 0
39
- requestAnimationFrame => @_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
40
-
41
- close: ->
42
- open = false
43
- highlightedIndex = -1
44
- @_trigger?.focus()
45
-
46
- selectIndex: (idx) ->
47
- item = items[idx]
48
- return unless item
49
- return if item.dataset.disabled?
50
- role = item.getAttribute('role')
51
- if role is 'menuitemcheckbox'
52
- checked = item.getAttribute('aria-checked') is 'true'
53
- item.setAttribute 'aria-checked', not checked
54
- @emit 'select', { id: item.dataset.item, checked: not checked }
55
- else if role is 'menuitemradio'
56
- group = item.closest('[data-radio-group]')
57
- if group
58
- group.querySelectorAll('[role="menuitemradio"]').forEach (r) ->
59
- r.setAttribute 'aria-checked', false
60
- item.setAttribute 'aria-checked', true
61
- @emit 'select', { id: item.dataset.item, value: item.dataset.item }
62
- else
63
- @emit 'select', item.dataset.item
64
- @close()
65
-
66
- _typeahead: (char) ->
67
- clearTimeout typeaheadTimer if typeaheadTimer
68
- typeaheadBuffer += char.toLowerCase()
69
- typeaheadTimer = setTimeout (-> typeaheadBuffer = ''), 500
70
- idx = items.findIndex (o) -> o.textContent.trim().toLowerCase().startsWith(typeaheadBuffer)
71
- if idx >= 0
72
- highlightedIndex = idx
73
- @_list?.querySelectorAll('[role="menuitem"]')[idx]?.focus()
74
-
75
- _applyPlacement: ->
76
- ARIA.position @_trigger, @_list, placement: 'bottom start', offset: 4
77
-
78
- onTriggerKeydown: (e) ->
79
- return if @disabled
80
- if e.key in ['ArrowDown', 'Enter', ' ']
81
- e.preventDefault()
82
- @openMenu()
83
-
84
- _focusItem: (idx) ->
85
- highlightedIndex = idx
86
- @_list?.querySelectorAll('[role="menuitem"]')[idx]?.focus()
87
-
88
- onMenuKeydown: (e) ->
89
- len = items.length
90
- return unless len
91
- ARIA.listNav e,
92
- next: => @_focusItem((highlightedIndex + 1) %% len)
93
- prev: => @_focusItem((highlightedIndex - 1) %% len)
94
- first: => @_focusItem(0)
95
- last: => @_focusItem(len - 1)
96
- select: => @selectIndex(highlightedIndex)
97
- dismiss: => @close()
98
- tab: => @close()
99
- char: => @_typeahead(e.key)
100
-
101
- ~>
102
- if @_list
103
- @_list.id = _id
104
- @_list.setAttribute 'popover', 'auto'
105
- @_applyPlacement()
106
- if @_trigger
107
- @_trigger.setAttribute 'aria-controls', _id
108
- ARIA.bindPopover open, (=> @_list), ((isOpen) => open = isOpen), (=> @_trigger)
109
-
110
- render
111
- .
112
- button ref: "_trigger"
113
- aria-haspopup: "menu"
114
- aria-expanded: !!open
115
- $open: open?!
116
- $disabled: @disabled?!
117
- disabled: @disabled
118
- @click: @toggle
119
- @keydown: @onTriggerKeydown
120
- triggerLabel
121
-
122
- . ref: "_slot", style: "display:none"
123
- slot
124
-
125
- div ref: "_list", role: "menu", $open: open?!, style: "position:fixed;margin:0;inset:auto", @keydown: @onMenuKeydown
126
- for item, idx in items
127
- div role: item.getAttribute('role') or 'menuitem'
128
- tabindex: "-1"
129
- aria-checked: item.getAttribute('aria-checked')?!
130
- $highlighted: (idx is highlightedIndex)?!
131
- $disabled: item.dataset.disabled?!
132
- @click: (=> @selectIndex(idx))
133
- @mouseenter: (=> highlightedIndex = idx)
134
- = item.textContent
@@ -1,151 +0,0 @@
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:: boolean := 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
- ARIA.positionBelow @_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]"),
45
- @_root?.querySelector("[data-menu-list=\"#{menuId}\"]"), 2, false
46
-
47
- selectItem: (menuId, itemId) ->
48
- @emit 'select', { menu: menuId, item: itemId }
49
- @_closeMenu()
50
- @_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]")?.focus()
51
-
52
- _onBarKeydown: (e) ->
53
- triggers = @_root?.querySelectorAll('[data-menu-trigger]')
54
- return unless triggers?.length
55
- focused = Array.from(triggers).indexOf(document.activeElement)
56
- return if focused < 0
57
- len = triggers.length
58
- switch e.key
59
- when 'ArrowRight'
60
- e.preventDefault()
61
- next = (focused + 1) %% len
62
- triggers[next]?.focus()
63
- if activeMenu
64
- @_openMenu(triggers[next]?.dataset.menuTrigger)
65
- when 'ArrowLeft'
66
- e.preventDefault()
67
- prev = (focused - 1) %% len
68
- triggers[prev]?.focus()
69
- if activeMenu
70
- @_openMenu(triggers[prev]?.dataset.menuTrigger)
71
- when 'ArrowDown', 'Enter', ' '
72
- e.preventDefault()
73
- menuId = triggers[focused]?.dataset.menuTrigger
74
- @_openMenu(menuId) if menuId
75
- when 'Escape'
76
- @_closeMenu()
77
-
78
- _onMenuKeydown: (e, menuId, menuItems) ->
79
- len = menuItems.length
80
- return unless len
81
- switch e.key
82
- when 'ArrowDown'
83
- e.preventDefault()
84
- highlightedIndex = (highlightedIndex + 1) %% len
85
- @_root?.querySelector("[data-menu-list=\"#{menuId}\"]")?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
86
- when 'ArrowUp'
87
- e.preventDefault()
88
- highlightedIndex = (highlightedIndex - 1) %% len
89
- @_root?.querySelector("[data-menu-list=\"#{menuId}\"]")?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
90
- when 'Enter', ' '
91
- e.preventDefault()
92
- item = menuItems[highlightedIndex]
93
- @selectItem(menuId, item?.dataset.item) if item
94
- when 'Escape', 'Tab'
95
- e.preventDefault() if e.key is 'Escape'
96
- @_closeMenu()
97
- @_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]")?.focus()
98
- when 'ArrowRight'
99
- e.preventDefault()
100
- @_closeMenu()
101
- triggers = @_root?.querySelectorAll('[data-menu-trigger]')
102
- focused = Array.from(triggers).findIndex (t) -> t.dataset.menuTrigger is menuId
103
- next = (focused + 1) %% triggers.length
104
- triggers[next]?.focus()
105
- @_openMenu(triggers[next]?.dataset.menuTrigger)
106
- when 'ArrowLeft'
107
- e.preventDefault()
108
- @_closeMenu()
109
- triggers = @_root?.querySelectorAll('[data-menu-trigger]')
110
- focused = Array.from(triggers).findIndex (t) -> t.dataset.menuTrigger is menuId
111
- prev = (focused - 1) %% triggers.length
112
- triggers[prev]?.focus()
113
- @_openMenu(triggers[prev]?.dataset.menuTrigger)
114
-
115
- ~>
116
- if activeMenu
117
- onDown = (e) => @_closeMenu() unless @_root?.contains(e.target)
118
- onScroll = => @_position(activeMenu)
119
- document.addEventListener 'mousedown', onDown
120
- window.addEventListener 'scroll', onScroll, true
121
- return ->
122
- document.removeEventListener 'mousedown', onDown
123
- window.removeEventListener 'scroll', onScroll, true
124
-
125
- render
126
- div ref: "_root", role: "menubar", $disabled: @disabled?!
127
-
128
- . ref: "_slot", style: "display:none"
129
- slot
130
-
131
- for menu in _menus
132
- button role: "menuitem", tabindex: "0"
133
- "data-menu-trigger": menu.dataset.menu
134
- aria-haspopup: "menu"
135
- aria-expanded: activeMenu is menu.dataset.menu
136
- $open: (activeMenu is menu.dataset.menu)?!
137
- @click: (=> if activeMenu is menu.dataset.menu then @_closeMenu() else @_openMenu(menu.dataset.menu))
138
- @keydown: @_onBarKeydown
139
- = menu.dataset.menu
140
-
141
- if activeMenu is menu.dataset.menu
142
- div role: "menu", $open: true, style: "position:fixed"
143
- "data-menu-list": menu.dataset.menu
144
- @keydown: (e) => @_onMenuKeydown(e, menu.dataset.menu, @_menuItemsFor(menu))
145
- for item, idx in @_menuItemsFor(menu)
146
- div role: "menuitem", tabindex: "-1"
147
- $highlighted: (idx is highlightedIndex)?!
148
- $value: item.dataset.item
149
- @click: (=> @selectItem(menu.dataset.menu, item.dataset.item))
150
- @mouseenter: (=> highlightedIndex = idx)
151
- = item.textContent
package/docs/ui/meter.rip DELETED
@@ -1,36 +0,0 @@
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:: number := 0
13
- @min:: number := 0
14
- @max:: number := 1
15
- @low:: any := null
16
- @high:: any := null
17
- @optimum:: any := null
18
- @label:: any := 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
@@ -1,203 +0,0 @@
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:: any[] := []
12
- @items:: any[] := []
13
- @placeholder:: string := "Select..."
14
- @disabled:: boolean := false
15
-
16
- open := false
17
- query := ''
18
- highlightedIndex := -1
19
- _ready := false
20
- _ignoreInputClickOnce := false
21
- _popupGuard =! ARIA.popupGuard()
22
- _listId =! "ms-#{Math.random().toString(36).slice(2, 8)}"
23
-
24
- filtered ~=
25
- q = query.trim().toLowerCase()
26
- return @items unless q
27
- @items.filter (item) ->
28
- label = if typeof item is 'string' then item else (item.label or item.name or String(item))
29
- label.toLowerCase().includes(q)
30
-
31
- # Block reopen briefly so the same pointer gesture that closed the popup
32
- # cannot immediately reopen it via focus/click side effects.
33
- _blockOpenBriefly: ->
34
- _popupGuard.block()
35
-
36
- _canOpen: ->
37
- _popupGuard.canOpen()
38
-
39
- _label: (item) ->
40
- if typeof item is 'string' then item else (item.label or item.name or String(item))
41
-
42
- _val: (item) ->
43
- if typeof item is 'string' then item else (item.value or item.id or String(item))
44
-
45
- _isSelected: (item) ->
46
- v = @_val(item)
47
- Array.isArray(@value) and v in @value
48
-
49
- _toggleItem: (item) ->
50
- return if @disabled
51
- v = @_val(item)
52
- arr = if Array.isArray(@value) then [...@value] else []
53
- if v in arr
54
- arr = arr.filter (x) -> x isnt v
55
- else
56
- arr.push v
57
- @value = arr
58
- @emit 'change', @value
59
-
60
- _removeChip: (v) ->
61
- return if @disabled
62
- @value = @value.filter (x) -> x isnt v
63
- @emit 'change', @value
64
-
65
- _onRemoveMousedown: (e) ->
66
- e.preventDefault()
67
- e.stopPropagation()
68
- @_blockOpenBriefly()
69
-
70
- _onInput: (e) ->
71
- query = e.target.value
72
- open = true
73
- highlightedIndex = if filtered.length > 0 then 0 else -1
74
-
75
- _openMenu: ->
76
- return if @disabled
77
- return unless @_canOpen()
78
- open = true
79
- highlightedIndex = if filtered.length > 0 then Math.max(highlightedIndex, 0) else -1
80
-
81
- _close: (restoreFocus = false, blockOpen = false) ->
82
- open = false
83
- query = ''
84
- highlightedIndex = -1
85
- @_blockOpenBriefly() if blockOpen
86
- @_input?.focus() if restoreFocus
87
-
88
- onFocusin: ->
89
- return unless @_canOpen()
90
- @_openMenu()
91
-
92
- _onInputMousedown: (e) ->
93
- return unless open and not query
94
- e.preventDefault()
95
- _ignoreInputClickOnce = true
96
- @_close(false, true)
97
-
98
- _onInputClick: ->
99
- if _ignoreInputClickOnce
100
- _ignoreInputClickOnce = false
101
- return
102
- return unless @_canOpen()
103
- @_openMenu()
104
-
105
- _onChipsClick: (e) ->
106
- return unless e.target is e.currentTarget
107
- if open and not query
108
- @_close(false, true)
109
- return
110
- return unless @_canOpen()
111
- @_input?.focus()
112
- @_openMenu() if document.activeElement is @_input
113
-
114
- onFocusout: ->
115
- setTimeout =>
116
- return if @_content?.contains(document.activeElement)
117
- @_close(false, true)
118
- , 0
119
-
120
- mounted: ->
121
- _ready = true
122
-
123
- _applyPlacement: ->
124
- ARIA.position @_content, @_list, placement: 'bottom start', offset: 2, matchWidth: true
125
-
126
- _onKeydown: (e) ->
127
- len = filtered.length
128
- switch e.key
129
- when 'ArrowDown'
130
- e.preventDefault()
131
- open = true
132
- highlightedIndex = (highlightedIndex + 1) %% len if len
133
- when 'ArrowUp'
134
- e.preventDefault()
135
- highlightedIndex = (highlightedIndex - 1) %% len if len
136
- when 'Enter'
137
- e.preventDefault()
138
- if highlightedIndex >= 0 and highlightedIndex < len
139
- @_toggleItem(filtered[highlightedIndex])
140
- when 'Escape'
141
- e.preventDefault()
142
- @_close()
143
- when 'Backspace'
144
- if not query and @value.length > 0
145
- @value = @value.slice(0, -1)
146
- @emit 'change', @value
147
- when 'Tab'
148
- @_close(false, true)
149
-
150
- ~>
151
- return unless _ready
152
- if @_list
153
- @_list.setAttribute 'popover', 'manual'
154
- @_applyPlacement()
155
- ARIA.bindPopover open, (=> @_list), ((isOpen) => open = isOpen), (=> @_input)
156
- ARIA.popupDismiss open, (=> @_list), (=> @_close(false, true)), [=> @_input, => @_content]
157
-
158
- render
159
- . ref: "_content", $open: open?!, $disabled: @disabled?!
160
-
161
- # Chip area + input
162
- . $chips: true, @click: @_onChipsClick
163
- for chip in @value
164
- span $chip: true
165
- "#{chip}"
166
- button $remove: true, aria-label: "Remove #{chip}", @mousedown: @_onRemoveMousedown, @click: (=> @_removeChip(chip))
167
- "✕"
168
- input ref: "_input", type: "text", autocomplete: "off"
169
- role: "combobox"
170
- aria-expanded: !!open
171
- aria-haspopup: "listbox"
172
- aria-controls: if open then _listId else undefined
173
- $disabled: @disabled?!
174
- disabled: @disabled
175
- placeholder: if @value.length is 0 then @placeholder else ''
176
- value: query
177
- @mousedown: @_onInputMousedown
178
- @input: @_onInput
179
- @click: @_onInputClick
180
- @keydown: @_onKeydown
181
-
182
- # Dropdown
183
- div ref: "_list"
184
- id: _listId
185
- role: "listbox"
186
- popover: "manual"
187
- hidden: not open
188
- aria-hidden: (open ? undefined : "true")
189
- $open: open?!
190
- aria-multiselectable: "true"
191
- style: "position:fixed;margin:0;inset:auto"
192
- for item, idx in filtered
193
- div role: "option", tabindex: "-1", id: "#{_listId}-#{idx}"
194
- $value: @_val(item)
195
- $selected: @_isSelected(item)?!
196
- $highlighted: (idx is highlightedIndex)?!
197
- aria-selected: !!@_isSelected(item)
198
- @click: (=> @_toggleItem(item))
199
- @mouseenter: (=> highlightedIndex = idx)
200
- @_label(item)
201
- if filtered.length is 0 and query
202
- div role: "status", aria-live: "polite", $empty: true
203
- "No results"
@@ -1,33 +0,0 @@
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:: string := ""
14
- @disabled:: boolean := false
15
- @required:: boolean := false
16
-
17
- focused := false
18
-
19
- onChange: (e) ->
20
- e.isTrusted or return
21
- @value = e.target.value
22
- @emit 'change', @value
23
-
24
- render
25
- select value: @value, disabled: @disabled, required: @required
26
- aria-disabled: @disabled?!
27
- aria-required: @required?!
28
- $disabled: @disabled?!
29
- $focused: focused?!
30
- @change: @onChange
31
- @focusin: (=> focused = true)
32
- @focusout: (=> focused = false)
33
- slot