avo 4.0.0.beta.4 → 4.0.0.beta.6
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +12 -12
- data/app/assets/builds/avo/application.css +45 -65
- data/app/assets/builds/avo/application.js +113 -113
- data/app/assets/builds/avo/application.js.map +4 -4
- data/app/assets/stylesheets/application.css +1 -0
- data/app/assets/stylesheets/css/components/button.css +5 -0
- data/app/assets/stylesheets/css/components/color_scheme_switcher.css +8 -23
- data/app/assets/stylesheets/css/components/input.css +12 -3
- data/app/assets/stylesheets/css/components/ui/card.css +2 -0
- data/app/assets/stylesheets/css/fields/code.css +3 -4
- data/app/assets/stylesheets/css/resource-controls.css +3 -3
- data/app/assets/stylesheets/css/sidebar.css +2 -6
- data/app/assets/stylesheets/css/table.css +4 -0
- data/app/assets/stylesheets/css/typography.css +3 -1
- data/app/components/avo/actions_component.html.erb +2 -1
- data/app/components/avo/actions_component.rb +1 -0
- data/app/components/avo/base_component.rb +36 -5
- data/app/components/avo/button_component.rb +1 -1
- data/app/components/avo/keyboard_shortcuts_component.rb +23 -2
- data/app/components/avo/modal_component.html.erb +1 -0
- data/app/components/avo/resource_component.rb +33 -4
- data/app/components/avo/sidebar/heading_component.html.erb +24 -0
- data/app/components/avo/sidebar/heading_component.rb +10 -0
- data/app/components/avo/sidebar/link_component.rb +1 -1
- data/app/components/avo/sidebar/section_component.html.erb +6 -26
- data/app/components/avo/sidebar_component.html.erb +16 -10
- data/app/components/avo/u_i/search_input_component.html.erb +8 -3
- data/app/components/avo/u_i/search_input_component.rb +1 -1
- data/app/components/avo/views/resource_index_component.html.erb +3 -2
- data/app/javascript/application.js +2 -1
- data/app/javascript/js/controllers/color_scheme_switcher_controller.js +1 -11
- data/app/javascript/js/controllers/dropdown_menu_controller.js +59 -3
- data/app/javascript/js/controllers/index_row_navigator_controller.js +185 -0
- data/app/javascript/js/controllers/item_select_all_controller.js +10 -0
- data/app/javascript/js/controllers/resource_search_controller.js +4 -0
- data/app/javascript/js/controllers.js +2 -0
- data/app/javascript/js/global_hotkeys.js +44 -9
- data/app/views/avo/actions/show.html.erb +2 -1
- data/app/views/avo/partials/_color_scheme_switcher.html.erb +42 -47
- data/app/views/avo/partials/_view_toggle_button.html.erb +6 -1
- data/lib/avo/concerns/row_controls_configuration.rb +1 -1
- data/lib/avo/version.rb +1 -1
- metadata +4 -1
|
@@ -2,7 +2,7 @@ import { Controller } from '@hotwired/stimulus'
|
|
|
2
2
|
import Cookies from 'js-cookie'
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
|
-
static targets = ['button', '
|
|
5
|
+
static targets = ['button', 'accentOption', 'themeLabel', 'themeOption']
|
|
6
6
|
|
|
7
7
|
connect() {
|
|
8
8
|
// Read from cookies (cookie is source of truth)
|
|
@@ -60,11 +60,6 @@ export default class extends Controller {
|
|
|
60
60
|
this.applyTheme()
|
|
61
61
|
this.updateThemeLabel()
|
|
62
62
|
this.updateActiveThemeOption()
|
|
63
|
-
|
|
64
|
-
// Close the dropdown
|
|
65
|
-
if (this.hasThemePanelTarget) {
|
|
66
|
-
this.themePanelTarget.setAttribute('hidden', true)
|
|
67
|
-
}
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
previewTheme(event) {
|
|
@@ -89,11 +84,6 @@ export default class extends Controller {
|
|
|
89
84
|
this.currentAccentValue = accent
|
|
90
85
|
this.saveAccent()
|
|
91
86
|
this.applyAccent()
|
|
92
|
-
|
|
93
|
-
// Close the dropdown
|
|
94
|
-
if (this.hasAccentPanelTarget) {
|
|
95
|
-
this.accentPanelTarget.setAttribute('hidden', true)
|
|
96
|
-
}
|
|
97
87
|
}
|
|
98
88
|
|
|
99
89
|
previewAccent(event) {
|
|
@@ -18,25 +18,81 @@ export default class extends Controller {
|
|
|
18
18
|
return this.menuTarget.hasAttribute('open')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
get focusableItems() {
|
|
22
|
+
return [...this.menuTarget.querySelectorAll('a, button')].filter(
|
|
23
|
+
(el) => !el.closest('[hidden]') && el.dataset.disabled !== 'true',
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
clickOutside(e) {
|
|
22
28
|
if (this.hasMenuTarget) {
|
|
23
29
|
const isInExemptionContainer = this.hasExemptionContainersValue && this.exemptionContainerTargets.some((container) => container.contains(e.target))
|
|
24
30
|
|
|
25
31
|
if (!isInExemptionContainer && this.isOpen) {
|
|
26
|
-
this.
|
|
32
|
+
this.close()
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
connect() {
|
|
32
38
|
useClickOutside(this)
|
|
39
|
+
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
disconnect() {
|
|
43
|
+
this.element.removeEventListener('keydown', this.boundHandleKeydown)
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
toggle() {
|
|
36
47
|
if (this.isOpen) {
|
|
37
|
-
this.
|
|
48
|
+
this.close()
|
|
38
49
|
} else {
|
|
39
|
-
this.
|
|
50
|
+
this.open()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
open() {
|
|
55
|
+
this.menuTarget.show()
|
|
56
|
+
this.element.addEventListener('keydown', this.boundHandleKeydown)
|
|
57
|
+
document.body.classList.add('dropdown-open')
|
|
58
|
+
this.dispatch('open', { bubbles: true })
|
|
59
|
+
requestAnimationFrame(() => {
|
|
60
|
+
const items = this.focusableItems
|
|
61
|
+
if (items.length === 0) return
|
|
62
|
+
|
|
63
|
+
const activeItem = items.find((el) =>
|
|
64
|
+
[...el.classList].some((cls) => cls.endsWith('--active')) || el.getAttribute('aria-selected') === 'true',
|
|
65
|
+
)
|
|
66
|
+
;(activeItem || items[0]).focus()
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
close() {
|
|
71
|
+
this.menuTarget.close()
|
|
72
|
+
this.element.removeEventListener('keydown', this.boundHandleKeydown)
|
|
73
|
+
document.body.classList.remove('dropdown-open')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
handleKeydown(event) {
|
|
77
|
+
const items = this.focusableItems
|
|
78
|
+
if (items.length === 0) return
|
|
79
|
+
|
|
80
|
+
const currentIndex = items.indexOf(document.activeElement)
|
|
81
|
+
|
|
82
|
+
switch (event.key) {
|
|
83
|
+
case 'ArrowDown':
|
|
84
|
+
event.preventDefault()
|
|
85
|
+
items[currentIndex < items.length - 1 ? currentIndex + 1 : 0].focus()
|
|
86
|
+
break
|
|
87
|
+
case 'ArrowUp':
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
items[currentIndex > 0 ? currentIndex - 1 : items.length - 1].focus()
|
|
90
|
+
break
|
|
91
|
+
case 'Escape':
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
this.close()
|
|
94
|
+
break
|
|
95
|
+
default:
|
|
40
96
|
}
|
|
41
97
|
}
|
|
42
98
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index Row Navigator Controller
|
|
3
|
+
*
|
|
4
|
+
* Enables keyboard-driven navigation and hotkeys on table rows.
|
|
5
|
+
*
|
|
6
|
+
* FEATURES:
|
|
7
|
+
* - Arrow keys (↑↓) to cycle through rows with visual focus indicator
|
|
8
|
+
* - Enter key to navigate to the focused row's detail page
|
|
9
|
+
* - Space bar to toggle row selection checkbox
|
|
10
|
+
* - Escape to clear focus (or deselect all if no row is focused)
|
|
11
|
+
* - Row hotkeys: when a row is focused, hotkeys work for that row's controls
|
|
12
|
+
*
|
|
13
|
+
* ROW HOTKEY HANDLING:
|
|
14
|
+
* The @github/hotkey library scans data-hotkey attributes on page load.
|
|
15
|
+
* To avoid the "last-registered wins" problem with multiple row controls
|
|
16
|
+
* sharing the same hotkey, we:
|
|
17
|
+
* 1. Remove data-hotkey from all row controls before the library scans
|
|
18
|
+
* 2. Store original values in data-hotkey-original
|
|
19
|
+
* 3. When a row is focused, add data-hotkey back ONLY to that row
|
|
20
|
+
* 4. When a hotkey fires, prevent other handlers via stopImmediatePropagation
|
|
21
|
+
*
|
|
22
|
+
* KEY DESIGN DECISIONS:
|
|
23
|
+
* - currentIndex = -1 when no row is focused (safe default)
|
|
24
|
+
* - Guards prevent keyboard handling in modals, dropdowns, and input fields
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { Controller } from '@hotwired/stimulus'
|
|
28
|
+
|
|
29
|
+
const TYPING_SELECTOR = 'input, textarea, select, [contenteditable]'
|
|
30
|
+
|
|
31
|
+
export default class extends Controller {
|
|
32
|
+
connect() {
|
|
33
|
+
this.currentIndex = -1
|
|
34
|
+
this.handleKeydown = this.handleKeydown.bind(this)
|
|
35
|
+
this.handleDropdownOpen = this.handleDropdownOpen.bind(this)
|
|
36
|
+
document.addEventListener('keydown', this.handleKeydown)
|
|
37
|
+
document.addEventListener('dropdown-menu:open', this.handleDropdownOpen)
|
|
38
|
+
|
|
39
|
+
// Remove data-hotkey from row controls before @github/hotkey library scans
|
|
40
|
+
// Store the original values so we can restore them for the focused row only
|
|
41
|
+
const controls = this.element.querySelectorAll('tr[data-visit-path] [data-hotkey]')
|
|
42
|
+
controls.forEach((control) => {
|
|
43
|
+
const hotkey = control.getAttribute('data-hotkey')
|
|
44
|
+
control.setAttribute('data-hotkey-original', hotkey)
|
|
45
|
+
control.removeAttribute('data-hotkey')
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
disconnect() {
|
|
50
|
+
document.removeEventListener('keydown', this.handleKeydown)
|
|
51
|
+
document.removeEventListener('dropdown-menu:open', this.handleDropdownOpen)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
handleDropdownOpen() {
|
|
55
|
+
const rows = Array.from(this.element.querySelectorAll('tr[data-visit-path]'))
|
|
56
|
+
if (rows.length) this.clearFocus(rows)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
handleKeydown(event) {
|
|
60
|
+
if (event.defaultPrevented) return
|
|
61
|
+
if (document.body.classList.contains('modal-open')) return
|
|
62
|
+
if (document.body.classList.contains('dropdown-open')) return
|
|
63
|
+
if (event.target.closest(TYPING_SELECTOR)) return
|
|
64
|
+
if (event.repeat && (event.key === 'Enter' || event.key === 'Escape' || event.key === ' ')) return
|
|
65
|
+
|
|
66
|
+
const rows = Array.from(this.element.querySelectorAll('tr[data-visit-path]'))
|
|
67
|
+
if (!rows.length) return
|
|
68
|
+
|
|
69
|
+
// Check for row hotkeys when a row is focused
|
|
70
|
+
if (this.currentIndex !== -1) {
|
|
71
|
+
if (this.handleRowHotkey(event, rows)) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Only handle navigation keys below
|
|
77
|
+
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape', ' '].includes(event.key)) return
|
|
78
|
+
|
|
79
|
+
this.currentIndex = this.normalizeCurrentIndex(rows.length)
|
|
80
|
+
|
|
81
|
+
if (event.key === 'Escape') {
|
|
82
|
+
if (this.currentIndex !== -1) {
|
|
83
|
+
event.preventDefault()
|
|
84
|
+
this.clearFocus(rows)
|
|
85
|
+
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// No keyboard-focused row — clear checkbox selection if any
|
|
90
|
+
const selectAllController = this.application.getControllerForElementAndIdentifier(
|
|
91
|
+
this.element.querySelector('[data-controller~="item-select-all"]'),
|
|
92
|
+
'item-select-all',
|
|
93
|
+
)
|
|
94
|
+
if (selectAllController) {
|
|
95
|
+
const selected = JSON.parse(selectAllController.element.dataset.selectedResources || '[]')
|
|
96
|
+
if (selected.length > 0) {
|
|
97
|
+
event.preventDefault()
|
|
98
|
+
selectAllController.deselectAll()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (event.key === 'Enter') {
|
|
106
|
+
if (this.currentIndex === -1) return
|
|
107
|
+
event.preventDefault()
|
|
108
|
+
const row = rows[this.currentIndex]
|
|
109
|
+
if (row?.dataset.visitPath) window.Turbo.visit(row.dataset.visitPath)
|
|
110
|
+
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (event.key === ' ') {
|
|
115
|
+
if (this.currentIndex === -1) return
|
|
116
|
+
event.preventDefault()
|
|
117
|
+
const checkbox = rows[this.currentIndex].querySelector('[data-item-select-all-target="itemCheckbox"]')
|
|
118
|
+
if (checkbox) checkbox.click()
|
|
119
|
+
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ArrowDown / ArrowUp
|
|
124
|
+
event.preventDefault()
|
|
125
|
+
if (event.key === 'ArrowDown') {
|
|
126
|
+
this.currentIndex = this.currentIndex < rows.length - 1 ? this.currentIndex + 1 : 0
|
|
127
|
+
} else {
|
|
128
|
+
this.currentIndex = this.currentIndex > 0 ? this.currentIndex - 1 : rows.length - 1
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
rows.forEach((r, i) => r.classList.toggle('is-keyboard-focused', i === this.currentIndex))
|
|
132
|
+
rows[this.currentIndex].scrollIntoView({ block: 'nearest' })
|
|
133
|
+
this.syncRowHotkeys(rows)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
normalizeCurrentIndex(rowsLength) {
|
|
137
|
+
if (this.currentIndex < -1) return -1
|
|
138
|
+
if (this.currentIndex >= rowsLength) return rowsLength - 1
|
|
139
|
+
|
|
140
|
+
return this.currentIndex
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
syncRowHotkeys(rows) {
|
|
144
|
+
// Add data-hotkey back ONLY for the focused row
|
|
145
|
+
rows.forEach((row, index) => {
|
|
146
|
+
const controls = row.querySelectorAll('[data-hotkey-original]')
|
|
147
|
+
controls.forEach((control) => {
|
|
148
|
+
if (index === this.currentIndex) {
|
|
149
|
+
// Restore hotkey on focused row
|
|
150
|
+
const hotkeyValue = control.getAttribute('data-hotkey-original')
|
|
151
|
+
control.setAttribute('data-hotkey', hotkeyValue)
|
|
152
|
+
} else {
|
|
153
|
+
// Remove hotkey from non-focused rows
|
|
154
|
+
control.removeAttribute('data-hotkey')
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
handleRowHotkey(event, rows) {
|
|
161
|
+
const focusedRow = rows[this.currentIndex]
|
|
162
|
+
if (!focusedRow) return false
|
|
163
|
+
|
|
164
|
+
// The hotkey is now on the focused row (added by syncRowHotkeys)
|
|
165
|
+
const control = focusedRow.querySelector(`[data-hotkey="${event.key}"]`)
|
|
166
|
+
if (!control) return false
|
|
167
|
+
|
|
168
|
+
event.preventDefault()
|
|
169
|
+
event.stopImmediatePropagation() // Prevent @github/hotkey library from firing
|
|
170
|
+
control.click()
|
|
171
|
+
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
clearFocus(rows) {
|
|
176
|
+
rows.forEach((r) => r.classList.remove('is-keyboard-focused'))
|
|
177
|
+
this.currentIndex = -1
|
|
178
|
+
// Clear all hotkeys when focus is cleared
|
|
179
|
+
rows.forEach((row) => {
|
|
180
|
+
row.querySelectorAll('[data-hotkey-original]').forEach((control) => {
|
|
181
|
+
control.removeAttribute('data-hotkey')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -45,6 +45,16 @@ export default class extends Controller {
|
|
|
45
45
|
this.selectedResourcesObserver.stop()
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
deselectAll() {
|
|
49
|
+
this.itemCheckboxTargets.forEach((checkbox) => checkbox.checked && checkbox.click())
|
|
50
|
+
this.checkboxTarget.checked = false
|
|
51
|
+
|
|
52
|
+
if (this.selectAllEnabled()) {
|
|
53
|
+
this.selectAllOverlay(false)
|
|
54
|
+
this.resetUnselected()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
toggle(event) {
|
|
49
59
|
const checked = !!event.target.checked
|
|
50
60
|
this.itemCheckboxTargets.forEach((checkbox) => checkbox.checked !== checked && checkbox.click())
|
|
@@ -21,6 +21,7 @@ import FilterController from './controllers/filter_controller'
|
|
|
21
21
|
import FormController from './controllers/form_controller'
|
|
22
22
|
import GridCoverEmptyStateController from './controllers/grid_cover_empty_state_controller'
|
|
23
23
|
import HiddenInputController from './controllers/hidden_input_controller'
|
|
24
|
+
import IndexRowNavigatorController from './controllers/index_row_navigator_controller'
|
|
24
25
|
import InputAutofocusController from './controllers/input_autofocus_controller'
|
|
25
26
|
import ItemSelectAllController from './controllers/item_select_all_controller'
|
|
26
27
|
import ItemSelectorController from './controllers/item_selector_controller'
|
|
@@ -78,6 +79,7 @@ application.register('filter', FilterController)
|
|
|
78
79
|
application.register('form', FormController)
|
|
79
80
|
application.register('grid-cover-empty-state', GridCoverEmptyStateController)
|
|
80
81
|
application.register('hidden-input', HiddenInputController)
|
|
82
|
+
application.register('index-row-navigator', IndexRowNavigatorController)
|
|
81
83
|
application.register('input-autofocus', InputAutofocusController)
|
|
82
84
|
application.register('item-select-all', ItemSelectAllController)
|
|
83
85
|
application.register('item-selector', ItemSelectorController)
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import { install } from '@github/hotkey'
|
|
11
11
|
|
|
12
|
+
const RESOURCE_SEARCH_INPUT_SELECTOR = '[data-resource-search-target="input"]'
|
|
13
|
+
const findResourceSearchInput = () => document.querySelector(RESOURCE_SEARCH_INPUT_SELECTOR)
|
|
14
|
+
|
|
12
15
|
// Use @github/hotkey for sequences and standard combos.
|
|
13
16
|
const ELEMENT_HOTKEYS = [
|
|
14
17
|
{
|
|
@@ -27,23 +30,55 @@ const DIRECT_HOTKEYS = [
|
|
|
27
30
|
match: (e) => e.key === '?' || (e.shiftKey && e.code === 'Slash'),
|
|
28
31
|
handle: () => document.dispatchEvent(new Event('persistent-modal:toggle')),
|
|
29
32
|
},
|
|
33
|
+
{
|
|
34
|
+
// "/" → focus the resource search input on index pages.
|
|
35
|
+
match: (e) => e.key === '/'
|
|
36
|
+
&& !e.shiftKey
|
|
37
|
+
&& !e.metaKey
|
|
38
|
+
&& !e.ctrlKey
|
|
39
|
+
&& !e.altKey
|
|
40
|
+
&& !!findResourceSearchInput(),
|
|
41
|
+
handle: () => {
|
|
42
|
+
const input = findResourceSearchInput()
|
|
43
|
+
if (input) input.focus()
|
|
44
|
+
},
|
|
45
|
+
},
|
|
30
46
|
]
|
|
31
47
|
|
|
32
48
|
const TYPING_SELECTOR = 'input, textarea, select, [contenteditable]'
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
// hotkey-fire is dispatched with { cancelable: true } but NOT { bubbles: true },
|
|
51
|
+
// so it never reaches a document-level listener. Must be registered on each element.
|
|
52
|
+
function hotkeyFireHandler(event) {
|
|
53
|
+
const el = event.currentTarget
|
|
54
|
+
const hotkey = el.getAttribute('data-hotkey')
|
|
55
|
+
|
|
56
|
+
// Apply feedback to ALL elements sharing this hotkey (e.g. desktop + mobile sidebar).
|
|
57
|
+
// @github/hotkey fires on the last-registered element which may be hidden.
|
|
58
|
+
const kbds = hotkey
|
|
59
|
+
? document.querySelectorAll(`[data-hotkey="${CSS.escape(hotkey)}"] kbd`)
|
|
60
|
+
: el.querySelectorAll('kbd')
|
|
41
61
|
|
|
42
|
-
|
|
62
|
+
if (!kbds.length) return
|
|
63
|
+
|
|
64
|
+
event.preventDefault()
|
|
65
|
+
kbds.forEach((kbd) => {
|
|
43
66
|
kbd.classList.add('kbd--called')
|
|
44
|
-
|
|
67
|
+
kbd.addEventListener('transitionend', () => kbd.classList.remove('kbd--called'), { once: true })
|
|
68
|
+
})
|
|
69
|
+
// Double rAF: the first fires before paint (style committed), the second
|
|
70
|
+
// fires after the browser has actually painted the kbd--called state.
|
|
71
|
+
requestAnimationFrame(() => {
|
|
72
|
+
requestAnimationFrame(() => el.click())
|
|
45
73
|
})
|
|
74
|
+
}
|
|
46
75
|
|
|
76
|
+
export function attachHotkeyFeedback(el) {
|
|
77
|
+
el.removeEventListener('hotkey-fire', hotkeyFireHandler)
|
|
78
|
+
el.addEventListener('hotkey-fire', hotkeyFireHandler)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function installGlobalHotkeys() {
|
|
47
82
|
document.addEventListener('turbo:load', () => {
|
|
48
83
|
document.querySelectorAll('kbd.kbd--called').forEach((kbd) => kbd.classList.remove('kbd--called'))
|
|
49
84
|
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
<% c.with_controls do %>
|
|
54
54
|
<%= a_button type: :button,
|
|
55
|
-
data: { action: 'click->modal#close' },
|
|
55
|
+
data: { action: 'click->modal#close', hotkey: "Escape" },
|
|
56
56
|
size: :md do %>
|
|
57
57
|
<%= @action.cancel_button_label %>
|
|
58
58
|
<% end %>
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
size: :md,
|
|
63
63
|
data: {
|
|
64
64
|
target: :submit_action,
|
|
65
|
+
hotkey: "Mod+Enter",
|
|
65
66
|
},
|
|
66
67
|
autofocus: @fields.reject { |field| field.is_a?(Avo::Fields::HiddenField) }.empty? do %>
|
|
67
68
|
<%= @action.confirm_button_label %>
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
<div class="color-scheme-switcher"
|
|
2
2
|
data-controller="color-scheme-switcher">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
data-action="mouseleave->color-scheme-switcher#revertTheme">
|
|
18
|
-
<div class="color-scheme-switcher__theme-options">
|
|
3
|
+
<%= render Avo::UI::DropdownComponent.new(
|
|
4
|
+
classes: "color-scheme-switcher__theme-popover"
|
|
5
|
+
) do |component| %>
|
|
6
|
+
<% component.with_trigger do %>
|
|
7
|
+
<button type="button"
|
|
8
|
+
data-action="<%= component.action %>"
|
|
9
|
+
class="color-scheme-switcher__button color-scheme-switcher__button--theme"
|
|
10
|
+
title="Neutral theme">
|
|
11
|
+
<span class="color-scheme-switcher__text" data-color-scheme-switcher-target="themeLabel">Brand</span>
|
|
12
|
+
</button>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% component.with_items do %>
|
|
15
|
+
<div class="color-scheme-switcher__theme-options"
|
|
16
|
+
data-action="mouseleave->color-scheme-switcher#revertTheme">
|
|
19
17
|
<% %w[brand slate stone gray zinc neutral taupe mauve mist olive].each do |theme| %>
|
|
20
18
|
<button type="button"
|
|
21
|
-
data-action="click->color-scheme-switcher#setTheme mouseenter->color-scheme-switcher#previewTheme"
|
|
19
|
+
data-action="click->color-scheme-switcher#setTheme click->dropdown-menu#close mouseenter->color-scheme-switcher#previewTheme"
|
|
22
20
|
data-theme="<%= theme %>"
|
|
23
21
|
data-color-scheme-switcher-target="themeOption"
|
|
24
22
|
class="color-scheme-switcher__theme-option">
|
|
@@ -27,32 +25,31 @@
|
|
|
27
25
|
</button>
|
|
28
26
|
<% end %>
|
|
29
27
|
</div>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<div class="color-scheme-switcher__accent-wrapper"
|
|
34
|
-
data-controller="toggle">
|
|
35
|
-
<button type="button"
|
|
36
|
-
data-color-scheme-switcher-target="button"
|
|
37
|
-
data-action="click->toggle#togglePanel"
|
|
38
|
-
class="color-scheme-switcher__button color-scheme-switcher__button--accent"
|
|
39
|
-
title="Accent color">
|
|
40
|
-
<%= svg "tabler/outline/color-swatch", class: "color-scheme-switcher__icon" %>
|
|
41
|
-
<span class="color-scheme-switcher__accent-badge">
|
|
42
|
-
<span class="color-scheme-switcher__accent-badge-preview color-scheme-switcher__accent-badge-preview--neutral"></span>
|
|
43
|
-
<% accent_colors.each do |accent| %>
|
|
44
|
-
<span class="color-scheme-switcher__accent-badge-preview color-scheme-switcher__accent-badge-preview--<%= accent %> bg-<%= accent %>-500"></span>
|
|
45
|
-
<% end %>
|
|
46
|
-
</span>
|
|
47
|
-
<span class="sr-only">Accent color</span>
|
|
48
|
-
</button>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% end %>
|
|
49
30
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
31
|
+
<%= render Avo::UI::DropdownComponent.new(
|
|
32
|
+
classes: "color-scheme-switcher__accent-popover"
|
|
33
|
+
) do |component| %>
|
|
34
|
+
<% component.with_trigger do %>
|
|
35
|
+
<button type="button"
|
|
36
|
+
data-color-scheme-switcher-target="button"
|
|
37
|
+
data-action="<%= component.action %>"
|
|
38
|
+
class="color-scheme-switcher__button color-scheme-switcher__button--accent"
|
|
39
|
+
title="Accent color">
|
|
40
|
+
<%= svg "tabler/outline/color-swatch", class: "color-scheme-switcher__icon" %>
|
|
41
|
+
<span class="color-scheme-switcher__accent-badge">
|
|
42
|
+
<span class="color-scheme-switcher__accent-badge-preview color-scheme-switcher__accent-badge-preview--neutral"></span>
|
|
43
|
+
<% accent_colors.each do |accent| %>
|
|
44
|
+
<span class="color-scheme-switcher__accent-badge-preview color-scheme-switcher__accent-badge-preview--<%= accent %> bg-<%= accent %>-500"></span>
|
|
45
|
+
<% end %>
|
|
46
|
+
</span>
|
|
47
|
+
<span class="sr-only">Accent color</span>
|
|
48
|
+
</button>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% component.with_items do %>
|
|
51
|
+
<div class="color-scheme-switcher__accent-options"
|
|
52
|
+
data-action="mouseleave->color-scheme-switcher#revertAccent">
|
|
56
53
|
<% (['neutral'] + accent_colors).each do |accent| %>
|
|
57
54
|
<%
|
|
58
55
|
preview_class = if accent == "neutral"
|
|
@@ -62,19 +59,17 @@
|
|
|
62
59
|
end
|
|
63
60
|
%>
|
|
64
61
|
<button type="button"
|
|
65
|
-
data-action="click->color-scheme-switcher#setAccent mouseenter->color-scheme-switcher#previewAccent"
|
|
62
|
+
data-action="click->color-scheme-switcher#setAccent click->dropdown-menu#close mouseenter->color-scheme-switcher#previewAccent"
|
|
66
63
|
data-accent="<%= accent %>"
|
|
67
64
|
data-color-scheme-switcher-target="accentOption"
|
|
68
|
-
data-tippy="tooltip"
|
|
69
|
-
title="<%= accent.capitalize %>"
|
|
70
65
|
class="color-scheme-switcher__accent-option">
|
|
71
66
|
<span class="<%= preview_class %>"></span>
|
|
72
67
|
<span class="sr-only"><%= accent.capitalize %></span>
|
|
73
68
|
</button>
|
|
74
69
|
<% end %>
|
|
75
70
|
</div>
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
<% end %>
|
|
72
|
+
<% end %>
|
|
78
73
|
|
|
79
74
|
<button type="button"
|
|
80
75
|
data-color-scheme-switcher-target="button"
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<% if available_view_types.count > 1 %>
|
|
2
|
+
<%
|
|
3
|
+
view_type_hotkeys = { table: "v t", grid: "v g", map: "v m" }
|
|
4
|
+
%>
|
|
2
5
|
<div class="flex">
|
|
3
6
|
<div class="button-group">
|
|
4
7
|
<% available_view_types.each do |type| %>
|
|
5
8
|
<% view_info = Avo.view_type_manager.find(type) %>
|
|
6
9
|
<% is_active_view = view_type.to_s == type.to_s %>
|
|
10
|
+
<% hotkey = view_type_hotkeys[type.to_sym] %>
|
|
7
11
|
|
|
8
12
|
<%= a_link url_for(params.permit!.merge(view_type: type)).to_s,
|
|
9
13
|
icon: is_active_view ? view_info&.[](:active_icon) : view_info&.[](:icon),
|
|
@@ -14,7 +18,8 @@
|
|
|
14
18
|
tippy: 'tooltip',
|
|
15
19
|
'turbo-frame': turbo_frame,
|
|
16
20
|
control: "view-type-toggle-#{type}",
|
|
17
|
-
is_active: is_active_view
|
|
21
|
+
is_active: is_active_view,
|
|
22
|
+
**(hotkey ? { hotkey: hotkey, show_hotkey_badge: false } : {})
|
|
18
23
|
}
|
|
19
24
|
%>
|
|
20
25
|
<% end %>
|
|
@@ -45,7 +45,7 @@ module Avo
|
|
|
45
45
|
class_names(
|
|
46
46
|
"text-end whitespace-nowrap px-3",
|
|
47
47
|
"w-px": render_row_controls_on_the_left?,
|
|
48
|
-
"*:opacity-0 group-hover:*:opacity-100": row_controls_configurations[:show_on_hover],
|
|
48
|
+
"bg-transparent before:opacity-0 group-hover:before:opacity-100 *:opacity-0 group-hover:*:opacity-100": row_controls_configurations[:show_on_hover],
|
|
49
49
|
"#{float_classes}": row_controls_configurations[:float]
|
|
50
50
|
)
|
|
51
51
|
end
|
data/lib/avo/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: avo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0.0.beta.
|
|
4
|
+
version: 4.0.0.beta.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrian Marin
|
|
@@ -645,6 +645,8 @@ files:
|
|
|
645
645
|
- app/components/avo/sidebar/base_item_component.rb
|
|
646
646
|
- app/components/avo/sidebar/group_component.html.erb
|
|
647
647
|
- app/components/avo/sidebar/group_component.rb
|
|
648
|
+
- app/components/avo/sidebar/heading_component.html.erb
|
|
649
|
+
- app/components/avo/sidebar/heading_component.rb
|
|
648
650
|
- app/components/avo/sidebar/link_component.html.erb
|
|
649
651
|
- app/components/avo/sidebar/link_component.rb
|
|
650
652
|
- app/components/avo/sidebar/section_component.html.erb
|
|
@@ -753,6 +755,7 @@ files:
|
|
|
753
755
|
- app/javascript/js/controllers/form_controller.js
|
|
754
756
|
- app/javascript/js/controllers/grid_cover_empty_state_controller.js
|
|
755
757
|
- app/javascript/js/controllers/hidden_input_controller.js
|
|
758
|
+
- app/javascript/js/controllers/index_row_navigator_controller.js
|
|
756
759
|
- app/javascript/js/controllers/input_autofocus_controller.js
|
|
757
760
|
- app/javascript/js/controllers/item_select_all_controller.js
|
|
758
761
|
- app/javascript/js/controllers/item_selector_controller.js
|