modal_stack 0.2.0 → 0.4.1

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/README.md +136 -52
  4. data/app/assets/javascripts/modal_stack.js +612 -63
  5. data/app/assets/stylesheets/modal_stack/bootstrap.css +120 -11
  6. data/app/assets/stylesheets/modal_stack/{tailwind.css → tailwind_v3.css} +82 -14
  7. data/app/assets/stylesheets/modal_stack/tailwind_v4.css +372 -0
  8. data/app/assets/stylesheets/modal_stack/vanilla.css +128 -11
  9. data/app/javascript/modal_stack/controllers/modal_stack_back_link_controller.js +32 -0
  10. data/app/javascript/modal_stack/controllers/modal_stack_controller.js +54 -0
  11. data/app/javascript/modal_stack/controllers/modal_stack_link_controller.js +29 -7
  12. data/app/javascript/modal_stack/install.js +7 -1
  13. data/app/javascript/modal_stack/orchestrator.js +132 -3
  14. data/app/javascript/modal_stack/orchestrator.test.js +264 -2
  15. data/app/javascript/modal_stack/runtime.js +222 -13
  16. data/app/javascript/modal_stack/runtime.test.js +151 -0
  17. data/app/javascript/modal_stack/state.js +338 -39
  18. data/app/javascript/modal_stack/state.test.js +400 -13
  19. data/app/views/modal_stack/_dialog.html.erb +1 -0
  20. data/app/views/modal_stack/_panel.html.erb +4 -0
  21. data/lib/generators/modal_stack/install/install_generator.rb +18 -4
  22. data/lib/generators/modal_stack/install/templates/initializer.rb +21 -5
  23. data/lib/generators/modal_stack/views/templates/_dialog.html.erb +9 -0
  24. data/lib/generators/modal_stack/views/templates/_panel.html.erb +18 -0
  25. data/lib/generators/modal_stack/views/views_generator.rb +50 -0
  26. data/lib/modal_stack/capybara.rb +21 -0
  27. data/lib/modal_stack/configuration.rb +43 -17
  28. data/lib/modal_stack/controller_extensions.rb +8 -1
  29. data/lib/modal_stack/engine.rb +2 -0
  30. data/lib/modal_stack/helpers/modal_back_link_helper.rb +48 -0
  31. data/lib/modal_stack/helpers/modal_stack_assets_helper.rb +15 -3
  32. data/lib/modal_stack/helpers/modal_stack_container_helper.rb +42 -15
  33. data/lib/modal_stack/turbo_streams_extension.rb +56 -0
  34. data/lib/modal_stack/version.rb +1 -1
  35. data/lib/modal_stack.rb +5 -1
  36. metadata +11 -3
@@ -0,0 +1,18 @@
1
+ <%# modal_stack panel template — override this file to customise the HTML
2
+ structure that wraps every modal/drawer/bottom-sheet/confirmation panel.
3
+ Available locals:
4
+ content — SafeBuffer, the yielded view content
5
+ back_button — SafeBuffer | nil, pre-rendered back button (nil when back: false)
6
+ wrapper_attrs — Hash { class:, data:, ...extras } — must stay on the root element
7
+ so the JS runtime can read its data attributes
8
+ size — Symbol :sm | :md | :lg | :xl
9
+ variant — Symbol :modal | :drawer | :bottom_sheet | :confirmation
10
+ dismissible — Boolean
11
+ side — Symbol | nil :left | :right | :top | :bottom
12
+ width — String | nil CSS value (e.g. "42rem")
13
+ height — String | nil CSS value
14
+ transition — Symbol | nil :slide | :fade | :none %>
15
+ <%= content_tag(:div, wrapper_attrs) do %>
16
+ <%= back_button %>
17
+ <%= content %>
18
+ <% end %>
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/base"
5
+
6
+ module ModalStack
7
+ module Generators
8
+ class ViewsGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ class_option :panel, type: :boolean, default: false,
12
+ desc: "Copy only the panel partial"
13
+ class_option :dialog, type: :boolean, default: false,
14
+ desc: "Copy only the dialog partial"
15
+
16
+ def copy_views
17
+ if options[:panel]
18
+ copy_panel
19
+ elsif options[:dialog]
20
+ copy_dialog
21
+ else
22
+ copy_panel
23
+ copy_dialog
24
+ end
25
+ end
26
+
27
+ def show_readme
28
+ say <<~TXT, :green
29
+
30
+ modal_stack views ejected to app/views/modal_stack/.
31
+
32
+ Edit the copied partials to override the default HTML structure.
33
+ The `wrapper_attrs` / `dialog_attrs` locals carry the required
34
+ data attributes — keep them on the root element so the JS runtime
35
+ continues to work.
36
+ TXT
37
+ end
38
+
39
+ private
40
+
41
+ def copy_panel
42
+ copy_file "_panel.html.erb", "app/views/modal_stack/_panel.html.erb"
43
+ end
44
+
45
+ def copy_dialog
46
+ copy_file "_dialog.html.erb", "app/views/modal_stack/_dialog.html.erb"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -14,6 +14,7 @@ module ModalStack
14
14
  module Capybara
15
15
  DIALOG_SELECTOR = "#modal-stack-root"
16
16
  LAYER_SELECTOR = '[data-modal-stack-target="layer"]:not([data-leaving])'
17
+ FRAME_SELECTOR = "[data-modal-stack-frame]:not([data-leaving])"
17
18
 
18
19
  # Scope Capybara matchers to a specific layer of the stack.
19
20
  #
@@ -81,5 +82,25 @@ module ModalStack
81
82
  def modal_stack_depth
82
83
  ::Capybara.current_session.all(:css, LAYER_SELECTOR, wait: 0).size
83
84
  end
85
+
86
+ # Capybara matcher: assert the top layer's path frame depth.
87
+ #
88
+ # expect(page).to have_modal_frames(2)
89
+ #
90
+ # Layers without a path read as a single frame.
91
+ def have_modal_frames(count, **)
92
+ have_css("#{LAYER_SELECTOR}[data-frame-depth=\"#{count}\"]", **)
93
+ end
94
+
95
+ # Scope Capybara to the *current* frame inside the top (or specified)
96
+ # layer — i.e. the frame that's not animating out.
97
+ def within_modal_frame(depth: nil, **, &)
98
+ layers = ::Capybara.current_session.all(:css, LAYER_SELECTOR, minimum: 1, **)
99
+ layer = depth ? layers[depth - 1] : layers.last
100
+ raise ::Capybara::ElementNotFound, "no modal_stack layer at depth #{depth}" unless layer
101
+
102
+ frame = layer.first(:css, FRAME_SELECTOR, minimum: 1, **)
103
+ ::Capybara.current_session.within(frame, &)
104
+ end
84
105
  end
85
106
  end
@@ -11,11 +11,16 @@ module ModalStack
11
11
  # end
12
12
  #
13
13
  class Configuration
14
- CSS_PROVIDERS = %i[tailwind bootstrap vanilla none].freeze
14
+ CSS_PROVIDERS = %i[tailwind_v3 tailwind_v4 bootstrap vanilla none].freeze
15
+ # Aliases accepted on input, normalized to a canonical CSS_PROVIDERS value.
16
+ # `:tailwind` predates the v3/v4 split — keep it working, map to v3 (no change
17
+ # in rendered CSS for existing apps).
18
+ CSS_PROVIDER_ALIASES = { tailwind: :tailwind_v3 }.freeze
15
19
  ASSETS_MODES = %i[importmap jsbundling sprockets auto].freeze
16
20
  VARIANTS = %i[modal drawer bottom_sheet confirmation].freeze
17
21
  SIZES = %i[sm md lg xl].freeze
18
22
  MAX_DEPTH_STRATEGIES = %i[raise warn silent].freeze
23
+ PATH_TRANSITIONS = %i[slide fade none].freeze
19
24
 
20
25
  attr_accessor :default_classes,
21
26
  :request_header,
@@ -33,29 +38,18 @@ module ModalStack
33
38
  :default_size,
34
39
  :default_dismissible,
35
40
  :max_depth,
36
- :max_depth_strategy
41
+ :max_depth_strategy,
42
+ :default_path_transition
37
43
 
38
44
  def initialize
39
- @css_provider = :tailwind
40
- @assets_mode = :auto
41
- @default_variant = :modal
42
- @default_size = :md
43
- @default_dismissible = true
44
- @max_depth = 5
45
- @max_depth_strategy = :warn
46
- @request_header = "X-Modal-Stack-Request"
47
- @dialog_id = "modal-stack-root"
48
- @stack_root_data_attribute = "modal-stack"
49
- @respect_reduced_motion = true
50
- @replace_turbo_confirm = false
51
- @i18n_scope = "modal_stack"
52
- @initializer_version = nil
53
- @silence_initializer_warning = false
45
+ apply_behavior_defaults
46
+ apply_naming_defaults
54
47
  @default_classes = default_classes_hash
55
48
  end
56
49
 
57
50
  def css_provider=(value)
58
51
  value = value.to_sym
52
+ value = CSS_PROVIDER_ALIASES.fetch(value, value)
59
53
  raise ArgumentError, "css_provider must be one of #{CSS_PROVIDERS.inspect}, got #{value.inspect}" unless CSS_PROVIDERS.include?(value)
60
54
 
61
55
  @css_provider = value
@@ -110,8 +104,40 @@ module ModalStack
110
104
  @max_depth_strategy = value
111
105
  end
112
106
 
107
+ def default_path_transition=(value)
108
+ value = value.to_sym
109
+ unless PATH_TRANSITIONS.include?(value)
110
+ raise ArgumentError,
111
+ "default_path_transition must be one of #{PATH_TRANSITIONS.inspect}, got #{value.inspect}"
112
+ end
113
+
114
+ @default_path_transition = value
115
+ end
116
+
113
117
  private
114
118
 
119
+ def apply_behavior_defaults
120
+ @css_provider = :tailwind_v3
121
+ @assets_mode = :auto
122
+ @default_variant = :modal
123
+ @default_size = :md
124
+ @default_dismissible = true
125
+ @max_depth = 5
126
+ @max_depth_strategy = :warn
127
+ @default_path_transition = :slide
128
+ @respect_reduced_motion = true
129
+ @replace_turbo_confirm = false
130
+ end
131
+
132
+ def apply_naming_defaults
133
+ @request_header = "X-Modal-Stack-Request"
134
+ @dialog_id = "modal-stack-root"
135
+ @stack_root_data_attribute = "modal-stack"
136
+ @i18n_scope = "modal_stack"
137
+ @initializer_version = nil
138
+ @silence_initializer_warning = false
139
+ end
140
+
115
141
  def default_classes_hash
116
142
  {
117
143
  modal_panel: nil,
@@ -5,7 +5,7 @@ module ModalStack
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- helper_method :modal_stack_request?
8
+ helper_method :modal_stack_request?, :modal_stack_config
9
9
  end
10
10
 
11
11
  class_methods do
@@ -39,6 +39,13 @@ module ModalStack
39
39
  end
40
40
  end
41
41
 
42
+ # Request-scoped accessor for `ModalStack.configuration`. Memoized so
43
+ # helpers that read several config values per render hit the global
44
+ # singleton once instead of N times.
45
+ def modal_stack_config
46
+ @modal_stack_config ||= ModalStack.configuration
47
+ end
48
+
42
49
  # True when the current request was issued by the modal_stack JS runtime
43
50
  # (signaled by the X-Modal-Stack-Request header on the fetch).
44
51
  def modal_stack_request?
@@ -9,9 +9,11 @@ module ModalStack
9
9
  require "modal_stack/helpers/modal_link_helper"
10
10
  require "modal_stack/helpers/modal_stack_container_helper"
11
11
  require "modal_stack/helpers/modal_stack_assets_helper"
12
+ require "modal_stack/helpers/modal_back_link_helper"
12
13
  include ModalStack::Helpers::ModalLinkHelper
13
14
  include ModalStack::Helpers::ModalStackContainerHelper
14
15
  include ModalStack::Helpers::ModalStackAssetsHelper
16
+ include ModalStack::Helpers::ModalBackLinkHelper
15
17
  end
16
18
  end
17
19
 
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModalStack
4
+ module Helpers
5
+ # Renders a button (or link) that steps back through the top layer's
6
+ # path. Wired to the `modal-stack-back-link` Stimulus controller, which
7
+ # delegates to `orchestrator.pathBack`.
8
+ #
9
+ # <%= modal_back_link "Back" %>
10
+ # <%= modal_back_link "Back to step 1", steps: 2 %>
11
+ # <%= modal_back_link class: "btn btn-link" do %>
12
+ # <span aria-hidden="true">←</span> Back
13
+ # <% end %>
14
+ module ModalBackLinkHelper
15
+ LINK_CONTROLLER = "modal-stack-back-link"
16
+ LINK_CLICK_ACTION = "click->modal-stack-back-link#trigger"
17
+
18
+ def modal_back_link(name = nil, options = {}, &block) # rubocop:disable Metrics/CyclomaticComplexity
19
+ if block
20
+ options = name.is_a?(Hash) ? name : {}
21
+ name = capture(&block)
22
+ end
23
+ options = options.dup
24
+ steps = Integer(options.delete(:steps) || 1)
25
+ raise ArgumentError, "steps must be a positive integer, got #{steps.inspect}" if steps < 1
26
+
27
+ options[:data] = back_link_data(options.delete(:data) || {}, steps)
28
+ options[:type] ||= "button"
29
+
30
+ button_tag(name || I18n.t("modal_stack.back", default: "Back"), **options)
31
+ end
32
+
33
+ private
34
+
35
+ def back_link_data(existing, steps)
36
+ existing.merge(
37
+ controller: merged_token(existing[:controller], LINK_CONTROLLER),
38
+ action: merged_token(existing[:action], LINK_CLICK_ACTION),
39
+ modal_stack_back_link_steps_value: steps
40
+ )
41
+ end
42
+
43
+ def merged_token(existing, addition)
44
+ [existing, addition].compact.join(" ").strip
45
+ end
46
+ end
47
+ end
48
+ end
@@ -11,7 +11,7 @@ module ModalStack
11
11
  # Returns an empty SafeBuffer when `config.css_provider = :none`,
12
12
  # so apps can call this unconditionally.
13
13
  def modal_stack_stylesheet_link_tag(**)
14
- provider = ModalStack.configuration.css_provider
14
+ provider = _modal_stack_config.css_provider
15
15
  return ActiveSupport::SafeBuffer.new if provider == :none
16
16
 
17
17
  stylesheet_link_tag("modal_stack/#{provider}", **)
@@ -23,12 +23,12 @@ module ModalStack
23
23
  # <%= modal_stack_dialog_tag %>
24
24
  #
25
25
  def modal_stack_dialog_tag(**html_options)
26
- config = ModalStack.configuration
26
+ config = _modal_stack_config
27
27
  attrs = html_options.dup
28
28
  attrs[:id] ||= config.dialog_id
29
29
  attrs[:data] = build_dialog_data(attrs[:data], config)
30
30
 
31
- content_tag(:dialog, "".html_safe, attrs)
31
+ render partial: "modal_stack/dialog", locals: { dialog_attrs: attrs }
32
32
  end
33
33
 
34
34
  # Merges caller-provided data attrs with the gem-managed ones (controller,
@@ -48,6 +48,18 @@ module ModalStack
48
48
  def modal_stack_javascript_tag(**)
49
49
  ActiveSupport::SafeBuffer.new
50
50
  end
51
+
52
+ private
53
+
54
+ # Prefers the request-scoped accessor injected by ControllerExtensions
55
+ # so we hit `ModalStack.configuration` once per request, not per call.
56
+ # Falls back to the global singleton for non-controller render contexts
57
+ # (mailers, ActionCable, isolated view tests).
58
+ def _modal_stack_config
59
+ return modal_stack_config if respond_to?(:modal_stack_config, true)
60
+
61
+ ModalStack.configuration
62
+ end
51
63
  end
52
64
  end
53
65
  end
@@ -12,24 +12,51 @@ module ModalStack
12
12
  module ModalStackContainerHelper
13
13
  DEFAULT_SIZE = :md
14
14
 
15
- def modal_stack_container(size: DEFAULT_SIZE, dismissible: true, variant: :modal, side: nil, width: nil, height: nil, html: {},
16
- &)
15
+ def modal_stack_container(size: DEFAULT_SIZE, dismissible: true, variant: :modal, side: nil, width: nil, height: nil,
16
+ back: false, transition: nil, html: {}, &)
17
+ attrs = build_panel_attrs(size: size, variant: variant, side: side,
18
+ dismissible: dismissible, width: width,
19
+ height: height, transition: transition, html: html)
20
+ body = capture(&)
21
+ back_html = back ? modal_stack_container_back_button : nil
22
+
23
+ render partial: "modal_stack/panel", locals: {
24
+ content: body, back_button: back_html, wrapper_attrs: attrs,
25
+ size: size, variant: variant, dismissible: dismissible,
26
+ side: side, width: width, height: height, transition: transition
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def build_panel_attrs(size:, variant:, side:, dismissible:, width:, height:, transition:, html:)
17
33
  classes = ["modal-stack__panel", "modal-stack__panel--#{variant}", "modal-stack__panel--size-#{size}"]
18
34
  classes << "modal-stack__panel--side-#{side}" if side
19
35
 
20
- attrs = {
21
- class: [classes, html[:class]].compact.join(" "),
22
- data: {
23
- modal_stack_size: size,
24
- modal_stack_variant: variant,
25
- modal_stack_dismissible: dismissible.to_s,
26
- modal_stack_side: side,
27
- modal_stack_width: width,
28
- modal_stack_height: height
29
- }.merge(html.fetch(:data, {})).compact
30
- }.merge(html.except(:class, :data))
31
-
32
- content_tag(:div, capture(&), **attrs)
36
+ data = {
37
+ modal_stack_size: size, modal_stack_variant: variant,
38
+ modal_stack_dismissible: dismissible.to_s, modal_stack_side: side,
39
+ modal_stack_width: width, modal_stack_height: height,
40
+ modal_stack_transition: transition
41
+ }.merge(html.fetch(:data, {})).compact
42
+
43
+ { class: [classes, html[:class]].compact.join(" "), data: data }
44
+ .merge(html.except(:class, :data))
45
+ end
46
+
47
+ # The back button stays in the DOM at all depths and is hidden by CSS
48
+ # when the layer is on its first frame (`[data-frame-depth="1"]`).
49
+ # That keeps the wizard structure stable across path_to/path_back —
50
+ # no Stimulus state needed.
51
+ def modal_stack_container_back_button
52
+ label = (defined?(I18n) ? I18n.t("modal_stack.back", default: "Back") : "Back")
53
+ button_tag(
54
+ label,
55
+ type: "button",
56
+ class: "modal-stack__panel-back",
57
+ data: { action: "click->modal-stack#pathBack" },
58
+ aria: { label: label }
59
+ )
33
60
  end
34
61
  end
35
62
  end
@@ -6,6 +6,7 @@ module ModalStack
6
6
  # so the standard `turbo_stream.foo(...)` form keeps working alongside.
7
7
  module TurboStreamsExtension
8
8
  HISTORY_MODES = %i[push replace].freeze
9
+ PATH_TRANSITIONS = %i[slide fade none].freeze
9
10
 
10
11
  # Push a new layer on top of the stack. The content is rendered using
11
12
  # the same options as Turbo's standard stream actions
@@ -64,10 +65,65 @@ module ModalStack
64
65
  turbo_stream_action_tag(:modal_close_all, target: ModalStack::TARGET_ID)
65
66
  end
66
67
 
68
+ # Navigate forward inside the top layer's path. The current frame is
69
+ # cached in memory so a subsequent `modal_path_back` (or browser back)
70
+ # restores it without a network round-trip. Pass `stale: true` (or set
71
+ # `X-Modal-Stack-Stale: true` on the response) to force a refetch on
72
+ # back.
73
+ def modal_path_to(content = nil, url: nil, transition: nil, stale: false, layer_id: nil, **rendering, &)
74
+ template = render_template(ModalStack::TARGET_ID, content, **rendering, &)
75
+ turbo_stream_action_tag(
76
+ :modal_path_to,
77
+ target: ModalStack::TARGET_ID,
78
+ template: template,
79
+ data: modal_data(
80
+ url: url,
81
+ transition: validate_path_transition(resolved_path_transition(transition)),
82
+ stale: stale ? "true" : nil,
83
+ layer_id: layer_id
84
+ )
85
+ )
86
+ end
87
+
88
+ # Step back through the top layer's path. Defaults to one frame; pass
89
+ # `steps: N` to collapse multiple frames at once. Clamps at the first
90
+ # frame — does not close the layer.
91
+ def modal_path_back(steps: 1, transition: nil)
92
+ n = Integer(steps)
93
+ raise ArgumentError, "steps must be a positive integer, got #{steps.inspect}" if n < 1
94
+
95
+ turbo_stream_action_tag(
96
+ :modal_path_back,
97
+ target: ModalStack::TARGET_ID,
98
+ data: modal_data(
99
+ steps: n,
100
+ transition: validate_path_transition(resolved_path_transition(transition))
101
+ )
102
+ )
103
+ end
104
+
67
105
  private
68
106
 
69
107
  def modal_data(**attrs)
70
108
  attrs.compact
71
109
  end
110
+
111
+ def validate_path_transition(value)
112
+ return nil if value.nil?
113
+
114
+ sym = value.to_sym
115
+ unless PATH_TRANSITIONS.include?(sym)
116
+ raise ArgumentError, "transition must be one of #{PATH_TRANSITIONS.inspect}, got #{value.inspect}"
117
+ end
118
+
119
+ sym
120
+ end
121
+
122
+ # Falls back to the configured default when a call site doesn't
123
+ # specify a transition. Pass `transition: :none` to disable
124
+ # explicitly.
125
+ def resolved_path_transition(value)
126
+ value || ModalStack.configuration.default_path_transition
127
+ end
72
128
  end
73
129
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ModalStack
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/modal_stack.rb CHANGED
@@ -10,11 +10,15 @@ module ModalStack
10
10
  # at the application level (config.dialog_id =, config.request_header =).
11
11
  TARGET_ID = "modal-stack-root"
12
12
  REQUEST_HEADER = "X-Modal-Stack-Request"
13
+ # Response header set by `path_to` controllers to mark the just-rendered
14
+ # frame as stale — the JS runtime refetches instead of restoring from its
15
+ # in-memory cache when the user steps back to it.
16
+ STALE_HEADER = "X-Modal-Stack-Stale"
13
17
 
14
18
  # Bumped when config/initializers/modal_stack.rb gains/loses an option,
15
19
  # so apps that haven't regenerated their initializer get a one-line
16
20
  # boot warning. Independent from the gem's VERSION.
17
- INITIALIZER_VERSION = "0.2.0"
21
+ INITIALIZER_VERSION = "0.4.0"
18
22
 
19
23
  class << self
20
24
  def configuration
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: modal_stack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Gagnaire
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-03 00:00:00.000000000 Z
11
+ date: 2026-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -70,8 +70,10 @@ files:
70
70
  - Rakefile
71
71
  - app/assets/javascripts/modal_stack.js
72
72
  - app/assets/stylesheets/modal_stack/bootstrap.css
73
- - app/assets/stylesheets/modal_stack/tailwind.css
73
+ - app/assets/stylesheets/modal_stack/tailwind_v3.css
74
+ - app/assets/stylesheets/modal_stack/tailwind_v4.css
74
75
  - app/assets/stylesheets/modal_stack/vanilla.css
76
+ - app/javascript/modal_stack/controllers/modal_stack_back_link_controller.js
75
77
  - app/javascript/modal_stack/controllers/modal_stack_controller.js
76
78
  - app/javascript/modal_stack/controllers/modal_stack_link_controller.js
77
79
  - app/javascript/modal_stack/index.js
@@ -83,8 +85,13 @@ files:
83
85
  - app/javascript/modal_stack/state.js
84
86
  - app/javascript/modal_stack/state.test.js
85
87
  - app/views/layouts/modal.html.erb
88
+ - app/views/modal_stack/_dialog.html.erb
89
+ - app/views/modal_stack/_panel.html.erb
86
90
  - lib/generators/modal_stack/install/install_generator.rb
87
91
  - lib/generators/modal_stack/install/templates/initializer.rb
92
+ - lib/generators/modal_stack/views/templates/_dialog.html.erb
93
+ - lib/generators/modal_stack/views/templates/_panel.html.erb
94
+ - lib/generators/modal_stack/views/views_generator.rb
88
95
  - lib/modal_stack.rb
89
96
  - lib/modal_stack/capybara.rb
90
97
  - lib/modal_stack/capybara/minitest.rb
@@ -92,6 +99,7 @@ files:
92
99
  - lib/modal_stack/configuration.rb
93
100
  - lib/modal_stack/controller_extensions.rb
94
101
  - lib/modal_stack/engine.rb
102
+ - lib/modal_stack/helpers/modal_back_link_helper.rb
95
103
  - lib/modal_stack/helpers/modal_link_helper.rb
96
104
  - lib/modal_stack/helpers/modal_stack_assets_helper.rb
97
105
  - lib/modal_stack/helpers/modal_stack_container_helper.rb