diff --git a/lib/class_variants.rb b/lib/class_variants.rb index 274ba55..e4e3663 100644 --- a/lib/class_variants.rb +++ b/lib/class_variants.rb @@ -5,8 +5,8 @@ module ClassVariants class << self - def build(*args, **kwargs) - Instance.new(*args, **kwargs) + def build(...) + Instance.new(...) end end end diff --git a/lib/class_variants/instance.rb b/lib/class_variants/instance.rb index 9f74e4b..7b06560 100644 --- a/lib/class_variants/instance.rb +++ b/lib/class_variants/instance.rb @@ -1,36 +1,29 @@ module ClassVariants class Instance - attr_reader :base, :variants, :compoundVariants, :defaults - # rubocop:disable Naming/VariableName - def initialize(classes = nil, base: nil, variants: {}, compoundVariants: [], defaults: {}) - warn <<~MSG if classes - (ClassVariants) DEPRECATION WARNING: Use of positional argument for default classes is deprecated - and will be removed in the next major version. Use the `base` keyword argument instead. - MSG + def initialize(**options, &block) + raise ArgumentError, "Use of hash config and code block is not supported" if !options.empty? && block_given? + + @base = options.empty? ? {} : {default: options.fetch(:base, nil)} + @variants = expand_variants(options.fetch(:variants, {})) + expand_compound_variants(options.fetch(:compoundVariants, [])) + @defaults = options.fetch(:defaults, {}) - @base = base || classes - @variants = expand_boolean_variants(variants) - @compoundVariants = compoundVariants - @defaults = defaults + instance_eval(&block) if block_given? end # rubocop:enable Naming/VariableName - def render(**overrides) + def render(slot = :default, **overrides) # Start with our default classes - result = [@base] + result = [@base[slot]] # Then merge the passed in overrides on top of the defaults - selected = @defaults.merge(overrides) + criteria = @defaults.merge(overrides) - selected.each do |variant_type, variant| - # dig the classes out and add them to the result - result << @variants.dig(variant_type, variant) - end + @variants.each do |candidate| + next unless candidate[:slot] == slot - @compoundVariants.each do |compound_variant| - if (compound_variant.keys - [:class]).all? { |key| selected[key] == compound_variant[key] } - result << compound_variant[:class] + if (candidate.keys - [:class, :slot]).all? { |key| criteria[key] == candidate[key] } + result << candidate[:class] end end @@ -43,19 +36,62 @@ def render(**overrides) private - def expand_boolean_variants(variants) - expanded = variants.map do |key, value| - case value + def base(klass = nil, &block) + raise ArgumentError, "Use of positional argument and code block is not supported" if klass && block_given? + + if block_given? + with_slots(&block).each do |slot| + @base[slot[:slot]] = slot[:class] + end + else + @base[:default] = klass + end + end + + def variant(**options, &block) + raise ArgumentError, "Use of class option and code block is not supported" if options.key?(:class) && block_given? + + if block_given? + with_slots(&block).each do |slot| + @variants << options.merge(slot) + end + else + @variants << options.merge(slot: :default) + end + end + + def defaults(**options) + @defaults = options + end + + def slot(name = :default, **options) + raise ArgumentError, "class option is required" unless options.key?(:class) + + @slots << options.merge(slot: name) + end + + def with_slots + @slots = [] + yield + @slots + end + + def expand_variants(variants) + variants.flat_map do |property, values| + case values when String - s_key = key.to_s - {s_key.delete_prefix("!").to_sym => {!s_key.start_with?("!") => value}} + {property.to_s.delete_prefix("!").to_sym => !property.to_s.start_with?("!"), :class => values, :slot => :default} else - {key => value} + values.map do |key, value| + {property => key, :class => value, :slot => :default} + end end end + end - expanded.reduce do |output, next_variant| - output.merge!(next_variant) { |_key, v1, v2| v1.merge!(v2) } + def expand_compound_variants(compound_variants) + compound_variants.map do |compound_variant| + compound_variant.merge(slot: :default) end end end diff --git a/test/block_test.rb b/test/block_test.rb new file mode 100644 index 0000000..a4284ae --- /dev/null +++ b/test/block_test.rb @@ -0,0 +1,51 @@ +require "test_helper" + +class BlockTest < Minitest::Test + def setup + @cv = ClassVariants.build do + base "text-white py-1 px-3 rounded-full" + + variant color: :primary, class: "bg-blue-500" + variant color: :secondary, class: "bg-purple-500" + variant color: :success, class: "bg-green-500" + + variant size: :sm, class: "py-1 px-3 text-xs" + variant size: :md, class: "py-1.5 px-4 text-sm" + variant size: :lg, class: "py-2 px-6 text-md" + + variant disabled: true, class: "opacity-50 bg-gray-500" + variant visible: false, class: "hidden" + + variant color: :success, disabled: true, class: "bg-green-100 text-green-700" + + defaults size: :sm + end + end + + def test_render_with_defaults + assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render + end + + def test_render_with_size + assert_equal "text-white py-1 px-3 rounded-full py-1.5 px-4 text-sm", @cv.render(size: :md) + end + + def test_render_with_size_and_color + assert_equal( + "text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs", + @cv.render(size: :sm, color: :success) + ) + end + + def test_boolean_variants + assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render(visible: true) + assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs hidden", @cv.render(visible: false) + end + + def test_compound_variants + assert_equal( + "text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs opacity-50 bg-gray-500 bg-green-100 text-green-700", + @cv.render(color: :success, disabled: true) + ) + end +end diff --git a/test/class_variants_test.rb b/test/class_variants_test.rb deleted file mode 100644 index 285cd1e..0000000 --- a/test/class_variants_test.rb +++ /dev/null @@ -1,57 +0,0 @@ -require "test_helper" - -class ClassVariantsTest < Minitest::Test - def setup - @cv = ClassVariants.build( - base: "rounded border", - variants: { - size: { - sm: "text-sm", - md: "text-md" - }, - color: { - red: "text-red", - green: "text-green" - }, - ring: "focus:ring-2", - visible: "inline-block", - "!visible": "hidden" - }, - compoundVariants: [ - {ring: true, color: :red, class: "focus:ring-red-600"}, - {ring: true, color: :green, class: "focus:ring-green-600"} - ], - defaults: { - size: :md - } - ) - end - - def test_base_with_defaults - assert_equal "rounded border text-md", @cv.render - end - - def test_base_with_defaults_overwrite - assert_equal "rounded border text-sm", @cv.render(size: :sm) - end - - def test_base_with_defaults_overwrite_and_add - assert_equal "rounded border text-sm text-green", @cv.render(size: :sm, color: :green) - end - - def test_boolean_variants - assert_equal "rounded border text-md inline-block", @cv.render(visible: true) - assert_equal "rounded border text-md hidden", @cv.render(visible: false) - end - - def test_compound_variants - assert_equal( - "rounded border text-md focus:ring-2 text-red focus:ring-red-600", - @cv.render(ring: true, color: :red) - ) - assert_equal( - "rounded border text-md focus:ring-2 text-green focus:ring-green-600", - @cv.render(ring: true, color: :green) - ) - end -end diff --git a/test/hash_test.rb b/test/hash_test.rb new file mode 100644 index 0000000..655f4b5 --- /dev/null +++ b/test/hash_test.rb @@ -0,0 +1,56 @@ +require "test_helper" + +class HashTest < Minitest::Test + def setup + @cv = ClassVariants.build( + base: "text-white py-1 px-3 rounded-full", + variants: { + color: { + primary: "bg-blue-500", + secondary: "bg-purple-500", + success: "bg-green-500" + }, + size: { + sm: "py-1 px-3 text-xs", + md: "py-1.5 px-4 text-sm", + lg: "py-2 px-6 text-md" + }, + disabled: "opacity-50 bg-gray-500", + "!visible": "hidden" + }, + compoundVariants: [ + {color: :success, disabled: true, class: "bg-green-100 text-green-700"} + ], + defaults: { + size: :sm + } + ) + end + + def test_render_with_defaults + assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render + end + + def test_render_with_size + assert_equal "text-white py-1 px-3 rounded-full py-1.5 px-4 text-sm", @cv.render(size: :md) + end + + def test_render_with_size_and_color + assert_equal( + "text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs", + @cv.render(size: :sm, color: :success) + ) + end + + def test_boolean_variants + assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render(visible: true) + assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs hidden", @cv.render(visible: false) + end + + def test_compound_variants + assert_equal( + "text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs opacity-50 bg-gray-500 bg-green-100 text-green-700", + @cv.render(color: :success, disabled: true) + ) + end +end diff --git a/test/slot_test.rb b/test/slot_test.rb new file mode 100644 index 0000000..c1b2c96 --- /dev/null +++ b/test/slot_test.rb @@ -0,0 +1,69 @@ +require "test_helper" + +class SlotTest < Minitest::Test + def setup + @cv = ClassVariants.build do + base do + slot :root, class: "rounded py-3 px-5 mb-4" + slot :title, class: "font-bold mb-1" + end + + variant variant: :outlined do + slot :root, class: "border" + end + + variant variant: :outlined, severity: :error do + slot :root, class: "border-red-700 dark:border-red-500" + slot :title, class: "text-red-700 dark:text-red-500" + slot :message, class: "text-red-600 dark:text-red-500" + end + + variant variant: :outlined, severity: :success do + slot :root, class: "border-green-700 dark:border-green-500" + slot :title, class: "text-green-700 dark:text-green-500" + slot :message, class: "text-green-600 dark:text-green-500" + end + + variant variant: :filled, severity: :error do + slot :root, class: "bg-red-100 dark:bg-red-800" + slot :title, class: "text-red-900 dark:text-red-50" + slot :message, class: "text-red-700 dark:text-red-200" + end + + variant variant: :filled, severity: :success do + slot :root, class: "bg-green-100 dark:bg-green-800" + slot :title, class: "text-green-900 dark:text-green-50" + slot :message, class: "text-green-700 dark:text-green-200" + end + + defaults variant: :filled, severity: :success + end + end + + def test_render_default_slot + assert_equal "", @cv.render + end + + def test_render_nonexistent_slot + assert_equal "", @cv.render(:nonexistent) + end + + def test_render_slot_with_defaults + assert_equal "rounded py-3 px-5 mb-4 bg-green-100 dark:bg-green-800", @cv.render(:root) + end + + def test_render_slot_with_variant + assert_equal "rounded py-3 px-5 mb-4 border border-green-700 dark:border-green-500", @cv.render(:root, variant: :outlined) + end + + def test_render_slot_without_base + assert_equal "text-green-700 dark:text-green-200", @cv.render(:message) + end + + def test_render_slot_with_unused_variant + assert_equal( + "rounded py-3 px-5 mb-4 border border-green-700 dark:border-green-500", + @cv.render(:root, variant: :outlined, type: :button) + ) + end +end