phlexi-menu 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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