atomic_view 0.1.8 → 0.1.10

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/atomic_view/controllers/chip_controller.js +8 -0
  3. data/app/assets/javascripts/atomic_view/controllers/command_palette_controller.js +96 -0
  4. data/app/assets/javascripts/atomic_view/controllers/dropdown_controller.js +42 -0
  5. data/app/assets/javascripts/atomic_view/controllers/modal_controller.js +23 -0
  6. data/app/assets/javascripts/atomic_view/controllers/theme_toggle_controller.js +45 -0
  7. data/app/assets/javascripts/atomic_view/controllers/toast_controller.js +20 -0
  8. data/app/assets/stylesheets/atomic_view/application.tailwind.css +0 -2
  9. data/app/assets/tailwind/atomic_view/engine.css +90 -29
  10. data/config/importmap.rb +45 -1
  11. data/lib/atomic_view/component.rb +1 -0
  12. data/lib/atomic_view/components/alert_component.erb +15 -0
  13. data/lib/atomic_view/components/alert_component.rb +63 -0
  14. data/lib/atomic_view/components/avatar_component.rb +63 -0
  15. data/lib/atomic_view/components/badge_component.rb +38 -0
  16. data/lib/atomic_view/components/button_component.rb +2 -23
  17. data/lib/atomic_view/components/card_component.rb +29 -0
  18. data/lib/atomic_view/components/chip_component.rb +56 -0
  19. data/lib/atomic_view/components/collection_check_boxes_component.erb +3 -0
  20. data/lib/atomic_view/components/collection_check_boxes_component.rb +25 -0
  21. data/lib/atomic_view/components/collection_radio_buttons_component.erb +3 -0
  22. data/lib/atomic_view/components/collection_radio_buttons_component.rb +25 -0
  23. data/lib/atomic_view/components/collection_select_component.erb +25 -0
  24. data/lib/atomic_view/components/collection_select_component.rb +21 -4
  25. data/lib/atomic_view/components/color_field_component.rb +3 -0
  26. data/lib/atomic_view/components/command_palette_component.erb +41 -0
  27. data/lib/atomic_view/components/command_palette_component.rb +88 -0
  28. data/lib/atomic_view/components/concerns/button_variants.rb +34 -0
  29. data/lib/atomic_view/components/concerns/field_chrome.rb +21 -0
  30. data/lib/atomic_view/components/concerns/section_support.rb +67 -0
  31. data/lib/atomic_view/components/date_field_component.rb +3 -9
  32. data/lib/atomic_view/components/date_select_component.rb +9 -0
  33. data/lib/atomic_view/components/datetime_local_field_component.rb +9 -0
  34. data/lib/atomic_view/components/datetime_select_component.rb +9 -0
  35. data/lib/atomic_view/components/dropdown_component.erb +11 -0
  36. data/lib/atomic_view/components/dropdown_component.rb +57 -0
  37. data/lib/atomic_view/components/empty_state_component.erb +10 -0
  38. data/lib/atomic_view/components/empty_state_component.rb +28 -0
  39. data/lib/atomic_view/components/field_component.erb +3 -3
  40. data/lib/atomic_view/components/field_component.rb +6 -46
  41. data/lib/atomic_view/components/grouped_collection_select_component.rb +5 -0
  42. data/lib/atomic_view/components/kbd_component.rb +31 -0
  43. data/lib/atomic_view/components/label_component.rb +16 -1
  44. data/lib/atomic_view/components/modal_component.erb +24 -0
  45. data/lib/atomic_view/components/modal_component.rb +57 -0
  46. data/lib/atomic_view/components/month_field_component.rb +9 -0
  47. data/lib/atomic_view/components/nav_item_component.erb +6 -0
  48. data/lib/atomic_view/components/nav_item_component.rb +36 -0
  49. data/lib/atomic_view/components/pagination_component.erb +19 -0
  50. data/lib/atomic_view/components/pagination_component.rb +81 -0
  51. data/lib/atomic_view/components/radio_button_component.rb +3 -0
  52. data/lib/atomic_view/components/range_field_component.rb +3 -0
  53. data/lib/atomic_view/components/segmented_control_component.erb +14 -0
  54. data/lib/atomic_view/components/segmented_control_component.rb +102 -0
  55. data/lib/atomic_view/components/select_component.erb +25 -0
  56. data/lib/atomic_view/components/select_component.rb +21 -7
  57. data/lib/atomic_view/components/submit_component.rb +2 -23
  58. data/lib/atomic_view/components/table_component.rb +52 -0
  59. data/lib/atomic_view/components/tabs_component.erb +5 -0
  60. data/lib/atomic_view/components/tabs_component.rb +74 -0
  61. data/lib/atomic_view/components/text_area_component.rb +5 -0
  62. data/lib/atomic_view/components/theme_toggle_component.rb +74 -0
  63. data/lib/atomic_view/components/time_select_component.rb +9 -0
  64. data/lib/atomic_view/components/time_zone_select_component.rb +5 -0
  65. data/lib/atomic_view/components/timeline_component/item_component.erb +10 -0
  66. data/lib/atomic_view/components/timeline_component/item_component.rb +50 -0
  67. data/lib/atomic_view/components/timeline_component.erb +10 -0
  68. data/lib/atomic_view/components/timeline_component.rb +61 -0
  69. data/lib/atomic_view/components/toast_component.erb +14 -0
  70. data/lib/atomic_view/components/toast_component.rb +68 -0
  71. data/lib/atomic_view/components/week_field_component.rb +9 -0
  72. data/lib/atomic_view/components/weekday_select_component.rb +5 -0
  73. data/lib/atomic_view/form_builder.rb +25 -0
  74. data/lib/atomic_view/version.rb +1 -1
  75. data/lib/atomic_view.rb +1 -0
  76. data/lib/generators/atomic_view/install_generator.rb +13 -12
  77. metadata +62 -4
@@ -1,6 +1,8 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class SubmitComponent < ViewComponent::Form::SubmitComponent
4
+ include AtomicView::Components::Concerns::ButtonVariants
5
+
4
6
  attr_reader :variant
5
7
 
6
8
  def initialize(form, value_or_options = nil, options = nil)
@@ -18,29 +20,6 @@ module AtomicView
18
20
  def html_class
19
21
  class_names(base_classes, variant_classes, @options[:class])
20
22
  end
21
-
22
- private
23
-
24
- def base_classes
25
- "h-8 rounded-md px-3 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]"
26
- end
27
-
28
- def variant_classes
29
- case variant
30
- when :primary
31
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90"
32
- when :secondary
33
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80"
34
- when :destructive
35
- "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20"
36
- when :muted
37
- "bg-transparent text-muted-foreground hover:bg-muted"
38
- when :link
39
- "text-primary underline-offset-4 hover:underline"
40
- else
41
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90"
42
- end
43
- end
44
23
  end
45
24
  end
46
25
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Table
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.
11
+ #
12
+ # <%= render AtomicView::Components::TableComponent.new do |table| %>
13
+ # <thead>
14
+ # <tr class="<%= table.head_class %>">
15
+ # <th class="px-3 py-2">Name</th>
16
+ # </tr>
17
+ # </thead>
18
+ # <tbody>
19
+ # <tr class="<%= table.row_class %>">
20
+ # <td class="px-3 py-2">Riverbend</td>
21
+ # </tr>
22
+ # </tbody>
23
+ # <% end %>
24
+ class TableComponent < AtomicView::Component
25
+ HEAD_ROW_CLASSES = "text-left text-xs font-semibold uppercase tracking-wide text-muted-foreground border-b border-border"
26
+ BODY_ROW_CLASSES = "border-b border-border hover:bg-offset"
27
+
28
+ def initialize(**options)
29
+ super()
30
+ @options = options
31
+ end
32
+
33
+ def call
34
+ tag.table(**@options.except(:class), class: class_names(base_classes, @options[:class])) { content }
35
+ end
36
+
37
+ def head_class(**options)
38
+ class_names(HEAD_ROW_CLASSES, options[:class])
39
+ end
40
+
41
+ def row_class(**options)
42
+ class_names(BODY_ROW_CLASSES, options[:class])
43
+ end
44
+
45
+ private
46
+
47
+ def base_classes
48
+ "w-full text-sm"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ <%= tag.div(**@html_options.except(:class), class: container_class) do %>
2
+ <% options.each do |option| %>
3
+ <%= link_to option[:label], option[:href], **tab_attributes(option) %>
4
+ <% end %>
5
+ <% end %>
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Tabs
6
+ #
7
+ # Renders an underlined tab bar for navigating between a record's
8
+ # sub-pages (e.g. Overview/Invoices/Activity). Like
9
+ # SegmentedControlComponent's link mode, `options` are {label:, href:}
10
+ # hashes and `selected` is compared against each option's `label:` --
11
+ # the consumer already has the current tab's display value in hand
12
+ # (e.g. from the request path), so matching on `label:` avoids making
13
+ # every caller build a comparable canonical href.
14
+ #
15
+ # Unlike SegmentedControlComponent's pill track (mutually-exclusive
16
+ # filter chips raised on a muted background), tabs read as page-level
17
+ # navigation -- a plain row of links with a colored underline on the
18
+ # active one, sharing a bottom divider with the rest of the row.
19
+ #
20
+ # Pass `turbo_frame:` to swap tabs without a full page load: every tab
21
+ # link gets a matching `data-turbo-frame`. Wrap *both* this component
22
+ # and the content below it in the same `<turbo-frame id="...">` --
23
+ # not just the content -- so the newly-active tab's underline arrives
24
+ # in the same swap as the new content, rather than the frame updating
25
+ # while the tab bar outside it still shows the old tab active. The
26
+ # component doesn't render that frame itself -- it stays agnostic
27
+ # about the content underneath, the same way it doesn't know
28
+ # what each `href` points to.
29
+ class TabsComponent < AtomicView::Component
30
+ attr_reader :options, :selected, :turbo_frame
31
+
32
+ def initialize(options:, selected:, turbo_frame: nil, **html_options)
33
+ super()
34
+ @options = options
35
+ @selected = selected
36
+ @turbo_frame = turbo_frame
37
+ @html_options = html_options
38
+ end
39
+
40
+ def active?(option)
41
+ option[:label] == selected
42
+ end
43
+
44
+ def container_class
45
+ class_names("flex gap-6 border-b border-border", @html_options[:class])
46
+ end
47
+
48
+ def tab_attributes(option)
49
+ attributes = {class: tab_classes(option)}
50
+ attributes[:"aria-current"] = "true" if active?(option)
51
+ attributes[:data] = {turbo_frame: turbo_frame} if turbo_frame.present?
52
+ attributes
53
+ end
54
+
55
+ private
56
+
57
+ def tab_classes(option)
58
+ class_names(base_classes, active?(option) ? active_classes : inactive_classes)
59
+ end
60
+
61
+ def base_classes
62
+ "-mb-px border-b-2 pb-3 text-sm font-semibold"
63
+ end
64
+
65
+ def active_classes
66
+ "border-accent text-accent"
67
+ end
68
+
69
+ def inactive_classes
70
+ "border-transparent text-muted-foreground hover:text-foreground"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,6 +1,11 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class TextAreaComponent < ViewComponent::Form::TextAreaComponent
4
+ include AtomicView::Components::Concerns::FieldChrome
5
+
6
+ def html_class
7
+ class_names(field_chrome_classes, "h-auto min-h-24 resize-y")
8
+ end
4
9
  end
5
10
  end
6
11
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # ThemeToggle
6
+ #
7
+ # A button that flips the `.dark` class on `<html>` -- the convention
8
+ # engine.css itself documents (see its "Explicit .dark class toggle
9
+ # only" comment) -- between light and dark, persisting the choice to
10
+ # `localStorage` and falling back to the OS preference
11
+ # (`prefers-color-scheme`) the first time a visitor arrives with no
12
+ # stored choice.
13
+ #
14
+ # The sun/moon crossfade is pure CSS (`dark:` utility classes, which
15
+ # resolve off that same `.dark` class per engine.css's
16
+ # `@custom-variant dark`) -- the `atomic-view--theme-toggle` Stimulus
17
+ # controller only toggles the class and persists it, no host app JS
18
+ # required, same as every other atomic_view controller (auto-pinned
19
+ # per config/importmap.rb).
20
+ #
21
+ # Applying `.dark` before first paint (to avoid a flash of the wrong
22
+ # theme) is the host app's concern -- a small inline script in
23
+ # `<head>`, e.g.:
24
+ #
25
+ # <script>
26
+ # try {
27
+ # var t = localStorage.getItem("atomic-view-theme");
28
+ # var dark = t ? t === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
29
+ # if (dark) document.documentElement.classList.add("dark");
30
+ # } catch (e) {}
31
+ # </script>
32
+ #
33
+ # This is the same FOUC-prevention step any dark-mode toggle needs,
34
+ # since Stimulus only connects after the DOM has already painted once.
35
+ class ThemeToggleComponent < AtomicView::Component
36
+ def initialize(**options)
37
+ super()
38
+ @options = options
39
+ end
40
+
41
+ def call
42
+ tag.button(
43
+ **@options.except(:class),
44
+ type: "button",
45
+ class: class_names(base_classes, @options[:class]),
46
+ data: {controller: "atomic-view--theme-toggle", action: "click->atomic-view--theme-toggle#toggle"},
47
+ aria: {label: "Toggle theme"}
48
+ ) { safe_join([sun_icon, moon_icon]) }
49
+ end
50
+
51
+ private
52
+
53
+ def base_classes
54
+ "relative inline-flex size-8 items-center justify-center rounded-full text-muted-foreground hover:bg-offset hover:text-foreground"
55
+ end
56
+
57
+ def sun_icon
58
+ icon("sun", options: {class: sun_classes}).to_s.html_safe
59
+ end
60
+
61
+ def moon_icon
62
+ icon("moon", options: {class: moon_classes}).to_s.html_safe
63
+ end
64
+
65
+ def sun_classes
66
+ "absolute size-4 rotate-0 scale-100 opacity-100 transition-all duration-300 dark:rotate-90 dark:scale-50 dark:opacity-0"
67
+ end
68
+
69
+ def moon_classes
70
+ "absolute size-4 -rotate-90 scale-50 opacity-0 transition-all duration-300 dark:rotate-0 dark:scale-100 dark:opacity-100"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,6 +1,15 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class TimeSelectComponent < ViewComponent::Form::TimeSelectComponent
4
+ include AtomicView::Components::Concerns::FieldChrome
5
+
6
+ def html_class
7
+ field_chrome_classes(size: :sm)
8
+ end
9
+
10
+ def call
11
+ content_tag(:div, super, class: "flex gap-2")
12
+ end
4
13
  end
5
14
  end
6
15
  end
@@ -1,6 +1,11 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class TimeZoneSelectComponent < ViewComponent::Form::TimeZoneSelectComponent
4
+ include AtomicView::Components::Concerns::FieldChrome
5
+
6
+ def html_class
7
+ field_chrome_classes(size: :sm)
8
+ end
4
9
  end
5
10
  end
6
11
  end
@@ -0,0 +1,10 @@
1
+ <%= marker %>
2
+ <div class="min-w-0 flex-1 pt-0.5">
3
+ <div class="text-sm text-foreground">
4
+ <%= meta %>
5
+ <% if time.present? %>
6
+ <%= local_time_ago(time, class: "ml-2 text-xs text-muted-foreground") %>
7
+ <% end %>
8
+ </div>
9
+ <%= content if content? %>
10
+ </div>
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ class TimelineComponent
6
+ # A single Timeline row: a marker (icon or avatar) plus a meta line
7
+ # (optional actor + description + time) and optional free-form block
8
+ # content. Rendered via `TimelineComponent#with_item` -- see
9
+ # `TimelineComponent`'s class docs.
10
+ #
11
+ # `time:` takes a real `Time`/`Date`/`DateTime`/
12
+ # `ActiveSupport::TimeWithZone` -- not a preformatted string -- and is
13
+ # rendered with the `local_time` gem's `local_time_ago` helper (see
14
+ # `AtomicView::Component`), which degrades gracefully to an absolute
15
+ # UTC timestamp server-side and upgrades to a relative "3 hours ago"
16
+ # (aging into a full date over time) client-side once the host app
17
+ # loads local_time's JS -- see `config/importmap.rb`'s "local-time"
18
+ # pin comment for that setup.
19
+ class ItemComponent < AtomicView::Component
20
+ attr_reader :icon_name, :avatar, :actor, :description, :time
21
+
22
+ def initialize(description:, icon: nil, avatar: nil, actor: nil, time: nil)
23
+ super()
24
+ @icon_name = icon
25
+ @avatar = avatar
26
+ @actor = actor
27
+ @description = description
28
+ @time = time
29
+ end
30
+
31
+ def marker
32
+ if avatar.present?
33
+ render(AvatarComponent.new(initials: avatar, class: "size-7 shrink-0 text-xs"))
34
+ else
35
+ tag.span(class: "flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground") do
36
+ icon(icon_name, options: {class: "size-3.5"}).to_s.html_safe
37
+ end
38
+ end
39
+ end
40
+
41
+ def meta
42
+ parts = []
43
+ parts << tag.strong(actor, class: "font-semibold") if actor.present?
44
+ parts << description
45
+ safe_join(parts, " ")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,10 @@
1
+ <%= tag.div(**@options.except(:class), class: container_class) do %>
2
+ <% items.each_with_index do |item, index| %>
3
+ <div class="<%= item_wrapper_class(index) %>">
4
+ <% if render_line?(index) %>
5
+ <div class="absolute left-[13px] top-7 -bottom-6 w-px bg-border"></div>
6
+ <% end %>
7
+ <%= item %>
8
+ </div>
9
+ <% end %>
10
+ <% end %>
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Timeline
6
+ #
7
+ # A vertical audit/activity feed -- e.g. a record's created/changed/
8
+ # commented history. Each row is a `with_item` slot rather than a plain
9
+ # data hash: every item needs the same marker + meta-line + connecting-
10
+ # line layout generated consistently, and slots let each item take
11
+ # strongly-typed keyword arguments (with real defaults) plus optional
12
+ # free-form block content, instead of a loose symbol-keyed hash.
13
+ #
14
+ # render(TimelineComponent.new) do |timeline|
15
+ # timeline.with_item(icon: "flag", actor: "Karen Will", description: "created this park", time: 1.year.ago)
16
+ #
17
+ # timeline.with_item(avatar: "KW", actor: "Karen Will", description: "commented", time: 3.hours.ago) do
18
+ # tag.div("Following up...", class: "mt-2 rounded-btn bg-offset p-2 text-sm")
19
+ # end
20
+ # end
21
+ #
22
+ # Each item needs exactly one marker: `icon:` (a Heroicon name, shown in
23
+ # a plain circular badge) or `avatar:` (initials, rendered via
24
+ # `AvatarComponent`) -- for a system event vs. something a person did.
25
+ # `actor:` is optional bold text before `description:` (omit it for
26
+ # system-generated events, e.g. "Status changed automatically"). `time:`
27
+ # takes a real `Time`/`Date`/`DateTime`/`ActiveSupport::TimeWithZone`
28
+ # rather than a preformatted string -- see `ItemComponent`'s class docs
29
+ # for how it's rendered. The block passed to `with_item` is optional
30
+ # free-form HTML-safe content below the meta line -- a diff comparison,
31
+ # a badge change, a comment bubble -- since that part varies too much
32
+ # to model as a keyword argument. See `ItemComponent` for the per-item
33
+ # API.
34
+ class TimelineComponent < AtomicView::Component
35
+ renders_many :items, "ItemComponent"
36
+
37
+ def initialize(**options)
38
+ super()
39
+ @options = options
40
+ end
41
+
42
+ def container_class
43
+ class_names("flex flex-col", @options[:class])
44
+ end
45
+
46
+ def item_wrapper_class(index)
47
+ class_names("relative flex gap-3.5", "pb-6" => !last?(index))
48
+ end
49
+
50
+ def render_line?(index)
51
+ !last?(index)
52
+ end
53
+
54
+ private
55
+
56
+ def last?(index)
57
+ index == items.length - 1
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ <%= tag.div(**html_options, class: html_class, data: data_attributes, role: "alert") do %>
2
+ <%= icon(icon_name, options: {class: icon_class}).to_s.html_safe %>
3
+ <div class="min-w-0 flex-1">
4
+ <p class="font-body text-sm font-semibold text-foreground"><%= title %></p>
5
+ <% if description.present? %>
6
+ <p class="mt-1 text-sm text-muted-foreground"><%= description %></p>
7
+ <% end %>
8
+ </div>
9
+ <% if dismissible %>
10
+ <%= tag.button(type: "button", class: "inline-flex shrink-0 items-center text-muted-foreground hover:text-foreground", data: {action: "click->atomic-view--toast#dismiss"}, aria: {label: "Dismiss"}) do %>
11
+ <%= icon("x-mark", variant: :mini, options: {class: "size-4"}).to_s.html_safe %>
12
+ <% end %>
13
+ <% end %>
14
+ <% end %>
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Toast
6
+ #
7
+ # A single, self-dismissing notification. This component only renders
8
+ # one toast and wires it to `atomic-view--toast`, the Stimulus controller
9
+ # that handles its own dismiss/auto-dismiss lifecycle (see
10
+ # `app/assets/javascripts/atomic_view/controllers/toast_controller.js`).
11
+ #
12
+ # Stacking multiple toasts (newest on top/bottom, fixed to a screen
13
+ # corner) is intentionally out of scope -- wrap however many
14
+ # ToastComponents a consuming app renders in a plain CSS container, e.g.
15
+ # `fixed bottom-4 right-4 flex flex-col-reverse gap-2 z-50`.
16
+ class ToastComponent < AtomicView::Component
17
+ attr_reader :variant, :title, :description, :dismissible, :auto_dismiss_ms
18
+
19
+ VARIANTS = {
20
+ success: {icon: "check-circle", border: "border-l-success", icon_color: "text-success"},
21
+ error: {icon: "x-circle", border: "border-l-destructive", icon_color: "text-destructive"},
22
+ info: {icon: "information-circle", border: "border-l-primary", icon_color: "text-primary"}
23
+ }.freeze
24
+
25
+ def initialize(title:, variant: :info, description: nil, dismissible: true, auto_dismiss_ms: nil, **options)
26
+ super()
27
+ @variant = variant
28
+ @title = title
29
+ @description = description
30
+ @dismissible = dismissible
31
+ @auto_dismiss_ms = auto_dismiss_ms
32
+ @options = options
33
+ end
34
+
35
+ def html_options
36
+ @options.except(:class, :data)
37
+ end
38
+
39
+ def html_class
40
+ class_names(base_classes, variant_config[:border], @options[:class])
41
+ end
42
+
43
+ def icon_name
44
+ variant_config[:icon]
45
+ end
46
+
47
+ def icon_class
48
+ class_names("size-5 shrink-0", variant_config[:icon_color])
49
+ end
50
+
51
+ def data_attributes
52
+ attributes = (@options[:data] || {}).merge(controller: "atomic-view--toast")
53
+ attributes["atomic-view--toast-auto-dismiss-ms-value"] = auto_dismiss_ms if auto_dismiss_ms.present?
54
+ attributes
55
+ end
56
+
57
+ private
58
+
59
+ def variant_config
60
+ VARIANTS.fetch(variant, VARIANTS[:info])
61
+ end
62
+
63
+ def base_classes
64
+ "flex items-start gap-3 rounded-card border border-border border-l-4 bg-surface p-3 shadow-soft"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,6 +1,15 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class WeekFieldComponent < ViewComponent::Form::WeekFieldComponent
4
+ def call
5
+ render FieldComponent.new(
6
+ form,
7
+ object_name,
8
+ method_name,
9
+ options,
10
+ ActionView::Helpers::Tags::WeekField
11
+ )
12
+ end
4
13
  end
5
14
  end
6
15
  end
@@ -1,6 +1,11 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class WeekdaySelectComponent < ViewComponent::Form::WeekdaySelectComponent
4
+ include AtomicView::Components::Concerns::FieldChrome
5
+
6
+ def html_class
7
+ field_chrome_classes(size: :sm)
8
+ end
4
9
  end
5
10
  end
6
11
  end
@@ -3,5 +3,30 @@
3
3
  module AtomicView
4
4
  class FormBuilder < ViewComponent::Form::Builder
5
5
  namespace Components
6
+
7
+ # view_component-form's Helpers::Rails module forwards most native Rails
8
+ # form helpers to their ViewComponent equivalents, but it misses
9
+ # `weekday_select` (added natively to Rails' FormBuilder in Rails 7).
10
+ # Without this override, `form.weekday_select` would bypass
11
+ # AtomicView::Components::WeekdaySelectComponent entirely and fall back to
12
+ # ActionView's unstyled native helper.
13
+ def weekday_select(method, options = {}, html_options = {})
14
+ render_component(
15
+ :weekday_select, @object_name, method,
16
+ objectify_options(options), @default_html_options.merge(html_options)
17
+ )
18
+ end
19
+
20
+ # Rails 7.1 renamed `field_helpers`' entry from `:text_area` to
21
+ # `:textarea`, so view_component-form's dynamic method generation (which
22
+ # iterates `field_helpers`) now defines a component-backed `textarea`
23
+ # method but leaves `text_area` falling through to Rails' native,
24
+ # unstyled helper. Without this override, `form.text_area` would bypass
25
+ # AtomicView::Components::TextAreaComponent entirely.
26
+ def text_area(method, options = {})
27
+ render_component(
28
+ :text_area, @object_name, method, objectify_options(options)
29
+ )
30
+ end
6
31
  end
7
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicView
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.10"
5
5
  end
data/lib/atomic_view.rb CHANGED
@@ -6,6 +6,7 @@ require 'atomic_view/configuration'
6
6
 
7
7
  require "tailwind_merge"
8
8
  require "heroicons"
9
+ require "local_time"
9
10
  require "view_component"
10
11
  require "view_component/form"
11
12
  require "zeitwerk"
@@ -2,16 +2,17 @@
2
2
 
3
3
  module AtomicView
4
4
  class InstallGenerator < Rails::Generators::Base
5
- source_root File.expand_path('templates', __dir__)
5
+ source_root File.expand_path("templates", __dir__)
6
6
 
7
- IMPORT = '@import "../builds/tailwind/atomic_view.css";'.freeze
7
+ IMPORT = '@import "../builds/tailwind/atomic_view.css";'
8
+ FLOATING_UI_PIN = 'pin "@floating-ui/dom", to: "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.8.0/+esm"'
8
9
 
9
10
  def add_atomic_view_tailwind
10
11
  return if options[:update_tailwind_only]
11
12
 
12
- tailwind_path = Rails.root.join('app/assets/tailwind/application.css')
13
+ tailwind_path = Rails.root.join("app/assets/tailwind/application.css")
13
14
  unless tailwind_path.exist?
14
- raise '`app/assets/tailwind/application.css` does not exist'
15
+ raise "`app/assets/tailwind/application.css` does not exist"
15
16
  end
16
17
 
17
18
  if tailwind_path.read.include?(IMPORT)
@@ -23,20 +24,20 @@ module AtomicView
23
24
  end
24
25
  end
25
26
 
26
- def add_atomic_view_js_controllers
27
+ def add_atomic_view_floating_ui
27
28
  return if options[:update_tailwind_only]
28
29
 
29
- controllers_path = Rails.root.join('app/javascript/controllers/index.js')
30
- if controllers_path.exist? && controllers_path.read.include?('eagerLoadControllersFrom("controllers", application)')
31
- if controllers_path.read.include?('eagerLoadControllersFrom("atomic_view", application)')
32
- puts '`app/javascript/controllers/index.js` already contains `eagerLoadControllersFrom("atomic_view", application)`'
30
+ importmap_path = Rails.root.join("config/importmap.rb")
31
+ if importmap_path.exist? && importmap_path.read.match?(/^pin\s+["']application["']/)
32
+ if importmap_path.read.include?(FLOATING_UI_PIN)
33
+ puts "`config/importmap.rb` already contains the `@floating-ui/dom` pin"
33
34
  else
34
- insert_into_file controllers_path, after: 'eagerLoadControllersFrom("controllers", application)' do
35
- "\neagerLoadControllersFrom(\"atomic_view\", application)\n"
35
+ insert_into_file importmap_path, after: /^pin\s+["']application["'].*\n/ do
36
+ "#{FLOATING_UI_PIN}\n"
36
37
  end
37
38
  end
38
39
  else
39
- puts '`app/javascript/controllers/index.js` does not exist or does not contain `eagerLoadControllersFrom("controllers", application)`'
40
+ puts '`config/importmap.rb` does not exist or does not contain `pin "application"`'
40
41
  end
41
42
  end
42
43
  end