phlexi-menu 0.0.2 → 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/export.json +4 -4
- data/export.rb +1 -1
- data/gemfiles/default.gemfile.lock +1 -1
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/lib/phlexi/menu/builder.rb +8 -1
- data/lib/phlexi/menu/component.rb +91 -40
- data/lib/phlexi/menu/item.rb +16 -2
- 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).
|
data/export.json
CHANGED
@@ -9,19 +9,19 @@
|
|
9
9
|
},
|
10
10
|
{
|
11
11
|
"path": "/Users/stefan/Documents/plutonium/phlexi-menu/export.rb",
|
12
|
-
"contents": "require \"json\"\nrequire \"find\"\n\ndef export_files_to_json(directory, extensions, output_file, exceptions = [])\n # Convert extensions to lowercase for case-insensitive matching\n extensions = extensions.map(&:downcase)\n\n # Array to store file data\n files_data = []\n\n # Find all files in directory and subdirectories\n Find.find(directory) do |path|\n # Skip if not a file\n next unless File.file?(path)\n next if exceptions.any? { |exception| path.include?(exception) }\n\n # Check if file extension matches any in our list\n ext = File.extname(path).downcase[1
|
12
|
+
"contents": "require \"json\"\nrequire \"find\"\n\ndef export_files_to_json(directory, extensions, output_file, exceptions = [])\n # Convert extensions to lowercase for case-insensitive matching\n extensions = extensions.map(&:downcase)\n\n # Array to store file data\n files_data = []\n\n # Find all files in directory and subdirectories\n Find.find(directory) do |path|\n # Skip if not a file\n next unless File.file?(path)\n next if exceptions.any? { |exception| path.include?(exception) }\n\n # Check if file extension matches any in our list\n ext = File.extname(path).downcase[1..] # Remove the leading dot\n next unless extensions.include?(ext)\n\n puts path\n\n begin\n # Read file contents\n contents = File.read(path)\n\n # Add to our array\n files_data << {\n \"path\" => path,\n \"contents\" => contents\n }\n rescue => e\n puts \"Error reading file #{path}: #{e.message}\"\n end\n end\n\n # Write to JSON file\n File.write(output_file, JSON.pretty_generate(files_data))\n\n puts \"Successfully exported #{files_data.length} files to #{output_file}\"\nend\n\n# Example usage (uncomment and modify as needed):\ndirectory = \"/Users/stefan/Documents/plutonium/phlexi-menu\"\nexceptions = [\"/.github/\", \"/.vscode/\", \"gemfiles\", \"pkg\", \"node_modules\"]\nextensions = [\"rb\", \"md\", \"yml\", \"yaml\", \"gemspec\"]\noutput_file = \"export.json\"\nexport_files_to_json(directory, extensions, output_file, exceptions)\n"
|
13
13
|
},
|
14
14
|
{
|
15
15
|
"path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi/menu/builder.rb",
|
16
|
-
"contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Menu\n # Builder class for constructing hierarchical menu structures.\n # Provides a DSL for creating nested menu items with support for labels,\n # URLs, icons, and badges.\n #\n # @example Basic usage\n # menu = Phlexi::Menu::Builder.new do |m|\n # m.item \"Home\", url: \"/\"\n # m.item \"Products\", url: \"/products\" do |products|\n # products.item \"All Products\", url: \"/products\"\n # products.item \"Add Product\", url: \"/products/new\"\n # end\n # end\n class Builder\n # @return [Array<Phlexi::Menu::Item>] The collection of top-level menu items\n attr_reader :items\n\n # Nested Item class that inherits from Phlexi::Menu::Item\n class Item < Phlexi::Menu::Item; end\n\n # Initializes a new menu builder.\n #\n # @yield [builder] Passes the builder instance to the block for menu construction\n # @yieldparam builder [Phlexi::Menu::Builder] The builder instance\n def initialize(&)\n @items = []\n\n yield self if block_given?\n end\n\n # Creates and adds a new menu item to the current menu level.\n #\n # @param label [String] The display text for the menu item\n # @param ** [Hash] Additional options passed to the Item constructor\n # @yield [item] Optional block for adding nested menu items\n # @yieldparam item [Phlexi::Menu::Item] The newly created menu item\n # @return [Phlexi::Menu::Item] The created menu item\n # @raise [ArgumentError] If the label is nil\n def item(label, **, &)\n raise ArgumentError, \"Label cannot be nil\" unless label\n\n new_item = self.class::Item.new(label, **, &)\n @items << new_item\n new_item\n end\n\n # Checks if the menu has any items.\n #\n # @return [Boolean] true if the menu has no items, false otherwise\n def empty?\n @items.empty?\n end\n\n # Returns the number of top-level items in the menu.\n #\n # @return [Integer] The count of top-level menu items\n def size\n @items.size\n end\n\n # Returns a string representation of the menu structure.\n #\n # @return [String] A human-readable representation of the menu\n def inspect\n \"#<#{self.class} items=#{@items.map(&:
|
16
|
+
"contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Menu\n # Builder class for constructing hierarchical menu structures.\n # Provides a DSL for creating nested menu items with support for labels,\n # URLs, icons, and badges.\n #\n # @example Basic usage\n # menu = Phlexi::Menu::Builder.new do |m|\n # m.item \"Home\", url: \"/\"\n # m.item \"Products\", url: \"/products\" do |products|\n # products.item \"All Products\", url: \"/products\"\n # products.item \"Add Product\", url: \"/products/new\"\n # end\n # end\n class Builder\n # @return [Array<Phlexi::Menu::Item>] The collection of top-level menu items\n attr_reader :items\n\n # Nested Item class that inherits from Phlexi::Menu::Item\n class Item < Phlexi::Menu::Item; end\n\n # Initializes a new menu builder.\n #\n # @yield [builder] Passes the builder instance to the block for menu construction\n # @yieldparam builder [Phlexi::Menu::Builder] The builder instance\n def initialize(&)\n @items = []\n\n yield self if block_given?\n end\n\n # Creates and adds a new menu item to the current menu level.\n #\n # @param label [String] The display text for the menu item\n # @param ** [Hash] Additional options passed to the Item constructor\n # @yield [item] Optional block for adding nested menu items\n # @yieldparam item [Phlexi::Menu::Item] The newly created menu item\n # @return [Phlexi::Menu::Item] The created menu item\n # @raise [ArgumentError] If the label is nil\n def item(label, **, &)\n raise ArgumentError, \"Label cannot be nil\" unless label\n\n new_item = self.class::Item.new(label, **, &)\n @items << new_item\n new_item\n end\n\n # Checks if the menu has any items.\n #\n # @return [Boolean] true if the menu has no items, false otherwise\n def empty?\n @items.empty?\n end\n\n # Returns the number of top-level items in the menu.\n #\n # @return [Integer] The count of top-level menu items\n def size\n @items.size\n end\n\n # Checks if this menu item has any nested items.\n #\n # @return [Boolean] true if the item has nested items, false otherwise\n def nested?\n !empty?\n end\n\n # Returns a string representation of the menu structure.\n #\n # @return [String] A human-readable representation of the menu\n def inspect\n \"#<#{self.class} items=#{@items.map(&:inspect)}>\"\n end\n end\n end\nend\n"
|
17
17
|
},
|
18
18
|
{
|
19
19
|
"path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi/menu/component.rb",
|
20
|
-
"contents": "# frozen_string_literal: true\n\nrequire \"phlex\"\n\nmodule Phlexi\n module Menu\n # Base menu component that other menu renderers can inherit from.\n # Provides the core rendering logic for hierarchical menus with support\n # for theming, icons, badges, and active state detection.\n #\n # @example Basic usage\n # class MyMenu < Phlexi::Menu::Component\n # class Theme < Theme\n # def self.theme\n # super.merge({\n # nav: \"bg-white shadow\",\n # item_label: \"text-gray-600\"\n # })\n # end\n # end\n # end\n class Component < COMPONENT_BASE\n # Theme class for customizing menu appearance\n class Theme < Phlexi::Menu::Theme; end\n\n # @return [Integer] The default maximum nesting depth for menu items\n DEFAULT_MAX_DEPTH = 3\n\n # Initializes a new menu component.\n #\n # @param menu [Phlexi::Menu::Builder] The menu structure to render\n # @param max_depth [Integer] Maximum nesting depth for menu items\n # @param options [Hash] Additional options passed to rendering methods\n def initialize(menu, max_depth:
|
20
|
+
"contents": "# frozen_string_literal: true\n\nrequire \"phlex\"\n\nmodule Phlexi\n module Menu\n # Base menu component that other menu renderers can inherit from.\n # Provides the core rendering logic for hierarchical menus with support\n # for theming, icons, badges, and active state detection.\n #\n # @example Basic usage\n # class MyMenu < Phlexi::Menu::Component\n # class Theme < Theme\n # def self.theme\n # super.merge({\n # nav: \"bg-white shadow\",\n # item_label: \"text-gray-600\"\n # })\n # end\n # end\n # end\n class Component < COMPONENT_BASE\n # Theme class for customizing menu appearance\n class Theme < Phlexi::Menu::Theme; end\n\n # @return [Integer] The default maximum nesting depth for menu items\n DEFAULT_MAX_DEPTH = 3\n\n # Initializes a new menu component.\n #\n # @param menu [Phlexi::Menu::Builder] The menu structure to render\n # @param max_depth [Integer] Maximum nesting depth for menu items\n # @param options [Hash] Additional options passed to rendering methods\n def initialize(menu, max_depth: default_max_depth, **options)\n @menu = menu\n @max_depth = max_depth\n @options = options\n super()\n end\n\n # Renders the menu structure as HTML.\n #\n # @return [String] The rendered HTML\n def view_template\n nav(class: themed(:nav)) do\n render_items(@menu.items)\n end\n end\n\n protected\n\n # Renders a collection of menu items with nesting support.\n #\n # @param items [Array<Phlexi::Menu::Item>] The items to render\n # @param depth [Integer] Current nesting depth\n def render_items(items, depth = 0)\n return if depth >= @max_depth\n return if items.empty?\n\n ul(class: themed(:items_container)) do\n items.each do |item|\n render_item_wrapper(item, depth)\n end\n end\n end\n\n # Renders the wrapper element for a menu item.\n #\n # @param item [Phlexi::Menu::Item] The item to wrap\n # @param depth [Integer] Current nesting depth\n def render_item_wrapper(item, depth)\n li(class: tokens(\n themed(:item_wrapper),\n active_class(item),\n item_parent_class(item)\n )) do\n render_item_content(item)\n render_items(item.items, depth + 1) if item.items.any?\n end\n end\n\n # Renders the content of a menu item, choosing between link and span.\n #\n # @param item [Phlexi::Menu::Item] The item to render content for\n def render_item_content(item)\n if item.url\n render_item_link(item)\n else\n render_item_span(item)\n end\n end\n\n # Renders a menu item as a link.\n #\n # @param item [Phlexi::Menu::Item] The item to render as a link\n def render_item_link(item)\n a(href: item.url, class: themed(:item_link)) do\n render_item_interior(item)\n end\n end\n\n # Renders a menu item as a span (for non-linking items).\n #\n # @param item [Phlexi::Menu::Item] The item to render as a span\n def render_item_span(item)\n span(class: themed(:item_span)) do\n render_item_interior(item)\n end\n end\n\n # Renders the interior content of a menu item (badges, icon, label).\n #\n # @param item [Phlexi::Menu::Item] The item to render interior content for\n def render_item_interior(item)\n render_leading_badge(item.leading_badge) if item.leading_badge\n render_icon(item.icon) if item.icon\n render_label(item.label)\n render_trailing_badge(item.trailing_badge) if item.trailing_badge\n end\n\n # Renders the item's label.\n #\n # @param label [String, Component] The label to render\n def render_label(label)\n phlexi_render(label) {\n span(class: themed(:item_label)) { label }\n }\n end\n\n # Renders the item's leading badge.\n #\n # @param badge [String, Component] The leading badge to render\n def render_leading_badge(badge)\n phlexi_render(badge) {\n span(class: themed(:leading_badge)) { badge }\n }\n end\n\n # Renders the item's trailing badge.\n #\n # @param badge [String, Component] The trailing badge to render\n def render_trailing_badge(badge)\n phlexi_render(badge) {\n span(class: themed(:trailing_badge)) { badge }\n }\n end\n\n # Renders the item's icon.\n #\n # @param icon [Class] The icon component class to render\n def render_icon(icon)\n return unless icon\n\n div(class: themed(:icon_wrapper)) do\n render icon.new(class: themed(:icon))\n end\n end\n\n # Determines the active state class for an item.\n #\n # @param item [Phlexi::Menu::Item] The item to check active state for\n # @return [String, nil] The active class name or nil\n def active_class(item)\n item.active?(self) ? themed(:active) : nil\n end\n\n # Determines the parent state class for an item.\n #\n # @param item [Phlexi::Menu::Item] The item to check parent state for\n # @return [String, nil] The parent class name or nil\n def item_parent_class(item)\n item.items.any? ? themed(:item_parent) : nil\n end\n\n # Resolves a theme component to its CSS classes.\n #\n # @param component [Symbol] The theme component to resolve\n # @return [String, nil] The resolved CSS classes or nil\n def themed(component)\n self.class::Theme.instance.resolve_theme(component)\n end\n\n # Renders either a component or simple value with fallback.\n #\n # @param arg [Object] The value to render\n # @yield The default rendering block\n # @raise [ArgumentError] If no block is provided\n def phlexi_render(arg, &)\n return unless arg\n raise ArgumentError, \"phlexi_render requires a default render block\" unless block_given?\n\n if arg.class < Phlex::SGML || arg.respond_to?(:render_in)\n render arg\n elsif arg.respond_to?(:to_proc)\n instance_exec(&arg)\n else\n yield\n end\n end\n\n def default_max_depth = self.class::DEFAULT_MAX_DEPTH\n end\n end\nend\n"
|
21
21
|
},
|
22
22
|
{
|
23
23
|
"path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi/menu/item.rb",
|
24
|
-
"contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Menu\n # Represents a single menu item in the navigation hierarchy.\n # Each item can have a label, URL, icon, badges, and nested child items.\n #\n # @example Basic menu item\n # item = Item.new(\"Home\", url: \"/\")\n #\n # @example Menu item with badges and icon\n # item = Item.new(\"Products\",\n # url: \"/products\",\n # icon: ProductIcon,\n # leading_badge: \"New\",\n # trailing_badge: \"5\")\n #\n # @example Nested menu items\n # item = Item.new(\"Admin\") do |admin|\n # admin.item \"Users\", url: \"/admin/users\"\n # admin.item \"Settings\", url: \"/admin/settings\"\n # end\n class Item\n # @return [String] The display text for the menu item\n attr_reader :label\n\n # @return [String, nil] The URL the menu item links to\n attr_reader :url\n\n # @return [Class, nil] The icon component class to be rendered\n attr_reader :icon\n\n # @return [String, Component, nil] The badge displayed before the label\n attr_reader :leading_badge\n\n # @return [String, Component, nil] The badge displayed after the label\n attr_reader :trailing_badge\n\n # @return [Array<Item>] Collection of nested menu items\n attr_reader :items\n\n # @return [Hash] Additional options for customizing the menu item\n attr_reader :options\n\n # Initializes a new menu item.\n #\n # @param label [String] The display text for the menu item\n # @param url [String, nil] The URL the menu item links to\n # @param icon [Class, nil] The icon component class\n # @param leading_badge [String, Component, nil] Badge displayed before the label\n # @param trailing_badge [String, Component, nil] Badge displayed after the label\n # @param options [Hash] Additional options (e.g., :active for custom active state logic)\n # @yield [item] Optional block for adding nested items\n # @yieldparam item [Item] The newly created menu item\n # @raise [ArgumentError] If the label is nil or empty\n def initialize(label, url: nil, icon: nil, leading_badge: nil, trailing_badge: nil, **options, &)\n raise ArgumentError, \"Label cannot be nil\" unless label\n\n @label = label\n @url = url\n @icon = icon\n @leading_badge = leading_badge\n @trailing_badge = trailing_badge\n @options = options\n @items = []\n\n yield self if block_given?\n end\n\n # Creates and adds a nested menu item.\n #\n # @param label [String] The display text for the nested item\n # @param ** [Hash] Additional options passed to the Item constructor\n # @yield [item] Optional block for adding further nested items\n # @yieldparam item [Item] The newly created nested item\n # @return [Item] The created nested item\n def item(label, **, &)\n new_item = self.class.new(label, **, &)\n @items << new_item\n new_item\n end\n\n # Determines if this menu item should be shown as active.\n # Checks in the following order:\n # 1. Custom active logic if provided in options\n # 2. URL match with current page\n # 3. Active state of any child items\n #\n # @param context [Object] The context object (typically a controller) for active state checking\n # @return [Boolean] true if the item should be shown as active, false otherwise\n def active?(context)\n # First check custom active logic if provided\n return @options[:active].call(context) if @options[:active].respond_to?(:call)\n\n # Then check if this item's URL matches current page\n if context.respond_to?(:helpers) && @url\n return true if context.helpers.current_page?(@url)\n end\n\n # Finally check if any child items are active\n @items.any? { |item| item.active?(context) }\n end\n\n # Checks if this menu item has any nested items.\n #\n # @return [Boolean] true if the item has nested items, false otherwise\n def nested?\n
|
24
|
+
"contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Menu\n # Represents a single menu item in the navigation hierarchy.\n # Each item can have a label, URL, icon, badges, and nested child items.\n #\n # @example Basic menu item\n # item = Item.new(\"Home\", url: \"/\")\n #\n # @example Menu item with badges and icon\n # item = Item.new(\"Products\",\n # url: \"/products\",\n # icon: ProductIcon,\n # leading_badge: \"New\",\n # trailing_badge: \"5\")\n #\n # @example Nested menu items\n # item = Item.new(\"Admin\") do |admin|\n # admin.item \"Users\", url: \"/admin/users\"\n # admin.item \"Settings\", url: \"/admin/settings\"\n # end\n class Item\n # @return [String] The display text for the menu item\n attr_reader :label\n\n # @return [String, nil] The URL the menu item links to\n attr_reader :url\n\n # @return [Class, nil] The icon component class to be rendered\n attr_reader :icon\n\n # @return [String, Component, nil] The badge displayed before the label\n attr_reader :leading_badge\n\n # @return [String, Component, nil] The badge displayed after the label\n attr_reader :trailing_badge\n\n # @return [Array<Item>] Collection of nested menu items\n attr_reader :items\n\n # @return [Hash] Additional options for customizing the menu item\n attr_reader :options\n\n # Initializes a new menu item.\n #\n # @param label [String] The display text for the menu item\n # @param url [String, nil] The URL the menu item links to\n # @param icon [Class, nil] The icon component class\n # @param leading_badge [String, Component, nil] Badge displayed before the label\n # @param trailing_badge [String, Component, nil] Badge displayed after the label\n # @param options [Hash] Additional options (e.g., :active for custom active state logic)\n # @yield [item] Optional block for adding nested items\n # @yieldparam item [Item] The newly created menu item\n # @raise [ArgumentError] If the label is nil or empty\n def initialize(label, url: nil, icon: nil, leading_badge: nil, trailing_badge: nil, **options, &)\n raise ArgumentError, \"Label cannot be nil\" unless label\n\n @label = label\n @url = url\n @icon = icon\n @leading_badge = leading_badge\n @trailing_badge = trailing_badge\n @options = options\n @items = []\n\n yield self if block_given?\n end\n\n # Creates and adds a nested menu item.\n #\n # @param label [String] The display text for the nested item\n # @param ** [Hash] Additional options passed to the Item constructor\n # @yield [item] Optional block for adding further nested items\n # @yieldparam item [Item] The newly created nested item\n # @return [Item] The created nested item\n def item(label, **, &)\n new_item = self.class.new(label, **, &)\n @items << new_item\n new_item\n end\n\n # Determines if this menu item should be shown as active.\n # Checks in the following order:\n # 1. Custom active logic if provided in options\n # 2. URL match with current page\n # 3. Active state of any child items\n #\n # @param context [Object] The context object (typically a controller) for active state checking\n # @return [Boolean] true if the item should be shown as active, false otherwise\n def active?(context)\n # First check custom active logic if provided\n return @options[:active].call(context) if @options[:active].respond_to?(:call)\n\n # Then check if this item's URL matches current page\n if context.respond_to?(:helpers) && @url\n return true if context.helpers.current_page?(@url)\n end\n\n # Finally check if any child items are active\n @items.any? { |item| item.active?(context) }\n end\n\n # Checks if the menu has any items.\n #\n # @return [Boolean] true if the menu has no items, false otherwise\n def empty?\n @items.empty?\n end\n\n # Returns the number of top-level items in the menu.\n #\n # @return [Integer] The count of top-level menu items\n def size\n @items.size\n end\n\n # Checks if this menu item has any nested items.\n #\n # @return [Boolean] true if the item has nested items, false otherwise\n def nested?\n !empty?\n end\n\n # Returns a string representation of the menu item.\n #\n # @return [String] A human-readable representation of the menu item\n def inspect\n \"#<#{self.class} label=#{@label} url=#{@url} items=#{@items.map(&:inspect)}>\"\n end\n end\n end\nend\n"
|
25
25
|
},
|
26
26
|
{
|
27
27
|
"path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi/menu/theme.rb",
|
data/export.rb
CHANGED
@@ -15,7 +15,7 @@ def export_files_to_json(directory, extensions, output_file, exceptions = [])
|
|
15
15
|
next if exceptions.any? { |exception| path.include?(exception) }
|
16
16
|
|
17
17
|
# Check if file extension matches any in our list
|
18
|
-
ext = File.extname(path).downcase[1
|
18
|
+
ext = File.extname(path).downcase[1..] # Remove the leading dot
|
19
19
|
next unless extensions.include?(ext)
|
20
20
|
|
21
21
|
puts path
|
data/lib/phlexi/menu/builder.rb
CHANGED
@@ -61,11 +61,18 @@ module Phlexi
|
|
61
61
|
@items.size
|
62
62
|
end
|
63
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
|
+
|
64
71
|
# Returns a string representation of the menu structure.
|
65
72
|
#
|
66
73
|
# @return [String] A human-readable representation of the menu
|
67
74
|
def inspect
|
68
|
-
"#<#{self.class} items=#{@items.map(&:
|
75
|
+
"#<#{self.class} items=#{@items.map(&:inspect)}>"
|
69
76
|
end
|
70
77
|
end
|
71
78
|
end
|
@@ -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
|
@@ -31,16 +31,13 @@ module Phlexi
|
|
31
31
|
# @param menu [Phlexi::Menu::Builder] The menu structure to render
|
32
32
|
# @param max_depth [Integer] Maximum nesting depth for menu items
|
33
33
|
# @param options [Hash] Additional options passed to rendering methods
|
34
|
-
def initialize(menu, max_depth:
|
34
|
+
def initialize(menu, max_depth: default_max_depth, **options)
|
35
35
|
@menu = menu
|
36
36
|
@max_depth = max_depth
|
37
37
|
@options = options
|
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.
|
@@ -197,6 +246,8 @@ module Phlexi
|
|
197
246
|
yield
|
198
247
|
end
|
199
248
|
end
|
249
|
+
|
250
|
+
def default_max_depth = self.class::DEFAULT_MAX_DEPTH
|
200
251
|
end
|
201
252
|
end
|
202
253
|
end
|
data/lib/phlexi/menu/item.rb
CHANGED
@@ -101,18 +101,32 @@ module Phlexi
|
|
101
101
|
@items.any? { |item| item.active?(context) }
|
102
102
|
end
|
103
103
|
|
104
|
+
# Checks if the menu has any items.
|
105
|
+
#
|
106
|
+
# @return [Boolean] true if the menu has no items, false otherwise
|
107
|
+
def empty?
|
108
|
+
@items.empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the number of top-level items in the menu.
|
112
|
+
#
|
113
|
+
# @return [Integer] The count of top-level menu items
|
114
|
+
def size
|
115
|
+
@items.size
|
116
|
+
end
|
117
|
+
|
104
118
|
# Checks if this menu item has any nested items.
|
105
119
|
#
|
106
120
|
# @return [Boolean] true if the item has nested items, false otherwise
|
107
121
|
def nested?
|
108
|
-
|
122
|
+
!empty?
|
109
123
|
end
|
110
124
|
|
111
125
|
# Returns a string representation of the menu item.
|
112
126
|
#
|
113
127
|
# @return [String] A human-readable representation of the menu item
|
114
128
|
def inspect
|
115
|
-
"#<#{self.class} label=#{@label} url=#{@url} items=#{@items.map(&:
|
129
|
+
"#<#{self.class} label=#{@label} url=#{@url} items=#{@items.map(&:inspect)}>"
|
116
130
|
end
|
117
131
|
end
|
118
132
|
end
|
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