nitro_kit 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 13 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: "13", 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 [&[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