avo 4.0.0.beta.2 → 4.0.0.beta.4

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/assets/builds/avo/application.css +355 -51
  4. data/app/assets/builds/avo/application.js +166 -166
  5. data/app/assets/builds/avo/application.js.map +4 -4
  6. data/app/assets/stylesheets/application.css +2 -0
  7. data/app/assets/stylesheets/css/components/hotkey.css +50 -0
  8. data/app/assets/stylesheets/css/components/input.css +0 -6
  9. data/app/assets/stylesheets/css/components/ui/state.css +129 -0
  10. data/app/assets/stylesheets/css/layout.css +3 -4
  11. data/app/assets/stylesheets/css/pagination.css +12 -6
  12. data/app/assets/stylesheets/css/typography.css +18 -1
  13. data/app/assets/svgs/avo/circle-minus.svg +3 -0
  14. data/app/components/avo/alert_component.rb +4 -4
  15. data/app/components/avo/backtrace_alert_component.html.erb +1 -1
  16. data/app/components/avo/base_component.rb +9 -0
  17. data/app/components/avo/button_component.rb +2 -1
  18. data/app/components/avo/debug/status_component.html.erb +2 -2
  19. data/app/components/avo/empty_state_component.html.erb +15 -4
  20. data/app/components/avo/empty_state_component.rb +9 -0
  21. data/app/components/avo/fields/common/files/view_type/grid_item_component.html.erb +1 -1
  22. data/app/components/avo/fields/common/key_value_component.html.erb +1 -1
  23. data/app/components/avo/fields/common/stars_component.html.erb +1 -1
  24. data/app/components/avo/fields/common/status_viewer_component.html.erb +3 -3
  25. data/app/components/avo/fields/preview_field/index_component.rb +1 -1
  26. data/app/components/avo/fields/stars_field/edit_component.html.erb +1 -1
  27. data/app/components/avo/filters_component.html.erb +1 -1
  28. data/app/components/avo/items/switcher_component.html.erb +1 -1
  29. data/app/components/avo/keyboard_shortcuts_component.html.erb +29 -0
  30. data/app/components/avo/keyboard_shortcuts_component.rb +127 -0
  31. data/app/components/avo/media_library/item_details_component.html.erb +2 -2
  32. data/app/components/avo/media_library/list_component.html.erb +1 -1
  33. data/app/components/avo/media_library/list_item_component.html.erb +2 -2
  34. data/app/components/avo/modal_component.html.erb +39 -15
  35. data/app/components/avo/modal_component.rb +10 -0
  36. data/app/components/avo/paginator_component.html.erb +23 -17
  37. data/app/components/avo/paginator_component.rb +18 -0
  38. data/app/components/avo/resource_component.rb +14 -7
  39. data/app/components/avo/sidebar/group_component.html.erb +1 -1
  40. data/app/components/avo/sidebar/link_component.html.erb +13 -5
  41. data/app/components/avo/sidebar/link_component.rb +17 -0
  42. data/app/components/avo/sidebar/section_component.html.erb +1 -1
  43. data/app/components/avo/sidebar_component.html.erb +2 -2
  44. data/app/components/avo/sidebar_profile_component.html.erb +1 -1
  45. data/app/components/avo/u_i/search_input_component.html.erb +2 -2
  46. data/app/components/avo/views/resource_index_component.rb +1 -1
  47. data/app/javascript/application.js +12 -28
  48. data/app/javascript/js/controllers/base_modal_controller.js +65 -0
  49. data/app/javascript/js/controllers/confirm_dialog_controller.js +18 -0
  50. data/app/javascript/js/controllers/modal_controller.js +18 -26
  51. data/app/javascript/js/controllers/persistent_modal_controller.js +50 -0
  52. data/app/javascript/js/controllers/search_controller.js +0 -4
  53. data/app/javascript/js/controllers/sidebar_controller.js +22 -0
  54. data/app/javascript/js/controllers.js +4 -0
  55. data/app/javascript/js/global_hotkeys.js +77 -0
  56. data/app/javascript/js/helpers/toggle_hidden.js +7 -0
  57. data/app/views/avo/actions/show.html.erb +1 -1
  58. data/app/views/avo/base/_date_time_filter.html.erb +1 -1
  59. data/app/views/avo/base/preview.html.erb +1 -1
  60. data/app/views/avo/debug/_valid_indicator.html.erb +2 -2
  61. data/app/views/avo/home/failed_to_load.html.erb +40 -13
  62. data/app/views/avo/media_library/_form.html.erb +1 -1
  63. data/app/views/avo/partials/_color_scheme_switcher.html.erb +4 -4
  64. data/app/views/avo/partials/_confirm_dialog.html.erb +3 -3
  65. data/app/views/avo/partials/_custom_tools_alert.html.erb +3 -3
  66. data/app/views/avo/partials/_sortable_component.html.erb +3 -3
  67. data/app/views/avo/partials/_table_header.html.erb +1 -1
  68. data/app/views/avo/private/_links_and_buttons.html.erb +2 -2
  69. data/app/views/avo/private/design.html.erb +4 -4
  70. data/app/views/avo/sidebar/_license_warning.html.erb +2 -2
  71. data/app/views/layouts/avo/application.html.erb +1 -0
  72. data/lib/avo/resources/base.rb +1 -0
  73. data/lib/avo/version.rb +1 -1
  74. data/lib/generators/avo/resource_generator.rb +3 -3
  75. data/lib/generators/avo/templates/initializer/avo.tt +4 -4
  76. data/lib/generators/avo/templates/resource_tools/partial.tt +1 -1
  77. metadata +11 -15
  78. data/app/assets/svgs/avo/arrow-circle-right.svg +0 -1
  79. data/app/assets/svgs/avo/bell.svg +0 -3
  80. data/app/assets/svgs/avo/color-swatch.svg +0 -1
  81. data/app/assets/svgs/avo/dashboards.svg +0 -6
  82. data/app/assets/svgs/avo/exclamation.svg +0 -1
  83. data/app/assets/svgs/avo/filter.svg +0 -1
  84. data/app/assets/svgs/avo/logout.svg +0 -3
  85. data/app/assets/svgs/avo/resources.svg +0 -13
  86. data/app/assets/svgs/avo/save.svg +0 -8
  87. data/app/assets/svgs/avo/selector.svg +0 -1
  88. data/app/assets/svgs/avo/sort-ascending.svg +0 -1
  89. data/app/assets/svgs/avo/sort-descending.svg +0 -1
  90. data/app/assets/svgs/avo/times.svg +0 -3
  91. data/app/assets/svgs/avo/tools.svg +0 -3
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::KeyboardShortcutsComponent < Avo::BaseComponent
4
+ def sections
5
+ @sections ||= [
6
+ build_section(
7
+ "Navigation",
8
+ [
9
+ shortcut(action: "Show keyboard shortcuts", keys: ["?"]),
10
+ shortcut(action: "Focus resource search", keys: {mac: ["Cmd", "K"], other: ["Ctrl", "K"]}),
11
+ shortcut(action: "Toggle sidebar", keys: {mac: ["Cmd", "\\"], other: ["Ctrl", "\\"]}),
12
+ shortcut(action: "Close modal", keys: ["Esc"]),
13
+ shortcut(
14
+ action: "Navigate options in the modal",
15
+ any_of: [["↑"], ["↓"]],
16
+ keys_aria_label: "Up arrow or down arrow"
17
+ ),
18
+ shortcut(action: "Go back", keys: ["B"])
19
+ ]
20
+ ),
21
+ build_section(
22
+ "Edit view",
23
+ [
24
+ shortcut(action: "Submit form", keys: {mac: ["Cmd", "↵"], other: ["Ctrl", "↵"]}),
25
+ shortcut(action: "Unfocus field", keys: ["Esc"])
26
+ ]
27
+ ),
28
+ build_section(
29
+ "Show view",
30
+ [
31
+ shortcut(action: "Delete record", keys: ["D"]),
32
+ shortcut(action: "Edit record", keys: ["E"])
33
+ ]
34
+ ),
35
+ build_section(
36
+ "Index view",
37
+ [
38
+ shortcut(action: "Create new record", keys: ["C"])
39
+ ]
40
+ )
41
+ ]
42
+ end
43
+
44
+ def render_shortcut_keys(shortcut)
45
+ if shortcut[:any_of].present?
46
+ render_shortcut_alternatives(shortcut[:any_of])
47
+ else
48
+ render_chord(shortcut[:keys])
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def build_section(title, shortcuts)
55
+ {
56
+ id: "hotkey-group-#{title.parameterize.underscore}",
57
+ title: title,
58
+ shortcuts: shortcuts
59
+ }
60
+ end
61
+
62
+ def shortcut(action:, keys: nil, any_of: nil, keys_aria_label: nil)
63
+ {
64
+ action: action,
65
+ keys: keys,
66
+ any_of: any_of,
67
+ keys_aria_label: keys_aria_label
68
+ }
69
+ end
70
+
71
+ def render_shortcut_alternatives(chords)
72
+ helpers.safe_join(
73
+ chords.flat_map.with_index do |chord, index|
74
+ [
75
+ (tag.span("or", class: "hotkey__or", aria: {hidden: true}) if index.positive?),
76
+ render_chord(chord)
77
+ ].compact
78
+ end
79
+ )
80
+ end
81
+
82
+ def render_chord(chord)
83
+ tag.span class: "hotkey__combo" do
84
+ helpers.safe_join(chord_fragments(chord))
85
+ end
86
+ end
87
+
88
+ def chord_fragments(chord)
89
+ return platform_split_fragments(chord) if platform_split_chord?(chord)
90
+
91
+ key_fragments(Array(chord))
92
+ end
93
+
94
+ def platform_split_fragments(chord)
95
+ [
96
+ platform_modifier_key,
97
+ *key_fragments(chord[:mac].drop(1), leading_separator: true)
98
+ ]
99
+ end
100
+
101
+ def key_fragments(keys, leading_separator: false)
102
+ keys.flat_map.with_index do |key, index|
103
+ [
104
+ (tag.span("+", class: "hotkey__key-separator") if leading_separator || index.positive?),
105
+ tag.kbd(key)
106
+ ].compact
107
+ end
108
+ end
109
+
110
+ def platform_modifier_key
111
+ tag.kbd do
112
+ helpers.safe_join([
113
+ tag.abbr("⌘", title: "Command", class: "no-underline os-pc:hidden"),
114
+ tag.abbr("CTRL", title: "CTRL", class: "no-underline os-mac:hidden")
115
+ ])
116
+ end
117
+ end
118
+
119
+ def platform_split_chord?(chord)
120
+ chord.is_a?(Hash) &&
121
+ chord[:mac].is_a?(Array) &&
122
+ chord[:other].is_a?(Array) &&
123
+ chord[:mac].size == chord[:other].size &&
124
+ chord[:mac].first.to_s == "Cmd" &&
125
+ chord[:other].first.to_s == "Ctrl"
126
+ end
127
+ end
@@ -1,5 +1,5 @@
1
1
  <div class="relative flex flex-col w-full max @container/details">
2
- <%= link_to helpers.svg('heroicons/outline/x-mark', class: "size-6"), helpers.avo.media_library_index_path,
2
+ <%= link_to helpers.svg('tabler/outline/x', class: "size-6"), helpers.avo.media_library_index_path,
3
3
  class: "absolute z-10 inset-auto end-0 top-0 mt-2 me-2 block bg-white p-1 rounded-lg text-slate-600 hover:text-slate-900",
4
4
  title: t('avo.close'),
5
5
  data: {
@@ -15,7 +15,7 @@
15
15
  <%= video_tag(helpers.main_app.url_for(@blob), controls: true, preload: false, class: 'w-full') %>
16
16
  <% else %>
17
17
  <div class="relative h-full flex flex-col justify-center items-center w-full bg-slate-100">
18
- <%= helpers.svg "heroicons/outline/document-text", class: 'h-10 text-gray-600 mb-2' %>
18
+ <%= helpers.svg "tabler/outline/file-text", class: 'h-10 text-gray-600 mb-2' %>
19
19
  </div>
20
20
  <% end %>
21
21
  <div class="flex justify-center w-full text-sm gap-4">
@@ -28,7 +28,7 @@
28
28
  media_library_attach_target: 'dropzone',
29
29
  action: 'click->media-library-attach#triggerFileBrowser',
30
30
  } do %>
31
- <%= helpers.svg 'heroicons/outline/cloud-arrow-up', class: 'size-6 text-gray-400' %> Upload a file
31
+ <%= helpers.svg 'tabler/outline/cloud-upload', class: 'size-6 text-gray-400' %> Upload a file
32
32
  <small>Click to browse or drag and drop</small>
33
33
  <% end %>
34
34
 
@@ -3,7 +3,7 @@
3
3
  class: "relative group min-h-full max-w-full flex-1 flex flex-col justify-between gap-2 border border-slate-200 p-1.5 rounded-xl hover:border-blue-500 hover:outline data-[selected=true]:border-blue-500 data-[selected=true]:outline outline-blue-500",
4
4
  data: do %>
5
5
  <% if false && @attaching %>
6
- <div class="absolute bg-blue-500 group-hover:opacity-100 group-data-[selected=true]:opacity-100 opacity-0 inset-auto start-0 top-0 text-white rounded-ss-xl rounded-ee-xl -ms-px -mt-px p-2"><div class="border border-white"><%= helpers.svg "heroicons/outline/check", class: 'group-data-[selected=true]:opacity-100 opacity-0 size-4' %></div></div>
6
+ <div class="absolute bg-blue-500 group-hover:opacity-100 group-data-[selected=true]:opacity-100 opacity-0 inset-auto start-0 top-0 text-white rounded-ss-xl rounded-ee-xl -ms-px -mt-px p-2"><div class="border border-white"><%= helpers.svg "tabler/outline/check", class: 'group-data-[selected=true]:opacity-100 opacity-0 size-4' %></div></div>
7
7
  <% end %>
8
8
  <div class="flex flex-col h-full aspect-video overflow-hidden rounded-lg justify-center items-center">
9
9
  <% if blob.image? %>
@@ -14,7 +14,7 @@
14
14
  <%= video_tag(helpers.main_app.url_for(blob), controls: true, preload: false, class: 'w-full') %>
15
15
  <% else %>
16
16
  <div class="relative h-full flex flex-col justify-center items-center w-full bg-slate-100">
17
- <%= helpers.svg "heroicons/outline/document-text", class: 'h-10 text-gray-600 mb-2' %>
17
+ <%= helpers.svg "tabler/outline/file-text", class: 'h-10 text-gray-600 mb-2' %>
18
18
  </div>
19
19
  <% end %>
20
20
  </div>
@@ -1,25 +1,40 @@
1
- <div class="<%= class_names("modal", {"modal--width-#{@width}": @width.present?, "modal--height-#{@height}": @height.present?}) %>"
2
- data-controller="modal modal-size"
3
- data-modal-target="modal"
4
- data-modal-close-modal-on-backdrop-click-value="<%= close_modal_on_backdrop_click %>"
5
- data-modal-size-current-width-value="<%= @width %>"
6
- data-modal-size-current-height-value="<%= @height %>"
7
- >
8
- <div aria-expanded="true" class="modal__overlay" data-modal-target="backdrop" data-action="click->modal#close"></div>
9
- <div aria-expanded="true" role="dialog" aria-modal="true" class="modal__card-container" data-modal-size-target="container">
1
+ <%= tag.div class: class_names("modal", @class, {"modal--width-#{@width}": @width.present?, "modal--height-#{@height}": @height.present?}), tabindex: -1, data: {
2
+ controller: "#{ctrl} modal-size",
3
+ "#{ctrl}-target": "modal",
4
+ "#{ctrl}-close-modal-on-backdrop-click-value": close_modal_on_backdrop_click,
5
+ "modal-size-current-width-value": @width,
6
+ "modal-size-current-height-value": @height,
7
+ **@data
8
+ }, **(@hidden ? { hidden: true } : {}) do %>
9
+ <%= tag.div(
10
+ aria: { expanded: true },
11
+ class: "modal__overlay",
12
+ data: {
13
+ "#{ctrl}-target": "backdrop",
14
+ action: "click->#{ctrl}#close"
15
+ }
16
+ ) %>
17
+ <div
18
+ aria-expanded="true"
19
+ role="dialog"
20
+ aria-modal="true"
21
+ class="modal__card-container"
22
+ data-modal-size-target="container"
23
+ >
10
24
  <div class="modal__card">
11
25
  <div class="card">
12
26
  <div class="card__wrapper">
13
27
  <% if has_header? %>
14
28
  <div class="card__header">
15
- <div class="flex gap-4 items-start w-full">
16
- <div class="flex grow items-center min-w-0">
17
- <div class="flex flex-col gap-0.5 items-start">
29
+ <div class="flex w-full items-start gap-4">
30
+ <div class="flex min-w-0 grow items-center">
31
+ <div class="flex flex-col items-start gap-0.5">
18
32
  <% if @title.present? || heading? %>
19
33
  <p class="modal__title">
20
34
  <%= @title.presence || heading %>
21
35
  </p>
22
36
  <% end %>
37
+
23
38
  <% if @description.present? %>
24
39
  <p class="card__description">
25
40
  <%= @description %>
@@ -27,20 +42,28 @@
27
42
  <% end %>
28
43
  </div>
29
44
  </div>
45
+
30
46
  <div class="flex items-center gap-2">
31
47
  <% if false && Rails.env.development? %>
32
48
  <%= render partial: "avo/modal/size_selector", locals: { type: 'width', width: @width, height: @height } %>
33
49
  <%= render partial: "avo/modal/size_selector", locals: { type: 'height', width: @width, height: @height } %>
34
50
  <% end %>
51
+
35
52
  <% if @show_close_button %>
36
- <button class="modal__close-button" data-action="click->modal#closeModal" aria-label="<%= t('avo.close') %>">
37
- <%= helpers.svg "heroicons/outline/x-mark", class: "size-5" %>
53
+ <button
54
+ class="modal__close-button"
55
+ data-action="click-><%= ctrl %>#closeModal"
56
+ aria-label="<%= t('avo.close') %>"
57
+ tabindex="-1"
58
+ >
59
+ <%= helpers.svg "tabler/outline/x", class: "size-5" %>
38
60
  </button>
39
61
  <% end %>
40
62
  </div>
41
63
  </div>
42
64
  </div>
43
65
  <% end %>
66
+
44
67
  <div class="card__body <%= @body_class %>">
45
68
  <% if content? %>
46
69
  <div class="modal__body-content overflow-auto">
@@ -48,6 +71,7 @@
48
71
  </div>
49
72
  <% end %>
50
73
  </div>
74
+
51
75
  <% if controls? %>
52
76
  <div class="card__footer modal__controls">
53
77
  <%= controls %>
@@ -57,4 +81,4 @@
57
81
  </div>
58
82
  </div>
59
83
  </div>
60
- </div>
84
+ <% end %>
@@ -12,6 +12,14 @@ class Avo::ModalComponent < Avo::BaseComponent
12
12
  prop :title
13
13
  prop :description
14
14
  prop :show_close_button, default: true
15
+ prop :behavior, default: :ephemeral # :ephemeral removes from DOM on close, :persistent toggles hidden attribute
16
+ prop :class, default: ""
17
+ prop :data, default: {}.freeze
18
+ prop :hidden, default: false
19
+
20
+ def stimulus_controller
21
+ (@behavior == :persistent) ? "persistent-modal" : "modal"
22
+ end
15
23
 
16
24
  def height_classes
17
25
  "max-h-[calc(100dvh-5rem)] min-h-1/4"
@@ -20,4 +28,6 @@ class Avo::ModalComponent < Avo::BaseComponent
20
28
  def has_header?
21
29
  @title.present? || @description.present? || heading? || @show_close_button
22
30
  end
31
+
32
+ def ctrl = stimulus_controller
23
33
  end
@@ -29,23 +29,29 @@
29
29
  </div>
30
30
 
31
31
  <%# Right: info + navigation grouped pill %>
32
- <div class="pagination__controls">
33
- <% if @resource.pagination_type.default? %>
34
- <div class="pagination__info">
35
- <span class="pagination__info-number">
36
- <%= "#{@pagy.from}-#{@pagy.to}" %>
37
- </span>
38
- &nbsp; of &nbsp;
39
- <span>
40
- <%= "#{@pagy.count}" %>
41
- </span>
42
- </div>
43
- <% end %>
32
+ <div class="pagination__controls-wrap">
33
+ <div class="pagination__controls">
34
+ <% if @resource.pagination_type.default? %>
35
+ <div class="pagination__info">
36
+ <span class="pagination__info-number">
37
+ <%= "#{formatted_number(@pagy.from)}-#{formatted_number(@pagy.to)}" %>
38
+ </span>
39
+ &nbsp; of &nbsp;
40
+ <% if @pagy.count >= 10_000 %>
41
+ <span title="<%= formatted_count %>" data-tippy="tooltip">
42
+ <%= number_to_social(@pagy.count, start_at: 10_000) %>
43
+ </span>
44
+ <% else %>
45
+ <span><%= formatted_count %></span>
46
+ <% end %>
47
+ </div>
48
+ <% end %>
44
49
 
45
- <% if @pagy.pages > 1 %>
46
- <div class="pagination__nav">
47
- <%== @pagy.series_nav(anchor_string: "data-turbo-frame=\"#{@turbo_frame}\"") %>
48
- </div>
49
- <% end %>
50
+ <% if @pagy.pages > 1 %>
51
+ <div class="pagination__nav">
52
+ <%== formatted_series_nav %>
53
+ </div>
54
+ <% end %>
55
+ </div>
50
56
  </div>
51
57
  </div>
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Avo::PaginatorComponent < Avo::BaseComponent
4
+ NUMBER_DELIMITER = "."
5
+
4
6
  prop :resource
5
7
  prop :parent_record
6
8
  prop :parent_resource
@@ -52,4 +54,20 @@ class Avo::PaginatorComponent < Avo::BaseComponent
52
54
  num = helpers.content_tag(:span, option, class: "pagination__per-page-option-num")
53
55
  "#{num} #{t("avo.per_page").downcase}".html_safe
54
56
  end
57
+
58
+ def formatted_count
59
+ formatted_number(@pagy.count)
60
+ end
61
+
62
+ def formatted_series_nav
63
+ @pagy.series_nav(anchor_string: %(data-turbo-frame="#{@turbo_frame}"))
64
+ .gsub(/>(\d{4,})</) { |match| match.sub($1, formatted_number($1)) }
65
+ .html_safe
66
+ end
67
+
68
+ private
69
+
70
+ def formatted_number(number)
71
+ helpers.number_with_delimiter(number, delimiter: NUMBER_DELIMITER)
72
+ end
55
73
  end
@@ -114,10 +114,11 @@ class Avo::ResourceComponent < Avo::BaseComponent
114
114
  style: :text,
115
115
  title: control.title,
116
116
  data: {
117
- tippy: control.title ? :tooltip : nil,
118
- action: via_belongs_to ? "click->modal#close" : nil
117
+ hotkey: "b",
118
+ action: ("click->modal#close" if via_belongs_to),
119
+ tippy: control.title ? :tooltip : nil
119
120
  }.compact,
120
- icon: "heroicons/outline/arrow-left" do
121
+ icon: "tabler/outline/arrow-left" do
121
122
  control.label
122
123
  end
123
124
  end
@@ -157,12 +158,13 @@ class Avo::ResourceComponent < Avo::BaseComponent
157
158
  title: control.title,
158
159
  aria_label: control.title,
159
160
  data: {
161
+ hotkey: "d",
160
162
  turbo_confirm: t("avo.are_you_sure", item: @resource.record.model_name.name.downcase),
161
163
  turbo_method: :delete,
162
164
  target: "control:destroy",
163
165
  control: :destroy,
164
166
  tippy: control.title ? :tooltip : nil,
165
- "resource-id": @resource.record_param,
167
+ "resource-id": @resource.record_param
166
168
  } do
167
169
  control.label
168
170
  end
@@ -172,8 +174,9 @@ class Avo::ResourceComponent < Avo::BaseComponent
172
174
  return unless can_see_the_save_button?
173
175
 
174
176
  data_attributes = {
177
+ hotkey: "Mod+Enter",
175
178
  turbo_confirm: @resource.confirm_on_save ? t("avo.are_you_sure") : nil
176
- }
179
+ }.compact
177
180
 
178
181
  add_stimulus_attributes_for(@resource, data_attributes, "saveButton")
179
182
 
@@ -181,7 +184,7 @@ class Avo::ResourceComponent < Avo::BaseComponent
181
184
  style: :primary,
182
185
  loading: true,
183
186
  type: :submit,
184
- icon: "avo/save",
187
+ icon: "tabler/outline/device-floppy",
185
188
  data: data_attributes do
186
189
  control.label
187
190
  end
@@ -194,7 +197,10 @@ class Avo::ResourceComponent < Avo::BaseComponent
194
197
  color: :accent,
195
198
  style: :primary,
196
199
  title: control.title,
197
- data: {tippy: control.title ? :tooltip : nil},
200
+ data: {
201
+ hotkey: "e",
202
+ tippy: control.title ? :tooltip : nil
203
+ }.compact,
198
204
  icon: "tabler/outline/edit" do
199
205
  control.label
200
206
  end
@@ -222,6 +228,7 @@ class Avo::ResourceComponent < Avo::BaseComponent
222
228
  style: :primary,
223
229
  icon: "tabler/outline/plus",
224
230
  data: {
231
+ hotkey: "c",
225
232
  target: :create
226
233
  } do
227
234
  control.label
@@ -14,7 +14,7 @@
14
14
  <span class="sidebar-icon <%= 'sidebar-icon--collapsed' if collapsed %>"
15
15
  data-menu-target="svg"
16
16
  >
17
- <%= helpers.svg 'heroicons/outline/chevron-down', class: 'h-4' %>
17
+ <%= helpers.svg 'tabler/outline/chevron-down', class: 'h-4' %>
18
18
  </span>
19
19
  </button>
20
20
  <% else %>
@@ -1,11 +1,15 @@
1
1
  <% if @path.present? %>
2
- <%= link_caller.send link_method, @path, class: "sidebar-link", active: @active, target: @target, data: @data, disabled: @disabled, "aria-disabled": @disabled, **@args do %>
2
+ <%= link_caller.send link_method, @path, class: "sidebar-link", active: @active, target: @target, data: link_data, disabled: @disabled, "aria-disabled": @disabled, **@args do %>
3
3
  <% if @reserve_icon_space || link_icon.present? %>
4
- <span class="sidebar-link__icon-wrapper sidebar-icon<%= ' sidebar-link__icon-wrapper--placeholder' if link_icon.blank? %>">
4
+ <span class="sidebar-link__icon-wrapper sidebar-icon <%= 'sidebar-link__icon-wrapper--placeholder' if link_icon.blank? %>">
5
5
  <%= helpers.svg link_icon, class: "sidebar-link__icon sidebar-icon" if link_icon.present? %>
6
6
  </span>
7
7
  <% end %>
8
+
8
9
  <span><%= @label %></span>
10
+
11
+ <%= hotkey_badge(@hotkey, class: "ms-auto") if @hotkey.present? %>
12
+
9
13
  <% if @target == :_blank %>
10
14
  <%= helpers.svg("tabler/outline/external-link", class: "sidebar-link__external-icon sidebar-icon") %>
11
15
  <% end %>
@@ -13,22 +17,26 @@
13
17
  <% else %>
14
18
  <%= content_tag :div, class: "sidebar-link", active: @active, target: @target, data: @data do %>
15
19
  <% if @reserve_icon_space || link_icon.present? %>
16
- <span class="sidebar-link__icon-wrapper sidebar-icon<%= ' sidebar-link__icon-wrapper--placeholder' if link_icon.blank? %>">
20
+ <span class="sidebar-link__icon-wrapper sidebar-icon <%= 'sidebar-link__icon-wrapper--placeholder' if link_icon.blank? %>">
17
21
  <%= helpers.svg link_icon, class: "sidebar-link__icon sidebar-icon" if link_icon.present? %>
18
22
  </span>
19
23
  <% end %>
24
+
20
25
  <span><%= @label %></span>
21
26
  <% end %>
22
27
  <% end %>
28
+
23
29
  <% if @items.present? && parent_link_active? %>
24
30
  <div class="sidebar-subitem__items">
25
31
  <% @items.each_with_index do |item, index| %>
26
32
  <% if item.path.present? %>
27
- <%= link_caller.send link_method, item.path, class: "sidebar-subitem #{subitem_bar_class(index)}", active: @active, target: item.target, data: item.data, disabled: @disabled, "aria-disabled": @disabled, **item.args do %>
33
+ <%= link_caller.send link_method, item.path, class: "sidebar-subitem #{subitem_bar_class(index)}", active: @active, target: item.target, data: subitem_data(item), disabled: @disabled, "aria-disabled": @disabled, **item.args do %>
28
34
  <span><%= item.try(:label).presence || item.try(:name).presence %></span>
35
+
36
+ <%= hotkey_badge(item.hotkey, class: "ms-auto") if item.try(:hotkey).present? %>
29
37
  <% end %>
30
38
  <% else %>
31
- <%= content_tag :div, class: "sidebar-subitem #{subitem_bar_class(index)}", active: @active, target: item.target, data: item.data do %>
39
+ <%= content_tag :div, class: "sidebar-subitem #{subitem_bar_class(index)}", active: @active, target: item.target, data: subitem_data(item) do %>
32
40
  <span><%= item.try(:label).presence || item.try(:name).presence %></span>
33
41
  <% end %>
34
42
  <% end %>
@@ -16,6 +16,15 @@ class Avo::Sidebar::LinkComponent < Avo::BaseComponent
16
16
  prop :reserve_icon_space, default: false
17
17
  prop :args, kind: :**, default: {}.freeze
18
18
  prop :items
19
+ prop :hotkey, default: nil
20
+
21
+ def link_data
22
+ build_link_data(@data, @hotkey)
23
+ end
24
+
25
+ def subitem_data(item)
26
+ build_link_data(item.data, item.hotkey)
27
+ end
19
28
 
20
29
  def is_external?
21
30
  # If the path contains the scheme, check if it includes the root path or not
@@ -67,4 +76,12 @@ class Avo::Sidebar::LinkComponent < Avo::BaseComponent
67
76
  ""
68
77
  end
69
78
  end
79
+
80
+ private
81
+
82
+ def build_link_data(data, hotkey)
83
+ return data if hotkey.blank?
84
+
85
+ data.merge(hotkey: hotkey.to_s.first)
86
+ end
70
87
  end
@@ -15,7 +15,7 @@
15
15
  <span class="sidebar-section__icon sidebar-icon <%= 'sidebar-icon--collapsed' if collapsed %>"
16
16
  data-menu-target="svg"
17
17
  >
18
- <%= helpers.svg 'heroicons/outline/chevron-down' %>
18
+ <%= helpers.svg 'tabler/outline/chevron-down' %>
19
19
  </span>
20
20
  </button>
21
21
  <% else %>
@@ -20,7 +20,7 @@
20
20
  <div>
21
21
  <div class="sidebar__nav-list">
22
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) %>
23
+ <%= render Avo::Sidebar::LinkComponent.new label: dashboard.navigation_label, path: helpers.avo_dashboards.dashboard_path(dashboard), hotkey: dashboard.try(:hotkey).presence %>
24
24
  <% end %>
25
25
  </div>
26
26
  </div>
@@ -28,7 +28,7 @@
28
28
 
29
29
  <div class="sidebar__nav-list">
30
30
  <% resources.sort_by { |r| r.navigation_label }.each do |resource| %>
31
- <%= render Avo::Sidebar::LinkComponent.new label: resource.navigation_label, path: helpers.resources_path(resource: resource), icon: resource.icon %>
31
+ <%= render Avo::Sidebar::LinkComponent.new label: resource.navigation_label, path: helpers.resources_path(resource: resource), icon: resource.icon, hotkey: resource.try(:hotkey).presence %>
32
32
  <% end %>
33
33
  </div>
34
34
 
@@ -41,7 +41,7 @@
41
41
  },
42
42
  class: 'sidebar-profile__form' do |form| %>
43
43
  <%= form.button turbo_confirm: t('avo.are_you_sure'), class: "sidebar-profile__sign-out" do %>
44
- <%= helpers.svg "avo/logout", class: 'sidebar-profile__sign-out-icon sidebar-icon' %> <%= t('avo.sign_out') %>
44
+ <%= helpers.svg "tabler/outline/logout", class: 'sidebar-profile__sign-out-icon sidebar-icon' %> <%= t('avo.sign_out') %>
45
45
  <% end %>
46
46
  <% end %>
47
47
  <% end %>
@@ -18,11 +18,11 @@
18
18
 
19
19
  <% if @with_shortcut %>
20
20
  <span class="search-input__suffix" aria-hidden="true">
21
- <kbd class="search-input__shortcut">
21
+ <kbd>
22
22
  <abbr title="Command" class="no-underline os-pc:hidden">⌘</abbr>
23
23
  <abbr title="CTRL" class="no-underline os-mac:hidden">CTRL</abbr>
24
24
  </kbd>
25
- <kbd class="search-input__shortcut">K</kbd>
25
+ <kbd>K</kbd>
26
26
  </span>
27
27
  <% end %>
28
28
  <% end %>
@@ -136,7 +136,7 @@ class Avo::Views::ResourceIndexComponent < Avo::ResourceComponent
136
136
 
137
137
  a_button size: :sm,
138
138
  color: :primary,
139
- icon: "avo/filter",
139
+ icon: "tabler/outline/filter",
140
140
  data: {
141
141
  controller: "avo-filters",
142
142
  action: "click->avo-filters#toggleFiltersArea",
@@ -5,41 +5,29 @@ import 'chartkick/chart.js/chart.esm'
5
5
  import 'mapkick/bundle'
6
6
  import 'regenerator-runtime/runtime'
7
7
  import * as ActiveStorage from '@rails/activestorage'
8
- import * as Mousetrap from 'mousetrap'
9
8
  import { Turbo } from '@hotwired/turbo-rails'
9
+ 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
14
 
14
15
  import './js/active-storage'
15
16
  import './js/controllers'
16
17
  import './js/custom-confirm'
17
18
  import './js/custom-stream-actions'
18
19
 
20
+ function installHotkeys(root = document) {
21
+ root.querySelectorAll('[data-hotkey]').forEach((el) => {
22
+ install(el)
23
+ })
24
+ }
25
+
19
26
  window.Avo.localStorage = new LocalStorageService()
20
27
 
21
28
  window.Turbolinks = Turbo
22
29
 
23
- let scrollTop = null
24
- Mousetrap.bind('r r r', () => {
25
- // Capture scroll position
26
- scrollTop = document.scrollingElement.scrollTop
27
-
28
- window.StreamActions.turbo_reload()
29
- })
30
-
31
- // Add the shift-pressed class to the body when the shift key is pressed
32
- document.addEventListener('keydown', (event) => {
33
- if (event.shiftKey) {
34
- document.body.classList.add('shift-pressed')
35
- }
36
- })
37
- // Remove the shift-pressed class from the body when the shift key is released
38
- document.addEventListener('keyup', (event) => {
39
- if (!event.shiftKey) {
40
- document.body.classList.remove('shift-pressed')
41
- }
42
- })
30
+ installGlobalHotkeys()
43
31
 
44
32
  function initTippy() {
45
33
  tippy('[data-tippy="tooltip"]', {
@@ -79,16 +67,12 @@ document.addEventListener('turbo:before-stream-render', () => {
79
67
  }, 1)
80
68
  })
81
69
 
70
+ document.addEventListener('turbo:frame-render', (e) => installHotkeys(e.target))
71
+
82
72
  document.addEventListener('turbo:load', () => {
73
+ installHotkeys()
83
74
  initTippy()
84
75
 
85
- // Restore scroll position after r r r turbo reload
86
- if (scrollTop) {
87
- setTimeout(() => {
88
- document.scrollingElement.scrollTo(0, scrollTop)
89
- scrollTop = 0
90
- }, 50)
91
- }
92
76
  setTimeout(() => {
93
77
  document.body.classList.remove('turbo-loading')
94
78
  }, 1)