atomic_view 0.5.2 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 165a6c2614a15ffc294ecb82a4c969e73bdc9821e05f5cd11961b2bc8670d863
4
- data.tar.gz: 19adaf0af4395f5880d1ad44a5f5fdb4b273d841a95ed5f072e88830c7521c18
3
+ metadata.gz: d26209da003c20d55578d1f8c1036c7f2d6d8f479e1eafadba499aebe8b417a6
4
+ data.tar.gz: '0781dfbfd8ddf9d8a1f2e5be5a13b1f2e3511cc776ba4feb39e439933885f1f7'
5
5
  SHA512:
6
- metadata.gz: 5beeb078b1ea0fff91b7ad5b9e13c8f3336da75a4eeb1f37f0f4686e2556bd1896b1dc3d87d5d4d562de8fe3b819e20b312c9f556411d49258172afc43c23ffe
7
- data.tar.gz: c69212fa8f25195f3724fa13a58db718235d0877379bd7ba7339c4354505d6e5af83430ca937ffd7957d137a4cd1496e3f272a1ee9ba8e358a39e7c9a5220bc7
6
+ metadata.gz: 242b73833f14000d4efdd0d41f2be4a5352bc8548fc8bfeddd90501578d8b351c85cfc034a9fc1d64f75809dc4fb206d02dfabc137aa81842d8408dfa4951c3b
7
+ data.tar.gz: f591c5a4004651687bdb6229c7b44a44b20084615c48e52d97cc378b339a67f6237ec33edbb8a80a3e2c41faba79b07055c4975749d846c0c2b05b5e287c871c
@@ -0,0 +1,26 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { open: Boolean }
5
+
6
+ connect() {
7
+ this.element.addEventListener("click", this.closeOnBackdrop)
8
+ if (this.openValue) this.open()
9
+ }
10
+
11
+ disconnect() {
12
+ this.element.removeEventListener("click", this.closeOnBackdrop)
13
+ }
14
+
15
+ open() {
16
+ this.element.showModal()
17
+ }
18
+
19
+ close() {
20
+ this.element.close()
21
+ }
22
+
23
+ closeOnBackdrop = (event) => {
24
+ if (event.target === this.element) this.close()
25
+ }
26
+ }
@@ -0,0 +1,27 @@
1
+ <%= tag.dialog(**html_options, id: id, class: html_class, data: data_attributes) do %>
2
+ <div class="flex items-center justify-between gap-4 border-b border-border p-4">
3
+ <% if title.present? %>
4
+ <h2 class="font-body text-lg font-semibold text-foreground"><%= title %></h2>
5
+ <% end %>
6
+
7
+ <div class="ml-auto flex items-center gap-2">
8
+ <% if actions? %>
9
+ <%= actions %>
10
+ <% end %>
11
+
12
+ <%= tag.button(type: "button", class: "inline-flex shrink-0 items-center text-muted-foreground hover:text-foreground", data: {action: "click->atomic-view--drawer#close"}, aria: {label: "Close"}) do %>
13
+ <%= close_icon %>
14
+ <% end %>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="flex-1 overflow-y-auto p-4 text-sm text-foreground">
19
+ <%= content %>
20
+ </div>
21
+
22
+ <% if footer? %>
23
+ <div class="flex justify-end gap-2 border-t border-border p-4">
24
+ <%= footer %>
25
+ </div>
26
+ <% end %>
27
+ <% end %>
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Drawer
6
+ #
7
+ # A side panel built on the native `<dialog>` element -- same structural
8
+ # trick as `ModalComponent` (see that component for the rationale), just
9
+ # pinned to one edge of the viewport (`side: :left`/`:right`, default
10
+ # `:right`) and stretched to full height instead of centered. `<dialog>`
11
+ # still gets focus-trap, `::backdrop` styling, and Esc-to-close for free;
12
+ # `atomic-view--drawer` (see
13
+ # `app/assets/javascripts/atomic_view/controllers/drawer_controller.js`)
14
+ # layers on `open`/`close` plus closing when the backdrop itself (not the
15
+ # content box) is clicked -- identical to Modal's controller.
16
+ #
17
+ # Layout is a fixed header (title, an optional `actions` slot for e.g. a
18
+ # secondary action button, and an always-present close button) and an
19
+ # optional fixed footer (`renders_one :footer`, typically Cancel/Confirm
20
+ # buttons) around a body that scrolls independently -- the body is the
21
+ # component's default slot (the block passed to `render`).
22
+ #
23
+ # A footer Confirm button that needs to submit a `form_with` in the body
24
+ # can't be nested inside it -- they're siblings, in separate header/
25
+ # body/footer containers. Give the form an explicit `id:` instead, and
26
+ # point the button at it with HTML5's `form="..."` attribute, which
27
+ # associates a button with a form by id no matter where in the DOM the
28
+ # button actually lives:
29
+ #
30
+ # render(DrawerComponent.new(id: "product-drawer", title: "Add a product")) do |drawer|
31
+ # drawer.with_footer do
32
+ # render ButtonComponent.new(nil, "Add product", {type: "submit", form: "product-drawer-form"})
33
+ # end
34
+ #
35
+ # form_with(model: @product, id: "product-drawer-form") { |form| ... }
36
+ # end
37
+ #
38
+ # == Turbo Streams: same content, opened two different ways
39
+ #
40
+ # A drawer's body is often "a page" -- e.g. a New Product form -- that
41
+ # should also work as an ordinary full-page route for a direct link,
42
+ # bookmark, or reload. Both cases render the exact same
43
+ # `DrawerComponent.new(id: "...", open: true) { ... }` call; the
44
+ # difference is only what's *around* it:
45
+ #
46
+ # * Direct visit (e.g. GET /products/new): render the underlying page
47
+ # (the product list) as normal, with this drawer -- `open: true` --
48
+ # included in that same response, already showing on top of it.
49
+ # * Opened from elsewhere via Turbo Streams: the triggering link/form
50
+ # targets `format: :turbo_stream`, and the response replaces or
51
+ # updates a container already sitting on the page with a fresh
52
+ # render of this same component (again `open: true`):
53
+ #
54
+ # <%= turbo_stream.update("product-drawer", render(
55
+ # AtomicView::Components::DrawerComponent.new(id: "product-drawer-dialog", open: true, title: "Add a product") do
56
+ # render "products/form"
57
+ # end
58
+ # )) %>
59
+ #
60
+ # Either way, Turbo inserts a brand new `<dialog>` node, so
61
+ # `atomic-view--drawer` never needs a bespoke "please open now" event to
62
+ # bridge the two cases -- `connect()` sees `open: true` and calls
63
+ # `showModal()` itself every time, whether that's on first paint or after
64
+ # a Turbo Stream swap. Closing it (backdrop click, Esc, or a Cancel
65
+ # button) is just `dialog.close()` -- the page behind it, already in the
66
+ # same document, is simply what's left after the drawer's gone; give the
67
+ # host app's own controller the job of updating the URL back if that
68
+ # matters.
69
+ class DrawerComponent < AtomicView::Component
70
+ renders_one :actions
71
+ renders_one :footer
72
+
73
+ attr_reader :id, :title, :side, :open
74
+
75
+ def initialize(id:, title: nil, side: :right, open: false, **options)
76
+ super()
77
+ @id = id
78
+ @title = title
79
+ @side = side
80
+ @open = open
81
+ @options = options
82
+ end
83
+
84
+ def html_options
85
+ @options.except(:class, :data)
86
+ end
87
+
88
+ def html_class
89
+ class_names(
90
+ # `hidden`/`open:flex` (not a bare `flex`) -- the dialog needs
91
+ # `display:flex` once shown (for the header/body/footer column
92
+ # layout), but a bare `flex` utility class is author-origin CSS
93
+ # that would permanently beat the user-agent stylesheet's
94
+ # `dialog:not([open]) { display: none }`, leaving the "closed"
95
+ # dialog fully rendered (and eating clicks) at all times. The
96
+ # `open:` variant (targeting the `[open]` attribute the browser
97
+ # itself adds/removes on showModal()/close()) keeps it hidden
98
+ # until actually shown, same as Modal gets for free by not
99
+ # setting `display` at all.
100
+ "m-0 hidden h-dvh max-h-none w-full flex-col bg-surface p-0 text-foreground shadow-panel open:flex backdrop:bg-backdrop sm:max-w-lg",
101
+ side_classes,
102
+ @options[:class]
103
+ )
104
+ end
105
+
106
+ def data_attributes
107
+ attributes = (@options[:data] || {}).merge(controller: "atomic-view--drawer")
108
+ attributes["atomic-view--drawer-open-value"] = true if open
109
+ attributes
110
+ end
111
+
112
+ def close_icon
113
+ icon("x-mark", variant: :mini, options: {class: "size-5"}).to_s.html_safe
114
+ end
115
+
116
+ private
117
+
118
+ def side_classes
119
+ (side == :left) ? "inset-y-0 left-0 right-auto" : "inset-y-0 right-0 left-auto"
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicView
4
- VERSION = "0.5.2"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic_view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Warrington
@@ -143,6 +143,7 @@ files:
143
143
  - app/assets/javascripts/atomic_view/controllers/auto_submit_controller.js
144
144
  - app/assets/javascripts/atomic_view/controllers/chip_controller.js
145
145
  - app/assets/javascripts/atomic_view/controllers/command_palette_controller.js
146
+ - app/assets/javascripts/atomic_view/controllers/drawer_controller.js
146
147
  - app/assets/javascripts/atomic_view/controllers/dropdown_controller.js
147
148
  - app/assets/javascripts/atomic_view/controllers/gantt_controller.js
148
149
  - app/assets/javascripts/atomic_view/controllers/hotkey_controller.js
@@ -200,6 +201,8 @@ files:
200
201
  - lib/atomic_view/components/date_select_component.rb
201
202
  - lib/atomic_view/components/datetime_local_field_component.rb
202
203
  - lib/atomic_view/components/datetime_select_component.rb
204
+ - lib/atomic_view/components/drawer_component.html.erb
205
+ - lib/atomic_view/components/drawer_component.rb
203
206
  - lib/atomic_view/components/dropdown_component.html.erb
204
207
  - lib/atomic_view/components/dropdown_component.rb
205
208
  - lib/atomic_view/components/email_field_component.rb