phlexi-menu 0.0.3 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7cc4d5883a4c76ae7455de7f38c33639dccee507b1406549a00e1f211906020e
4
- data.tar.gz: 23332831a113a1dc53515afe6449861cb8282b2f1ecd2f4dfde1eaf427cb0d52
3
+ metadata.gz: 9bb5cb841e6404ef962a4003718f3e2f9827dd165f417dd2a1ef9f2b5d0e07b2
4
+ data.tar.gz: 422533e0556c9e51f5c49eb5225ecb0d0bbb4c21af00b9ac56afcf4f723b7687
5
5
  SHA512:
6
- metadata.gz: ab3e7aad92d888a87599f33fcffa5e52c413e8b7bffaba4852e4d07e2fdd65e1b3dbf3f031542f95995a78fbac3fd29cf4609db7c332581af23249f7b07f84c2
7
- data.tar.gz: bd393a7fa7bdc00f2c20ee7227bbde090584d87f2c310cdf07d77c1cf4607871142860223f1ca267bfeedfb40bb724b3d88f04df89ef3f4eac6c80a62c59cf2a
6
+ metadata.gz: 2acde964c44d59ba554b024e83143d784214faf41d74cb823152a86c98dd88863d659910e3bb21f5cd2f9e2c4c7445dc2344c8260c9977d1af9cc50f4929246f
7
+ data.tar.gz: 9e651553370ad7a00ff40c0243b36729054208531eae757b945220423bc27075df0304e47f591f1c66b68a91265ed8567aa70b83653b3d69a48033fbe99392a3
data/README.md CHANGED
@@ -13,7 +13,10 @@ Phlexi::Menu is a flexible and powerful menu builder for Ruby applications. It p
13
13
  - [Basic Usage](#basic-usage)
14
14
  - [Menu Items](#menu-items)
15
15
  - [Component Options](#component-options)
16
+ - [Nesting and Depth Limits](#nesting-and-depth-limits)
16
17
  - [Theming](#theming)
18
+ - [Static Theming](#static-theming)
19
+ - [Depth-Aware Theming](#depth-aware-theming)
17
20
  - [Badge Components](#badge-components)
18
21
  - [Rails Integration](#rails-integration)
19
22
  - [Advanced Usage](#advanced-usage)
@@ -25,10 +28,11 @@ Phlexi::Menu is a flexible and powerful menu builder for Ruby applications. It p
25
28
 
26
29
  ## Features
27
30
 
28
- - Hierarchical menu structure with controlled nesting depth
31
+ - Hierarchical menu structure with intelligent depth control
29
32
  - Support for icons and dual-badge system (leading and trailing badges)
30
33
  - Intelligent active state detection
31
- - Flexible theming system
34
+ - Flexible theming system with depth awareness
35
+ - Smart nesting behavior based on depth limits
32
36
  - Works seamlessly with Phlex components
33
37
  - Rails-compatible URL handling
34
38
  - Customizable rendering components
@@ -64,14 +68,15 @@ class MainMenu < Phlexi::Menu::Component
64
68
  super.merge({
65
69
  nav: "bg-white shadow",
66
70
  items_container: "space-y-1",
67
- item_wrapper: "relative",
71
+ item_wrapper: ->(depth) { "relative pl-#{depth * 4}" },
68
72
  item_link: "flex items-center px-4 py-2 hover:bg-gray-50",
69
73
  item_span: "flex items-center px-4 py-2",
70
- item_label: "mx-3",
74
+ item_label: ->(depth) { "mx-3 text-gray-#{600 + (depth * 100)}" },
71
75
  leading_badge: "mr-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-600",
72
76
  trailing_badge: "ml-auto px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-600",
73
77
  icon: "h-5 w-5",
74
- active: "bg-blue-50 text-blue-600"
78
+ active: "bg-blue-50 text-blue-600",
79
+ item_parent: "has-children"
75
80
  })
76
81
  end
77
82
  end
@@ -90,7 +95,7 @@ menu = Phlexi::Menu::Builder.new do |m|
90
95
  users.item "All Users", url: "/users"
91
96
  users.item "Add User", url: "/users/new"
92
97
  end
93
-
98
+
94
99
  m.item "Settings",
95
100
  url: "/settings",
96
101
  icon: SettingsIcon,
@@ -128,8 +133,54 @@ MainMenu.new(
128
133
  )
129
134
  ```
130
135
 
136
+ ### Nesting and Depth Limits
137
+
138
+ Phlexi::Menu intelligently handles menu nesting based on the specified maximum depth:
139
+
140
+ ```ruby
141
+ # Create a deeply nested menu structure
142
+ menu = Phlexi::Menu::Builder.new do |m|
143
+ m.item "Level 0" do |l0| # Will be nested (depth 0)
144
+ l0.item "Level 1" do |l1| # Will be nested if max_depth > 2
145
+ l1.item "Level 2" # Will be nested if max_depth > 3
146
+ l1.item "Level 3" # Won't be nested if max_depth <= 3
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ # Render with depth limit
153
+ menu_component = MainMenu.new(menu, max_depth: 2)
154
+ ```
155
+
156
+ Key behaviors:
157
+ - Items are only treated as nested if their children can be rendered within the depth limit
158
+ - Parent styling classes (item_parent theme) are only applied to items whose children will be shown
159
+ - Nesting structure automatically adjusts based on the max_depth setting
160
+ - Depth-aware theme values receive the actual rendered depth of each item
161
+
162
+ Example with max_depth of 2:
163
+ ```ruby
164
+ menu = Phlexi::Menu::Builder.new do |m|
165
+ m.item "Products" do |products| # depth 0, gets parent styling
166
+ products.item "Categories" do |cats| # depth 1, gets parent styling
167
+ cats.item "Electronics" # depth 2, no parent styling
168
+ cats.item "Books" do |books| # depth 2, no parent styling
169
+ books.item "Fiction" # not rendered (depth 3)
170
+ end
171
+ end
172
+ end
173
+ end
174
+ ```
175
+
131
176
  ### Theming
132
177
 
178
+ Phlexi::Menu provides two approaches to theming: static and depth-aware.
179
+
180
+ #### Static Theming
181
+
182
+ Basic theme configuration with fixed classes:
183
+
133
184
  ```ruby
134
185
  class CustomMenu < Phlexi::Menu::Component
135
186
  class Theme < Theme
@@ -151,6 +202,70 @@ class CustomMenu < Phlexi::Menu::Component
151
202
  end
152
203
  ```
153
204
 
205
+ #### Depth-Aware Theming
206
+
207
+ Advanced theme configuration with depth-sensitive classes:
208
+
209
+ ```ruby
210
+ class DepthAwareMenu < Phlexi::Menu::Component
211
+ class Theme < Theme
212
+ def self.theme
213
+ super.merge({
214
+ # Static classes
215
+ nav: "bg-white shadow",
216
+
217
+ # Progressive indentation
218
+ item_wrapper: ->(depth) { "relative pl-#{depth * 4}" },
219
+
220
+ # Gradually fading text
221
+ item_label: ->(depth) { "mx-3 text-gray-#{600 + (depth * 100)}" },
222
+
223
+ # Different icon styles per level
224
+ icon: ->(depth) {
225
+ base = "h-5 w-5"
226
+ color = depth.zero? ? "text-primary" : "text-gray-400"
227
+ [base, color]
228
+ },
229
+
230
+ # Smaller text at deeper levels
231
+ item_link: ->(depth) {
232
+ size = depth.zero? ? "text-base" : "text-sm"
233
+ ["flex items-center px-4 py-2 hover:bg-gray-50", size]
234
+ }
235
+ })
236
+ end
237
+ end
238
+ end
239
+ ```
240
+
241
+ Theme values can be either:
242
+ - Static strings for consistent styling
243
+ - Arrays of classes that will be joined
244
+ - Callables (procs/lambdas) that receive the current depth and return strings or arrays
245
+
246
+ ### Advanced Usage
247
+
248
+ #### Component Customization
249
+
250
+ You can customize the nesting behavior by overriding the nested? method:
251
+
252
+ ```ruby
253
+ class CustomMenu < Phlexi::Menu::Component
254
+ protected
255
+
256
+ def nested?(item, depth)
257
+ # Custom logic for when to treat items as nested
258
+ return false if depth >= @max_depth - 1 # Reserve last level
259
+ return false if item.items.empty? # No empty parents
260
+
261
+ # Allow nesting only for items with certain attributes
262
+ item.options[:allow_nesting]
263
+ end
264
+ end
265
+ ```
266
+
267
+
268
+
154
269
  ### Badge Components
155
270
 
156
271
  Badges can be either strings or Phlex components:
@@ -295,4 +410,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/radioa
295
410
 
296
411
  ## License
297
412
 
298
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
413
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexi-menu (0.0.1)
4
+ phlexi-menu (0.1.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.0.1)
4
+ phlexi-menu (0.1.0)
5
5
  phlex (~> 1.11)
6
6
  phlexi-field
7
7
  zeitwerk
@@ -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
@@ -38,9 +38,6 @@ module Phlexi
38
38
  super()
39
39
  end
40
40
 
41
- # Renders the menu structure as HTML.
42
- #
43
- # @return [String] The rendered HTML
44
41
  def view_template
45
42
  nav(class: themed(:nav)) do
46
43
  render_items(@menu.items)
@@ -57,7 +54,7 @@ module Phlexi
57
54
  return if depth >= @max_depth
58
55
  return if items.empty?
59
56
 
60
- ul(class: themed(:items_container)) do
57
+ ul(class: themed(:items_container, depth)) do
61
58
  items.each do |item|
62
59
  render_item_wrapper(item, depth)
63
60
  end
@@ -70,114 +67,166 @@ module Phlexi
70
67
  # @param depth [Integer] Current nesting depth
71
68
  def render_item_wrapper(item, depth)
72
69
  li(class: tokens(
73
- themed(:item_wrapper),
74
- active_class(item),
75
- item_parent_class(item)
70
+ themed(:item_wrapper, depth),
71
+ item_parent_class(item, depth),
72
+ active?(item) ? themed(:active, depth) : nil
76
73
  )) do
77
- render_item_content(item)
78
- render_items(item.items, depth + 1) if item.items.any?
74
+ render_item_content(item, depth)
75
+ render_items(item.items, depth + 1) if nested?(item, depth)
79
76
  end
80
77
  end
81
78
 
79
+ def nested?(item, depth)
80
+ has_children = item.items.any?
81
+ within_depth = (depth + 1) < @max_depth
82
+ has_children && within_depth
83
+ end
84
+
85
+ def item_parent_class(item, depth)
86
+ nested?(item, depth) ? themed(:item_parent, depth) : nil
87
+ end
88
+
89
+ def active?(item)
90
+ item.active?(self)
91
+ end
92
+
82
93
  # Renders the content of a menu item, choosing between link and span.
83
94
  #
84
95
  # @param item [Phlexi::Menu::Item] The item to render content for
85
- def render_item_content(item)
96
+ # @param depth [Integer] Current nesting depth
97
+ def render_item_content(item, depth)
86
98
  if item.url
87
- render_item_link(item)
99
+ render_item_link(item, depth)
88
100
  else
89
- render_item_span(item)
101
+ render_item_span(item, depth)
90
102
  end
91
103
  end
92
104
 
93
105
  # Renders a menu item as a link.
94
106
  #
95
107
  # @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)
108
+ # @param depth [Integer] Current nesting depth
109
+ def render_item_link(item, depth)
110
+ a(
111
+ href: item.url,
112
+ class: tokens(
113
+ themed(:item_link, depth),
114
+ active?(item) ? themed(:active, depth) : nil
115
+ )
116
+ ) do
117
+ render_item_interior(item, depth)
99
118
  end
100
119
  end
101
120
 
102
121
  # Renders a menu item as a span (for non-linking items).
103
122
  #
104
123
  # @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)
124
+ # @param depth [Integer] Current nesting depth
125
+ def render_item_span(item, depth)
126
+ span(class: themed(:item_span, depth)) do
127
+ render_item_interior(item, depth)
108
128
  end
109
129
  end
110
130
 
111
131
  # Renders the interior content of a menu item (badges, icon, label).
112
132
  #
113
133
  # @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
134
+ # @param depth [Integer] Current nesting depth
135
+ def render_item_interior(item, depth)
136
+ render_leading_badge(item.leading_badge, depth) if item.leading_badge
137
+ render_icon(item.icon, depth) if item.icon
138
+ render_label(item.label, depth)
139
+ render_trailing_badge(item.trailing_badge, depth) if item.trailing_badge
119
140
  end
120
141
 
121
142
  # Renders the item's label.
122
143
  #
123
144
  # @param label [String, Component] The label to render
124
- def render_label(label)
145
+ # @param depth [Integer] Current nesting depth
146
+ def render_label(label, depth)
125
147
  phlexi_render(label) {
126
- span(class: themed(:item_label)) { label }
148
+ span(class: themed(:item_label, depth)) { label }
127
149
  }
128
150
  end
129
151
 
130
152
  # Renders the item's leading badge.
131
153
  #
132
154
  # @param badge [String, Component] The leading badge to render
133
- def render_leading_badge(badge)
155
+ # @param depth [Integer] Current nesting depth
156
+ def render_leading_badge(badge, depth)
134
157
  phlexi_render(badge) {
135
- span(class: themed(:leading_badge)) { badge }
158
+ span(class: themed(:leading_badge, depth)) { badge }
136
159
  }
137
160
  end
138
161
 
139
162
  # Renders the item's trailing badge.
140
163
  #
141
164
  # @param badge [String, Component] The trailing badge to render
142
- def render_trailing_badge(badge)
165
+ # @param depth [Integer] Current nesting depth
166
+ def render_trailing_badge(badge, depth)
143
167
  phlexi_render(badge) {
144
- span(class: themed(:trailing_badge)) { badge }
168
+ span(class: themed(:trailing_badge, depth)) { badge }
145
169
  }
146
170
  end
147
171
 
148
172
  # Renders the item's icon.
149
173
  #
150
174
  # @param icon [Class] The icon component class to render
151
- def render_icon(icon)
175
+ # @param depth [Integer] Current nesting depth
176
+ def render_icon(icon, depth)
152
177
  return unless icon
153
178
 
154
- div(class: themed(:icon_wrapper)) do
155
- render icon.new(class: themed(:icon))
179
+ div(class: themed(:icon_wrapper, depth)) do
180
+ render icon.new(class: themed(:icon, depth))
156
181
  end
157
182
  end
158
183
 
159
184
  # Determines the active state class for an item.
160
185
  #
161
186
  # @param item [Phlexi::Menu::Item] The item to check active state for
187
+ # @param depth [Integer] Current nesting depth
162
188
  # @return [String, nil] The active class name or nil
163
- def active_class(item)
164
- item.active?(self) ? themed(:active) : nil
189
+ def active_class(item, depth)
190
+ active?(item) ? themed(:active, depth) : nil
191
+ end
192
+
193
+ # Helper method to check if an item is active
194
+ #
195
+ # @param item [Phlexi::Menu::Item] The item to check
196
+ # @return [Boolean] Whether the item is active
197
+ def active?(item)
198
+ item.active?(self)
199
+ end
200
+
201
+ # Determines if an item should be treated as nested based on its contents
202
+ # and the current depth relative to the maximum allowed depth.
203
+ #
204
+ # @param item [Phlexi::Menu::Item] The item to check
205
+ # @param depth [Integer] Current nesting depth
206
+ # @return [Boolean] Whether the item should be treated as nested
207
+ def nested?(item, depth)
208
+ item.nested? && (depth + 1) < @max_depth
165
209
  end
166
210
 
167
211
  # Determines the parent state class for an item.
168
212
  #
169
213
  # @param item [Phlexi::Menu::Item] The item to check parent state for
214
+ # @param depth [Integer] Current nesting depth
170
215
  # @return [String, nil] The parent class name or nil
171
- def item_parent_class(item)
172
- item.items.any? ? themed(:item_parent) : nil
216
+ def item_parent_class(item, depth)
217
+ nested?(item, depth) ? themed(:item_parent, depth) : nil
173
218
  end
174
219
 
175
220
  # Resolves a theme component to its CSS classes.
176
221
  #
177
222
  # @param component [Symbol] The theme component to resolve
223
+ # @param depth [Integer] Current nesting depth
178
224
  # @return [String, nil] The resolved CSS classes or nil
179
- def themed(component)
180
- self.class::Theme.instance.resolve_theme(component)
225
+ def themed(component, depth = 0)
226
+ theme = self.class::Theme.instance.resolve_theme(component)
227
+ return nil if theme.nil?
228
+ return theme unless theme.respond_to?(:call)
229
+ theme.call(depth)
181
230
  end
182
231
 
183
232
  # Renders either a component or simple value with fallback.
@@ -3,19 +3,34 @@ 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: nil, # Badge before label
29
+ trailing_badge: nil, # Badge after label
30
+
31
+ # Icon elements
32
+ icon: nil, # Icon styling
33
+ icon_wrapper: nil # Icon container
19
34
  }.freeze
20
35
  end
21
36
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlexi
4
4
  module Menu
5
- VERSION = "0.0.3"
5
+ VERSION = "0.1.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
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.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich