phlexi-menu 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c226e208c70d349bcf13eda150e4762c8ec190f4c406307586d8951fa594517b
4
+ data.tar.gz: 8247e9a50e5ba3aa18b23181716e4efdde83c5609ced908fd3a101a820b04f89
5
+ SHA512:
6
+ metadata.gz: 89dc1111d457ceb9cf21e7163f03a404813489422e367297763f0c0900700b2121810c0bfeb2a2e63458705b649aee0653a9bced8211338e8482a9b86137a194
7
+ data.tar.gz: 72379ee9c33bdb2fe05d72b53ed1fb32e07dd90c55935c6f3bf91f7d3838cb4e574a55e6d7f875ac05708ee6619936331295695a41a508ce0551c799ad12913b
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
data/Appraisals ADDED
@@ -0,0 +1,7 @@
1
+ appraise "default" do
2
+ # gem "phlex", "~> 1.10"
3
+ end
4
+
5
+ appraise "rails-7" do
6
+ gem "rails", "~> 7.1.3", ">= 7.1.3.4"
7
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.0.1] - 2024-12-10
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Stefan Froelich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # Phlexi::Menu
2
+
3
+ Phlexi::Menu is a flexible and powerful menu builder for Ruby applications. It provides an elegant way to create hierarchical menus with support for icons, badges, and active state detection.
4
+
5
+ [![Ruby](https://github.com/radioactive-labs/phlexi-menu/actions/workflows/main.yml/badge.svg)](https://github.com/radioactive-labs/phlexi-menu/actions/workflows/main.yml)
6
+
7
+ ## Table of Contents
8
+
9
+ - [Features](#features)
10
+ - [Prerequisites](#prerequisites)
11
+ - [Installation](#installation)
12
+ - [Usage](#usage)
13
+ - [Basic Usage](#basic-usage)
14
+ - [Menu Items](#menu-items)
15
+ - [Component Options](#component-options)
16
+ - [Theming](#theming)
17
+ - [Badge Components](#badge-components)
18
+ - [Rails Integration](#rails-integration)
19
+ - [Advanced Usage](#advanced-usage)
20
+ - [Component Customization](#component-customization)
21
+ - [Dynamic Menus](#dynamic-menus)
22
+ - [Development](#development)
23
+ - [Contributing](#contributing)
24
+ - [License](#license)
25
+
26
+ ## Features
27
+
28
+ - Hierarchical menu structure with controlled nesting depth
29
+ - Support for icons and dual-badge system (leading and trailing badges)
30
+ - Intelligent active state detection
31
+ - Flexible theming system
32
+ - Works seamlessly with Phlex components
33
+ - Rails-compatible URL handling
34
+ - Customizable rendering components
35
+
36
+ ## Prerequisites
37
+
38
+ - Ruby >= 3.2.2
39
+ - Rails (optional, but recommended)
40
+ - Phlex (~> 1.11)
41
+
42
+ ## Installation
43
+
44
+ Add this line to your application's Gemfile:
45
+
46
+ ```ruby
47
+ gem 'phlexi-menu'
48
+ ```
49
+
50
+ And then execute:
51
+
52
+ ```bash
53
+ $ bundle install
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### Basic Usage
59
+
60
+ ```ruby
61
+ class MainMenu < Phlexi::Menu::Component
62
+ class Theme < Theme
63
+ def self.theme
64
+ super.merge({
65
+ nav: "bg-white shadow",
66
+ items_container: "space-y-1",
67
+ item_wrapper: "relative",
68
+ item_link: "flex items-center px-4 py-2 hover:bg-gray-50",
69
+ item_span: "flex items-center px-4 py-2",
70
+ item_label: "mx-3",
71
+ leading_badge: "mr-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-600",
72
+ trailing_badge: "ml-auto px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-600",
73
+ icon: "h-5 w-5",
74
+ active: "bg-blue-50 text-blue-600"
75
+ })
76
+ end
77
+ end
78
+ end
79
+
80
+ # Using the menu
81
+ menu = Phlexi::Menu::Builder.new do |m|
82
+ m.item "Dashboard",
83
+ url: "/",
84
+ icon: DashboardIcon
85
+
86
+ m.item "Users",
87
+ url: "/users",
88
+ leading_badge: "Beta",
89
+ trailing_badge: "23" do |users|
90
+ users.item "All Users", url: "/users"
91
+ users.item "Add User", url: "/users/new"
92
+ end
93
+
94
+ m.item "Settings",
95
+ url: "/settings",
96
+ icon: SettingsIcon,
97
+ leading_badge: CustomBadgeComponent
98
+ end
99
+
100
+ # In your view
101
+ render MainMenu.new(menu, max_depth: 2)
102
+ ```
103
+
104
+ ### Menu Items
105
+
106
+ Menu items support several options:
107
+
108
+ ```ruby
109
+ m.item "Menu Item",
110
+ url: "/path", # URL for the menu item
111
+ icon: IconComponent, # Icon component class
112
+ leading_badge: "Beta", # Leading badge (status/type indicators)
113
+ trailing_badge: "99+", # Trailing badge (counts/notifications)
114
+ active: ->(context) { # Custom active state logic
115
+ context.controller_name == "products"
116
+ }
117
+ ```
118
+
119
+ ### Component Options
120
+
121
+ The menu component accepts these initialization options:
122
+
123
+ ```ruby
124
+ MainMenu.new(
125
+ menu, # The menu instance
126
+ max_depth: 3, # Maximum nesting depth (default: 3)
127
+ **options # Additional options passed to templates
128
+ )
129
+ ```
130
+
131
+ ### Theming
132
+
133
+ ```ruby
134
+ class CustomMenu < Phlexi::Menu::Component
135
+ class Theme < Theme
136
+ def self.theme
137
+ super.merge({
138
+ nav: "bg-white shadow rounded-lg",
139
+ items_container: "space-y-1",
140
+ item_wrapper: "relative",
141
+ item_link: "flex items-center px-4 py-2 hover:bg-gray-50",
142
+ item_span: "flex items-center px-4 py-2",
143
+ item_label: "mx-3",
144
+ leading_badge: "mr-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-600",
145
+ trailing_badge: "ml-auto px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-600",
146
+ icon: "h-5 w-5",
147
+ active: "bg-blue-50 text-blue-600"
148
+ })
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ ### Badge Components
155
+
156
+ Badges can be either strings or Phlex components:
157
+
158
+ ```ruby
159
+ class CustomBadgeComponent < ApplicationComponent
160
+ def view_template
161
+ div(class: "flex items-center") do
162
+ span(class: "h-2 w-2 rounded-full bg-blue-400")
163
+ span(class: "ml-2") { "New" }
164
+ end
165
+ end
166
+ end
167
+
168
+ # Usage
169
+ m.item "Products", leading_badge: CustomBadgeComponent
170
+ ```
171
+
172
+ ### Rails Integration
173
+
174
+ In your controller:
175
+
176
+ ```ruby
177
+ class ApplicationController < ActionController::Base
178
+ def navigation
179
+ @navigation ||= Phlexi::Menu::Builder.new do |m|
180
+ m.item "Home",
181
+ url: root_path,
182
+ icon: HomeIcon
183
+
184
+ if user_signed_in?
185
+ m.item "Account",
186
+ url: account_path,
187
+ trailing_badge: notifications_count do |account|
188
+ account.item "Profile", url: profile_path
189
+ account.item "Settings", url: settings_path
190
+ account.item "Logout", url: logout_path
191
+ end
192
+ end
193
+
194
+ if current_user&.admin?
195
+ m.item "Admin",
196
+ url: admin_path,
197
+ leading_badge: "Admin"
198
+ end
199
+ end
200
+ end
201
+ helper_method :navigation
202
+ end
203
+ ```
204
+
205
+ Note: The menu component uses Rails' `current_page?` helper for default active state detection. If you're not using Rails or want custom active state logic, provide an `active` callable to your menu items:
206
+
207
+ ```ruby
208
+ m.item "Custom Active", url: "/path", active: ->(context) {
209
+ # Your custom active state logic here
210
+ context.request.path.start_with?("/path")
211
+ }
212
+ ```
213
+
214
+ ## Advanced Usage
215
+
216
+ ### Component Customization
217
+
218
+ You can customize specific rendering steps:
219
+
220
+ ```ruby
221
+ class CustomMenu < Phlexi::Menu::Component
222
+ # Override just what you need
223
+ def render_item_interior(item)
224
+ div(class: "flex items-center gap-2") do
225
+ render_leading_badge(item.leading_badge) if item.leading_badge
226
+ render_icon(item.icon) if item.icon
227
+ span(class: themed(:item_label)) { item.label.upcase }
228
+ render_trailing_badge(item.trailing_badge) if item.trailing_badge
229
+ end
230
+ end
231
+
232
+ def render_leading_badge(badge)
233
+ div(class: tokens(themed(:leading_badge), "flex items-center")) do
234
+ span { "●" }
235
+ span(class: "ml-1") { badge }
236
+ end
237
+ end
238
+ end
239
+ ```
240
+
241
+ The component provides these customization points:
242
+ - `render_items`: Handles collection of items and nesting
243
+ - `render_item_wrapper`: Wraps individual items
244
+ - `render_item_content`: Chooses between link and span rendering
245
+ - `render_item_interior`: Handles the item's internal layout
246
+ - `render_leading_badge`: Renders the leading badge
247
+ - `render_trailing_badge`: Renders the trailing badge
248
+ - `render_icon`: Renders the icon component
249
+
250
+ ### Dynamic Menus
251
+
252
+ Example of building menus based on user permissions:
253
+
254
+ ```ruby
255
+ Phlexi::Menu::Builder.new do |m|
256
+ # Basic items
257
+ m.item "Home", url: root_path
258
+
259
+ # Authorization-based items
260
+ if current_user.can?(:manage, :products)
261
+ m.item "Products", url: products_path do |products|
262
+ products.item "All Products", url: products_path
263
+ products.item "Categories", url: categories_path if current_user.can?(:manage, :categories)
264
+ products.item "New Product", url: new_product_path
265
+ end
266
+ end
267
+
268
+ # Dynamic items from database
269
+ current_user.organizations.each do |org|
270
+ m.item org.name, url: organization_path(org), icon: OrgIcon
271
+ end
272
+ end
273
+ ```
274
+
275
+ ## Development
276
+
277
+ After checking out the repo:
278
+
279
+ 1. Run `bin/setup` to install dependencies
280
+ 2. Run `bin/appraise install` to install appraisal gemfiles
281
+ 3. Run `bin/appraise rake test` to run the tests against all supported versions
282
+ 4. You can also run `bin/console` for an interactive prompt
283
+
284
+ For development against a single version, you can just use `bundle exec rake test`.
285
+
286
+ ## Contributing
287
+
288
+ Bug reports and pull requests are welcome on GitHub at https://github.com/radioactive-labs/phlexi-menu.
289
+
290
+ 1. Fork it
291
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
292
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
293
+ 4. Push to the branch (`git push origin my-new-feature`)
294
+ 5. Create new Pull Request
295
+
296
+ ## License
297
+
298
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "standard/rake"
6
+
7
+ task default: %i[test standard]
8
+
9
+ # https://juincc.medium.com/how-to-setup-minitest-for-your-gems-development-f29c4bee13c2
10
+ Rake::TestTask.new do |t|
11
+ t.libs << "test"
12
+ t.test_files = FileList["test/**/*_test.rb"]
13
+ t.verbose = true
14
+ end
data/config.ru ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+
6
+ Bundler.require :default, :development
data/export.json ADDED
@@ -0,0 +1,82 @@
1
+ [
2
+ {
3
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/CHANGELOG.md",
4
+ "contents": "## [Unreleased]\n\n## [0.0.1] - 2024-12-10\n\n- Initial release\n"
5
+ },
6
+ {
7
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/README.md",
8
+ "contents": "# Phlexi::Menu\n\nPhlexi::Menu is a flexible and powerful menu builder for Ruby applications. It provides an elegant way to create hierarchical menus with support for icons, badges, and active state detection.\n\n[![Ruby](https://github.com/radioactive-labs/phlexi-menu/actions/workflows/main.yml/badge.svg)](https://github.com/radioactive-labs/phlexi-menu/actions/workflows/main.yml)\n\n## Table of Contents\n\n- [Features](#features)\n- [Prerequisites](#prerequisites)\n- [Installation](#installation)\n- [Usage](#usage)\n - [Basic Usage](#basic-usage)\n - [Menu Items](#menu-items)\n - [Component Options](#component-options)\n - [Theming](#theming)\n - [Badge Components](#badge-components)\n - [Rails Integration](#rails-integration)\n- [Advanced Usage](#advanced-usage)\n - [Component Customization](#component-customization)\n - [Dynamic Menus](#dynamic-menus)\n- [Development](#development)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Features\n\n- Hierarchical menu structure with controlled nesting depth\n- Support for icons and dual-badge system (leading and trailing badges)\n- Intelligent active state detection\n- Flexible theming system\n- Works seamlessly with Phlex components\n- Rails-compatible URL handling\n- Customizable rendering components\n\n## Prerequisites\n\n- Ruby >= 3.2.2\n- Rails (optional, but recommended)\n- Phlex (~> 1.11)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'phlexi-menu'\n```\n\nAnd then execute:\n\n```bash\n$ bundle install\n```\n\n## Usage\n\n### Basic Usage\n\n```ruby\nclass MainMenu < Phlexi::Menu::Component\n class Theme < Theme\n def self.theme\n super.merge({\n nav: \"bg-white shadow\",\n items_container: \"space-y-1\",\n item_wrapper: \"relative\",\n item_link: \"flex items-center px-4 py-2 hover:bg-gray-50\",\n item_span: \"flex items-center px-4 py-2\",\n item_label: \"mx-3\",\n leading_badge: \"mr-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-600\",\n trailing_badge: \"ml-auto px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-600\",\n icon: \"h-5 w-5\",\n active: \"bg-blue-50 text-blue-600\"\n })\n end\n end\nend\n\n# Using the menu\nmenu = Phlexi::Menu::Builder.new do |m|\n m.item \"Dashboard\", \n url: \"/\", \n icon: DashboardIcon\n \n m.item \"Users\", \n url: \"/users\", \n leading_badge: \"Beta\",\n trailing_badge: \"23\" do |users|\n users.item \"All Users\", url: \"/users\"\n users.item \"Add User\", url: \"/users/new\"\n end\n \n m.item \"Settings\", \n url: \"/settings\", \n icon: SettingsIcon,\n leading_badge: CustomBadgeComponent\nend\n\n# In your view\nrender MainMenu.new(menu, max_depth: 2)\n```\n\n### Menu Items\n\nMenu items support several options:\n\n```ruby\nm.item \"Menu Item\",\n url: \"/path\", # URL for the menu item\n icon: IconComponent, # Icon component class\n leading_badge: \"Beta\", # Leading badge (status/type indicators)\n trailing_badge: \"99+\", # Trailing badge (counts/notifications)\n active: ->(context) { # Custom active state logic\n context.controller_name == \"products\"\n }\n```\n\n### Component Options\n\nThe menu component accepts these initialization options:\n\n```ruby\nMainMenu.new(\n menu, # The menu instance\n max_depth: 3, # Maximum nesting depth (default: 3)\n **options # Additional options passed to templates\n)\n```\n\n### Theming\n\n```ruby\nclass CustomMenu < Phlexi::Menu::Component\n class Theme < Theme\n def self.theme\n super.merge({\n nav: \"bg-white shadow rounded-lg\",\n items_container: \"space-y-1\",\n item_wrapper: \"relative\",\n item_link: \"flex items-center px-4 py-2 hover:bg-gray-50\",\n item_span: \"flex items-center px-4 py-2\",\n item_label: \"mx-3\",\n leading_badge: \"mr-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-600\",\n trailing_badge: \"ml-auto px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-600\",\n icon: \"h-5 w-5\",\n active: \"bg-blue-50 text-blue-600\"\n })\n end\n end\nend\n```\n\n### Badge Components\n\nBadges can be either strings or Phlex components:\n\n```ruby\nclass CustomBadgeComponent < ApplicationComponent\n def view_template\n div(class: \"flex items-center\") do\n span(class: \"h-2 w-2 rounded-full bg-blue-400\")\n span(class: \"ml-2\") { \"New\" }\n end\n end\nend\n\n# Usage\nm.item \"Products\", leading_badge: CustomBadgeComponent\n```\n\n### Rails Integration\n\nIn your controller:\n\n```ruby\nclass ApplicationController < ActionController::Base\n def navigation\n @navigation ||= Phlexi::Menu::Builder.new do |m|\n m.item \"Home\", \n url: root_path, \n icon: HomeIcon\n \n if user_signed_in?\n m.item \"Account\", \n url: account_path,\n trailing_badge: notifications_count do |account|\n account.item \"Profile\", url: profile_path\n account.item \"Settings\", url: settings_path\n account.item \"Logout\", url: logout_path\n end\n end\n\n if current_user&.admin?\n m.item \"Admin\", \n url: admin_path, \n leading_badge: \"Admin\"\n end\n end\n end\n helper_method :navigation\nend\n```\n\nNote: The menu component uses Rails' `current_page?` helper for default active state detection. If you're not using Rails or want custom active state logic, provide an `active` callable to your menu items:\n\n```ruby\nm.item \"Custom Active\", url: \"/path\", active: ->(context) {\n # Your custom active state logic here\n context.request.path.start_with?(\"/path\")\n}\n```\n\n## Advanced Usage\n\n### Component Customization\n\nYou can customize specific rendering steps:\n\n```ruby\nclass CustomMenu < Phlexi::Menu::Component\n # Override just what you need\n def render_item_interior(item)\n div(class: \"flex items-center gap-2\") do\n render_leading_badge(item.leading_badge) if item.leading_badge\n render_icon(item.icon) if item.icon\n span(class: themed(:item_label)) { item.label.upcase }\n render_trailing_badge(item.trailing_badge) if item.trailing_badge\n end\n end\n\n def render_leading_badge(badge)\n div(class: tokens(themed(:leading_badge), \"flex items-center\")) do\n span { \"●\" }\n span(class: \"ml-1\") { badge }\n end\n end\nend\n```\n\nThe component provides these customization points:\n- `render_items`: Handles collection of items and nesting\n- `render_item_wrapper`: Wraps individual items\n- `render_item_content`: Chooses between link and span rendering\n- `render_item_interior`: Handles the item's internal layout\n- `render_leading_badge`: Renders the leading badge\n- `render_trailing_badge`: Renders the trailing badge\n- `render_icon`: Renders the icon component\n\n### Dynamic Menus\n\nExample of building menus based on user permissions:\n\n```ruby\nPhlexi::Menu::Builder.new do |m|\n # Basic items\n m.item \"Home\", url: root_path\n \n # Authorization-based items\n if current_user.can?(:manage, :products)\n m.item \"Products\", url: products_path do |products|\n products.item \"All Products\", url: products_path\n products.item \"Categories\", url: categories_path if current_user.can?(:manage, :categories)\n products.item \"New Product\", url: new_product_path\n end\n end\n \n # Dynamic items from database\n current_user.organizations.each do |org|\n m.item org.name, url: organization_path(org), icon: OrgIcon\n end\nend\n```\n\n## Development\n\nAfter checking out the repo:\n\n1. Run `bin/setup` to install dependencies\n2. Run `bin/appraise install` to install appraisal gemfiles \n3. Run `bin/appraise rake test` to run the tests against all supported versions\n4. You can also run `bin/console` for an interactive prompt\n\nFor development against a single version, you can just use `bundle exec rake test`.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/radioactive-labs/phlexi-menu.\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT)."
9
+ },
10
+ {
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..-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
+ },
14
+ {
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 class Builder\n attr_reader :items\n\n class Item < Phlexi::Menu::Item; end\n\n def initialize(&)\n @items = []\n\n yield self if block_given?\n end\n\n def item(label, **, &)\n new_item = self.class::Item.new(label, **, &)\n @items << new_item\n new_item\n end\n end\n end\nend\n"
17
+ },
18
+ {
19
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi/menu/component.rb",
20
+ "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Menu\n # Base menu component that other menu renderers can inherit from\n class Component < COMPONENT_BASE\n class Theme < Phlexi::Menu::Theme; end\n\n DEFAULT_MAX_DEPTH = 3\n\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 def view_template\n nav(class: themed(:nav)) do\n render_items(@menu.items)\n end\n end\n\n protected\n\n # Base implementation handles nesting and delegates individual item rendering\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 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 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 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 def render_item_span(item)\n span(class: themed(:item_span)) do\n render_item_interior(item)\n end\n end\n\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 def render_label(label)\n phlexi_render(label) {\n span(class: themed(:item_label)) { label }\n }\n end\n\n def render_leading_badge(badge)\n phlexi_render(badge) {\n span(class: themed(:leading_badge)) { badge.to_s }\n }\n end\n\n def render_trailing_badge(badge)\n phlexi_render(badge) {\n span(class: themed(:trailing_badge)) { badge.to_s }\n }\n end\n\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 def active_class(item)\n item.active?(context) ? themed(:active) : nil\n end\n\n def item_parent_class(item)\n item.items.any? ? themed(:item_parent) : nil\n end\n\n def themed(component)\n self.class::Theme.instance.resolve_theme(component)\n end\n\n def phlexi_render(arg, &)\n return unless arg\n raise ArgumentError, \"phlexi_render requires a default render block\" unless block_given?\n\n # Handle Phlex components or Rails Renderables\n if arg.class < Phlex::SGML || arg.respond_to?(:render_in)\n render arg\n # Handle procs\n elsif arg.respond_to?(:to_proc)\n instance_exec(&arg)\n else\n yield\n end\n end\n end\n end\nend\n"
21
+ },
22
+ {
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 class Item\n attr_reader :label, :url, :icon, :leading_badge, :trailing_badge, :items, :options\n\n def initialize(label, url: nil, icon: nil, leading_badge: nil, trailing_badge: nil, **options, &)\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 def item(label, **, &)\n new_item = self.class.new(label, **, &)\n @items << new_item\n new_item\n end\n\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 end\n end\nend\n"
25
+ },
26
+ {
27
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi/menu/theme.rb",
28
+ "contents": "require \"phlexi-field\"\n\nmodule Phlexi\n module Menu\n class Theme < Phlexi::Field::Theme\n def self.theme\n @theme ||= {\n nav: nil,\n items_container: nil,\n item_wrapper: nil,\n item_parent: nil,\n item_link: nil,\n item_span: nil,\n item_label: nil,\n leading_badge: nil,\n trailing_badge: nil,\n icon: nil,\n active: nil\n }.freeze\n end\n end\n end\nend\n"
29
+ },
30
+ {
31
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi/menu/version.rb",
32
+ "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Menu\n VERSION = \"0.0.1\"\n end\nend\n"
33
+ },
34
+ {
35
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi/menu.rb",
36
+ "contents": "# frozen_string_literal: true\n\nrequire \"zeitwerk\"\nrequire \"phlex\"\nrequire \"active_support/core_ext/object/blank\"\n\nmodule Phlexi\n NIL_VALUE = :__i_phlexi_i__\n\n module Menu\n Loader = Zeitwerk::Loader.new.tap do |loader|\n loader.tag = File.basename(__FILE__, \".rb\")\n loader.ignore(\"#{__dir__}/menu/version.rb\")\n loader.inflector.inflect(\n \"phlexi-menu\" => \"Phlexi\",\n \"phlexi\" => \"Phlexi\"\n )\n loader.push_dir(File.expand_path(\"..\", __dir__))\n loader.setup\n end\n\n COMPONENT_BASE = (defined?(::ApplicationComponent) ? ::ApplicationComponent : Phlex::HTML)\n\n class Error < StandardError; end\n\n def self.object_primary_key(object)\n if object.class.respond_to?(:primary_key)\n object.send(object.class.primary_key.to_sym)\n elsif object.respond_to?(:id)\n object.id\n end\n end\n end\nend\n"
37
+ },
38
+ {
39
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/lib/phlexi-menu.rb",
40
+ "contents": "# frozen_string_literal: true\n\nrequire_relative \"phlexi/menu/version\"\nrequire_relative \"phlexi/menu\"\n"
41
+ },
42
+ {
43
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/phlexi-menu.gemspec",
44
+ "contents": "# frozen_string_literal: true\n\nrequire_relative \"lib/phlexi/menu/version\"\n\nGem::Specification.new do |spec|\n spec.name = \"phlexi-menu\"\n spec.version = Phlexi::Menu::VERSION\n spec.authors = [\"Stefan Froelich\"]\n spec.email = [\"sfroelich01@gmail.com\"]\n\n spec.summary = \"A flexible and powerful menu builder for Ruby applications\"\n spec.description = \"Phlexi::Menu is a flexible menu builder for Ruby applications that provides hierarchical menus, active state detection, icons, badges, and a powerful theming system. Built with Phlex components, it offers a modern approach to building navigation menus.\"\n spec.homepage = \"https://github.com/radioactive-labs/phlexi-menu\"\n spec.license = \"MIT\"\n spec.required_ruby_version = \">= 3.2.2\"\n\n spec.metadata[\"allowed_push_host\"] = \"https://rubygems.org\"\n\n spec.metadata[\"homepage_uri\"] = spec.homepage\n spec.metadata[\"source_code_uri\"] = spec.homepage\n spec.metadata[\"changelog_uri\"] = spec.homepage\n\n # Specify which files should be added to the gem when it is released.\n # The `git ls-files -z` loads the files in the RubyGem that have been added into git.\n gemspec = File.basename(__FILE__)\n spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|\n ls.readlines(\"\\x0\", chomp: true).reject do |f|\n (f == gemspec) ||\n f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])\n end\n end\n spec.bindir = \"exe\"\n spec.executables = spec.files.grep(%r{\\Aexe/}) { |f| File.basename(f) }\n spec.require_paths = [\"lib\"]\n\n spec.add_dependency \"phlex\", \"~> 1.11\"\n spec.add_dependency \"zeitwerk\"\n spec.add_dependency \"phlexi-field\"\n\n spec.add_development_dependency \"rake\"\n spec.add_development_dependency \"minitest\"\n spec.add_development_dependency \"minitest-reporters\"\n spec.add_development_dependency \"standard\"\n # spec.add_development_dependency \"brakeman\"\n spec.add_development_dependency \"bundle-audit\"\n spec.add_development_dependency \"appraisal\"\n spec.add_development_dependency \"combustion\"\n spec.add_development_dependency \"phlex-testing-capybara\"\nend\n"
45
+ },
46
+ {
47
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/internal/app/controllers/users_controller.rb",
48
+ "contents": "class UsersController < ActionController::Base\n def create\n render plain: \"OK\"\n end\nend\n"
49
+ },
50
+ {
51
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/internal/app/models/post.rb",
52
+ "contents": "class Post < ActiveRecord::Base\n belongs_to :user\n\n validates :body, presence: true\nend\n"
53
+ },
54
+ {
55
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/internal/app/models/user.rb",
56
+ "contents": "class User < ActiveRecord::Base\n has_many :posts\n\n validates :name, presence: true\nend\n"
57
+ },
58
+ {
59
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/internal/config/database.yml",
60
+ "contents": "test:\n adapter: sqlite3\n database: db/combustion_test.sqlite\n"
61
+ },
62
+ {
63
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/internal/config/routes.rb",
64
+ "contents": "# frozen_string_literal: true\n\nRails.application.routes.draw do\n # Add your own routes here, or remove this file if you don't have need for it.\n resources :users, only: [:create]\nend\n"
65
+ },
66
+ {
67
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/internal/config/storage.yml",
68
+ "contents": "test:\n service: Disk\n root: tmp/storage\n"
69
+ },
70
+ {
71
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/internal/db/schema.rb",
72
+ "contents": "# frozen_string_literal: true\n\nActiveRecord::Schema.define do\n create_table :users, force: true do |t|\n t.string :name\n t.string :email\n t.timestamps null: false\n end\n\n create_table :posts, force: true do |t|\n t.belongs_to :user\n t.string :body\n t.timestamps null: false\n end\nend\n"
73
+ },
74
+ {
75
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/phlexi/menu_test.rb",
76
+ "contents": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nmodule Phlexi\n class MenuTest < Minitest::Test\n include Capybara::DSL\n include Phlex::Testing::Capybara::ViewHelper\n\n class TestIcon < Phlex::HTML\n def initialize(**attributes)\n @attributes = attributes\n super()\n end\n\n def view_template\n div(**@attributes) { \"Test Icon\" }\n end\n end\n\n class TestComponent < Phlex::HTML\n def initialize(**attributes)\n @attributes = attributes\n super()\n end\n\n def view_template\n div(**@attributes) { \"Test Component\" }\n end\n end\n\n class CustomThemeMenu < Phlexi::Menu::Component\n class Theme < Theme\n def self.theme\n super.merge({\n nav: \"custom-nav\",\n item_label: \"custom-label\"\n })\n end\n end\n end\n\n class TestMenu < Phlexi::Menu::Component\n class Theme < Theme\n def self.theme\n super.merge({\n nav: \"test-nav\",\n items_container: \"test-items\",\n item_wrapper: \"test-item\",\n item_parent: \"test-parent\",\n item_link: \"test-link\",\n item_span: \"test-span\",\n item_label: \"test-label\",\n leading_badge: \"test-leading-badge\",\n trailing_badge: \"test-trailing-badge\",\n icon: \"test-icon\",\n active: \"test-active\"\n })\n end\n end\n end\n\n class MockContext\n class MockHelpers\n def initialize(current_page_path)\n @current_page_path = current_page_path\n end\n\n def current_page?(path)\n path == @current_page_path\n end\n end\n\n class MockRequest\n attr_reader :path\n\n def initialize(path)\n @path = path\n end\n end\n\n attr_reader :request_path, :current_page_path\n\n def initialize(request_path: \"/\", current_page_path: \"/\")\n @request_path = request_path\n @current_page_path = current_page_path\n end\n\n def request\n @request ||= MockRequest.new(@request_path)\n end\n\n def helpers\n @helpers ||= MockHelpers.new(@current_page_path)\n end\n end\n\n def setup\n @menu = Phlexi::Menu::Builder.new do |m|\n m.item \"Home\",\n url: \"/\",\n icon: TestIcon,\n leading_badge: \"New\",\n trailing_badge: \"2\"\n\n m.item \"Products\",\n url: \"/products\" do |products|\n products.item \"All Products\",\n url: \"/products\",\n leading_badge: TestComponent\n products.item \"Add Product\",\n url: \"/products/new\"\n end\n\n m.item \"Settings\",\n url: \"/settings\",\n active: ->(context) { context.respond_to?(:request) && context.request.path.start_with?(\"/settings\") }\n end\n end\n\n def test_menu_structure\n assert_equal 3, @menu.items.length\n\n # Test first level items\n home = @menu.items[0]\n assert_equal \"Home\", home.label\n assert_equal \"/\", home.url\n assert_equal TestIcon, home.icon\n assert_equal \"New\", home.leading_badge\n assert_equal \"2\", home.trailing_badge\n assert_empty home.items\n\n # Test nested items\n products = @menu.items[1]\n assert_equal \"Products\", products.label\n assert_equal \"/products\", products.url\n assert_equal 2, products.items.length\n\n # Test nested item properties\n all_products = products.items[0]\n assert_equal \"All Products\", all_products.label\n assert_equal \"/products\", all_products.url\n assert_equal TestComponent, all_products.leading_badge\n end\n\n def test_menu_rendering\n render TestMenu.new(@menu)\n\n # Test basic structure\n assert has_css?(\".test-nav\")\n assert has_css?(\".test-items\")\n\n # Test top-level items count\n assert_equal 3, all(\".test-nav > .test-items > .test-item\", minimum: 0).count\n\n # Test Home item structure and content\n assert has_css?(\".test-nav > .test-items > .test-item:first-child .test-link[href='/']\")\n assert has_css?(\".test-nav > .test-items > .test-item:first-child .test-leading-badge\", text: \"New\")\n assert has_css?(\".test-nav > .test-items > .test-item:first-child .test-icon\", text: \"Test Icon\")\n assert has_css?(\".test-nav > .test-items > .test-item:first-child .test-label\", text: \"Home\")\n assert has_css?(\".test-nav > .test-items > .test-item:first-child .test-trailing-badge\", text: \"2\")\n\n # Test Products item and its nested structure\n products_item = \".test-nav > .test-items > .test-item:nth-child(2)\"\n assert has_css?(\"#{products_item} .test-link[href='/products']\")\n assert has_css?(\"#{products_item} .test-label\", text: \"Products\")\n assert has_css?(\"#{products_item}.test-parent\")\n\n # Test nested items under Products\n assert_equal 2, all(\"#{products_item} > .test-items > .test-item\", minimum: 0).count\n\n # Test All Products item\n all_products = \"#{products_item} > .test-items > .test-item:first-child\"\n assert has_css?(\"#{all_products} .test-link[href='/products']\")\n assert has_css?(\"#{all_products} .test-leading-badge\", text: \"Phlexi::MenuTest::TestComponent\")\n assert has_css?(\"#{all_products} .test-label\", text: \"All Products\")\n\n # Test Add Product item\n add_product = \"#{products_item} > .test-items > .test-item:last-child\"\n assert has_css?(\"#{add_product} .test-link[href='/products/new']\")\n assert has_css?(\"#{add_product} .test-label\", text: \"Add Product\")\n\n # Test Settings item\n settings_item = \".test-nav > .test-items > .test-item:last-child\"\n assert has_css?(\"#{settings_item} .test-link[href='/settings']\")\n assert has_css?(\"#{settings_item} .test-label\", text: \"Settings\")\n end\n\n def test_active_state_detection\n # Test direct URL match\n mock_context = MockContext.new(\n request_path: \"/\",\n current_page_path: \"/\"\n )\n assert @menu.items[0].active?(mock_context), \"Home item should be active when current page matches\"\n\n # Test custom active logic\n mock_context = MockContext.new(\n request_path: \"/settings/profile\",\n current_page_path: \"/other\"\n )\n assert @menu.items[2].active?(mock_context), \"Settings should be active when path starts with /settings\"\n\n # Test parent active state through child URL match\n mock_context = MockContext.new(\n request_path: \"/other\",\n current_page_path: \"/products/new\"\n )\n assert @menu.items[1].active?(mock_context), \"Products menu should be active when a child URL matches\"\n\n # Test direct child URL match\n mock_context = MockContext.new(\n request_path: \"/products\",\n current_page_path: \"/products\"\n )\n assert @menu.items[1].items[0].active?(mock_context), \"Child item should be active when its URL matches\"\n\n # Test parent isn't active when URLs don't match\n mock_context = MockContext.new(\n request_path: \"/other\",\n current_page_path: \"/other\"\n )\n refute @menu.items[1].active?(mock_context), \"Products menu should not be active when no URLs match\"\n end\n\n def test_max_depth_rendering\n deep_menu = Phlexi::Menu::Builder.new do |m|\n m.item \"Level 1\" do |l1|\n l1.item \"Level 2\" do |l2|\n l2.item \"Level 3\" do |l3|\n l3.item \"Level 4\"\n end\n end\n end\n end\n\n # Test default max depth (3)\n render TestMenu.new(deep_menu)\n\n assert has_css?(\".test-label\", text: \"Level 1\")\n assert has_css?(\".test-label\", text: \"Level 2\")\n assert has_css?(\".test-label\", text: \"Level 3\")\n refute has_css?(\".test-label\", text: \"Level 4\")\n\n # Test custom max depth\n render TestMenu.new(deep_menu, max_depth: 2)\n\n assert has_css?(\".test-label\", text: \"Level 1\")\n assert has_css?(\".test-label\", text: \"Level 2\")\n refute has_css?(\".test-label\", text: \"Level 3\")\n end\n\n def test_component_rendering\n menu = Phlexi::Menu::Builder.new do |m|\n m.item \"Item\",\n leading_badge: TestComponent.new,\n trailing_badge: TestComponent.new\n end\n\n render TestMenu.new(menu)\n\n # <nav class=\"test-nav\">\n # <ul class=\"test-items\">\n # <li class=\"test-item\">\n # <span class=\"test-span\">\n # <div>Test Component</div>\n # <span class=\"test-label\">Item</span>\n # <div>Test Component</div>\n # </span>\n # </li>\n # </ul>\n # </nav>\n\n # Check the number of TestComponent instances\n assert_equal 2, all(\"div\", text: \"Test Component\", minimum: 0).count\n\n # Check the label exists with correct text\n assert has_css?(\".test-label\", text: \"Item\")\n end\n\n def test_theme_customization\n render CustomThemeMenu.new(@menu)\n\n # Test basic theme customization\n assert has_css?(\".custom-nav\")\n\n # Test specific label presence\n assert has_css?(\".custom-label\", text: \"Home\")\n assert has_css?(\".custom-label\", text: \"Products\")\n assert has_css?(\".custom-label\", text: \"Settings\")\n\n # Test label count\n assert_equal 5, all(\".custom-label\", minimum: 0).count\n end\n end\nend\n"
77
+ },
78
+ {
79
+ "path": "/Users/stefan/Documents/plutonium/phlexi-menu/test/test_helper.rb",
80
+ "contents": "require \"phlexi-menu\"\n\nrequire \"minitest/autorun\"\nrequire \"minitest/reporters\"\nMinitest::Reporters.use!\n\nrequire \"phlex/testing/capybara\"\nrequire \"capybara/minitest\"\n\ndef gem_present?(gem_name)\n Gem::Specification.find_all_by_name(gem_name).any?\nend\n\nreturn unless gem_present?(\"rails\")\n\nrequire \"combustion\"\nCombustion.path = \"test/internal\"\nCombustion.initialize! :active_record, :action_controller\n\nRails.application.config.action_dispatch.show_exceptions = :none\n"
81
+ }
82
+ ]
data/export.rb ADDED
@@ -0,0 +1,48 @@
1
+ require "json"
2
+ require "find"
3
+
4
+ def export_files_to_json(directory, extensions, output_file, exceptions = [])
5
+ # Convert extensions to lowercase for case-insensitive matching
6
+ extensions = extensions.map(&:downcase)
7
+
8
+ # Array to store file data
9
+ files_data = []
10
+
11
+ # Find all files in directory and subdirectories
12
+ Find.find(directory) do |path|
13
+ # Skip if not a file
14
+ next unless File.file?(path)
15
+ next if exceptions.any? { |exception| path.include?(exception) }
16
+
17
+ # Check if file extension matches any in our list
18
+ ext = File.extname(path).downcase[1..-1] # Remove the leading dot
19
+ next unless extensions.include?(ext)
20
+
21
+ puts path
22
+
23
+ begin
24
+ # Read file contents
25
+ contents = File.read(path)
26
+
27
+ # Add to our array
28
+ files_data << {
29
+ "path" => path,
30
+ "contents" => contents
31
+ }
32
+ rescue => e
33
+ puts "Error reading file #{path}: #{e.message}"
34
+ end
35
+ end
36
+
37
+ # Write to JSON file
38
+ File.write(output_file, JSON.pretty_generate(files_data))
39
+
40
+ puts "Successfully exported #{files_data.length} files to #{output_file}"
41
+ end
42
+
43
+ # Example usage (uncomment and modify as needed):
44
+ directory = "/Users/stefan/Documents/plutonium/phlexi-menu"
45
+ exceptions = ["/.github/", "/.vscode/", "gemfiles", "pkg", "node_modules"]
46
+ extensions = ["rb", "md", "yml", "yaml", "gemspec"]
47
+ output_file = "export.json"
48
+ export_files_to_json(directory, extensions, output_file, exceptions)
@@ -0,0 +1,5 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec path: "../"