atomic_view 0.1.9 → 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 +91 -28
  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 +60 -2
@@ -4,6 +4,8 @@ module AtomicView
4
4
  module Components
5
5
  # Button
6
6
  class ButtonComponent < ViewComponent::Form::ButtonComponent
7
+ include AtomicView::Components::Concerns::ButtonVariants
8
+
7
9
  attr_reader :label, :variant, :size
8
10
 
9
11
  def initialize(form, label_or_options = nil, options = nil)
@@ -28,29 +30,6 @@ module AtomicView
28
30
  content_tag(:button, label, @options)
29
31
  end
30
32
  end
31
-
32
- private
33
-
34
- def base_classes
35
- "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]"
36
- end
37
-
38
- def variant_classes
39
- case variant
40
- when :primary
41
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90"
42
- when :secondary
43
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80"
44
- when :destructive
45
- "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20"
46
- when :muted
47
- "bg-transparent text-muted-foreground hover:bg-muted"
48
- when :link
49
- "text-primary underline-offset-4 hover:underline"
50
- else
51
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90"
52
- end
53
- end
54
33
  end
55
34
  end
56
35
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ class CardComponent < AtomicView::Component
6
+ attr_reader :hoverable
7
+
8
+ def initialize(hoverable: false, **options)
9
+ super()
10
+ @hoverable = hoverable
11
+ @options = options
12
+ end
13
+
14
+ def call
15
+ tag.div(**@options.except(:class), class: class_names(base_classes, hoverable_classes, @options[:class])) { content }
16
+ end
17
+
18
+ private
19
+
20
+ def base_classes
21
+ "bg-surface rounded-card border border-border p-2.5"
22
+ end
23
+
24
+ def hoverable_classes
25
+ "hover:shadow-soft transition-shadow" if hoverable
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Chip
6
+ #
7
+ # A dismissible chip is automatically wired to `atomic-view--chip`, the
8
+ # Stimulus controller that handles removing it from the DOM on click
9
+ # (see `app/assets/javascripts/atomic_view/controllers/chip_controller.js`).
10
+ # Pass `dismiss_button_options` only to customize that behavior, e.g. to
11
+ # trigger a network request instead of (or in addition to) the default
12
+ # remove animation.
13
+ class ChipComponent < AtomicView::Component
14
+ attr_reader :dismissible, :dismiss_button_options, :leading
15
+
16
+ def initialize(dismissible: false, dismiss_button_options: {}, leading: nil, **options)
17
+ super()
18
+ @dismissible = dismissible
19
+ @dismiss_button_options = dismiss_button_options
20
+ @leading = leading
21
+ @options = options
22
+ end
23
+
24
+ def call
25
+ tag.span(**@options.except(:class, :data), class: class_names(base_classes, @options[:class]), data: data_attributes) do
26
+ safe_join([leading, content, dismiss_button].compact)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def base_classes
33
+ "inline-flex items-center gap-1 rounded-pill bg-offset px-2 py-0.5 text-xs font-medium text-foreground border border-border"
34
+ end
35
+
36
+ def data_attributes
37
+ attributes = (@options[:data] || {}).dup
38
+ return attributes unless dismissible
39
+
40
+ attributes[:controller] = [attributes[:controller], "atomic-view--chip"].compact.join(" ")
41
+ attributes
42
+ end
43
+
44
+ def dismiss_button
45
+ return unless dismissible
46
+
47
+ options = dismiss_button_options.except(:class, :data)
48
+ data = {action: "click->atomic-view--chip#remove"}.merge(dismiss_button_options[:data] || {})
49
+
50
+ tag.button(**options, type: "button", data: data, class: class_names("inline-flex items-center", dismiss_button_options[:class])) do
51
+ icon("x-mark", variant: :mini, options: {class: "size-3.5"}).to_s.html_safe
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ <%= content_tag :div, class: "flex flex-col gap-2" do %>
2
+ <%= collection_check_boxes_tag %>
3
+ <% end %>
@@ -1,6 +1,31 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class CollectionCheckBoxesComponent < ViewComponent::Form::CollectionCheckBoxesComponent
4
+ def html_class
5
+ "peer border-input dark:bg-input/30 checked:bg-primary checked:text-primary-foreground dark:checked:bg-primary checked:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
6
+ end
7
+
8
+ def collection_check_boxes_tag
9
+ item_proc = element_proc || default_element_proc
10
+
11
+ ActionView::Helpers::Tags::CollectionCheckBoxes.new(
12
+ object_name,
13
+ method_name,
14
+ @view_context,
15
+ collection,
16
+ value_method,
17
+ text_method,
18
+ options,
19
+ html_options,
20
+ &content
21
+ ).render(&item_proc)
22
+ end
23
+
24
+ private
25
+
26
+ def default_element_proc
27
+ ->(builder) { @view_context.content_tag(:div, builder.checkbox + builder.label, class: "flex items-center gap-2") }
28
+ end
4
29
  end
5
30
  end
6
31
  end
@@ -0,0 +1,3 @@
1
+ <%= content_tag :div, class: "flex flex-col gap-2" do %>
2
+ <%= collection_radio_buttons_tag %>
3
+ <% end %>
@@ -1,6 +1,31 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class CollectionRadioButtonsComponent < ViewComponent::Form::CollectionRadioButtonsComponent
4
+ def html_class
5
+ "peer border-input dark:bg-input/30 checked:bg-primary checked:text-primary-foreground dark:checked:bg-primary checked:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-full border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
6
+ end
7
+
8
+ def collection_radio_buttons_tag
9
+ item_proc = element_proc || default_element_proc
10
+
11
+ ActionView::Helpers::Tags::CollectionRadioButtons.new(
12
+ object_name,
13
+ method_name,
14
+ @view_context,
15
+ collection,
16
+ value_method,
17
+ text_method,
18
+ options,
19
+ html_options,
20
+ &content
21
+ ).render(&item_proc)
22
+ end
23
+
24
+ private
25
+
26
+ def default_element_proc
27
+ ->(builder) { @view_context.content_tag(:div, builder.radio_button + builder.label, class: "flex items-center gap-2") }
28
+ end
4
29
  end
5
30
  end
6
31
  end
@@ -0,0 +1,25 @@
1
+ <%= content_tag :div, class: container_html_class do %>
2
+ <% if left_section? %>
3
+ <% if left_section_addon? %>
4
+ <span class="inline-flex items-center rounded-l-btn border border-r-0 border-ring/10 dark:border-white/10 bg-transparent dark:bg-white/5 px-3 text-primary dark:text-white sm:text-sm"><%= left_section %></span>
5
+ <% elsif left_section_interaction? %>
6
+ <%= left_section %>
7
+ <% else %>
8
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
9
+ <%= left_section %>
10
+ </div>
11
+ <% end %>
12
+ <% end %>
13
+ <%= select_tag %>
14
+ <% if right_section? %>
15
+ <% if right_section_addon? %>
16
+ <span class="inline-flex items-center rounded-r-btn border border-l-0 border-ring/10 dark:border-white/10 bg-transparent dark:bg-white/5 px-3 text-primary dark:text-white sm:text-sm"><%= right_section %></span>
17
+ <% elsif right_section_interaction? %>
18
+ <%= right_section %>
19
+ <% else %>
20
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
21
+ <%= right_section %>
22
+ </div>
23
+ <% end %>
24
+ <% end %>
25
+ <% end %>
@@ -1,14 +1,31 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class CollectionSelectComponent < ViewComponent::Form::CollectionSelectComponent
4
+ include AtomicView::Components::Concerns::FieldChrome
5
+ include AtomicView::Components::Concerns::SectionSupport
6
+
4
7
  def html_class
5
8
  class_names(
6
- *%W[block w-full h-9 min-w-0 flex-1 rounded-md border-0 py-1 text-sm shadow-xs ring-1],
7
- "disabled:cursor-not-allowed disabled:bg-disabled disabled:text-disabled-foreground disabled:ring-disabled-ring",
8
- "bg-transparent dark:bg-white/5 text-primary ring-ring/10 dark:ring-white/10 placeholder:text-placeholder dark:text-white focus:ring-focus-ring focus:border-ring/20 dark:focus:ring-focus-ring",
9
- "text-error ring-error-ring placeholder:text-error-placeholder focus:ring-error-focus-ring" => method_errors?
9
+ field_chrome_classes(size: :sm),
10
+ "pl-10" => left_section? && !(left_section_addon? || left_section_interaction?),
11
+ "pr-10" => right_section? && !(right_section_addon? || right_section_interaction?),
12
+ "shadow-none rounded-none rounded-r-btn ring-inset" => left_section_addon? || left_section_interaction?,
13
+ "shadow-none rounded-none rounded-l-btn ring-inset" => right_section_addon? || right_section_interaction?
10
14
  )
11
15
  end
16
+
17
+ def select_tag
18
+ ActionView::Helpers::Tags::CollectionSelect.new(
19
+ object_name,
20
+ method_name,
21
+ @view_context,
22
+ collection,
23
+ value_method,
24
+ text_method,
25
+ options,
26
+ html_options
27
+ ).render
28
+ end
12
29
  end
13
30
  end
14
31
  end
@@ -1,6 +1,9 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class ColorFieldComponent < ViewComponent::Form::ColorFieldComponent
4
+ def html_class
5
+ "rounded-well h-8 w-8 border border-border p-1 cursor-pointer"
6
+ end
4
7
  end
5
8
  end
6
9
  end
@@ -0,0 +1,41 @@
1
+ <%= tag.dialog(**html_options, id: id, class: html_class, data: data_attributes) do %>
2
+ <%= tag.input(
3
+ type: "search",
4
+ placeholder: placeholder,
5
+ autocomplete: "off",
6
+ autofocus: true,
7
+ class: "w-full border-0 bg-transparent px-3 py-2 text-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-0",
8
+ data: input_data_attributes
9
+ ) %>
10
+
11
+ <div class="max-h-80 overflow-y-auto border-t border-border py-2">
12
+ <% sections.each do |section| %>
13
+ <div role="group">
14
+ <p class="px-3 pb-1 pt-2 text-xs uppercase text-muted-foreground"><%= section[:label] %></p>
15
+
16
+ <% section[:results].each do |result| %>
17
+ <%= link_to(
18
+ result[:href] || "#",
19
+ class: "flex items-center justify-between gap-3 px-3 py-1.5 text-sm text-foreground hover:bg-offset",
20
+ data: row_data_attributes(result)
21
+ ) do %>
22
+ <span><%= result[:label] %></span>
23
+ <% if result[:hint].present? %>
24
+ <span class="inline-flex items-center gap-0.5">
25
+ <% Array(result[:hint]).each do |key| %>
26
+ <%= render(AtomicView::Components::KbdComponent.new) { key } %>
27
+ <% end %>
28
+ </span>
29
+ <% end %>
30
+ <% end %>
31
+ <% end %>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+
36
+ <% if footer? %>
37
+ <div class="border-t border-border bg-offset px-3 py-2 text-xs text-muted-foreground">
38
+ <%= footer %>
39
+ </div>
40
+ <% end %>
41
+ <% end %>
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # CommandPalette
6
+ #
7
+ # A `Cmd/Ctrl+K` search dialog built on the native `<dialog>` element
8
+ # (same structural trick as `ModalComponent` -- see that component for
9
+ # the rationale -- but wired to its own `atomic-view--command-palette`
10
+ # controller rather than sharing Modal's, so the two stay independently
11
+ # simple with no inter-component dependency). `<dialog>` gets
12
+ # focus-trap, `::backdrop` styling, and Esc-to-close for free; the
13
+ # controller layers on the global open shortcut, client-side
14
+ # filter-as-you-type, and arrow-key navigation.
15
+ #
16
+ # Results are supplied as plain data rather than slots:
17
+ #
18
+ # sections: [
19
+ # { label: "Pages", results: [
20
+ # { label: "Dashboard", href: "/dashboard", hint: "G D" },
21
+ # { label: "Settings", href: "/settings" }
22
+ # ] }
23
+ # ]
24
+ #
25
+ # `SegmentedControlComponent` and `PaginationComponent` already use this
26
+ # array-of-hashes shape for link collections, and it's the better fit
27
+ # here (over slots, `DropdownComponent`'s approach): every row needs the
28
+ # same two JS hooks -- a `[data-atomic-view--command-palette-target="row"]`
29
+ # marker and a `data-search-text` attribute -- generated consistently so
30
+ # the filter/keyboard-nav controller can rely on them, which a
31
+ # free-form slot would push onto every consumer to wire up by hand. The
32
+ # component stays agnostic about what a result *links to* (any `href`
33
+ # works -- a path, an external URL, a `turbo_frame`-aware route, etc.),
34
+ # which is the same kind of agnosticism `DropdownComponent` keeps about
35
+ # its menu content, just expressed as data instead of markup.
36
+ #
37
+ # `result[:hint]`, when given, renders as a `<kbd>` -- e.g. a keyboard
38
+ # shortcut like "G D" -- next to the result label.
39
+ #
40
+ # `footer` is a slot for a bottom hint bar documenting shortcuts/facets
41
+ # (e.g. "Type # to access projects") -- free-form content, since it's
42
+ # typically a sentence with `KbdComponent`s mixed in rather than
43
+ # structured data.
44
+ class CommandPaletteComponent < AtomicView::Component
45
+ renders_one :footer
46
+
47
+ attr_reader :id, :placeholder, :sections
48
+
49
+ def initialize(id:, sections: [], placeholder: "Search...", **options)
50
+ super()
51
+ @id = id
52
+ @sections = sections
53
+ @placeholder = placeholder
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("mx-auto mt-[15vh] w-full max-w-lg overflow-hidden rounded-card bg-surface p-0 text-foreground shadow-panel backdrop:bg-backdrop", @options[:class])
63
+ end
64
+
65
+ def data_attributes
66
+ (@options[:data] || {}).merge(controller: "atomic-view--command-palette")
67
+ end
68
+
69
+ def input_data_attributes
70
+ {
71
+ "atomic-view--command-palette-target" => "input",
72
+ :action => "input->atomic-view--command-palette#filter keydown->atomic-view--command-palette#navigate"
73
+ }
74
+ end
75
+
76
+ def row_data_attributes(result)
77
+ {
78
+ "atomic-view--command-palette-target" => "row",
79
+ :search_text => search_text_for(result)
80
+ }
81
+ end
82
+
83
+ def search_text_for(result)
84
+ (result[:search_text] || result[:label]).to_s.downcase
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ module Concerns
6
+ module ButtonVariants
7
+ private
8
+
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]"
11
+ end
12
+
13
+ def variant_classes
14
+ case variant
15
+ when :primary
16
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90"
17
+ when :secondary
18
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80"
19
+ when :destructive
20
+ "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20"
21
+ when :muted
22
+ "bg-transparent text-muted-foreground hover:bg-muted"
23
+ when :link
24
+ "text-primary underline-offset-4 hover:underline"
25
+ when :outline
26
+ "bg-surface text-foreground border-[1.5px] border-border hover:bg-offset"
27
+ else
28
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ module Concerns
6
+ module FieldChrome
7
+ private
8
+
9
+ def field_chrome_classes(size: :base)
10
+ class_names(
11
+ "block w-full appearance-none h-8 min-w-0 z-10 flex-1 rounded-btn border-0 py-1 shadow-xs ring-1",
12
+ (size == :sm) ? "text-sm" : "text-base",
13
+ "disabled:cursor-not-allowed disabled:bg-disabled disabled:text-disabled-foreground disabled:ring-disabled-ring",
14
+ "bg-transparent dark:bg-white/5 text-foreground ring-ring/10 dark:ring-white/10 placeholder:text-placeholder focus:ring-focus-ring focus:border-ring/20 dark:focus:ring-focus-ring",
15
+ "text-error ring-error-ring placeholder:text-error-placeholder focus:ring-error-focus-ring" => method_errors?
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ module Concerns
6
+ module SectionSupport
7
+ SECTION_OPTION_KEYS = %i[
8
+ container_class
9
+ left_section
10
+ left_section_as_addon
11
+ left_section_as_interaction
12
+ right_section
13
+ right_section_as_addon
14
+ right_section_as_interaction
15
+ ].freeze
16
+
17
+ # The options hash minus the section/container keys SectionSupport
18
+ # itself consumes -- pass this (not the raw `options`) to whatever
19
+ # renders the actual `<input>`/`<select>` tag, so these keys don't
20
+ # leak through as literal (invalid) HTML attributes.
21
+ def options_without_sections
22
+ options.except(*SECTION_OPTION_KEYS)
23
+ end
24
+
25
+ def container_html_class
26
+ class_names(
27
+ "relative rounded-btn shadow-xs",
28
+ {"flex" => left_section_addon? || left_section_interaction? || right_section_addon? || right_section_interaction?},
29
+ options[:container_class]
30
+ )
31
+ end
32
+
33
+ def left_section
34
+ options[:left_section]
35
+ end
36
+
37
+ def left_section?
38
+ left_section.present?
39
+ end
40
+
41
+ def left_section_addon?
42
+ options[:left_section_as_addon].present?
43
+ end
44
+
45
+ def left_section_interaction?
46
+ options[:left_section_as_interaction].present?
47
+ end
48
+
49
+ def right_section
50
+ options[:right_section] || method_errors? && raw(icon("exclamation-circle", variant: :mini, options: {class: "size-5 text-destructive"}))
51
+ end
52
+
53
+ def right_section?
54
+ right_section.present?
55
+ end
56
+
57
+ def right_section_addon?
58
+ options[:right_section_as_addon].present?
59
+ end
60
+
61
+ def right_section_interaction?
62
+ options[:right_section_as_interaction].present?
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,16 +1,10 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class DateFieldComponent < ViewComponent::Form::DateFieldComponent
4
+ include AtomicView::Components::Concerns::FieldChrome
5
+
4
6
  def html_class
5
- class_names(
6
- class_names(
7
- *%W[block w-full h-9 min-w-0 flex-1 rounded-sm border-0 py-1 text-sm shadow-xs ring-1],
8
- "disabled:cursor-not-allowed disabled:bg-disabled disabled:text-disabled-foreground disabled:ring-disabled-ring",
9
- "bg-transparent dark:bg-white/5 text-primary ring-ring/10 dark:ring-white/10 placeholder:text-placeholder dark:text-white focus:ring-focus-ring focus:border-ring/20 dark:focus:ring-focus-ring",
10
- "text-error ring-error-ring placeholder:text-error-placeholder focus:ring-error-focus-ring" => method_errors?
11
- ),
12
- options[:class]
13
- )
7
+ class_names(field_chrome_classes(size: :sm), options[:class])
14
8
  end
15
9
  end
16
10
  end
@@ -1,6 +1,15 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class DateSelectComponent < ViewComponent::Form::DateSelectComponent
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,15 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class DatetimeLocalFieldComponent < ViewComponent::Form::DatetimeLocalFieldComponent
4
+ def call
5
+ render FieldComponent.new(
6
+ form,
7
+ object_name,
8
+ method_name,
9
+ options,
10
+ ActionView::Helpers::Tags::DatetimeLocalField
11
+ )
12
+ end
4
13
  end
5
14
  end
6
15
  end
@@ -1,6 +1,15 @@
1
1
  module AtomicView
2
2
  module Components
3
3
  class DatetimeSelectComponent < ViewComponent::Form::DatetimeSelectComponent
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
@@ -0,0 +1,11 @@
1
+ <%= tag.div(**html_options, class: html_class, data: data_attributes) do %>
2
+ <%= tag.div(class: "inline-block", data: trigger_data_attributes) do %>
3
+ <%= trigger %>
4
+ <% end %>
5
+
6
+ <% if menu? %>
7
+ <%= tag.div(class: menu_class, data: menu_data_attributes) do %>
8
+ <%= menu %>
9
+ <% end %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Dropdown
6
+ #
7
+ # A generic trigger + floating menu, positioned by `@floating-ui/dom` and
8
+ # wired to `atomic-view--dropdown` (see
9
+ # `app/assets/javascripts/atomic_view/controllers/dropdown_controller.js`).
10
+ # The controller handles opening/closing, edge-aware positioning
11
+ # (flip/shift so the menu stays on-screen near a viewport edge), closing
12
+ # on an outside click or Esc, and keeping the menu anchored to the
13
+ # trigger while the page scrolls or resizes.
14
+ #
15
+ # This component only provides the trigger/menu structure and wiring --
16
+ # it has no opinion about what's inside either slot. Both the
17
+ # org/park-switcher menu (grouped sections with uppercase labels,
18
+ # dividers, a checkmark on the current selection) and the notification
19
+ # bell flyout (a list of notification rows) are the *same*
20
+ # `DropdownComponent` with different slot content -- compose that markup
21
+ # with existing primitives (e.g. `Badge`/`Avatar`) inside the `menu`
22
+ # slot rather than teaching this component about either use case.
23
+ class DropdownComponent < AtomicView::Component
24
+ renders_one :trigger
25
+ renders_one :menu
26
+
27
+ def initialize(**options)
28
+ super()
29
+ @options = options
30
+ end
31
+
32
+ def html_options
33
+ @options.except(:class, :data)
34
+ end
35
+
36
+ def html_class
37
+ class_names("relative inline-block", @options[:class])
38
+ end
39
+
40
+ def data_attributes
41
+ (@options[:data] || {}).merge(controller: "atomic-view--dropdown")
42
+ end
43
+
44
+ def trigger_data_attributes
45
+ {"atomic-view--dropdown-target" => "trigger", :action => "click->atomic-view--dropdown#toggle"}
46
+ end
47
+
48
+ def menu_data_attributes
49
+ {"atomic-view--dropdown-target" => "menu"}
50
+ end
51
+
52
+ def menu_class
53
+ "hidden absolute z-50 bg-surface border border-border rounded-card shadow-panel"
54
+ end
55
+ end
56
+ end
57
+ end