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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +149 -0
- data/lib/daisy_ui/base.rb +24 -4
- data/lib/daisy_ui/engine.rb +4 -0
- data/lib/daisy_ui/mcp_server/variants_guide.rb +99 -0
- data/lib/daisy_ui/mcp_server.rb +16 -1
- data/lib/daisy_ui/tailwind/app_sources.rb +73 -0
- data/lib/daisy_ui/tailwind/candidate.rb +38 -3
- data/lib/daisy_ui/tailwind/command.rb +21 -4
- data/lib/daisy_ui/tailwind/sources.rb +11 -8
- data/lib/daisy_ui/tailwind.rb +45 -1
- data/lib/daisy_ui/unknown_modifier_error.rb +9 -2
- data/lib/daisy_ui/updated_at.rb +1 -1
- data/lib/daisy_ui/variants/match.rb +89 -0
- data/lib/daisy_ui/variants/rendering.rb +68 -0
- data/lib/daisy_ui/variants/resolution.rb +53 -0
- data/lib/daisy_ui/variants/schema.rb +73 -0
- data/lib/daisy_ui/variants.rb +160 -0
- data/lib/daisy_ui/version.rb +1 -1
- data/lib/daisy_ui.rb +2 -0
- data/lib/tasks/daisyui.rake +12 -0
- metadata +9 -1
|
@@ -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
|
|
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
|
data/lib/daisy_ui/tailwind.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
component.modifier_groups.map { |group, members| "#{group}: #{members.join(', ')}" }.join("; ")
|
|
29
36
|
end
|
|
30
37
|
end
|
|
31
38
|
end
|
data/lib/daisy_ui/updated_at.rb
CHANGED
|
@@ -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
|
data/lib/daisy_ui/version.rb
CHANGED
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
|