phlexi-menu 0.0.1

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 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: "../"