fernandes-ui 0.1.2 → 0.1.4
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/ui.esm.js +3 -6
- data/app/assets/javascripts/ui.js +3 -6
- data/app/behaviors/ui/command_dialog_behavior.rb +0 -8
- data/app/behaviors/ui/empty_behavior.rb +6 -6
- data/app/behaviors/ui/input_behavior.rb +1 -1
- data/app/behaviors/ui/input_group_behavior.rb +3 -1
- data/app/behaviors/ui/input_group_button_behavior.rb +7 -6
- data/app/behaviors/ui/menubar_checkbox_item_behavior.rb +1 -1
- data/app/behaviors/ui/menubar_radio_item_behavior.rb +1 -1
- data/app/behaviors/ui/popover_behavior.rb +11 -3
- data/app/behaviors/ui/spinner_behavior.rb +1 -1
- data/app/behaviors/ui/switch_behavior.rb +49 -49
- data/app/behaviors/ui/toggle_group_behavior.rb +2 -2
- data/app/behaviors/ui/toggle_group_item_behavior.rb +7 -3
- data/app/behaviors/ui/tooltip_behavior.rb +12 -2
- data/app/components/ui/base.rb +8 -0
- data/app/components/ui/calendar.rb +51 -0
- data/app/components/ui/carousel_next.rb +2 -2
- data/app/components/ui/carousel_previous.rb +2 -2
- data/app/components/ui/date_picker.rb +1 -1
- data/app/components/ui/date_picker_trigger.rb +1 -1
- data/app/components/ui/dropdown_menu_trigger.rb +3 -2
- data/app/components/ui/input_group_button.rb +9 -2
- data/app/components/ui/navigation_menu_content.rb +1 -1
- data/app/components/ui/navigation_menu_item.rb +1 -1
- data/app/components/ui/navigation_menu_link.rb +1 -1
- data/app/components/ui/navigation_menu_list.rb +1 -1
- data/app/components/ui/navigation_menu_trigger.rb +1 -1
- data/app/components/ui/popover.rb +26 -1
- data/app/components/ui/select.rb +14 -2
- data/app/components/ui/sonner_toaster.rb +1 -1
- data/app/components/ui/table.rb +7 -7
- data/app/components/ui/table_body.rb +3 -3
- data/app/components/ui/table_footer.rb +3 -3
- data/app/components/ui/table_header.rb +3 -3
- data/app/components/ui/table_row.rb +2 -2
- data/app/components/ui/toggle_group.rb +8 -2
- data/app/components/ui/toggle_group_item.rb +4 -3
- data/app/components/ui/tooltip.rb +26 -2
- data/app/helpers/ui/popover_behavior.rb +5 -2
- data/app/javascript/ui/controllers/collapsible_controller.js +2 -0
- data/app/javascript/ui/controllers/dropdown_controller.js +8 -14
- data/app/view_components/ui/calendar_component.rb +62 -0
- data/app/view_components/ui/dropdown_menu_trigger_component.rb +14 -30
- data/app/view_components/ui/input_group_button_component.rb +16 -1
- data/app/view_components/ui/navigation_menu_content_component.rb +1 -1
- data/app/view_components/ui/navigation_menu_item_component.rb +1 -1
- data/app/view_components/ui/navigation_menu_link_component.rb +1 -1
- data/app/view_components/ui/navigation_menu_list_component.rb +1 -1
- data/app/view_components/ui/navigation_menu_trigger_component.rb +1 -1
- data/app/view_components/ui/popover_component.rb +25 -4
- data/app/view_components/ui/popover_trigger_component.rb +5 -0
- data/app/view_components/ui/select_component.rb +13 -2
- data/app/view_components/ui/table_body_component.rb +1 -1
- data/app/view_components/ui/table_component.rb +4 -4
- data/app/view_components/ui/table_footer_component.rb +1 -1
- data/app/view_components/ui/table_header_component.rb +1 -1
- data/app/view_components/ui/table_row_component.rb +2 -2
- data/app/view_components/ui/tooltip_component.rb +33 -2
- data/app/view_components/ui/tooltip_trigger_component.rb +24 -4
- data/app/views/ui/_avatar.html.erb +8 -4
- data/app/views/ui/_button.html.erb +16 -0
- data/app/views/ui/_date_picker.html.erb +4 -4
- data/app/views/ui/_popover.html.erb +30 -4
- data/app/views/ui/_select.html.erb +13 -2
- data/app/views/ui/_skeleton.html.erb +1 -1
- data/app/views/ui/_tooltip.html.erb +28 -2
- data/app/views/ui/dropdown_menu/_trigger.html.erb +3 -1
- data/app/views/ui/input_group/_button.html.erb +14 -3
- data/app/views/ui/popover/_trigger.html.erb +22 -2
- data/app/views/ui/tooltip/_trigger.html.erb +16 -21
- data/lib/ui/version.rb +1 -1
- metadata +2 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# @example First trigger (receives initial focus)
|
|
12
12
|
# render UI::Trigger.new(first: true) { "Home" }
|
|
13
13
|
class UI::NavigationMenuTrigger < Phlex::HTML
|
|
14
|
-
include UI::
|
|
14
|
+
include UI::NavigationMenuTriggerBehavior
|
|
15
15
|
|
|
16
16
|
# @param first [Boolean] Whether this is the first trigger (gets tabindex=0)
|
|
17
17
|
# @param classes [String] Additional CSS classes to merge
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# Container for popover trigger and content.
|
|
6
6
|
# Uses PopoverBehavior concern for shared styling logic.
|
|
7
7
|
#
|
|
8
|
+
# Supports asChild pattern for composition without wrapper elements.
|
|
9
|
+
#
|
|
8
10
|
# @example Basic usage
|
|
9
11
|
# render UI::Popover.new do
|
|
10
12
|
# render UI::Trigger.new do
|
|
@@ -14,9 +16,20 @@
|
|
|
14
16
|
# plain "Popover content"
|
|
15
17
|
# end
|
|
16
18
|
# end
|
|
19
|
+
#
|
|
20
|
+
# @example With asChild - pass attributes to custom element
|
|
21
|
+
# render UI::Popover.new(as_child: true) do |popover_attrs|
|
|
22
|
+
# render UI::InputGroupAddon.new(**popover_attrs) do
|
|
23
|
+
# render UI::PopoverTrigger.new(as_child: true) do |trigger_attrs|
|
|
24
|
+
# render UI::InputGroupButton.new(**trigger_attrs) { "Info" }
|
|
25
|
+
# end
|
|
26
|
+
# render UI::PopoverContent.new { "Content" }
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
17
29
|
class UI::Popover < Phlex::HTML
|
|
18
30
|
include UI::PopoverBehavior
|
|
19
31
|
|
|
32
|
+
# @param as_child [Boolean] When true, yields attributes to block instead of rendering wrapper
|
|
20
33
|
# @param placement [String] Placement of the popover (e.g., "bottom", "top-start")
|
|
21
34
|
# @param offset [Integer] Distance in pixels from the trigger
|
|
22
35
|
# @param trigger [String] Trigger type ("click" or "hover")
|
|
@@ -24,6 +37,7 @@ class UI::Popover < Phlex::HTML
|
|
|
24
37
|
# @param classes [String] Additional CSS classes to merge
|
|
25
38
|
# @param attributes [Hash] Additional HTML attributes
|
|
26
39
|
def initialize(
|
|
40
|
+
as_child: false,
|
|
27
41
|
placement: "bottom",
|
|
28
42
|
offset: 4,
|
|
29
43
|
trigger: "click",
|
|
@@ -33,6 +47,7 @@ class UI::Popover < Phlex::HTML
|
|
|
33
47
|
side_offset: nil,
|
|
34
48
|
**attributes
|
|
35
49
|
)
|
|
50
|
+
@as_child = as_child
|
|
36
51
|
@placement = placement
|
|
37
52
|
@offset = side_offset || offset
|
|
38
53
|
@trigger = trigger
|
|
@@ -43,6 +58,16 @@ class UI::Popover < Phlex::HTML
|
|
|
43
58
|
end
|
|
44
59
|
|
|
45
60
|
def view_template(&block)
|
|
46
|
-
|
|
61
|
+
popover_attrs = popover_html_attributes.deep_merge(@attributes)
|
|
62
|
+
|
|
63
|
+
if @as_child
|
|
64
|
+
# Yield data attributes to block - child receives controller setup
|
|
65
|
+
yield(popover_attrs) if block_given?
|
|
66
|
+
else
|
|
67
|
+
# Default: render wrapper div with controller
|
|
68
|
+
div(**popover_attrs) do
|
|
69
|
+
yield if block_given?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
47
72
|
end
|
|
48
73
|
end
|
data/app/components/ui/select.rb
CHANGED
|
@@ -29,14 +29,26 @@ class UI::Select < Phlex::HTML
|
|
|
29
29
|
|
|
30
30
|
# @param value [String] Currently selected value
|
|
31
31
|
# @param classes [String] Additional CSS classes to merge
|
|
32
|
+
# @param as_child [Boolean] If true, renders without wrapper div but preserves controller on inner element
|
|
32
33
|
# @param attributes [Hash] Additional HTML attributes
|
|
33
|
-
def initialize(value: nil, classes: "", **attributes)
|
|
34
|
+
def initialize(value: nil, classes: "", as_child: false, **attributes)
|
|
34
35
|
@value = value
|
|
35
36
|
@classes = classes
|
|
37
|
+
@as_child = as_child
|
|
36
38
|
@attributes = attributes
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def view_template(&block)
|
|
40
|
-
|
|
42
|
+
select_attrs = select_html_attributes.deep_merge(@attributes)
|
|
43
|
+
|
|
44
|
+
if @as_child
|
|
45
|
+
# When as_child, we still need a wrapper for the Stimulus controller
|
|
46
|
+
# but we use a minimal inline wrapper that doesn't break flex layouts
|
|
47
|
+
# Override class to use 'contents' which makes the element invisible to layout
|
|
48
|
+
select_attrs[:class] = "contents"
|
|
49
|
+
span(**select_attrs, &block)
|
|
50
|
+
else
|
|
51
|
+
div(**select_attrs, &block)
|
|
52
|
+
end
|
|
41
53
|
end
|
|
42
54
|
end
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# close_button: true
|
|
15
15
|
# ) %>
|
|
16
16
|
class UI::SonnerToaster < Phlex::HTML
|
|
17
|
-
include UI::
|
|
17
|
+
include UI::SonnerToasterBehavior
|
|
18
18
|
|
|
19
19
|
# @param position [String] Toast position (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right)
|
|
20
20
|
# @param theme [String] Theme (light, dark, system)
|
data/app/components/ui/table.rb
CHANGED
|
@@ -16,30 +16,30 @@ class UI::Table < Phlex::HTML
|
|
|
16
16
|
|
|
17
17
|
# DSL methods
|
|
18
18
|
def header(classes: "", **attributes, &block)
|
|
19
|
-
render
|
|
19
|
+
render UI::TableHeader.new(classes: classes, **attributes, &block)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def body(classes: "", **attributes, &block)
|
|
23
|
-
render
|
|
23
|
+
render UI::TableBody.new(classes: classes, **attributes, &block)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def footer(classes: "", **attributes, &block)
|
|
27
|
-
render
|
|
27
|
+
render UI::TableFooter.new(classes: classes, **attributes, &block)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def caption(classes: "", **attributes, &block)
|
|
31
|
-
render
|
|
31
|
+
render UI::TableCaption.new(classes: classes, **attributes, &block)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def row(classes: "", **attributes, &block)
|
|
35
|
-
render
|
|
35
|
+
render UI::TableRow.new(classes: classes, **attributes, &block)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def head(classes: "", **attributes, &block)
|
|
39
|
-
render
|
|
39
|
+
render UI::TableHead.new(classes: classes, **attributes, &block)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def cell(classes: "", **attributes, &block)
|
|
43
|
-
render
|
|
43
|
+
render UI::TableCell.new(classes: classes, **attributes, &block)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -16,14 +16,14 @@ class UI::TableBody < Phlex::HTML
|
|
|
16
16
|
|
|
17
17
|
# DSL methods
|
|
18
18
|
def row(classes: "", **attributes, &block)
|
|
19
|
-
render
|
|
19
|
+
render UI::TableRow.new(classes: classes, **attributes, &block)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def head(classes: "", **attributes, &block)
|
|
23
|
-
render
|
|
23
|
+
render UI::TableHead.new(classes: classes, **attributes, &block)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def cell(classes: "", **attributes, &block)
|
|
27
|
-
render
|
|
27
|
+
render UI::TableCell.new(classes: classes, **attributes, &block)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -16,14 +16,14 @@ class UI::TableFooter < Phlex::HTML
|
|
|
16
16
|
|
|
17
17
|
# DSL methods
|
|
18
18
|
def row(classes: "", **attributes, &block)
|
|
19
|
-
render
|
|
19
|
+
render UI::TableRow.new(classes: classes, **attributes, &block)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def head(classes: "", **attributes, &block)
|
|
23
|
-
render
|
|
23
|
+
render UI::TableHead.new(classes: classes, **attributes, &block)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def cell(classes: "", **attributes, &block)
|
|
27
|
-
render
|
|
27
|
+
render UI::TableCell.new(classes: classes, **attributes, &block)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -16,14 +16,14 @@ class UI::TableHeader < Phlex::HTML
|
|
|
16
16
|
|
|
17
17
|
# DSL methods
|
|
18
18
|
def row(classes: "", **attributes, &block)
|
|
19
|
-
render
|
|
19
|
+
render UI::TableRow.new(classes: classes, **attributes, &block)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def head(classes: "", **attributes, &block)
|
|
23
|
-
render
|
|
23
|
+
render UI::TableHead.new(classes: classes, **attributes, &block)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def cell(classes: "", **attributes, &block)
|
|
27
|
-
render
|
|
27
|
+
render UI::TableCell.new(classes: classes, **attributes, &block)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -16,10 +16,10 @@ class UI::TableRow < Phlex::HTML
|
|
|
16
16
|
|
|
17
17
|
# DSL methods
|
|
18
18
|
def head(classes: "", **attributes, &block)
|
|
19
|
-
render
|
|
19
|
+
render UI::TableHead.new(classes: classes, **attributes, &block)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def cell(classes: "", **attributes, &block)
|
|
23
|
-
render
|
|
23
|
+
render UI::TableCell.new(classes: classes, **attributes, &block)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -41,7 +41,8 @@ class UI::ToggleGroup < Phlex::HTML
|
|
|
41
41
|
|
|
42
42
|
def view_template(&block)
|
|
43
43
|
div(**toggle_group_html_attributes.deep_merge(@attributes)) do
|
|
44
|
-
# Store context for child items to access
|
|
44
|
+
# Store context for child items to access via thread-local
|
|
45
|
+
# This works across both Phlex internal render and Rails render calls
|
|
45
46
|
@context = {
|
|
46
47
|
variant: @variant,
|
|
47
48
|
size: @size,
|
|
@@ -49,7 +50,12 @@ class UI::ToggleGroup < Phlex::HTML
|
|
|
49
50
|
spacing: @spacing,
|
|
50
51
|
value: @value
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
Thread.current[:ui_toggle_group_context] = @context
|
|
54
|
+
begin
|
|
55
|
+
yield if block_given?
|
|
56
|
+
ensure
|
|
57
|
+
Thread.current[:ui_toggle_group_context] = nil
|
|
58
|
+
end
|
|
53
59
|
end
|
|
54
60
|
end
|
|
55
61
|
|
|
@@ -37,9 +37,10 @@ class UI::ToggleGroupItem < Phlex::HTML
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def before_template
|
|
40
|
-
# Try to inherit from parent ToggleGroup context
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
# Try to inherit from parent ToggleGroup context using thread-local
|
|
41
|
+
# This works across both Phlex internal render and Rails render calls
|
|
42
|
+
parent_context = Thread.current[:ui_toggle_group_context]
|
|
43
|
+
if parent_context
|
|
43
44
|
@variant ||= parent_context[:variant]
|
|
44
45
|
@size ||= parent_context[:size]
|
|
45
46
|
@group_type = parent_context[:type]
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Root container for tooltip. Manages tooltip state via Stimulus controller.
|
|
6
6
|
#
|
|
7
|
+
# Supports asChild pattern for composition without wrapper elements.
|
|
8
|
+
#
|
|
7
9
|
# @example Basic usage
|
|
8
10
|
# render UI::Tooltip.new do
|
|
9
11
|
# render UI::Trigger.new { "Hover me" }
|
|
@@ -17,15 +19,37 @@
|
|
|
17
19
|
# end
|
|
18
20
|
# render UI::Content.new { "Tooltip text" }
|
|
19
21
|
# end
|
|
22
|
+
#
|
|
23
|
+
# @example With asChild - pass attributes to custom element
|
|
24
|
+
# render UI::Tooltip.new(as_child: true) do |tooltip_attrs|
|
|
25
|
+
# render UI::InputGroupAddon.new(**tooltip_attrs) do
|
|
26
|
+
# render UI::TooltipTrigger.new(as_child: true) do |trigger_attrs|
|
|
27
|
+
# render UI::InputGroupButton.new(**trigger_attrs) { "Info" }
|
|
28
|
+
# end
|
|
29
|
+
# render UI::TooltipContent.new { "Content" }
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
20
32
|
class UI::Tooltip < Phlex::HTML
|
|
21
33
|
include UI::TooltipBehavior
|
|
22
34
|
|
|
35
|
+
# @param as_child [Boolean] When true, yields attributes to block instead of rendering wrapper
|
|
23
36
|
# @param attributes [Hash] Additional HTML attributes
|
|
24
|
-
def initialize(**attributes)
|
|
37
|
+
def initialize(as_child: false, **attributes)
|
|
38
|
+
@as_child = as_child
|
|
25
39
|
@attributes = attributes
|
|
26
40
|
end
|
|
27
41
|
|
|
28
42
|
def view_template(&block)
|
|
29
|
-
|
|
43
|
+
tooltip_attrs = tooltip_html_attributes.deep_merge(@attributes)
|
|
44
|
+
|
|
45
|
+
if @as_child
|
|
46
|
+
# Yield data attributes to block - child receives controller setup
|
|
47
|
+
yield(tooltip_attrs) if block_given?
|
|
48
|
+
else
|
|
49
|
+
# Default: render wrapper div with controller
|
|
50
|
+
div(**tooltip_attrs) do
|
|
51
|
+
yield if block_given?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
30
54
|
end
|
|
31
55
|
end
|
|
@@ -8,10 +8,13 @@ module UI::PopoverBehavior
|
|
|
8
8
|
# Returns HTML attributes for the popover container element
|
|
9
9
|
def popover_html_attributes
|
|
10
10
|
attributes_value = respond_to?(:attributes, true) ? attributes : @attributes
|
|
11
|
+
user_data = attributes_value[:data] || {}
|
|
12
|
+
other_attributes = attributes_value.except(:data)
|
|
13
|
+
|
|
11
14
|
{
|
|
12
15
|
class: popover_classes,
|
|
13
|
-
data: popover_data_attributes
|
|
14
|
-
}.merge(
|
|
16
|
+
data: popover_data_attributes.merge(user_data)
|
|
17
|
+
}.merge(other_attributes)
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
# Returns combined CSS classes for the popover
|
|
@@ -45,6 +45,8 @@ export default class extends Controller {
|
|
|
45
45
|
if (isOpen) {
|
|
46
46
|
// Opening: remove hidden and set height
|
|
47
47
|
content.removeAttribute("hidden")
|
|
48
|
+
// Force reflow to get correct scrollHeight after removing hidden
|
|
49
|
+
void content.offsetHeight
|
|
48
50
|
content.style.height = `${content.scrollHeight}px`
|
|
49
51
|
} else {
|
|
50
52
|
// Closing: animate to 0, then add hidden after transition
|
|
@@ -82,17 +82,14 @@ export default class extends Controller {
|
|
|
82
82
|
const trigger = event.currentTarget
|
|
83
83
|
const submenu = trigger.nextElementSibling
|
|
84
84
|
|
|
85
|
-
// Remove DOM focus from currently focused element to clear :focus pseudo-class
|
|
86
|
-
if (document.activeElement && document.activeElement.hasAttribute('role') &&
|
|
87
|
-
document.activeElement.getAttribute('role') === 'menuitem') {
|
|
88
|
-
document.activeElement.blur()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
85
|
// Remove keyboard focus from all items, except this one
|
|
92
86
|
clearAllTabindexes(this.element)
|
|
93
87
|
|
|
94
|
-
// Set tabindex="0" on the hovered trigger
|
|
88
|
+
// Set tabindex="0" on the hovered trigger and focus it
|
|
89
|
+
// Focusing the new item instead of blurring the old one keeps focus inside
|
|
90
|
+
// the dropdown, preventing handleFocusOut from closing the menu
|
|
95
91
|
trigger.setAttribute('tabindex', '0')
|
|
92
|
+
trigger.focus()
|
|
96
93
|
|
|
97
94
|
// Track the hovered item for keyboard navigation continuity
|
|
98
95
|
this.lastHoveredItem = trigger
|
|
@@ -147,17 +144,14 @@ export default class extends Controller {
|
|
|
147
144
|
trackHoveredItem(event) {
|
|
148
145
|
const item = event.currentTarget
|
|
149
146
|
|
|
150
|
-
// Remove DOM focus from currently focused element to clear :focus pseudo-class
|
|
151
|
-
if (document.activeElement && document.activeElement.hasAttribute('role') &&
|
|
152
|
-
document.activeElement.getAttribute('role') === 'menuitem') {
|
|
153
|
-
document.activeElement.blur()
|
|
154
|
-
}
|
|
155
|
-
|
|
156
147
|
// Remove keyboard focus from all items, except this one
|
|
157
148
|
clearAllTabindexes(this.element)
|
|
158
149
|
|
|
159
|
-
// Set tabindex="0" on the hovered item
|
|
150
|
+
// Set tabindex="0" on the hovered item and focus it
|
|
151
|
+
// Focusing the new item instead of blurring the old one keeps focus inside
|
|
152
|
+
// the dropdown, preventing handleFocusOut from closing the menu
|
|
160
153
|
item.setAttribute('tabindex', '0')
|
|
154
|
+
item.focus()
|
|
161
155
|
|
|
162
156
|
// Track the hovered item for keyboard navigation continuity
|
|
163
157
|
this.lastHoveredItem = item
|
|
@@ -121,6 +121,14 @@ class UI::CalendarComponent < ViewComponent::Base
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
def dropdowns_html
|
|
124
|
+
if use_native_select?
|
|
125
|
+
native_dropdowns_html
|
|
126
|
+
else
|
|
127
|
+
ui_select_dropdowns_html
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def native_dropdowns_html
|
|
124
132
|
content_tag(:div, class: "flex items-center gap-1") do
|
|
125
133
|
safe_join([month_select_html, year_select_html])
|
|
126
134
|
end
|
|
@@ -143,6 +151,60 @@ class UI::CalendarComponent < ViewComponent::Base
|
|
|
143
151
|
end
|
|
144
152
|
end
|
|
145
153
|
|
|
154
|
+
def ui_select_dropdowns_html
|
|
155
|
+
content_tag(:div, class: "flex items-center gap-1 px-8 flex-1") do
|
|
156
|
+
safe_join([
|
|
157
|
+
ui_month_select_html,
|
|
158
|
+
ui_year_select_html
|
|
159
|
+
])
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ui_month_select_html
|
|
164
|
+
render(UI::SelectComponent.new(classes: "flex-1", value: (@month.month - 1).to_s)) do
|
|
165
|
+
safe_join([
|
|
166
|
+
render(UI::SelectTriggerComponent.new(classes: "h-9 w-full gap-1 px-2 text-sm font-medium border-0 shadow-none")),
|
|
167
|
+
render(UI::SelectContentComponent.new(classes: "min-w-[8rem]")) {
|
|
168
|
+
safe_join(Date::MONTHNAMES.compact.each_with_index.map { |name, i|
|
|
169
|
+
render(UI::SelectItemComponent.new(value: i.to_s)) { name }
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
tag.input(
|
|
173
|
+
type: "hidden",
|
|
174
|
+
value: (@month.month - 1).to_s,
|
|
175
|
+
data: {
|
|
176
|
+
ui__select_target: "hiddenInput",
|
|
177
|
+
ui__calendar_target: "monthSelect",
|
|
178
|
+
action: "change->ui--calendar#goToMonth"
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
])
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def ui_year_select_html
|
|
186
|
+
current_year = Date.today.year
|
|
187
|
+
render(UI::SelectComponent.new(value: @month.year.to_s)) do
|
|
188
|
+
safe_join([
|
|
189
|
+
render(UI::SelectTriggerComponent.new(classes: "min-w-[75px] h-9 w-auto gap-1 px-2 text-sm font-medium border-0 shadow-none")),
|
|
190
|
+
render(UI::SelectContentComponent.new(classes: "min-w-[5rem] max-h-[200px]")) {
|
|
191
|
+
safe_join(((current_year - @year_range)..(current_year + 10)).map { |y|
|
|
192
|
+
render(UI::SelectItemComponent.new(value: y.to_s)) { y.to_s }
|
|
193
|
+
})
|
|
194
|
+
},
|
|
195
|
+
tag.input(
|
|
196
|
+
type: "hidden",
|
|
197
|
+
value: @month.year.to_s,
|
|
198
|
+
data: {
|
|
199
|
+
ui__select_target: "hiddenInput",
|
|
200
|
+
ui__calendar_target: "yearSelect",
|
|
201
|
+
action: "change->ui--calendar#goToYear"
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
])
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
146
208
|
def table_html
|
|
147
209
|
content_tag(:table, class: "w-full border-collapse space-y-1", role: "grid") do
|
|
148
210
|
safe_join([
|
|
@@ -4,47 +4,31 @@
|
|
|
4
4
|
class UI::DropdownMenuTriggerComponent < ViewComponent::Base
|
|
5
5
|
include UI::DropdownMenuTriggerBehavior
|
|
6
6
|
|
|
7
|
+
renders_one :trigger_content
|
|
8
|
+
|
|
7
9
|
def initialize(as_child: false, classes: "", **attributes)
|
|
8
10
|
@as_child = as_child
|
|
9
11
|
@classes = classes
|
|
10
12
|
@attributes = attributes
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
# Returns trigger attributes for as_child mode
|
|
16
|
+
def trigger_attrs
|
|
17
|
+
attrs = dropdown_menu_trigger_html_attributes.deep_merge(@attributes)
|
|
18
|
+
# When as_child is true, only pass functional attributes (data, aria, tabindex, role)
|
|
19
|
+
# The child component handles its own styling (following shadcn pattern)
|
|
20
|
+
attrs.except(:class)
|
|
21
|
+
end
|
|
22
|
+
|
|
13
23
|
def call
|
|
14
|
-
|
|
24
|
+
all_attrs = dropdown_menu_trigger_html_attributes.deep_merge(@attributes)
|
|
15
25
|
|
|
16
26
|
if @as_child
|
|
17
|
-
# asChild mode:
|
|
18
|
-
|
|
19
|
-
doc = Nokogiri::HTML::DocumentFragment.parse(rendered)
|
|
20
|
-
first_element = doc.children.find { |node| node.element? }
|
|
21
|
-
|
|
22
|
-
if first_element
|
|
23
|
-
# Merge data attributes (convert Rails naming to HTML)
|
|
24
|
-
trigger_attrs.fetch(:data, {}).each do |key, value|
|
|
25
|
-
html_key = key.to_s.gsub("__", "--").tr("_", "-")
|
|
26
|
-
first_element["data-#{html_key}"] = value
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Merge CSS classes with TailwindMerge
|
|
30
|
-
if trigger_attrs[:class]
|
|
31
|
-
existing_classes = first_element["class"] || ""
|
|
32
|
-
merged_classes = TailwindMerge::Merger.new.merge([existing_classes, trigger_attrs[:class]].join(" "))
|
|
33
|
-
first_element["class"] = merged_classes
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Merge other attributes (except data and class)
|
|
37
|
-
trigger_attrs.except(:data, :class).each do |key, value|
|
|
38
|
-
first_element[key.to_s] = value
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
doc.to_html.html_safe
|
|
42
|
-
else
|
|
43
|
-
content
|
|
44
|
-
end
|
|
27
|
+
# asChild mode: yield attributes to block, child handles rendering
|
|
28
|
+
content
|
|
45
29
|
else
|
|
46
30
|
# Default mode: render as div
|
|
47
|
-
content_tag :div, **
|
|
31
|
+
content_tag :div, **all_attrs do
|
|
48
32
|
content
|
|
49
33
|
end
|
|
50
34
|
end
|
|
@@ -31,6 +31,21 @@ class UI::InputGroupButtonComponent < ViewComponent::Base
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def call
|
|
34
|
-
|
|
34
|
+
# Get base attributes from behavior (type, variant, classes)
|
|
35
|
+
base_attrs = input_group_button_attributes
|
|
36
|
+
|
|
37
|
+
# Extract button-specific params that ui/button accepts as direct arguments
|
|
38
|
+
button_params = {
|
|
39
|
+
type: base_attrs[:type],
|
|
40
|
+
variant: base_attrs[:variant],
|
|
41
|
+
classes: base_attrs[:classes],
|
|
42
|
+
content: content
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Merge additional HTML attributes (data, role, aria, tabindex, etc.) into attributes hash
|
|
46
|
+
html_attrs = base_attrs.except(:type, :variant, :classes, :"data-size")
|
|
47
|
+
button_params[:attributes] = html_attrs unless html_attrs.empty?
|
|
48
|
+
|
|
49
|
+
render partial: "ui/button", locals: button_params
|
|
35
50
|
end
|
|
36
51
|
end
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Container for navigation menu content that appears when trigger is activated.
|
|
6
6
|
class UI::NavigationMenuContentComponent < ViewComponent::Base
|
|
7
|
-
include UI::
|
|
7
|
+
include UI::NavigationMenuContentBehavior
|
|
8
8
|
|
|
9
9
|
# @param viewport [Boolean] Whether content should be rendered in viewport
|
|
10
10
|
# @param classes [String] Additional CSS classes to merge
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Wrapper for individual navigation menu item.
|
|
6
6
|
class UI::NavigationMenuItemComponent < ViewComponent::Base
|
|
7
|
-
include UI::
|
|
7
|
+
include UI::NavigationMenuItemBehavior
|
|
8
8
|
|
|
9
9
|
# @param value [String] Optional value for controlled state
|
|
10
10
|
# @param classes [String] Additional CSS classes to merge
|
|
@@ -21,7 +21,7 @@ require "nokogiri"
|
|
|
21
21
|
# <%= link_to "About", about_path %>
|
|
22
22
|
# <% end %>
|
|
23
23
|
class UI::NavigationMenuLinkComponent < ViewComponent::Base
|
|
24
|
-
include UI::
|
|
24
|
+
include UI::NavigationMenuLinkBehavior
|
|
25
25
|
include UI::SharedAsChildBehavior
|
|
26
26
|
|
|
27
27
|
# @param href [String] URL for the link (ignored when as_child: true)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Container for navigation menu items.
|
|
6
6
|
class UI::NavigationMenuListComponent < ViewComponent::Base
|
|
7
|
-
include UI::
|
|
7
|
+
include UI::NavigationMenuListBehavior
|
|
8
8
|
|
|
9
9
|
# @param classes [String] Additional CSS classes to merge
|
|
10
10
|
# @param attributes [Hash] Additional HTML attributes
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Button that toggles the navigation menu content.
|
|
6
6
|
# Includes an animated chevron icon.
|
|
7
7
|
class UI::NavigationMenuTriggerComponent < ViewComponent::Base
|
|
8
|
-
include UI::
|
|
8
|
+
include UI::NavigationMenuTriggerBehavior
|
|
9
9
|
|
|
10
10
|
# @param first [Boolean] Whether this is the first trigger (gets tabindex=0)
|
|
11
11
|
# @param classes [String] Additional CSS classes to merge
|