view_component_atomix 0.1.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +744 -0
- data/lib/view_component/atomix/accessors.rb +129 -0
- data/lib/view_component/atomix/attribute_helpers.rb +15 -0
- data/lib/view_component/atomix/attrs.rb +17 -0
- data/lib/view_component/atomix/base.rb +226 -0
- data/lib/view_component/atomix/builder.rb +77 -0
- data/lib/view_component/atomix/compiler.rb +147 -0
- data/lib/view_component/atomix/condition_resolver.rb +79 -0
- data/lib/view_component/atomix/configuration.rb +103 -0
- data/lib/view_component/atomix/declaration_queue.rb +50 -0
- data/lib/view_component/atomix/dynamic_properties/literal_key.rb +68 -0
- data/lib/view_component/atomix/dynamic_properties/source_cache.rb +31 -0
- data/lib/view_component/atomix/dynamic_properties/source_parser.rb +61 -0
- data/lib/view_component/atomix/dynamic_properties.rb +75 -0
- data/lib/view_component/atomix/dynamic_style.rb +109 -0
- data/lib/view_component/atomix/enumerated_style.rb +39 -0
- data/lib/view_component/atomix/hashing.rb +47 -0
- data/lib/view_component/atomix/helper.rb +31 -0
- data/lib/view_component/atomix/immutable.rb +20 -0
- data/lib/view_component/atomix/known_properties.rb +102 -0
- data/lib/view_component/atomix/owner_state.rb +48 -0
- data/lib/view_component/atomix/property.rb +183 -0
- data/lib/view_component/atomix/railtie.rb +46 -0
- data/lib/view_component/atomix/registry.rb +208 -0
- data/lib/view_component/atomix/resolution_session.rb +81 -0
- data/lib/view_component/atomix/rule.rb +42 -0
- data/lib/view_component/atomix/sheet.rb +98 -0
- data/lib/view_component/atomix/style.rb +80 -0
- data/lib/view_component/atomix/tasks/build.rake +17 -0
- data/lib/view_component/atomix/theme_compiler.rb +19 -0
- data/lib/view_component/atomix/theme_resolution.rb +66 -0
- data/lib/view_component/atomix/trait_classifier.rb +111 -0
- data/lib/view_component/atomix/vars.rb +97 -0
- data/lib/view_component/atomix/version.rb +7 -0
- data/lib/view_component/atomix/weak_collection.rb +62 -0
- data/lib/view_component/atomix.rb +301 -0
- data/lib/view_component_atomix.rb +3 -0
- metadata +162 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
Rule = Struct.new(:selector, :property, :value, :conditions, :priority, keyword_init: true) do
|
|
6
|
+
def initialize(selector:, property:, value:, conditions: [], priority: nil)
|
|
7
|
+
super(
|
|
8
|
+
selector: Immutable.copy(selector),
|
|
9
|
+
property: Immutable.copy(property),
|
|
10
|
+
value: Immutable.copy(value),
|
|
11
|
+
conditions: Immutable.copy(conditions),
|
|
12
|
+
priority: priority || Property.priority(property, conditions)
|
|
13
|
+
)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_css
|
|
18
|
+
pseudos = conditions.reject { |c| c.start_with?("@") }
|
|
19
|
+
.sort_by { |c| c.start_with?("::") ? 1 : 0 }
|
|
20
|
+
at_rules = conditions.select { |c| c.start_with?("@") }
|
|
21
|
+
.sort_by { |c| Property.condition_priority(c) }
|
|
22
|
+
|
|
23
|
+
# StyleX adds one class per at-rule so a matching media query can
|
|
24
|
+
# override a pseudo-class even when cascade layers are disabled.
|
|
25
|
+
repetitions = 0
|
|
26
|
+
if selector.start_with?(".")
|
|
27
|
+
repetitions = at_rules.length
|
|
28
|
+
repetitions += 1 if pseudos.any? { |pseudo| pseudo.include?(":where(") }
|
|
29
|
+
end
|
|
30
|
+
css = "#{selector * (1 + repetitions)}#{pseudos.join}{#{declarations}}"
|
|
31
|
+
at_rules.reverse_each { |at| css = "#{at}{#{css}}" }
|
|
32
|
+
css
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def declarations
|
|
38
|
+
Array(value).map { |v| "#{property}:#{v}" }.join(";")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
class Sheet
|
|
6
|
+
attr_reader :sets, :vars
|
|
7
|
+
|
|
8
|
+
def initialize(sets = {}, vars = {})
|
|
9
|
+
@sets = Immutable.copy(sets)
|
|
10
|
+
@vars = Immutable.copy(vars)
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
EMPTY = new
|
|
15
|
+
|
|
16
|
+
def [](name)
|
|
17
|
+
return nil if name.nil?
|
|
18
|
+
|
|
19
|
+
key = name.to_sym
|
|
20
|
+
return sets[key] if sets.key?(key)
|
|
21
|
+
|
|
22
|
+
owner = vars_for(key)
|
|
23
|
+
return owner[key] if owner
|
|
24
|
+
|
|
25
|
+
raise KeyError, "Unknown trait #{name.inspect}. Available sets: #{names.inspect}, vars: #{var_names.inspect}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fetch(name, *args, **kwargs)
|
|
29
|
+
invoke(name, self[name], *args, **kwargs)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def key?(name)
|
|
33
|
+
sets.key?(name.to_sym) || !vars_for(name).nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def names
|
|
37
|
+
sets.keys
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def var_names
|
|
41
|
+
vars.keys
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def vars_for(name)
|
|
45
|
+
key = name.to_sym
|
|
46
|
+
return vars[key] if vars.key?(key)
|
|
47
|
+
|
|
48
|
+
global = ViewComponent::Atomix.configured_vars
|
|
49
|
+
global if global&.names&.key?(key)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def empty?
|
|
53
|
+
sets.empty? && vars.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def merge(other)
|
|
57
|
+
return self if other.nil? || other.empty?
|
|
58
|
+
|
|
59
|
+
Sheet.new(sets.merge(other.sets), vars.merge(other.vars))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_h
|
|
63
|
+
sets
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def props(*styles)
|
|
67
|
+
ViewComponent::Atomix.props(*styles)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
71
|
+
if sets.key?(name)
|
|
72
|
+
invoke(name, sets[name], *args, **kwargs)
|
|
73
|
+
elsif args.empty? && kwargs.empty? && (owner = vars_for(name))
|
|
74
|
+
owner[name]
|
|
75
|
+
else
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def respond_to_missing?(name, include_private = false)
|
|
81
|
+
sets.key?(name) || !vars_for(name).nil? || super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def invoke(name, set, *args, **kwargs)
|
|
87
|
+
return set.call(*args, **kwargs) if callable?(set)
|
|
88
|
+
return set if args.empty? && kwargs.empty?
|
|
89
|
+
|
|
90
|
+
raise ArgumentError, "Trait #{name.inspect} is static and takes no arguments"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def callable?(set)
|
|
94
|
+
set.is_a?(DynamicStyle) || set.is_a?(EnumeratedStyle)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
class Style
|
|
6
|
+
attr_reader :classes, :inline
|
|
7
|
+
|
|
8
|
+
def initialize(classes = {}, inline = {})
|
|
9
|
+
assign(Immutable.copy(classes), Immutable.copy(inline))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.combine(styles)
|
|
13
|
+
styles = styles.reject(&:empty?)
|
|
14
|
+
return EMPTY if styles.empty?
|
|
15
|
+
return styles.first if styles.one?
|
|
16
|
+
|
|
17
|
+
classes = {}
|
|
18
|
+
inline = {}
|
|
19
|
+
styles.each do |style|
|
|
20
|
+
classes.update(style.classes)
|
|
21
|
+
inline.update(style.inline)
|
|
22
|
+
end
|
|
23
|
+
allocate.send(:assign, classes.freeze, inline.freeze)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def merge(other)
|
|
27
|
+
return self if other.nil? || other.empty?
|
|
28
|
+
|
|
29
|
+
Style.combine([self, other])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def empty?
|
|
33
|
+
classes.empty? && inline.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def class_names
|
|
37
|
+
names = classes.values.compact.flatten
|
|
38
|
+
names.empty? ? nil : names.join(" ")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def inline_style
|
|
42
|
+
return nil if inline.empty?
|
|
43
|
+
|
|
44
|
+
inline.map { |name, value| "#{name}:#{value}" }.join(";")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_attrs
|
|
48
|
+
attrs = Attrs.new
|
|
49
|
+
names = class_names
|
|
50
|
+
declarations = inline_style
|
|
51
|
+
attrs[:class] = names if names
|
|
52
|
+
attrs[:style] = declarations if declarations
|
|
53
|
+
attrs
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_s
|
|
57
|
+
class_names.to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ==(other)
|
|
61
|
+
other.is_a?(Style) && classes == other.classes && inline == other.inline
|
|
62
|
+
end
|
|
63
|
+
alias eql? ==
|
|
64
|
+
|
|
65
|
+
def hash
|
|
66
|
+
[classes, inline].hash
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def assign(classes, inline)
|
|
72
|
+
@classes = classes
|
|
73
|
+
@inline = inline
|
|
74
|
+
freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
EMPTY = new
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :view_component do
|
|
4
|
+
namespace :atomix do
|
|
5
|
+
desc "Compile all ViewComponent::Atomix rules into a CSS file"
|
|
6
|
+
task build: :environment do
|
|
7
|
+
Rails.application.eager_load!
|
|
8
|
+
ViewComponent::Atomix.eager_load!
|
|
9
|
+
ViewComponent::Atomix.compile_dynamic_styles!
|
|
10
|
+
path = ViewComponent::Atomix.write_css(force: true)
|
|
11
|
+
puts "Wrote #{ViewComponent::Atomix.registry.size} rules to #{path}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
task "assets:precompile"
|
|
17
|
+
Rake::Task["assets:precompile"].prerequisites.unshift("view_component:atomix:build")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
module ThemeCompiler
|
|
6
|
+
# A variable group may span multiple style_vars declarations.
|
|
7
|
+
def self.compile(id, names, overrides)
|
|
8
|
+
digest_input = overrides.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}=#{v.inspect}" }
|
|
9
|
+
class_name = Hashing.class_name("theme", id, digest_input)
|
|
10
|
+
Atomix.registry.transaction do
|
|
11
|
+
overrides.each do |key, value|
|
|
12
|
+
Vars.register(".#{class_name}", names.fetch(key.to_sym), value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
Style.new({ "theme:#{id}" => [class_name] })
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
module ThemeResolution
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def resolved_theme
|
|
9
|
+
resolve_owner(:theme) do |reentrant|
|
|
10
|
+
reentrant ? theme_context : compile_theme
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compile_theme
|
|
15
|
+
styles_state.themes.drain do |declaration|
|
|
16
|
+
built = ViewComponent::Atomix.registry.with_source(declaration.source) do
|
|
17
|
+
build_theme(declaration.vars, declaration.overrides, declaration.block)
|
|
18
|
+
end
|
|
19
|
+
styles_state.theme_own = (styles_state.theme_own || Style::EMPTY).merge(built)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
theme_context
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def theme_context
|
|
26
|
+
inherited = inherited_theme
|
|
27
|
+
matching_mixins = styles_mixins.select { |mod| mod < Theme && theme_accessor_for(mod) == :theme }
|
|
28
|
+
layers = [inherited, companion_theme, *matching_mixins.map { |mod| mod.send(:resolved_theme) },
|
|
29
|
+
styles_state.theme_own || Style::EMPTY]
|
|
30
|
+
layers.reduce(Style::EMPTY, :merge)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inherited_theme
|
|
34
|
+
return Style::EMPTY unless is_a?(Class) && superclass.respond_to?(:resolved_theme, true)
|
|
35
|
+
|
|
36
|
+
superclass.send(:resolved_theme)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_theme(vars, overrides, block)
|
|
40
|
+
overrides = block.call if block
|
|
41
|
+
unless overrides.is_a?(Hash)
|
|
42
|
+
raise ArgumentError, "Theme overrides must be a Hash, got #{overrides.class}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if vars
|
|
46
|
+
return commit_owner_declaration do
|
|
47
|
+
ViewComponent::Atomix.create_theme(vars, **overrides)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sheet = resolved_styles
|
|
52
|
+
grouped = overrides.group_by do |key, _|
|
|
53
|
+
owner = sheet.vars_for(key) ||
|
|
54
|
+
raise(KeyError, "Unknown var #{key.inspect} in theme for #{self}. Available vars: #{sheet.var_names.inspect}")
|
|
55
|
+
owner.id
|
|
56
|
+
end
|
|
57
|
+
commit_owner_declaration do
|
|
58
|
+
grouped.map do |id, pairs|
|
|
59
|
+
names = pairs.to_h { |key, _| [key.to_sym, sheet.vars_for(key).var_name(key)] }
|
|
60
|
+
ThemeCompiler.compile(id, names, pairs.to_h)
|
|
61
|
+
end.reduce(Style::EMPTY, :merge)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
# Classifies the DSL without emitting CSS. Available references contain only
|
|
6
|
+
# inherited sets and sets compiled earlier in declaration order.
|
|
7
|
+
class TraitClassifier
|
|
8
|
+
Result = Struct.new(:kind, :properties, :references, :members, :default, :fn, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def initialize(sets:, inherited_sets:, conditions:)
|
|
11
|
+
@sets = sets
|
|
12
|
+
@inherited_sets = inherited_sets
|
|
13
|
+
@conditions = conditions
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def classify(set_name, definition)
|
|
17
|
+
if Property.known?(Property.normalize_key(set_name))
|
|
18
|
+
raise ArgumentError, "Trait #{set_name.inspect} is named after a CSS property. Pick another name."
|
|
19
|
+
end
|
|
20
|
+
if Sheet.method_defined?(set_name)
|
|
21
|
+
raise ArgumentError, "Trait #{set_name.inspect} collides with a method on the styles sheet. Pick another name."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
case definition
|
|
25
|
+
when Proc
|
|
26
|
+
Result.new(kind: :dynamic, fn: definition)
|
|
27
|
+
when Hash
|
|
28
|
+
if definition.key?(:dynamic) || definition.key?(:using)
|
|
29
|
+
classify_dynamic(set_name, definition)
|
|
30
|
+
else
|
|
31
|
+
classify_hash(set_name, definition)
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "Trait #{set_name.inspect} must be a Hash or a lambda, got #{definition.class}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def classify_dynamic(set_name, definition)
|
|
41
|
+
unknown = definition.keys - %i[dynamic using]
|
|
42
|
+
raise ArgumentError, "Dynamic trait #{set_name.inspect} has unknown options #{unknown.inspect}" if unknown.any?
|
|
43
|
+
|
|
44
|
+
properties = definition[:dynamic]
|
|
45
|
+
fn = definition[:using]
|
|
46
|
+
unless properties.is_a?(Array) && properties.all? { |property| property.is_a?(Symbol) || property.is_a?(String) }
|
|
47
|
+
raise ArgumentError, "Dynamic trait #{set_name.inspect} requires dynamic: with an Array of property names"
|
|
48
|
+
end
|
|
49
|
+
raise ArgumentError, "Dynamic trait #{set_name.inspect} requires using: with a lambda" unless fn.is_a?(Proc)
|
|
50
|
+
|
|
51
|
+
Result.new(kind: :dynamic, fn: fn, properties: properties)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def classify_hash(set_name, definition)
|
|
55
|
+
references = {}
|
|
56
|
+
members = {}
|
|
57
|
+
properties = {}
|
|
58
|
+
default = nil
|
|
59
|
+
|
|
60
|
+
definition.each do |key, value|
|
|
61
|
+
if key == :default
|
|
62
|
+
if value.is_a?(Hash)
|
|
63
|
+
raise ArgumentError,
|
|
64
|
+
"Default for trait #{set_name.inspect} must name a member, not define styles"
|
|
65
|
+
end
|
|
66
|
+
default = value
|
|
67
|
+
elsif @conditions.breakpoint?(key) && value.is_a?(Hash)
|
|
68
|
+
raise ArgumentError,
|
|
69
|
+
"#{key.inspect} in trait #{set_name.inspect} is a configured breakpoint, so it cannot name a " \
|
|
70
|
+
"member. Conditions must be nested under a property, and members need a name that is not a breakpoint."
|
|
71
|
+
elsif reference_key?(key)
|
|
72
|
+
references[key] = value
|
|
73
|
+
elsif member_key?(key, value)
|
|
74
|
+
members[key] = value
|
|
75
|
+
else
|
|
76
|
+
properties[key] = value
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if members.any? && (references.any? || properties.any?)
|
|
81
|
+
raise ArgumentError,
|
|
82
|
+
"Trait #{set_name.inspect} mixes enumerated members #{members.keys.inspect} with " \
|
|
83
|
+
"#{(references.keys + properties.keys).inspect}. A trait is either a list of members or a style."
|
|
84
|
+
end
|
|
85
|
+
if default && members.empty?
|
|
86
|
+
raise ArgumentError, "Trait #{set_name.inspect} has a default but no members"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if members.any?
|
|
90
|
+
Result.new(kind: :enumerated, members: members, default: default)
|
|
91
|
+
elsif references.any?
|
|
92
|
+
Result.new(kind: :combination, references: references, properties: properties)
|
|
93
|
+
else
|
|
94
|
+
Result.new(kind: :plain, properties: definition)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def reference_key?(key)
|
|
99
|
+
key.is_a?(Symbol) && (@sets.key?(key) || @inherited_sets.key?(key))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def member_key?(key, value)
|
|
103
|
+
return false unless key.is_a?(Symbol) && value.is_a?(Hash)
|
|
104
|
+
return false if Property.known?(Property.normalize_key(key))
|
|
105
|
+
return false if @conditions.condition?(key)
|
|
106
|
+
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponent
|
|
4
|
+
module Atomix
|
|
5
|
+
class Vars
|
|
6
|
+
attr_reader :id, :names
|
|
7
|
+
|
|
8
|
+
def initialize(id, definitions)
|
|
9
|
+
self.class.validate_definitions!(definitions)
|
|
10
|
+
@id = Immutable.copy(id)
|
|
11
|
+
@names = {}
|
|
12
|
+
@refs = {}
|
|
13
|
+
|
|
14
|
+
Atomix.registry.transaction do
|
|
15
|
+
definitions.each do |key, value|
|
|
16
|
+
var = Hashing.var_name(id, key)
|
|
17
|
+
@names[key.to_sym] = var
|
|
18
|
+
@refs[key.to_sym] = "var(#{var})"
|
|
19
|
+
Atomix.registry.replace_root(var)
|
|
20
|
+
Vars.register(":root", var, value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@names = Immutable.copy(@names)
|
|
25
|
+
@refs = Immutable.copy(@refs)
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def [](key)
|
|
30
|
+
@refs.fetch(key.to_sym) do
|
|
31
|
+
raise KeyError, "Unknown var #{key.inspect}. Available vars: #{@refs.keys.inspect}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def var_name(key)
|
|
36
|
+
@names.fetch(key.to_sym)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
@refs
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def method_missing(name, *args)
|
|
44
|
+
return super unless @refs.key?(name) && args.empty?
|
|
45
|
+
|
|
46
|
+
@refs[name]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def respond_to_missing?(name, include_private = false)
|
|
50
|
+
@refs.key?(name) || super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def theme(**overrides)
|
|
54
|
+
Atomix.create_theme(self, **overrides)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.validate_definitions!(definitions)
|
|
58
|
+
unless definitions.is_a?(Hash)
|
|
59
|
+
raise ArgumentError, "Variable definitions must be a Hash, got #{definitions.class}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
definitions.each_key do |key|
|
|
63
|
+
next unless method_defined?(key.to_sym) || Sheet.method_defined?(key.to_sym)
|
|
64
|
+
|
|
65
|
+
raise ArgumentError, "Variable #{key.inspect} collides with a method on Vars or the styles sheet. Pick another name."
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.register(selector, var, value, conditions = [])
|
|
70
|
+
return if value.nil?
|
|
71
|
+
|
|
72
|
+
case value
|
|
73
|
+
when Hash
|
|
74
|
+
value.each do |key, nested|
|
|
75
|
+
next if nested.nil?
|
|
76
|
+
|
|
77
|
+
if key == :default
|
|
78
|
+
register(selector, var, nested, conditions)
|
|
79
|
+
else
|
|
80
|
+
register(selector, var, nested, conditions + [Atomix.config.resolve_condition(key)])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
Atomix.registry.add(
|
|
85
|
+
Rule.new(
|
|
86
|
+
selector: selector,
|
|
87
|
+
property: var,
|
|
88
|
+
value: Property.normalize_value(var, value),
|
|
89
|
+
conditions: conditions,
|
|
90
|
+
priority: conditions.sum(0) { |c| Property.condition_priority(c) }
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "weakref"
|
|
4
|
+
|
|
5
|
+
module ViewComponent
|
|
6
|
+
module Atomix
|
|
7
|
+
class WeakCollection
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@references = []
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def <<(object)
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
compact!
|
|
18
|
+
@references << WeakRef.new(object) unless @references.any? { |reference| dereference(reference)&.equal?(object) }
|
|
19
|
+
end
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def each
|
|
24
|
+
return enum_for(:each) unless block_given?
|
|
25
|
+
|
|
26
|
+
objects = @mutex.synchronize do
|
|
27
|
+
compact!
|
|
28
|
+
@references.filter_map { |reference| dereference(reference) }
|
|
29
|
+
end
|
|
30
|
+
objects.each { |object| yield object }
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def delete_if
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@references.delete_if do |reference|
|
|
37
|
+
object = dereference(reference)
|
|
38
|
+
object.nil? || yield(object)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear
|
|
45
|
+
@mutex.synchronize { @references.clear }
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def compact!
|
|
52
|
+
@references.select!(&:weakref_alive?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def dereference(reference)
|
|
56
|
+
reference.__getobj__
|
|
57
|
+
rescue WeakRef::RefError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|