pop_frame 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c97cbc621cda0a6f45a67e3eefd506adc5a5f3afa77d8f772b046e47a9eeb6f5
4
+ data.tar.gz: f897c73e4d3d9dc7a56bbe31456d6c3ef9794fbd9908ad2fb1c011f809b04419
5
+ SHA512:
6
+ metadata.gz: 3cc96e33d4d42113337b7e158f67a6069c076bcd382f780bed30be103457e26709ad636baa80c73b67dd4856138c17a7a826dd0e87d3ea37a6231f7b2746d245
7
+ data.tar.gz: 16a5bdd0e5e4c0f8c470bda16dbfc10f1c3e4f7a33a51d3337ffd1adaf3dd33152f9a108b70c407d7387e5f44b90444baaed7d3b6cd1b607517d9d31f3e998c5
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Change Log
2
+
3
+ ## [v0.1.0] Oct 9, 2026
4
+
5
+ - `pop_frame_tag(id, src) { trigger }` renders a button that opens a native popover holding a Turbo
6
+ Frame. The frame loads `src` (anything `url_for` takes) the first time the popover opens, and
7
+ reloads in place on a morph refresh, staying open. `pop_frame.css` anchors the popover to its
8
+ button.
9
+
10
+ [v0.1.0]: https://github.com/RoleModel/pop-frame/releases/tag/v0.1.0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2026 RoleModel Software
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # Pop Frame
2
+
3
+ <div>
4
+ <img src="https://github.com/RoleModel/pop-frame/actions/workflows/ci.yml/badge.svg" alt="CI">
5
+ <img src="https://img.shields.io/gem/v/pop_frame" alt="Gem Version">
6
+ </div>
7
+ <br/>
8
+
9
+ A `turbo-frame` in a native popover, loaded the first time it opens.
10
+
11
+ ![title image](title_image.png)
12
+
13
+ Popover libraries were built for browsers that couldn't open a popover, position it, or keep it
14
+ from being clipped. Browsers can do all of that now. A JavaScript popover also has to be re-wired
15
+ every time Turbo morphs the page; a native one is just HTML, so a morph leaves it alone.
16
+
17
+ Pop Frame is one view helper and five lines of CSS. The browser handles the rest:
18
+
19
+ - The [Popover API][mdn-popover] opens and closes it, closes it on Escape or an outside click, and
20
+ keeps `aria-expanded` up to date.
21
+ - The [top layer][mdn-top-layer] keeps it above everything, so an `overflow` ancestor can't clip it.
22
+ - [CSS anchor positioning][mdn-anchor] places it under the button that opened it.
23
+ - Turbo loads the frame and refreshes it when the page morphs.
24
+
25
+ There's no JavaScript to install, configure, or keep alive across morphs.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ bundle add pop_frame
31
+ ```
32
+
33
+ Then include the stylesheet. With Propshaft or Sprockets, add it to your layout:
34
+
35
+ ```erb
36
+ <%= stylesheet_link_tag 'pop_frame' %>
37
+ ```
38
+
39
+ If a JavaScript bundler builds your CSS (cssbundling-rails, webpack), copy
40
+ [`pop_frame.css`](app/assets/stylesheets/pop_frame.css) into your own stylesheets instead.
41
+
42
+ ## Basic Usage
43
+
44
+ Give `pop_frame_tag` an id for the frame and where to load it from. The block becomes the button's
45
+ content. (Examples use [slim] syntax.)
46
+
47
+ ```ruby
48
+ = pop_frame_tag dom_id(person, :week), person_week_path(person), class: 'btn' do
49
+ | This week
50
+ ```
51
+
52
+ which renders
53
+
54
+ ```html
55
+ <button type="button" popovertarget="person_1_week_popover" class="btn">This week</button>
56
+ <div id="person_1_week_popover" class="pop-frame" popover="auto">
57
+ <turbo-frame id="person_1_week" src="/people/1/week" loading="lazy" refresh="morph">Loading…</turbo-frame>
58
+ </div>
59
+ ```
60
+
61
+ The response must contain a `turbo-frame` with the same id, as with any Turbo Frame:
62
+
63
+ ```ruby
64
+ = turbo_frame_tag dom_id(@person, :week) do
65
+ = render 'people/week', person: @person
66
+ ```
67
+
68
+ Nothing is fetched until the popover first opens. After that, it keeps its content between opens.
69
+ Links and forms inside it navigate the frame, just as they would in any other `turbo-frame`.
70
+
71
+ > [!TIP]
72
+ > Each frame needs an id that's unique on the page. `dom_id` with a suffix, as above, is an easy way
73
+ > to get one, even when the same record shows up in several places.
74
+
75
+ ## Options
76
+
77
+ `pop_frame_tag(id, src, loading: 'Loading…', **button_options, &block)`
78
+
79
+ | Argument | Description | Default |
80
+ | ---------------- | ----------------------------------------------------------------------------------- | ------------ |
81
+ | `id` | The frame's id. The popover's id is derived from it (`"#{id}_popover"`). | required |
82
+ | `src` | Where the frame loads from. Takes anything [`url_for`][url-for] does. | required |
83
+ | `loading:` | Shown until the frame loads: plain text, or markup like `render 'spinner'`. | `'Loading…'` |
84
+ | `button_options` | HTML options for the button (`class:`, `data:`, `aria:` and so on). | `{}` |
85
+ | `&block` | The button's content. | required |
86
+
87
+ ## Styling
88
+
89
+ The stylesheet only positions the popover. Its look is up to you:
90
+
91
+ ```css
92
+ /* the popover */
93
+ .pop-frame { padding: 1rem; border-radius: 8px; box-shadow: 0 8px 24px #0002; }
94
+
95
+ /* its button, while open */
96
+ .btn:has(+ :popover-open) { outline: 2px solid royalblue; }
97
+
98
+ /* the frame, while it loads */
99
+ .pop-frame turbo-frame[busy] { opacity: 0.5; }
100
+ ```
101
+
102
+ The popover opens below its button, aligned to the button's start edge, and flips to fit when it's
103
+ near the edge of the screen. To place it differently, override `position-area` and
104
+ `position-try-fallbacks` on `.pop-frame[popover]`.
105
+
106
+ ## Page Refreshes
107
+
108
+ Pop Frame is built for pages that [refresh with morphing][turbo-morph]. When a save elsewhere on the
109
+ page (or a broadcast) morphs it:
110
+
111
+ - An open popover **stays open**, and its content is swapped for the fresh version as soon as it
112
+ arrives, with no flash of the loading state.
113
+ - Popovers that were opened earlier and are now closed reload in the background, so they're current
114
+ the next time they open.
115
+ - Popovers that were never opened stay unloaded.
116
+
117
+ > [!NOTE]
118
+ > Morph refreshes need `turbo_refreshes_with method: :morph` in the page's `<head>`. That helper
119
+ > writes to `content_for(:head)`, so your layout has to `yield :head`. If it doesn't, use
120
+ > `turbo_refresh_method_tag :morph` in the layout instead.
121
+
122
+ ## Browser Support
123
+
124
+ The popover itself works anywhere the Popover API does (Baseline 2024). Positioning it under its
125
+ button needs CSS anchor positioning: Chrome 125, Safari 26, Firefox 147. Older browsers still open
126
+ the popover, just centered on the screen by the browser's default popover styles.
127
+
128
+ Opening a popover from your own JavaScript with `showPopover()` leaves it with nothing to anchor to.
129
+ Pass the button as its source, `popover.showPopover({ source: button })`, to keep it in place.
130
+
131
+ ## System Tests
132
+
133
+ Because it's ordinary HTML, Capybara needs nothing special:
134
+
135
+ ```ruby
136
+ click_on 'This week'
137
+
138
+ within '.pop-frame' do
139
+ assert_text 'Week of Oct 12'
140
+ end
141
+ ```
142
+
143
+ ## Development
144
+
145
+ After cloning the repository, install dependencies with `bundle install`.
146
+
147
+ Run the test suite with `bin/rails test:all`. Its system tests drive headless Chrome with [Cuprite].
148
+ Lint with `bin/rubocop`.
149
+
150
+ Run the dummy app's server on port 3000 with `bin/dev` (set `PORT` to change it). It's a page of
151
+ popovers to try by hand.
152
+
153
+ Releases are cut from the **Bump Version** workflow in the Actions tab. It takes the version bump and
154
+ a changelog entry, then tags the release, publishes it on GitHub, and pushes the gem to RubyGems.
155
+
156
+ ## Acknowledgments
157
+
158
+ **Pop Frame** is [MIT-licensed](MIT-LICENSE), open-source software from [RoleModel Software][rms].
159
+
160
+ [RoleModel Software][rms] is a world-class, collaborative software development team dedicated to delivering the highest quality custom web and mobile software solutions while cultivating a work environment where community, family, learning, and mentoring flourish.
161
+
162
+ [slim]: https://github.com/slim-template/slim
163
+ [mdn-popover]: https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
164
+ [mdn-top-layer]: https://developer.mozilla.org/en-US/docs/Glossary/Top_layer
165
+ [mdn-anchor]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning
166
+ [url-for]: https://api.rubyonrails.org/classes/ActionView/RoutingUrlFor.html#method-i-url_for
167
+ [turbo-morph]: https://turbo.hotwired.dev/handbook/page_refreshes
168
+ [Cuprite]: https://github.com/rubycdp/cuprite
169
+ [rms]: https://rolemodelsoftware.com/
@@ -0,0 +1,7 @@
1
+ /* Anchored to the button that opened it, below and aligned to its start, flipping when it won't fit. */
2
+ .pop-frame[popover] {
3
+ inset: auto;
4
+ margin: 4px 0 0;
5
+ position-area: block-end span-inline-end;
6
+ position-try-fallbacks: flip-block, flip-inline;
7
+ }
@@ -0,0 +1,12 @@
1
+ module PopFrame
2
+ class Engine < ::Rails::Engine
3
+ initializer 'pop_frame.helper' do
4
+ ActiveSupport.on_load(:action_view) { include PopFrame::Helper }
5
+ end
6
+
7
+ # Propshaft puts every engine's app/assets on the load path by itself; Sprockets wants it named.
8
+ initializer 'pop_frame.assets' do |app|
9
+ app.config.assets.precompile << 'pop_frame.css' if app.config.respond_to?(:assets)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ module PopFrame
2
+ module Helper
3
+ # A button whose block is its content, opening a popover with a Turbo Frame that loads +src+
4
+ # (anything url_for takes) the first time it's shown, and reloads in place on a morph refresh.
5
+ def pop_frame_tag(id, src, loading: 'Loading…', **button_options, &)
6
+ popover_id = "#{id}_popover"
7
+
8
+ safe_join [
9
+ tag.button(type: 'button', popovertarget: popover_id, **button_options, &),
10
+ tag.div(id: popover_id, class: 'pop-frame', popover: 'auto') do
11
+ tag.turbo_frame(loading, id:, src: url_for(src), loading: 'lazy', refresh: 'morph')
12
+ end
13
+ ]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module PopFrame
2
+ VERSION = '0.1.0'
3
+ end
data/lib/pop_frame.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'pop_frame/version'
2
+ require 'pop_frame/helper'
3
+ require 'pop_frame/engine'
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pop_frame
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Cohen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionview
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 7.1.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: railties
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 7.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 7.1.0
40
+ description: A view helper for a button that opens a lazily loaded Turbo Frame in
41
+ a native popover, anchored to the button with CSS. No JavaScript of its own.
42
+ email:
43
+ - andy.cohen@rolemodelsoftware.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - app/assets/stylesheets/pop_frame.css
52
+ - lib/pop_frame.rb
53
+ - lib/pop_frame/engine.rb
54
+ - lib/pop_frame/helper.rb
55
+ - lib/pop_frame/version.rb
56
+ homepage: https://github.com/RoleModel/pop-frame
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ homepage_uri: https://github.com/RoleModel/pop-frame
61
+ source_code_uri: https://github.com/RoleModel/pop-frame
62
+ changelog_uri: https://github.com/RoleModel/pop-frame/blob/main/CHANGELOG.md
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '3.1'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 4.0.16
78
+ specification_version: 4
79
+ summary: A Turbo Frame in a native popover, loaded when it opens.
80
+ test_files: []