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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/atomic_view/controllers/chip_controller.js +8 -0
- data/app/assets/javascripts/atomic_view/controllers/command_palette_controller.js +96 -0
- data/app/assets/javascripts/atomic_view/controllers/dropdown_controller.js +42 -0
- data/app/assets/javascripts/atomic_view/controllers/modal_controller.js +23 -0
- data/app/assets/javascripts/atomic_view/controllers/theme_toggle_controller.js +45 -0
- data/app/assets/javascripts/atomic_view/controllers/toast_controller.js +20 -0
- data/app/assets/stylesheets/atomic_view/application.tailwind.css +0 -2
- data/app/assets/tailwind/atomic_view/engine.css +90 -29
- data/config/importmap.rb +45 -1
- data/lib/atomic_view/component.rb +1 -0
- data/lib/atomic_view/components/alert_component.erb +15 -0
- data/lib/atomic_view/components/alert_component.rb +63 -0
- data/lib/atomic_view/components/avatar_component.rb +63 -0
- data/lib/atomic_view/components/badge_component.rb +38 -0
- data/lib/atomic_view/components/button_component.rb +2 -23
- data/lib/atomic_view/components/card_component.rb +29 -0
- data/lib/atomic_view/components/chip_component.rb +56 -0
- data/lib/atomic_view/components/collection_check_boxes_component.erb +3 -0
- data/lib/atomic_view/components/collection_check_boxes_component.rb +25 -0
- data/lib/atomic_view/components/collection_radio_buttons_component.erb +3 -0
- data/lib/atomic_view/components/collection_radio_buttons_component.rb +25 -0
- data/lib/atomic_view/components/collection_select_component.erb +25 -0
- data/lib/atomic_view/components/collection_select_component.rb +21 -4
- data/lib/atomic_view/components/color_field_component.rb +3 -0
- data/lib/atomic_view/components/command_palette_component.erb +41 -0
- data/lib/atomic_view/components/command_palette_component.rb +88 -0
- data/lib/atomic_view/components/concerns/button_variants.rb +34 -0
- data/lib/atomic_view/components/concerns/field_chrome.rb +21 -0
- data/lib/atomic_view/components/concerns/section_support.rb +67 -0
- data/lib/atomic_view/components/date_field_component.rb +3 -9
- data/lib/atomic_view/components/date_select_component.rb +9 -0
- data/lib/atomic_view/components/datetime_local_field_component.rb +9 -0
- data/lib/atomic_view/components/datetime_select_component.rb +9 -0
- data/lib/atomic_view/components/dropdown_component.erb +11 -0
- data/lib/atomic_view/components/dropdown_component.rb +57 -0
- data/lib/atomic_view/components/empty_state_component.erb +10 -0
- data/lib/atomic_view/components/empty_state_component.rb +28 -0
- data/lib/atomic_view/components/field_component.erb +3 -3
- data/lib/atomic_view/components/field_component.rb +6 -46
- data/lib/atomic_view/components/grouped_collection_select_component.rb +5 -0
- data/lib/atomic_view/components/kbd_component.rb +31 -0
- data/lib/atomic_view/components/label_component.rb +16 -1
- data/lib/atomic_view/components/modal_component.erb +24 -0
- data/lib/atomic_view/components/modal_component.rb +57 -0
- data/lib/atomic_view/components/month_field_component.rb +9 -0
- data/lib/atomic_view/components/nav_item_component.erb +6 -0
- data/lib/atomic_view/components/nav_item_component.rb +36 -0
- data/lib/atomic_view/components/pagination_component.erb +19 -0
- data/lib/atomic_view/components/pagination_component.rb +81 -0
- data/lib/atomic_view/components/radio_button_component.rb +3 -0
- data/lib/atomic_view/components/range_field_component.rb +3 -0
- data/lib/atomic_view/components/segmented_control_component.erb +14 -0
- data/lib/atomic_view/components/segmented_control_component.rb +102 -0
- data/lib/atomic_view/components/select_component.erb +25 -0
- data/lib/atomic_view/components/select_component.rb +21 -7
- data/lib/atomic_view/components/submit_component.rb +2 -23
- data/lib/atomic_view/components/table_component.rb +52 -0
- data/lib/atomic_view/components/tabs_component.erb +5 -0
- data/lib/atomic_view/components/tabs_component.rb +74 -0
- data/lib/atomic_view/components/text_area_component.rb +5 -0
- data/lib/atomic_view/components/theme_toggle_component.rb +74 -0
- data/lib/atomic_view/components/time_select_component.rb +9 -0
- data/lib/atomic_view/components/time_zone_select_component.rb +5 -0
- data/lib/atomic_view/components/timeline_component/item_component.erb +10 -0
- data/lib/atomic_view/components/timeline_component/item_component.rb +50 -0
- data/lib/atomic_view/components/timeline_component.erb +10 -0
- data/lib/atomic_view/components/timeline_component.rb +61 -0
- data/lib/atomic_view/components/toast_component.erb +14 -0
- data/lib/atomic_view/components/toast_component.rb +68 -0
- data/lib/atomic_view/components/week_field_component.rb +9 -0
- data/lib/atomic_view/components/weekday_select_component.rb +5 -0
- data/lib/atomic_view/form_builder.rb +25 -0
- data/lib/atomic_view/version.rb +1 -1
- data/lib/atomic_view.rb +1 -0
- data/lib/generators/atomic_view/install_generator.rb +13 -12
- metadata +62 -4
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
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
|
|
@@ -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
|