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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +12 -12
  3. data/app/assets/builds/avo/application.css +45 -65
  4. data/app/assets/builds/avo/application.js +113 -113
  5. data/app/assets/builds/avo/application.js.map +4 -4
  6. data/app/assets/stylesheets/application.css +1 -0
  7. data/app/assets/stylesheets/css/components/button.css +5 -0
  8. data/app/assets/stylesheets/css/components/color_scheme_switcher.css +8 -23
  9. data/app/assets/stylesheets/css/components/input.css +12 -3
  10. data/app/assets/stylesheets/css/components/ui/card.css +2 -0
  11. data/app/assets/stylesheets/css/fields/code.css +3 -4
  12. data/app/assets/stylesheets/css/resource-controls.css +3 -3
  13. data/app/assets/stylesheets/css/sidebar.css +2 -6
  14. data/app/assets/stylesheets/css/table.css +4 -0
  15. data/app/assets/stylesheets/css/typography.css +3 -1
  16. data/app/components/avo/actions_component.html.erb +2 -1
  17. data/app/components/avo/actions_component.rb +1 -0
  18. data/app/components/avo/base_component.rb +36 -5
  19. data/app/components/avo/button_component.rb +1 -1
  20. data/app/components/avo/keyboard_shortcuts_component.rb +23 -2
  21. data/app/components/avo/modal_component.html.erb +1 -0
  22. data/app/components/avo/resource_component.rb +33 -4
  23. data/app/components/avo/sidebar/heading_component.html.erb +24 -0
  24. data/app/components/avo/sidebar/heading_component.rb +10 -0
  25. data/app/components/avo/sidebar/link_component.rb +1 -1
  26. data/app/components/avo/sidebar/section_component.html.erb +6 -26
  27. data/app/components/avo/sidebar_component.html.erb +16 -10
  28. data/app/components/avo/u_i/search_input_component.html.erb +8 -3
  29. data/app/components/avo/u_i/search_input_component.rb +1 -1
  30. data/app/components/avo/views/resource_index_component.html.erb +3 -2
  31. data/app/javascript/application.js +2 -1
  32. data/app/javascript/js/controllers/color_scheme_switcher_controller.js +1 -11
  33. data/app/javascript/js/controllers/dropdown_menu_controller.js +59 -3
  34. data/app/javascript/js/controllers/index_row_navigator_controller.js +185 -0
  35. data/app/javascript/js/controllers/item_select_all_controller.js +10 -0
  36. data/app/javascript/js/controllers/resource_search_controller.js +4 -0
  37. data/app/javascript/js/controllers.js +2 -0
  38. data/app/javascript/js/global_hotkeys.js +44 -9
  39. data/app/views/avo/actions/show.html.erb +2 -1
  40. data/app/views/avo/partials/_color_scheme_switcher.html.erb +42 -47
  41. data/app/views/avo/partials/_view_toggle_button.html.erb +6 -1
  42. data/lib/avo/concerns/row_controls_configuration.rb +1 -1
  43. data/lib/avo/version.rb +1 -1
  44. metadata +4 -1
@@ -1,3 +1,4 @@
1
+ /* @layer base, components, utilities; */
1
2
  @import "tailwindcss";
2
3
 
3
4
  /* TODO: Figure out a way to add those dynamically */
@@ -141,6 +141,11 @@
141
141
  --btn-color-accent-content: var(--color-rose-600);
142
142
  --btn-color-accent-foreground: var(--color-white);
143
143
  }
144
+
145
+ /* The kbd element is taller than the button text, so we need to offset it */
146
+ &>span >kbd {
147
+ @apply -my-1;
148
+ }
144
149
  }
145
150
 
146
151
  .dark {
@@ -43,24 +43,20 @@
43
43
  }
44
44
 
45
45
  /* Theme (neutrals) dropdown */
46
- .color-scheme-switcher__theme-wrapper {
47
- @apply relative;
48
- }
49
-
50
46
  .color-scheme-switcher__button--theme {
51
47
  @apply gap-1;
52
48
  }
53
49
 
54
- .color-scheme-switcher__theme-panel {
55
- @apply absolute top-full mt-2 start-0 z-40;
56
- @apply rounded-lg bg-white dark:bg-neutral-800;
57
- @apply border border-neutral-200 dark:border-neutral-700;
58
- @apply shadow-lg p-2 min-w-[160px];
59
- animation: css-animate-slide-down 0.15s ease-out;
50
+ .color-scheme-switcher__theme-popover {
51
+ @apply start-0 end-auto min-w-[160px];
52
+ }
53
+
54
+ .color-scheme-switcher__accent-popover {
55
+ @apply min-w-[160px];
60
56
  }
61
57
 
62
58
  .color-scheme-switcher__theme-options {
63
- @apply flex flex-col gap-0.5;
59
+ @apply flex flex-col gap-0.5 self-stretch;
64
60
  }
65
61
 
66
62
  .color-scheme-switcher__theme-option {
@@ -126,20 +122,9 @@
126
122
  }
127
123
 
128
124
  /* Accent color dropdown */
129
- .color-scheme-switcher__accent-wrapper {
130
- @apply relative;
131
- }
132
-
133
- .color-scheme-switcher__accent-panel {
134
- @apply absolute top-full mt-2 end-0 z-40;
135
- @apply rounded-lg bg-white dark:bg-neutral-800;
136
- @apply border border-neutral-200 dark:border-neutral-700;
137
- @apply shadow-lg p-2 min-w-[160px];
138
- animation: css-animate-slide-down 0.15s ease-out;
139
- }
140
125
 
141
126
  .color-scheme-switcher__accent-options {
142
- @apply grid grid-cols-3 gap-2;
127
+ @apply grid grid-cols-3 gap-2 self-stretch;
143
128
  }
144
129
 
145
130
  .color-scheme-switcher__accent-option {
@@ -266,8 +266,17 @@
266
266
  );
267
267
  }
268
268
 
269
- /* Default (Mac): + K — narrower */
269
+ /* Single-key shortcut: "/" */
270
270
  .search-input__input.search-input__input--with-shortcut {
271
+ padding-inline-end: calc(
272
+ var(--input-icon-offset) +
273
+ var(--input-icon-size) +
274
+ var(--input-icon-gap) * 2
275
+ );
276
+ }
277
+
278
+ /* Two-key shortcut: ⌘K (Mac) */
279
+ .search-input__input.search-input__input--with-two-key-shortcut {
271
280
  padding-inline-end: calc(
272
281
  var(--input-icon-offset) +
273
282
  var(--input-icon-size) * 2 +
@@ -275,8 +284,8 @@
275
284
  );
276
285
  }
277
286
 
278
- /* PC: CTRL + K — wider */
279
- body.os-pc .search-input__input.search-input__input--with-shortcut {
287
+ /* Two-key shortcut: Ctrl+K (PC — wider because "CTRL" is wider than "⌘") */
288
+ body.os-pc .search-input__input.search-input__input--with-two-key-shortcut {
280
289
  padding-inline-end: calc(
281
290
  var(--input-icon-offset) +
282
291
  var(--input-icon-size) * 3 +
@@ -29,6 +29,8 @@
29
29
  @apply flex flex-col items-start self-stretch w-full;
30
30
  /* offset 1px from the wrapper to match the design */
31
31
  padding: calc(var(--spacing) - 1px);
32
+ /* prevent scrollIntoView from shifting the table horizontally by the padding amount */
33
+ scroll-padding-inline: calc(var(--spacing) );
32
34
  }
33
35
 
34
36
  /* for pagination we need the padding to be 4px */
@@ -38,10 +38,9 @@
38
38
  @apply border border-tertiary rounded-lg;
39
39
  }
40
40
 
41
- /* Overlay CodeMirror fullscreen and side preview */
42
- .CodeMirror-fullscreen,
43
- .editor-preview-active-side {
44
- z-index: 50;
41
+ /* Lift the main stacking context above the navbar (z-index: 100) when CodeMirror is fullscreen */
42
+ .main-content-area:has(.CodeMirror-fullscreen) {
43
+ z-index: 101;
45
44
  }
46
45
 
47
46
  /* EasyMDE shell */
@@ -1,8 +1,8 @@
1
- @layer base {
1
+ @layer components {
2
2
  .resource-controls {
3
3
  .resource-control,
4
- button,
5
- a {
4
+ button:not(.button),
5
+ a:not(.button) {
6
6
  @apply text-content-secondary;
7
7
 
8
8
  &:hover {
@@ -46,10 +46,6 @@
46
46
  @apply flex flex-col px-3 w-full;
47
47
  }
48
48
 
49
- .sidebar__nav-list {
50
- @apply w-full space-y-1;
51
- }
52
-
53
49
  .sidebar__footer {
54
50
  @apply shrink-0;
55
51
  }
@@ -60,11 +56,11 @@
60
56
  /* ================================================ */
61
57
 
62
58
  .sidebar-section {
63
- @apply pt-4;
59
+ @apply not-first:mt-4;
64
60
  }
65
61
 
66
62
  .sidebar-section__header {
67
- @apply flex items-center w-full text-start p-2 pb-1 gap-2 rounded-lg text-content-secondary text-base font-medium leading-6 cursor-pointer border-none bg-transparent;
63
+ @apply flex items-center w-full text-start p-2 pb-1 gap-2 rounded-lg text-content-secondary text-base font-medium leading-6 border-none bg-transparent;
68
64
  }
69
65
 
70
66
  .sidebar-section__header-inner {
@@ -58,3 +58,7 @@
58
58
  .selected-row {
59
59
  background: var(--color-table-row-hover);
60
60
  }
61
+
62
+ .table-row.is-keyboard-focused {
63
+ background: color-mix(in oklab, var(--color-background), var(--color-accent) 10%);
64
+ }
@@ -25,10 +25,12 @@ kbd {
25
25
  border: 1px solid currentColor;
26
26
  background-color: color-mix(in srgb, currentColor 8%, transparent); */
27
27
  box-shadow: var(--box-shadow-search-input-shortcut);
28
+ transition: transform 0.08s ease-out, box-shadow 0.08s ease-out;
28
29
  }
29
30
 
30
31
  kbd.kbd--called {
31
- opacity: 0.4;
32
+ transform: scale(0.75) translateY(2px);
33
+ box-shadow: none;
32
34
  }
33
35
 
34
36
  @layer base {
@@ -23,7 +23,8 @@
23
23
  action: component.action,
24
24
  actions_dropdown_button: @resource.model_key,
25
25
  tippy: "tooltip",
26
- tippy_content: @title
26
+ tippy_content: @title,
27
+ **(@hotkey.present? ? (@as_row_control ? {hotkey_original: @hotkey} : {hotkey: @hotkey}) : {})
27
28
  } do %>
28
29
  <%= @label %>
29
30
  <% end %>
@@ -28,6 +28,7 @@ class Avo::ActionsComponent < Avo::BaseComponent
28
28
  prop :resource
29
29
  prop :view
30
30
  prop :host_component
31
+ prop :hotkey
31
32
 
32
33
  delegate_missing_to :@host_component
33
34
 
@@ -16,16 +16,47 @@ class Avo::BaseComponent < ViewComponent::Base
16
16
  def component_name = self.class.name.to_s.underscore
17
17
 
18
18
  # Renders a <kbd> badge for a hotkey string.
19
- # Skips modifier combos like "Meta+Enter" only renders simple keys.
19
+ # Supports modifier tokens rendered in a friendly OS-aware way.
20
20
  def hotkey_badge(hotkey, **html_options)
21
- first_key = hotkey.to_s.split.first
22
- return if first_key.blank? || first_key.include?("+")
23
-
24
- tag.kbd(first_key.upcase, **html_options)
21
+ # `@github/hotkey` uses:
22
+ # - `+` for modifier combos (e.g. "Mod+Enter")
23
+ # - spaces for sequences/alternatives (e.g. "g n" or "Meta+Enter Control+Enter")
24
+ #
25
+ # Render key parts for the badge, mapping supported modifiers to OS-aware glyphs.
26
+ keys = hotkey.to_s.split(/[+\s]+/).reject(&:blank?)
27
+
28
+ first_key = keys.first
29
+ return if first_key.blank?
30
+
31
+ content_tag(:span, **html_options) do
32
+ # Render multiple keys (e.g. "g n") inside a wrapper so any provided
33
+ # classes (like `ms-auto`) are applied once.
34
+ safe_join(keys.map { |key| render_hotkey_badge_key(key) }, " ")
35
+ end
25
36
  end
26
37
 
27
38
  private
28
39
 
40
+ def render_hotkey_badge_key(key)
41
+ token = key.to_s.strip
42
+
43
+ case token.downcase
44
+ when "mod"
45
+ tag.kbd do
46
+ helpers.safe_join([
47
+ tag.abbr("⌘", title: "Command", class: "no-underline os-pc:hidden"),
48
+ tag.abbr("CTRL", title: "CTRL", class: "no-underline os-mac:hidden")
49
+ ])
50
+ end
51
+ when "enter", "return"
52
+ tag.kbd("↵")
53
+ when "escape"
54
+ tag.kbd("Esc")
55
+ else
56
+ tag.kbd(token.upcase)
57
+ end
58
+ end
59
+
29
60
  # Use the @parent_resource to fetch the field using the @reflection name.
30
61
  def field
31
62
  find_association_field(resource: @parent_resource, association: params[:related_name] || @reflection.name)
@@ -79,6 +79,6 @@ class Avo::ButtonComponent < Avo::BaseComponent
79
79
  def render_content
80
80
  concat helpers.svg(@icon, class: class_names("button__icon", @icon_class)) if @icon.present?
81
81
  concat content if content.present?
82
- concat hotkey_badge(@args.dig(:data, :hotkey)) if @args.dig(:data, :hotkey)
82
+ concat hotkey_badge(@args.dig(:data, :hotkey)) if @args.dig(:data, :hotkey) && @args.dig(:data, :show_hotkey_badge) != false
83
83
  end
84
84
  end
@@ -29,13 +29,34 @@ class Avo::KeyboardShortcutsComponent < Avo::BaseComponent
29
29
  "Show view",
30
30
  [
31
31
  shortcut(action: "Delete record", keys: ["D"]),
32
- shortcut(action: "Edit record", keys: ["E"])
32
+ shortcut(action: "Edit record", keys: ["E"]),
33
+ shortcut(action: "Open actions", keys: ["A"])
34
+ ]
35
+ ),
36
+ build_section(
37
+ "Action",
38
+ [
39
+ shortcut(action: "Run action", keys: {mac: ["Cmd", "↵"], other: ["Ctrl", "↵"]}),
40
+ shortcut(action: "Cancel action", keys: ["Esc"])
33
41
  ]
34
42
  ),
35
43
  build_section(
36
44
  "Index view",
37
45
  [
38
- shortcut(action: "Create new record", keys: ["C"])
46
+ shortcut(action: "Focus search", keys: ["/"]),
47
+ shortcut(action: "Create new record", keys: ["C"]),
48
+ shortcut(action: "Open actions", keys: ["A"]),
49
+ shortcut(
50
+ action: "Navigate rows / actions",
51
+ any_of: [["↑"], ["↓"]],
52
+ keys_aria_label: "Up arrow or down arrow"
53
+ ),
54
+ shortcut(action: "Open record", keys: ["↵"]),
55
+ shortcut(action: "Select / deselect row", keys: ["Space"]),
56
+ shortcut(action: "Deselect rows", keys: ["Esc"]),
57
+ shortcut(action: "Table view", keys: ["V", "T"]),
58
+ shortcut(action: "Grid view", keys: ["V", "G"]),
59
+ shortcut(action: "Map view", keys: ["V", "M"])
39
60
  ]
40
61
  )
41
62
  ]
@@ -51,6 +51,7 @@
51
51
 
52
52
  <% if @show_close_button %>
53
53
  <button
54
+ type="button"
54
55
  class="modal__close-button"
55
56
  data-action="click-><%= ctrl %>#closeModal"
56
57
  aria-label="<%= t('avo.close') %>"
@@ -126,6 +126,12 @@ class Avo::ResourceComponent < Avo::BaseComponent
126
126
  def render_actions_list(actions_list)
127
127
  return unless can_see_the_actions_button?
128
128
 
129
+ # Actions button hotkey "a" only on index pages (non-nested).
130
+ # Pass as_row_control so the template can use hotkey_original for row controls,
131
+ # allowing the index-row-navigator controller to manage hotkey visibility.
132
+ as_row_control = @item.present?
133
+ hotkey = "a" if instance_of?(Avo::Views::ResourceIndexComponent) && @reflection.nil?
134
+
129
135
  render Avo::ActionsComponent.new(
130
136
  actions: @actions,
131
137
  resource: @resource,
@@ -139,7 +145,8 @@ class Avo::ResourceComponent < Avo::BaseComponent
139
145
  icon: actions_list.icon,
140
146
  icon_class: actions_list.icon_class,
141
147
  title: actions_list.title,
142
- as_row_control: instance_of?(Avo::Index::ResourceControlsComponent)
148
+ as_row_control:,
149
+ hotkey: hotkey
143
150
  )
144
151
  end
145
152
 
@@ -151,6 +158,13 @@ class Avo::ResourceComponent < Avo::BaseComponent
151
158
  policy_method = is_a_related_resource? ? :can_delete? : :can_see_the_destroy_button?
152
159
  return unless send policy_method
153
160
 
161
+ # Row hotkeys: detect if we're rendering in a row control and use data-hotkey-original
162
+ # so the index-row-navigator controller can manage the hotkey visibility.
163
+ # Same as edit button: prevents @github/hotkey from registering all row buttons at once.
164
+ is_row_control = row_controls_context?
165
+ hotkey_attr = is_row_control ? :hotkey_original : :hotkey
166
+ data_attrs = {hotkey_attr => "d"}
167
+
154
168
  a_link destroy_path,
155
169
  style: :text,
156
170
  color: :red,
@@ -158,7 +172,7 @@ class Avo::ResourceComponent < Avo::BaseComponent
158
172
  title: control.title,
159
173
  aria_label: control.title,
160
174
  data: {
161
- hotkey: "d",
175
+ **data_attrs,
162
176
  turbo_confirm: t("avo.are_you_sure", item: @resource.record.model_name.name.downcase),
163
177
  turbo_method: :delete,
164
178
  target: "control:destroy",
@@ -193,12 +207,21 @@ class Avo::ResourceComponent < Avo::BaseComponent
193
207
  def render_edit_button(control)
194
208
  return unless can_see_the_edit_button?
195
209
 
210
+ # Row hotkeys are handled by index-row-navigator controller:
211
+ # - Use data-hotkey-original for index row controls (controller moves it to data-hotkey when row is focused)
212
+ # - Use data-hotkey directly for show page buttons (always available)
213
+ # This prevents the @github/hotkey library from registering all row buttons at once,
214
+ # which would cause the "last-registered wins" problem.
215
+ is_row_control = row_controls_context?
216
+ hotkey_attr = is_row_control ? :hotkey_original : :hotkey
217
+ data_attrs = {hotkey_attr => "e"}
218
+
196
219
  a_link edit_path,
197
220
  color: :accent,
198
221
  style: :primary,
199
222
  title: control.title,
200
223
  data: {
201
- hotkey: "e",
224
+ **data_attrs,
202
225
  tippy: control.title ? :tooltip : nil
203
226
  }.compact,
204
227
  icon: "tabler/outline/edit" do
@@ -223,12 +246,14 @@ class Avo::ResourceComponent < Avo::BaseComponent
223
246
  def render_create_button(control)
224
247
  return unless can_see_the_create_button?
225
248
 
249
+ hotkey = "c" if instance_of?(Avo::Views::ResourceIndexComponent) && @reflection.nil?
250
+
226
251
  a_link create_path,
227
252
  color: :accent,
228
253
  style: :primary,
229
254
  icon: "tabler/outline/plus",
230
255
  data: {
231
- hotkey: "c",
256
+ hotkey:,
232
257
  target: :create
233
258
  } do
234
259
  control.label
@@ -249,6 +274,10 @@ class Avo::ResourceComponent < Avo::BaseComponent
249
274
  end
250
275
  end
251
276
 
277
+ def row_controls_context?
278
+ is_a?(Avo::Index::ResourceControlsComponent)
279
+ end
280
+
252
281
  def render_link_to(link)
253
282
  a_link link.path,
254
283
  color: link.color,
@@ -0,0 +1,24 @@
1
+ <% if @collapsable %>
2
+ <button
3
+ type="button"
4
+ class="<%= class_names("sidebar-section__header justify-between", "cursor-pointer": @collapsable) %>"
5
+ data-action="click->menu#triggerCollapse"
6
+ data-menu-target="trigger"
7
+ aria-expanded="<%= !@collapsed %>"
8
+ >
9
+ <div class="sidebar-section__header-inner">
10
+ <%= helpers.svg @icon, class: "sidebar-section__icon sidebar-icon" if @icon.present? %>
11
+ <span><%= @title %></span>
12
+ </div>
13
+ <span class="sidebar-section__icon sidebar-icon <%= 'sidebar-icon--collapsed' if @collapsed %>"
14
+ data-menu-target="svg"
15
+ >
16
+ <%= helpers.svg 'tabler/outline/chevron-down' %>
17
+ </span>
18
+ </button>
19
+ <% else %>
20
+ <div class="sidebar-section__header">
21
+ <%= helpers.svg @icon, class: "sidebar-section__icon sidebar-icon" if @icon.present? %>
22
+ <span><%= @title %></span>
23
+ </div>
24
+ <% end %>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::Sidebar::HeadingComponent < Avo::BaseComponent
4
+ prop :title
5
+ prop :icon
6
+ prop :collapsable, default: false
7
+ prop :collapsed, default: false
8
+
9
+ def render? = @title.present?
10
+ end
@@ -82,6 +82,6 @@ class Avo::Sidebar::LinkComponent < Avo::BaseComponent
82
82
  def build_link_data(data, hotkey)
83
83
  return data if hotkey.blank?
84
84
 
85
- data.merge(hotkey: hotkey.to_s.first)
85
+ data.merge(hotkey: hotkey)
86
86
  end
87
87
  end
@@ -1,30 +1,10 @@
1
1
  <%= tag.div class: "sidebar-section", data: data do %>
2
- <% if item.name.present? %>
3
- <% if collapsable %>
4
- <button
5
- type="button"
6
- class="sidebar-section__header justify-between"
7
- data-action="click->menu#triggerCollapse"
8
- data-menu-target="trigger"
9
- aria-expanded="<%= !collapsed %>"
10
- >
11
- <div class="sidebar-section__header-inner">
12
- <%= helpers.svg icon, class: "sidebar-section__icon sidebar-icon" if icon.present? %>
13
- <span><%= item.name %></span>
14
- </div>
15
- <span class="sidebar-section__icon sidebar-icon <%= 'sidebar-icon--collapsed' if collapsed %>"
16
- data-menu-target="svg"
17
- >
18
- <%= helpers.svg 'tabler/outline/chevron-down' %>
19
- </span>
20
- </button>
21
- <% else %>
22
- <div class="sidebar-section__header">
23
- <%= helpers.svg icon, class: "sidebar-section__icon sidebar-icon" if icon.present? %>
24
- <span><%= item.name %></span>
25
- </div>
26
- <% end %>
27
- <% end %>
2
+ <%= render Avo::Sidebar::HeadingComponent.new(
3
+ title: item.name,
4
+ icon: icon,
5
+ collapsable: collapsable,
6
+ collapsed: collapsed
7
+ ) %>
28
8
 
29
9
  <%= content_tag :div,
30
10
  hidden: collapsed,
@@ -17,23 +17,27 @@
17
17
  <%= render custom_sidebar_component %>
18
18
  <% else %>
19
19
  <% if dashboards.present? %>
20
- <div>
21
- <div class="sidebar__nav-list">
22
- <% dashboards.sort_by { |r| r.navigation_label }.each do |dashboard| %>
23
- <%= render Avo::Sidebar::LinkComponent.new label: dashboard.navigation_label, path: helpers.avo_dashboards.dashboard_path(dashboard), hotkey: dashboard.try(:hotkey).presence %>
24
- <% end %>
25
- </div>
20
+ <div class="sidebar-section">
21
+ <%= render Avo::Sidebar::HeadingComponent.new title: t('avo.dashboards'), icon: "tabler/outline/layout-dashboard" %>
22
+
23
+ <% dashboards.sort_by { |r| r.navigation_label }.each do |dashboard| %>
24
+ <%= render Avo::Sidebar::LinkComponent.new label: dashboard.navigation_label, path: helpers.avo_dashboards.dashboard_path(dashboard), hotkey: dashboard.try(:hotkey).presence %>
25
+ <% end %>
26
26
  </div>
27
27
  <% end %>
28
28
 
29
- <div class="sidebar__nav-list">
29
+ <div class="sidebar-section">
30
+ <%= render Avo::Sidebar::HeadingComponent.new title: t('avo.resources'), icon: "tabler/outline/topology-star-3" %>
31
+
30
32
  <% resources.sort_by { |r| r.navigation_label }.each do |resource| %>
31
33
  <%= render Avo::Sidebar::LinkComponent.new label: resource.navigation_label, path: helpers.resources_path(resource: resource), icon: resource.icon, hotkey: resource.try(:hotkey).presence %>
32
34
  <% end %>
33
35
  </div>
34
36
 
35
37
  <% if tools.present? %>
36
- <div class="sidebar__nav-list">
38
+ <div class="sidebar-section">
39
+ <%= render Avo::Sidebar::HeadingComponent.new title: t('avo.tools'), icon: "tabler/outline/tool" %>
40
+
37
41
  <% tools.each do |partial| %>
38
42
  <%= render partial: "/avo/sidebar/items/#{partial}" %>
39
43
  <% end %>
@@ -41,7 +45,9 @@
41
45
  <% end %>
42
46
 
43
47
  <% if pages.present? %>
44
- <div class="sidebar__nav-list">
48
+ <div class="sidebar-section">
49
+ <%= render Avo::Sidebar::HeadingComponent.new title: t('avo.pages'), icon: "tabler/outline/forms" %>
50
+
45
51
  <% pages.each do |page| %>
46
52
  <%= render Avo::Sidebar::LinkComponent.new label: page.get_navigation_label, path: page.path %>
47
53
  <% end %>
@@ -49,7 +55,7 @@
49
55
  <% end %>
50
56
 
51
57
  <% if Avo::MediaLibrary.configuration.visible? %>
52
- <%= render Avo::Sidebar::LinkComponent.new label: 'Media Library', path: helpers.avo.media_library_index_path, active: :exclusive %>
58
+ <%= render Avo::Sidebar::LinkComponent.new label: 'Media Library', path: helpers.avo.media_library_index_path, active: :exclusive, icon: "tabler/outline/photo-star" %>
53
59
  <% end %>
54
60
  <% end %>
55
61
 
@@ -3,9 +3,10 @@
3
3
  id: @id,
4
4
  placeholder: @placeholder,
5
5
  class: class_names(
6
+ @classes,
6
7
  "search-input__input",
7
- {"search-input__input--with-shortcut": @with_shortcut},
8
- @classes
8
+ "search-input__input--with-shortcut": @shortcut == :slash,
9
+ "search-input__input--with-two-key-shortcut": @shortcut == :cmd_k
9
10
  ),
10
11
  disabled: @disabled,
11
12
  autocomplete: "off",
@@ -16,7 +17,11 @@
16
17
  <%= helpers.svg "tabler/outline/search" %>
17
18
  </span>
18
19
 
19
- <% if @with_shortcut %>
20
+ <% if @shortcut == :slash %>
21
+ <span class="search-input__suffix" aria-hidden="true">
22
+ <kbd>/</kbd>
23
+ </span>
24
+ <% elsif @shortcut == :cmd_k %>
20
25
  <span class="search-input__suffix" aria-hidden="true">
21
26
  <kbd>
22
27
  <abbr title="Command" class="no-underline os-pc:hidden">⌘</abbr>
@@ -6,7 +6,7 @@ class Avo::UI::SearchInputComponent < Avo::BaseComponent
6
6
  prop :value
7
7
  prop :placeholder
8
8
  prop :disabled, default: false
9
- prop :with_shortcut, default: false
9
+ prop :shortcut, default: nil # :slash or :cmd_k
10
10
  prop :classes
11
11
  prop :data, default: -> { {} }
12
12
  end
@@ -5,7 +5,7 @@
5
5
  model_name: @resource.model_name.to_s,
6
6
  resource_name: @resource.class.to_s,
7
7
  **@resource.stimulus_data_attributes
8
- } do %>
8
+ }.tap { |d| d[:controller] = "#{d[:controller]} index-row-navigator" if @reflection.blank? } do %>
9
9
 
10
10
  <%= render_cards_component %>
11
11
 
@@ -86,8 +86,9 @@
86
86
  value: params[:q],
87
87
  id: nil,
88
88
  placeholder: t('avo.search.placeholder'),
89
+ shortcut: :slash,
89
90
  data: {
90
- action: "input->resource-search#search",
91
+ action: "input->resource-search#search keydown->resource-search#blurOnEscape",
91
92
  "resource-search-target": "input"
92
93
  }) %>
93
94
  <% end %>
@@ -10,7 +10,7 @@ import { install } from '@github/hotkey'
10
10
  import tippy from 'tippy.js'
11
11
 
12
12
  import { LocalStorageService } from './js/local-storage-service'
13
- import { installGlobalHotkeys } from './js/global_hotkeys'
13
+ import { attachHotkeyFeedback, installGlobalHotkeys } from './js/global_hotkeys'
14
14
 
15
15
  import './js/active-storage'
16
16
  import './js/controllers'
@@ -20,6 +20,7 @@ import './js/custom-stream-actions'
20
20
  function installHotkeys(root = document) {
21
21
  root.querySelectorAll('[data-hotkey]').forEach((el) => {
22
22
  install(el)
23
+ attachHotkeyFeedback(el)
23
24
  })
24
25
  }
25
26