active_admin_prism 0.1.0

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +133 -0
  3. data/INTEGRATION.md +1012 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +319 -0
  6. data/app/assets/javascripts/active_admin_prism/prism.js +1 -0
  7. data/app/assets/stylesheets/active_admin_prism/_variable_overrides.scss +43 -0
  8. data/app/assets/stylesheets/active_admin_prism/prism.css +1 -0
  9. data/app/views/active_admin/devise/confirmations/new.html.erb +26 -0
  10. data/app/views/active_admin/devise/passwords/edit.html.erb +29 -0
  11. data/app/views/active_admin/devise/passwords/new.html.erb +26 -0
  12. data/app/views/active_admin/devise/registrations/new.html.erb +32 -0
  13. data/app/views/active_admin/devise/sessions/new.html.erb +37 -0
  14. data/app/views/active_admin/devise/shared/_brand.html.erb +34 -0
  15. data/app/views/active_admin/devise/unlocks/new.html.erb +25 -0
  16. data/js-src/prism.js +270 -0
  17. data/lib/active_admin/views/filters_sidebar.rb +69 -0
  18. data/lib/active_admin/views/flash_messages.rb +77 -0
  19. data/lib/active_admin/views/index_table_actions.rb +46 -0
  20. data/lib/active_admin/views/prism_sidebar.rb +212 -0
  21. data/lib/active_admin_prism/configuration.rb +180 -0
  22. data/lib/active_admin_prism/engine.rb +18 -0
  23. data/lib/active_admin_prism/version.rb +5 -0
  24. data/lib/active_admin_prism.rb +33 -0
  25. data/lib/formtastic/inputs/prism_toggle_input.rb +31 -0
  26. data/lib/generators/active_admin_prism/error_pages/error_pages_generator.rb +49 -0
  27. data/lib/generators/active_admin_prism/error_pages/templates/404.html +164 -0
  28. data/lib/generators/active_admin_prism/error_pages/templates/500.html +173 -0
  29. data/lib/generators/active_admin_prism/install/install_generator.rb +69 -0
  30. data/lib/prism_icons.rb +82 -0
  31. data/lib/prism_login_helper.rb +31 -0
  32. data/lib/prism_toggle_tag.rb +35 -0
  33. data/scss-src/_base.scss +281 -0
  34. data/scss-src/_buttons.scss +134 -0
  35. data/scss-src/_forms.scss +272 -0
  36. data/scss-src/_icons.scss +27 -0
  37. data/scss-src/_login.scss +306 -0
  38. data/scss-src/_panels.scss +249 -0
  39. data/scss-src/_sidebar.scss +479 -0
  40. data/scss-src/_tables.scss +142 -0
  41. data/scss-src/_variables.scss +101 -0
  42. data/scss-src/prism.scss +15 -0
  43. metadata +157 -0
@@ -0,0 +1,34 @@
1
+ <%#
2
+ Shared brand block for every Devise auth page Prism reskins (sign in,
3
+ sign up, forgot/reset password, resend confirmation, resend unlock) —
4
+ each action's own view renders this instead of duplicating the
5
+ logo/app-name/subtitle markup, so all of them redesign (or degrade back
6
+ to stock ActiveAdmin) together instead of drifting out of sync. Gated by
7
+ ActiveAdminPrism::Configuration#login_page, the same flag the
8
+ sign-in page itself uses (see lib/prism_login_helper.rb for the
9
+ logo/app-name resolution logic behind prism_login_logo_html/
10
+ prism_login_app_name).
11
+
12
+ Locals:
13
+ action_title - the page's own translated title (e.g. "Sign up",
14
+ "Change your password") — the caller has already run
15
+ it through `title(...)` for its @page_title side
16
+ effect before passing it in here, so this partial
17
+ never needs to call `title` itself.
18
+ subtitle - what to show under the app name in branded mode.
19
+ Every caller except sessions#new just passes
20
+ action_title again; sessions#new passes
21
+ config.login_tagline instead, since that one has its
22
+ own dedicated, independently configurable copy.
23
+ %>
24
+ <% if ActiveAdminPrism.configuration.login_page %>
25
+ <div class="prism-login-brand">
26
+ <div class="prism-login-logo"><%= prism_login_logo_html %></div>
27
+ <h2 class="prism-login-title"><%= prism_login_app_name %></h2>
28
+ <% if subtitle.present? %>
29
+ <p class="prism-login-tagline"><%= subtitle %></p>
30
+ <% end %>
31
+ </div>
32
+ <% else %>
33
+ <h2><%= active_admin_application.site_title(self) %> <%= action_title %></h2>
34
+ <% end %>
@@ -0,0 +1,25 @@
1
+ <%#
2
+ Overrides ActiveAdmin's own active_admin/devise/unlocks/new.html.erb the
3
+ same way sessions/new.html.erb does (see that file's header comment for
4
+ the Gemfile-order mechanism this relies on). Form fields/behavior below
5
+ are byte-for-byte ActiveAdmin's own template; only the header markup
6
+ (shared devise/shared/_brand partial) and the links wrapper are new.
7
+ %>
8
+ <div id="login" class="prism-login">
9
+ <% action_title = title t('active_admin.devise.unlock.title') %>
10
+ <%= render "active_admin/devise/shared/brand", action_title: action_title, subtitle: action_title %>
11
+
12
+ <%= render partial: "active_admin/devise/shared/error_messages", resource: resource %>
13
+ <%= active_admin_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f|
14
+ f.inputs do
15
+ f.input :email
16
+ end
17
+ f.actions do
18
+ f.action :submit, label: t('active_admin.devise.unlock.submit'), button_html: { value: t('active_admin.devise.unlock.submit') }
19
+ end
20
+ end %>
21
+
22
+ <div class="prism-login-links">
23
+ <%= render partial: "active_admin/devise/shared/links" %>
24
+ </div>
25
+ </div>
data/js-src/prism.js ADDED
@@ -0,0 +1,270 @@
1
+ // This is the real source — mirrors scss-src/*.scss's role for the CSS
2
+ // side. `bin/build-js` minifies this into
3
+ // app/assets/javascripts/active_admin_prism/prism.js, the file actually
4
+ // registered via ActiveAdmin.application.register_javascript and shipped
5
+ // to hosts. Never hand-edit that compiled copy — edit this file and
6
+ // rebuild. See bin/build-css's own comment for why shipping pre-minified
7
+ // assets matters even though Sprockets hosts would otherwise re-minify on
8
+ // their own `assets:precompile` — Propshaft hosts have no such step.
9
+ //
10
+ // Prism sidebar behaviour: collapsible nav groups + mobile off-canvas
11
+ // toggle. Vanilla JS, no dependency on jQuery/Stimulus/a bundler.
12
+ (function () {
13
+ "use strict";
14
+
15
+ var STORAGE_PREFIX = "prism_sidebar_";
16
+
17
+ function storageKey(li) {
18
+ return STORAGE_PREFIX + li.id;
19
+ }
20
+
21
+ function setOpen(li, open) {
22
+ li.classList.toggle("open", open);
23
+ try {
24
+ window.localStorage.setItem(storageKey(li), open ? "1" : "0");
25
+ } catch (e) {
26
+ // localStorage unavailable (private mode, disabled, etc.) — degrade to
27
+ // in-memory only, no persistence across reloads.
28
+ }
29
+ }
30
+
31
+ function restoreGroupState(sidebar) {
32
+ var groups = sidebar.querySelectorAll(".prism-nav-item.has-children");
33
+ groups.forEach(function (li) {
34
+ var stored;
35
+ try {
36
+ stored = window.localStorage.getItem(storageKey(li));
37
+ } catch (e) {
38
+ stored = null;
39
+ }
40
+
41
+ if (stored === "1") {
42
+ li.classList.add("open");
43
+ } else if (stored === "0") {
44
+ li.classList.remove("open");
45
+ }
46
+ // stored === null: leave the server-rendered default (open only when
47
+ // it contains the active item) untouched.
48
+ });
49
+ }
50
+
51
+ function onToggleClick(event) {
52
+ var toggle = event.target.closest("[data-prism-toggle]");
53
+ if (!toggle) return;
54
+
55
+ var li = toggle.closest(".prism-nav-item");
56
+ if (!li) return;
57
+
58
+ setOpen(li, !li.classList.contains("open"));
59
+ }
60
+
61
+ function onSidebarButtonClick(event) {
62
+ var button = event.target.closest("[data-prism-toggle-sidebar]");
63
+ if (!button) return;
64
+
65
+ document.body.classList.toggle("prism-sidebar-open");
66
+ }
67
+
68
+ document.addEventListener("DOMContentLoaded", function () {
69
+ var sidebar = document.getElementById("header");
70
+ if (!sidebar || !sidebar.classList.contains("prism-sidebar")) return;
71
+
72
+ restoreGroupState(sidebar);
73
+ sidebar.addEventListener("click", onToggleClick);
74
+ sidebar.addEventListener("click", onSidebarButtonClick);
75
+ });
76
+ })();
77
+
78
+ // Routes every plain `data-confirm` link (row-level View/Edit/Delete
79
+ // actions, any other rails-ujs confirm) through ActiveAdmin's own styled
80
+ // jQuery UI dialog — the same one it already uses for Batch Actions —
81
+ // instead of the native, unstylable browser confirm(). Batch Actions links
82
+ // are left alone; they already run their own confirm flow (with dynamic
83
+ // per-action inputs) via active_admin/base.js.
84
+ //
85
+ // This overrides $.rails.allowAction, jquery-ujs's documented extension
86
+ // point for custom confirm dialogs (jquery_ujs.js: "may be overridden with
87
+ // custom confirm dialog in $.rails.confirm" / allowAction). allowAction is
88
+ // called synchronously and expects true/false; since our dialog is async,
89
+ // we return false to block the original action, then re-`click()` the same
90
+ // element after the user confirms — allowAction runs again, sees the
91
+ // "prismConfirmed" flag, and returns true so rails-ujs's normal
92
+ // data-method handling proceeds untouched.
93
+ (function () {
94
+ "use strict";
95
+
96
+ document.addEventListener("DOMContentLoaded", function () {
97
+ // Set by Pages::Base#body_classes when
98
+ // ActiveAdminPrism.configuration.styled_confirms is false.
99
+ if (document.body.classList.contains("prism-styled-confirms-disabled")) return;
100
+ if (typeof jQuery === "undefined" || !jQuery.rails || !window.ActiveAdmin || !window.ActiveAdmin.ModalDialog) {
101
+ return;
102
+ }
103
+
104
+ var $ = jQuery;
105
+ var originalAllowAction = $.rails.allowAction;
106
+
107
+ $.rails.allowAction = function (element) {
108
+ var message = element.data("confirm");
109
+ if (!message) return originalAllowAction.call($.rails, element);
110
+ if (element.closest(".batch_actions_selector").length) {
111
+ return originalAllowAction.call($.rails, element);
112
+ }
113
+ if (element.data("prismConfirmed")) {
114
+ element.removeData("prismConfirmed");
115
+ return true;
116
+ }
117
+
118
+ new window.ActiveAdmin.ModalDialog(message, {}, function () {
119
+ element.data("prismConfirmed", true);
120
+ element[0].click();
121
+ });
122
+
123
+ return false;
124
+ };
125
+ });
126
+ })();
127
+
128
+ // Flash messages: dismiss button + auto-hide. See
129
+ // lib/active_admin/views/flash_messages.rb, which renders the
130
+ // ".prism-flash-dismiss" button markup this responds to, and two data
131
+ // attributes this reads at runtime so Ruby-side config controls behavior
132
+ // without needing to rebuild this static asset:
133
+ // - "data-prism-auto-dismiss-ms" (present only when
134
+ // ActiveAdminPrism.configuration.flash_auto_dismiss is true)
135
+ // - "data-prism-transition-ms" (ActiveAdminPrism::Configuration
136
+ // #flash_transition_ms) — applied as an inline transition-duration
137
+ // (overriding the CSS default) and reused as the removal delay, so the
138
+ // fade-out animation and the element's actual removal always stay in
139
+ // sync regardless of the configured duration.
140
+ (function () {
141
+ "use strict";
142
+
143
+ var DEFAULT_TRANSITION_MS = 320;
144
+
145
+ function dismiss(flash, transitionMs) {
146
+ if (flash.dataset.prismDismissed) return;
147
+ flash.dataset.prismDismissed = "true";
148
+ flash.style.transitionDuration = transitionMs + "ms";
149
+ flash.classList.add("prism-flash-hide");
150
+ setTimeout(function () {
151
+ flash.remove();
152
+ }, transitionMs);
153
+ }
154
+
155
+ document.addEventListener("DOMContentLoaded", function () {
156
+ var flashesWrapper = document.querySelector(".flashes");
157
+ var flashes = document.querySelectorAll(".flashes .flash");
158
+ if (!flashes.length) return;
159
+
160
+ var transitionMs = (flashesWrapper && parseInt(flashesWrapper.dataset.prismTransitionMs, 10)) || DEFAULT_TRANSITION_MS;
161
+ var autoDismissMs = flashesWrapper && parseInt(flashesWrapper.dataset.prismAutoDismissMs, 10);
162
+
163
+ if (autoDismissMs) {
164
+ flashes.forEach(function (flash) {
165
+ setTimeout(function () {
166
+ dismiss(flash, transitionMs);
167
+ }, autoDismissMs);
168
+ });
169
+ }
170
+
171
+ document.addEventListener("click", function (event) {
172
+ var button = event.target.closest("[data-prism-flash-dismiss]");
173
+ if (!button) return;
174
+ dismiss(button.closest(".flash"), transitionMs);
175
+ });
176
+ });
177
+ })();
178
+
179
+ // "Filters" sidebar panel: collapses to a single icon button, expanding to
180
+ // the full form on click. See scss-src/_panels.scss (#filters_sidebar_section)
181
+ // for the actual collapse/expand styling — this only toggles the ".open"
182
+ // class an inline click responds to. Skipped entirely when
183
+ // ActiveAdminPrism.configuration.collapsible_filters is false (the
184
+ // "prism-filters-collapsible-disabled" body class, set in
185
+ // lib/active_admin/views/flash_messages.rb#body_classes).
186
+ (function () {
187
+ "use strict";
188
+
189
+ document.addEventListener("DOMContentLoaded", function () {
190
+ if (document.body.classList.contains("prism-filters-collapsible-disabled")) return;
191
+
192
+ var section = document.getElementById("filters_sidebar_section");
193
+ if (!section) return;
194
+
195
+ var toggle = section.querySelector(":scope > h3");
196
+ if (!toggle) return;
197
+
198
+ toggle.setAttribute("role", "button");
199
+ toggle.setAttribute("tabindex", "0");
200
+ toggle.setAttribute("aria-expanded", section.classList.contains("open") ? "true" : "false");
201
+ // Mirrored onto <body> because #main_content (the table/grid area)
202
+ // is a *sibling* of #sidebar, not a descendant of this section — CSS
203
+ // can't reach across siblings, so scss-src/_panels.scss keys the
204
+ // #main_content/#sidebar width reflow off this body class instead.
205
+ document.body.classList.toggle("prism-filters-open", section.classList.contains("open"));
206
+
207
+ function toggleOpen() {
208
+ var open = section.classList.toggle("open");
209
+ toggle.setAttribute("aria-expanded", open ? "true" : "false");
210
+ document.body.classList.toggle("prism-filters-open", open);
211
+ }
212
+
213
+ toggle.addEventListener("click", toggleOpen);
214
+ toggle.addEventListener("keydown", function (event) {
215
+ if (event.key !== "Enter" && event.key !== " ") return;
216
+ event.preventDefault();
217
+ toggleOpen();
218
+ });
219
+ });
220
+ })();
221
+
222
+ // "Languages" sidebar dropdown (see
223
+ // lib/active_admin/views/prism_sidebar.rb#render_language_switcher) —
224
+ // click/keyboard toggle plus click-outside-to-close, matching how a normal
225
+ // dropdown popover behaves. Absent from the page entirely (nothing for this
226
+ // to attach to) when ActiveAdminPrism.configuration.language_switcher or
227
+ // #languages is empty, so no explicit disabled-body-class check is needed
228
+ // here, unlike prism.js's other config-gated behaviors.
229
+ (function () {
230
+ "use strict";
231
+
232
+ function closeAll(except) {
233
+ document.querySelectorAll(".prism-sidebar-lang.open").forEach(function (wrapper) {
234
+ if (wrapper === except) return;
235
+ wrapper.classList.remove("open");
236
+ var toggle = wrapper.querySelector(".prism-lang-toggle");
237
+ if (toggle) toggle.setAttribute("aria-expanded", "false");
238
+ });
239
+ }
240
+
241
+ document.addEventListener("DOMContentLoaded", function () {
242
+ var toggle = document.querySelector(".prism-lang-toggle");
243
+ if (!toggle) return;
244
+
245
+ var wrapper = toggle.closest(".prism-sidebar-lang");
246
+
247
+ function toggleOpen() {
248
+ var open = wrapper.classList.toggle("open");
249
+ toggle.setAttribute("aria-expanded", open ? "true" : "false");
250
+ if (open) closeAll(wrapper);
251
+ }
252
+
253
+ toggle.setAttribute("role", "button");
254
+ toggle.setAttribute("tabindex", "0");
255
+
256
+ toggle.addEventListener("click", function (event) {
257
+ event.stopPropagation();
258
+ toggleOpen();
259
+ });
260
+ toggle.addEventListener("keydown", function (event) {
261
+ if (event.key !== "Enter" && event.key !== " ") return;
262
+ event.preventDefault();
263
+ toggleOpen();
264
+ });
265
+
266
+ document.addEventListener("click", function () {
267
+ closeAll();
268
+ });
269
+ });
270
+ })();
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Views
5
+ # Reopens ActiveAdmin::Views::SidebarSection (used for every sidebar
6
+ # section, e.g. "Filters", "Search Status", any custom
7
+ # `sidebar :title do ... end` block) to inject a real inline SVG icon
8
+ # into the title of the *Filters* section specifically — matching the
9
+ # id ActiveAdmin::SidebarSection#id computes for it
10
+ # ("filters_sidebar_section"). Every other sidebar section is untouched.
11
+ #
12
+ # The label text is wrapped in its own span so CSS can visually hide it
13
+ # while collapsed without touching the icon (see
14
+ # scss-src/_panels.scss's #filters_sidebar_section rules, and
15
+ # prism.js's filters-collapse handler for the actual toggle).
16
+ #
17
+ # This only runs when ActiveAdminPrism.configuration.collapsible_filters
18
+ # is true — when it's false the section is left exactly as ActiveAdmin
19
+ # renders it by default (plain text title, no icon).
20
+ class SidebarSection < Panel
21
+ FILTERS_SECTION_ID = "filters_sidebar_section"
22
+
23
+ def build(section)
24
+ @section = section
25
+ super(@section.title)
26
+ add_class @section.custom_class if @section.custom_class
27
+ self.id = @section.id
28
+
29
+ if @section.id == FILTERS_SECTION_ID && ActiveAdminPrism.configuration.collapsible_filters
30
+ add_class "prism-collapsible-panel"
31
+
32
+ # Panel#build already appended the plain-text @title (h3) as the
33
+ # first child, followed by @contents (the panel_contents div).
34
+ # Replace it with an [icon, label-span] version — built fresh
35
+ # rather than mutated in place (Arbre elements don't expose an
36
+ # API to insert content before an existing child).
37
+ #
38
+ # Panel#add_child redirects *any* add_child call on this object to
39
+ # @contents once @contents exists (so plain builder calls inside a
40
+ # panel's do-block land in .panel_contents, not as top-level
41
+ # siblings) — building a fresh h3 via the normal `h3(...)` verb at
42
+ # this point would silently land *inside* .panel_contents instead
43
+ # of replacing @title. Building it while @contents is
44
+ # (temporarily) nil bypasses that redirect — but add_child still
45
+ # *appends* (to the end of `children`, after the contents div,
46
+ # since that div itself is still sitting in `children` even
47
+ # though the @contents variable is momentarily nil), so it has to
48
+ # be explicitly deleted from wherever it landed before unshifting
49
+ # it to the front — otherwise it ends up in `children` twice.
50
+ children.delete(@title)
51
+ icon = ActiveAdminPrism::Icons.svg(:filter, css_class: "prism-filter-icon")
52
+ contents = @contents
53
+ @contents = nil
54
+ new_title = h3 do
55
+ text_node icon.html_safe if icon
56
+ span @section.title.to_s, class: "prism-filter-label"
57
+ end
58
+ @contents = contents
59
+
60
+ children.delete(new_title)
61
+ children.unshift(new_title)
62
+ @title = new_title
63
+ end
64
+
65
+ build_sidebar_content
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Views
5
+ module Pages
6
+ # Reopens ActiveAdmin::Views::Pages::Base to add a dismiss ("x")
7
+ # button to each flash message, and to render the configured
8
+ # auto-dismiss delay as a data attribute prism.js reads at runtime
9
+ # (see ActiveAdminPrism::Configuration#flash_dismissible /
10
+ # #flash_auto_dismiss / #flash_auto_dismiss_seconds).
11
+ #
12
+ # The stock #build_flash_messages just renders
13
+ # `div message, class: "flash flash_#{type}"` — no room for a button.
14
+ # When flash_dismissible is false, that exact stock markup is what
15
+ # this renders; only when it's true (the default) does the message
16
+ # get wrapped with a dismiss button alongside it.
17
+ #
18
+ # Auto-dismiss timing and the click handler for the dismiss button
19
+ # both live in active_admin_prism/prism.js (CSS alone can't
20
+ # remove an element from the DOM or run a timer).
21
+ class Base
22
+ def build_flash_messages
23
+ config = ActiveAdminPrism.configuration
24
+ flashes_data = { "prism-transition-ms": config.flash_transition_ms.to_i }
25
+ if config.flash_auto_dismiss
26
+ flashes_data[:"prism-auto-dismiss-ms"] = (config.flash_auto_dismiss_seconds.to_f * 1000).round
27
+ end
28
+
29
+ div class: "flashes", data: flashes_data do
30
+ flash_messages.each do |type, messages|
31
+ [*messages].each do |message|
32
+ if config.flash_dismissible
33
+ div class: "flash flash_#{type}" do
34
+ span message, class: "prism-flash-message"
35
+ button class: "prism-flash-dismiss", type: "button",
36
+ "aria-label": "Dismiss", data: { "prism-flash-dismiss": true } do
37
+ text_node ActiveAdminPrism::Icons.svg(:x, css_class: "prism-flash-dismiss-icon", size: 14).html_safe
38
+ end
39
+ end
40
+ else
41
+ div message, class: "flash flash_#{type}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ # Read by prism.js / scss-src's CSS to decide whether to route
49
+ # data-confirm links through the styled dialog, whether the Filters
50
+ # sidebar panel collapses to an icon, and whether ActiveAdmin's
51
+ # original #footer should stay hidden (see
52
+ # ActiveAdminPrism::Configuration #styled_confirms /
53
+ # #collapsible_filters / #sidebar_footer).
54
+ #
55
+ # Can't call `super` here — #body_classes is defined directly on
56
+ # this same class (not a superclass), so reopening and redefining
57
+ # it replaces that definition outright; there's no ancestor
58
+ # implementation left for `super` to reach; (Arbre::Element#method_missing
59
+ # would silently redirect it right back to this same method,
60
+ # recursing forever). The body has to be copied instead.
61
+ def body_classes
62
+ config = ActiveAdminPrism.configuration
63
+ classes = Arbre::HTML::ClassList.new [
64
+ params[:action],
65
+ params[:controller].tr("/", "_"),
66
+ "active_admin", "logged_in",
67
+ active_admin_namespace.name.to_s + "_namespace"
68
+ ]
69
+ classes << "prism-styled-confirms-disabled" unless config.styled_confirms
70
+ classes << "prism-filters-collapsible-disabled" unless config.collapsible_filters
71
+ classes << "prism-sidebar-footer-disabled" unless config.sidebar_footer
72
+ classes
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Views
5
+ class IndexAsTable
6
+ class IndexTableFor
7
+ # ActiveAdmin's default View/Edit/Delete row links (built by
8
+ # IndexTableFor#defaults) already carry distinct classes —
9
+ # "view_link", "edit_link", "delete_link" — specifically so themes
10
+ # can hook into them. This swaps their text label for an icon +
11
+ # a visually-hidden label (kept for screen readers/tooltips),
12
+ # without touching the url/method/data-confirm behavior at all.
13
+ # Any other custom `item(...)` call (e.g. from a host's own
14
+ # `actions do |resource| item "Preview", ... end` block) is
15
+ # untouched, since it won't carry one of these classes.
16
+ class TableActions
17
+ ACTION_ICONS = {
18
+ "view_link" => :eye,
19
+ "edit_link" => :pencil,
20
+ "delete_link" => :trash
21
+ }.freeze
22
+
23
+ def item(*args, **kwargs)
24
+ unless ActiveAdminPrism.configuration.colorize_action_icons
25
+ return text_node helpers.link_to(*args, **kwargs)
26
+ end
27
+
28
+ label = args[0]
29
+ url = args[1]
30
+ icon_name = ACTION_ICONS.values_at(*kwargs[:class].to_s.split).compact.first
31
+
32
+ unless icon_name && (icon = ActiveAdminPrism::Icons.svg(icon_name, css_class: "prism-action-icon"))
33
+ return text_node helpers.link_to(*args, **kwargs)
34
+ end
35
+
36
+ content = helpers.safe_join([
37
+ icon.html_safe,
38
+ helpers.content_tag(:span, label, class: "prism-visually-hidden")
39
+ ])
40
+ text_node helpers.link_to(content, url, **kwargs)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end