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 +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