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 +4 -4
- data/README.md +122 -7
- data/gemfiles/default.gemfile.lock +1 -1
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/lib/phlexi/menu/component.rb +88 -39
- data/lib/phlexi/menu/theme.rb +26 -11
- data/lib/phlexi/menu/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9bb5cb841e6404ef962a4003718f3e2f9827dd165f417dd2a1ef9f2b5d0e07b2
|
4
|
+
data.tar.gz: 422533e0556c9e51f5c49eb5225ecb0d0bbb4c21af00b9ac56afcf4f723b7687
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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).
|
@@ -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
|
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
|
-
|
75
|
-
|
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
|
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
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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.
|
data/lib/phlexi/menu/theme.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
data/lib/phlexi/menu/version.rb
CHANGED