nitro_kit 0.2.0 → 0.3.0

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +6 -4
  4. data/app/components/nitro_kit/accordion.rb +68 -32
  5. data/app/components/nitro_kit/alert.rb +69 -0
  6. data/app/components/nitro_kit/avatar.rb +52 -0
  7. data/app/components/nitro_kit/badge.rb +46 -19
  8. data/app/components/nitro_kit/button.rb +99 -66
  9. data/app/components/nitro_kit/button_group.rb +18 -13
  10. data/app/components/nitro_kit/card.rb +49 -9
  11. data/app/components/nitro_kit/checkbox.rb +59 -41
  12. data/app/components/nitro_kit/checkbox_group.rb +38 -0
  13. data/app/components/nitro_kit/combobox.rb +138 -0
  14. data/app/components/nitro_kit/component.rb +45 -14
  15. data/app/components/nitro_kit/datepicker.rb +9 -0
  16. data/app/components/nitro_kit/dialog.rb +95 -0
  17. data/app/components/nitro_kit/dropdown.rb +112 -70
  18. data/app/components/nitro_kit/field.rb +221 -56
  19. data/app/components/nitro_kit/field_group.rb +12 -6
  20. data/app/components/nitro_kit/fieldset.rb +42 -7
  21. data/app/components/nitro_kit/form_builder.rb +45 -22
  22. data/app/components/nitro_kit/icon.rb +29 -8
  23. data/app/components/nitro_kit/input.rb +20 -10
  24. data/app/components/nitro_kit/label.rb +18 -5
  25. data/app/components/nitro_kit/pagination.rb +98 -0
  26. data/app/components/nitro_kit/radio_button.rb +28 -27
  27. data/app/components/nitro_kit/radio_button_group.rb +53 -0
  28. data/app/components/nitro_kit/select.rb +72 -0
  29. data/app/components/nitro_kit/switch.rb +49 -39
  30. data/app/components/nitro_kit/table.rb +56 -0
  31. data/app/components/nitro_kit/tabs.rb +98 -0
  32. data/app/components/nitro_kit/textarea.rb +26 -0
  33. data/app/components/nitro_kit/toast.rb +104 -0
  34. data/app/components/nitro_kit/tooltip.rb +53 -0
  35. data/app/helpers/nitro_kit/accordion_helper.rb +2 -0
  36. data/app/helpers/nitro_kit/alert_helper.rb +11 -0
  37. data/app/helpers/nitro_kit/avatar_helper.rb +9 -0
  38. data/app/helpers/nitro_kit/badge_helper.rb +3 -5
  39. data/app/helpers/nitro_kit/button_group_helper.rb +2 -0
  40. data/app/helpers/nitro_kit/button_helper.rb +37 -28
  41. data/app/helpers/nitro_kit/card_helper.rb +2 -0
  42. data/app/helpers/nitro_kit/checkbox_helper.rb +19 -16
  43. data/app/helpers/nitro_kit/combobox_helper.rb +9 -0
  44. data/app/helpers/nitro_kit/datepicker_helper.rb +9 -0
  45. data/app/helpers/nitro_kit/dialog_helper.rb +9 -0
  46. data/app/helpers/nitro_kit/dropdown_helper.rb +3 -1
  47. data/app/helpers/nitro_kit/field_group_helper.rb +9 -0
  48. data/app/helpers/nitro_kit/field_helper.rb +4 -2
  49. data/app/helpers/nitro_kit/fieldset_helper.rb +9 -0
  50. data/app/helpers/nitro_kit/form_helper.rb +13 -0
  51. data/app/helpers/nitro_kit/icon_helper.rb +3 -1
  52. data/app/helpers/nitro_kit/input_helper.rb +35 -0
  53. data/app/helpers/nitro_kit/label_helper.rb +12 -9
  54. data/app/helpers/nitro_kit/pagination_helper.rb +42 -0
  55. data/app/helpers/nitro_kit/radio_button_helper.rb +15 -12
  56. data/app/helpers/nitro_kit/select_helper.rb +24 -0
  57. data/app/helpers/nitro_kit/switch_helper.rb +4 -10
  58. data/app/helpers/nitro_kit/table_helper.rb +9 -0
  59. data/app/helpers/nitro_kit/tabs_helper.rb +9 -0
  60. data/app/helpers/nitro_kit/textarea_helper.rb +9 -0
  61. data/app/helpers/nitro_kit/toast_helper.rb +36 -0
  62. data/app/helpers/nitro_kit/tooltip_helper.rb +9 -0
  63. data/lib/generators/nitro_kit/add_generator.rb +38 -41
  64. data/lib/generators/nitro_kit/install_generator.rb +2 -1
  65. data/lib/nitro_kit/engine.rb +4 -0
  66. data/lib/nitro_kit/schema_builder.rb +90 -16
  67. data/lib/nitro_kit/version.rb +1 -1
  68. data/lib/nitro_kit.rb +39 -1
  69. data/lib/tasks/nitro_kit_tasks.rake +4 -0
  70. metadata +37 -10
  71. data/app/components/nitro_kit/radio_group.rb +0 -35
  72. data/app/helpers/application_helper.rb +0 -109
  73. data/lib/nitro_kit/railtie.rb +0 -8
@@ -1,37 +1,33 @@
1
1
  module NitroKit
2
2
  class Checkbox < Component
3
- include ActionView::Helpers::FormTagHelper
3
+ def initialize(label: nil, id: nil, **attrs)
4
+ @id = id || "nk--" + SecureRandom.hex(4)
5
+ @label = label
4
6
 
5
- def initialize(name, value: "1", label: nil, **attrs)
6
- super(**attrs)
7
-
8
- @name = name
9
- @label_text = label
7
+ super(
8
+ attrs,
9
+ id: @id,
10
+ type: "checkbox",
11
+ class: input_class
12
+ )
10
13
  end
11
14
 
12
- attr_reader(
13
- :name,
14
- :value,
15
- :label_text
16
- )
15
+ alias :html_label :label
16
+
17
+ attr_reader :label, :id
17
18
 
18
19
  def view_template
19
- div(class: merge(["isolate inline-flex items-center gap-2", attrs[:class]])) do
20
- label(class: "relative flex shrink-0") do
21
- input(
22
- **attrs,
23
- type: "checkbox",
24
- class: class_names(
25
- "peer appearance-none shadow-sm size-4 rounded-sm border text-foreground",
26
- "checked:bg-primary checked:border-primary",
27
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
28
- )
29
- )
20
+ div(class: wrapper_class) do
21
+ html_label(
22
+ class: "inline-grid *:[grid-area:1/1] shrink-0 place-items-center group/checkbox"
23
+ ) do
24
+ input(**attrs)
30
25
  checkmark
26
+ dash
31
27
  end
32
28
 
33
- if label_text.present?
34
- render(Label.new(for: attrs[:id])) { label_text }
29
+ if label.present? || block_given?
30
+ render(Label.new(for: id)) { label || yield }
35
31
  end
36
32
  end
37
33
  end
@@ -39,24 +35,46 @@ module NitroKit
39
35
  private
40
36
 
41
37
  def checkmark
42
- span(
43
- class: class_names(
44
- "absolute w-full h-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
45
- "text-zinc-50 dark:text-zinc-950 opacity-0 peer-checked:opacity-100 pointer-events-none"
46
- )
47
- ) do
48
- svg(
49
- class: "size-full",
50
- viewbox: "0 0 20 20",
51
- fill: "currentColor",
52
- stroke: "currentColor",
53
- stroke_width: 1
54
- ) do |svg|
55
- svg.path(
56
- "d" => "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
57
- )
58
- end
38
+ svg(
39
+ class: merge_class(svg_class, "group-has-[:checked]/checkbox:visible"),
40
+ viewbox: "0 0 16 16",
41
+ fill: "none",
42
+ stroke: "currentColor",
43
+ stroke_linecap: "round",
44
+ stroke_linejoin: "round",
45
+ stroke_width: 3
46
+ ) do |svg|
47
+ svg.path(d: "M 3 8 L 6 12 L 12 5")
48
+ end
49
+ end
50
+
51
+ def dash
52
+ svg(
53
+ class: merge_class(svg_class, "group-has-[:indeterminate]/checkbox:visible"),
54
+ viewbox: "0 0 16 16",
55
+ fill: "none",
56
+ stroke: "currentColor",
57
+ stroke_linecap: "round",
58
+ stroke_width: 3
59
+ ) do |svg|
60
+ svg.line(x1: "3", y1: "8", x2: "12", y2: "8")
59
61
  end
60
62
  end
63
+
64
+ def input_class
65
+ [
66
+ "appearance-none shadow-sm size-4 rounded-sm border text-foreground",
67
+ "checked:bg-primary checked:border-primary indeterminate:bg-primary indeterminate:border-primary",
68
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ring-offset-2 ring-offset-background"
69
+ ]
70
+ end
71
+
72
+ def svg_class
73
+ "size-3 text-zinc-50 [&>svg]:size-full dark:text-zinc-950 pointer-events-none invisible"
74
+ end
75
+
76
+ def wrapper_class
77
+ "isolate inline-flex items-center gap-2"
78
+ end
61
79
  end
62
80
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NitroKit
4
+ class CheckboxGroup < Component
5
+ def initialize(options = nil, **attrs)
6
+ @options = options
7
+
8
+ super(
9
+ attrs,
10
+ class: "flex items-start flex-col gap-2"
11
+ )
12
+ end
13
+
14
+ attr_reader :options
15
+
16
+ def view_template
17
+ div(**attrs) do
18
+ if block_given?
19
+ yield
20
+ else
21
+ options.map { |option| item(*option) }
22
+ end
23
+ end
24
+ end
25
+
26
+ def title(text = nil, **attrs, &block)
27
+ render(Label.new(**attrs)) do
28
+ text_or_block(text, &block)
29
+ end
30
+ end
31
+
32
+ def item(text = nil, **attrs, &block)
33
+ render(Checkbox.new(**attrs)) do
34
+ text_or_block(text, &block)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NitroKit
4
+ class Combobox < Component
5
+ def initialize(
6
+ options: [],
7
+ id: nil,
8
+
9
+ placement: "bottom",
10
+ tab_inserts_suggestions: true,
11
+ first_option_selection_mode: "selected",
12
+ scroll_into_view_options: {block: "nearest", inline: "nearest"},
13
+
14
+ **attrs
15
+ )
16
+ # floating-ui options
17
+ @placement = placement
18
+
19
+ # combobox-nav options
20
+ @tab_inserts_suggestions = tab_inserts_suggestions
21
+ @first_option_selection_mode = first_option_selection_mode
22
+ @scroll_into_view_options = scroll_into_view_options
23
+
24
+ @id = id || "nk--combobox-" + SecureRandom.hex(4)
25
+
26
+ @options = options
27
+
28
+ super(
29
+ attrs,
30
+ type: "text",
31
+ class: input_class,
32
+ data: {
33
+ nk__combobox_target: "input",
34
+ action: %w[
35
+ focusin->nk--combobox#open
36
+ focusin@window->nk--combobox#focusShift
37
+ click@window->nk--combobox#windowClick
38
+ input->nk--combobox#input
39
+ keydown.esc->nk--combobox#clear
40
+ keydown.down->nk--combobox#open
41
+ ]
42
+ },
43
+ aria: {
44
+ controls: id(:listbox)
45
+ }
46
+ )
47
+ end
48
+
49
+ attr_reader(
50
+ :options,
51
+ :placement,
52
+ :tab_inserts_suggestions,
53
+ :first_option_selection_mode,
54
+ :scroll_into_view_options
55
+ )
56
+
57
+ def view_template
58
+ div(
59
+ data: {
60
+ class: "isolate",
61
+ slot: "control",
62
+ controller: "nk--combobox",
63
+ nk__combobox_placement_value: placement,
64
+ nk__combobox_tab_inserts_suggestions_value: tab_inserts_suggestions.to_s,
65
+ nk__combobox_first_option_selection_mode_value: first_option_selection_mode.to_s,
66
+ nk__combobox_scroll_into_view_options_value: scroll_into_view_options&.to_json
67
+ }
68
+ ) do
69
+ span(class: wrapper_class) do
70
+ render(Input.new(**attrs))
71
+ chevron_icon
72
+ end
73
+
74
+ # Since a combobox can function like a <select> element where the displayed
75
+ # value and the form value differ, include the value in a hidden field
76
+ input(
77
+ type: "hidden",
78
+ value: attrs[:value],
79
+ data: {nk__combobox_target: "hiddenField"}
80
+ )
81
+
82
+ ul(
83
+ role: "listbox",
84
+ id: id(:listbox),
85
+ class: list_class,
86
+ data: {nk__combobox_target: "list", state: "closed"}
87
+ ) do
88
+ options.each do |(key, value)|
89
+ li(
90
+ role: "option",
91
+ data: {value:},
92
+ class: merge_class(option_class)
93
+ ) { key }
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def id(suffix)
102
+ "#{@id}-#{suffix}"
103
+ end
104
+
105
+ def wrapper_class
106
+ "inline-grid *:[grid-area:1/1] group/combobox"
107
+ end
108
+
109
+ def input_class
110
+ "pr-8"
111
+ end
112
+
113
+ def list_class
114
+ [
115
+ "absolute top-0 left-0 p-1 bg-background rounded-md border shadow-sm w-fit max-w-sm flex-col flex z-10",
116
+ "max-h-60 overflow-y-auto",
117
+ "data-[state=closed]:hidden [&:not(:has([role=option]))]:hidden",
118
+ "[&_[aria-selected]]:bg-muted"
119
+ ]
120
+ end
121
+
122
+ def option_class
123
+ "hidden flex-none px-2 py-1 rounded font-medium truncate cursor-pointer hover:bg-muted [&[role=option]]:block"
124
+ end
125
+
126
+ def chevron_icon
127
+ svg(
128
+ class: "size-4 self-center place-self-end mr-2 pointer-events-none text-muted-foreground group-hover/combobox:text-foreground",
129
+ viewbox: "0 0 24 24",
130
+ fill: "none",
131
+ stroke: "currentColor",
132
+ stroke_width: 1
133
+ ) do |svg|
134
+ svg.path(d: "m6 9 6 6 6-6")
135
+ end
136
+ end
137
+ end
138
+ end
@@ -1,29 +1,60 @@
1
- module NitroKit
2
- Merger = TailwindMerge::Merger.new
1
+ # frozen_string_literal: true
3
2
 
3
+ module NitroKit
4
4
  class Component < Phlex::HTML
5
- attr_reader :attrs
6
-
7
- def initialize(**attrs)
8
- @attrs = attrs.symbolize_keys
5
+ def initialize(*hashes, **defaults)
6
+ @attrs = merge_attrs(*hashes, **defaults)
9
7
  end
10
8
 
11
9
  attr_reader :attrs
12
10
 
13
- def merge(*args)
14
- self.class.merge(*args)
11
+ private
12
+
13
+ # Merge attributes with some special cases for matching keys
14
+ def merge_attrs(*hashes, **defaults)
15
+ defaults.merge(*hashes) do |key, old_value, new_value|
16
+ case key
17
+ when :class
18
+ # Use TailwindMerge to merge class names
19
+ merge_class(old_value, new_value)
20
+ when :data
21
+ # Merge data hashes with some special cases for Stimulus
22
+ merge_data(old_value, new_value)
23
+ else
24
+ new_value
25
+ end
26
+ end
15
27
  end
16
28
 
17
- def self.merge(*args)
18
- Merger.merge(args)
29
+ alias :mattr :merge_attrs
30
+
31
+ def merge_class(*args)
32
+ @@merger ||= TailwindMerge::Merger.new
33
+ @@merger.merge(args)
19
34
  end
20
35
 
21
- def data_merge(*hashes)
22
- hashes.compact.reverse.reduce({}) do |acc, hash|
23
- acc.deep_merge(hash) do |_key, old_val, new_val|
24
- [old_val, new_val].compact.join(" ")
36
+ def merge_data(*hashes)
37
+ hashes.compact.reduce({}) do |acc, hash|
38
+ acc.deep_merge(hash) do |key, old_val, new_val|
39
+ # Concat Stimulus actions
40
+ case key
41
+ when :action, :controller
42
+ [new_val, old_val].compact.join(" ")
43
+ else
44
+ new_val
45
+ end
25
46
  end
26
47
  end
27
48
  end
49
+
50
+ def text_or_block(text = nil, &block)
51
+ if text
52
+ plain(text)
53
+ elsif block_given?
54
+ yield
55
+ else
56
+ nil
57
+ end
58
+ end
28
59
  end
29
60
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NitroKit
4
+ class Datepicker < Component
5
+ def view_template
6
+ render(Input.new(type: "text", **attrs, data: {controller: "nk--datepicker"}))
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NitroKit
4
+ class Dialog < Component
5
+ def initialize(identifier: nil, **attrs)
6
+ @identifier = identifier || SecureRandom.hex(6)
7
+
8
+ super(
9
+ attrs,
10
+ data: {controller: "nk--dialog", action: "click->nk--dialog#clickOutside"}
11
+ )
12
+ end
13
+
14
+ attr_reader :identifier
15
+
16
+ def view_template
17
+ div(**attrs) do
18
+ yield
19
+ end
20
+ end
21
+
22
+ def trigger(text = nil, **attrs, &block)
23
+ render(
24
+ NitroKit::Button.new(**mattr(attrs, data: {nk__dialog_target: "trigger", action: "click->nk--dialog#open"}))
25
+ ) do
26
+ text_or_block(text, &block)
27
+ end
28
+ end
29
+
30
+ alias :html_dialog :dialog
31
+
32
+ def dialog(**attrs)
33
+ html_dialog(
34
+ **mattr(
35
+ attrs,
36
+ class: dialog_class,
37
+ data: {nk__dialog_target: "dialog"},
38
+ aria: {
39
+ labelledby: id(:title),
40
+ describedby: id(:description)
41
+ }
42
+ )
43
+ ) do
44
+ yield
45
+ end
46
+ end
47
+
48
+ def close_button(**attrs)
49
+ render(
50
+ Button.new(
51
+ **mattr(
52
+ attrs,
53
+ variant: :ghost,
54
+ size: :sm,
55
+ class: "absolute top-2 right-2",
56
+ data: {action: "nk--dialog#close"}
57
+ )
58
+ )
59
+ ) do
60
+ render(Icon.new(:x))
61
+ end
62
+ end
63
+
64
+ def title(text = nil, **attrs, &block)
65
+ h2(**mattr(attrs, id: id(:title), class: "text-lg font-semibold mb-2")) do
66
+ text_or_block(text, &block)
67
+ end
68
+ end
69
+
70
+ def description(text = nil, **attrs, &block)
71
+ div(
72
+ **mattr(
73
+ attrs,
74
+ id: id(:description),
75
+ class: "text-muted-foreground mb-6 text-sm leading-relaxed"
76
+ )
77
+ ) do
78
+ text_or_block(text, &block)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def id(suffix = nil)
85
+ "nk-#{identifier}#{suffix ? "-#{suffix}" : ""}"
86
+ end
87
+
88
+ def dialog_class
89
+ [
90
+ "border rounded-xl max-w-lg w-full bg-background text-foreground shadow-lg m-auto p-6",
91
+ "dark:backdrop:bg-black/50"
92
+ ]
93
+ end
94
+ end
95
+ end
@@ -1,111 +1,153 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NitroKit
2
4
  class Dropdown < Component
3
- include Phlex::Rails::Helpers::LinkTo
4
-
5
- CONTENT = [
6
- "w-max-content absolute top-0 left-0",
7
- "p-1 bg-background rounded-md border shadow-sm",
8
- "w-fit max-w-sm flex-col text-left",
9
- "[&[aria-hidden=true]]:hidden flex"
10
- ].freeze
11
-
12
- TRIGGER = "inline-block"
13
-
14
- TITLE = "px-3 pt-2 pb-1.5 text-muted-foreground text-sm"
15
-
16
- ITEM = [
17
- "px-3 py-1.5 rounded",
18
- "font-medium truncate",
19
- "cursor-default"
20
- ].freeze
5
+ ITEM_VARIANTS = %i[default destructive]
21
6
 
22
- ITEM_VARIANTS = {
23
- default: ["hover:bg-muted"],
24
- destructive: ["text-destructive-foreground hover:bg-destructive hover:text-white"]
25
- }.freeze
26
-
27
- SEPARATOR = "border-t my-1 -mx-1"
7
+ include Phlex::Rails::Helpers::LinkTo
28
8
 
29
9
  def initialize(placement: nil, **attrs)
30
10
  @placement = placement
31
- @attrs = attrs
11
+
12
+ super(
13
+ attrs,
14
+ data: {
15
+ controller: "nk--dropdown",
16
+ nk__dropdown_placement_value: placement
17
+ }
18
+ )
32
19
  end
33
20
 
34
21
  attr_reader :placement
35
22
 
36
- def view_template(&block)
37
- div(
38
- data: data_merge(
39
- {:controller => "nk--dropdown", :"nk--dropdown-placement-value" => placement},
40
- attrs[:data]
41
- ),
42
- &block
43
- )
23
+ def view_template
24
+ div(**mattr(attrs)) do
25
+ yield
26
+ end
44
27
  end
45
28
 
46
- def trigger(**attrs, &block)
47
- div(
29
+ def trigger(text = nil, as: NitroKit::Button, **attrs, &block)
30
+ trigger_attrs = mattr(
31
+ attrs,
48
32
  aria: {haspopup: "true", expanded: "false"},
49
- **attrs,
50
- class: merge([TRIGGER, attrs[:class]]),
51
- data: data_merge(
52
- {:"nk--dropdown-target" => "trigger", :action => "click->nk--dropdown#toggle"},
53
- attrs[:data]
54
- ),
55
- &block
33
+ data: {nk__dropdown_target: "trigger", action: "click->nk--dropdown#toggle"}
56
34
  )
57
- end
58
35
 
59
- def content(**attrs, &block)
36
+ case as
37
+ when Symbol
38
+ send(as, **trigger_attrs) do
39
+ text_or_block(text, &block)
40
+ end
41
+ else
42
+ render(as.new(**trigger_attrs)) do
43
+ text_or_block(text, &block)
44
+ end
45
+ end
46
+ end
60
47
 
61
- class_list = div(
62
- role: "menu",
63
- aria: {hidden: "true"},
64
- **attrs,
65
- class: merge([CONTENT, attrs[:class]]),
66
- data: data_merge(
67
- {:"nk--dropdown-target" => "content"},
68
- attrs[:data]
69
- ),
70
- &block
71
- )
48
+ def content(as: :div, **attrs)
49
+ div(
50
+ **mattr(
51
+ attrs,
52
+ role: "menu",
53
+ aria: {hidden: "true"},
54
+ class: content_class,
55
+ data: {nk__dropdown_target: "content"},
56
+ popover: true
57
+ )
58
+ ) do
59
+ yield
60
+ end
72
61
  end
73
62
 
74
63
  def title(text = nil, **attrs, &block)
75
- class_list = merge([TITLE, attrs[:class]])
76
- div(**attrs, class: class_list) { text || block.call }
64
+ div(**mattr(attrs, class: title_class)) do
65
+ text_or_block(text, &block)
66
+ end
77
67
  end
78
68
 
79
- def item(
80
- text = nil,
81
- href = nil,
82
- variant: :default,
83
- **attrs
84
- )
85
- common_attrs = {
69
+ def item(text = nil, href: nil, variant: :default, **attrs, &block)
70
+ common_attrs = mattr(
71
+ attrs,
86
72
  role: "menuitem",
87
73
  tabindex: "-1",
88
- **attrs,
89
- class: merge([ITEM, ITEM_VARIANTS[variant], attrs[:class]])
90
- }
74
+ class: [item_class, item_variant_class(variant)]
75
+ )
91
76
 
92
77
  if href
93
78
  link_to(href, **common_attrs) do
94
- text || yield
79
+ text_or_block(text, &block)
95
80
  end
96
81
  else
97
82
  div(**common_attrs) do
98
- text || yield
83
+ text_or_block(text, &block)
99
84
  end
100
85
  end
101
86
  end
102
87
 
88
+ def item_to(
89
+ text_or_href,
90
+ href = nil,
91
+ **attrs,
92
+ &block
93
+ )
94
+ href = text_or_href if block_given?
95
+ item(text_or_href, href: href, **attrs, &block)
96
+ end
97
+
103
98
  def destructive_item(*args, **attrs, &block)
104
99
  item(*args, **attrs, variant: :destructive, &block)
105
100
  end
106
101
 
102
+ def destructive_item_to(text_or_block, href = nil, **attrs, &block)
103
+ href = args.shift if block_given?
104
+ destructive_item(text_or_block, href: href, **attrs, &block)
105
+ end
106
+
107
107
  def separator
108
- div(class: SEPARATOR)
108
+ hr(class: separator_class)
109
+ end
110
+
111
+ private
112
+
113
+ def content_class
114
+ [
115
+ "z-10 w-max-content absolute top-0 left-0",
116
+ "p-1 bg-background text-foreground rounded-md border shadow-sm",
117
+ "w-fit max-w-sm flex-col text-left",
118
+ "[&[aria-hidden=true]]:hidden flex"
119
+ ]
120
+ end
121
+
122
+ def trigger_class
123
+ ""
124
+ end
125
+
126
+ def title_class
127
+ "px-3 pt-2 pb-1.5 text-muted-foreground text-sm"
128
+ end
129
+
130
+ def item_class
131
+ [
132
+ "px-3 py-1.5 rounded",
133
+ "font-medium truncate",
134
+ "cursor-default"
135
+ ]
136
+ end
137
+
138
+ def item_variant_class(variant)
139
+ case variant
140
+ when :default
141
+ "[&[href]]:hover:bg-muted"
142
+ when :destructive
143
+ "text-destructive-foreground [&[href]]:hover:bg-destructive [&[href]]:hover:text-white"
144
+ else
145
+ raise ArgumentError, "Unknown variant: #{variant.inspect}"
146
+ end
147
+ end
148
+
149
+ def separator_class
150
+ "border-t my-1 -mx-1"
109
151
  end
110
152
  end
111
153
  end