atomic_view 0.3.0 → 0.4.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 +4 -4
- data/lib/atomic_view/components/lazy_pagination_component.html.erb +8 -0
- data/lib/atomic_view/components/lazy_pagination_component.rb +43 -0
- data/lib/atomic_view/components/table_component/cell_component.html.erb +6 -0
- data/lib/atomic_view/components/table_component/cell_component.rb +58 -0
- data/lib/atomic_view/components/table_component/column_component.rb +54 -0
- data/lib/atomic_view/components/table_component/row_component.html.erb +3 -0
- data/lib/atomic_view/components/table_component/row_component.rb +55 -0
- data/lib/atomic_view/components/table_component.rb +51 -6
- data/lib/atomic_view/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75aad701c9a9b4598ba4a086f8a3c1c37faaac0bbf23f4ef2927b4d667364f1b
|
|
4
|
+
data.tar.gz: 1a63769c7c94188b829105777227267be43df6480e6b2f864a5c7be4e831a179
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba9732b3bf6ed19951e2c29ac2feafa7af299679135645bf28650808fd9ff6a6c9e9c3d994be9d23319b69a4679e4e7a8d67a73ae46603a963544a11430e0579
|
|
7
|
+
data.tar.gz: 10aff2eb5c66efb09918961c87cc708941cd30721c769cd3c2402082d09898098b32389cdfffe52519bbd3bedfa34bb8f2f265694cb0ed5226148abcfcbcd058
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<% if next_page? %>
|
|
2
|
+
<%= tag.turbo_frame(id: id, loading: :lazy, src: next_page_path, class: "flex items-center justify-center gap-2 py-3") do %>
|
|
3
|
+
<span class="size-3.5 animate-spin rounded-full border-2 border-border border-t-foreground motion-reduce:animate-none" aria-hidden="true"></span>
|
|
4
|
+
<span class="sr-only">Loading more…</span>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% else %>
|
|
7
|
+
<%= tag.turbo_frame(id: id) %>
|
|
8
|
+
<% end %>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtomicView
|
|
4
|
+
module Components
|
|
5
|
+
# LazyPagination
|
|
6
|
+
#
|
|
7
|
+
# The infinite-scroll counterpart to PaginationComponent's page-number
|
|
8
|
+
# links -- use PaginationComponent for numbered pages, this one for a
|
|
9
|
+
# list that loads its next page automatically as the user scrolls.
|
|
10
|
+
# Stays equally pagination-gem agnostic: the caller resolves both
|
|
11
|
+
# `next_page` (any presence-checkable value, e.g. a Pagy `next` page
|
|
12
|
+
# number) and `next_page_path` (the URL that lazily loaded page comes
|
|
13
|
+
# from) themselves.
|
|
14
|
+
#
|
|
15
|
+
# render(AtomicView::Components::LazyPaginationComponent.new(
|
|
16
|
+
# next_page: @pagy.next,
|
|
17
|
+
# next_page_path: @pagy.next && bookings_path(page: @pagy.next)
|
|
18
|
+
# ))
|
|
19
|
+
#
|
|
20
|
+
# Renders a `<turbo-frame loading="lazy" src="...">` (the same
|
|
21
|
+
# `tag.turbo_frame` technique GanttComponent's row/date pagination
|
|
22
|
+
# uses -- see that class's docs -- rather than a hard dependency on
|
|
23
|
+
# turbo-rails' view helpers) wrapping a spinner while `next_page` is
|
|
24
|
+
# present. Once there's no next page, it renders an empty
|
|
25
|
+
# `<turbo-frame>` with the same `id` instead of nothing at all, so a
|
|
26
|
+
# later `turbo_stream.replace(id, ...)` response from the last real
|
|
27
|
+
# page's fetch always has a target to replace.
|
|
28
|
+
class LazyPaginationComponent < AtomicView::Component
|
|
29
|
+
attr_reader :next_page, :next_page_path, :id
|
|
30
|
+
|
|
31
|
+
def initialize(next_page:, next_page_path: nil, id: :pagination)
|
|
32
|
+
super()
|
|
33
|
+
@next_page = next_page
|
|
34
|
+
@next_page_path = next_page_path
|
|
35
|
+
@id = id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def next_page?
|
|
39
|
+
next_page.present?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<%= tag.td(**html_options, class: html_class) do %>
|
|
2
|
+
<% if linked? %>
|
|
3
|
+
<%= link_to "", path, class: "absolute inset-0", tabindex: (first ? 0 : -1), "aria-hidden": (first ? nil : "true"), "aria-label": (first ? label : nil), data: {row_link: true} %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<%= tag.span(content, class: "relative") %>
|
|
6
|
+
<% end %>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtomicView
|
|
4
|
+
module Components
|
|
5
|
+
class TableComponent
|
|
6
|
+
# TableComponent::Cell
|
|
7
|
+
#
|
|
8
|
+
# One <td> within a RowComponent -- see that class's docs for the
|
|
9
|
+
# standalone-render requirement and the per-cell stretched-link
|
|
10
|
+
# design. `first` and `clickable` are set by RowComponent's `cells`
|
|
11
|
+
# slot from the row itself, not passed by the caller.
|
|
12
|
+
#
|
|
13
|
+
# Visible content renders as a normal sibling of the (empty,
|
|
14
|
+
# transparent) stretched `<a>`, wrapped in its own `position:
|
|
15
|
+
# relative` span so a real link nested inside a cell's content (e.g.
|
|
16
|
+
# a second, more specific action) still stays clickable above the
|
|
17
|
+
# row-wide overlay.
|
|
18
|
+
class CellComponent < AtomicView::Component
|
|
19
|
+
attr_reader :path, :label, :first, :align, :clickable
|
|
20
|
+
|
|
21
|
+
def initialize(path:, label:, first:, align: :left, clickable: true, **options)
|
|
22
|
+
super()
|
|
23
|
+
@path = path
|
|
24
|
+
@label = label
|
|
25
|
+
@first = first
|
|
26
|
+
@align = align
|
|
27
|
+
@clickable = clickable
|
|
28
|
+
@options = options
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def linked?
|
|
32
|
+
clickable && path.present?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def html_class
|
|
36
|
+
class_names(base_classes, align_classes, @options[:class])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def html_options
|
|
40
|
+
@options.except(:class)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def base_classes
|
|
46
|
+
"relative px-2.5 py-2.5 border-b border-border/5"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def align_classes
|
|
50
|
+
case align
|
|
51
|
+
when :center then "text-center font-semibold"
|
|
52
|
+
when :right then "text-right"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtomicView
|
|
4
|
+
module Components
|
|
5
|
+
class TableComponent
|
|
6
|
+
# TableComponent::Column
|
|
7
|
+
#
|
|
8
|
+
# One <th> in the header row TableComponent auto-renders once at
|
|
9
|
+
# least one `with_column` is given -- see TableComponent's docs.
|
|
10
|
+
#
|
|
11
|
+
# `label` defaults to `model.human_attribute_name(attribute)`
|
|
12
|
+
# (mirroring Rails' own form label defaulting) when a `model:` is
|
|
13
|
+
# given -- TableComponent passes its own `model:` through to every
|
|
14
|
+
# column automatically, so callers only need `attribute:`. Pass
|
|
15
|
+
# `label:` directly instead for a column with no backing
|
|
16
|
+
# attribute (e.g. a blank header over a row of "Edit" links).
|
|
17
|
+
class ColumnComponent < AtomicView::Component
|
|
18
|
+
attr_reader :model, :attribute, :align, :width
|
|
19
|
+
|
|
20
|
+
def initialize(model: nil, attribute: nil, label: nil, align: :left, width: nil)
|
|
21
|
+
super()
|
|
22
|
+
raise ArgumentError, "label is required when model is not given" if model.nil? && label.nil?
|
|
23
|
+
|
|
24
|
+
@model = model
|
|
25
|
+
@attribute = attribute
|
|
26
|
+
@label = label
|
|
27
|
+
@align = align
|
|
28
|
+
@width = width
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def label
|
|
32
|
+
@label || model.human_attribute_name(attribute)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
tag.th(label, scope: "col", class: header_class)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def header_class
|
|
42
|
+
class_names("p-2 font-medium", align_class, width)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def align_class
|
|
46
|
+
case align
|
|
47
|
+
when :center then "text-center"
|
|
48
|
+
when :right then "text-right"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtomicView
|
|
4
|
+
module Components
|
|
5
|
+
class TableComponent
|
|
6
|
+
# TableComponent::Row
|
|
7
|
+
#
|
|
8
|
+
# A single <tr>, renderable standalone -- no parent TableComponent
|
|
9
|
+
# instance required. That matters because a turbo_stream
|
|
10
|
+
# `append`/`update` response renders exactly one row outside of any
|
|
11
|
+
# table context, e.g.:
|
|
12
|
+
#
|
|
13
|
+
# turbo_stream.append(:bookings_body, AtomicView::Components::TableComponent::RowComponent.new(
|
|
14
|
+
# record: @booking, label: @booking.camper_name, path: booking_path(@booking)
|
|
15
|
+
# ) { |row| row.with_cell { @booking.camper_name } })
|
|
16
|
+
#
|
|
17
|
+
# `path` defaults to `record` itself so `path_to(record)`-style route
|
|
18
|
+
# helpers resolve it via `to_model`/`to_param` the same way `link_to`
|
|
19
|
+
# would; pass `path:` explicitly when the row's destination isn't
|
|
20
|
+
# just `record`'s own show page.
|
|
21
|
+
#
|
|
22
|
+
# Each `with_cell` becomes its own stretched link to `path` -- a
|
|
23
|
+
# single `<a>` can't span multiple `<td>` siblings the way it could
|
|
24
|
+
# wrap a `<div>` row -- but only the first cell's link stays in the
|
|
25
|
+
# tab order (`tabindex="0"`; the rest are `tabindex="-1"` and
|
|
26
|
+
# `aria-hidden`), so a keyboard/screen-reader user hits one focus
|
|
27
|
+
# stop per row rather than one per cell. `label` is that first
|
|
28
|
+
# link's accessible name. Pass `clickable: false` for a row that
|
|
29
|
+
# shouldn't navigate anywhere -- cells then render as plain,
|
|
30
|
+
# unlinked content.
|
|
31
|
+
class RowComponent < AtomicView::Component
|
|
32
|
+
renders_many :cells, ->(**kwargs) { CellComponent.new(path: path, label: label, first: cells.empty?, clickable: clickable, **kwargs) }
|
|
33
|
+
|
|
34
|
+
attr_reader :record, :label, :path, :clickable
|
|
35
|
+
|
|
36
|
+
def initialize(record:, label:, path: nil, clickable: true, **options)
|
|
37
|
+
super()
|
|
38
|
+
@record = record
|
|
39
|
+
@label = label
|
|
40
|
+
@path = path || record
|
|
41
|
+
@clickable = clickable
|
|
42
|
+
@options = options
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def html_class
|
|
46
|
+
class_names(clickable ? "hover:bg-offset" : nil, @options[:class])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def html_options
|
|
50
|
+
@options.except(:class)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -4,10 +4,10 @@ module AtomicView
|
|
|
4
4
|
module Components
|
|
5
5
|
# Table
|
|
6
6
|
#
|
|
7
|
-
# A thin styling wrapper around <table>.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
7
|
+
# A thin styling wrapper around <table>. By default this owns no
|
|
8
|
+
# row/column concept -- the consuming view builds its own
|
|
9
|
+
# <thead>/<tbody> markup and calls the yielded component's #head_class
|
|
10
|
+
# / #row_class helpers to pick up the shared styling:
|
|
11
11
|
#
|
|
12
12
|
# <%= render AtomicView::Components::TableComponent.new do |table| %>
|
|
13
13
|
# <thead>
|
|
@@ -21,17 +21,53 @@ module AtomicView
|
|
|
21
21
|
# </tr>
|
|
22
22
|
# </tbody>
|
|
23
23
|
# <% end %>
|
|
24
|
+
#
|
|
25
|
+
# `with_column` is an opt-in alternative to hand-building the <thead>:
|
|
26
|
+
# give it at least one column and TableComponent renders the header row
|
|
27
|
+
# itself, from `<th>`s built by TableComponent::ColumnComponent -- you
|
|
28
|
+
# still own `<tbody>` content (typically TableComponent::RowComponent
|
|
29
|
+
# rows) in the block, same as before:
|
|
30
|
+
#
|
|
31
|
+
# <%= render AtomicView::Components::TableComponent.new(model: Booking) do |table| %>
|
|
32
|
+
# <% table.with_column(attribute: :camper_name) %>
|
|
33
|
+
# <% table.with_column(attribute: :status, align: :center) %>
|
|
34
|
+
# <% table.with_column(label: "") %>
|
|
35
|
+
#
|
|
36
|
+
# <tbody>
|
|
37
|
+
# <% @bookings.each do |booking| %>
|
|
38
|
+
# <%= render(AtomicView::Components::TableComponent::RowComponent.new(record: booking, label: booking.camper_name, path: booking_path(booking))) do |row| %>
|
|
39
|
+
# <% row.with_cell { booking.camper_name } %>
|
|
40
|
+
# <% row.with_cell(align: :center) { render(BadgeComponent.new) { booking.status } } %>
|
|
41
|
+
# <% row.with_cell { render(LinkComponent.new(edit_booking_path(booking), variant: :outline)) { "Edit" } } %>
|
|
42
|
+
# <% end %>
|
|
43
|
+
# <% end %>
|
|
44
|
+
# </tbody>
|
|
45
|
+
# <% end %>
|
|
46
|
+
#
|
|
47
|
+
# A table with no columns given renders exactly as before -- `with_column`
|
|
48
|
+
# is opt-in, not a second required API. `model:` is passed through to
|
|
49
|
+
# each column so `with_column(attribute: :camper_name)` can default its
|
|
50
|
+
# label via `model.human_attribute_name(:camper_name)`; a column with no
|
|
51
|
+
# backing attribute (like the blank "" header above, for an actions
|
|
52
|
+
# column) just passes `label:` directly instead.
|
|
24
53
|
class TableComponent < AtomicView::Component
|
|
25
54
|
HEAD_ROW_CLASSES = "text-left text-xs font-semibold uppercase tracking-wide text-muted-foreground border-b border-border"
|
|
26
55
|
BODY_ROW_CLASSES = "border-b border-border hover:bg-offset"
|
|
27
56
|
|
|
28
|
-
|
|
57
|
+
renders_many :columns, ->(**kwargs) { ColumnComponent.new(model: model, **kwargs) }
|
|
58
|
+
|
|
59
|
+
attr_reader :model
|
|
60
|
+
|
|
61
|
+
def initialize(model: nil, **options)
|
|
29
62
|
super()
|
|
63
|
+
@model = model
|
|
30
64
|
@options = options
|
|
31
65
|
end
|
|
32
66
|
|
|
33
67
|
def call
|
|
34
|
-
tag.table(**@options.except(:class), class: class_names(base_classes, @options[:class]))
|
|
68
|
+
tag.table(**@options.except(:class), class: class_names(base_classes, @options[:class])) do
|
|
69
|
+
safe_join([columns_header, content].compact)
|
|
70
|
+
end
|
|
35
71
|
end
|
|
36
72
|
|
|
37
73
|
def head_class(**options)
|
|
@@ -44,6 +80,15 @@ module AtomicView
|
|
|
44
80
|
|
|
45
81
|
private
|
|
46
82
|
|
|
83
|
+
def columns_header
|
|
84
|
+
return unless columns?
|
|
85
|
+
tag.thead { tag.tr(class: head_class) { safe_join(columns) } }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def columns?
|
|
89
|
+
columns.any?
|
|
90
|
+
end
|
|
91
|
+
|
|
47
92
|
def base_classes
|
|
48
93
|
"w-full text-sm"
|
|
49
94
|
end
|
data/lib/atomic_view/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joel Warrington
|
|
@@ -219,6 +219,8 @@ files:
|
|
|
219
219
|
- lib/atomic_view/components/index_page_component.rb
|
|
220
220
|
- lib/atomic_view/components/kbd_component.rb
|
|
221
221
|
- lib/atomic_view/components/label_component.rb
|
|
222
|
+
- lib/atomic_view/components/lazy_pagination_component.html.erb
|
|
223
|
+
- lib/atomic_view/components/lazy_pagination_component.rb
|
|
222
224
|
- lib/atomic_view/components/link_component.html.erb
|
|
223
225
|
- lib/atomic_view/components/link_component.rb
|
|
224
226
|
- lib/atomic_view/components/modal_component.html.erb
|
|
@@ -245,6 +247,11 @@ files:
|
|
|
245
247
|
- lib/atomic_view/components/show_page_component.rb
|
|
246
248
|
- lib/atomic_view/components/submit_component.rb
|
|
247
249
|
- lib/atomic_view/components/table_component.rb
|
|
250
|
+
- lib/atomic_view/components/table_component/cell_component.html.erb
|
|
251
|
+
- lib/atomic_view/components/table_component/cell_component.rb
|
|
252
|
+
- lib/atomic_view/components/table_component/column_component.rb
|
|
253
|
+
- lib/atomic_view/components/table_component/row_component.html.erb
|
|
254
|
+
- lib/atomic_view/components/table_component/row_component.rb
|
|
248
255
|
- lib/atomic_view/components/tabs_component.html.erb
|
|
249
256
|
- lib/atomic_view/components/tabs_component.rb
|
|
250
257
|
- lib/atomic_view/components/telephone_field_component.rb
|