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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/ui.esm.js +3 -6
  3. data/app/assets/javascripts/ui.js +3 -6
  4. data/app/behaviors/ui/command_dialog_behavior.rb +0 -8
  5. data/app/behaviors/ui/empty_behavior.rb +6 -6
  6. data/app/behaviors/ui/input_behavior.rb +1 -1
  7. data/app/behaviors/ui/input_group_behavior.rb +3 -1
  8. data/app/behaviors/ui/input_group_button_behavior.rb +7 -6
  9. data/app/behaviors/ui/menubar_checkbox_item_behavior.rb +1 -1
  10. data/app/behaviors/ui/menubar_radio_item_behavior.rb +1 -1
  11. data/app/behaviors/ui/popover_behavior.rb +11 -3
  12. data/app/behaviors/ui/spinner_behavior.rb +1 -1
  13. data/app/behaviors/ui/switch_behavior.rb +49 -49
  14. data/app/behaviors/ui/toggle_group_behavior.rb +2 -2
  15. data/app/behaviors/ui/toggle_group_item_behavior.rb +7 -3
  16. data/app/behaviors/ui/tooltip_behavior.rb +12 -2
  17. data/app/components/ui/base.rb +8 -0
  18. data/app/components/ui/calendar.rb +51 -0
  19. data/app/components/ui/carousel_next.rb +2 -2
  20. data/app/components/ui/carousel_previous.rb +2 -2
  21. data/app/components/ui/date_picker.rb +1 -1
  22. data/app/components/ui/date_picker_trigger.rb +1 -1
  23. data/app/components/ui/dropdown_menu_trigger.rb +3 -2
  24. data/app/components/ui/input_group_button.rb +9 -2
  25. data/app/components/ui/navigation_menu_content.rb +1 -1
  26. data/app/components/ui/navigation_menu_item.rb +1 -1
  27. data/app/components/ui/navigation_menu_link.rb +1 -1
  28. data/app/components/ui/navigation_menu_list.rb +1 -1
  29. data/app/components/ui/navigation_menu_trigger.rb +1 -1
  30. data/app/components/ui/popover.rb +26 -1
  31. data/app/components/ui/select.rb +14 -2
  32. data/app/components/ui/sonner_toaster.rb +1 -1
  33. data/app/components/ui/table.rb +7 -7
  34. data/app/components/ui/table_body.rb +3 -3
  35. data/app/components/ui/table_footer.rb +3 -3
  36. data/app/components/ui/table_header.rb +3 -3
  37. data/app/components/ui/table_row.rb +2 -2
  38. data/app/components/ui/toggle_group.rb +8 -2
  39. data/app/components/ui/toggle_group_item.rb +4 -3
  40. data/app/components/ui/tooltip.rb +26 -2
  41. data/app/helpers/ui/popover_behavior.rb +5 -2
  42. data/app/javascript/ui/controllers/collapsible_controller.js +2 -0
  43. data/app/javascript/ui/controllers/dropdown_controller.js +8 -14
  44. data/app/view_components/ui/calendar_component.rb +62 -0
  45. data/app/view_components/ui/dropdown_menu_trigger_component.rb +14 -30
  46. data/app/view_components/ui/input_group_button_component.rb +16 -1
  47. data/app/view_components/ui/navigation_menu_content_component.rb +1 -1
  48. data/app/view_components/ui/navigation_menu_item_component.rb +1 -1
  49. data/app/view_components/ui/navigation_menu_link_component.rb +1 -1
  50. data/app/view_components/ui/navigation_menu_list_component.rb +1 -1
  51. data/app/view_components/ui/navigation_menu_trigger_component.rb +1 -1
  52. data/app/view_components/ui/popover_component.rb +25 -4
  53. data/app/view_components/ui/popover_trigger_component.rb +5 -0
  54. data/app/view_components/ui/select_component.rb +13 -2
  55. data/app/view_components/ui/table_body_component.rb +1 -1
  56. data/app/view_components/ui/table_component.rb +4 -4
  57. data/app/view_components/ui/table_footer_component.rb +1 -1
  58. data/app/view_components/ui/table_header_component.rb +1 -1
  59. data/app/view_components/ui/table_row_component.rb +2 -2
  60. data/app/view_components/ui/tooltip_component.rb +33 -2
  61. data/app/view_components/ui/tooltip_trigger_component.rb +24 -4
  62. data/app/views/ui/_avatar.html.erb +8 -4
  63. data/app/views/ui/_button.html.erb +16 -0
  64. data/app/views/ui/_date_picker.html.erb +4 -4
  65. data/app/views/ui/_popover.html.erb +30 -4
  66. data/app/views/ui/_select.html.erb +13 -2
  67. data/app/views/ui/_skeleton.html.erb +1 -1
  68. data/app/views/ui/_tooltip.html.erb +28 -2
  69. data/app/views/ui/dropdown_menu/_trigger.html.erb +3 -1
  70. data/app/views/ui/input_group/_button.html.erb +14 -3
  71. data/app/views/ui/popover/_trigger.html.erb +22 -2
  72. data/app/views/ui/tooltip/_trigger.html.erb +16 -21
  73. data/lib/ui/version.rb +1 -1
  74. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da1c77278662e1886130b12bbcbc630a4d19cb0651c81362e6006e8f1a9e5351
4
- data.tar.gz: 26c135df8c057f54aa32c8a630e0f551ad1bab59c70b7addf2e1ac7e1a1ff3c4
3
+ metadata.gz: a5e204a6fcf76008dccc34ec8487f8b2bd80ba924a43aeecd59a44802d744ade
4
+ data.tar.gz: f470c5ab397812019513fdbb08d6c77166613f06be3e5f22ec801905b32860c7
5
5
  SHA512:
6
- metadata.gz: 26f10d3e9aa1d9ade5064cb6d9cd8b3f6b52c6eed32ddab06d4860128399aa2c88a94fc284b0fa8be23863f50ac0b8a53faf4655b4c1f5fd31bdc38eb57cda6c
7
- data.tar.gz: 85458b4aaedfeae39402e9e008f1b329c11a0ce95489f9a44b62183224a4c2848ed005ebc29ab5bf88d5b71eded993321d67d72b8093432650184e211e224b5f
6
+ metadata.gz: dfce0d35ccdb4fcc6a09108aef9eb7e197046a9c2bed89545cc5a979f7d7e870f593ba545f632cdf3021557a726f4c1d7d726a0cf08a31f51a9896c376a35fa8
7
+ data.tar.gz: 22dee7451abe1a42880c98ec161c33e5baf43b264ded6399e8d3c5a24c6f55d9a0bf7355ff684e5c90a4121924babcc6f39dac98a5c48788e00624c413125ca8
@@ -1703,11 +1703,9 @@ class DropdownController extends Controller {
1703
1703
  openSubmenuHandler(event) {
1704
1704
  const trigger = event.currentTarget;
1705
1705
  const submenu = trigger.nextElementSibling;
1706
- if (document.activeElement && document.activeElement.hasAttribute("role") && document.activeElement.getAttribute("role") === "menuitem") {
1707
- document.activeElement.blur();
1708
- }
1709
1706
  clearAllTabindexes(this.element);
1710
1707
  trigger.setAttribute("tabindex", "0");
1708
+ trigger.focus();
1711
1709
  this.lastHoveredItem = trigger;
1712
1710
  if (this.closeSubmenuTimeouts.has(trigger)) {
1713
1711
  clearTimeout(this.closeSubmenuTimeouts.get(trigger));
@@ -1740,11 +1738,9 @@ class DropdownController extends Controller {
1740
1738
  }
1741
1739
  trackHoveredItem(event) {
1742
1740
  const item = event.currentTarget;
1743
- if (document.activeElement && document.activeElement.hasAttribute("role") && document.activeElement.getAttribute("role") === "menuitem") {
1744
- document.activeElement.blur();
1745
- }
1746
1741
  clearAllTabindexes(this.element);
1747
1742
  item.setAttribute("tabindex", "0");
1743
+ item.focus();
1748
1744
  this.lastHoveredItem = item;
1749
1745
  }
1750
1746
  toggleCheckbox(event) {
@@ -2802,6 +2798,7 @@ class CollapsibleController extends Controller {
2802
2798
  content.dataset.state = state;
2803
2799
  if (isOpen) {
2804
2800
  content.removeAttribute("hidden");
2801
+ void content.offsetHeight;
2805
2802
  content.style.height = `${content.scrollHeight}px`;
2806
2803
  } else {
2807
2804
  if (animate) {
@@ -1588,11 +1588,9 @@
1588
1588
  openSubmenuHandler(event) {
1589
1589
  const trigger = event.currentTarget;
1590
1590
  const submenu = trigger.nextElementSibling;
1591
- if (document.activeElement && document.activeElement.hasAttribute("role") && document.activeElement.getAttribute("role") === "menuitem") {
1592
- document.activeElement.blur();
1593
- }
1594
1591
  clearAllTabindexes(this.element);
1595
1592
  trigger.setAttribute("tabindex", "0");
1593
+ trigger.focus();
1596
1594
  this.lastHoveredItem = trigger;
1597
1595
  if (this.closeSubmenuTimeouts.has(trigger)) {
1598
1596
  clearTimeout(this.closeSubmenuTimeouts.get(trigger));
@@ -1625,11 +1623,9 @@
1625
1623
  }
1626
1624
  trackHoveredItem(event) {
1627
1625
  const item = event.currentTarget;
1628
- if (document.activeElement && document.activeElement.hasAttribute("role") && document.activeElement.getAttribute("role") === "menuitem") {
1629
- document.activeElement.blur();
1630
- }
1631
1626
  clearAllTabindexes(this.element);
1632
1627
  item.setAttribute("tabindex", "0");
1628
+ item.focus();
1633
1629
  this.lastHoveredItem = item;
1634
1630
  }
1635
1631
  toggleCheckbox(event) {
@@ -2661,6 +2657,7 @@
2661
2657
  content.dataset.state = state;
2662
2658
  if (isOpen) {
2663
2659
  content.removeAttribute("hidden");
2660
+ void content.offsetHeight;
2664
2661
  content.style.height = `${content.scrollHeight}px`;
2665
2662
  } else {
2666
2663
  if (animate) {
@@ -1,20 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  # UI::CommandDialogBehavior
5
4
 
6
- #
7
-
8
5
  # @ui_component Command Dialog
9
6
 
10
7
  # @ui_category other
11
8
 
12
- #
13
-
14
9
  # @ui_anatomy Command Dialog - Root container with state management (required)
15
10
 
16
- #
17
-
18
11
  # @ui_feature Keyboard navigation
19
12
 
20
13
  # @ui_feature Custom styling with Tailwind classes
@@ -23,7 +16,6 @@
23
16
 
24
17
  # @ui_feature Animation support
25
18
 
26
- #
27
19
  module UI::CommandDialogBehavior
28
20
  def command_dialog_base_classes
29
21
  ""
@@ -18,7 +18,7 @@ module UI::EmptyBehavior
18
18
  def empty_html_attributes
19
19
  {
20
20
  class: empty_classes,
21
- data: { slot: "empty" }
21
+ data: {slot: "empty"}
22
22
  }
23
23
  end
24
24
 
@@ -36,7 +36,7 @@ module UI::EmptyHeaderBehavior
36
36
  def empty_header_html_attributes
37
37
  {
38
38
  class: empty_header_classes,
39
- data: { slot: "empty-header" }
39
+ data: {slot: "empty-header"}
40
40
  }
41
41
  end
42
42
 
@@ -54,7 +54,7 @@ module UI::EmptyMediaBehavior
54
54
  def empty_media_html_attributes
55
55
  {
56
56
  class: empty_media_classes,
57
- data: { slot: "empty-media" }
57
+ data: {slot: "empty-media"}
58
58
  }
59
59
  end
60
60
 
@@ -86,7 +86,7 @@ module UI::EmptyTitleBehavior
86
86
  def empty_title_html_attributes
87
87
  {
88
88
  class: empty_title_classes,
89
- data: { slot: "empty-title" }
89
+ data: {slot: "empty-title"}
90
90
  }
91
91
  end
92
92
 
@@ -104,7 +104,7 @@ module UI::EmptyDescriptionBehavior
104
104
  def empty_description_html_attributes
105
105
  {
106
106
  class: empty_description_classes,
107
- data: { slot: "empty-description" }
107
+ data: {slot: "empty-description"}
108
108
  }
109
109
  end
110
110
 
@@ -122,7 +122,7 @@ module UI::EmptyContentBehavior
122
122
  def empty_content_html_attributes
123
123
  {
124
124
  class: empty_content_classes,
125
- data: { slot: "empty-content" }
125
+ data: {slot: "empty-content"}
126
126
  }
127
127
  end
128
128
 
@@ -27,7 +27,7 @@ module UI::InputBehavior
27
27
  name: @name,
28
28
  id: @id,
29
29
  readonly: @readonly,
30
- data: { slot: "input" }
30
+ data: {slot: "input"}
31
31
  }.compact
32
32
  end
33
33
 
@@ -32,8 +32,9 @@ module UI::InputGroupBehavior
32
32
  private
33
33
 
34
34
  # Base classes - exact match from shadcn/ui
35
+ # Uses CSS variable --radius for customizable border radius (defaults to 0.375rem / rounded-md)
35
36
  def input_group_base_classes
36
- "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none"
37
+ "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-[var(--radius,0.375rem)] border shadow-xs transition-[color,box-shadow] outline-none"
37
38
  end
38
39
 
39
40
  # Height classes
@@ -52,6 +53,7 @@ module UI::InputGroupBehavior
52
53
  end
53
54
 
54
55
  # Focus state classes
56
+ # Uses box-shadow for focus ring which automatically follows border-radius
55
57
  def input_group_focus_classes
56
58
  "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]"
57
59
  end
@@ -25,7 +25,7 @@ module UI::InputGroupButtonBehavior
25
25
  variant: @variant,
26
26
  classes: input_group_button_classes,
27
27
  "data-size": @size
28
- }.merge(attributes_value).compact
28
+ }.deep_merge(attributes_value).compact
29
29
  end
30
30
 
31
31
  # Returns combined CSS classes for the button
@@ -72,18 +72,19 @@ module UI::InputGroupButtonBehavior
72
72
  end
73
73
 
74
74
  # Size-specific classes
75
+ # Uses calc(var(--radius, 0.375rem) - 5px) to create slightly smaller rounded corners for nested buttons
75
76
  def input_group_button_size_classes
76
77
  case @size.to_s
77
78
  when "xs"
78
- "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2"
79
+ "h-6 gap-1 px-2 rounded-[calc(var(--radius,0.375rem)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2"
79
80
  when "sm"
80
- "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5"
81
+ "h-8 px-2.5 gap-1.5 rounded-[calc(var(--radius,0.375rem)-5px)] has-[>svg]:px-2.5"
81
82
  when "icon-xs"
82
- "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0"
83
+ "size-6 rounded-[calc(var(--radius,0.375rem)-5px)] p-0 has-[>svg]:p-0"
83
84
  when "icon-sm"
84
- "size-8 p-0 has-[>svg]:p-0"
85
+ "size-8 rounded-[calc(var(--radius,0.375rem)-5px)] p-0 has-[>svg]:p-0"
85
86
  else
86
- "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2"
87
+ "h-6 gap-1 px-2 rounded-[calc(var(--radius,0.375rem)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2"
87
88
  end
88
89
  end
89
90
  end
@@ -39,7 +39,7 @@ module UI::MenubarCheckboxItemBehavior
39
39
  attributes_value = respond_to?(:attributes, true) ? attributes : @attributes
40
40
  (attributes_value&.fetch(:data, {}) || {}).merge({
41
41
  "ui--menubar-target": "item",
42
- action: "click->ui--menubar#toggleCheckbox mouseenter->ui--menubar#trackHoveredItem",
42
+ action: "click->ui--menubar#selectItem mouseenter->ui--menubar#trackHoveredItem",
43
43
  state: checked? ? "checked" : "unchecked"
44
44
  })
45
45
  end
@@ -39,7 +39,7 @@ module UI::MenubarRadioItemBehavior
39
39
  attributes_value = respond_to?(:attributes, true) ? attributes : @attributes
40
40
  base_data = {
41
41
  "ui--menubar-target": "item",
42
- action: "click->ui--menubar#selectRadio mouseenter->ui--menubar#trackHoveredItem",
42
+ action: "click->ui--menubar#selectItem mouseenter->ui--menubar#trackHoveredItem",
43
43
  state: checked? ? "checked" : "unchecked"
44
44
  }
45
45
 
@@ -25,12 +25,20 @@
25
25
  #
26
26
  module UI::PopoverBehavior
27
27
  # Returns HTML attributes for the popover container element
28
+ # When used with asChild, only data attributes are returned (no classes)
28
29
  def popover_html_attributes
29
30
  attributes_value = respond_to?(:attributes, true) ? attributes : @attributes
30
- {
31
- class: popover_classes,
31
+ attrs = {
32
32
  data: popover_data_attributes
33
- }.merge(attributes_value)
33
+ }
34
+
35
+ # Only add container classes if not using asChild
36
+ # When asChild is true, the child component handles its own styling
37
+ unless instance_variable_defined?(:@as_child) && @as_child
38
+ attrs[:class] = popover_classes
39
+ end
40
+
41
+ attrs.merge(attributes_value)
34
42
  end
35
43
 
36
44
  # Returns combined CSS classes for the popover
@@ -18,7 +18,7 @@ module UI::SpinnerBehavior
18
18
  class: spinner_classes,
19
19
  role: "status",
20
20
  "aria-label": "Loading",
21
- data: { slot: "spinner" }
21
+ data: {slot: "spinner"}
22
22
  }
23
23
  end
24
24
 
@@ -28,62 +28,62 @@
28
28
  # @ui_related toggle
29
29
  #
30
30
  module UI::SwitchBehavior
31
- # Returns HTML attributes for the switch button element
32
- def switch_html_attributes
33
- {
34
- class: switch_classes,
35
- role: "switch",
36
- type: "button",
37
- tabindex: @disabled ? -1 : 0,
38
- "aria-checked": switch_aria_checked,
39
- "data-state": switch_state,
40
- "data-slot": "switch",
41
- data: switch_data_attributes
42
- }.tap do |attrs|
43
- if @disabled
44
- attrs[:disabled] = true
45
- attrs["aria-disabled"] = "true"
31
+ # Returns HTML attributes for the switch button element
32
+ def switch_html_attributes
33
+ {
34
+ class: switch_classes,
35
+ role: "switch",
36
+ type: "button",
37
+ tabindex: @disabled ? -1 : 0,
38
+ "aria-checked": switch_aria_checked,
39
+ "data-state": switch_state,
40
+ "data-slot": "switch",
41
+ data: switch_data_attributes
42
+ }.tap do |attrs|
43
+ if @disabled
44
+ attrs[:disabled] = true
45
+ attrs["aria-disabled"] = "true"
46
+ end
46
47
  end
47
48
  end
48
- end
49
49
 
50
- # Returns combined CSS classes for the switch
51
- def switch_classes
52
- classes_value = respond_to?(:classes, true) ? classes : @classes
53
- TailwindMerge::Merger.new.merge([
54
- switch_base_classes,
55
- classes_value
56
- ].compact.join(" "))
57
- end
50
+ # Returns combined CSS classes for the switch
51
+ def switch_classes
52
+ classes_value = respond_to?(:classes, true) ? classes : @classes
53
+ TailwindMerge::Merger.new.merge([
54
+ switch_base_classes,
55
+ classes_value
56
+ ].compact.join(" "))
57
+ end
58
58
 
59
- # Returns CSS classes for the thumb element
60
- def switch_thumb_classes
61
- "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
62
- end
59
+ # Returns CSS classes for the thumb element
60
+ def switch_thumb_classes
61
+ "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
62
+ end
63
63
 
64
- private
64
+ private
65
65
 
66
- # Base classes applied to all switches
67
- def switch_base_classes
68
- "peer data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
69
- end
66
+ # Base classes applied to all switches
67
+ def switch_base_classes
68
+ "peer data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
69
+ end
70
70
 
71
- # Returns the switch state (checked or unchecked)
72
- def switch_state
73
- @checked ? "checked" : "unchecked"
74
- end
71
+ # Returns the switch state (checked or unchecked)
72
+ def switch_state
73
+ @checked ? "checked" : "unchecked"
74
+ end
75
75
 
76
- # Returns aria-checked attribute value
77
- def switch_aria_checked
78
- @checked.to_s
79
- end
76
+ # Returns aria-checked attribute value
77
+ def switch_aria_checked
78
+ @checked.to_s
79
+ end
80
80
 
81
- # Returns data attributes for Stimulus controller
82
- def switch_data_attributes
83
- {
84
- controller: "ui--switch",
85
- ui__switch_checked_value: @checked,
86
- action: "click->ui--switch#toggle keydown->ui--switch#handleKeydown"
87
- }
88
- end
81
+ # Returns data attributes for Stimulus controller
82
+ def switch_data_attributes
83
+ {
84
+ controller: "ui--switch",
85
+ ui__switch_checked_value: @checked,
86
+ action: "click->ui--switch#toggle keydown->ui--switch#handleKeydown"
87
+ }
88
+ end
89
89
  end
@@ -11,11 +11,11 @@ module UI::ToggleGroupBehavior
11
11
  def toggle_group_html_attributes
12
12
  attrs = {
13
13
  class: toggle_group_classes,
14
- role: @type == "single" ? "radiogroup" : "group",
14
+ role: (@type == "single") ? "radiogroup" : "group",
15
15
  data: {
16
16
  controller: "ui--toggle-group",
17
17
  "ui--toggle-group-type-value": @type || "single",
18
- "ui--toggle-group-value-value": @value&.to_json || (@type == "multiple" ? "[]" : "null")
18
+ "ui--toggle-group-value-value": @value&.to_json || ((@type == "multiple") ? "[]" : "null")
19
19
  }
20
20
  }
21
21
 
@@ -12,10 +12,14 @@ module UI::ToggleGroupItemBehavior
12
12
  attrs = {
13
13
  class: toggle_group_item_classes,
14
14
  type: "button",
15
- role: @group_type == "single" ? "radio" : "button",
15
+ role: (@group_type == "single") ? "radio" : "button",
16
16
  disabled: @disabled ? true : nil,
17
- "aria-pressed": @group_type == "multiple" ? (@pressed ? "true" : "false") : nil,
18
- "aria-checked": @group_type == "single" ? (@pressed ? "true" : "false") : nil,
17
+ "aria-pressed": if @group_type == "multiple"
18
+ @pressed ? "true" : "false"
19
+ end,
20
+ "aria-checked": if @group_type == "single"
21
+ @pressed ? "true" : "false"
22
+ end,
19
23
  data: {
20
24
  "ui--toggle-group-target": "item",
21
25
  action: "click->ui--toggle-group#toggle",
@@ -22,10 +22,20 @@ require "tailwind_merge"
22
22
  #
23
23
  module UI::TooltipBehavior
24
24
  # Returns HTML attributes for the tooltip root element
25
+ # When as_child is true, don't include the "contents" class since
26
+ # the parent element will handle its own styling
25
27
  def tooltip_html_attributes
26
- {
28
+ attrs = {
27
29
  data: tooltip_data_attributes
28
- }.compact
30
+ }
31
+
32
+ # Only add "contents" class when not using asChild
33
+ # asChild mode passes data attributes to parent, which handles its own styling
34
+ unless instance_variable_defined?(:@as_child) && @as_child
35
+ attrs[:class] = "contents"
36
+ end
37
+
38
+ attrs.compact
29
39
  end
30
40
 
31
41
  # Returns data attributes for the tooltip controller
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UI
4
+ class Base < Phlex::HTML
5
+ # Register lucide_icon as an output helper (returns HTML)
6
+ register_output_helper :lucide_icon
7
+ end
8
+ end
@@ -138,6 +138,14 @@ class UI::Calendar < Phlex::HTML
138
138
  end
139
139
 
140
140
  def render_dropdowns
141
+ if use_native_select?
142
+ render_native_dropdowns
143
+ else
144
+ render_ui_select_dropdowns
145
+ end
146
+ end
147
+
148
+ def render_native_dropdowns
141
149
  div(class: "flex items-center gap-1") do
142
150
  select(
143
151
  data: {ui__calendar_target: "monthSelect", action: "change->ui--calendar#goToMonth"},
@@ -160,6 +168,49 @@ class UI::Calendar < Phlex::HTML
160
168
  end
161
169
  end
162
170
 
171
+ def render_ui_select_dropdowns
172
+ div(class: "flex items-center gap-1 px-8 flex-1") do
173
+ # Month select
174
+ render UI::Select.new(classes: "flex-1", value: (@month.month - 1).to_s) do
175
+ render UI::SelectTrigger.new(classes: "h-9 w-full gap-1 px-2 text-sm font-medium border-0 shadow-none")
176
+ render UI::SelectContent.new(classes: "min-w-[8rem]") do
177
+ Date::MONTHNAMES.compact.each_with_index do |name, i|
178
+ render UI::SelectItem.new(value: i.to_s) { name }
179
+ end
180
+ end
181
+ input(
182
+ type: "hidden",
183
+ value: (@month.month - 1).to_s,
184
+ data: {
185
+ ui__select_target: "hiddenInput",
186
+ ui__calendar_target: "monthSelect",
187
+ action: "change->ui--calendar#goToMonth"
188
+ }
189
+ )
190
+ end
191
+
192
+ # Year select
193
+ render UI::Select.new(value: @month.year.to_s) do
194
+ render UI::SelectTrigger.new(classes: "min-w-[75px] h-9 w-auto gap-1 px-2 text-sm font-medium border-0 shadow-none")
195
+ render UI::SelectContent.new(classes: "min-w-[5rem] max-h-[200px]") do
196
+ current_year = Date.today.year
197
+ ((current_year - @year_range)..(current_year + 10)).each do |y|
198
+ render UI::SelectItem.new(value: y.to_s) { y.to_s }
199
+ end
200
+ end
201
+ input(
202
+ type: "hidden",
203
+ value: @month.year.to_s,
204
+ data: {
205
+ ui__select_target: "hiddenInput",
206
+ ui__calendar_target: "yearSelect",
207
+ action: "change->ui--calendar#goToYear"
208
+ }
209
+ )
210
+ end
211
+ end
212
+ end
213
+
163
214
  def render_table
164
215
  table(class: "w-full border-collapse space-y-1", role: "grid") do
165
216
  thead { render_weekdays }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class UI::CarouselNext < Phlex::HTML
3
+ class UI::CarouselNext < UI::Base
4
4
  def initialize(classes: nil, **attributes)
5
5
  @classes = classes
6
6
  @attributes = attributes
@@ -10,7 +10,7 @@ class UI::CarouselNext < Phlex::HTML
10
10
  extend UI::CarouselNextBehavior
11
11
 
12
12
  render UI::Button.new(**carousel_next_html_attributes, classes: carousel_next_classes, variant: :outline, size: :icon) do
13
- raw(safe(helpers.lucide_icon("arrow-right", class: "size-4")))
13
+ lucide_icon("arrow-right", class: "size-4")
14
14
  span(class: "sr-only") { "Next slide" }
15
15
  end
16
16
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class UI::CarouselPrevious < Phlex::HTML
3
+ class UI::CarouselPrevious < UI::Base
4
4
  def initialize(classes: nil, **attributes)
5
5
  @classes = classes
6
6
  @attributes = attributes
@@ -10,7 +10,7 @@ class UI::CarouselPrevious < Phlex::HTML
10
10
  extend UI::CarouselPreviousBehavior
11
11
 
12
12
  render UI::Button.new(**carousel_previous_html_attributes, classes: carousel_previous_classes, variant: :outline, size: :icon) do
13
- raw(safe(helpers.lucide_icon("arrow-left", class: "size-4")))
13
+ lucide_icon("arrow-left", class: "size-4")
14
14
  span(class: "sr-only") { "Previous slide" }
15
15
  end
16
16
  end
@@ -116,7 +116,7 @@ class UI::DatePicker < Phlex::HTML
116
116
 
117
117
  # Render the trigger button
118
118
  def trigger(placeholder: @placeholder, selected: @selected, icon: :chevron, classes: "", **attributes, &block)
119
- render Trigger.new(
119
+ render UI::DatePickerTrigger.new(
120
120
  placeholder: placeholder,
121
121
  selected: selected,
122
122
  icon: icon,
@@ -7,7 +7,7 @@
7
7
  # render UI::Trigger.new(placeholder: "Pick a date")
8
8
  #
9
9
  class UI::DatePickerTrigger < Phlex::HTML
10
- include DatePickerTriggerBehavior
10
+ include UI::DatePickerTriggerBehavior
11
11
 
12
12
  # @param placeholder [String] Placeholder text when no date selected
13
13
  # @param selected [Date, Range, Array] Currently selected date(s) for display
@@ -31,8 +31,9 @@ class UI::DropdownMenuTrigger < Phlex::HTML
31
31
  trigger_attrs = dropdown_menu_trigger_html_attributes.deep_merge(@attributes)
32
32
 
33
33
  if @as_child
34
- # Yield attributes to block - child receives trigger behavior
35
- yield(trigger_attrs) if block_given?
34
+ # When as_child is true, only pass functional attributes (data, aria, tabindex, role)
35
+ # The child component handles its own styling (following shadcn pattern)
36
+ yield(trigger_attrs.except(:class)) if block_given?
36
37
  else
37
38
  # Default: render wrapper div with trigger behavior
38
39
  div(**trigger_attrs) do
@@ -28,11 +28,18 @@ class UI::InputGroupButton < Phlex::HTML
28
28
  end
29
29
 
30
30
  def view_template(&block)
31
+ # Merge classes from @attributes[:class] with input_group_button_classes
32
+ # This is needed when as_child patterns pass classes through attributes
33
+ merged_classes = TailwindMerge::Merger.new.merge([
34
+ input_group_button_classes,
35
+ @attributes[:class]
36
+ ].compact.join(" "))
37
+
31
38
  # Use input_group_button_classes which includes button behavior + input group styling
32
39
  button(
33
40
  type: @type,
34
- class: input_group_button_classes,
35
- **@attributes.except(:type, :variant, :size)
41
+ class: merged_classes,
42
+ **@attributes.except(:type, :variant, :size, :class)
36
43
  ) do
37
44
  yield if block_given?
38
45
  end
@@ -14,7 +14,7 @@
14
14
  # end
15
15
  # end
16
16
  class UI::NavigationMenuContent < Phlex::HTML
17
- include UI::ContentBehavior
17
+ include UI::NavigationMenuContentBehavior
18
18
 
19
19
  # @param viewport [Boolean] Whether content should be rendered in viewport (inherited from parent)
20
20
  # @param classes [String] Additional CSS classes to merge
@@ -17,7 +17,7 @@
17
17
  # render UI::Link.new(href: "/about") { "About" }
18
18
  # end
19
19
  class UI::NavigationMenuItem < Phlex::HTML
20
- include UI::ItemBehavior
20
+ include UI::NavigationMenuItemBehavior
21
21
 
22
22
  # @param value [String] Optional value for controlled state
23
23
  # @param classes [String] Additional CSS classes to merge
@@ -20,7 +20,7 @@
20
20
  # link_to "About", about_path, **link_attrs
21
21
  # end
22
22
  class UI::NavigationMenuLink < Phlex::HTML
23
- include UI::LinkBehavior
23
+ include UI::NavigationMenuLinkBehavior
24
24
  include UI::SharedAsChildBehavior
25
25
 
26
26
  # @param href [String] URL for the link (ignored when as_child: true)