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,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "declaration"
4
+
5
+ module Vident
6
+ module Internals
7
+ # Fluent builder returned by `action(...)`. If no chain methods are called,
8
+ # raw args pass through untouched so bare `action :click` still works.
9
+ class ActionBuilder
10
+ KWARG_KEYS = %i[on call_method modifier keyboard window on_controller when].freeze
11
+
12
+ def initialize(*args, **meta)
13
+ @args = args
14
+ unknown = meta.keys - KWARG_KEYS
15
+ raise ArgumentError, "action: unknown option(s) #{unknown.inspect}. Valid: #{KWARG_KEYS.inspect}" unless unknown.empty?
16
+
17
+ @event = meta[:on]
18
+ @method_name = meta[:call_method]
19
+ @modifiers = meta.key?(:modifier) ? Array(meta[:modifier]) : nil
20
+ @keyboard = meta[:keyboard]
21
+ @window = meta.fetch(:window, false)
22
+ @controller_ref = meta[:on_controller]
23
+ @when_proc = meta[:when]
24
+ @touched = !meta.empty?
25
+ end
26
+
27
+ def on(event)
28
+ @event = event
29
+ @touched = true
30
+ self
31
+ end
32
+
33
+ def call_method(name)
34
+ @method_name = name
35
+ @touched = true
36
+ self
37
+ end
38
+
39
+ def modifier(*mods)
40
+ (@modifiers ||= []).concat(mods)
41
+ @touched = true
42
+ self
43
+ end
44
+
45
+ def keyboard(str)
46
+ @keyboard = str
47
+ @touched = true
48
+ self
49
+ end
50
+
51
+ def window
52
+ @window = true
53
+ @touched = true
54
+ self
55
+ end
56
+
57
+ def on_controller(ref)
58
+ @controller_ref = ref
59
+ @touched = true
60
+ self
61
+ end
62
+
63
+ def when(callable = nil, &block)
64
+ @when_proc = block || callable
65
+ self
66
+ end
67
+
68
+ def to_declaration
69
+ return Declaration.new(args: @args.freeze, when_proc: @when_proc, meta: {}.freeze) unless @touched
70
+ Declaration.new(args: [build_descriptor].freeze, when_proc: @when_proc, meta: {}.freeze)
71
+ end
72
+
73
+ private
74
+
75
+ def build_descriptor
76
+ h = base_descriptor.dup
77
+ h[:event] = @event if @event
78
+ h[:method] = @method_name if @method_name
79
+ h[:options] = @modifiers.dup if @modifiers
80
+ h[:keyboard] = @keyboard if @keyboard
81
+ h[:window] = true if @window
82
+ h[:controller] = @controller_ref if @controller_ref
83
+ h
84
+ end
85
+
86
+ def base_descriptor
87
+ case @args
88
+ in [Symbol => m] then {method: m}
89
+ in [Symbol => e, Symbol => m] then {event: e, method: m}
90
+ in [Symbol => e, String => ctrl, Symbol => m] then {event: e, method: m, controller: ctrl}
91
+ in [Hash => h] then h
92
+ else {}
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+
5
+ module Vident
6
+ module Internals
7
+ module AttributeWriter
8
+ module_function
9
+
10
+ def call(plan)
11
+ Registry::KINDS.reduce({}) do |acc, kind|
12
+ acc.merge(kind.value_class.to_data_hash(plan.public_send(kind.name)))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Vident
6
+ module Internals
7
+ # Builds the root element's CSS class list with a 6-tier precedence cascade.
8
+ # Tiers (left-to-right): component_name, then the highest-priority non-nil of
9
+ # root_element_classes / root_element_attributes[:classes] / root_element(class:) /
10
+ # html_options[:class], then classes: prop (always appended), then stimulus class maps.
11
+ module ClassListBuilder
12
+ CLASSNAME_SEPARATOR = " "
13
+
14
+ module_function
15
+
16
+ def call(
17
+ component_name: nil,
18
+ root_element_classes: nil,
19
+ root_element_attributes_classes: nil,
20
+ root_element_html_class: nil,
21
+ html_options_class: nil,
22
+ classes_prop: nil,
23
+ stimulus_classes: nil,
24
+ stimulus_class_names: nil,
25
+ tailwind_merger: nil
26
+ )
27
+ parts = []
28
+ parts << component_name if component_name
29
+
30
+ if html_options_class
31
+ parts.concat(Array.wrap(html_options_class))
32
+ elsif root_element_html_class
33
+ parts.concat(Array.wrap(root_element_html_class))
34
+ elsif root_element_attributes_classes
35
+ parts.concat(Array.wrap(root_element_attributes_classes))
36
+ elsif root_element_classes
37
+ parts.concat(Array.wrap(root_element_classes))
38
+ end
39
+
40
+ parts.concat(Array.wrap(classes_prop)) if classes_prop
41
+
42
+ parts.compact!
43
+
44
+ if stimulus_classes && stimulus_class_names && !stimulus_class_names.empty?
45
+ parts.concat(stimulus_class_css(stimulus_classes, stimulus_class_names))
46
+ end
47
+
48
+ flattened = parts.flat_map { |s| s.to_s.split(/\s+/) }.reject(&:empty?)
49
+ deduped = flattened.uniq
50
+ return nil if deduped.empty?
51
+
52
+ joined = deduped.join(CLASSNAME_SEPARATOR)
53
+ tailwind_merger ? tailwind_merger.merge(joined) : joined
54
+ end
55
+
56
+ def stimulus_class_css(class_maps, names)
57
+ names_set = names.map { |n| n.to_s.dasherize }.to_set
58
+ class_maps.select { |cm| names_set.include?(cm.name) }.map(&:css)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ module Internals
5
+ # One unresolved DSL entry; the Resolver parses it into typed Stimulus
6
+ # value objects at instance init time.
7
+ Declaration = Data.define(:args, :when_proc, :meta) do
8
+ def self.of(*args, when_proc: nil, **meta)
9
+ new(args: args.freeze, when_proc: when_proc, meta: meta.freeze)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "declaration"
4
+
5
+ module Vident
6
+ module Internals
7
+ # Frozen per-class aggregate of raw `Declaration` entries from `stimulus do` blocks.
8
+ # Keyed kinds use `[key, Declaration]` pairs so later same-key entries replace earlier ones;
9
+ # positional kinds are flat arrays where later blocks append.
10
+ class Declarations < Data.define(
11
+ :controllers,
12
+ :actions,
13
+ :targets,
14
+ :outlets,
15
+ :values,
16
+ :params,
17
+ :class_maps,
18
+ :values_from_props
19
+ )
20
+ EMPTY_ARRAY = [].freeze
21
+
22
+ def self.empty = @empty ||= new(
23
+ controllers: EMPTY_ARRAY,
24
+ actions: EMPTY_ARRAY,
25
+ targets: EMPTY_ARRAY,
26
+ outlets: EMPTY_ARRAY,
27
+ values: EMPTY_ARRAY,
28
+ params: EMPTY_ARRAY,
29
+ class_maps: EMPTY_ARRAY,
30
+ values_from_props: EMPTY_ARRAY
31
+ ).freeze
32
+
33
+ def any?
34
+ !controllers.empty? || !actions.empty? || !targets.empty? ||
35
+ !outlets.empty? || !values.empty? || !params.empty? ||
36
+ !class_maps.empty? || !values_from_props.empty?
37
+ end
38
+
39
+ def merge(other)
40
+ self.class.new(
41
+ controllers: concat_positional(controllers, other.controllers),
42
+ actions: concat_positional(actions, other.actions),
43
+ targets: concat_positional(targets, other.targets),
44
+ outlets: merge_keyed(outlets, other.outlets),
45
+ values: merge_keyed(values, other.values),
46
+ params: merge_keyed(params, other.params),
47
+ class_maps: merge_keyed(class_maps, other.class_maps),
48
+ values_from_props: (values_from_props + other.values_from_props).uniq.freeze
49
+ )
50
+ end
51
+
52
+ private
53
+
54
+ def concat_positional(a, b) = (a + b).freeze
55
+
56
+ def merge_keyed(a, b)
57
+ merged = {}
58
+ a.each { |(k, d)| merged[k] = d }
59
+ b.each { |(k, d)| merged[k] = d }
60
+ merged.to_a.freeze
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "plan"
5
+
6
+ module Vident
7
+ module Internals
8
+ # Per-instance mutable accumulator; seals into a frozen Plan once rendering begins.
9
+ class Draft
10
+ def initialize(**collections)
11
+ @collections = Registry.names.to_h { |name| [name, []] }
12
+ collections.each { |k, v| @collections[k] = v.dup if @collections.key?(k) }
13
+ @sealed = false
14
+ end
15
+
16
+ Registry.each do |kind|
17
+ define_method(kind.name) { @collections[kind.name] }
18
+
19
+ define_method(:"add_#{kind.name}") do |value_or_values|
20
+ raise_if_sealed!
21
+ Array(value_or_values).each { |v| @collections[kind.name] << v }
22
+ self
23
+ end
24
+ end
25
+
26
+ def sealed? = @sealed
27
+
28
+ def seal!
29
+ return @plan if @sealed
30
+ @sealed = true
31
+ @collections.each_value(&:freeze)
32
+ @collections.freeze
33
+ @plan = Plan.new(**@collections)
34
+ end
35
+
36
+ def plan = seal!
37
+
38
+ private
39
+
40
+ def raise_if_sealed!
41
+ return unless @sealed
42
+ raise ::Vident::StateError,
43
+ "cannot modify stimulus attributes after rendering has begun"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "declaration"
4
+ require_relative "declarations"
5
+ require_relative "action_builder"
6
+ require_relative "target_builder"
7
+
8
+ module Vident
9
+ module Internals
10
+ # Block receiver for `stimulus do ... end`. Records raw Declaration entries;
11
+ # parsing into typed Stimulus value objects is deferred to the Resolver.
12
+ class DSL
13
+ attr_reader :caller_location
14
+
15
+ def initialize(caller_location: nil)
16
+ @caller_location = caller_location
17
+ @controllers = []
18
+ @actions = []
19
+ @targets = []
20
+ @outlets = []
21
+ @values = []
22
+ @params = []
23
+ @class_maps = []
24
+ @values_from_props = []
25
+ end
26
+
27
+ # ---- plural (kwargs) forms --------------------------------------
28
+
29
+ def controllers(*args)
30
+ args.each do |arg|
31
+ case arg
32
+ in Array
33
+ controller(*arg)
34
+ else
35
+ controller(arg)
36
+ end
37
+ end
38
+ self
39
+ end
40
+
41
+ def actions(*names)
42
+ names.each do |name|
43
+ case name
44
+ in Array
45
+ action(*name)
46
+ else
47
+ action(name)
48
+ end
49
+ end
50
+ self
51
+ end
52
+
53
+ def targets(*names)
54
+ names.each do |name|
55
+ case name
56
+ in Array
57
+ target(*name)
58
+ else
59
+ target(name)
60
+ end
61
+ end
62
+ self
63
+ end
64
+
65
+ def values(**hash)
66
+ hash.each { |k, v| record_keyed(@values, k, v) }
67
+ self
68
+ end
69
+
70
+ def params(**hash)
71
+ hash.each { |k, v| record_keyed(@params, k, v) }
72
+ self
73
+ end
74
+
75
+ def classes(**hash)
76
+ hash.each { |k, v| record_keyed(@class_maps, k, v) }
77
+ self
78
+ end
79
+
80
+ # Positional Hash arg supports keys like `"admin--users"` that can't be Ruby kwargs.
81
+ def outlets(positional = nil, **hash)
82
+ if positional.is_a?(Hash)
83
+ positional.each { |k, v| record_keyed(@outlets, k, v) }
84
+ elsif !positional.nil?
85
+ raise ArgumentError, "outlets: positional arg must be a Hash, got #{positional.class}"
86
+ end
87
+ hash.each { |k, v| record_keyed(@outlets, k, v) }
88
+ self
89
+ end
90
+
91
+ def values_from_props(*names)
92
+ @values_from_props.concat(names.map(&:to_sym))
93
+ self
94
+ end
95
+
96
+ # ---- singular forms --------------------------------------------
97
+
98
+ def controller(*args, **meta)
99
+ @controllers << Declaration.of(*args, **meta)
100
+ self
101
+ end
102
+
103
+ def action(*args, **meta)
104
+ builder = ActionBuilder.new(*args, **meta)
105
+ @actions << builder
106
+ builder
107
+ end
108
+
109
+ def target(*args)
110
+ builder = TargetBuilder.new(*args)
111
+ @targets << builder
112
+ builder
113
+ end
114
+
115
+ def value(name, *args, **meta)
116
+ entry = [name, Declaration.of(*args, **meta)]
117
+ replace_or_append(@values, entry)
118
+ self
119
+ end
120
+
121
+ def param(name, *args, **meta)
122
+ entry = [name, Declaration.of(*args, **meta)]
123
+ replace_or_append(@params, entry)
124
+ self
125
+ end
126
+
127
+ def outlet(name, *args, **meta)
128
+ entry = [name, Declaration.of(*args, **meta)]
129
+ replace_or_append(@outlets, entry)
130
+ self
131
+ end
132
+
133
+ def class_map(name, *args, **meta)
134
+ entry = [name, Declaration.of(*args, **meta)]
135
+ replace_or_append(@class_maps, entry)
136
+ self
137
+ end
138
+
139
+ # ---- folding ----------------------------------------------------
140
+
141
+ def to_declarations
142
+ Declarations.new(
143
+ controllers: @controllers.dup.freeze,
144
+ actions: @actions.map(&:to_declaration).freeze,
145
+ targets: @targets.map(&:to_declaration).freeze,
146
+ outlets: @outlets.dup.freeze,
147
+ values: @values.dup.freeze,
148
+ params: @params.dup.freeze,
149
+ class_maps: @class_maps.dup.freeze,
150
+ values_from_props: @values_from_props.dup.freeze
151
+ ).freeze
152
+ end
153
+
154
+ private
155
+
156
+ def record_keyed(bucket, key, value)
157
+ entry = [key, Declaration.of(value)]
158
+ replace_or_append(bucket, entry)
159
+ end
160
+
161
+ def replace_or_append(bucket, entry)
162
+ key = entry.first
163
+ idx = bucket.index { |(k, _)| k == key }
164
+ if idx
165
+ bucket[idx] = entry
166
+ else
167
+ bucket << entry
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+
5
+ module Vident
6
+ module Internals
7
+ Plan = Data.define(*Registry.names)
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../stimulus/controller"
4
+ require_relative "../stimulus/action"
5
+ require_relative "../stimulus/target"
6
+ require_relative "../stimulus/outlet"
7
+ require_relative "../stimulus/value"
8
+ require_relative "../stimulus/param"
9
+ require_relative "../stimulus/class_map"
10
+
11
+ module Vident
12
+ module Internals
13
+ module Registry
14
+ Kind = Data.define(:name, :plural_name, :singular_name, :value_class, :keyed) do
15
+ alias_method :keyed?, :keyed
16
+ end
17
+
18
+ KINDS = [
19
+ Kind.new(:controllers, :controllers, :controller, Vident::Stimulus::Controller, false),
20
+ Kind.new(:actions, :actions, :action, Vident::Stimulus::Action, false),
21
+ Kind.new(:targets, :targets, :target, Vident::Stimulus::Target, false),
22
+ Kind.new(:outlets, :outlets, :outlet, Vident::Stimulus::Outlet, true),
23
+ Kind.new(:values, :values, :value, Vident::Stimulus::Value, true),
24
+ Kind.new(:params, :params, :param, Vident::Stimulus::Param, true),
25
+ Kind.new(:class_maps, :classes, :class, Vident::Stimulus::ClassMap, true)
26
+ ].freeze
27
+
28
+ BY_NAME = KINDS.to_h { |k| [k.name, k] }.freeze
29
+
30
+ def self.fetch(name) = BY_NAME.fetch(name)
31
+
32
+ def self.each(&block) = KINDS.each(&block)
33
+
34
+ def self.names = BY_NAME.keys
35
+ end
36
+ end
37
+ end