modal_stack 0.1.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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +37 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +748 -0
  6. data/Rakefile +12 -0
  7. data/app/assets/javascripts/modal_stack.js +756 -0
  8. data/app/assets/stylesheets/modal_stack/bootstrap.css +232 -0
  9. data/app/assets/stylesheets/modal_stack/tailwind.css +303 -0
  10. data/app/assets/stylesheets/modal_stack/vanilla.css +219 -0
  11. data/app/javascript/modal_stack/controllers/modal_stack_controller.js +149 -0
  12. data/app/javascript/modal_stack/controllers/modal_stack_link_controller.js +34 -0
  13. data/app/javascript/modal_stack/index.js +15 -0
  14. data/app/javascript/modal_stack/install.js +15 -0
  15. data/app/javascript/modal_stack/orchestrator.js +98 -0
  16. data/app/javascript/modal_stack/orchestrator.test.js +260 -0
  17. data/app/javascript/modal_stack/runtime.js +217 -0
  18. data/app/javascript/modal_stack/runtime.test.js +134 -0
  19. data/app/javascript/modal_stack/state.js +315 -0
  20. data/app/javascript/modal_stack/state.test.js +508 -0
  21. data/app/views/layouts/modal.html.erb +6 -0
  22. data/lib/generators/modal_stack/install/install_generator.rb +224 -0
  23. data/lib/generators/modal_stack/install/templates/initializer.rb +57 -0
  24. data/lib/modal_stack/capybara/minitest.rb +9 -0
  25. data/lib/modal_stack/capybara/rspec.rb +9 -0
  26. data/lib/modal_stack/capybara.rb +85 -0
  27. data/lib/modal_stack/configuration.rb +90 -0
  28. data/lib/modal_stack/controller_extensions.rb +73 -0
  29. data/lib/modal_stack/engine.rb +44 -0
  30. data/lib/modal_stack/helpers/modal_link_helper.rb +65 -0
  31. data/lib/modal_stack/helpers/modal_stack_assets_helper.rb +45 -0
  32. data/lib/modal_stack/helpers/modal_stack_container_helper.rb +36 -0
  33. data/lib/modal_stack/initializer_version_check.rb +33 -0
  34. data/lib/modal_stack/turbo_streams_extension.rb +73 -0
  35. data/lib/modal_stack/version.rb +5 -0
  36. data/lib/modal_stack.rb +36 -0
  37. metadata +130 -0
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModalStack
4
+ module Helpers
5
+ # Layout-level helpers for wiring modal_stack into the host app.
6
+ module ModalStackAssetsHelper
7
+ # Renders a <link> for the configured CSS provider:
8
+ #
9
+ # <%= modal_stack_stylesheet_link_tag %>
10
+ #
11
+ # Returns an empty SafeBuffer when `config.css_provider = :none`,
12
+ # so apps can call this unconditionally.
13
+ def modal_stack_stylesheet_link_tag(**)
14
+ provider = ModalStack.configuration.css_provider
15
+ return ActiveSupport::SafeBuffer.new if provider == :none
16
+
17
+ stylesheet_link_tag("modal_stack/#{provider}", **)
18
+ end
19
+
20
+ # Renders the singleton <dialog> root that the modal-stack Stimulus
21
+ # controller binds to. Drop into your application layout:
22
+ #
23
+ # <%= modal_stack_dialog_tag %>
24
+ #
25
+ def modal_stack_dialog_tag(**html_options)
26
+ config = ModalStack.configuration
27
+ attrs = html_options.dup
28
+ attrs[:id] ||= config.dialog_id
29
+
30
+ existing_data = attrs[:data] || {}
31
+ controllers = [existing_data[:controller], config.stack_root_data_attribute].compact.join(" ").strip
32
+ attrs[:data] = existing_data.merge(controller: controllers)
33
+
34
+ content_tag(:dialog, "".html_safe, attrs)
35
+ end
36
+
37
+ # Emits a no-op SafeBuffer for now — kept as a stable hook for apps
38
+ # that prefer a single line in their layout. The actual JS loading
39
+ # is handled by the host app's bundler / importmap.
40
+ def modal_stack_javascript_tag(**)
41
+ ActiveSupport::SafeBuffer.new
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModalStack
4
+ module Helpers
5
+ # Wraps the layout content in the panel structure expected by the
6
+ # JS runtime. The layout `modal.html.erb` typically reads:
7
+ #
8
+ # <%= modal_stack_container size: :md, dismissible: true do %>
9
+ # <%= yield %>
10
+ # <% end %>
11
+ #
12
+ module ModalStackContainerHelper
13
+ DEFAULT_SIZE = :md
14
+
15
+ def modal_stack_container(size: DEFAULT_SIZE, dismissible: true, variant: :modal, side: nil, width: nil, height: nil, html: {},
16
+ &)
17
+ classes = ["modal-stack__panel", "modal-stack__panel--#{variant}", "modal-stack__panel--size-#{size}"]
18
+ classes << "modal-stack__panel--side-#{side}" if side
19
+
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)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModalStack
4
+ module InitializerVersionCheck
5
+ module_function
6
+
7
+ def perform
8
+ return if ModalStack.configuration.silence_initializer_warning
9
+
10
+ stamped = ModalStack.configuration.initializer_version
11
+ shipped = ModalStack::INITIALIZER_VERSION
12
+
13
+ if stamped.nil?
14
+ warn(
15
+ "[modal_stack] config/initializers/modal_stack.rb has no " \
16
+ "config.initializer_version. The initializer template shipped " \
17
+ "with #{shipped} introduces options the older template did " \
18
+ "not have — regenerate with `bin/rails g modal_stack:install " \
19
+ "--skip-layout --force`. Set " \
20
+ "`config.silence_initializer_warning = true` to silence."
21
+ )
22
+ elsif stamped != shipped
23
+ warn(
24
+ "[modal_stack] config/initializers/modal_stack.rb is stamped " \
25
+ "for v#{stamped} but the gem ships v#{shipped}. The template " \
26
+ "may have new options — review the diff or regenerate with " \
27
+ "`bin/rails g modal_stack:install --skip-layout --force`. Set " \
28
+ "`config.silence_initializer_warning = true` to silence."
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModalStack
4
+ # Custom Turbo Stream actions for stack manipulation. Mixed into
5
+ # Turbo::Streams::TagBuilder via the :turbo_streams_tag_builder load hook
6
+ # so the standard `turbo_stream.foo(...)` form keeps working alongside.
7
+ module TurboStreamsExtension
8
+ HISTORY_MODES = %i[push replace].freeze
9
+
10
+ # Push a new layer on top of the stack. The content is rendered using
11
+ # the same options as Turbo's standard stream actions
12
+ # (partial:/locals:/template:/...).
13
+ #
14
+ # variant: :modal (default) | :drawer | :bottom_sheet | :confirmation
15
+ # dismissible: true (default) | false
16
+ # url: override the URL associated with this layer (defaults to the request path)
17
+ # side: only meaningful for :drawer — :left | :right | :top | :bottom
18
+ # size: :sm | :md | :lg | :xl | string
19
+ # width/height: CSS length values (e.g. "42rem", "70vh", "min(90vw, 56rem)")
20
+ def modal_push(content = nil, variant: :modal, dismissible: true, url: nil, side: nil, size: nil, width: nil, height: nil, **rendering,
21
+ &)
22
+ template = render_template(ModalStack::TARGET_ID, content, **rendering, &)
23
+ turbo_stream_action_tag(
24
+ :modal_push,
25
+ target: ModalStack::TARGET_ID,
26
+ template: template,
27
+ data: modal_data(variant: variant, dismissible: dismissible, url: url, side: side, size: size, width: width, height: height)
28
+ )
29
+ end
30
+
31
+ # Pop the top layer.
32
+ def modal_pop
33
+ turbo_stream_action_tag(:modal_pop, target: ModalStack::TARGET_ID)
34
+ end
35
+
36
+ # Replace the top layer's content. Defaults to history.replaceState
37
+ # (no new history entry). Pass history: :push for a wizard-step semantic
38
+ # where browser-back returns to the previous step.
39
+ def modal_replace(content = nil, variant: nil, dismissible: nil, url: nil, history: :replace, layer_id: nil, side: nil, size: nil,
40
+ width: nil, height: nil, **rendering, &)
41
+ raise ArgumentError, "history: must be #{HISTORY_MODES.inspect}, got #{history.inspect}" unless HISTORY_MODES.include?(history)
42
+
43
+ template = render_template(ModalStack::TARGET_ID, content, **rendering, &)
44
+ turbo_stream_action_tag(
45
+ :modal_replace,
46
+ target: ModalStack::TARGET_ID,
47
+ template: template,
48
+ data: modal_data(
49
+ variant: variant,
50
+ dismissible: dismissible,
51
+ url: url,
52
+ side: side,
53
+ size: size,
54
+ width: width,
55
+ height: height,
56
+ history_mode: history,
57
+ layer_id: layer_id
58
+ )
59
+ )
60
+ end
61
+
62
+ # Tear down the entire stack.
63
+ def modal_close_all
64
+ turbo_stream_action_tag(:modal_close_all, target: ModalStack::TARGET_ID)
65
+ end
66
+
67
+ private
68
+
69
+ def modal_data(**attrs)
70
+ attrs.compact
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModalStack
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "modal_stack/version"
4
+
5
+ module ModalStack
6
+ class Error < StandardError; end
7
+
8
+ # Default IDs / headers — exposed for code that needs the values
9
+ # without instantiating Configuration. Configuration overrides them
10
+ # at the application level (config.dialog_id =, config.request_header =).
11
+ TARGET_ID = "modal-stack-root"
12
+ REQUEST_HEADER = "X-Modal-Stack-Request"
13
+
14
+ # Bumped when config/initializers/modal_stack.rb gains/loses an option,
15
+ # so apps that haven't regenerated their initializer get a one-line
16
+ # boot warning. Independent from the gem's VERSION.
17
+ INITIALIZER_VERSION = "0.1.0"
18
+
19
+ class << self
20
+ def configuration
21
+ @configuration ||= Configuration.new
22
+ end
23
+
24
+ def configure
25
+ yield configuration
26
+ end
27
+
28
+ def reset_configuration!
29
+ @configuration = Configuration.new
30
+ end
31
+ end
32
+ end
33
+
34
+ require_relative "modal_stack/configuration"
35
+ require_relative "modal_stack/initializer_version_check"
36
+ require "modal_stack/engine" if defined?(Rails::Engine)
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: modal_stack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Florian Gagnaire
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-05-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: turbo-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.6'
55
+ description: |
56
+ modal_stack adds a navigation stack on top of Hotwire: push N modals/drawers/bottom
57
+ sheets, deep-link the top of the stack via native Rails URLs, get full browser
58
+ history (back/forward) support, and drive everything from imperative Turbo Stream
59
+ actions (modal_push, modal_pop, modal_replace).
60
+ email:
61
+ - gagnaire.flo@gmail.com
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - CHANGELOG.md
67
+ - CODE_OF_CONDUCT.md
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - app/assets/javascripts/modal_stack.js
72
+ - app/assets/stylesheets/modal_stack/bootstrap.css
73
+ - app/assets/stylesheets/modal_stack/tailwind.css
74
+ - app/assets/stylesheets/modal_stack/vanilla.css
75
+ - app/javascript/modal_stack/controllers/modal_stack_controller.js
76
+ - app/javascript/modal_stack/controllers/modal_stack_link_controller.js
77
+ - app/javascript/modal_stack/index.js
78
+ - app/javascript/modal_stack/install.js
79
+ - app/javascript/modal_stack/orchestrator.js
80
+ - app/javascript/modal_stack/orchestrator.test.js
81
+ - app/javascript/modal_stack/runtime.js
82
+ - app/javascript/modal_stack/runtime.test.js
83
+ - app/javascript/modal_stack/state.js
84
+ - app/javascript/modal_stack/state.test.js
85
+ - app/views/layouts/modal.html.erb
86
+ - lib/generators/modal_stack/install/install_generator.rb
87
+ - lib/generators/modal_stack/install/templates/initializer.rb
88
+ - lib/modal_stack.rb
89
+ - lib/modal_stack/capybara.rb
90
+ - lib/modal_stack/capybara/minitest.rb
91
+ - lib/modal_stack/capybara/rspec.rb
92
+ - lib/modal_stack/configuration.rb
93
+ - lib/modal_stack/controller_extensions.rb
94
+ - lib/modal_stack/engine.rb
95
+ - lib/modal_stack/helpers/modal_link_helper.rb
96
+ - lib/modal_stack/helpers/modal_stack_assets_helper.rb
97
+ - lib/modal_stack/helpers/modal_stack_container_helper.rb
98
+ - lib/modal_stack/initializer_version_check.rb
99
+ - lib/modal_stack/turbo_streams_extension.rb
100
+ - lib/modal_stack/version.rb
101
+ homepage: https://github.com/Metalzoid/modal_stack
102
+ licenses:
103
+ - MIT
104
+ metadata:
105
+ homepage_uri: https://github.com/Metalzoid/modal_stack
106
+ source_code_uri: https://github.com/Metalzoid/modal_stack
107
+ changelog_uri: https://github.com/Metalzoid/modal_stack/blob/main/CHANGELOG.md
108
+ bug_tracker_uri: https://github.com/Metalzoid/modal_stack/issues
109
+ rubygems_mfa_required: 'true'
110
+ allowed_push_host: https://rubygems.org
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 3.2.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.5.22
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Stackable modals, drawers and bottom sheets for Hotwire-powered Rails apps.
130
+ test_files: []