daisyui 2.1.1 → 2.2.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.
@@ -13,6 +13,10 @@ module DaisyUI
13
13
  that the Ruby and templates under each PATH use, plus the components those
14
14
  render. @import the output from your Tailwind entry.
15
15
 
16
+ With --app, prints the sources of the app's own components (DaisyUI::Base
17
+ subclasses, after requiring each --require FILE) instead: their variants,
18
+ compounds and state-prefixed classes, for a file you commit.
19
+
16
20
  TEXT
17
21
 
18
22
  def self.run(argv, out: $stdout, err: $stderr) = new(out:, err:).run(argv)
@@ -25,16 +29,16 @@ module DaisyUI
25
29
  def run(argv)
26
30
  require "optparse"
27
31
 
28
- options = { components: [], format: "css", responsive: [] }
32
+ options = { components: [], format: "css", responsive: [], require: [] }
29
33
  parser = parser(options)
30
34
  paths = parser.parse(argv)
31
35
  return help(parser) if options[:help]
32
- return usage(parser) if paths.empty? && options[:components].empty?
36
+ return usage(parser) if paths.empty? && options[:components].empty? && !options[:app]
33
37
 
34
- result = Tailwind.sources(scan: paths, **options.slice(:components, :format, :responsive, :prefix))
38
+ result = Tailwind.sources(scan: paths, **sources_options(options))
35
39
  options[:output] ? write(options[:output], result) : @out.write(result)
36
40
  0
37
- rescue OptionParser::ParseError, ArgumentError, SystemCallError => e
41
+ rescue OptionParser::ParseError, ArgumentError, SystemCallError, LoadError => e
38
42
  @err.puts "daisyui-tailwind: #{e.message}"
39
43
  1
40
44
  end
@@ -53,10 +57,23 @@ module DaisyUI
53
57
  options[:responsive].concat(breakpoints)
54
58
  end
55
59
  opts.on("--prefix PREFIX", "The DaisyUI.configuration.prefix your app sets") { |prefix| options[:prefix] = prefix }
60
+ opts.on("--app", "The app's own components instead of the gem's") { options[:app] = true }
61
+ opts.on("-r", "--require FILE", "With --app: load FILE first, e.g. config/environment.rb (repeatable)") do |file|
62
+ options[:require] << file
63
+ end
56
64
  opts.on("-h", "--help", "Show this help") { options[:help] = true }
57
65
  end
58
66
  end
59
67
 
68
+ # With --app: load the app's files, then freeze DaisyUI as boot does.
69
+ def sources_options(options)
70
+ if options[:app]
71
+ options[:require].each { |file| require File.expand_path(file) }
72
+ DaisyUI.finalize!
73
+ end
74
+ options.slice(:components, :format, :responsive, :prefix, :app)
75
+ end
76
+
60
77
  def help(parser)
61
78
  @out.puts parser
62
79
  0
@@ -22,6 +22,16 @@ module DaisyUI
22
22
 
23
23
  attr_reader :components
24
24
 
25
+ # @api private Shared with AppSources.
26
+ def self.validate_options!(breakpoints, prefix)
27
+ breakpoints.each do |breakpoint|
28
+ raise ArgumentError, "invalid breakpoint #{breakpoint.inspect}: give the variant name, e.g. sm or @md" unless BREAKPOINT.match?(breakpoint)
29
+ end
30
+ return if prefix.empty? || PREFIX.match?(prefix)
31
+
32
+ raise ArgumentError, "invalid prefix #{prefix.inspect}"
33
+ end
34
+
25
35
  def initialize(manifest, components, responsive: [], prefix: nil)
26
36
  @manifest = manifest
27
37
  @components = components
@@ -90,14 +100,7 @@ module DaisyUI
90
100
 
91
101
  def inline(classes) = %(@source inline("#{classes.join(' ')}");)
92
102
 
93
- def validate!
94
- @breakpoints.each do |breakpoint|
95
- raise ArgumentError, "invalid breakpoint #{breakpoint.inspect}: give the variant name, e.g. sm or @md" unless BREAKPOINT.match?(breakpoint)
96
- end
97
- return if @prefix.empty? || PREFIX.match?(@prefix)
98
-
99
- raise ArgumentError, "invalid prefix #{@prefix.inspect}"
100
- end
103
+ def validate! = self.class.validate_options!(@breakpoints, @prefix)
101
104
  end
102
105
  end
103
106
  end
@@ -37,11 +37,55 @@ module DaisyUI
37
37
  # components. `responsive:` adds breakpoint variants (`%w[sm md]`) of the
38
38
  # classes `responsive:` renders; `prefix:` defaults to the
39
39
  # DaisyUI.configuration.prefix the components render with.
40
- def sources(scan: [], components: [], format: :css, responsive: [], prefix: DaisyUI.configuration.prefix)
40
+ #
41
+ # `app: true` gives the app's own components instead (app_components),
42
+ # or `app: [Widget, …]` the ones named: see AppSources. Their file is
43
+ # separate from the gem components' and meant to be committed.
44
+ def sources(scan: [], components: [], app: false, format: :css, responsive: [], prefix: DaisyUI.configuration.prefix)
45
+ return app_sources(app, scan:, components:).new(app == true ? app_components : app, responsive:, prefix:).render(format) if app
46
+
41
47
  manifest = self.manifest
42
48
  used = used_components(scan:, components:, manifest:)
43
49
  Sources.new(manifest, used, responsive:, prefix:).render(format)
44
50
  end
51
+
52
+ # The app's DaisyUI::Base subclasses, sorted by name: every named one
53
+ # whose constant still names it (a class a code reload replaced is
54
+ # skipped), defined outside the gem's lib/ or a gem component the app
55
+ # reopened to declare variants (the gem's own never do). A Rails app is
56
+ # eager-loaded first.
57
+ def app_components
58
+ ::Rails.application.eager_load! if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
59
+ ClassMerge::DaisyGroups.components.select { |component| current?(component) && app_owned?(component) }.sort_by(&:name)
60
+ end
61
+
62
+ # Whether the committed app sources at `path` differ from what the
63
+ # declarations give now (or the file is missing): a spec can fail on it,
64
+ # the way the gem's own manifest spec does.
65
+ def stale?(path, app: true, **)
66
+ !File.file?(path) || File.read(path) != sources(app:, **)
67
+ end
68
+
69
+ private
70
+
71
+ def app_sources(app, scan:, components:)
72
+ return AppSources if scan.empty? && components.empty?
73
+
74
+ raise ArgumentError, "app: gives the app's own components, a file of its own; " \
75
+ "scan: and components: give the gem's (#{app.inspect} with #{(scan + components).inspect})"
76
+ end
77
+
78
+ def app_owned?(component)
79
+ return true unless Object.const_source_location(component.name)&.first.to_s.start_with?(File.join(ROOT, "lib", ""))
80
+
81
+ !component.variant_definitions.equal?(Variants::EMPTY)
82
+ end
83
+
84
+ def current?(component)
85
+ Object.const_get(component.name).equal?(component)
86
+ rescue NameError
87
+ false
88
+ end
45
89
  end
46
90
  end
47
91
  end
@@ -23,9 +23,16 @@ module DaisyUI
23
23
  name = "#{name}##{part}" if part
24
24
  message = "#{name} has no modifier #{modifier.inspect}"
25
25
  suggestions = DidYouMean::SpellChecker.new(dictionary: known.map(&:to_s)).correct(modifier.to_s)
26
- return message if suggestions.empty?
26
+ message = "#{message}. Did you mean? #{suggestions.map { |s| ":#{s}" }.join(', ')}" unless suggestions.empty?
27
+ groups.empty? ? message : "#{message}. Variants: #{groups}"
28
+ end
29
+
30
+ # The component's groups and their members, "size: sm, lg; color: …";
31
+ # a sub-component part has modifiers of its own, so none for it.
32
+ def groups
33
+ return "" if part || !component.respond_to?(:modifier_groups)
27
34
 
28
- "#{message}. Did you mean? #{suggestions.map { |s| ":#{s}" }.join(', ')}"
35
+ component.modifier_groups.map { |group, members| "#{group}: #{members.join(', ')}" }.join("; ")
29
36
  end
30
37
  end
31
38
  end
@@ -3,5 +3,5 @@
3
3
  module DaisyUI
4
4
  # This timestamp is automatically updated when releasing a new version
5
5
  # Format: YYYY-MM-DD HH:MM:SS UTC
6
- UPDATED_AT = "2026-10-17 07:27:19 UTC"
6
+ UPDATED_AT = "2026-10-27 04:51:27 UTC"
7
7
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DaisyUI
4
+ class Variants
5
+ # What a `compound` or `on` match means, and the checks that make a typo
6
+ # fail where it is declared (at boot) rather than never match:
7
+ #
8
+ # | value | matches when |
9
+ # |------------------|-----------------------------------------------------|
10
+ # | `:member` | that member is selected (given, defaulted or set) |
11
+ # | `[:a, :b]` | any of them is; a `nil` in the list means "or none" |
12
+ # | `nil` / `false` | no member of that group is |
13
+ # | `true` | any member of that group is |
14
+ # | `true` / `false` | a flag is on / off |
15
+ #
16
+ # @api private
17
+ class Match
18
+ def initialize(klass)
19
+ @klass = klass
20
+ end
21
+
22
+ # `selection` is { group => member or nil }, `on` the flags given.
23
+ def self.holds?(match, selection, on)
24
+ match.all? do |key, value|
25
+ if selection.key?(key)
26
+ member = selection[key]
27
+ case value
28
+ when true then !member.nil?
29
+ when false, nil then member.nil?
30
+ when Array then value.include?(member)
31
+ else member == value
32
+ end
33
+ else
34
+ on.include?(key) == value
35
+ end
36
+ end
37
+ end
38
+
39
+ def match!(match)
40
+ match.each do |key, value|
41
+ if groups.key?(key)
42
+ value_members(value).each { |member| member!(key, member) }
43
+ elsif flags.include?(key)
44
+ raise ArgumentError, "#{owner}: flag #{key.inspect} matches true or false, got #{value.inspect}" unless BOOLS.include?(value)
45
+ else
46
+ raise ArgumentError, "#{owner} has no variant or flag #{key.inspect}"
47
+ end
48
+ end
49
+ end
50
+
51
+ def member!(group, member)
52
+ raise ArgumentError, "#{owner} has no variant #{group.inspect}" unless groups.key?(group)
53
+ return if groups[group].include?(member)
54
+
55
+ raise ArgumentError, "#{owner}: #{group.inspect} has no member #{member.inspect}; #{group}: #{groups[group].join(', ')}"
56
+ end
57
+
58
+ def state!(name)
59
+ return if @klass.variant_definitions[:states].key?(name)
60
+
61
+ raise ArgumentError, "#{owner} has no state #{name.inspect}: declare it with `state #{name.inspect}, \"<variant>\"` first"
62
+ end
63
+
64
+ def slot!(name)
65
+ return if name == :root || @klass.variant_definitions[:slots].key?(name)
66
+
67
+ raise ArgumentError, "#{owner} has no slot #{name.inspect}: declare it with `slot #{name.inspect}` first"
68
+ end
69
+
70
+ private
71
+
72
+ BOOLS = [true, false].freeze
73
+ private_constant :BOOLS
74
+
75
+ def value_members(value)
76
+ return [] if value.nil? || value == true || value == false
77
+
78
+ Array(value).compact
79
+ end
80
+
81
+ def groups = @klass.modifier_groups
82
+
83
+ # Ungrouped modifiers and registered flags.
84
+ def flags = Schema.flags(@klass)
85
+
86
+ def owner = @klass.name || @klass.inspect
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DaisyUI
4
+ class Variants
5
+ # The instance side of the Variants API, included in DaisyUI::Base. A
6
+ # component without declarations (every gem component) skips it all.
7
+ module Rendering
8
+ private
9
+
10
+ # The `base` classes a `variants` block declared.
11
+ def variant_base_classes
12
+ apply_prefix(self.class.variant_definitions[:base])
13
+ end
14
+
15
+ # For `slot` (:root is the component itself): the classes of the
16
+ # members a default or compound selected (the root's given members are
17
+ # modifier_classes already), of every compound that holds, and of every
18
+ # `on` rule that holds, prefixed with its states.
19
+ def variant_classes(slot = :root)
20
+ definitions = self.class.variant_definitions
21
+ return [] if definitions.equal?(EMPTY)
22
+
23
+ resolution = variant_resolution
24
+ [
25
+ *variant_member_classes(resolution, slot, definitions),
26
+ *resolution.compounds.map { |compound| apply_prefix(compound[:class][slot]) },
27
+ *resolution.rules.flat_map { |rule| state_classes(rule, slot, definitions[:states]) }
28
+ ].compact
29
+ end
30
+
31
+ # Classes for one of the component's parts: the slot's own, those its
32
+ # variants, compounds and `on` rules give it, then `options[:class]`
33
+ # (removed from `options`, as component_classes does).
34
+ def slot_classes(name, options = self.options)
35
+ slots = self.class.variant_definitions[:slots]
36
+ raise ArgumentError, "#{self.class.name || self.class.inspect} has no slot #{name.inspect}" unless slots.key?(name)
37
+
38
+ merge_classes(apply_prefix(slots[name]), *variant_classes(name), options.delete(:class))
39
+ end
40
+
41
+ # `options` with the slot's classes, to splat into the part.
42
+ def slot_options(name, options = self.options)
43
+ classes = slot_classes(name, options)
44
+ classes ? options.merge(class: classes) : options
45
+ end
46
+
47
+ def variant_resolution
48
+ @variant_resolution ||= Resolution.new(self.class, modifiers)
49
+ end
50
+
51
+ def variant_member_classes(resolution, slot, definitions)
52
+ if slot == :root
53
+ resolution.implied_members.map { |member| apply_prefix(modifier_map[member]) }
54
+ else
55
+ resolution.active.map { |member| apply_prefix(definitions[:slot_classes].dig(member, slot)) }
56
+ end
57
+ end
58
+
59
+ def state_classes(rule, slot, states)
60
+ return [] unless (classes = rule[:class][slot])
61
+
62
+ rule[:states].flat_map do |state|
63
+ classes.split.map { |token| "#{states.fetch(state)}:#{apply_prefix(token)}" }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DaisyUI
4
+ class Variants
5
+ # Which members, compounds and `on` rules hold for one instance, worked
6
+ # out explicitly rather than left to ClassMerge, so a default or a set
7
+ # member never renders beside the member the caller gave, with class
8
+ # merging on or off:
9
+ #
10
+ # 1. the caller's members (the last one per group, as ClassMerge keeps)
11
+ # 2. defaults, for groups the caller left empty
12
+ # 3. compounds in declaration order; each one's `set` fills or replaces a
13
+ # member the caller did not name, and later compounds see it
14
+ # 4. `on` rules, against the final selection
15
+ #
16
+ # @api private
17
+ class Resolution
18
+ attr_reader :compounds, :rules
19
+
20
+ def initialize(klass, given)
21
+ definitions = klass.variant_definitions
22
+ @given = given
23
+ @selection = klass.modifier_groups.to_h { |group, members| [group, given.reverse.find { |m| members.include?(m) }] }
24
+ @named = @selection.compact.keys
25
+ definitions[:defaults].each { |group, member| @selection[group] ||= member }
26
+ @compounds = matching_compounds(definitions[:compounds])
27
+ @rules = definitions[:rules].select { |rule| holds?(rule[:match]) }
28
+ end
29
+
30
+ # Members a default or a compound selected, not the caller.
31
+ def implied_members
32
+ @selection.filter_map { |group, member| member unless @named.include?(group) }
33
+ end
34
+
35
+ # Every member and flag in effect, for the slots' per-member classes.
36
+ def active = (@given | implied_members)
37
+
38
+ private
39
+
40
+ # One at a time: a compound's `set` is seen by the compounds after it.
41
+ def matching_compounds(compounds)
42
+ compounds.each_with_object([]) do |compound, matched|
43
+ next unless holds?(compound[:match])
44
+
45
+ matched << compound
46
+ compound[:set].each { |group, member| @selection[group] = member unless @named.include?(group) }
47
+ end
48
+ end
49
+
50
+ def holds?(match) = Match.holds?(match, @selection, @given)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DaisyUI
4
+ class Variants
5
+ # `Component.variants.schema`: a component's declarations as plain,
6
+ # frozen data. A component that only calls `register_modifiers` gets its
7
+ # variants and flags, with the rest empty.
8
+ #
9
+ # @api private
10
+ class Schema
11
+ # Ungrouped modifiers and the flags registered without a class.
12
+ def self.flags(klass)
13
+ grouped = klass.modifier_groups.values.flatten
14
+ ((klass.modifiers || {}).keys - grouped) | klass.flags
15
+ end
16
+
17
+ def initialize(klass)
18
+ @klass = klass
19
+ @definitions = klass.variant_definitions
20
+ end
21
+
22
+ def to_h
23
+ Shareable.share(
24
+ base:,
25
+ variants: @klass.modifier_groups.transform_values(&:dup),
26
+ flags: self.class.flags(@klass),
27
+ defaults: @definitions[:defaults].dup,
28
+ compounds: @definitions[:compounds].map { |entry| { match: entry[:match], class: shown(entry[:class]), set: entry[:set] } },
29
+ on: @definitions[:rules].map { |entry| { states: entry[:states], match: entry[:match], class: shown(entry[:class]) } },
30
+ states: @definitions[:states].dup,
31
+ slots: { root: base, **@definitions[:slots] }
32
+ )
33
+ end
34
+
35
+ # Every class token the declarations can render, `prefix` included the
36
+ # way Base#apply_prefix adds it (after any state), sorted.
37
+ def literal_classes(prefix)
38
+ plain = [base, *modifier_classes, *@definitions[:slots].values, *slot_member_classes, *compound_classes]
39
+ plain = plain.compact.flat_map(&:split).map { |token| "#{prefix}#{token}" }
40
+ (plain + state_classes(prefix)).uniq.sort
41
+ end
42
+
43
+ private
44
+
45
+ def base
46
+ classes = [@klass.component_class&.to_s, @definitions[:base]].compact.join(" ")
47
+ classes.empty? ? nil : classes
48
+ end
49
+
50
+ def modifier_classes = (@klass.modifiers || {}).values
51
+
52
+ def slot_member_classes = @definitions[:slot_classes].values.flat_map(&:values)
53
+
54
+ def compound_classes = @definitions[:compounds].flat_map { |entry| entry[:class].values }
55
+
56
+ def state_classes(prefix)
57
+ @definitions[:rules].flat_map do |rule|
58
+ tokens = rule[:class].values.flat_map(&:split)
59
+ rule[:states].flat_map do |state|
60
+ tokens.map { |token| "#{@definitions[:states].fetch(state)}:#{prefix}#{token}" }
61
+ end
62
+ end
63
+ end
64
+
65
+ # A root-only class shows as its String, per-slot classes as the Hash.
66
+ def shown(classes)
67
+ return nil if classes.empty?
68
+
69
+ classes.keys == [:root] ? classes[:root] : classes
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DaisyUI
4
+ # The declarative layer over `register_modifiers`: compound looks, defaults,
5
+ # browser states and parts (slots), declared once in the class body.
6
+ #
7
+ # class Widget < DaisyUI::Button
8
+ # variants do
9
+ # base "font-medium"
10
+ # variant :size, sm: "btn-sm [--fontsize:var(--text-sm)]"
11
+ # default :size, :md
12
+ # compound style: :outline, color: nil, class: "[--btn-bg:var(--color-base-100)]"
13
+ # state :hover, "hover"
14
+ # on :hover, style: %i[ghost link], class: "[--btn-bg:transparent]"
15
+ # end
16
+ # end
17
+ #
18
+ # `variant` and `flag` write through `register_modifiers` (so ClassMerge
19
+ # groups, inheritance and finalize! stay as they are); everything else lands
20
+ # in the class's `variant_definitions`, a frozen Hash replaced on every
21
+ # write. `Widget.variants.schema` describes it all as plain data. Matching,
22
+ # validation and rendering live in Variants::Match, Variants::Resolution and
23
+ # Variants::Rendering.
24
+ class Variants
25
+ EMPTY = Shareable.make(
26
+ { base: nil, defaults: {}, compounds: [], rules: [], states: {}, slots: {}, slot_classes: {} }
27
+ )
28
+ STATE_PREFIX = /\A[^\s"'`;{}\\]+\z/
29
+
30
+ # The DSL for `klass`, with `block` evaluated against it.
31
+ def self.build(klass, &block)
32
+ new(klass).tap { |dsl| dsl.instance_exec(&block) if block }
33
+ end
34
+
35
+ def initialize(klass)
36
+ @klass = klass
37
+ end
38
+
39
+ # Classes every instance gets, after the inherited base.
40
+ def base(classes)
41
+ write(base: [definitions[:base], classes].compact.join(" "))
42
+ end
43
+
44
+ # A group of alternatives: `variant :size, sm: "btn-sm", lg: "btn-lg"`. A
45
+ # Hash value gives classes per slot (`sm: { root: "…", menu: "…" }`).
46
+ def variant(group, **members)
47
+ register(group => members.transform_values { |classes| root_classes(classes) })
48
+ members.each { |member, classes| write_slot_classes(member, classes) }
49
+ self
50
+ end
51
+
52
+ # An on/off modifier: `flag :wide, "btn-wide"`, or a flag with no class of
53
+ # its own (`flag :collapsed`) that compounds and `on` rules can match.
54
+ def flag(name, classes = nil)
55
+ return tap { @klass.register_flags(name) } if classes.nil?
56
+
57
+ register(name => root_classes(classes))
58
+ write_slot_classes(name, classes)
59
+ self
60
+ end
61
+
62
+ # The member used when the caller names no member of `group`.
63
+ def default(group, member)
64
+ check.member!(group, member)
65
+ write(defaults: definitions[:defaults].merge(group => member))
66
+ end
67
+
68
+ # Adds `class:` and sets the variants in `set:` (members the caller did
69
+ # not name) when every key of `match` holds. See Variants::Match.
70
+ def compound(**match)
71
+ classes = match.delete(:class)
72
+ set = match.delete(:set) || {}
73
+ raise ArgumentError, "#{owner}: compound #{match.inspect} needs class: or set:" if classes.nil? && set.empty?
74
+
75
+ check.match!(match)
76
+ set.each { |group, member| check.member!(group, member) }
77
+ add(:compounds, match:, class: slot_hash(classes), set:)
78
+ end
79
+
80
+ # Names a Tailwind variant prefix once: `state :open, "[.dropdown[open]>&]"`.
81
+ def state(name, prefix)
82
+ raise ArgumentError, "#{owner}: state #{name.inspect} needs a Tailwind variant, got #{prefix.inspect}" unless STATE_PREFIX.match?(prefix.to_s)
83
+
84
+ write(states: definitions[:states].merge(name => prefix.to_s))
85
+ end
86
+
87
+ # Like compound, with every class prefixed by each of the `states`.
88
+ def on(states, **match)
89
+ states = Array(states)
90
+ classes = match.delete(:class)
91
+ raise ArgumentError, "#{owner}: on #{states.inspect} needs class:" if classes.nil?
92
+
93
+ states.each { |name| check.state!(name) }
94
+ check.match!(match)
95
+ add(:rules, states:, match:, class: slot_hash(classes))
96
+ end
97
+
98
+ # A part of the component (`root` is the component itself), with the
99
+ # classes it always gets. Read with Base#slot_classes / #slot_options.
100
+ def slot(name, classes = nil)
101
+ raise ArgumentError, "#{owner}: :root is the component itself, not a slot to declare" if name == :root
102
+
103
+ write(slots: definitions[:slots].merge(name => classes))
104
+ end
105
+
106
+ # Plain, frozen data describing the component, for docs pages, galleries
107
+ # and design-tool parity checks.
108
+ def schema
109
+ Schema.new(@klass).to_h
110
+ end
111
+
112
+ # Every class the component's declarations can render, prefixed as
113
+ # DaisyUI.configuration.prefix renders them: what Tailwind needs to see.
114
+ def literal_classes(prefix: nil)
115
+ Schema.new(@klass).literal_classes(prefix.to_s)
116
+ end
117
+
118
+ private
119
+
120
+ def definitions = @klass.variant_definitions
121
+
122
+ def owner = @klass.name || @klass.inspect
123
+
124
+ def check = Match.new(@klass)
125
+
126
+ def write(**changes)
127
+ @klass.variant_definitions = definitions.merge(changes)
128
+ self
129
+ end
130
+
131
+ def add(key, **entry)
132
+ write(key => [*definitions[key], entry])
133
+ end
134
+
135
+ def register(modifiers)
136
+ @klass.register_modifiers(modifiers)
137
+ end
138
+
139
+ def root_classes(classes)
140
+ classes.is_a?(Hash) ? slot_hash(classes)[:root].to_s : classes.to_s
141
+ end
142
+
143
+ # Slot parts of a member, kept apart from `modifiers`, which only ever
144
+ # holds the root's String.
145
+ def write_slot_classes(member, classes)
146
+ parts = classes.is_a?(Hash) ? slot_hash(classes).except(:root) : {}
147
+ slot_classes = definitions[:slot_classes].except(member)
148
+ write(slot_classes: parts.empty? ? slot_classes : slot_classes.merge(member => parts))
149
+ end
150
+
151
+ # `"a b"` or `{ root: "a", menu: "b" }` as `{ slot => classes }`.
152
+ def slot_hash(classes)
153
+ return {} if classes.nil?
154
+ return { root: classes.to_s } unless classes.is_a?(Hash)
155
+
156
+ classes.each_key { |slot| check.slot!(slot) }
157
+ classes.transform_values(&:to_s)
158
+ end
159
+ end
160
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DaisyUI
4
- VERSION = "2.1.1"
4
+ VERSION = "2.2.1"
5
5
  end
data/lib/daisy_ui.rb CHANGED
@@ -14,6 +14,8 @@ loader.ignore("#{__dir__}/daisy_ui/updated_at.rb")
14
14
  # The Rails engine references Rails::Engine, so it is required explicitly below
15
15
  # (only when Rails is present) rather than autoloaded.
16
16
  loader.ignore("#{__dir__}/daisy_ui/engine.rb")
17
+ # lib/tasks holds the engine's rake tasks, no constants.
18
+ loader.ignore("#{__dir__}/tasks")
17
19
  loader.setup # ready!
18
20
  loader.load_file("#{__dir__}/daisy_ui/base.rb")
19
21
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loaded by the Rails engine (Rails loads an engine's lib/tasks/*.rake).
4
+ namespace :daisyui do
5
+ desc "Write the Tailwind sources of the app's own DaisyUI components to OUTPUT (stdout without one)"
6
+ task :tailwind, [:output] => :environment do |_task, args|
7
+ argv = ["--app"]
8
+ argv.push("--output", args[:output]) if args[:output]
9
+ status = DaisyUI::Tailwind::Command.run(argv)
10
+ exit(status) unless status.zero?
11
+ end
12
+ end