vident 1.0.1 → 2.0.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +49 -18
  4. data/lib/vident/caching.rb +4 -110
  5. data/lib/vident/capabilities/caching.rb +98 -0
  6. data/lib/vident/capabilities/child_element_rendering.rb +92 -0
  7. data/lib/vident/capabilities/class_list_building.rb +23 -0
  8. data/lib/vident/capabilities/declarable.rb +39 -0
  9. data/lib/vident/capabilities/identifiable.rb +54 -0
  10. data/lib/vident/capabilities/inspectable.rb +17 -0
  11. data/lib/vident/capabilities/root_element_rendering.rb +31 -0
  12. data/lib/vident/capabilities/stimulus_data_emitting.rb +98 -0
  13. data/lib/vident/capabilities/stimulus_declaring.rb +79 -0
  14. data/lib/vident/capabilities/stimulus_draft.rb +51 -0
  15. data/lib/vident/capabilities/stimulus_mutation.rb +60 -0
  16. data/lib/vident/capabilities/stimulus_parsing.rb +144 -0
  17. data/lib/vident/capabilities/tailwind.rb +18 -0
  18. data/lib/vident/component.rb +14 -76
  19. data/lib/vident/engine.rb +6 -5
  20. data/lib/vident/error.rb +16 -0
  21. data/lib/vident/internals/action_builder.rb +97 -0
  22. data/lib/vident/internals/attribute_writer.rb +17 -0
  23. data/lib/vident/internals/class_list_builder.rb +62 -0
  24. data/lib/vident/internals/declaration.rb +13 -0
  25. data/lib/vident/internals/declarations.rb +64 -0
  26. data/lib/vident/internals/draft.rb +47 -0
  27. data/lib/vident/internals/dsl.rb +172 -0
  28. data/lib/vident/internals/plan.rb +9 -0
  29. data/lib/vident/internals/registry.rb +37 -0
  30. data/lib/vident/internals/resolver.rb +316 -0
  31. data/lib/vident/internals/target_builder.rb +23 -0
  32. data/lib/vident/stable_id.rb +3 -3
  33. data/lib/vident/stimulus/action.rb +127 -0
  34. data/lib/vident/stimulus/base.rb +26 -0
  35. data/lib/vident/stimulus/class_map.rb +57 -0
  36. data/lib/vident/stimulus/collection.rb +40 -0
  37. data/lib/vident/stimulus/combinable.rb +30 -0
  38. data/lib/vident/stimulus/controller.rb +45 -0
  39. data/lib/vident/stimulus/naming.rb +9 -9
  40. data/lib/vident/stimulus/null.rb +7 -0
  41. data/lib/vident/stimulus/outlet.rb +93 -0
  42. data/lib/vident/stimulus/param.rb +56 -0
  43. data/lib/vident/stimulus/target.rb +48 -0
  44. data/lib/vident/stimulus/value.rb +57 -0
  45. data/lib/vident/stimulus_null.rb +4 -8
  46. data/lib/vident/tailwind.rb +4 -17
  47. data/lib/vident/types.rb +28 -0
  48. data/lib/vident/version.rb +1 -6
  49. data/lib/vident.rb +44 -36
  50. data/skills/vident/SKILL.md +133 -21
  51. data/skills/vident/api-reference.md +662 -0
  52. data/skills/vident/examples.md +505 -0
  53. metadata +40 -28
  54. data/lib/vident/child_element_helper.rb +0 -64
  55. data/lib/vident/class_list_builder.rb +0 -112
  56. data/lib/vident/component_attribute_resolver.rb +0 -87
  57. data/lib/vident/component_class_lists.rb +0 -34
  58. data/lib/vident/stimulus/primitive.rb +0 -38
  59. data/lib/vident/stimulus.rb +0 -31
  60. data/lib/vident/stimulus_action.rb +0 -133
  61. data/lib/vident/stimulus_action_collection.rb +0 -11
  62. data/lib/vident/stimulus_attribute_base.rb +0 -67
  63. data/lib/vident/stimulus_attributes.rb +0 -129
  64. data/lib/vident/stimulus_builder.rb +0 -119
  65. data/lib/vident/stimulus_class.rb +0 -59
  66. data/lib/vident/stimulus_class_collection.rb +0 -11
  67. data/lib/vident/stimulus_collection_base.rb +0 -51
  68. data/lib/vident/stimulus_component.rb +0 -75
  69. data/lib/vident/stimulus_controller.rb +0 -41
  70. data/lib/vident/stimulus_controller_collection.rb +0 -14
  71. data/lib/vident/stimulus_data_attribute_builder.rb +0 -32
  72. data/lib/vident/stimulus_helper.rb +0 -66
  73. data/lib/vident/stimulus_outlet.rb +0 -90
  74. data/lib/vident/stimulus_outlet_collection.rb +0 -11
  75. data/lib/vident/stimulus_param.rb +0 -42
  76. data/lib/vident/stimulus_param_collection.rb +0 -11
  77. data/lib/vident/stimulus_target.rb +0 -47
  78. data/lib/vident/stimulus_target_collection.rb +0 -18
  79. data/lib/vident/stimulus_value.rb +0 -39
  80. data/lib/vident/stimulus_value_collection.rb +0 -11
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ module Capabilities
5
+ module RootElementRendering
6
+ def root_element_attributes = {}
7
+
8
+ def root_element_classes
9
+ nil
10
+ end
11
+
12
+ def root_element(**overrides, &block)
13
+ raise NoMethodError, "subclass must implement root_element"
14
+ end
15
+
16
+ # Dispatches to the adapter-specific `root_element` on subclasses
17
+ # (Phlex / ViewComponent). Keep as `def` not `alias_method` so Ruby's
18
+ # dynamic dispatch finds the subclass override.
19
+ def root(...)
20
+ root_element(...)
21
+ end
22
+
23
+ private
24
+
25
+ def root_element_tag_type
26
+ tag = resolved_root_element_attributes[:element_tag] || @element_tag
27
+ tag.presence&.to_sym || :div
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../internals/attribute_writer"
4
+ require_relative "../internals/class_list_builder"
5
+
6
+ module Vident
7
+ module Capabilities
8
+ module StimulusDataEmitting
9
+ # For components rendering the root via a third-party helper instead of
10
+ # `root_element(...)`. `extra_classes` lands at the lowest-priority tier
11
+ # so it cannot be clobbered by a cascade winner.
12
+ def root_element_class_list(extra_classes = nil)
13
+ extra = resolved_root_element_attributes
14
+ extra_html_options = extra[:html_options] || {}
15
+ combined_classes = [@classes, extra_classes].flatten.compact
16
+ result = ::Vident::Internals::ClassListBuilder.call(
17
+ component_name: component_name,
18
+ root_element_classes: root_element_classes,
19
+ root_element_attributes_classes: extra[:classes],
20
+ html_options_class: @html_options[:class] || extra_html_options[:class],
21
+ classes_prop: combined_classes.empty? ? nil : combined_classes,
22
+ tailwind_merger: tailwind_merger
23
+ )
24
+ result || ""
25
+ end
26
+
27
+ # Seals the Draft on first call (idempotent). Symbol keys.
28
+ def root_element_data_attributes
29
+ plan = seal_draft
30
+ data_attrs = ::Vident::Internals::AttributeWriter.call(plan)
31
+
32
+ extra = resolved_root_element_attributes
33
+ extra_data = (extra[:html_options] || {})[:data] || {}
34
+
35
+ merged = data_attrs.dup
36
+ merged.merge!(symbolize_keys(extra_data))
37
+ merged.merge!(symbolize_keys(@html_options[:data] || {}))
38
+ merged
39
+ end
40
+
41
+ private
42
+
43
+ def resolved_root_element_attributes
44
+ return @__vident_rea if defined?(@__vident_rea)
45
+ value = root_element_attributes
46
+ @__vident_rea = value.is_a?(Hash) ? value : {}
47
+ end
48
+
49
+ def build_root_element_attributes(overrides)
50
+ plan = seal_draft
51
+ data_attrs = ::Vident::Internals::AttributeWriter.call(plan)
52
+
53
+ extra = resolved_root_element_attributes
54
+ extra_html_options = extra[:html_options] || {}
55
+ extra_class = extra[:classes]
56
+ extra_id = extra[:id]
57
+ extra_data = extra_html_options[:data] || {}
58
+
59
+ # data precedence (low→high): Plan fragments → attrs html_options[:data]
60
+ # → instance html_options[:data] → overrides[:data].
61
+ merged_data = data_attrs.dup
62
+ merged_data.merge!(symbolize_keys(extra_data))
63
+ merged_data.merge!(symbolize_keys(@html_options[:data] || {}))
64
+ merged_data.merge!(symbolize_keys(overrides[:data] || {}))
65
+
66
+ class_list = ::Vident::Internals::ClassListBuilder.call(
67
+ component_name: component_name,
68
+ root_element_classes: root_element_classes,
69
+ root_element_attributes_classes: extra_class,
70
+ root_element_html_class: overrides[:class],
71
+ html_options_class: @html_options[:class] || extra_html_options[:class],
72
+ classes_prop: @classes,
73
+ tailwind_merger: tailwind_merger
74
+ )
75
+
76
+ merged = {}
77
+ merged.merge!(extra_html_options.except(:data, :class))
78
+ merged.merge!(@html_options.except(:data, :class))
79
+ merged.merge!(overrides.except(:data, :class))
80
+ merged[:class] = class_list if class_list
81
+ merged[:data] = merged_data unless merged_data.empty?
82
+ merged[:id] ||= extra_id || id
83
+
84
+ merged
85
+ end
86
+
87
+ # Intentionally NOT `hash.symbolize_keys` (ActiveSupport): that calls
88
+ # `to_sym` on every key, including integers, and would raise on keys
89
+ # that don't respond to `to_sym`. Here we convert only String keys and
90
+ # leave anything else (Symbols already) untouched — safe for the
91
+ # user-supplied `data:` hashes that reach this method.
92
+ def symbolize_keys(hash)
93
+ return {} unless hash.is_a?(Hash)
94
+ hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../internals/declarations"
4
+ require_relative "../internals/dsl"
5
+
6
+ module Vident
7
+ module Capabilities
8
+ module StimulusDeclaring
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ @__vident_declarations = ::Vident::Internals::Declarations.empty
13
+ @__vident_no_stimulus_controller = false
14
+ end
15
+
16
+ class_methods do
17
+ def declarations
18
+ @__vident_declarations ||= ::Vident::Internals::Declarations.empty
19
+ end
20
+
21
+ def no_stimulus_controller
22
+ if declarations.any?
23
+ raise ::Vident::DeclarationError,
24
+ "#{name || "anonymous component"} called `no_stimulus_controller` after " \
25
+ "`stimulus do` already recorded DSL entries. Declare `no_stimulus_controller` " \
26
+ "before any `stimulus do` block."
27
+ end
28
+ @__vident_no_stimulus_controller = true
29
+ end
30
+
31
+ def has_stimulus_controller
32
+ @__vident_no_stimulus_controller = false
33
+ end
34
+
35
+ def stimulus_controller?
36
+ !@__vident_no_stimulus_controller
37
+ end
38
+
39
+ # Second+ calls append (positional) or last-write-wins (keyed).
40
+ def stimulus(&block)
41
+ call_site = caller_locations(1, 1)&.first
42
+ dsl = ::Vident::Internals::DSL.new(caller_location: call_site)
43
+ dsl.instance_eval(&block) if block
44
+ fresh = dsl.to_declarations
45
+
46
+ if !stimulus_controller? && fresh.any?
47
+ location = call_site ? " at #{call_site.path}:#{call_site.lineno}" : ""
48
+ raise ::Vident::DeclarationError,
49
+ "#{name || "anonymous component"} declared `no_stimulus_controller` but `stimulus do` emitted DSL entries#{location}. " \
50
+ "A class with no implied controller cannot route DSL entries; drop the `stimulus do` block or remove `no_stimulus_controller`."
51
+ end
52
+
53
+ @__vident_declarations = declarations.merge(fresh).freeze
54
+ end
55
+
56
+ def stimulus_scoped_event(event)
57
+ :"#{component_name}:#{event.to_s.camelize(:lower)}"
58
+ end
59
+
60
+ def stimulus_scoped_event_on_window(event)
61
+ :"#{component_name}:#{event.to_s.camelize(:lower)}@window"
62
+ end
63
+
64
+ def inherited(subclass)
65
+ super
66
+ subclass.instance_variable_set(:@__vident_declarations, declarations)
67
+ subclass.instance_variable_set(
68
+ :@__vident_no_stimulus_controller,
69
+ instance_variable_get(:@__vident_no_stimulus_controller) || false
70
+ )
71
+ end
72
+ end
73
+
74
+ def stimulus_scoped_event(event) = self.class.stimulus_scoped_event(event)
75
+
76
+ def stimulus_scoped_event_on_window(event) = self.class.stimulus_scoped_event_on_window(event)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../internals/resolver"
4
+
5
+ module Vident
6
+ module Capabilities
7
+ module StimulusDraft
8
+ def after_component_initialize
9
+ end
10
+
11
+ # Flag set before the guards so a sealed Draft can't trap us in a
12
+ # loop where every subsequent call re-takes the sealed branch.
13
+ def resolve_stimulus_attributes_at_render_time
14
+ return if @__vident_procs_resolved
15
+ @__vident_procs_resolved = true
16
+ return if @__vident_draft.nil? || @__vident_draft.sealed?
17
+ ::Vident::Internals::Resolver.resolve_procs_into(
18
+ @__vident_draft, self.class.declarations, self
19
+ )
20
+ end
21
+
22
+ private
23
+
24
+ # DSL procs defer to render (`view_context` isn't wired yet at init time).
25
+ def after_initialize
26
+ @__vident_draft = ::Vident::Internals::Resolver.call(
27
+ self.class.declarations, self, phase: :static
28
+ )
29
+ @stimulus_outlet_host&.add_stimulus_outlets(self)
30
+ after_component_initialize
31
+ end
32
+
33
+ def raise_if_sealed!
34
+ if @__vident_draft.nil?
35
+ raise ::Vident::StateError,
36
+ "stimulus Draft is nil — Literal::Data `after_initialize` never fired. " \
37
+ "If you subclassed and overrode `initialize`, ensure `super` is called. " \
38
+ "If you instantiated with `allocate`, call `after_initialize` manually."
39
+ end
40
+ return unless @__vident_draft.sealed?
41
+ raise ::Vident::StateError,
42
+ "cannot modify stimulus attributes after rendering has begun"
43
+ end
44
+
45
+ def seal_draft
46
+ resolve_stimulus_attributes_at_render_time
47
+ @__vident_plan ||= @__vident_draft.seal!
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../internals/registry"
4
+ require_relative "../stimulus/collection"
5
+
6
+ module Vident
7
+ module Capabilities
8
+ module StimulusMutation
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ unless ancestors.include?(::Vident::Capabilities::Identifiable)
13
+ raise ::Vident::DeclarationError,
14
+ "#{name || "anonymous component"} must include Vident::Capabilities::Identifiable before Vident::Capabilities::StimulusMutation"
15
+ end
16
+ end
17
+
18
+ ::Vident::Internals::Registry.each do |kind|
19
+ define_method(:"add_stimulus_#{kind.plural_name}") do |input|
20
+ raise_if_sealed!
21
+ values = unwrap_mutator_input(kind, input)
22
+ values.each { |v| @__vident_draft.public_send(:"add_#{kind.name}", v) if v }
23
+ self
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # Array input is ONE entry — does not splat across multiple entries
30
+ # (mirrors the DSL's plural→singular forwarding).
31
+ #
32
+ # Hash input is only accepted for keyed kinds (values, params, class_maps,
33
+ # outlets) and Actions. Passing a Hash to a non-keyed, non-Action kind
34
+ # (controllers, targets) raises ParseError — use a String or Symbol instead.
35
+ def unwrap_mutator_input(kind, input)
36
+ case input
37
+ in nil
38
+ []
39
+ in ^(kind.value_class) => v
40
+ [v]
41
+ in ::Vident::Stimulus::Collection => coll
42
+ coll.items
43
+ in Hash => h if kind.keyed?
44
+ h.map { |name, raw| kind.value_class.parse(name, raw, implied: implied_controller, component_id: id) }
45
+ in Hash => h if kind.value_class == ::Vident::Stimulus::Action
46
+ [kind.value_class.parse(h, implied: implied_controller, component_id: id)]
47
+ in Hash
48
+ raise ::Vident::ParseError,
49
+ "add_stimulus_#{kind.plural_name}: Hash input is not valid for #{kind.plural_name}. " \
50
+ "Use a String or Symbol instead, e.g. add_stimulus_#{kind.plural_name}(\"name\"). " \
51
+ "Hash input is only accepted for keyed kinds (values, params, classes, outlets) and actions."
52
+ in Array => a
53
+ [kind.value_class.parse(*a, implied: implied_controller, component_id: id)]
54
+ else
55
+ [kind.value_class.parse(input, implied: implied_controller, component_id: id)]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../internals/registry"
4
+ require_relative "../stimulus/collection"
5
+
6
+ module Vident
7
+ module Capabilities
8
+ # Include order: Identifiable must be included before this module
9
+ # (generated methods call `implied_controller` and `id`).
10
+ module StimulusParsing
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ unless ancestors.include?(::Vident::Capabilities::Identifiable)
15
+ raise ::Vident::DeclarationError,
16
+ "#{name || "anonymous component"} must include Vident::Capabilities::Identifiable before Vident::Capabilities::StimulusParsing"
17
+ end
18
+ end
19
+
20
+ ::Vident::Internals::Registry.each do |kind|
21
+ define_method(:"stimulus_#{kind.singular_name}") do |*args|
22
+ return args.first if args.length == 1 && args.first.is_a?(kind.value_class)
23
+ kind.value_class.parse(*args, implied: implied_controller, component_id: id)
24
+ end
25
+
26
+ define_method(:"stimulus_#{kind.plural_name}") do |*args|
27
+ return ::Vident::Stimulus::Collection.new(kind: kind, items: []) if args.empty? || args.all?(&:nil?)
28
+ return args.first if args.length == 1 && args.first.is_a?(::Vident::Stimulus::Collection)
29
+
30
+ items = []
31
+ args.each do |arg|
32
+ case arg
33
+ in ^(kind.value_class) => v
34
+ items << v
35
+ in ::Vident::Stimulus::Collection => coll
36
+ items.concat(coll.items)
37
+ in Hash => h if kind.keyed?
38
+ h.each { |name, val| items << kind.value_class.parse(name, val, implied: implied_controller, component_id: id) }
39
+ in Hash => h
40
+ items << kind.value_class.parse(h, implied: implied_controller, component_id: id)
41
+ in Array => a
42
+ items << kind.value_class.parse(*a, implied: implied_controller, component_id: id)
43
+ else
44
+ items << kind.value_class.parse(arg, implied: implied_controller, component_id: id)
45
+ end
46
+ end
47
+ ::Vident::Stimulus::Collection.new(kind: kind, items: items)
48
+ end
49
+ end
50
+
51
+ # Class-level builders reject cross-controller forms (receiver's implied
52
+ # controller would be silently ignored) — call `Vident::Stimulus::*.parse`
53
+ # directly for those. `stimulus_outlet` additionally requires an explicit
54
+ # selector (no component_id to auto-scope).
55
+ class_methods do
56
+ def stimulus_controller
57
+ implied_controller_for_class
58
+ end
59
+
60
+ def stimulus_target(*args)
61
+ case args
62
+ in [String => ctrl_path, Symbol]
63
+ raise ::Vident::ParseError,
64
+ "#{name}.stimulus_target does not accept cross-controller form at class level. " \
65
+ "Call Vident::Stimulus::Target.parse(#{ctrl_path.inspect}, ...) directly."
66
+ else
67
+ ::Vident::Stimulus::Target.parse(*args, implied: implied_controller_for_class)
68
+ end
69
+ end
70
+
71
+ def stimulus_action(*args)
72
+ case args
73
+ in [String => ctrl_path, Symbol]
74
+ raise ::Vident::ParseError,
75
+ "#{name}.stimulus_action does not accept cross-controller form at class level. " \
76
+ "Call Vident::Stimulus::Action.parse(#{ctrl_path.inspect}, ...) directly."
77
+ in [Symbol, String, Symbol]
78
+ raise ::Vident::ParseError,
79
+ "#{name}.stimulus_action does not accept cross-controller form at class level. " \
80
+ "Call Vident::Stimulus::Action.parse directly."
81
+ else
82
+ ::Vident::Stimulus::Action.parse(*args, implied: implied_controller_for_class)
83
+ end
84
+ end
85
+
86
+ def stimulus_value(*args)
87
+ case args
88
+ in [String, Symbol, *]
89
+ raise ::Vident::ParseError,
90
+ "#{name}.stimulus_value does not accept cross-controller form at class level. " \
91
+ "Call Vident::Stimulus::Value.parse directly."
92
+ else
93
+ ::Vident::Stimulus::Value.parse(*args, implied: implied_controller_for_class)
94
+ end
95
+ end
96
+
97
+ def stimulus_param(*args)
98
+ case args
99
+ in [String, Symbol, *]
100
+ raise ::Vident::ParseError,
101
+ "#{name}.stimulus_param does not accept cross-controller form at class level. " \
102
+ "Call Vident::Stimulus::Param.parse directly."
103
+ else
104
+ ::Vident::Stimulus::Param.parse(*args, implied: implied_controller_for_class)
105
+ end
106
+ end
107
+
108
+ def stimulus_class(*args)
109
+ case args
110
+ in [String, Symbol, *]
111
+ raise ::Vident::ParseError,
112
+ "#{name}.stimulus_class does not accept cross-controller form at class level. " \
113
+ "Call Vident::Stimulus::ClassMap.parse directly."
114
+ else
115
+ ::Vident::Stimulus::ClassMap.parse(*args, implied: implied_controller_for_class)
116
+ end
117
+ end
118
+
119
+ def stimulus_outlet(*args)
120
+ case args
121
+ in [Symbol => _name, String => _selector] | [String => _name, String => _selector]
122
+ ::Vident::Stimulus::Outlet.parse(*args, implied: implied_controller_for_class)
123
+ else
124
+ raise ::Vident::ParseError,
125
+ "#{name}.stimulus_outlet requires (name, selector) — no component_id at class level. " \
126
+ "Use instance-level `component.stimulus_outlet(:name)` for auto-selector, " \
127
+ "or `#{name}.stimulus_outlet(:name, '.selector')` with an explicit selector."
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ # Memoised on the class's own singleton — Ruby doesn't share
134
+ # singleton ivars through inheritance, so subclasses get their own.
135
+ def implied_controller_for_class
136
+ @__vident_class_implied_controller ||= ::Vident::Stimulus::Controller.new(
137
+ path: stimulus_identifier_path,
138
+ name: stimulus_identifier
139
+ )
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ module Capabilities
5
+ module Tailwind
6
+ def tailwind_merger
7
+ return unless tailwind_merge_available?
8
+ return @tailwind_merger if defined?(@tailwind_merger)
9
+
10
+ @tailwind_merger = Thread.current[:vident_tailwind_merger] ||= ::TailwindMerge::Merger.new
11
+ end
12
+
13
+ def tailwind_merge_available?
14
+ defined?(::TailwindMerge::Merger) && ::TailwindMerge::Merger.respond_to?(:new)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,84 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vident
4
+ # Composition root. Include order mirrors capability dependencies.
5
+ # Caching is opt-in and NOT included here.
4
6
  module Component
5
7
  extend ActiveSupport::Concern
6
8
 
7
- # Base class for all Vident components, which provides common functionality and properties.
8
- included do
9
- # The HTML tag to use for the root element of the component, defaults to `:div`.
10
- prop :element_tag, Symbol, default: :div
11
- # ID of the component. If not set, a random ID is generated.
12
- prop :id, _Nilable(String)
13
- # Classes to apply to the root element (they add to the `class` attribute)
14
- prop :classes, _Union(String, _Array(String)), default: -> { [] }
15
- # HTML options to apply to the root element (will merge into and potentially override html_options of the element)
16
- prop :html_options, Hash, default: -> { {} }
17
- end
18
-
19
- class_methods do
20
- # Returns the names of the properties defined in the component class.
21
- def prop_names
22
- literal_properties.properties_index.keys.map(&:to_sym)
23
- end
24
- end
25
-
26
- include StimulusComponent
27
- include ComponentClassLists
28
- include ComponentAttributeResolver
29
-
30
- include ChildElementHelper
31
- include Tailwind
32
- include StimulusHelper
33
-
34
- # Override this method to perform any initialisation after attributes are set
35
- def after_component_initialize
36
- end
37
-
38
- # This can be overridden to return an array of extra class names, or a string of class names.
39
- def root_element_classes
40
- end
41
-
42
- # Properties/attributes passed to the "root" element of the component. You normally override this method to
43
- # return a hash of attributes that should be applied to the root element of your component.
44
- def root_element_attributes = {}
45
-
46
- # Create a new component instance with optional overrides for properties.
47
- def clone(overrides = {}) = self.class.new(**to_h.merge(**overrides))
48
-
49
- def inspect(klass_name = "Component")
50
- attr_text = to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")
51
- "#<#{self.class.name}<Vident::#{klass_name}> #{attr_text}>"
52
- end
53
-
54
- # Generate a unique ID for a component, can be overridden as required. Makes it easier to setup things like ARIA
55
- # attributes which require elements to reference by ID. Note this overrides the `id` accessor
56
- def id = @id.presence || random_id
57
-
58
- # Return the names of the properties defined in the component class.
59
- def prop_names = self.class.prop_names
60
-
61
- private
62
-
63
- # Called by Literal::Properties after the component is initialized.
64
- def after_initialize
65
- prepare_component_attributes
66
- after_component_initialize if respond_to?(:after_component_initialize)
67
- end
68
-
69
- def root_element(&block)
70
- raise NoMethodError, "You must implement the `root_element` method in your component"
71
- end
72
-
73
- def root(...)
74
- root_element(...)
75
- end
76
-
77
- def root_element_tag_type = @element_tag.presence&.to_sym || :div
78
-
79
- # Generate a random ID for the component, which is used to ensure uniqueness in the DOM.
80
- def random_id
81
- @random_id ||= "#{component_name}-#{StableId.next_id_in_sequence}"
82
- end
9
+ include ::Vident::Capabilities::Tailwind
10
+ include ::Vident::Capabilities::Declarable
11
+ include ::Vident::Capabilities::Identifiable
12
+ include ::Vident::Capabilities::StimulusDeclaring
13
+ include ::Vident::Capabilities::StimulusParsing
14
+ include ::Vident::Capabilities::StimulusMutation
15
+ include ::Vident::Capabilities::StimulusDraft
16
+ include ::Vident::Capabilities::StimulusDataEmitting
17
+ include ::Vident::Capabilities::ClassListBuilding
18
+ include ::Vident::Capabilities::RootElementRendering
19
+ include ::Vident::Capabilities::ChildElementRendering
20
+ include ::Vident::Capabilities::Inspectable
83
21
  end
84
22
  end
data/lib/vident/engine.rb CHANGED
@@ -1,13 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Vident
4
+ # Registers acronym inflections Zeitwerk needs to load `Vident::Internals::DSL`
5
+ # and `Vident::Phlex::HTML` from their respective files.
2
6
  class Engine < ::Rails::Engine
3
- lib_path = File.expand_path("../../../lib/", __FILE__)
4
- config.autoload_paths << lib_path
5
- config.eager_load_paths << lib_path
6
-
7
7
  config.before_initialize do
8
8
  Rails.autoloaders.each do |autoloader|
9
9
  autoloader.inflector.inflect(
10
- "version" => "VERSION"
10
+ "dsl" => "DSL",
11
+ "html" => "HTML"
11
12
  )
12
13
  end
13
14
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ # Base for all gem-raised exceptions; consumers can rescue by category.
5
+ class Error < StandardError; end
6
+
7
+ class DeclarationError < Error; end
8
+
9
+ class ParseError < Error; end
10
+
11
+ class RenderError < Error; end
12
+
13
+ class StateError < Error; end
14
+
15
+ class ConfigurationError < Error; end
16
+ end