active_admin_prism 0.1.1 → 0.1.3

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.
@@ -0,0 +1,76 @@
1
+ <%# Overrides ActiveAdmin's own layouts/active_admin_logged_out.html.erb
2
+ (used for every Devise page — sign in, sign up, forgot/reset password,
3
+ resend confirmation, resend unlock) purely to upgrade its flash
4
+ rendering to this gem's toast cards (icon + dismiss button + countdown
5
+ bar), matching the same markup lib/active_admin/views/flash_messages.rb
6
+ renders for logged-in pages. Devise's pages route through this plain
7
+ ERB layout instead of ActiveAdmin::Views::Pages::Base/Arbre at all, so
8
+ that Ruby-side override alone never reaches them — this file is the
9
+ only way to reach them. Everything else here is copied verbatim from
10
+ ActiveAdmin core's own version; only the ".flashes" block differs. %>
11
+ <!DOCTYPE html>
12
+ <html lang="<%=I18n.locale%>">
13
+ <head>
14
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
15
+
16
+ <title><%= [@page_title, ActiveAdmin.application.site_title(self)].compact.join(" | ") %></title>
17
+
18
+ <% ActiveAdmin.application.stylesheets.each do |style, options| %>
19
+ <% if ActiveAdmin.application.use_webpacker %>
20
+ <%= stylesheet_pack_tag style, **options %>
21
+ <% else %>
22
+ <%= stylesheet_link_tag style, **options %>
23
+ <% end %>
24
+ <% end %>
25
+ <% ActiveAdmin.application.javascripts.each do |path, options| %>
26
+ <% if ActiveAdmin.application.use_webpacker %>
27
+ <%= javascript_pack_tag path, **options %>
28
+ <% else %>
29
+ <%= javascript_include_tag path, **options %>
30
+ <% end %>
31
+ <% end %>
32
+
33
+ <%= favicon_link_tag ActiveAdmin.application.favicon if ActiveAdmin.application.favicon %>
34
+
35
+ <% ActiveAdmin.application.meta_tags_for_logged_out_pages.each do |name, content| %>
36
+ <%= tag(:meta, name: name, content: content) %>
37
+ <% end %>
38
+
39
+ <%= csrf_meta_tags %>
40
+ <%= csp_meta_tag %>
41
+ </head>
42
+ <body class="active_admin logged_out <%= controller.action_name %>">
43
+ <div id="wrapper">
44
+
45
+ <div id="content_wrapper">
46
+ <% prism_config = ActiveAdminPrism.configuration %>
47
+ <% prism_auto_dismiss_ms = prism_config.flash_auto_dismiss_ms %>
48
+ <div class="flashes"
49
+ data-prism-transition-ms="<%= prism_config.flash_transition_ms.to_i %>"
50
+ <%= "data-prism-auto-dismiss-ms=\"#{prism_auto_dismiss_ms}\"".html_safe if prism_auto_dismiss_ms %>>
51
+ <% flash_messages.each do |type, message| %>
52
+ <div class="flash flash_<%= type %>"
53
+ <%= "style=\"--prism-flash-duration: #{prism_auto_dismiss_ms}ms\"".html_safe if prism_auto_dismiss_ms %>>
54
+ <%= ActiveAdminPrism::Icons.svg(ActiveAdminPrism::Icons.flash_icon_name(type), css_class: "prism-flash-icon")&.html_safe %>
55
+ <span class="prism-flash-message"><%= message %></span>
56
+ <% if prism_config.flash_dismissible %>
57
+ <button class="prism-flash-dismiss" type="button" aria-label="Dismiss" data-prism-flash-dismiss="true">
58
+ <%= ActiveAdminPrism::Icons.svg(:x, css_class: "prism-flash-dismiss-icon", size: 14)&.html_safe %>
59
+ </button>
60
+ <% end %>
61
+ <% if prism_auto_dismiss_ms %>
62
+ <div class="prism-flash-progress"></div>
63
+ <% end %>
64
+ </div>
65
+ <% end %>
66
+ </div>
67
+ <div id="active_admin_content">
68
+ <%= yield %>
69
+ </div>
70
+ </div>
71
+ <div id="footer">
72
+ <%= yield :footer %>
73
+ </div>
74
+ </div>
75
+ </body>
76
+ </html>
data/js-src/prism.js CHANGED
@@ -75,6 +75,55 @@
75
75
  });
76
76
  })();
77
77
 
78
+ // Desktop sidebar rail-collapse toggle (ActiveAdminPrism::Configuration
79
+ // #sidebar_collapsible) — separate from the mobile off-canvas toggle above:
80
+ // that one slides the whole sidebar on/off screen below 900px; this one
81
+ // shrinks it to a narrow icon-only rail on desktop instead, and the two
82
+ // don't interact (scss-src/_sidebar.scss scopes rail-collapse to nothing in
83
+ // particular, but the mobile media query's own `transform`/`position: fixed`
84
+ // rules take over below 900px regardless of this class). State persists to
85
+ // localStorage under its own key, same convention as the nav-group state
86
+ // above, so a returning visitor keeps their last choice.
87
+ (function () {
88
+ "use strict";
89
+
90
+ var STORAGE_KEY = "prism_sidebar_collapsed";
91
+
92
+ function setCollapsed(toggle, collapsed) {
93
+ document.body.classList.toggle("prism-sidebar-collapsed", collapsed);
94
+ toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
95
+ toggle.setAttribute("aria-label", collapsed ? "Expand sidebar" : "Collapse sidebar");
96
+ try {
97
+ window.localStorage.setItem(STORAGE_KEY, collapsed ? "1" : "0");
98
+ } catch (e) {
99
+ // localStorage unavailable — degrade to in-memory only, no
100
+ // persistence across reloads.
101
+ }
102
+ }
103
+
104
+ document.addEventListener("DOMContentLoaded", function () {
105
+ var toggle = document.querySelector("[data-prism-toggle-sidebar-collapse]");
106
+ if (!toggle) return;
107
+
108
+ var stored;
109
+ try {
110
+ stored = window.localStorage.getItem(STORAGE_KEY);
111
+ } catch (e) {
112
+ stored = null;
113
+ }
114
+ if (stored === "1") setCollapsed(toggle, true);
115
+
116
+ toggle.addEventListener("click", function () {
117
+ setCollapsed(toggle, !document.body.classList.contains("prism-sidebar-collapsed"));
118
+ });
119
+ toggle.addEventListener("keydown", function (event) {
120
+ if (event.key !== "Enter" && event.key !== " ") return;
121
+ event.preventDefault();
122
+ setCollapsed(toggle, !document.body.classList.contains("prism-sidebar-collapsed"));
123
+ });
124
+ });
125
+ })();
126
+
78
127
  // Sidebar menu search box (lib/active_admin/views/prism_sidebar.rb
79
128
  // #render_search_box) — filters the "Pages" nav as the visitor types,
80
129
  // matching an item's own label at ANY nesting depth, not just top-level
@@ -222,7 +271,9 @@
222
271
  });
223
272
  })();
224
273
 
225
- // Flash messages: dismiss button + auto-hide. See
274
+ // Flash messages: dismiss button + auto-hide, toast-style (see
275
+ // scss-src/_base.scss for the floating card layout and the
276
+ // ".prism-flash-progress" countdown bar this keeps in sync with). See
226
277
  // lib/active_admin/views/flash_messages.rb, which renders the
227
278
  // ".prism-flash-dismiss" button markup this responds to, and two data
228
279
  // attributes this reads at runtime so Ruby-side config controls behavior
@@ -249,6 +300,36 @@
249
300
  }, transitionMs);
250
301
  }
251
302
 
303
+ // Hovering a toast pauses both its countdown bar (CSS
304
+ // "animation-play-state: paused" on :hover, in scss-src/_base.scss) and
305
+ // the actual removal timer here, tracking whatever time was left rather
306
+ // than restarting from the full duration — a flash a visitor is
307
+ // mid-read shouldn't silently vanish out from under them just because
308
+ // its timer happened to elapse while their mouse was still over it.
309
+ function armAutoDismiss(flash, totalMs, transitionMs) {
310
+ var remainingMs = totalMs;
311
+ var timer = null;
312
+ var startedAt = null;
313
+
314
+ function start() {
315
+ startedAt = Date.now();
316
+ timer = setTimeout(function () {
317
+ dismiss(flash, transitionMs);
318
+ }, remainingMs);
319
+ }
320
+
321
+ function pause() {
322
+ if (!timer) return;
323
+ clearTimeout(timer);
324
+ timer = null;
325
+ remainingMs -= Date.now() - startedAt;
326
+ }
327
+
328
+ flash.addEventListener("mouseenter", pause);
329
+ flash.addEventListener("mouseleave", start);
330
+ start();
331
+ }
332
+
252
333
  document.addEventListener("DOMContentLoaded", function () {
253
334
  var flashesWrapper = document.querySelector(".flashes");
254
335
  var flashes = document.querySelectorAll(".flashes .flash");
@@ -259,9 +340,7 @@
259
340
 
260
341
  if (autoDismissMs) {
261
342
  flashes.forEach(function (flash) {
262
- setTimeout(function () {
263
- dismiss(flash, transitionMs);
264
- }, autoDismissMs);
343
+ armAutoDismiss(flash, autoDismissMs, transitionMs);
265
344
  });
266
345
  }
267
346
 
@@ -301,18 +380,46 @@
301
380
  // #main_content/#sidebar width reflow off this body class instead.
302
381
  document.body.classList.toggle("prism-filters-open", section.classList.contains("open"));
303
382
 
304
- function toggleOpen() {
305
- var open = section.classList.toggle("open");
383
+ function setOpen(open) {
384
+ section.classList.toggle("open", open);
306
385
  toggle.setAttribute("aria-expanded", open ? "true" : "false");
307
386
  document.body.classList.toggle("prism-filters-open", open);
308
387
  }
309
388
 
389
+ function toggleOpen() {
390
+ setOpen(!section.classList.contains("open"));
391
+ }
392
+
310
393
  toggle.addEventListener("click", toggleOpen);
311
394
  toggle.addEventListener("keydown", function (event) {
312
395
  if (event.key !== "Enter" && event.key !== " ") return;
313
396
  event.preventDefault();
314
397
  toggleOpen();
315
398
  });
399
+
400
+ // The active-filters bar (see lib/active_admin/views/active_filters_bar.rb,
401
+ // ActiveAdminPrism::Configuration#active_filters_bar) is a shortcut
402
+ // straight to the Filters form itself — clicking anywhere on it always
403
+ // *opens* the panel (never toggles it closed; that's still the
404
+ // funnel icon's job) and scrolls it into view, since #sidebar isn't
405
+ // sticky the way the left nav is and can scroll out of view on a
406
+ // long table.
407
+ var activeFiltersBar = document.getElementById("prism_active_filters_bar");
408
+ if (activeFiltersBar) {
409
+ activeFiltersBar.setAttribute("role", "button");
410
+ activeFiltersBar.setAttribute("tabindex", "0");
411
+ activeFiltersBar.setAttribute("aria-label", "Show filters");
412
+
413
+ var openFromBar = function (event) {
414
+ if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") return;
415
+ if (event.type === "keydown") event.preventDefault();
416
+ setOpen(true);
417
+ section.scrollIntoView({ behavior: "smooth", block: "nearest" });
418
+ };
419
+
420
+ activeFiltersBar.addEventListener("click", openFromBar);
421
+ activeFiltersBar.addEventListener("keydown", openFromBar);
422
+ }
316
423
  });
317
424
  })();
318
425
 
@@ -365,3 +472,31 @@
365
472
  });
366
473
  });
367
474
  })();
475
+
476
+ // Auto-enhances every plain <select> ActiveAdmin renders (filters, form
477
+ // inputs, association pickers) into a searchable Select2 widget when
478
+ // ActiveAdminPrism.configuration.select2 is true (the
479
+ // "prism-select2-enabled" body class, set in
480
+ // lib/active_admin/views/flash_messages.rb#body_classes). Select2 itself
481
+ // (vendor/select2/select2.full.min.js, MIT licensed) is concatenated just
482
+ // above this file in the compiled asset (see bin/build-js) — turning this
483
+ // flag on needs no host-side JS/npm setup of its own. Skips any <select>
484
+ // a host already initialized manually (the per-field
485
+ // input_html: { class: "..." } + own .select2() call approach predating
486
+ // this flag, still documented in INTEGRATION.md) so the two can coexist
487
+ // without double-initializing the same element.
488
+ (function () {
489
+ "use strict";
490
+
491
+ document.addEventListener("DOMContentLoaded", function () {
492
+ if (!document.body.classList.contains("prism-select2-enabled")) return;
493
+ if (typeof jQuery === "undefined" || !jQuery.fn.select2) return;
494
+
495
+ var $ = jQuery;
496
+ $("select").each(function () {
497
+ var $select = $(this);
498
+ if ($select.data("select2")) return;
499
+ $select.select2({ width: "100%" });
500
+ });
501
+ });
502
+ })();
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Filters
5
+ # ActiveAdmin core auto-registers this sidebar section on every resource
6
+ # (see active_admin/filters/resource_extension.rb#add_search_status_sidebar_section)
7
+ # to summarize the current scope + active filters inside #sidebar,
8
+ # alongside the Filters form. Skip it entirely when
9
+ # ActiveAdminPrism.configuration.active_filters_bar is on — see
10
+ # active_filters_bar.rb's Pages::Index override, which renders the same
11
+ # content next to Batch Actions instead.
12
+ class ActiveSidebar
13
+ def sidebar_options
14
+ {
15
+ only: :index,
16
+ if: -> {
17
+ active_admin_config.current_filters_enabled? &&
18
+ (params[:q] || params[:scope]) &&
19
+ !ActiveAdminPrism.configuration.active_filters_bar
20
+ }
21
+ }
22
+ end
23
+ end
24
+ end
25
+
26
+ module Views
27
+ module Pages
28
+ # Relocates the "Search status" summary (current scope + active
29
+ # filters — AA core's ActiveAdmin::Filters::ActiveSidebar, reopened
30
+ # above to skip its normal #sidebar rendering when this is on) into
31
+ # the same row as the Batch Actions button/scopes tabs (AA core's own
32
+ # ".table_tools", rendered by Pages::Index#build_table_tools) instead
33
+ # of ActiveAdmin's own #sidebar.
34
+ #
35
+ # Why not #sidebar: ActiveAdminPrism.configuration.collapsible_filters
36
+ # squeezes the *entire* #sidebar column down to a 72px icon gutter
37
+ # while the Filters form is collapsed (scss-src/_panels.scss) — every
38
+ # child of #sidebar shrinks with it, this section included. Its
39
+ # content (one "field contains value" sentence per active filter) has
40
+ # nowhere to fit in 72px and overflows out over the table instead of
41
+ # shrinking cleanly.
42
+ #
43
+ # ActiveAdminPrism::Configuration#active_filters_bar controls this —
44
+ # false restores ActiveAdmin's stock behavior (rendered inside
45
+ # #sidebar, squeezed along with it when collapsible_filters is on).
46
+ class Index
47
+ # AA core's own #build_table_tools (active_admin/views/pages/index.rb)
48
+ # only renders ".table_tools" at all `if any_table_tools?` (batch
49
+ # actions, scopes, or multiple index presenters) — grouping the
50
+ # controls it does render into their own ".table_tools_actions" wrapper
51
+ # (a new element, not AA's) is what lets scss-src/_panels.scss push
52
+ # this bar to the opposite end of the same flex row via
53
+ # `justify-content: space-between` regardless of whether that group
54
+ # is present at all, rather than only ever being able to sit beside
55
+ # it when it happens to exist.
56
+ def build_table_tools
57
+ show_bar = active_filters_bar?
58
+ return unless any_table_tools? || show_bar
59
+
60
+ div class: "table_tools" do
61
+ if any_table_tools?
62
+ div class: "table_tools_actions" do
63
+ build_batch_actions_selector
64
+ build_scopes
65
+ build_index_list
66
+ end
67
+ end
68
+ build_active_filters_bar if show_bar
69
+ end
70
+ end
71
+
72
+ def active_filters_bar?
73
+ return false unless ActiveAdminPrism.configuration.active_filters_bar
74
+ return false unless active_admin_config
75
+ # active_admin_config should always be an ActiveAdmin::Resource here
76
+ # (only a Resource's :index action ever reaches Pages::Index), but
77
+ # #current_filters_enabled? is defined via Filters::ResourceExtension
78
+ # rather than on ActiveAdmin::Config itself — guard rather than
79
+ # assume, the same defensive style as the rest of this gem.
80
+ return false unless active_admin_config.respond_to?(:current_filters_enabled?)
81
+ return false unless active_admin_config.current_filters_enabled?
82
+
83
+ (params[:q] || params[:scope]).present?
84
+ end
85
+
86
+ def build_active_filters_bar
87
+ div id: "prism_active_filters_bar", class: "prism-active-filters-bar" do
88
+ active_filters_sidebar_content
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -3,17 +3,21 @@
3
3
  module ActiveAdmin
4
4
  module Views
5
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 /
6
+ # Reopens ActiveAdmin::Views::Pages::Base to render flash messages as
7
+ # floating toast cards (icon + message + dismiss button + a
8
+ # countdown progress bar) instead of ActiveAdmin's plain inline
9
+ # banner, and to render the configured auto-dismiss delay as a data
10
+ # attribute prism.js reads at runtime (see
11
+ # ActiveAdminPrism::Configuration#flash_dismissible /
10
12
  # #flash_auto_dismiss / #flash_auto_dismiss_seconds).
11
13
  #
12
14
  # 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.
15
+ # `div message, class: "flash flash_#{type}"` — no room for an icon,
16
+ # a button, or a progress bar. When flash_dismissible is false, the
17
+ # dismiss button is dropped; the icon and (if flash_auto_dismiss is
18
+ # on) the progress bar still render either way — see
19
+ # scss-src/_base.scss for the toast styling/positioning and the
20
+ # progress bar's CSS animation.
17
21
  #
18
22
  # Auto-dismiss timing and the click handler for the dismiss button
19
23
  # both live in active_admin_prism/prism.js (CSS alone can't
@@ -22,35 +26,50 @@ module ActiveAdmin
22
26
  def build_flash_messages
23
27
  config = ActiveAdminPrism.configuration
24
28
  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
29
+ auto_dismiss_ms = config.flash_auto_dismiss_ms
30
+ flashes_data[:"prism-auto-dismiss-ms"] = auto_dismiss_ms if auto_dismiss_ms
28
31
 
29
32
  div class: "flashes", data: flashes_data do
30
33
  flash_messages.each do |type, messages|
31
34
  [*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
35
+ build_flash(type, message, config, auto_dismiss_ms)
43
36
  end
44
37
  end
45
38
  end
46
39
  end
47
40
 
41
+ def build_flash(type, message, config, auto_dismiss_ms)
42
+ flash_options = { class: "flash flash_#{type}" }
43
+ flash_options[:style] = "--prism-flash-duration: #{auto_dismiss_ms}ms" if auto_dismiss_ms
44
+
45
+ div flash_options do
46
+ text_node flash_icon(type)
47
+ span message, class: "prism-flash-message"
48
+ if config.flash_dismissible
49
+ button class: "prism-flash-dismiss", type: "button",
50
+ "aria-label": "Dismiss", data: { "prism-flash-dismiss": true } do
51
+ text_node ActiveAdminPrism::Icons.svg(:x, css_class: "prism-flash-dismiss-icon", size: 14).html_safe
52
+ end
53
+ end
54
+ div class: "prism-flash-progress" if auto_dismiss_ms
55
+ end
56
+ end
57
+
58
+ # See lib/prism_icons.rb's ActiveAdminPrism::Icons.flash_icon_name —
59
+ # shared with the plain-ERB logged-out layout, which needs the same
60
+ # mapping but can't reach this Arbre-only method.
61
+ def flash_icon(type)
62
+ name = ActiveAdminPrism::Icons.flash_icon_name(type)
63
+ (ActiveAdminPrism::Icons.svg(name, css_class: "prism-flash-icon") || "").html_safe
64
+ end
65
+
48
66
  # Read by prism.js / scss-src's CSS to decide whether to route
49
67
  # 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
68
+ # sidebar panel collapses to an icon, whether ActiveAdmin's
69
+ # original #footer should stay hidden, and whether every plain
70
+ # <select> should be auto-enhanced into Select2 (see
52
71
  # ActiveAdminPrism::Configuration #styled_confirms /
53
- # #collapsible_filters / #sidebar_footer).
72
+ # #collapsible_filters / #sidebar_footer / #select2).
54
73
  #
55
74
  # Can't call `super` here — #body_classes is defined directly on
56
75
  # this same class (not a superclass), so reopening and redefining
@@ -69,6 +88,7 @@ module ActiveAdmin
69
88
  classes << "prism-styled-confirms-disabled" unless config.styled_confirms
70
89
  classes << "prism-filters-collapsible-disabled" unless config.collapsible_filters
71
90
  classes << "prism-sidebar-footer-disabled" unless config.sidebar_footer
91
+ classes << "prism-select2-enabled" if config.select2
72
92
  classes
73
93
  end
74
94
  end
@@ -40,7 +40,10 @@ module ActiveAdmin
40
40
  end
41
41
 
42
42
  div class: "prism-sidebar-inner" do
43
- div(class: "prism-sidebar-brand") { site_title @namespace }
43
+ div(class: "prism-sidebar-brand") do
44
+ render_collapse_toggle if ActiveAdminPrism.configuration.sidebar_collapsible
45
+ site_title @namespace
46
+ end
44
47
  render_language_switcher if ActiveAdminPrism.configuration.language_switcher
45
48
  render_search_box if ActiveAdminPrism.configuration.menu_search
46
49
  div(class: "prism-sidebar-scroll") do
@@ -79,6 +82,27 @@ module ActiveAdmin
79
82
  ).html_safe
80
83
  end
81
84
 
85
+ # A hamburger icon button at the top of the sidebar, beside the brand
86
+ # — the same :menu icon the mobile off-canvas toggle uses, so both
87
+ # read as "the sidebar toggle" at a glance even though they're two
88
+ # separate buttons for two separate behaviors (this one only ever
89
+ # renders/matters at desktop widths; see scss-src/_sidebar.scss).
90
+ # Toggles the whole sidebar between full width and a narrow
91
+ # icon-only rail — see ActiveAdminPrism::Configuration
92
+ # #sidebar_collapsible and js-src/prism.js for the click handler
93
+ # (persists to localStorage) and scss-src/_sidebar.scss for the
94
+ # collapsed-state styling.
95
+ def render_collapse_toggle
96
+ span(
97
+ class: "prism-sidebar-collapse-toggle",
98
+ data: { "prism-toggle-sidebar-collapse": true },
99
+ role: "button", tabindex: "0", "aria-expanded": "true",
100
+ "aria-label": "Collapse sidebar"
101
+ ) do
102
+ text_node ActiveAdminPrism::Icons.svg(:menu, css_class: "prism-sidebar-collapse-icon").html_safe
103
+ end
104
+ end
105
+
82
106
  # A small "Languages" dropdown near the top of the sidebar — see
83
107
  # ActiveAdminPrism::Configuration#language_switcher/#languages. Built
84
108
  # here directly (rather than via the `admin.build_menu
@@ -191,7 +215,12 @@ module ActiveAdmin
191
215
  end
192
216
 
193
217
  def render_group_toggle(item, label)
194
- span(class: "prism-nav-group-toggle", data: { "prism-toggle": true }) do
218
+ # title: a plain native tooltip inert most of the time (the
219
+ # sidebar already shows this label as text), but the only way to
220
+ # identify this item by hover once ActiveAdminPrism::Configuration
221
+ # #sidebar_collapsible has shrunk the sidebar to its icon-only rail
222
+ # and .prism-nav-label itself is hidden (see scss-src/_sidebar.scss).
223
+ span(class: "prism-nav-group-toggle", data: { "prism-toggle": true }, title: label) do
195
224
  text_node icon_for(item)
196
225
  span(label, class: "prism-nav-label")
197
226
  text_node chevron_svg
@@ -201,11 +230,14 @@ module ActiveAdmin
201
230
  def render_leaf(item, label, url)
202
231
  link_options = item.html_options.except(:icon)
203
232
  link_options[:class] = ["prism-nav-link", link_options[:class]].compact.join(" ")
233
+ # Same rail-collapse tooltip as render_group_toggle above — a host's
234
+ # own explicit :title (via html_options) wins over the label.
235
+ link_options[:title] ||= label
204
236
 
205
237
  if real_url?(url)
206
238
  text_node helpers.link_to(link_body(item, label), url, **link_options)
207
239
  else
208
- span(class: "prism-nav-link") { link_body(item, label) }
240
+ span(class: "prism-nav-link", title: label) { link_body(item, label) }
209
241
  end
210
242
  end
211
243
 
@@ -149,6 +149,56 @@ module ActiveAdminPrism
149
149
  # all.
150
150
  attr_accessor :menu_search
151
151
 
152
+ # Whether every plain <select> ActiveAdmin renders (filters, form
153
+ # inputs, association pickers — anything, no per-field opt-in needed)
154
+ # is automatically enhanced into a searchable Select2 widget. false
155
+ # (the default) leaves every select exactly as ActiveAdmin renders it;
156
+ # you can still opt individual fields into Select2 yourself either way
157
+ # (see INTEGRATION.md's Select2 section) — this flag only controls the
158
+ # blanket, no-setup-needed behavior. Select2 itself (JS + its own base
159
+ # CSS) ships vendored inside this gem's assets either way — turning
160
+ # this on requires no extra gem/npm dependency or JS of your own;
161
+ # see js-src/prism.js and vendor/select2/.
162
+ attr_accessor :select2
163
+
164
+ # Whether the "current scope + active filters" summary (AA core's own
165
+ # "Search status" sidebar section, auto-added to every resource) renders
166
+ # as a compact bar above the index table instead of inside #sidebar.
167
+ # true (the default) avoids it getting squeezed/overflowing alongside
168
+ # the Filters panel when collapsible_filters is on (see
169
+ # lib/active_admin/views/active_filters_bar.rb). false restores
170
+ # ActiveAdmin's stock behavior — rendered inside #sidebar as its own
171
+ # panel, above/below the Filters form depending on registration order.
172
+ attr_accessor :active_filters_bar
173
+
174
+ # Whether a toggle button renders at the bottom of the Prism sidebar
175
+ # (below the nav/account area) that collapses it to a narrow icon-only
176
+ # rail on desktop, expanding back to full width (with labels) on
177
+ # another click — state persists across page loads via localStorage,
178
+ # the same convention already used for per-group nav expand/collapse
179
+ # state (see lib/active_admin/views/prism_sidebar.rb and js-src/prism.js).
180
+ # Purely a desktop feature: the existing off-canvas mobile toggle
181
+ # (below 900px) is unaffected either way. false renders no toggle at
182
+ # all — the sidebar is always full width, matching this gem's behavior
183
+ # before this flag existed.
184
+ attr_accessor :sidebar_collapsible
185
+
186
+ # Milliseconds equivalent of #flash_auto_dismiss_seconds, or nil when
187
+ # #flash_auto_dismiss is off — the exact number both flash-rendering
188
+ # paths this gem ships (lib/active_admin/views/flash_messages.rb's
189
+ # Arbre override for logged-in pages, and the plain-ERB
190
+ # app/views/layouts/active_admin_logged_out.html.erb for Devise's
191
+ # sign-in/password/etc pages) need to render identically: as the
192
+ # "data-prism-auto-dismiss-ms" attribute js-src/prism.js reads to
193
+ # schedule removal, and as each flash's own
194
+ # "--prism-flash-duration" inline style the countdown bar
195
+ # (scss-src/_base.scss's ".prism-flash-progress") animates against.
196
+ def flash_auto_dismiss_ms
197
+ return nil unless flash_auto_dismiss
198
+
199
+ (flash_auto_dismiss_seconds.to_f * 1000).round
200
+ end
201
+
152
202
  def initialize
153
203
  @sidebar = true
154
204
  @colorize_action_icons = true
@@ -170,6 +220,9 @@ module ActiveAdminPrism
170
220
  { label: "Français", locale: :fr }
171
221
  ]
172
222
  @menu_search = true
223
+ @select2 = false
224
+ @active_filters_bar = true
225
+ @sidebar_collapsible = true
173
226
  end
174
227
  end
175
228
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveAdminPrism
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -12,6 +12,7 @@ require_relative "active_admin/views/prism_sidebar"
12
12
  require_relative "active_admin/views/index_table_actions"
13
13
  require_relative "active_admin/views/flash_messages"
14
14
  require_relative "active_admin/views/filters_sidebar"
15
+ require_relative "active_admin/views/active_filters_bar"
15
16
  require_relative "formtastic/inputs/prism_toggle_input"
16
17
 
17
18
  module ActiveAdminPrism
data/lib/prism_icons.rb CHANGED
@@ -36,7 +36,11 @@ module ActiveAdminPrism
36
36
  '<path d="M10 11v6M14 11v6"/>',
37
37
  filter: '<path d="M4 5h16l-6.5 7.5v6l-3 1.5v-7.5Z"/>',
38
38
  globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/>' \
39
- '<path d="M12 3c2.5 2.4 4 5.6 4 9s-1.5 6.6-4 9c-2.5-2.4-4-5.6-4-9s1.5-6.6 4-9Z"/>'
39
+ '<path d="M12 3c2.5 2.4 4 5.6 4 9s-1.5 6.6-4 9c-2.5-2.4-4-5.6-4-9s1.5-6.6 4-9Z"/>',
40
+ check_circle: '<circle cx="12" cy="12" r="9"/><path d="M8 12.5l2.5 2.5L16 9"/>',
41
+ alert_circle: '<circle cx="12" cy="12" r="9"/><path d="M12 7.5v6"/><circle cx="12" cy="16.5" r="0.1" fill="currentColor" stroke-width="2.5"/>',
42
+ alert_triangle: '<path d="M12 4 2.5 20h19L12 4Z"/><path d="M12 10v4.5"/><circle cx="12" cy="17.2" r="0.1" fill="currentColor" stroke-width="2.5"/>',
43
+ info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5.5"/><circle cx="12" cy="7.8" r="0.1" fill="currentColor" stroke-width="2.5"/>'
40
44
  }.freeze
41
45
 
42
46
  # Returns an inline <svg>...</svg> string for `name`, or nil if unknown.
@@ -49,6 +53,27 @@ module ActiveAdminPrism
49
53
  %(stroke-linejoin="round" aria-hidden="true">#{inner}</svg>)
50
54
  end
51
55
 
56
+ # Maps a flash's key (Rails' own :notice/:alert conventions, plus
57
+ # :error/:warning/:success/:info as an app might set directly) to one
58
+ # of the four icons above — anything else (a host's own custom flash
59
+ # key) falls back to the neutral :info icon rather than none at all.
60
+ # Shared by both flash-rendering paths this gem ships: the Arbre-based
61
+ # one for logged-in pages (lib/active_admin/views/flash_messages.rb)
62
+ # and the plain-ERB logged-out layout (app/views/layouts/
63
+ # active_admin_logged_out.html.erb) — Devise's sign-in/password/etc
64
+ # pages render through a completely separate layout ActiveAdmin
65
+ # itself ships, not through Pages::Base/Arbre at all, so that
66
+ # Ruby-side override alone never reaches them.
67
+ FLASH_ICONS = {
68
+ notice: :check_circle, success: :check_circle,
69
+ alert: :alert_triangle, warning: :alert_triangle,
70
+ error: :alert_circle
71
+ }.freeze
72
+
73
+ def self.flash_icon_name(type)
74
+ FLASH_ICONS[type.to_sym] || :info
75
+ end
76
+
52
77
  # The default sign-in page mark (see ActiveAdminPrism::Configuration
53
78
  # #login_logo) — a generic glass-prism-with-spectrum motif distinct from
54
79
  # the 24x24 line-icon set above (bigger, multi-color, animated via