diff --git a/Gemfile.lock b/Gemfile.lock
index e235a589..48869346 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,7 +1,8 @@
PATH
remote: .
specs:
- rolemodel-rails (1.1.0)
+ rolemodel-rails (2.0.0)
+ benchmark
rails (> 7.1)
GEM
diff --git a/README.md b/README.md
index a076891e..8d329810 100644
--- a/README.md
+++ b/README.md
@@ -28,12 +28,15 @@ rails db:create
Add this line to your application's Gemfile:
```ruby
-gem 'rolemodel-rails', group: :development
+gem 'rolemodel-rails'
```
+> [!IMPORTANT]
+> We used to recommend putting rolemodel-rails in the development gem group. This is no longer supported, because some Rolemodel namespaced utility modules and classes are expected to be available at runtime.
+
And then execute:
- $ bundle
+ $ bundle install
## Usage
@@ -116,10 +119,6 @@ bin/new_generator testing/fantasitic_specs 'A Fantastic Testing Framework'
We use the embeded Rails apps (`example_rails_current` & `example_rails_legacy`) to test generators against. They reference the rolemodel-rails gem by local path,
so you can navigate into one of them and run your generator for immediate feedback while developing.
-> [!IMPORTANT]
-> Before submitting a PR, run `bin/bump_version` to ensure the Gem builds successfully & that everything continues to stay in sync & up-to-date.
-> For very small changes & bug fixes, prefer `bin/bump_version --patch`.
-
## Testing
Generator specs should be added to the [spec](./spec) directory.
diff --git a/example_rails_legacy/Gemfile.lock b/example_rails_legacy/Gemfile.lock
index bca04a45..379115b4 100644
--- a/example_rails_legacy/Gemfile.lock
+++ b/example_rails_legacy/Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
- rolemodel-rails (1.1.0)
+ rolemodel-rails (2.0.0)
rails (> 7.1)
GEM
diff --git a/lib/generators/rolemodel/optics/icons/README.md b/lib/generators/rolemodel/optics/icons/README.md
index fbf26ea9..8697e32f 100644
--- a/lib/generators/rolemodel/optics/icons/README.md
+++ b/lib/generators/rolemodel/optics/icons/README.md
@@ -4,4 +4,9 @@
## What you get
-Adds icon helper for handling material icons and custom icons
+Adds icon helper for handling icon generation using the provider of your choice.
+
+## Icon Builder Customization
+
+Run the generator with the `-i` option to install the IconBuilders themselves in your
+application's `lib/rolemodel/optics` directory.
diff --git a/lib/generators/rolemodel/optics/icons/icons_generator.rb b/lib/generators/rolemodel/optics/icons/icons_generator.rb
index 2614d65a..2e2b5f46 100644
--- a/lib/generators/rolemodel/optics/icons/icons_generator.rb
+++ b/lib/generators/rolemodel/optics/icons/icons_generator.rb
@@ -4,6 +4,7 @@ module Rolemodel
module Optics
# Generates the icon helper and icon builders for the chosen icon library
class IconsGenerator < Rolemodel::GeneratorBase
+ BUILDER_DIR = 'rolemodel/optics'
SUPPORTED_LIBRARIES = HashWithIndifferentAccess.new(
material: 'filled, size, weight, emphasis, additional_classes, color, hover_text',
phosphor: 'duotone, filled, size, weight, additional_classes, color, hover_text',
@@ -14,30 +15,34 @@ class IconsGenerator < Rolemodel::GeneratorBase
).freeze
source_root File.expand_path('templates', __dir__)
+ class_option :install_builders, aliases: '-i', type: :boolean, default: false,
+ desc: "Install IconBuilder classes for customization"
+
class_exclusive do
SUPPORTED_LIBRARIES.keys.each do |library|
class_option library, type: :boolean, lazy_default: false, desc: "Use #{library} icon library"
end
end
- def remove_existing_icon_helper_and_builders
- remove_dir 'app/icon_builders'
- remove_file 'app/helpers/icon_helper.rb'
- end
-
def add_view_helper
@chosen_library = capture_user_selection
@supported_properties = SUPPORTED_LIBRARIES.fetch(@chosen_library)
- template 'app/icon_builders/icon_builder.rb'
- template "app/icon_builders/#{@chosen_library}_icon_builder.rb"
- template 'app/helpers/icon_helper.rb'
+ template 'app/helpers/icon_helper.rb', force: true
+ end
+
+ def install_builders
+ return unless options.install_builders?
+
+ source_paths << File.expand_path(BUILDER_DIR, Rolemodel::GEM_LIB)
+ copy_file 'icon_builder.rb', "lib/#{BUILDER_DIR}/icon_builder.rb", force: true
+ copy_file "#{@chosen_library}_icon_builder.rb", "lib/#{BUILDER_DIR}/#{@chosen_library}_icon_builder.rb", force: true
end
private
def capture_user_selection
- options.except(*%i[skip_namespace skip_collision_check]).invert[true] || ask(
+ options.slice(*SUPPORTED_LIBRARIES.keys).invert[true] || ask(
'What icon library would you like to add?',
default: SUPPORTED_LIBRARIES.keys.first.to_s,
limited_to: SUPPORTED_LIBRARIES.keys.map(&:to_s)
diff --git a/lib/generators/rolemodel/optics/icons/templates/app/helpers/icon_helper.rb.tt b/lib/generators/rolemodel/optics/icons/templates/app/helpers/icon_helper.rb.tt
index 20e70863..46beee6c 100644
--- a/lib/generators/rolemodel/optics/icons/templates/app/helpers/icon_helper.rb.tt
+++ b/lib/generators/rolemodel/optics/icons/templates/app/helpers/icon_helper.rb.tt
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'rolemodel/optics'
+
# Helper method that renders icons from your icon library of choice.
#
# Usage:
@@ -14,11 +16,11 @@
module IconHelper
# <%= @supported_properties %>
def icon(name, **)
- <%= @chosen_library.classify %>IconBuilder.new(name, **).build
+ Rolemodel::Optics::<%= @chosen_library.classify %>IconBuilder.new(name, **).build
end
# <%= @supported_properties %>
def flash_icon(type, **)
- <%= @chosen_library.classify %>IconBuilder.flash_icon(type, **).build
+ Rolemodel::Optics::<%= @chosen_library.classify %>IconBuilder.flash_icon(type, **).build
end
end
diff --git a/lib/rolemodel-rails.rb b/lib/rolemodel-rails.rb
index d6ccc1df..06e1ef26 100644
--- a/lib/rolemodel-rails.rb
+++ b/lib/rolemodel-rails.rb
@@ -3,6 +3,8 @@
module Rolemodel
NODE_VERSION = '24.12.0'
RUBY_VERSION = '4.0.1'
+
+ GEM_LIB = File.expand_path(__dir__)
end
require 'rolemodel/version'
diff --git a/lib/rolemodel/optics.rb b/lib/rolemodel/optics.rb
new file mode 100644
index 00000000..9c5c32e0
--- /dev/null
+++ b/lib/rolemodel/optics.rb
@@ -0,0 +1,9 @@
+module Rolemodel::Optics
+ autoload :IconBuilder, 'rolemodel/optics/icon_builder'
+ autoload :CustomIconBuilder, 'rolemodel/optics/custom_icon_builder'
+ autoload :FeatherIconBuilder, 'rolemodel/optics/feather_icon_builder'
+ autoload :LucideIconBuilder, 'rolemodel/optics/lucide_icon_builder'
+ autoload :MaterialIconBuilder, 'rolemodel/optics/material_icon_builder'
+ autoload :PhosphorIconBuilder, 'rolemodel/optics/phosphor_icon_builder'
+ autoload :TablerIconBuilder, 'rolemodel/optics/tabler_icon_builder'
+end
diff --git a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/custom_icon_builder.rb b/lib/rolemodel/optics/custom_icon_builder.rb
similarity index 94%
rename from lib/generators/rolemodel/optics/icons/templates/app/icon_builders/custom_icon_builder.rb
rename to lib/rolemodel/optics/custom_icon_builder.rb
index cef0e2c1..bc5f5858 100644
--- a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/custom_icon_builder.rb
+++ b/lib/rolemodel/optics/custom_icon_builder.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# CustomIconBuilder is an IconBuilder that allows for custom SVG icons to be used in the application.
-class CustomIconBuilder < IconBuilder
+class Rolemodel::Optics::CustomIconBuilder < Rolemodel::Optics::IconBuilder
def self.flash_icons
{
notice: 'circle-check',
diff --git a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/feather_icon_builder.rb b/lib/rolemodel/optics/feather_icon_builder.rb
similarity index 82%
rename from lib/generators/rolemodel/optics/icons/templates/app/icon_builders/feather_icon_builder.rb
rename to lib/rolemodel/optics/feather_icon_builder.rb
index 8396d578..bf31e6ce 100644
--- a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/feather_icon_builder.rb
+++ b/lib/rolemodel/optics/feather_icon_builder.rb
@@ -2,7 +2,7 @@
# FeatherIconBuilder is an IconBuilder that allows for Feather icons to be used in the application.
# https://feathericons.com/
-class FeatherIconBuilder < IconBuilder
+class Rolemodel::Optics::FeatherIconBuilder < Rolemodel::Optics::IconBuilder
def self.flash_icons
{
notice: 'check-circle',
diff --git a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/icon_builder.rb b/lib/rolemodel/optics/icon_builder.rb
similarity index 73%
rename from lib/generators/rolemodel/optics/icons/templates/app/icon_builders/icon_builder.rb
rename to lib/rolemodel/optics/icon_builder.rb
index 4f60bcab..4f68aeae 100644
--- a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/icon_builder.rb
+++ b/lib/rolemodel/optics/icon_builder.rb
@@ -1,14 +1,16 @@
# frozen_string_literal: true
-class IconBuilder
+class Rolemodel::Optics::IconBuilder
include ActionView::Helpers::TagHelper
attr_reader :name, :filled, :size, :weight, :emphasis, :duotone, :additional_classes, :color, :hover_text
+ alias title hover_text
DEFAULT_SIZE = 'medium'
DEFAULT_WEIGHT = 'normal'
DEFAULT_EMPHASIS = 'normal'
+ # TODO: consider Data.define a custom options object if all of these parameters are indeed necessary.
def initialize( # rubocop:disable Metrics/ParameterLists
name,
filled: false,
@@ -42,32 +44,21 @@ def self.flash_icons
def build
options = {
class: tag_classes.compact_blank.join(' '),
- title: hover_text
- }
-
- if color.present?
+ title: hover_text,
# color: primary, neutral, alerts-notice, alerts-warning, alerts-danger, alerts-info
- options[:style] = "#{color_attribute}: var(--op-color-#{color}-base);"
- end
+ style: ("#{color_attribute}: var(--op-color-#{color}-base);" if color.present?)
+ }.compact_blank
- tag.send(tag_method, tag_contents, **options)
+ tag.public_send(tag_method, tag_contents, **options)
end
private
- def tag_method
- raise NotImplementedError
- end
-
- def tag_contents
- ''
- end
-
def tag_classes
['icon', size == DEFAULT_SIZE ? '' : "icon--#{size}", additional_classes]
end
- def color_attribute
- 'color'
- end
+ def color_attribute = 'color'
+ def tag_contents = ''
+ def tag_method = :i
end
diff --git a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/lucide_icon_builder.rb b/lib/rolemodel/optics/lucide_icon_builder.rb
similarity index 82%
rename from lib/generators/rolemodel/optics/icons/templates/app/icon_builders/lucide_icon_builder.rb
rename to lib/rolemodel/optics/lucide_icon_builder.rb
index 9ce21bad..7905484f 100644
--- a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/lucide_icon_builder.rb
+++ b/lib/rolemodel/optics/lucide_icon_builder.rb
@@ -2,7 +2,7 @@
# LucideIconBuilder is an IconBuilder that allows for Lucide icons to be used in the application.
# https://lucide.dev/icons/
-class LucideIconBuilder < IconBuilder
+class Rolemodel::Optics::LucideIconBuilder < Rolemodel::Optics::IconBuilder
def self.flash_icons
{
notice: 'circle-check',
diff --git a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/material_icon_builder.rb b/lib/rolemodel/optics/material_icon_builder.rb
similarity index 88%
rename from lib/generators/rolemodel/optics/icons/templates/app/icon_builders/material_icon_builder.rb
rename to lib/rolemodel/optics/material_icon_builder.rb
index b20d8be4..2310fe16 100644
--- a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/material_icon_builder.rb
+++ b/lib/rolemodel/optics/material_icon_builder.rb
@@ -2,7 +2,7 @@
# MaterialIconBuilder is an IconBuilder that allows for Material icons to be used in the application.
# https://fonts.google.com/icons
-class MaterialIconBuilder < IconBuilder
+class Rolemodel::Optics::MaterialIconBuilder < Rolemodel::Optics::IconBuilder
def self.flash_icons
{
notice: 'check_circle',
diff --git a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/phosphor_icon_builder.rb b/lib/rolemodel/optics/phosphor_icon_builder.rb
similarity index 86%
rename from lib/generators/rolemodel/optics/icons/templates/app/icon_builders/phosphor_icon_builder.rb
rename to lib/rolemodel/optics/phosphor_icon_builder.rb
index bcc7c59a..b128165e 100644
--- a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/phosphor_icon_builder.rb
+++ b/lib/rolemodel/optics/phosphor_icon_builder.rb
@@ -2,7 +2,7 @@
# PhosphorIconBuilder is an IconBuilder that allows for Phosphor icons to be used in the application.
# https://phosphoricons.com/
-class PhosphorIconBuilder < IconBuilder
+class Rolemodel::Optics::PhosphorIconBuilder < Rolemodel::Optics::IconBuilder
def self.flash_icons
{
notice: 'check-circle',
diff --git a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/tabler_icon_builder.rb b/lib/rolemodel/optics/tabler_icon_builder.rb
similarity index 83%
rename from lib/generators/rolemodel/optics/icons/templates/app/icon_builders/tabler_icon_builder.rb
rename to lib/rolemodel/optics/tabler_icon_builder.rb
index 4fdd9995..66650b10 100644
--- a/lib/generators/rolemodel/optics/icons/templates/app/icon_builders/tabler_icon_builder.rb
+++ b/lib/rolemodel/optics/tabler_icon_builder.rb
@@ -2,7 +2,7 @@
# TablerIconBuilder is an IconBuilder that allows for Tabler icons to be used in the application.
# https://tablericons.com/
-class TablerIconBuilder < IconBuilder
+class Rolemodel::Optics::TablerIconBuilder < Rolemodel::Optics::IconBuilder
def self.flash_icons
{
notice: 'circle-check',
diff --git a/lib/rolemodel/utility.rb b/lib/rolemodel/utility.rb
new file mode 100644
index 00000000..79df8b3a
--- /dev/null
+++ b/lib/rolemodel/utility.rb
@@ -0,0 +1,3 @@
+module Rolemodel::Utility
+ autoload :TaskTools, 'rolemodel/utility/task_tools'
+end
diff --git a/lib/rolemodel/utility/task_tools.rb b/lib/rolemodel/utility/task_tools.rb
new file mode 100644
index 00000000..c89f33c3
--- /dev/null
+++ b/lib/rolemodel/utility/task_tools.rb
@@ -0,0 +1,42 @@
+require 'benchmark'
+
+module Rolemodel
+ module Utility
+ module TaskTools
+ PROGRESS = %w[⠏ ⠇ ⠧ ⠦ ⠴ ⠼ ⠸ ⠹ ⠙ ⠋].cycle
+
+ # based on the migration helper of the same name
+ def say_with_time(message, &)
+ say message
+ time = Benchmark.measure(&)
+ say '%.4fs' % time.real, subitem: true
+ end
+
+ def say(message, subitem: false)
+ puts "#{subitem ? ' ->' : '--'} #{message}" # rubocop:disable Rails/Output
+ end
+
+ ##
+ # Indicate the progress of a long-running process
+ #
+ # Usage (with a known total):
+ # 100.times do |i|
+ # indicate_progress(i, 100)
+ # end
+ #
+ # Only update every 'report_interval' iteration for eye-trackable animation speed
+ # Also displays a completion percentage if a total is provided
+ def indicate_progress(index, total = nil, report_interval: 9)
+ return unless (index % report_interval).zero?
+
+ print("#{PROGRESS.next} #{to_percent(index, total) if total}\r") # rubocop:disable Rails/Output
+ end
+
+ private
+
+ def to_percent(index, total)
+ '%3.f%%' % (index / total.to_f * 100.0)
+ end
+ end
+ end
+end
diff --git a/lib/rolemodel/version.rb b/lib/rolemodel/version.rb
index 1a8d23a7..1e0fd13e 100644
--- a/lib/rolemodel/version.rb
+++ b/lib/rolemodel/version.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Rolemodel
- VERSION = '1.1.0'
+ VERSION = '2.0.0'
end
diff --git a/rolemodel-rails.gemspec b/rolemodel-rails.gemspec
index 86b7962e..d132406d 100644
--- a/rolemodel-rails.gemspec
+++ b/rolemodel-rails.gemspec
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib']
spec.add_dependency 'rails', '> 7.1'
+ spec.add_dependency 'benchmark'
spec.add_development_dependency 'bundler', '~> 4'
spec.add_development_dependency 'generator_spec', '~> 0.10'
diff --git a/spec/generators/rolemodel/optics/icons_generator_spec.rb b/spec/generators/rolemodel/optics/icons_generator_spec.rb
index a5a0da6b..f2704cdc 100644
--- a/spec/generators/rolemodel/optics/icons_generator_spec.rb
+++ b/spec/generators/rolemodel/optics/icons_generator_spec.rb
@@ -5,20 +5,36 @@
run_generator_against_test_app
end
- it 'adds the correct helper and builders' do
- assert_file 'app/helpers/icon_helper.rb'
- assert_file 'app/icon_builders/icon_builder.rb'
- assert_file 'app/icon_builders/material_icon_builder.rb'
+ it 'adds the correct helper' do
+ assert_file 'app/helpers/icon_helper.rb' do |helper|
+ assert_instance_method :icon, helper
+ assert_match(/MaterialIconBuilder/, helper)
+ end
end
end
context 'selecting an alternate icon library via command line option' do
before { run_generator_against_test_app(['--phosphor']) }
- it 'adds the correct helper and builders' do
- assert_file 'app/helpers/icon_helper.rb'
- assert_file 'app/icon_builders/icon_builder.rb'
- assert_file 'app/icon_builders/phosphor_icon_builder.rb'
+ it 'adds the correct helper' do
+ assert_file 'app/helpers/icon_helper.rb' do |helper|
+ assert_instance_method :icon, helper
+ assert_match(/PhosphorIconBuilder/, helper)
+ end
+ end
+ end
+
+ context 'selecting an alternate icon library via prompt' do
+ before do
+ respond_to_prompt with: 'feather' # choose an icon library
+ run_generator_against_test_app
+ end
+
+ it 'adds the correct helper' do
+ assert_file 'app/helpers/icon_helper.rb' do |helper|
+ assert_instance_method :icon, helper
+ assert_match(/FeatherIconBuilder/, helper)
+ end
end
end
@@ -27,16 +43,38 @@
assert_no_file 'app/helpers/icon_helper.rb'
run_generator_against_test_app(['--tabler'])
- assert_file 'app/helpers/icon_helper.rb'
- assert_file 'app/icon_builders/tabler_icon_builder.rb'
+ assert_file 'app/helpers/icon_helper.rb' do |helper|
+ assert_instance_method :icon, helper
+ assert_match(/TablerIconBuilder/, helper)
+ assert_no_match(/PhosphorIconBuilder/, helper)
+ end
- respond_to_prompt with: 'feather' # choose an icon library
- run_generator_against_test_app
- assert_file 'app/helpers/icon_helper.rb'
- assert_file 'app/icon_builders/feather_icon_builder.rb'
+ run_generator_against_test_app(['--lucide'])
+ assert_file 'app/helpers/icon_helper.rb' do |helper|
+ assert_instance_method :icon, helper
+ assert_match(/LucideIconBuilder/, helper)
+ assert_no_match(/TablerIconBuilder/, helper)
+ end
+ end
+ end
+
+ context 'installing builders' do
+ before do
+ run_generator_against_test_app(['--phosphor', '--install-builders'])
+ end
+
+ it 'copies the base IconBuilder and the chosen library builder to the app lib directory' do
+ assert_file 'lib/rolemodel/optics/icon_builder.rb'
+ assert_file 'lib/rolemodel/optics/phosphor_icon_builder.rb'
+ end
+ end
+
+ context 'not installing builders (default)' do
+ before { run_generator_against_test_app(['--phosphor']) }
- assert_no_file 'app/icon_builders/tabler_icon_builder.rb'
- assert_no_file 'app/icon_builders/material_icon_builder.rb'
+ it 'does not copy any builder files to the app lib directory' do
+ assert_no_file 'lib/rolemodel/optics/icon_builder.rb'
+ assert_no_file 'lib/rolemodel/optics/phosphor_icon_builder.rb'
end
end
end
diff --git a/spec/helpers/icon_builder_spec.rb b/spec/helpers/icon_builder_spec.rb
new file mode 100644
index 00000000..403c0246
--- /dev/null
+++ b/spec/helpers/icon_builder_spec.rb
@@ -0,0 +1,21 @@
+require 'action_view'
+require 'erb'
+require 'rolemodel/optics'
+
+
+RSpec.describe Rolemodel::Optics::MaterialIconBuilder, type: :helper do
+ include ActionView::Helpers
+
+ let(:builder) { Rolemodel::Optics::MaterialIconBuilder.new('home', **options) }
+
+ describe 'default options' do
+ let(:options) { {} }
+ it 'renders an icon with the correct class' do
+ expect(builder.build).to eq('home')
+ end
+ end
+
+ describe 'hover_text'
+ describe 'color'
+ describe 'additional_classes'
+end