atomic_view 0.2.2 → 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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/atomic_view/controllers/tooltip_controller.js +35 -0
  3. data/lib/atomic_view/components/badge_component.rb +25 -5
  4. data/lib/atomic_view/components/card_component/section_component.rb +23 -0
  5. data/lib/atomic_view/components/card_component.html.erb +11 -0
  6. data/lib/atomic_view/components/card_component.rb +66 -5
  7. data/lib/atomic_view/components/concerns/button_variants.rb +1 -1
  8. data/lib/atomic_view/components/index_page_component.html.erb +35 -0
  9. data/lib/atomic_view/components/index_page_component.rb +68 -0
  10. data/lib/atomic_view/components/lazy_pagination_component.html.erb +8 -0
  11. data/lib/atomic_view/components/lazy_pagination_component.rb +43 -0
  12. data/lib/atomic_view/components/navigation_tree_component/item_component.html.erb +4 -4
  13. data/lib/atomic_view/components/navigation_tree_component/item_component.rb +2 -1
  14. data/lib/atomic_view/components/navigation_tree_component.html.erb +11 -3
  15. data/lib/atomic_view/components/navigation_tree_component.rb +32 -3
  16. data/lib/atomic_view/components/page_header_component.html.erb +17 -0
  17. data/lib/atomic_view/components/page_header_component.rb +53 -0
  18. data/lib/atomic_view/components/show_page_component.html.erb +28 -0
  19. data/lib/atomic_view/components/show_page_component.rb +72 -0
  20. data/lib/atomic_view/components/table_component/cell_component.html.erb +6 -0
  21. data/lib/atomic_view/components/table_component/cell_component.rb +58 -0
  22. data/lib/atomic_view/components/table_component/column_component.rb +54 -0
  23. data/lib/atomic_view/components/table_component/row_component.html.erb +3 -0
  24. data/lib/atomic_view/components/table_component/row_component.rb +55 -0
  25. data/lib/atomic_view/components/table_component.rb +51 -6
  26. data/lib/atomic_view/components/tooltip_component.html.erb +13 -0
  27. data/lib/atomic_view/components/tooltip_component.rb +97 -0
  28. data/lib/atomic_view/version.rb +1 -1
  29. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd4eda901113f75bade919c83f2f6e45bc8e01727ca22aed9b6c4ab85ef55693
4
- data.tar.gz: 566969e9ac8ba89e384e85b427f836a15226bf27834524caae0355efef80b39f
3
+ metadata.gz: 75aad701c9a9b4598ba4a086f8a3c1c37faaac0bbf23f4ef2927b4d667364f1b
4
+ data.tar.gz: 1a63769c7c94188b829105777227267be43df6480e6b2f864a5c7be4e831a179
5
5
  SHA512:
6
- metadata.gz: 76933653def4779402ce08951a2dc75642d4e1e5a6e350127944f2a598216b1d09ef929021e204780f2a3957ed7394cdeae40a09819be4c9ddf464c6de9f9711
7
- data.tar.gz: e05db9862990d64563fe7310828681d194f08c3ed6c33b9cb5b365c8fd944e121b76e001adeed404b54783b51ae85a4b2ac59f348571df5f039b8aca8bc598f6
6
+ metadata.gz: ba9732b3bf6ed19951e2c29ac2feafa7af299679135645bf28650808fd9ff6a6c9e9c3d994be9d23319b69a4679e4e7a8d67a73ae46603a963544a11430e0579
7
+ data.tar.gz: 10aff2eb5c66efb09918961c87cc708941cd30721c769cd3c2402082d09898098b32389cdfffe52519bbd3bedfa34bb8f2f265694cb0ed5226148abcfcbcd058
@@ -0,0 +1,35 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { computePosition, offset, flip, shift, autoUpdate } from "@floating-ui/dom"
3
+
4
+ export default class extends Controller {
5
+ static targets = ["trigger", "content"]
6
+ static values = { placement: { type: String, default: "top" } }
7
+
8
+ show() {
9
+ this.contentTarget.classList.remove("hidden")
10
+ this.cleanup = autoUpdate(this.triggerTarget, this.contentTarget, () => {
11
+ computePosition(this.triggerTarget, this.contentTarget, {
12
+ strategy: "fixed",
13
+ placement: this.placementValue,
14
+ middleware: [offset(8), flip(), shift({ padding: 8 })]
15
+ }).then(({ x, y }) => {
16
+ Object.assign(this.contentTarget.style, { left: `${x}px`, top: `${y}px` })
17
+ })
18
+ })
19
+ document.addEventListener("keydown", this.onKeydown)
20
+ }
21
+
22
+ hide() {
23
+ this.contentTarget.classList.add("hidden")
24
+ if (this.cleanup) this.cleanup()
25
+ document.removeEventListener("keydown", this.onKeydown)
26
+ }
27
+
28
+ onKeydown = (event) => {
29
+ if (event.key === "Escape") this.hide()
30
+ }
31
+
32
+ disconnect() {
33
+ this.hide()
34
+ }
35
+ }
@@ -2,6 +2,20 @@
2
2
 
3
3
  module AtomicView
4
4
  module Components
5
+ # Badge
6
+ #
7
+ # A small rectangular status/label pill. `default`/`secondary` are solid
8
+ # fills for general-purpose emphasis (a count, a category tag);
9
+ # `success`/`warning`/`destructive`/`info` are soft, bordered/tinted
10
+ # semantic colors for actual state (active/upcoming/cancelled/etc.) --
11
+ # the same `border-x bg-x/10 text-x` treatment AlertComponent already
12
+ # uses for its variants, just as a compact pill instead of a banner.
13
+ # `outline` is the neutral, colorless version of that same bordered
14
+ # style, for a status that doesn't map to any of the semantic colors.
15
+ #
16
+ # Every variant carries a `border` (transparent on the solid fills) so
17
+ # badges of different variants sitting side by side stay the same
18
+ # height regardless of which ones happen to have a visible border.
5
19
  class BadgeComponent < AtomicView::Component
6
20
  attr_reader :variant
7
21
 
@@ -18,19 +32,25 @@ module AtomicView
18
32
  private
19
33
 
20
34
  def base_classes
21
- "inline-flex items-center rounded-pill px-1.5 py-0.5 text-xs font-medium"
35
+ "inline-flex items-center rounded-btn border px-1.5 py-0.5 text-xs font-medium"
22
36
  end
23
37
 
24
38
  def variant_classes
25
39
  case variant
26
40
  when :secondary
27
- "bg-secondary text-secondary-foreground"
41
+ "border-transparent bg-secondary text-secondary-foreground"
28
42
  when :destructive
29
- "bg-destructive text-destructive-foreground"
43
+ "border-destructive bg-destructive/10 text-destructive"
44
+ when :success
45
+ "border-success bg-success/10 text-success"
46
+ when :warning
47
+ "border-warning bg-warning/10 text-warning"
48
+ when :info
49
+ "border-info bg-info/10 text-info"
30
50
  when :outline
31
- "bg-transparent border border-border text-foreground"
51
+ "border-border bg-transparent text-foreground"
32
52
  else
33
- "bg-primary text-primary-foreground"
53
+ "border-transparent bg-primary text-primary-foreground"
34
54
  end
35
55
  end
36
56
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ class CardComponent
6
+ # CardComponent::Section
7
+ #
8
+ # One row of a sectioned card -- see CardComponent's docs. Just a
9
+ # padded, free-form content block; CardComponent's `divide-y` wrapper
10
+ # is what adds the border between sections, not this component.
11
+ class SectionComponent < AtomicView::Component
12
+ def initialize(**options)
13
+ super()
14
+ @options = options
15
+ end
16
+
17
+ def call
18
+ tag.div(**@options.except(:class), class: class_names(CardComponent::PADDING_CLASSES, @options[:class])) { content }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ <%= tag.div(**@options.except(:class), class: html_class) do %>
2
+ <% if sectioned? %>
3
+ <%= tag.div(class: divider_class) do %>
4
+ <% sections.each do |section| %>
5
+ <%= section %>
6
+ <% end %>
7
+ <% end %>
8
+ <% else %>
9
+ <%= content %>
10
+ <% end %>
11
+ <% end %>
@@ -2,23 +2,84 @@
2
2
 
3
3
  module AtomicView
4
4
  module Components
5
+ # Card
6
+ #
7
+ # A generic surface container for grouping related content. `default` is
8
+ # a plain bordered panel; `destructive` is the same shape with a tinted
9
+ # red border/background, for a "danger zone" section (e.g. an archive or
10
+ # delete action with its consequences explained) -- the same
11
+ # `border-destructive bg-destructive/10` treatment AlertComponent already
12
+ # uses for its own error variant. The variant only colors the card
13
+ # itself; any heading/button inside it is the caller's own content.
14
+ #
15
+ # Pass plain content for a single padded panel. For a settings-list style
16
+ # card -- several rows, each with its own label/action, a divider
17
+ # between them but not around the outside -- use `with_section` instead:
18
+ #
19
+ # render(AtomicView::Components::CardComponent.new(variant: :destructive)) do |card|
20
+ # card.with_section do
21
+ # tag.div(class: "flex items-center justify-between gap-4") do
22
+ # tag.div { tag.p("Archive this rental", class: "font-medium") + tag.p("...", class: "text-sm text-muted-foreground") } +
23
+ # render(LinkComponent.new("#", variant: :muted)) { "Archive" }
24
+ # end
25
+ # end
26
+ #
27
+ # card.with_section { ... }
28
+ # end
29
+ #
30
+ # Each section gets the card's own padding (rather than the card as a
31
+ # whole), and a `divide-y` on the section container adds a border
32
+ # between consecutive sections automatically -- there's no first/last
33
+ # index to track, Tailwind's `divide-y` only ever borders the boundary
34
+ # between two siblings. A card with no sections given renders exactly as
35
+ # before -- `with_section` is opt-in, not a second required API.
5
36
  class CardComponent < AtomicView::Component
6
- attr_reader :hoverable
37
+ PADDING_CLASSES = "p-2.5 px-4"
7
38
 
8
- def initialize(hoverable: false, **options)
39
+ renders_many :sections, "SectionComponent"
40
+
41
+ attr_reader :hoverable, :variant
42
+
43
+ def initialize(variant: :default, hoverable: false, **options)
9
44
  super()
45
+ @variant = variant
10
46
  @hoverable = hoverable
11
47
  @options = options
12
48
  end
13
49
 
14
- def call
15
- tag.div(**@options.except(:class), class: class_names(base_classes, hoverable_classes, @options[:class])) { content }
50
+ def sectioned?
51
+ sections.any?
52
+ end
53
+
54
+ def html_class
55
+ class_names(base_classes, variant_classes, hoverable_classes, padding_class, @options[:class])
56
+ end
57
+
58
+ # The divider color between sections is tied to the variant rather
59
+ # than a flat `divide-border` -- a neutral gray divider reads fine on
60
+ # the plain surface background, but loses almost all contrast against
61
+ # a tinted `bg-destructive/10` panel in light mode.
62
+ def divider_class
63
+ class_names("divide-y", (variant == :destructive) ? "divide-destructive/30" : "divide-border")
16
64
  end
17
65
 
18
66
  private
19
67
 
20
68
  def base_classes
21
- "bg-surface rounded-card border border-border p-2.5"
69
+ "bg-surface rounded-card border"
70
+ end
71
+
72
+ def padding_class
73
+ PADDING_CLASSES unless sectioned?
74
+ end
75
+
76
+ def variant_classes
77
+ case variant
78
+ when :destructive
79
+ "border-destructive bg-destructive/10"
80
+ else
81
+ "border-border"
82
+ end
22
83
  end
23
84
 
24
85
  def hoverable_classes
@@ -7,7 +7,7 @@ module AtomicView
7
7
  private
8
8
 
9
9
  def base_classes
10
- "h-7 rounded-btn px-2.5 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
10
+ "h-9 rounded-btn px-2.5 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
11
11
  end
12
12
 
13
13
  def variant_classes
@@ -0,0 +1,35 @@
1
+ <% breadcrumbs_content = breadcrumbs.to_s %>
2
+ <% title_content = title.to_s %>
3
+ <% badge_content = badge.to_s %>
4
+ <% subtitle_content = subtitle.to_s %>
5
+ <% actions_content = actions.to_s %>
6
+
7
+ <%= tag.div(**@options.except(:class, :id), id: html_id, class: html_class) do %>
8
+ <%= tag.div(class: "mb-4") do %>
9
+ <%= render(AtomicView::Components::PageHeaderComponent.new) do |header| %>
10
+ <% header.with_breadcrumbs { breadcrumbs_content } if breadcrumbs? %>
11
+ <% header.with_title { title_content } if title? %>
12
+ <% header.with_badge { badge_content } if badge? %>
13
+ <% header.with_subtitle { subtitle_content } if subtitle? %>
14
+ <% header.with_actions { actions_content } if actions? %>
15
+ <% end %>
16
+ <% end %>
17
+
18
+ <% if filters? %>
19
+ <%= tag.div(filters, class: "mb-4") %>
20
+ <% end %>
21
+
22
+ <% if toolbar? %>
23
+ <%= tag.div(toolbar, class: "mb-4") %>
24
+ <% end %>
25
+
26
+ <% if empty? %>
27
+ <%= empty_state if empty_state? %>
28
+ <% else %>
29
+ <%= content %>
30
+
31
+ <% if pagination? %>
32
+ <%= tag.div(pagination, class: "mt-4") %>
33
+ <% end %>
34
+ <% end %>
35
+ <% end %>
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # IndexPage
6
+ #
7
+ # The layout shell for a collection page: a PageHeaderComponent up top,
8
+ # an optional `filters` row (e.g. status pills), an optional `toolbar`
9
+ # row below that (search, dropdowns), then either the default content
10
+ # block or an `empty_state` slot, and finally an optional `pagination`
11
+ # slot. This is layout only -- the content region has no idea what's
12
+ # inside it. It doesn't reference TableComponent, doesn't assume rows or
13
+ # columns, and doesn't care whether the caller renders a table, a card
14
+ # grid, or anything else. A smarter, column-aware table is an app-level
15
+ # concern to build on top of this, not something this component owns.
16
+ #
17
+ # render(AtomicView::Components::IndexPageComponent.new(empty: @rentals.empty?, dom_id: :rentals)) do |page|
18
+ # page.with_title { "Rentals" }
19
+ # page.with_subtitle { "#{@rental_count} rentals across #{@park_count} parks" }
20
+ # page.with_actions { render(LinkComponent.new(new_rental_path)) { "New rental" } }
21
+ # page.with_filters { render(SegmentedControlComponent.new(options: STATUS_OPTIONS, selected: params[:status] || "All")) }
22
+ # page.with_toolbar { render(SearchFieldComponent.new(...)) }
23
+ # page.with_pagination { render(PaginationComponent.new(current_page: @pagy.page, total_pages: @pagy.pages, path_for_page: ->(p) { rentals_path(page: p) })) }
24
+ # page.with_empty_state { render(EmptyStateComponent.new(title: "No rentals yet")) }
25
+ #
26
+ # render(AtomicView::Components::TableComponent.new) { ... }
27
+ # end
28
+ #
29
+ # `breadcrumbs`/`title`/`badge`/`subtitle`/`actions` are forwarded
30
+ # straight into an internally-rendered PageHeaderComponent -- see its
31
+ # docs for what each renders. `empty:` and `dom_id:` stay plain
32
+ # caller-supplied values rather than being derived from a model/Pagy
33
+ # object, so this component never needs to know about ActiveRecord or a
34
+ # pagination library -- an app-level wrapper (e.g. a `ListViewComponent`
35
+ # that already knows about `Pagy`/routes) is the right place to compute
36
+ # them before calling this one.
37
+ class IndexPageComponent < AtomicView::Component
38
+ renders_one :breadcrumbs
39
+ renders_one :title
40
+ renders_one :badge
41
+ renders_one :subtitle
42
+ renders_one :actions
43
+ renders_one :filters
44
+ renders_one :toolbar
45
+ renders_one :empty_state
46
+ renders_one :pagination
47
+
48
+ attr_reader :dom_id
49
+
50
+ def initialize(empty: false, dom_id: nil, **options)
51
+ super()
52
+ @empty = empty
53
+ @dom_id = dom_id
54
+ @options = options
55
+ end
56
+
57
+ def empty? = @empty
58
+
59
+ def html_class
60
+ class_names(@options[:class])
61
+ end
62
+
63
+ def html_id
64
+ @options[:id] || dom_id
65
+ end
66
+ end
67
+ end
68
+ end
@@ -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
@@ -4,10 +4,10 @@
4
4
  <% if icon_name.present? %>
5
5
  <%= icon(icon_name, options: {class: "size-4 shrink-0"}).to_s.html_safe %>
6
6
  <% end %>
7
- <%= label %>
8
- <%= icon("chevron-right", options: {class: "ml-auto size-4 shrink-0 transition-transform"}).to_s.html_safe %>
7
+ <span class="truncate group-data-[collapsed]/nav:hidden"><%= label %></span>
8
+ <%= icon("chevron-right", options: {class: "ml-auto size-4 shrink-0 transition-transform group-data-[collapsed]/nav:hidden"}).to_s.html_safe %>
9
9
  <% end %>
10
- <div class="ml-6 mt-1 flex flex-col gap-1">
10
+ <div class="ml-6 mt-1 flex flex-col gap-1 group-data-[collapsed]/nav:hidden">
11
11
  <% items.each do |item| %>
12
12
  <%= item %>
13
13
  <% end %>
@@ -18,6 +18,6 @@
18
18
  <% if icon_name.present? %>
19
19
  <%= icon(icon_name, options: {class: "size-4 shrink-0"}).to_s.html_safe %>
20
20
  <% end %>
21
- <%= label %>
21
+ <span class="truncate group-data-[collapsed]/nav:hidden"><%= label %></span>
22
22
  <% end %>
23
23
  <% end %>
@@ -38,7 +38,8 @@ module AtomicView
38
38
  private
39
39
 
40
40
  def base_classes
41
- "flex items-center gap-2 rounded-btn px-2.5 py-1.5 text-sm font-medium"
41
+ "flex items-center gap-2 rounded-btn px-2.5 py-1.5 text-sm font-medium " \
42
+ "group-data-[collapsed]/nav:justify-center group-data-[collapsed]/nav:px-2"
42
43
  end
43
44
 
44
45
  def active_classes
@@ -1,8 +1,16 @@
1
- <%= tag.nav(**@options.except(:class), class: container_class) do %>
2
- <% if label.present? %>
1
+ <%= tag.nav(**html_options, class: container_class, data: data_attributes) do %>
2
+ <% if label.present? && !collapsed? %>
3
3
  <div class="px-2.5 pb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground"><%= label %></div>
4
4
  <% end %>
5
5
  <% items.each do |item| %>
6
- <%= item %>
6
+ <% if collapsed? %>
7
+ <%= render(AtomicView::Components::TooltipComponent.new(text: item.label, placement: tooltip_placement)) do |tooltip| %>
8
+ <% tooltip.with_trigger do %>
9
+ <%= item %>
10
+ <% end %>
11
+ <% end %>
12
+ <% else %>
13
+ <%= item %>
14
+ <% end %>
7
15
  <% end %>
8
16
  <% end %>
@@ -21,19 +21,48 @@ module AtomicView
21
21
  # as a disclosure (a `<summary>` row with a trailing chevron) whose own
22
22
  # `with_item` children render nested inside it once open -- see
23
23
  # `ItemComponent` for the per-item API.
24
+ #
25
+ # Pass `collapsed: true` for an icon-only rail -- labels, chevrons, and
26
+ # any open group's nested items are hidden with CSS (a `group/nav`
27
+ # marker plus `group-data-[collapsed]/nav:*` variants on each item, the
28
+ # same "no JS" approach as the disclosure groups above), not swapped out
29
+ # in Ruby. Each top-level item is automatically wrapped in a
30
+ # `TooltipComponent` showing its `label:` on hover, since the icon alone
31
+ # loses the item's name -- nested items stay unwrapped, since their
32
+ # container is hidden while collapsed (unreachable in a rail without a
33
+ # flyout, which is out of scope here):
34
+ #
35
+ # render(NavigationTreeComponent.new(collapsed: true)) do |tree|
36
+ # tree.with_item(label: "Overview", href: "#", icon: "home", active: true)
37
+ # end
24
38
  class NavigationTreeComponent < AtomicView::Component
25
39
  renders_many :items, "ItemComponent"
26
40
 
27
- attr_reader :label
41
+ attr_reader :label, :tooltip_placement
28
42
 
29
- def initialize(label: nil, **options)
43
+ def initialize(label: nil, collapsed: false, tooltip_placement: "right", **options)
30
44
  super()
31
45
  @label = label
46
+ @collapsed = collapsed
47
+ @tooltip_placement = tooltip_placement
32
48
  @options = options
33
49
  end
34
50
 
51
+ def collapsed?
52
+ @collapsed
53
+ end
54
+
35
55
  def container_class
36
- class_names("flex flex-col gap-1", @options[:class])
56
+ class_names("group/nav flex flex-col gap-1", collapsed? ? "items-center" : nil, @options[:class])
57
+ end
58
+
59
+ def html_options
60
+ @options.except(:class, :data)
61
+ end
62
+
63
+ def data_attributes
64
+ attributes = @options[:data] || {}
65
+ collapsed? ? attributes.merge(collapsed: true) : attributes
37
66
  end
38
67
  end
39
68
  end
@@ -0,0 +1,17 @@
1
+ <%= tag.div(**@options.except(:class), class: html_class) do %>
2
+ <div>
3
+ <% if breadcrumbs? %>
4
+ <div class="mb-1 text-xs font-semibold text-primary"><%= breadcrumbs %></div>
5
+ <% end %>
6
+ <div class="flex items-center gap-2">
7
+ <h1 class="text-2xl font-bold text-foreground sm:text-xl"><%= title %></h1>
8
+ <% if badge? %><%= badge %><% end %>
9
+ </div>
10
+ <% if subtitle? %>
11
+ <div class="mt-1 text-sm text-muted-foreground"><%= subtitle %></div>
12
+ <% end %>
13
+ </div>
14
+ <% if actions? %>
15
+ <div class="flex shrink-0 items-center gap-2"><%= actions %></div>
16
+ <% end %>
17
+ <% end %>
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # PageHeader
6
+ #
7
+ # The title/breadcrumb/badge/actions cluster shared by IndexPageComponent
8
+ # and ShowPageComponent (and usable standalone, e.g. above a lazily-loaded
9
+ # turbo-frame fragment that has no page shell of its own). Every region is
10
+ # a free-form slot -- this component only lays them out, it has no
11
+ # opinion about hrefs, badge variants, or what an action is.
12
+ #
13
+ # render(AtomicView::Components::PageHeaderComponent.new) do |header|
14
+ # header.with_breadcrumbs { link_to "Rentals", rentals_path, class: "text-xs font-semibold text-primary" }
15
+ # header.with_title { "RS002 · Karen Wilson" }
16
+ # header.with_badge { render(BadgeComponent.new) { "Active" } }
17
+ # header.with_subtitle { "rental_9f21ba7c" }
18
+ # header.with_actions { render(LinkComponent.new(edit_rental_path(@rental), variant: :outline)) { "Edit" } }
19
+ # end
20
+ #
21
+ # `title` is the one slot every caller is expected to fill -- there's no
22
+ # runtime enforcement of that (this gem doesn't raise on missing slots
23
+ # elsewhere either, e.g. DropdownComponent's `menu`), just a documented
24
+ # expectation. `badge` renders inline right after the title (e.g. a
25
+ # status pill); `subtitle` renders on its own line below (a record count,
26
+ # a copyable reference id); `breadcrumbs` renders above the title as a
27
+ # small eyebrow link; `actions` renders as a button/link cluster on the
28
+ # opposite side of the row, wrapping above the title/breadcrumb column on
29
+ # narrow viewports.
30
+ class PageHeaderComponent < AtomicView::Component
31
+ renders_one :breadcrumbs
32
+ renders_one :title
33
+ renders_one :badge
34
+ renders_one :subtitle
35
+ renders_one :actions
36
+
37
+ def initialize(**options)
38
+ super()
39
+ @options = options
40
+ end
41
+
42
+ def html_class
43
+ class_names(base_classes, @options[:class])
44
+ end
45
+
46
+ private
47
+
48
+ def base_classes
49
+ "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,28 @@
1
+ <% breadcrumbs_content = breadcrumbs.to_s %>
2
+ <% title_content = title.to_s %>
3
+ <% badge_content = badge.to_s %>
4
+ <% subtitle_content = subtitle.to_s %>
5
+ <% actions_content = actions.to_s %>
6
+
7
+ <%= tag.article(**@options.except(:class), class: html_class) do %>
8
+ <%= tag.div(class: "mb-8") do %>
9
+ <%= render(AtomicView::Components::PageHeaderComponent.new) do |header| %>
10
+ <% header.with_breadcrumbs { breadcrumbs_content } if breadcrumbs? %>
11
+ <% header.with_title { title_content } if title? %>
12
+ <% header.with_badge { badge_content } if badge? %>
13
+ <% header.with_subtitle { subtitle_content } if subtitle? %>
14
+ <% header.with_actions { actions_content } if actions? %>
15
+ <% end %>
16
+ <% end %>
17
+
18
+ <% if highlight? %>
19
+ <%= tag.div(highlight, class: "mb-8") %>
20
+ <% end %>
21
+
22
+ <%= tag.div(class: body_class) do %>
23
+ <% if sidebar? %>
24
+ <%= tag.div(sidebar, class: sidebar_class) %>
25
+ <% end %>
26
+ <%= tag.div(content, class: main_class) %>
27
+ <% end %>
28
+ <% end %>
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # ShowPage
6
+ #
7
+ # The layout shell for a single-record detail page: a PageHeaderComponent
8
+ # up top, an optional full-width `highlight` region below it, then a
9
+ # two-column body -- the default content block as the main column, and an
10
+ # optional `sidebar` slot as a second column. This is layout only -- it
11
+ # has no opinion about what a record's detail page actually contains.
12
+ # `highlight` is deliberately not called "stepper" or "progress": it's
13
+ # just a full-width slot for whatever sits between the header and the
14
+ # body (a lifecycle stepper, a banner, nothing at all) -- naming it after
15
+ # a specific piece of content would smuggle a content assumption into a
16
+ # layout component. Same reasoning for `sidebar`: it's a column, not a
17
+ # "details panel" -- fill it with a CardComponent, a plain `<dl>`,
18
+ # whatever the record calls for.
19
+ #
20
+ # render(AtomicView::Components::ShowPageComponent.new) do |page|
21
+ # page.with_breadcrumbs { link_to "Rentals", rentals_path, class: "text-xs font-semibold text-primary" }
22
+ # page.with_title { "RS002 · Karen Wilson" }
23
+ # page.with_badge { render(BadgeComponent.new) { "Active" } }
24
+ # page.with_actions { render(LinkComponent.new(edit_rental_path(@rental), variant: :outline)) { "Edit" } }
25
+ # page.with_highlight { render(RentalStepperComponent.new(rental: @rental)) }
26
+ # page.with_sidebar { render "rentals/details_panel", rental: @rental }
27
+ #
28
+ # tag.section { tag.h2("Overview", class: "text-lg font-semibold mb-2") + render(CardComponent.new) { @rental.notes } }
29
+ # end
30
+ #
31
+ # `breadcrumbs`/`title`/`badge`/`subtitle`/`actions` are forwarded
32
+ # straight into an internally-rendered PageHeaderComponent -- see its
33
+ # docs for what each renders. The default content block is the main
34
+ # column; when `sidebar` is omitted, the main column spans full width
35
+ # rather than leaving an empty second column.
36
+ #
37
+ # The two-column grid (sidebar first in document order, reordered after
38
+ # the main column from `sm:` up) matches every existing hand-rolled show
39
+ # page in the consuming app -- that's the "good defaults" this component
40
+ # exists to stop re-typing, not a novel layout choice.
41
+ class ShowPageComponent < AtomicView::Component
42
+ renders_one :breadcrumbs
43
+ renders_one :title
44
+ renders_one :badge
45
+ renders_one :subtitle
46
+ renders_one :actions
47
+ renders_one :highlight
48
+ renders_one :sidebar
49
+
50
+ def initialize(**options)
51
+ super()
52
+ @options = options
53
+ end
54
+
55
+ def html_class
56
+ class_names(@options[:class])
57
+ end
58
+
59
+ def body_class
60
+ class_names("grid grid-cols-1 gap-8", {"sm:grid-cols-3 sm:gap-12" => sidebar?})
61
+ end
62
+
63
+ def sidebar_class
64
+ "flex flex-col gap-8 sm:order-2 sm:col-span-1"
65
+ end
66
+
67
+ def main_class
68
+ class_names("flex flex-col gap-4 sm:gap-8", {"sm:order-1 sm:col-span-2" => sidebar?})
69
+ end
70
+ end
71
+ end
72
+ 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,3 @@
1
+ <%= tag.tr(**html_options, class: html_class) do %>
2
+ <%= safe_join(cells) %>
3
+ <% 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>. Consistent with the rest of the
8
- # gem, this does not own row/column iteration -- the consuming view
9
- # builds its own <thead>/<tbody> markup and calls the yielded component's
10
- # #head_class / #row_class helpers to pick up the shared styling.
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
- def initialize(**options)
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])) { content }
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
@@ -0,0 +1,13 @@
1
+ <%= tag.span(**html_options, class: html_class, data: data_attributes) do %>
2
+ <%= tag.span(class: "inline-block", data: trigger_data_attributes, aria: {describedby: content_id}) do %>
3
+ <%= trigger %>
4
+ <% end %>
5
+
6
+ <%= tag.span(id: content_id, role: "tooltip", class: content_class, data: content_data_attributes) do %>
7
+ <% if body? %>
8
+ <%= body %>
9
+ <% else %>
10
+ <%= text %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Tooltip
6
+ #
7
+ # A trigger plus a floating label, positioned by `@floating-ui/dom` and
8
+ # wired to `atomic-view--tooltip` (see
9
+ # `app/assets/javascripts/atomic_view/controllers/tooltip_controller.js`).
10
+ # The controller shows the tooltip on hover/focus of the trigger and
11
+ # hides it on mouseleave/blur/Esc, keeping it anchored to the trigger
12
+ # while the page scrolls or resizes.
13
+ #
14
+ # The bubble uses `strategy: "fixed"` (and a `fixed` class, not
15
+ # `absolute`) rather than `DropdownComponent`'s approach -- an
16
+ # absolutely-positioned bubble's containing block is the trigger's own
17
+ # `relative` wrapper span, so its shrink-to-fit width silently caps out
18
+ # at the *trigger's* rendered width no matter how generous `max-w-*` is.
19
+ # `fixed` makes the viewport the containing block instead, so
20
+ # `content_class` (below) actually controls the bubble's width.
21
+ #
22
+ # Pass `text:` for the common case of a short plain-text label:
23
+ #
24
+ # render(TooltipComponent.new(text: "Delete")) do |tooltip|
25
+ # tooltip.with_trigger { icon_button }
26
+ # end
27
+ #
28
+ # or a `body` slot for anything richer than plain text.
29
+ #
30
+ # The bubble defaults to `max-w-xs` (20rem) -- wide enough for a short
31
+ # label or two lines of `body` content. Pass `content_class:` to widen
32
+ # (or otherwise restyle) it for longer text:
33
+ #
34
+ # render(TooltipComponent.new(text: "...", content_class: "max-w-sm"))
35
+ #
36
+ # It's a separate option from `class:`, which targets the outer trigger
37
+ # wrapper -- `content_class:` is merged (via TailwindMerge) onto the
38
+ # floating bubble itself, so `max-w-sm` here replaces the default
39
+ # `max-w-xs` rather than fighting it.
40
+ class TooltipComponent < AtomicView::Component
41
+ renders_one :trigger
42
+ renders_one :body
43
+
44
+ PLACEMENTS = %w[top bottom left right].freeze
45
+
46
+ attr_reader :text, :placement
47
+
48
+ def initialize(text: nil, placement: "top", content_class: nil, **options)
49
+ super()
50
+ @text = text
51
+ @placement = PLACEMENTS.include?(placement) ? placement : "top"
52
+ @id = "tooltip-#{SecureRandom.hex(4)}"
53
+ @content_class = content_class
54
+ @options = options
55
+ end
56
+
57
+ def html_options
58
+ @options.except(:class, :data)
59
+ end
60
+
61
+ def html_class
62
+ class_names("relative inline-block", @options[:class])
63
+ end
64
+
65
+ def data_attributes
66
+ (@options[:data] || {}).merge(
67
+ :controller => "atomic-view--tooltip",
68
+ "atomic-view--tooltip-placement-value" => placement
69
+ )
70
+ end
71
+
72
+ def trigger_data_attributes
73
+ {
74
+ "atomic-view--tooltip-target" => "trigger",
75
+ :action => "mouseenter->atomic-view--tooltip#show mouseleave->atomic-view--tooltip#hide " \
76
+ "focusin->atomic-view--tooltip#show focusout->atomic-view--tooltip#hide"
77
+ }
78
+ end
79
+
80
+ def content_id
81
+ @id
82
+ end
83
+
84
+ def content_data_attributes
85
+ {"atomic-view--tooltip-target" => "content"}
86
+ end
87
+
88
+ def content_class
89
+ class_names(
90
+ "pointer-events-none hidden fixed z-50 max-w-xs rounded-well bg-foreground px-2 py-1 text-xs " \
91
+ "font-medium text-surface shadow-soft",
92
+ @content_class
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicView
4
- VERSION = "0.2.2"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic_view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Warrington
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-14 00:00:00.000000000 Z
11
+ date: 2026-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -148,6 +148,7 @@ files:
148
148
  - app/assets/javascripts/atomic_view/controllers/modal_controller.js
149
149
  - app/assets/javascripts/atomic_view/controllers/theme_toggle_controller.js
150
150
  - app/assets/javascripts/atomic_view/controllers/toast_controller.js
151
+ - app/assets/javascripts/atomic_view/controllers/tooltip_controller.js
151
152
  - app/assets/stylesheets/atomic_view/application.css
152
153
  - app/assets/stylesheets/atomic_view/application.tailwind.css
153
154
  - app/assets/tailwind/atomic_view/engine.css
@@ -171,7 +172,9 @@ files:
171
172
  - lib/atomic_view/components/avatar_component.rb
172
173
  - lib/atomic_view/components/badge_component.rb
173
174
  - lib/atomic_view/components/button_component.rb
175
+ - lib/atomic_view/components/card_component.html.erb
174
176
  - lib/atomic_view/components/card_component.rb
177
+ - lib/atomic_view/components/card_component/section_component.rb
175
178
  - lib/atomic_view/components/check_box_component.rb
176
179
  - lib/atomic_view/components/chip_component.rb
177
180
  - lib/atomic_view/components/collection_check_boxes_component.html.erb
@@ -212,8 +215,12 @@ files:
212
215
  - lib/atomic_view/components/gantt_component/row_component.html.erb
213
216
  - lib/atomic_view/components/gantt_component/row_component.rb
214
217
  - lib/atomic_view/components/grouped_collection_select_component.rb
218
+ - lib/atomic_view/components/index_page_component.html.erb
219
+ - lib/atomic_view/components/index_page_component.rb
215
220
  - lib/atomic_view/components/kbd_component.rb
216
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
217
224
  - lib/atomic_view/components/link_component.html.erb
218
225
  - lib/atomic_view/components/link_component.rb
219
226
  - lib/atomic_view/components/modal_component.html.erb
@@ -224,6 +231,8 @@ files:
224
231
  - lib/atomic_view/components/navigation_tree_component/item_component.html.erb
225
232
  - lib/atomic_view/components/navigation_tree_component/item_component.rb
226
233
  - lib/atomic_view/components/number_field_component.rb
234
+ - lib/atomic_view/components/page_header_component.html.erb
235
+ - lib/atomic_view/components/page_header_component.rb
227
236
  - lib/atomic_view/components/pagination_component.html.erb
228
237
  - lib/atomic_view/components/pagination_component.rb
229
238
  - lib/atomic_view/components/password_field_component.rb
@@ -234,8 +243,15 @@ files:
234
243
  - lib/atomic_view/components/segmented_control_component.rb
235
244
  - lib/atomic_view/components/select_component.html.erb
236
245
  - lib/atomic_view/components/select_component.rb
246
+ - lib/atomic_view/components/show_page_component.html.erb
247
+ - lib/atomic_view/components/show_page_component.rb
237
248
  - lib/atomic_view/components/submit_component.rb
238
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
239
255
  - lib/atomic_view/components/tabs_component.html.erb
240
256
  - lib/atomic_view/components/tabs_component.rb
241
257
  - lib/atomic_view/components/telephone_field_component.rb
@@ -250,6 +266,8 @@ files:
250
266
  - lib/atomic_view/components/timeline_component/item_component.rb
251
267
  - lib/atomic_view/components/toast_component.html.erb
252
268
  - lib/atomic_view/components/toast_component.rb
269
+ - lib/atomic_view/components/tooltip_component.html.erb
270
+ - lib/atomic_view/components/tooltip_component.rb
253
271
  - lib/atomic_view/components/url_field_component.rb
254
272
  - lib/atomic_view/components/week_field_component.rb
255
273
  - lib/atomic_view/components/weekday_select_component.rb