phlexi-menu 0.0.3 → 0.2.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.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexi-menu (0.0.1)
4
+ phlexi-menu (0.2.0)
5
5
  phlex (~> 1.11)
6
6
  phlexi-field
7
7
  zeitwerk
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+
5
+ module Phlexi
6
+ module Menu
7
+ # A component for rendering badge elements in menus
8
+ #
9
+ # @example Basic usage
10
+ # Badge.new("New!", class: "badge-primary")
11
+ #
12
+ # @example With custom styling
13
+ # Badge.new("2", class: "badge-notification")
14
+ #
15
+ class Badge < COMPONENT_BASE
16
+ # Initialize a new badge component
17
+ #
18
+ # @param content [String] The text content to display in the badge
19
+ # @param options [Hash] Additional HTML attributes for the badge element
20
+ # @option options [String] :class CSS classes to apply to the badge
21
+ def initialize(content, **options)
22
+ @content = content
23
+ @options = options
24
+ super()
25
+ end
26
+
27
+ def view_template
28
+ span(class: @options[:class]) { @content }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -47,27 +47,6 @@ module Phlexi
47
47
  new_item
48
48
  end
49
49
 
50
- # Checks if the menu has any items.
51
- #
52
- # @return [Boolean] true if the menu has no items, false otherwise
53
- def empty?
54
- @items.empty?
55
- end
56
-
57
- # Returns the number of top-level items in the menu.
58
- #
59
- # @return [Integer] The count of top-level menu items
60
- def size
61
- @items.size
62
- end
63
-
64
- # Checks if this menu item has any nested items.
65
- #
66
- # @return [Boolean] true if the item has nested items, false otherwise
67
- def nested?
68
- !empty?
69
- end
70
-
71
50
  # Returns a string representation of the menu structure.
72
51
  #
73
52
  # @return [String] A human-readable representation of the menu
@@ -14,7 +14,7 @@ module Phlexi
14
14
  # def self.theme
15
15
  # super.merge({
16
16
  # nav: "bg-white shadow",
17
- # item_label: "text-gray-600"
17
+ # item_label: ->(depth) { "text-gray-#{600 + (depth * 100)}" }
18
18
  # })
19
19
  # end
20
20
  # end
@@ -23,6 +23,8 @@ module Phlexi
23
23
  # Theme class for customizing menu appearance
24
24
  class Theme < Phlexi::Menu::Theme; end
25
25
 
26
+ class Badge < Phlexi::Menu::Badge; end
27
+
26
28
  # @return [Integer] The default maximum nesting depth for menu items
27
29
  DEFAULT_MAX_DEPTH = 3
28
30
 
@@ -31,20 +33,18 @@ module Phlexi
31
33
  # @param menu [Phlexi::Menu::Builder] The menu structure to render
32
34
  # @param max_depth [Integer] Maximum nesting depth for menu items
33
35
  # @param options [Hash] Additional options passed to rendering methods
36
+ # @raise [ArgumentError] If menu is nil
34
37
  def initialize(menu, max_depth: default_max_depth, **options)
38
+ raise ArgumentError, "Menu cannot be nil" if menu.nil?
39
+
35
40
  @menu = menu
36
41
  @max_depth = max_depth
37
42
  @options = options
38
43
  super()
39
44
  end
40
45
 
41
- # Renders the menu structure as HTML.
42
- #
43
- # @return [String] The rendered HTML
44
46
  def view_template
45
- nav(class: themed(:nav)) do
46
- render_items(@menu.items)
47
- end
47
+ nav(class: themed(:nav)) { render_items(@menu.items) }
48
48
  end
49
49
 
50
50
  protected
@@ -53,14 +53,12 @@ module Phlexi
53
53
  #
54
54
  # @param items [Array<Phlexi::Menu::Item>] The items to render
55
55
  # @param depth [Integer] Current nesting depth
56
+ # @return [void]
56
57
  def render_items(items, depth = 0)
57
- return if depth >= @max_depth
58
- return if items.empty?
58
+ return if depth >= @max_depth || items.empty?
59
59
 
60
- ul(class: themed(:items_container)) do
61
- items.each do |item|
62
- render_item_wrapper(item, depth)
63
- end
60
+ ul(class: themed(:items_container, depth)) do
61
+ items.each { |item| render_item_wrapper(item, depth) }
64
62
  end
65
63
  end
66
64
 
@@ -68,116 +66,197 @@ module Phlexi
68
66
  #
69
67
  # @param item [Phlexi::Menu::Item] The item to wrap
70
68
  # @param depth [Integer] Current nesting depth
69
+ # @return [void]
71
70
  def render_item_wrapper(item, depth)
72
- li(class: tokens(
73
- themed(:item_wrapper),
74
- active_class(item),
75
- item_parent_class(item)
76
- )) do
77
- render_item_content(item)
78
- render_items(item.items, depth + 1) if item.items.any?
71
+ li(class: compute_item_wrapper_classes(item, depth)) do
72
+ render_item_content(item, depth)
73
+ render_nested_items(item, depth)
79
74
  end
80
75
  end
81
76
 
77
+ # Computes CSS classes for item wrapper
78
+ #
79
+ # @param item [Phlexi::Menu::Item] The menu item
80
+ # @param depth [Integer] Current nesting depth
81
+ # @return [String] Space-separated CSS classes
82
+ def compute_item_wrapper_classes(item, depth)
83
+ tokens(
84
+ themed(:item_wrapper, depth),
85
+ item_parent_class(item, depth),
86
+ active?(item) ? themed(:active, depth) : nil
87
+ )
88
+ end
89
+
90
+ # Renders nested items if present and within depth limit
91
+ #
92
+ # @param item [Phlexi::Menu::Item] The parent menu item
93
+ # @param depth [Integer] Current nesting depth
94
+ # @return [void]
95
+ def render_nested_items(item, depth)
96
+ render_items(item.items, depth + 1) if nested?(item, depth)
97
+ end
98
+
82
99
  # Renders the content of a menu item, choosing between link and span.
83
100
  #
84
101
  # @param item [Phlexi::Menu::Item] The item to render content for
85
- def render_item_content(item)
102
+ # @param depth [Integer] Current nesting depth
103
+ # @return [void]
104
+ def render_item_content(item, depth)
86
105
  if item.url
87
- render_item_link(item)
106
+ render_item_link(item, depth)
88
107
  else
89
- render_item_span(item)
108
+ render_item_span(item, depth)
90
109
  end
91
110
  end
92
111
 
93
112
  # Renders a menu item as a link.
94
113
  #
95
114
  # @param item [Phlexi::Menu::Item] The item to render as a link
96
- def render_item_link(item)
97
- a(href: item.url, class: themed(:item_link)) do
98
- render_item_interior(item)
115
+ # @param depth [Integer] Current nesting depth
116
+ # @return [void]
117
+ def render_item_link(item, depth)
118
+ a(
119
+ href: item.url,
120
+ class: tokens(themed(:item_link, depth), active_class(item, depth))
121
+ ) do
122
+ render_item_interior(item, depth)
99
123
  end
100
124
  end
101
125
 
102
126
  # Renders a menu item as a span (for non-linking items).
103
127
  #
104
128
  # @param item [Phlexi::Menu::Item] The item to render as a span
105
- def render_item_span(item)
106
- span(class: themed(:item_span)) do
107
- render_item_interior(item)
129
+ # @param depth [Integer] Current nesting depth
130
+ # @return [void]
131
+ def render_item_span(item, depth)
132
+ span(class: themed(:item_span, depth)) do
133
+ render_item_interior(item, depth)
108
134
  end
109
135
  end
110
136
 
111
137
  # Renders the interior content of a menu item (badges, icon, label).
112
138
  #
113
139
  # @param item [Phlexi::Menu::Item] The item to render interior content for
114
- def render_item_interior(item)
115
- render_leading_badge(item.leading_badge) if item.leading_badge
116
- render_icon(item.icon) if item.icon
117
- render_label(item.label)
118
- render_trailing_badge(item.trailing_badge) if item.trailing_badge
140
+ # @param depth [Integer] Current nesting depth
141
+ # @return [void]
142
+ def render_item_interior(item, depth)
143
+ render_leading_badge(item, depth) if item.leading_badge
144
+ render_icon(item.icon, depth) if item.icon
145
+ render_label(item.label, depth)
146
+ render_trailing_badge(item, depth) if item.trailing_badge
119
147
  end
120
148
 
121
149
  # Renders the item's label.
122
150
  #
123
151
  # @param label [String, Component] The label to render
124
- def render_label(label)
125
- phlexi_render(label) {
126
- span(class: themed(:item_label)) { label }
127
- }
152
+ # @param depth [Integer] Current nesting depth
153
+ # @return [void]
154
+ def render_label(label, depth)
155
+ phlexi_render(label) do
156
+ span(class: themed(:item_label, depth)) { label }
157
+ end
158
+ end
159
+
160
+ # Renders the leading badge if present
161
+ #
162
+ # @param item [Phlexi::Menu::Item] The menu item
163
+ # @param depth [Integer] Current nesting depth
164
+ # @return [void]
165
+ def render_leading_badge(item, depth)
166
+ return unless item.leading_badge
167
+
168
+ div(class: themed(:leading_badge_wrapper, depth)) do
169
+ render_badge(item.leading_badge, item.leading_badge_options, :leading_badge, depth)
170
+ end
128
171
  end
129
172
 
130
- # Renders the item's leading badge.
173
+ # Renders the trailing badge if present
131
174
  #
132
- # @param badge [String, Component] The leading badge to render
133
- def render_leading_badge(badge)
134
- phlexi_render(badge) {
135
- span(class: themed(:leading_badge)) { badge }
136
- }
175
+ # @param item [Phlexi::Menu::Item] The menu item
176
+ # @param depth [Integer] Current nesting depth
177
+ # @return [void]
178
+ def render_trailing_badge(item, depth)
179
+ return unless item.trailing_badge
180
+
181
+ div(class: themed(:trailing_badge_wrapper, depth)) do
182
+ render_badge(item.trailing_badge, item.trailing_badge_options, :trailing_badge, depth)
183
+ end
137
184
  end
138
185
 
139
- # Renders the item's trailing badge.
186
+ # Renders a badge with given options
140
187
  #
141
- # @param badge [String, Component] The trailing badge to render
142
- def render_trailing_badge(badge)
143
- phlexi_render(badge) {
144
- span(class: themed(:trailing_badge)) { badge }
145
- }
188
+ # @param badge [Object] The badge content
189
+ # @param options [Hash] Badge rendering options
190
+ # @param type [Symbol] Badge type (leading or trailing)
191
+ # @param depth [Integer] Current nesting depth
192
+ # @return [void]
193
+ def render_badge(badge, options, type, depth)
194
+ phlexi_render(badge) do
195
+ render self.class::Badge.new(badge, **options)
196
+ end
146
197
  end
147
198
 
148
199
  # Renders the item's icon.
149
200
  #
150
201
  # @param icon [Class] The icon component class to render
151
- def render_icon(icon)
202
+ # @param depth [Integer] Current nesting depth
203
+ # @return [void]
204
+ def render_icon(icon, depth)
152
205
  return unless icon
153
206
 
154
- div(class: themed(:icon_wrapper)) do
155
- render icon.new(class: themed(:icon))
207
+ div(class: themed(:icon_wrapper, depth)) do
208
+ render icon.new(class: themed(:icon, depth))
156
209
  end
157
210
  end
158
211
 
159
212
  # Determines the active state class for an item.
160
213
  #
161
214
  # @param item [Phlexi::Menu::Item] The item to check active state for
215
+ # @param depth [Integer] Current nesting depth
162
216
  # @return [String, nil] The active class name or nil
163
- def active_class(item)
164
- item.active?(self) ? themed(:active) : nil
217
+ def active_class(item, depth)
218
+ active?(item) ? themed(:active, depth) : nil
219
+ end
220
+
221
+ # Helper method to check if an item is active
222
+ #
223
+ # @param item [Phlexi::Menu::Item] The item to check
224
+ # @return [Boolean] Whether the item is active
225
+ def active?(item)
226
+ item.active?(self)
227
+ end
228
+
229
+ # Determines if an item should be treated as nested based on its contents
230
+ # and the current depth relative to the maximum allowed depth.
231
+ #
232
+ # @param item [Phlexi::Menu::Item] The item to check
233
+ # @param depth [Integer] Current nesting depth
234
+ # @return [Boolean] Whether the item should be treated as nested
235
+ def nested?(item, depth)
236
+ has_children = item.items.any?
237
+ within_depth = (depth + 1) < @max_depth
238
+ has_children && within_depth
165
239
  end
166
240
 
167
241
  # Determines the parent state class for an item.
168
242
  #
169
243
  # @param item [Phlexi::Menu::Item] The item to check parent state for
244
+ # @param depth [Integer] Current nesting depth
170
245
  # @return [String, nil] The parent class name or nil
171
- def item_parent_class(item)
172
- item.items.any? ? themed(:item_parent) : nil
246
+ def item_parent_class(item, depth)
247
+ nested?(item, depth) ? themed(:item_parent, depth) : nil
173
248
  end
174
249
 
175
250
  # Resolves a theme component to its CSS classes.
176
251
  #
177
252
  # @param component [Symbol] The theme component to resolve
253
+ # @param depth [Integer] Current nesting depth
178
254
  # @return [String, nil] The resolved CSS classes or nil
179
- def themed(component)
180
- self.class::Theme.instance.resolve_theme(component)
255
+ def themed(component, depth = 0)
256
+ theme = self.class::Theme.instance.resolve_theme(component)
257
+ return nil if theme.nil?
258
+ return theme unless theme.respond_to?(:call)
259
+ theme.call(depth)
181
260
  end
182
261
 
183
262
  # Renders either a component or simple value with fallback.
@@ -185,6 +264,7 @@ module Phlexi
185
264
  # @param arg [Object] The value to render
186
265
  # @yield The default rendering block
187
266
  # @raise [ArgumentError] If no block is provided
267
+ # @return [void]
188
268
  def phlexi_render(arg, &)
189
269
  return unless arg
190
270
  raise ArgumentError, "phlexi_render requires a default render block" unless block_given?
@@ -198,6 +278,7 @@ module Phlexi
198
278
  end
199
279
  end
200
280
 
281
+ # @return [Integer] The default maximum depth for the menu
201
282
  def default_max_depth = self.class::DEFAULT_MAX_DEPTH
202
283
  end
203
284
  end
@@ -20,6 +20,11 @@ module Phlexi
20
20
  # admin.item "Users", url: "/admin/users"
21
21
  # admin.item "Settings", url: "/admin/settings"
22
22
  # end
23
+ #
24
+ # @example Custom active state logic
25
+ # Item.new("Dashboard", url: "/dashboard", active: -> (context) {
26
+ # context.controller.controller_name == "dashboards"
27
+ # })
23
28
  class Item
24
29
  # @return [String] The display text for the menu item
25
30
  attr_reader :label
@@ -33,9 +38,15 @@ module Phlexi
33
38
  # @return [String, Component, nil] The badge displayed before the label
34
39
  attr_reader :leading_badge
35
40
 
41
+ # @return [Hash] Options for the leading badge
42
+ attr_reader :leading_badge_options
43
+
36
44
  # @return [String, Component, nil] The badge displayed after the label
37
45
  attr_reader :trailing_badge
38
46
 
47
+ # @return [Hash] Options for the trailing badge
48
+ attr_reader :trailing_badge_options
49
+
39
50
  # @return [Array<Item>] Collection of nested menu items
40
51
  attr_reader :items
41
52
 
@@ -55,14 +66,12 @@ module Phlexi
55
66
  # @raise [ArgumentError] If the label is nil or empty
56
67
  def initialize(label, url: nil, icon: nil, leading_badge: nil, trailing_badge: nil, **options, &)
57
68
  raise ArgumentError, "Label cannot be nil" unless label
58
-
59
69
  @label = label
60
70
  @url = url
61
71
  @icon = icon
62
- @leading_badge = leading_badge
63
- @trailing_badge = trailing_badge
64
- @options = options
65
72
  @items = []
73
+ @options = options
74
+ setup_badges(leading_badge, trailing_badge, options)
66
75
 
67
76
  yield self if block_given?
68
77
  end
@@ -70,16 +79,44 @@ module Phlexi
70
79
  # Creates and adds a nested menu item.
71
80
  #
72
81
  # @param label [String] The display text for the nested item
73
- # @param ** [Hash] Additional options passed to the Item constructor
82
+ # @param args [Hash] Additional options passed to the Item constructor
74
83
  # @yield [item] Optional block for adding further nested items
75
84
  # @yieldparam item [Item] The newly created nested item
76
85
  # @return [Item] The created nested item
77
- def item(label, **, &)
78
- new_item = self.class.new(label, **, &)
86
+ def item(label, **args, &)
87
+ new_item = self.class.new(label, **args, &)
79
88
  @items << new_item
80
89
  new_item
81
90
  end
82
91
 
92
+ # Add a leading badge to the menu item
93
+ #
94
+ # @param badge [String, Component] The badge content
95
+ # @param opts [Hash] Additional options for the badge
96
+ # @return [self] Returns self for method chaining
97
+ # @raise [ArgumentError] If badge is nil
98
+ def with_leading_badge(badge, **opts)
99
+ raise ArgumentError, "Badge cannot be nil" if badge.nil?
100
+
101
+ @leading_badge = badge
102
+ @leading_badge_options = opts.freeze
103
+ self
104
+ end
105
+
106
+ # Add a trailing badge to the menu item
107
+ #
108
+ # @param badge [String, Component] The badge content
109
+ # @param opts [Hash] Additional options for the badge
110
+ # @return [self] Returns self for method chaining
111
+ # @raise [ArgumentError] If badge is nil
112
+ def with_trailing_badge(badge, **opts)
113
+ raise ArgumentError, "Badge cannot be nil" if badge.nil?
114
+
115
+ @trailing_badge = badge
116
+ @trailing_badge_options = opts.freeze
117
+ self
118
+ end
119
+
83
120
  # Determines if this menu item should be shown as active.
84
121
  # Checks in the following order:
85
122
  # 1. Custom active logic if provided in options
@@ -89,44 +126,54 @@ module Phlexi
89
126
  # @param context [Object] The context object (typically a controller) for active state checking
90
127
  # @return [Boolean] true if the item should be shown as active, false otherwise
91
128
  def active?(context)
92
- # First check custom active logic if provided
93
- return @options[:active].call(context) if @options[:active].respond_to?(:call)
94
-
95
- # Then check if this item's URL matches current page
96
- if context.respond_to?(:helpers) && @url
97
- return true if context.helpers.current_page?(@url)
98
- end
129
+ check_custom_active_state(context) ||
130
+ check_current_page_match(context) ||
131
+ check_nested_items_active(context)
132
+ end
99
133
 
100
- # Finally check if any child items are active
101
- @items.any? { |item| item.active?(context) }
134
+ # Returns a string representation of the menu item.
135
+ #
136
+ # @return [String] A human-readable representation of the menu item
137
+ def inspect
138
+ "#<#{self.class} label=#{@label.inspect} url=#{@url.inspect} items=#{@items.inspect}>"
102
139
  end
103
140
 
104
- # Checks if the menu has any items.
141
+ private
142
+
143
+ # Sets up the badge attributes
105
144
  #
106
- # @return [Boolean] true if the menu has no items, false otherwise
107
- def empty?
108
- @items.empty?
145
+ # @param leading_badge [String, Component, nil] The leading badge
146
+ # @param trailing_badge [String, Component, nil] The trailing badge
147
+ # @param options [Hash] Options containing badge configurations
148
+ def setup_badges(leading_badge, trailing_badge, options)
149
+ @leading_badge = leading_badge
150
+ @leading_badge_options = (options.delete(:leading_badge_options) || {}).freeze
151
+ @trailing_badge = trailing_badge
152
+ @trailing_badge_options = (options.delete(:trailing_badge_options) || {}).freeze
109
153
  end
110
154
 
111
- # Returns the number of top-level items in the menu.
155
+ # Checks if there's custom active state logic
112
156
  #
113
- # @return [Integer] The count of top-level menu items
114
- def size
115
- @items.size
157
+ # @param context [Object] The context for active state checking
158
+ # @return [Boolean] Result of custom active check
159
+ def check_custom_active_state(context)
160
+ @options[:active].respond_to?(:call) && @options[:active].call(context)
116
161
  end
117
162
 
118
- # Checks if this menu item has any nested items.
163
+ # Checks if the current page matches the item's URL
119
164
  #
120
- # @return [Boolean] true if the item has nested items, false otherwise
121
- def nested?
122
- !empty?
165
+ # @param context [Object] The context for URL matching
166
+ # @return [Boolean] Whether the current page matches
167
+ def check_current_page_match(context)
168
+ context.respond_to?(:helpers) && @url && context.helpers.current_page?(@url)
123
169
  end
124
170
 
125
- # Returns a string representation of the menu item.
171
+ # Checks if any nested items are active
126
172
  #
127
- # @return [String] A human-readable representation of the menu item
128
- def inspect
129
- "#<#{self.class} label=#{@label} url=#{@url} items=#{@items.map(&:inspect)}>"
173
+ # @param context [Object] The context for checking nested items
174
+ # @return [Boolean] Whether any nested items are active
175
+ def check_nested_items_active(context)
176
+ @items.any? { |item| item.active?(context) }
130
177
  end
131
178
  end
132
179
  end
@@ -3,19 +3,36 @@ require "phlexi-field"
3
3
  module Phlexi
4
4
  module Menu
5
5
  class Theme < Phlexi::Field::Theme
6
+ # Defines the default theme structure with nil values
7
+ # Can be overridden in subclasses to provide custom styling
8
+ #
9
+ # @return [Hash] Default theme structure with nil values
6
10
  def self.theme
7
11
  @theme ||= {
8
- nav: nil,
9
- items_container: nil,
10
- item_wrapper: nil,
11
- item_parent: nil,
12
- item_link: nil,
13
- item_span: nil,
14
- item_label: nil,
15
- leading_badge: nil,
16
- trailing_badge: nil,
17
- icon: nil,
18
- active: nil
12
+ # Container elements
13
+ nav: nil, # Navigation wrapper
14
+ items_container: nil, # <ul> list container
15
+
16
+ # Item structure elements
17
+ item_wrapper: nil, # <li> item wrapper
18
+ item_parent: nil, # Additional class for items with visible children
19
+ item_link: nil, # <a> for clickable items
20
+ item_span: nil, # <span> for non-clickable items
21
+ item_label: nil, # Label text wrapper
22
+
23
+ # Interactive states
24
+ active: nil, # Active/selected state
25
+ hover: nil, # Hover state
26
+
27
+ # Badge elements
28
+ leading_badge_wrapper: nil, # Wrapper for leading badge
29
+ trailing_badge_wrapper: nil, # Wrapper for trailing badge
30
+ leading_badge: nil, # Badge before label
31
+ trailing_badge: nil, # Badge after label
32
+
33
+ # Icon elements
34
+ icon: nil, # Icon styling
35
+ icon_wrapper: nil # Icon container
19
36
  }.freeze
20
37
  end
21
38
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlexi
4
4
  module Menu
5
- VERSION = "0.0.3"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlexi-menu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-11 00:00:00.000000000 Z
11
+ date: 2024-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex
@@ -180,6 +180,7 @@ files:
180
180
  - LICENSE.txt
181
181
  - README.md
182
182
  - Rakefile
183
+ - changes.patch
183
184
  - config.ru
184
185
  - export.json
185
186
  - export.rb
@@ -189,6 +190,7 @@ files:
189
190
  - gemfiles/rails_7.gemfile.lock
190
191
  - lib/phlexi-menu.rb
191
192
  - lib/phlexi/menu.rb
193
+ - lib/phlexi/menu/badge.rb
192
194
  - lib/phlexi/menu/builder.rb
193
195
  - lib/phlexi/menu/component.rb
194
196
  - lib/phlexi/menu/item.rb