phlexi-menu 0.1.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.
data/changes.patch ADDED
@@ -0,0 +1,393 @@
1
+ diff --git a/lib/phlexi/menu/component.rb b/lib/phlexi/menu/component.rb
2
+ index 7f35b74..b9ecc5e 100644
3
+ --- a/lib/phlexi/menu/component.rb
4
+ +++ b/lib/phlexi/menu/component.rb
5
+ @@ -31,7 +31,10 @@ module Phlexi
6
+ # @param menu [Phlexi::Menu::Builder] The menu structure to render
7
+ # @param max_depth [Integer] Maximum nesting depth for menu items
8
+ # @param options [Hash] Additional options passed to rendering methods
9
+ + # @raise [ArgumentError] If menu is nil
10
+ def initialize(menu, max_depth: default_max_depth, **options)
11
+ + raise ArgumentError, "Menu cannot be nil" if menu.nil?
12
+ +
13
+ @menu = menu
14
+ @max_depth = max_depth
15
+ @options = options
16
+ @@ -39,9 +42,7 @@ module Phlexi
17
+ end
18
+
19
+ def view_template
20
+ - nav(class: themed(:nav)) do
21
+ - render_items(@menu.items)
22
+ - end
23
+ + nav(class: themed(:nav)) { render_items(@menu.items) }
24
+ end
25
+
26
+ protected
27
+ @@ -50,14 +51,12 @@ module Phlexi
28
+ #
29
+ # @param items [Array<Phlexi::Menu::Item>] The items to render
30
+ # @param depth [Integer] Current nesting depth
31
+ + # @return [void]
32
+ def render_items(items, depth = 0)
33
+ - return if depth >= @max_depth
34
+ - return if items.empty?
35
+ + return if depth >= @max_depth || items.empty?
36
+
37
+ ul(class: themed(:items_container, depth)) do
38
+ - items.each do |item|
39
+ - render_item_wrapper(item, depth)
40
+ - end
41
+ + items.each { |item| render_item_wrapper(item, depth) }
42
+ end
43
+ end
44
+
45
+ @@ -65,21 +64,41 @@ module Phlexi
46
+ #
47
+ # @param item [Phlexi::Menu::Item] The item to wrap
48
+ # @param depth [Integer] Current nesting depth
49
+ + # @return [void]
50
+ def render_item_wrapper(item, depth)
51
+ - li(class: tokens(
52
+ + li(class: compute_item_wrapper_classes(item, depth)) do
53
+ + render_item_content(item, depth)
54
+ + render_nested_items(item, depth)
55
+ + end
56
+ + end
57
+ +
58
+ + # Computes CSS classes for item wrapper
59
+ + #
60
+ + # @param item [Phlexi::Menu::Item] The menu item
61
+ + # @param depth [Integer] Current nesting depth
62
+ + # @return [String] Space-separated CSS classes
63
+ + def compute_item_wrapper_classes(item, depth)
64
+ + tokens(
65
+ themed(:item_wrapper, depth),
66
+ item_parent_class(item, depth),
67
+ active?(item) ? themed(:active, depth) : nil
68
+ - )) do
69
+ - render_item_content(item, depth)
70
+ - render_items(item.items, depth + 1) if nested?(item, depth)
71
+ - end
72
+ + )
73
+ + end
74
+ +
75
+ + # Renders nested items if present and within depth limit
76
+ + #
77
+ + # @param item [Phlexi::Menu::Item] The parent menu item
78
+ + # @param depth [Integer] Current nesting depth
79
+ + # @return [void]
80
+ + def render_nested_items(item, depth)
81
+ + render_items(item.items, depth + 1) if nested?(item, depth)
82
+ end
83
+
84
+ # Renders the content of a menu item, choosing between link and span.
85
+ #
86
+ # @param item [Phlexi::Menu::Item] The item to render content for
87
+ # @param depth [Integer] Current nesting depth
88
+ + # @return [void]
89
+ def render_item_content(item, depth)
90
+ if item.url
91
+ render_item_link(item, depth)
92
+ @@ -92,13 +111,11 @@ module Phlexi
93
+ #
94
+ # @param item [Phlexi::Menu::Item] The item to render as a link
95
+ # @param depth [Integer] Current nesting depth
96
+ + # @return [void]
97
+ def render_item_link(item, depth)
98
+ a(
99
+ href: item.url,
100
+ - class: tokens(
101
+ - themed(:item_link, depth),
102
+ - active_class(item, depth)
103
+ - )
104
+ + class: tokens(themed(:item_link, depth), active_class(item, depth))
105
+ ) do
106
+ render_item_interior(item, depth)
107
+ end
108
+ @@ -108,6 +125,7 @@ module Phlexi
109
+ #
110
+ # @param item [Phlexi::Menu::Item] The item to render as a span
111
+ # @param depth [Integer] Current nesting depth
112
+ + # @return [void]
113
+ def render_item_span(item, depth)
114
+ span(class: themed(:item_span, depth)) do
115
+ render_item_interior(item, depth)
116
+ @@ -118,47 +136,69 @@ module Phlexi
117
+ #
118
+ # @param item [Phlexi::Menu::Item] The item to render interior content for
119
+ # @param depth [Integer] Current nesting depth
120
+ + # @return [void]
121
+ def render_item_interior(item, depth)
122
+ - render_leading_badge(item.leading_badge, depth) if item.leading_badge
123
+ + render_leading_badge(item, depth) if item.leading_badge
124
+ render_icon(item.icon, depth) if item.icon
125
+ render_label(item.label, depth)
126
+ - render_trailing_badge(item.trailing_badge, depth) if item.trailing_badge
127
+ + render_trailing_badge(item, depth) if item.trailing_badge
128
+ end
129
+
130
+ # Renders the item's label.
131
+ #
132
+ # @param label [String, Component] The label to render
133
+ # @param depth [Integer] Current nesting depth
134
+ + # @return [void]
135
+ def render_label(label, depth)
136
+ - phlexi_render(label) {
137
+ + phlexi_render(label) do
138
+ span(class: themed(:item_label, depth)) { label }
139
+ - }
140
+ + end
141
+ + end
142
+ +
143
+ + # Renders the leading badge if present
144
+ + #
145
+ + # @param item [Phlexi::Menu::Item] The menu item
146
+ + # @param depth [Integer] Current nesting depth
147
+ + # @return [void]
148
+ + def render_leading_badge(item, depth)
149
+ + return unless item.leading_badge
150
+ +
151
+ + div(class: themed(:leading_badge_wrapper, depth)) do
152
+ + render_badge(item.leading_badge, item.leading_badge_options, :leading_badge, depth)
153
+ + end
154
+ end
155
+
156
+ - # Renders the item's leading badge.
157
+ + # Renders the trailing badge if present
158
+ #
159
+ - # @param badge [String, Component] The leading badge to render
160
+ + # @param item [Phlexi::Menu::Item] The menu item
161
+ # @param depth [Integer] Current nesting depth
162
+ - def render_leading_badge(badge, depth)
163
+ - phlexi_render(badge) {
164
+ - span(class: themed(:leading_badge, depth)) { badge }
165
+ - }
166
+ + # @return [void]
167
+ + def render_trailing_badge(item, depth)
168
+ + return unless item.trailing_badge
169
+ +
170
+ + div(class: themed(:trailing_badge_wrapper, depth)) do
171
+ + render_badge(item.trailing_badge, item.trailing_badge_options, :trailing_badge, depth)
172
+ + end
173
+ end
174
+
175
+ - # Renders the item's trailing badge.
176
+ + # Renders a badge with given options
177
+ #
178
+ - # @param badge [String, Component] The trailing badge to render
179
+ + # @param badge [Object] The badge content
180
+ + # @param options [Hash] Badge rendering options
181
+ + # @param type [Symbol] Badge type (leading or trailing)
182
+ # @param depth [Integer] Current nesting depth
183
+ - def render_trailing_badge(badge, depth)
184
+ - phlexi_render(badge) {
185
+ - span(class: themed(:trailing_badge, depth)) { badge }
186
+ - }
187
+ + # @return [void]
188
+ + def render_badge(badge, options, type, depth)
189
+ + phlexi_render(badge) do
190
+ + render BadgeComponent.new(badge, **options)
191
+ + end
192
+ end
193
+
194
+ # Renders the item's icon.
195
+ #
196
+ # @param icon [Class] The icon component class to render
197
+ # @param depth [Integer] Current nesting depth
198
+ + # @return [void]
199
+ def render_icon(icon, depth)
200
+ return unless icon
201
+
202
+ @@ -222,6 +262,7 @@ module Phlexi
203
+ # @param arg [Object] The value to render
204
+ # @yield The default rendering block
205
+ # @raise [ArgumentError] If no block is provided
206
+ + # @return [void]
207
+ def phlexi_render(arg, &)
208
+ return unless arg
209
+ raise ArgumentError, "phlexi_render requires a default render block" unless block_given?
210
+ @@ -235,6 +276,7 @@ module Phlexi
211
+ end
212
+ end
213
+
214
+ + # @return [Integer] The default maximum depth for the menu
215
+ def default_max_depth = self.class::DEFAULT_MAX_DEPTH
216
+ end
217
+ end
218
+ diff --git a/lib/phlexi/menu/item.rb b/lib/phlexi/menu/item.rb
219
+ index 4d128d6..b8b5614 100644
220
+ --- a/lib/phlexi/menu/item.rb
221
+ +++ b/lib/phlexi/menu/item.rb
222
+ @@ -20,6 +20,11 @@ module Phlexi
223
+ # admin.item "Users", url: "/admin/users"
224
+ # admin.item "Settings", url: "/admin/settings"
225
+ # end
226
+ + #
227
+ + # @example Custom active state logic
228
+ + # Item.new("Dashboard", url: "/dashboard", active: -> (context) {
229
+ + # context.controller.controller_name == "dashboards"
230
+ + # })
231
+ class Item
232
+ # @return [String] The display text for the menu item
233
+ attr_reader :label
234
+ @@ -33,9 +38,15 @@ module Phlexi
235
+ # @return [String, Component, nil] The badge displayed before the label
236
+ attr_reader :leading_badge
237
+
238
+ + # @return [Hash] Options for the leading badge
239
+ + attr_reader :leading_badge_options
240
+ +
241
+ # @return [String, Component, nil] The badge displayed after the label
242
+ attr_reader :trailing_badge
243
+
244
+ + # @return [Hash] Options for the trailing badge
245
+ + attr_reader :trailing_badge_options
246
+ +
247
+ # @return [Array<Item>] Collection of nested menu items
248
+ attr_reader :items
249
+
250
+ @@ -55,14 +66,12 @@ module Phlexi
251
+ # @raise [ArgumentError] If the label is nil or empty
252
+ def initialize(label, url: nil, icon: nil, leading_badge: nil, trailing_badge: nil, **options, &)
253
+ raise ArgumentError, "Label cannot be nil" unless label
254
+ -
255
+ @label = label
256
+ @url = url
257
+ @icon = icon
258
+ - @leading_badge = leading_badge
259
+ - @trailing_badge = trailing_badge
260
+ - @options = options
261
+ @items = []
262
+ + @options = options
263
+ + setup_badges(leading_badge, trailing_badge, options)
264
+
265
+ yield self if block_given?
266
+ end
267
+ @@ -70,16 +79,44 @@ module Phlexi
268
+ # Creates and adds a nested menu item.
269
+ #
270
+ # @param label [String] The display text for the nested item
271
+ - # @param ** [Hash] Additional options passed to the Item constructor
272
+ + # @param args [Hash] Additional options passed to the Item constructor
273
+ # @yield [item] Optional block for adding further nested items
274
+ # @yieldparam item [Item] The newly created nested item
275
+ # @return [Item] The created nested item
276
+ - def item(label, **, &)
277
+ - new_item = self.class.new(label, **, &)
278
+ + def item(label, **args, &)
279
+ + new_item = self.class.new(label, **args, &)
280
+ @items << new_item
281
+ new_item
282
+ end
283
+
284
+ + # Add a leading badge to the menu item
285
+ + #
286
+ + # @param badge [String, Component] The badge content
287
+ + # @param opts [Hash] Additional options for the badge
288
+ + # @return [self] Returns self for method chaining
289
+ + # @raise [ArgumentError] If badge is nil
290
+ + def with_leading_badge(badge, **opts)
291
+ + raise ArgumentError, "Badge cannot be nil" if badge.nil?
292
+ +
293
+ + @leading_badge = badge
294
+ + @leading_badge_options = opts
295
+ + self
296
+ + end
297
+ +
298
+ + # Add a trailing badge to the menu item
299
+ + #
300
+ + # @param badge [String, Component] The badge content
301
+ + # @param opts [Hash] Additional options for the badge
302
+ + # @return [self] Returns self for method chaining
303
+ + # @raise [ArgumentError] If badge is nil
304
+ + def with_trailing_badge(badge, **opts)
305
+ + raise ArgumentError, "Badge cannot be nil" if badge.nil?
306
+ +
307
+ + @trailing_badge = badge
308
+ + @trailing_badge_options = opts
309
+ + self
310
+ + end
311
+ +
312
+ # Determines if this menu item should be shown as active.
313
+ # Checks in the following order:
314
+ # 1. Custom active logic if provided in options
315
+ @@ -89,23 +126,54 @@ module Phlexi
316
+ # @param context [Object] The context object (typically a controller) for active state checking
317
+ # @return [Boolean] true if the item should be shown as active, false otherwise
318
+ def active?(context)
319
+ - # First check custom active logic if provided
320
+ - return @options[:active].call(context) if @options[:active].respond_to?(:call)
321
+ -
322
+ - # Then check if this item's URL matches current page
323
+ - if context.respond_to?(:helpers) && @url
324
+ - return true if context.helpers.current_page?(@url)
325
+ - end
326
+ -
327
+ - # Finally check if any child items are active
328
+ - @items.any? { |item| item.active?(context) }
329
+ + check_custom_active_state(context) ||
330
+ + check_current_page_match(context) ||
331
+ + check_nested_items_active(context)
332
+ end
333
+
334
+ # Returns a string representation of the menu item.
335
+ #
336
+ # @return [String] A human-readable representation of the menu item
337
+ def inspect
338
+ - "#<#{self.class} label=#{@label} url=#{@url} items=#{@items.map(&:inspect)}>"
339
+ + "#<#{self.class} label=#{@label.inspect} url=#{@url.inspect} items=#{@items.inspect}>"
340
+ + end
341
+ +
342
+ + private
343
+ +
344
+ + # Sets up the badge attributes
345
+ + #
346
+ + # @param leading_badge [String, Component, nil] The leading badge
347
+ + # @param trailing_badge [String, Component, nil] The trailing badge
348
+ + # @param options [Hash] Options containing badge configurations
349
+ + def setup_badges(leading_badge, trailing_badge, options)
350
+ + @leading_badge = leading_badge
351
+ + @leading_badge_options = options.delete(:leading_badge_options) || {}
352
+ + @trailing_badge = trailing_badge
353
+ + @trailing_badge_options = options.delete(:trailing_badge_options) || {}
354
+ + end
355
+ +
356
+ + # Checks if there's custom active state logic
357
+ + #
358
+ + # @param context [Object] The context for active state checking
359
+ + # @return [Boolean] Result of custom active check
360
+ + def check_custom_active_state(context)
361
+ + @options[:active].respond_to?(:call) && @options[:active].call(context)
362
+ + end
363
+ +
364
+ + # Checks if the current page matches the item's URL
365
+ + #
366
+ + # @param context [Object] The context for URL matching
367
+ + # @return [Boolean] Whether the current page matches
368
+ + def check_current_page_match(context)
369
+ + context.respond_to?(:helpers) && @url && context.helpers.current_page?(@url)
370
+ + end
371
+ +
372
+ + # Checks if any nested items are active
373
+ + #
374
+ + # @param context [Object] The context for checking nested items
375
+ + # @return [Boolean] Whether any nested items are active
376
+ + def check_nested_items_active(context)
377
+ + @items.any? { |item| item.active?(context) }
378
+ end
379
+ end
380
+ end
381
+ diff --git a/lib/phlexi/menu/theme.rb b/lib/phlexi/menu/theme.rb
382
+ index 076b137..415498b 100644
383
+ --- a/lib/phlexi/menu/theme.rb
384
+ +++ b/lib/phlexi/menu/theme.rb
385
+ @@ -25,6 +25,8 @@ module Phlexi
386
+ hover: nil, # Hover state
387
+
388
+ # Badge elements
389
+ + leading_badge_wrapper: nil, # Wrapper for leading badge
390
+ + trailing_badge_wrapper: nil, # Wrapper for trailing badge
391
+ leading_badge: nil, # Badge before label
392
+ trailing_badge: nil, # Badge after label
393
+
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexi-menu (0.1.0)
4
+ phlexi-menu (0.2.0)
5
5
  phlex (~> 1.11)
6
6
  phlexi-field
7
7
  zeitwerk
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexi-menu (0.1.0)
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