hotwire-astra-ui 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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +360 -0
- data/Rakefile +14 -0
- data/app/assets/stylesheets/hotwire/astra_ui/astra_ui.css +104 -0
- data/app/components/hotwire/astra_ui/modal_component.html.erb +31 -0
- data/app/components/hotwire/astra_ui/modal_component.rb +48 -0
- data/app/helpers/hotwire/astra_ui/modal_helper.rb +19 -0
- data/app/javascript/hotwire-astra-ui.js +68 -0
- data/app/javascript/hotwire_astra_ui/modal_controller.js +163 -0
- data/config/importmap.rb +3 -0
- data/config/routes.rb +2 -0
- data/lib/generators/hotwire_astra_ui/install_generator.rb +60 -0
- data/lib/hotwire/astra_ui/engine.rb +15 -0
- data/lib/hotwire/astra_ui/ui.rb +12 -0
- data/lib/hotwire/astra_ui/version.rb +5 -0
- data/lib/tasks/hotwire/astra/ui_tasks.rake +4 -0
- metadata +137 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1d6d9fe4802aac692c58a1dad36a6613cf42220f13636c01183dff8ab09c4d60
|
|
4
|
+
data.tar.gz: 2f45ead1f195c0dfa698fb34cc50e763b067243c0dcb17d04271933b8b26b8b9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 03beaf5ee10b1cc94703c64526eaad38ead87dc8349a6bff927908e624e75380da5b21e8296997de4fdb544d790ac549cad4101ac62b771a24872918a25bebdc
|
|
7
|
+
data.tar.gz: a56a0cd7cef6920f6e71779321a9897234285ce3f2b1672e846fadc2f30841544a01edec0f80cdf3fa3cd8d582a4d2af0ff7335e67b106ecf50c50fbce85d39c
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Fernand Arioja
|
|
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,360 @@
|
|
|
1
|
+
# Hotwire Astra UI 🌌
|
|
2
|
+
|
|
3
|
+
**Hotwire Astra UI** is a **production-ready, accessible modal system for Rails** built with **Hotwire (Turbo + Stimulus)** and **Importmap**.
|
|
4
|
+
|
|
5
|
+
It enables **fully server-rendered modals** using Turbo Frames, with **zero client-side configuration** and **no JavaScript frameworks**.
|
|
6
|
+
|
|
7
|
+
If you are building Rails 7+ applications and want predictable, scalable modal behavior without SPA complexity, Astra UI is designed for you.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
### 🔌 Zero-JavaScript Setup
|
|
14
|
+
Designed for **Rails 7+**, **Hotwire**, and **Importmap**.
|
|
15
|
+
Install the gem, run the generator, and start rendering modals immediately.
|
|
16
|
+
|
|
17
|
+
No client state. No bundlers. No framework lock-in.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### 🪜 Recursive (Stacked) Modals
|
|
22
|
+
Open modals **on top of modals** using nested Turbo Frames.
|
|
23
|
+
|
|
24
|
+
Each modal layer:
|
|
25
|
+
- is independently closable
|
|
26
|
+
- maintains its own lifecycle
|
|
27
|
+
- works with normal Rails controllers
|
|
28
|
+
|
|
29
|
+
Infinite stacking, zero configuration.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### 🎞️ CSS-Driven Animations
|
|
34
|
+
Entry and exit transitions are powered by **CSS variables**, not JavaScript.
|
|
35
|
+
|
|
36
|
+
- Works with Tailwind or plain CSS
|
|
37
|
+
- Respects `prefers-reduced-motion`
|
|
38
|
+
- Fully overrideable
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### 🔄 Automatic Close on Turbo Success
|
|
43
|
+
Modals close automatically after successful Turbo form submissions.
|
|
44
|
+
|
|
45
|
+
No callbacks. No client logic.
|
|
46
|
+
Just follow the Turbo lifecycle.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### 🎨 Headless UI Design
|
|
51
|
+
Astra UI ships with safe defaults, but **nothing is opinionated**.
|
|
52
|
+
|
|
53
|
+
- Override everything with CSS variables
|
|
54
|
+
- Compatible with design systems
|
|
55
|
+
- No forced colors, spacing, or typography
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### ♿ Accessibility-First
|
|
60
|
+
- Proper dialog semantics
|
|
61
|
+
- Keyboard navigation
|
|
62
|
+
- Focus management
|
|
63
|
+
- ESC and backdrop handling
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 📦 Installation
|
|
68
|
+
|
|
69
|
+
Add the gem to your `Gemfile`:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
gem "hotwire-astra-ui", "0.1.0"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Install and run the generator:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bundle install
|
|
79
|
+
bin/rails generate hotwire_astra_ui:install
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The installer registers the Stimulus controller for you. If your app does not have `app/javascript/controllers/index.js`, add this registration manually:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import HotwireAstraUiModalController from "hotwire_astra_ui/modal_controller"
|
|
86
|
+
application.register("hotwire-astra-ui-modal", HotwireAstraUiModalController)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The installer adds a persistent Turbo Frame to your layout:
|
|
90
|
+
|
|
91
|
+
```erb
|
|
92
|
+
<%= turbo_frame_tag "astra_modal" %>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This frame is used to render modal content.
|
|
96
|
+
|
|
97
|
+
## 📦 npm Package (JS + CSS)
|
|
98
|
+
|
|
99
|
+
The JS-only shell is also published to npm:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm install @digi-archive/hotwire-astra-ui
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If you want to pin via Importmap (using the jspm CDN):
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# config/importmap.rb
|
|
109
|
+
pin "@digi-archive/hotwire-astra-ui", to: "https://ga.jspm.io/npm:@digi-archive/hotwire-astra-ui@0.1.0/app/javascript/hotwire-astra-ui.js"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Register the controller:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
import HotwireAstraUiModalController from "@digi-archive/hotwire-astra-ui"
|
|
116
|
+
application.register("hotwire-astra-ui-modal", HotwireAstraUiModalController)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Load the CSS from the same CDN:
|
|
120
|
+
|
|
121
|
+
```erb
|
|
122
|
+
<link rel="stylesheet" href="https://ga.jspm.io/npm:@digi-archive/hotwire-astra-ui@0.1.0/hotwire-astra-ui.css">
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 🧩 JS-Only (Importmap Pin)
|
|
126
|
+
|
|
127
|
+
If you only want the **Stimulus controller** (and will provide your own HTML/CSS), you can pin the JS directly with Importmap from the gem:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# config/importmap.rb
|
|
131
|
+
pin "hotwire_astra_ui/modal_controller", to: ""
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Register the controller:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import HotwireAstraUiModalController from "hotwire_astra_ui/modal_controller"
|
|
138
|
+
application.register("hotwire-astra-ui-modal", HotwireAstraUiModalController)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
You must also provide:
|
|
142
|
+
|
|
143
|
+
- The modal HTML structure with the correct `data-controller` and target attributes
|
|
144
|
+
- The CSS (either copy `app/assets/stylesheets/hotwire/astra_ui/astra_ui.css` or reimplement it)
|
|
145
|
+
|
|
146
|
+
Example HTML structure (JS-only):
|
|
147
|
+
|
|
148
|
+
```erb
|
|
149
|
+
<div data-controller="hotwire-astra-ui-modal"
|
|
150
|
+
data-hotwire-astra-ui-modal-target="backdrop"
|
|
151
|
+
data-action="click->hotwire-astra-ui-modal#backdropClick"
|
|
152
|
+
class="astra-modal-backdrop astra-default">
|
|
153
|
+
|
|
154
|
+
<div class="astra-modal-container astra-modal-size-md"
|
|
155
|
+
data-hotwire-astra-ui-modal-target="container"
|
|
156
|
+
data-action="click->hotwire-astra-ui-modal#stopPropagation"
|
|
157
|
+
role="dialog"
|
|
158
|
+
aria-modal="true"
|
|
159
|
+
aria-label="Example Dialog"
|
|
160
|
+
tabindex="-1">
|
|
161
|
+
<div class="astra-modal-header">
|
|
162
|
+
<h2>Example Dialog</h2>
|
|
163
|
+
<button type="button" data-action="hotwire-astra-ui-modal#close" class="astra-close-btn" aria-label="Close dialog">×</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="astra-modal-body">
|
|
167
|
+
Your content goes here.
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 📦 npm + Importmap (JS + CSS)
|
|
174
|
+
|
|
175
|
+
If you publish the npm package `@digi-archive/hotwire-astra-ui`, you can pin it via Importmap using the jspm CDN:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# config/importmap.rb
|
|
179
|
+
pin "@digi-archive/hotwire-astra-ui", to: "https://ga.jspm.io/npm:@digi-archive/hotwire-astra-ui@0.1.0/app/javascript/hotwire-astra-ui.js"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Register the controller:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
import HotwireAstraUiModalController from "@digi-archive/hotwire-astra-ui"
|
|
186
|
+
application.register("hotwire-astra-ui-modal", HotwireAstraUiModalController)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
To load the CSS from npm, include the stylesheet from the same CDN:
|
|
190
|
+
|
|
191
|
+
```erb
|
|
192
|
+
<link rel="stylesheet" href="https://ga.jspm.io/npm:@digi-archive/hotwire-astra-ui@0.1.0/hotwire-astra-ui.css">
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## 🎨 Styles
|
|
196
|
+
|
|
197
|
+
Import the base stylesheet:
|
|
198
|
+
|
|
199
|
+
```css
|
|
200
|
+
/*
|
|
201
|
+
*= require hotwire_astra_ui/astra_ui
|
|
202
|
+
*/
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
All visual customization is done via CSS variables.
|
|
206
|
+
|
|
207
|
+
## 🚀 Basic Usage
|
|
208
|
+
|
|
209
|
+
1. Trigger the Modal
|
|
210
|
+
|
|
211
|
+
Target the modal Turbo Frame:
|
|
212
|
+
|
|
213
|
+
```erb
|
|
214
|
+
<%= link_to "New Post", new_post_path, data: { turbo_frame: "astra_modal" } %>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
2. Render the Modal in a View Template
|
|
218
|
+
```ruby
|
|
219
|
+
<%= astra_modal(title: "Create a New Post") do %>
|
|
220
|
+
<%= render partial: "form" %>
|
|
221
|
+
<% end %>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
That’s it.
|
|
225
|
+
No JavaScript. No client state.
|
|
226
|
+
|
|
227
|
+
## 🧩 Advanced Usage
|
|
228
|
+
|
|
229
|
+
### 🧰 Component Parameters
|
|
230
|
+
|
|
231
|
+
`Hotwire::AstraUi::ModalComponent.new` accepts:
|
|
232
|
+
|
|
233
|
+
- `title:` string or nil. If present, renders the header and provides `aria-labelledby`.
|
|
234
|
+
- `id:` Turbo Frame id (default: `"astra_modal"`).
|
|
235
|
+
- `theme_class:` CSS class applied to the backdrop (default: `"astra-default"`).
|
|
236
|
+
- `aria_label:` used when `title` is nil (default: `"Dialog"`).
|
|
237
|
+
- `size:` one of `:sm`, `:md`, `:lg`, `:xl` (default: `:md`).
|
|
238
|
+
- `backdrop_close:` boolean for backdrop click to close (default: `true`).
|
|
239
|
+
- `return_focus:` CSS selector to focus after close (default: `nil`).
|
|
240
|
+
|
|
241
|
+
### 🪄 Convenience Helper
|
|
242
|
+
|
|
243
|
+
Render from a view:
|
|
244
|
+
|
|
245
|
+
```erb
|
|
246
|
+
<%= astra_modal(title: "Create a New Post") do %>
|
|
247
|
+
<%= render partial: "form" %>
|
|
248
|
+
<% end %>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 🔁 Stacked (Nested) Modals
|
|
252
|
+
|
|
253
|
+
Open a modal from inside another modal by targeting the next frame:
|
|
254
|
+
|
|
255
|
+
```erb
|
|
256
|
+
<%= link_to "Confirm Delete",
|
|
257
|
+
confirm_delete_path,
|
|
258
|
+
data: { turbo_frame: "astra_modal_next" } %>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Template:
|
|
262
|
+
|
|
263
|
+
```erb
|
|
264
|
+
<%= astra_modal(title: "Are you sure?", id: "astra_modal_next") do %>
|
|
265
|
+
This action cannot be undone.
|
|
266
|
+
<% end %>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Each modal layer remains isolated and predictable.
|
|
270
|
+
|
|
271
|
+
### 🔒 Close Modals from the Server (Turbo Streams)
|
|
272
|
+
|
|
273
|
+
Close the modal directly from the server:
|
|
274
|
+
|
|
275
|
+
```erb
|
|
276
|
+
<%= close_astra_modal_tag %>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Close a specific frame by id:
|
|
280
|
+
|
|
281
|
+
```erb
|
|
282
|
+
<%= close_astra_modal_tag(id: "astra_modal_next") %>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Useful for:
|
|
286
|
+
|
|
287
|
+
- form submissions
|
|
288
|
+
- background jobs
|
|
289
|
+
- multi-step workflows
|
|
290
|
+
|
|
291
|
+
### 🧱 Size Variants
|
|
292
|
+
|
|
293
|
+
Set a modal size with `size:`:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
render Hotwire::AstraUi::ModalComponent.new(title: "Large Modal", size: :lg)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Available sizes: `:sm`, `:md`, `:lg`, `:xl`.
|
|
300
|
+
|
|
301
|
+
### 🧲 Backdrop Click & Focus Return
|
|
302
|
+
|
|
303
|
+
Disable backdrop click to close:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
render Hotwire::AstraUi::ModalComponent.new(title: "Locked", backdrop_close: false)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Return focus to a specific element after close:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
render Hotwire::AstraUi::ModalComponent.new(title: "Edit", return_focus: "#edit-button")
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## 🎨 Customization (CSS Variables)
|
|
316
|
+
|
|
317
|
+
Override the look without touching gem code:
|
|
318
|
+
|
|
319
|
+
```css
|
|
320
|
+
:root {
|
|
321
|
+
--astra-modal-bg: #1e293b;
|
|
322
|
+
--astra-modal-radius: 0px;
|
|
323
|
+
--astra-backdrop-blur: 10px;
|
|
324
|
+
--astra-transition-duration: 500ms;
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Works with:
|
|
329
|
+
|
|
330
|
+
- Tailwind CSS
|
|
331
|
+
- Dark mode
|
|
332
|
+
- Design tokens
|
|
333
|
+
- Enterprise UI standards
|
|
334
|
+
|
|
335
|
+
## 🧪 Development
|
|
336
|
+
|
|
337
|
+
Run the dummy app locally:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
cd test/dummy
|
|
341
|
+
bin/rails s
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## ✅ Testing
|
|
345
|
+
|
|
346
|
+
Run the test suite:
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
bin/test
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Or directly via Rake:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
bundle exec rake test
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
## License
|
|
360
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
4
|
+
load "rails/tasks/engine.rake"
|
|
5
|
+
|
|
6
|
+
require "bundler/gem_tasks"
|
|
7
|
+
require "rake/testtask"
|
|
8
|
+
|
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
|
10
|
+
t.libs << "test"
|
|
11
|
+
t.pattern = "test/**/*_test.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
task default: :test
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* Hotwire Astra UI - Core Styles
|
|
2
|
+
Customization: Override these variables in your application.css
|
|
3
|
+
*/
|
|
4
|
+
:root {
|
|
5
|
+
/* Layout & Depth */
|
|
6
|
+
--astra-modal-z: 1000;
|
|
7
|
+
--astra-modal-max-width: 500px;
|
|
8
|
+
--astra-modal-radius: 0.75rem;
|
|
9
|
+
--astra-modal-bg: #ffffff;
|
|
10
|
+
--astra-backdrop-color: rgba(15, 23, 42, 0.6);
|
|
11
|
+
--astra-backdrop-blur: 4px;
|
|
12
|
+
|
|
13
|
+
/* Animation Timings */
|
|
14
|
+
--astra-transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
15
|
+
--astra-transition-duration: 300ms;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* --- Backdrop --- */
|
|
19
|
+
.astra-modal-backdrop {
|
|
20
|
+
position: fixed;
|
|
21
|
+
inset: 0;
|
|
22
|
+
z-index: var(--astra-modal-z);
|
|
23
|
+
background-color: var(--astra-backdrop-color);
|
|
24
|
+
backdrop-filter: blur(var(--astra-backdrop-blur));
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
padding: 1rem;
|
|
29
|
+
width: 100%;
|
|
30
|
+
|
|
31
|
+
/* Initial state for animation */
|
|
32
|
+
opacity: 0;
|
|
33
|
+
transition: opacity var(--astra-transition-duration) var(--astra-transition-timing);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.astra-backdrop-enter-active {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.astra-modal-container {
|
|
41
|
+
width: 100%;
|
|
42
|
+
max-width: var(--astra-modal-max-width);
|
|
43
|
+
border-radius: var(--astra-modal-radius);
|
|
44
|
+
background: var(--astra-modal-bg);
|
|
45
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
|
|
48
|
+
/* Initial state for animation */
|
|
49
|
+
opacity: 0;
|
|
50
|
+
transform: translateY(20px) scale(0.95);
|
|
51
|
+
transition:
|
|
52
|
+
opacity var(--astra-transition-duration) var(--astra-transition-timing),
|
|
53
|
+
transform var(--astra-transition-duration) var(--astra-transition-timing);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.astra-modal-size-sm { --astra-modal-max-width: 360px; }
|
|
57
|
+
.astra-modal-size-md { --astra-modal-max-width: 520px; }
|
|
58
|
+
.astra-modal-size-lg { --astra-modal-max-width: 720px; }
|
|
59
|
+
.astra-modal-size-xl { --astra-modal-max-width: 960px; }
|
|
60
|
+
|
|
61
|
+
.astra-enter-active {
|
|
62
|
+
opacity: 1;
|
|
63
|
+
transform: translateY(0) scale(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* --- Content Areas --- */
|
|
67
|
+
.astra-modal-header {
|
|
68
|
+
padding: 1.25rem;
|
|
69
|
+
border-bottom: 1px solid #e2e8f0;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.astra-modal-header h2 {
|
|
76
|
+
margin: 0;
|
|
77
|
+
font-size: 1.25rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
color: #1e293b;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.astra-modal-body {
|
|
83
|
+
padding: 1.5rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.astra-close-btn {
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
background: transparent;
|
|
89
|
+
border: none;
|
|
90
|
+
font-size: 1.5rem;
|
|
91
|
+
color: #94a3b8;
|
|
92
|
+
line-height: 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.astra-close-btn:hover {
|
|
96
|
+
color: #475569;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@media (prefers-reduced-motion: reduce) {
|
|
100
|
+
.astra-modal-backdrop,
|
|
101
|
+
.astra-modal-container {
|
|
102
|
+
transition: none;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<%= helpers.turbo_frame_tag @id, data: { turbo_action: "none" } do %>
|
|
2
|
+
<div data-controller="hotwire-astra-ui-modal"
|
|
3
|
+
data-hotwire-astra-ui-modal-backdrop-close-value="<%= backdrop_close_value %>"
|
|
4
|
+
<%= return_focus.present? ? "data-hotwire-astra-ui-modal-return-focus-value=\"#{return_focus}\"" : "" %>
|
|
5
|
+
data-hotwire-astra-ui-modal-target="backdrop"
|
|
6
|
+
data-action="click->hotwire-astra-ui-modal#backdropClick"
|
|
7
|
+
class="astra-modal-backdrop <%= @theme_class %>">
|
|
8
|
+
|
|
9
|
+
<div class="astra-modal-container <%= size_class %>"
|
|
10
|
+
data-hotwire-astra-ui-modal-target="container"
|
|
11
|
+
data-action="click->hotwire-astra-ui-modal#stopPropagation"
|
|
12
|
+
role="dialog"
|
|
13
|
+
aria-modal="true"
|
|
14
|
+
<%= @title.present? ? "aria-labelledby=\"#{title_id}\"" : "aria-label=\"#{aria_label}\"" %>
|
|
15
|
+
tabindex="-1">
|
|
16
|
+
<div class="astra-modal-header">
|
|
17
|
+
<% if @title.present? %>
|
|
18
|
+
<h2 id="<%= title_id %>"><%= @title %></h2>
|
|
19
|
+
<% end %>
|
|
20
|
+
<button type="button" data-action="hotwire-astra-ui-modal#close" class="astra-close-btn" aria-label="Close dialog">×</button>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="astra-modal-body">
|
|
24
|
+
<%= content %>
|
|
25
|
+
|
|
26
|
+
<%# This is the "landing zone" for a potential second modal %>
|
|
27
|
+
<%= helpers.turbo_frame_tag next_modal_id %>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Hotwire::AstraUi
|
|
2
|
+
class ModalComponent < ViewComponent::Base
|
|
3
|
+
def initialize(
|
|
4
|
+
title: nil,
|
|
5
|
+
id: nil,
|
|
6
|
+
theme_class: "astra-default",
|
|
7
|
+
aria_label: "Dialog",
|
|
8
|
+
size: :md,
|
|
9
|
+
backdrop_close: true,
|
|
10
|
+
return_focus: nil
|
|
11
|
+
)
|
|
12
|
+
@title = title
|
|
13
|
+
# Default to the base 'astra_modal' if no specific ID is provided
|
|
14
|
+
@id = id || "astra_modal"
|
|
15
|
+
@theme_class = theme_class
|
|
16
|
+
@aria_label = aria_label
|
|
17
|
+
@size = size
|
|
18
|
+
@backdrop_close = backdrop_close
|
|
19
|
+
@return_focus = return_focus
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def next_modal_id
|
|
23
|
+
"#{@id}_next"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def title_id
|
|
27
|
+
"#{@id}_title"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def aria_label
|
|
31
|
+
@aria_label
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def size_class
|
|
35
|
+
return nil if @size.nil?
|
|
36
|
+
|
|
37
|
+
"astra-modal-size-#{@size}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def backdrop_close_value
|
|
41
|
+
@backdrop_close ? "true" : "false"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def return_focus
|
|
45
|
+
@return_focus
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Hotwire
|
|
2
|
+
module AstraUi
|
|
3
|
+
module ModalHelper
|
|
4
|
+
def astra_modal_frame_tag
|
|
5
|
+
turbo_frame_tag "astra_modal"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def astra_modal(title:, **options, &block)
|
|
9
|
+
render Hotwire::AstraUi::ModalComponent.new(title: title, **options) do
|
|
10
|
+
capture(&block)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def close_astra_modal_tag(id: "astra_modal")
|
|
15
|
+
turbo_stream.replace id, html: ""
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["container", "backdrop", "body"]
|
|
5
|
+
static values = {
|
|
6
|
+
title: String,
|
|
7
|
+
size: { type: String, default: "md" },
|
|
8
|
+
backdropClose: { type: Boolean, default: true }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.buildModal()
|
|
13
|
+
this.animateIn()
|
|
14
|
+
document.body.style.overflow = "hidden"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
document.body.style.overflow = "auto"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
buildModal() {
|
|
22
|
+
const originalContent = this.element.innerHTML
|
|
23
|
+
|
|
24
|
+
this.element.innerHTML = `
|
|
25
|
+
<div class="astra-modal-backdrop" data-astra-ui-shell-target="backdrop" data-action="click->astra-ui-shell#backdropClick">
|
|
26
|
+
<div class="astra-modal-container astra-modal-size-${this.sizeValue}" data-astra-ui-shell-target="container" data-action="click->astra-ui-shell#stopPropagation">
|
|
27
|
+
<div class="astra-header">
|
|
28
|
+
<h3>${this.titleValue || ""}</h3>
|
|
29
|
+
<button type="button" data-action="astra-ui-shell#close" style="cursor:pointer;border:none;background:none;font-size:1.5rem;">×</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="astra-body" data-astra-ui-shell-target="body">
|
|
32
|
+
${originalContent}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
animateIn() {
|
|
40
|
+
requestAnimationFrame(() => {
|
|
41
|
+
this.backdropTarget.classList.add("astra-enter-active")
|
|
42
|
+
this.containerTarget.classList.add("astra-enter-active")
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
backdropClick(e) {
|
|
47
|
+
if (!this.backdropCloseValue) return
|
|
48
|
+
if (e.target === this.backdropTarget) this.close()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
stopPropagation(e) {
|
|
52
|
+
e.stopPropagation()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async close() {
|
|
56
|
+
this.backdropTarget.classList.remove("astra-enter-active")
|
|
57
|
+
this.containerTarget.classList.remove("astra-enter-active")
|
|
58
|
+
await new Promise(res => setTimeout(res, 300))
|
|
59
|
+
|
|
60
|
+
const frame = this.element.closest("turbo-frame")
|
|
61
|
+
if (frame) {
|
|
62
|
+
frame.src = null
|
|
63
|
+
frame.innerHTML = ""
|
|
64
|
+
} else {
|
|
65
|
+
this.element.innerHTML = ""
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["container", "backdrop"]
|
|
5
|
+
static values = {
|
|
6
|
+
backdropClose: { type: Boolean, default: true },
|
|
7
|
+
returnFocus: String
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.previouslyFocused = document.activeElement
|
|
12
|
+
this.handleKeydown = this.handleKeydown.bind(this)
|
|
13
|
+
document.addEventListener("keydown", this.handleKeydown)
|
|
14
|
+
|
|
15
|
+
this.adjustZIndex()
|
|
16
|
+
this.animateIn()
|
|
17
|
+
this.lockScroll()
|
|
18
|
+
this.focusFirst()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
disconnect() {
|
|
22
|
+
document.removeEventListener("keydown", this.handleKeydown)
|
|
23
|
+
this.restoreFocus()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
adjustZIndex() {
|
|
27
|
+
// Check how many modals are currently open
|
|
28
|
+
const openModals = this.openModals().length
|
|
29
|
+
if (openModals > 1) {
|
|
30
|
+
// Offset the Z-index so the new one is on top
|
|
31
|
+
const baseZ = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--astra-modal-z')) || 1000
|
|
32
|
+
this.element.style.zIndex = baseZ + (openModals * 10)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
lockScroll() {
|
|
37
|
+
if (this.openModals().length === 1) {
|
|
38
|
+
document.body.style.overflow = "hidden"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
animateIn() {
|
|
43
|
+
this.backdropTarget.classList.add("astra-backdrop-enter")
|
|
44
|
+
|
|
45
|
+
requestAnimationFrame(() => {
|
|
46
|
+
this.containerTarget.classList.add("astra-enter-active")
|
|
47
|
+
this.backdropTarget.classList.add("astra-backdrop-enter-active")
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async close(e = null) {
|
|
52
|
+
if (e) e.preventDefault()
|
|
53
|
+
if (!this.isTopmost()) return
|
|
54
|
+
|
|
55
|
+
this.containerTarget.classList.remove("astra-enter-active")
|
|
56
|
+
this.backdropTarget.classList.remove("astra-backdrop-enter-active")
|
|
57
|
+
|
|
58
|
+
await new Promise(res => setTimeout(res, this.transitionDurationMs()))
|
|
59
|
+
|
|
60
|
+
// Important: Only clear the specific frame this modal lives in
|
|
61
|
+
const frame = this.element.closest("turbo-frame")
|
|
62
|
+
frame.src = null
|
|
63
|
+
frame.innerHTML = ""
|
|
64
|
+
|
|
65
|
+
// Only restore scrolling if this was the last modal
|
|
66
|
+
if (this.openModals().length === 0) {
|
|
67
|
+
document.body.style.overflow = "auto"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
backdropClick(e) {
|
|
72
|
+
if (!this.backdropCloseValue) return
|
|
73
|
+
if (e.target === this.backdropTarget) {
|
|
74
|
+
this.close(e)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stopPropagation(e) {
|
|
79
|
+
e.stopPropagation()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
handleKeydown(e) {
|
|
83
|
+
if (!this.isTopmost()) return
|
|
84
|
+
|
|
85
|
+
if (e.key === "Escape") {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
this.close()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (e.key === "Tab") {
|
|
92
|
+
this.trapFocus(e)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isTopmost() {
|
|
97
|
+
const modals = this.openModals()
|
|
98
|
+
return modals.length > 0 && modals[modals.length - 1] === this.backdropTarget
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
openModals() {
|
|
102
|
+
return Array.from(document.querySelectorAll('.astra-modal-backdrop'))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
focusableElements() {
|
|
106
|
+
return Array.from(
|
|
107
|
+
this.containerTarget.querySelectorAll(
|
|
108
|
+
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
focusFirst() {
|
|
114
|
+
const focusables = this.focusableElements()
|
|
115
|
+
if (focusables.length > 0) {
|
|
116
|
+
focusables[0].focus()
|
|
117
|
+
} else {
|
|
118
|
+
this.containerTarget.focus()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
trapFocus(e) {
|
|
123
|
+
const focusables = this.focusableElements()
|
|
124
|
+
if (focusables.length === 0) {
|
|
125
|
+
e.preventDefault()
|
|
126
|
+
this.containerTarget.focus()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const first = focusables[0]
|
|
131
|
+
const last = focusables[focusables.length - 1]
|
|
132
|
+
|
|
133
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
134
|
+
e.preventDefault()
|
|
135
|
+
last.focus()
|
|
136
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
137
|
+
e.preventDefault()
|
|
138
|
+
first.focus()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
restoreFocus() {
|
|
143
|
+
if (this.hasReturnFocusValue) {
|
|
144
|
+
const target = document.querySelector(this.returnFocusValue)
|
|
145
|
+
if (target && target.focus) {
|
|
146
|
+
target.focus()
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.previouslyFocused && this.previouslyFocused.focus) {
|
|
152
|
+
this.previouslyFocused.focus()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
transitionDurationMs() {
|
|
157
|
+
const raw = getComputedStyle(document.documentElement).getPropertyValue('--astra-transition-duration').trim()
|
|
158
|
+
if (raw.endsWith("ms")) return parseFloat(raw)
|
|
159
|
+
if (raw.endsWith("s")) return parseFloat(raw) * 1000
|
|
160
|
+
const parsed = parseFloat(raw)
|
|
161
|
+
return Number.isNaN(parsed) ? 300 : parsed
|
|
162
|
+
}
|
|
163
|
+
}
|
data/config/importmap.rb
ADDED
data/config/routes.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module HotwireAstraUi
|
|
2
|
+
module Generators
|
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
|
5
|
+
|
|
6
|
+
desc "Configures the application for Hotwire Astra UI"
|
|
7
|
+
|
|
8
|
+
def add_turbo_frame_to_layout
|
|
9
|
+
layout_file = "app/views/layouts/application.html.erb"
|
|
10
|
+
|
|
11
|
+
if File.exist?(layout_file)
|
|
12
|
+
# Check if it's already there to avoid duplicates
|
|
13
|
+
if File.read(layout_file).include?('turbo_frame_tag "astra_modal"')
|
|
14
|
+
say_status("skipped", "turbo_frame_tag already exists in application.html.erb", :yellow)
|
|
15
|
+
else
|
|
16
|
+
# Insert before the closing body tag
|
|
17
|
+
insert_into_file layout_file, "\n <%= turbo_frame_tag \"astra_modal\" %>", before: /^ <\/body>/
|
|
18
|
+
say_status("inserted", "turbo_frame_tag into application.html.erb", :green)
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
say_status("error", "application.html.erb not found. Please add <%= turbo_frame_tag 'astra_modal' %> manually.", :red)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_initializer
|
|
26
|
+
initializer "hotwire_astra_ui.rb" do
|
|
27
|
+
<<~RUBY
|
|
28
|
+
# Configuration for Hotwire Astra UI
|
|
29
|
+
# You can add global theme settings here later
|
|
30
|
+
RUBY
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def register_stimulus_controller
|
|
35
|
+
controllers_index = "app/javascript/controllers/index.js"
|
|
36
|
+
import_line = 'import HotwireAstraUiModalController from "hotwire_astra_ui/modal_controller"'
|
|
37
|
+
register_line = 'application.register("hotwire-astra-ui-modal", HotwireAstraUiModalController)'
|
|
38
|
+
|
|
39
|
+
if File.exist?(controllers_index)
|
|
40
|
+
file_contents = File.read(controllers_index)
|
|
41
|
+
|
|
42
|
+
if file_contents.include?(import_line) || file_contents.include?(register_line)
|
|
43
|
+
say_status("skipped", "Stimulus controller already registered in controllers/index.js", :yellow)
|
|
44
|
+
else
|
|
45
|
+
append_to_file controllers_index, "\n#{import_line}\n#{register_line}\n"
|
|
46
|
+
say_status("inserted", "Registered Hotwire Astra UI Stimulus controller", :green)
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
say_status("warning", "controllers/index.js not found. Please register the controller manually.", :yellow)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def show_post_install_message
|
|
54
|
+
say "\n🚀 Hotwire Astra UI is installed!", :magenta
|
|
55
|
+
say "1. Use data-turbo-frame='astra_modal' on your links."
|
|
56
|
+
say "2. Render the component: render Hotwire::AstraUi::ModalComponent.new(title: 'Hello')"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Hotwire
|
|
2
|
+
module AstraUi
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
# This hook allows the host app to see the engine's assets
|
|
5
|
+
initializer "hotwire_astra_ui.assets" do |app|
|
|
6
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# This automatically pins the engine's JS in the host's importmap
|
|
10
|
+
initializer "hotwire_astra_ui.importmap", before: "importmap" do |app|
|
|
11
|
+
(app.config.importmap.paths ||= []) << root.join("config/importmap.rb")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require "importmap-rails"
|
|
2
|
+
require "view_component"
|
|
3
|
+
require "hotwire/astra_ui/version"
|
|
4
|
+
require "hotwire/astra_ui/engine"
|
|
5
|
+
|
|
6
|
+
module Hotwire
|
|
7
|
+
module AstraUi
|
|
8
|
+
# Namespace module for the gem. Loading this file wires up the engine
|
|
9
|
+
# and version, which is enough for the host app to mount assets and
|
|
10
|
+
# importmap pins.
|
|
11
|
+
end
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hotwire-astra-ui
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Fernand
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 8.1.2
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 8.1.2
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: view_component
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: turbo-rails
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: stimulus-rails
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: importmap-rails
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
description: Hotwire Astra UI provides production-ready, accessible modal dialogs
|
|
84
|
+
for Rails apps using Turbo Frames and Stimulus. Importmap-friendly, server-rendered,
|
|
85
|
+
and framework-free, with stacked modals, CSS-driven animations, and full customization
|
|
86
|
+
via CSS variables.
|
|
87
|
+
email:
|
|
88
|
+
- dev.cloud.asm@gmail.com
|
|
89
|
+
executables: []
|
|
90
|
+
extensions: []
|
|
91
|
+
extra_rdoc_files: []
|
|
92
|
+
files:
|
|
93
|
+
- MIT-LICENSE
|
|
94
|
+
- README.md
|
|
95
|
+
- Rakefile
|
|
96
|
+
- app/assets/stylesheets/hotwire/astra_ui/astra_ui.css
|
|
97
|
+
- app/components/hotwire/astra_ui/modal_component.html.erb
|
|
98
|
+
- app/components/hotwire/astra_ui/modal_component.rb
|
|
99
|
+
- app/helpers/hotwire/astra_ui/modal_helper.rb
|
|
100
|
+
- app/javascript/hotwire-astra-ui.js
|
|
101
|
+
- app/javascript/hotwire_astra_ui/modal_controller.js
|
|
102
|
+
- config/importmap.rb
|
|
103
|
+
- config/routes.rb
|
|
104
|
+
- lib/generators/hotwire_astra_ui/install_generator.rb
|
|
105
|
+
- lib/hotwire/astra_ui/engine.rb
|
|
106
|
+
- lib/hotwire/astra_ui/ui.rb
|
|
107
|
+
- lib/hotwire/astra_ui/version.rb
|
|
108
|
+
- lib/tasks/hotwire/astra/ui_tasks.rake
|
|
109
|
+
homepage: https://www.digi-archive.com/articles/hotwire-astra-ui
|
|
110
|
+
licenses:
|
|
111
|
+
- MIT
|
|
112
|
+
metadata:
|
|
113
|
+
allowed_push_host: https://rubygems.org/
|
|
114
|
+
homepage_uri: https://www.digi-archive.com/articles/hotwire-astra-ui
|
|
115
|
+
source_code_uri: https://github.com/wwwfernand/hotwire-astra-ui
|
|
116
|
+
changelog_uri: https://github.com/wwwfernand/hotwire-astra-ui/blob/main/CHANGELOG.md
|
|
117
|
+
post_install_message:
|
|
118
|
+
rdoc_options: []
|
|
119
|
+
require_paths:
|
|
120
|
+
- lib
|
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
requirements: []
|
|
132
|
+
rubygems_version: 3.5.11
|
|
133
|
+
signing_key:
|
|
134
|
+
specification_version: 4
|
|
135
|
+
summary: Accessible Hotwire modal component for Rails (Turbo + Stimulus) with Importmap
|
|
136
|
+
support
|
|
137
|
+
test_files: []
|