vident 1.0.1 → 1.0.2

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +4 -1
  4. data/lib/vident/component_attribute_resolver.rb +27 -8
  5. data/lib/vident/component_class_lists.rb +3 -0
  6. data/lib/vident/stimulus_builder.rb +28 -11
  7. data/lib/vident/stimulus_helper.rb +4 -4
  8. data/lib/vident/version.rb +1 -1
  9. data/lib/vident2/caching.rb +93 -0
  10. data/lib/vident2/component.rb +538 -0
  11. data/lib/vident2/engine.rb +18 -0
  12. data/lib/vident2/error.rb +30 -0
  13. data/lib/vident2/internals/action_builder.rb +101 -0
  14. data/lib/vident2/internals/attribute_writer.rb +22 -0
  15. data/lib/vident2/internals/class_list_builder.rb +79 -0
  16. data/lib/vident2/internals/declaration.rb +17 -0
  17. data/lib/vident2/internals/declarations.rb +76 -0
  18. data/lib/vident2/internals/draft.rb +60 -0
  19. data/lib/vident2/internals/dsl.rb +198 -0
  20. data/lib/vident2/internals/plan.rb +12 -0
  21. data/lib/vident2/internals/registry.rb +41 -0
  22. data/lib/vident2/internals/resolver.rb +306 -0
  23. data/lib/vident2/internals/target_builder.rb +29 -0
  24. data/lib/vident2/phlex/html.rb +84 -0
  25. data/lib/vident2/phlex.rb +9 -0
  26. data/lib/vident2/stimulus/action.rb +140 -0
  27. data/lib/vident2/stimulus/class_map.rb +69 -0
  28. data/lib/vident2/stimulus/collection.rb +42 -0
  29. data/lib/vident2/stimulus/controller.rb +59 -0
  30. data/lib/vident2/stimulus/naming.rb +26 -0
  31. data/lib/vident2/stimulus/null.rb +16 -0
  32. data/lib/vident2/stimulus/outlet.rb +113 -0
  33. data/lib/vident2/stimulus/param.rb +62 -0
  34. data/lib/vident2/stimulus/target.rb +57 -0
  35. data/lib/vident2/stimulus/value.rb +77 -0
  36. data/lib/vident2/tailwind.rb +19 -0
  37. data/lib/vident2/version.rb +5 -0
  38. data/lib/vident2/view_component/base.rb +124 -0
  39. data/lib/vident2/view_component.rb +9 -0
  40. data/lib/vident2.rb +50 -0
  41. data/skills/vident/SKILL.md +11 -2
  42. data/skills/vident/api-reference.md +518 -0
  43. data/skills/vident/examples.md +492 -0
  44. metadata +35 -1
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "literal"
4
+ require_relative "naming"
5
+
6
+ module Vident2
7
+ module Stimulus
8
+ # `data-controller` fragment. Fields:
9
+ # - `path` : raw underscored path (e.g. `"admin/user_card_component"`)
10
+ # - `name` : dasherized/joined form (e.g. `"admin--user-card-component"`)
11
+ # - `alias_name`: optional Symbol the DSL uses to refer back to this
12
+ # controller from other entries (`action(:save, on: :admin)`).
13
+ class Controller < ::Literal::Data
14
+ prop :path, String
15
+ prop :name, String
16
+ prop :alias_name, _Nilable(Symbol), default: nil
17
+
18
+ # `.parse(path = nil, as: nil, implied:)`
19
+ #
20
+ # No positional arg -> clone the implied controller (for unambiguous
21
+ # "refer to my own controller" use).
22
+ #
23
+ # One positional arg (String | Symbol) -> explicit controller path.
24
+ # `implied:` is unused in that branch but is accepted for uniformity
25
+ # with the other kinds' `.parse` signatures.
26
+ def self.parse(*args, as: nil, implied:, component_id: nil)
27
+ case args.size
28
+ when 0
29
+ new(path: implied.path, name: implied.name, alias_name: as)
30
+ when 1
31
+ raw = args[0]
32
+ path = raw.to_s
33
+ new(path: path, name: Naming.stimulize_path(path), alias_name: as)
34
+ else
35
+ raise ::Vident2::ParseError, "Controller.parse: expected 0 or 1 positional args, got #{args.size}"
36
+ end
37
+ end
38
+
39
+ def identifier = name
40
+
41
+ def to_s = name
42
+
43
+ def to_data_pair = [:controller, name]
44
+
45
+ def to_h = {controller: name}
46
+ # Ruby's `{**x}` splat calls #to_hash; alias so users can splat the
47
+ # data-attr pair directly into a tag's `data:` option.
48
+ alias_method :to_hash, :to_h
49
+
50
+ # Space-joined. Order preserved, duplicates kept (caller dedups).
51
+ def self.to_data_hash(controllers)
52
+ return {} if controllers.empty?
53
+ joined = controllers.map(&:name).reject(&:empty?).join(" ")
54
+ return {} if joined.empty?
55
+ {controller: joined}
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ module Vident2
6
+ module Stimulus
7
+ # Pure naming helpers shared across value classes. No state, no
8
+ # inheritance — the v1 `StimulusAttributeBase` tree goes away in V2;
9
+ # each value class is a `Literal::Data` and just calls these module
10
+ # functions directly.
11
+ module Naming
12
+ module_function
13
+
14
+ # `"admin/users"` -> `"admin--users"`. Symbol or String accepted.
15
+ def stimulize_path(path)
16
+ path.to_s.split("/").map(&:dasherize).join("--")
17
+ end
18
+
19
+ # `:my_thing` -> `"myThing"`. Used for action method names and
20
+ # target names (not for attribute keys, which dasherize instead).
21
+ def js_name(name)
22
+ name.to_s.camelize(:lower)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident2
4
+ module Stimulus
5
+ # Sentinel that serialises to the literal string "null". Use in
6
+ # `value` / `param` positions where the Stimulus side expects a
7
+ # JSON-parsed JS `null` (Object / Array value types).
8
+ #
9
+ # A bare `nil` drops the attribute entirely. Reach for Null only when
10
+ # you need an explicit `"null"` in the emitted HTML.
11
+ Null = Object.new
12
+ def Null.inspect = "Vident2::Stimulus::Null"
13
+ def Null.to_s = "null"
14
+ Null.freeze
15
+ end
16
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "literal"
4
+ require_relative "naming"
5
+ require_relative "controller"
6
+
7
+ module Vident2
8
+ module Stimulus
9
+ # `data-<ctrl>-<name>-outlet` fragment. `selector` is the CSS selector
10
+ # the Stimulus runtime uses to resolve the outlet on the page.
11
+ #
12
+ # Auto-selector: `"#<component_id> [data-controller~=<outlet>]"`. If
13
+ # `component_id` is nil (host id not yet known) the `#<id>` prefix is
14
+ # omitted — caller must backfill if needed.
15
+ class Outlet < ::Literal::Data
16
+ prop :controller, Controller
17
+ prop :name, String
18
+ prop :selector, String
19
+
20
+ # `.parse(*args, implied:, component_id:)` grammar mirrors v1:
21
+ # (Symbol) -> outlet name on implied, auto-selector
22
+ # (String) -> outlet identifier, auto-selector
23
+ # (Array[name, sel]) -> explicit [name, selector] pair
24
+ # (Symbol|String, String) -> (name, explicit selector)
25
+ # (String, Symbol, String) -> (ctrl_path, name, selector)
26
+ # (<component instance>) -> grab stimulus_identifier and build auto-selector
27
+ def self.parse(*args, implied:, component_id: nil)
28
+ case args
29
+ in [Symbol => sym]
30
+ name = sym.to_s.dasherize
31
+ new(
32
+ controller: implied,
33
+ name: name,
34
+ selector: auto_selector(name, component_id: component_id)
35
+ )
36
+ in [String => str]
37
+ name = str.dasherize
38
+ new(
39
+ controller: implied,
40
+ name: name,
41
+ selector: auto_selector(str, component_id: component_id)
42
+ )
43
+ in [[identifier, selector]]
44
+ new(
45
+ controller: implied,
46
+ name: identifier.to_s.dasherize,
47
+ selector: selector
48
+ )
49
+ in [Symbol => sym, String => sel]
50
+ new(
51
+ controller: implied,
52
+ name: sym.to_s.dasherize,
53
+ selector: sel
54
+ )
55
+ in [String => id_or_name, String => sel]
56
+ new(
57
+ controller: implied,
58
+ name: id_or_name.dasherize,
59
+ selector: sel
60
+ )
61
+ in [String => ctrl_path, Symbol => sym, String => sel]
62
+ new(
63
+ controller: Controller.parse(ctrl_path, implied: implied),
64
+ name: sym.to_s.dasherize,
65
+ selector: sel
66
+ )
67
+ else
68
+ component_like = args.size == 1 ? args[0] : nil
69
+ if component_like && component_like.respond_to?(:stimulus_identifier)
70
+ ident = component_like.stimulus_identifier
71
+ new(
72
+ controller: implied,
73
+ name: ident,
74
+ selector: auto_selector(ident, component_id: component_id)
75
+ )
76
+ else
77
+ raise ::Vident2::ParseError, "Outlet.parse: invalid arguments #{args.inspect}"
78
+ end
79
+ end
80
+ end
81
+
82
+ def self.auto_selector(outlet_identifier, component_id:)
83
+ prefix = component_id ? "##{css_escape_ident(component_id)} " : ""
84
+ "#{prefix}[data-controller~=#{outlet_identifier}]"
85
+ end
86
+
87
+ # CSS-escapes anything outside the bare identifier alphabet
88
+ # (`A-Za-z0-9_-`) using the `\HH ` hex form (with trailing space
89
+ # delimiter). Bare `\<char>` works for many punctuation cases but
90
+ # not for whitespace, parens, or non-ASCII — the hex form is
91
+ # always valid per CSS Syntax §4.3.7.
92
+ def self.css_escape_ident(id)
93
+ id.to_s.gsub(/[^A-Za-z0-9_-]/) { |c| "\\#{c.ord.to_s(16)} " }
94
+ end
95
+
96
+ def to_s = selector
97
+
98
+ def data_attribute_key = :"#{controller.name}-#{name}-outlet"
99
+
100
+ def to_data_pair = [data_attribute_key, selector]
101
+
102
+ def to_h = {data_attribute_key => selector}
103
+ alias_method :to_hash, :to_h
104
+
105
+ def self.to_data_hash(outlets)
106
+ outlets.each_with_object({}) do |o, acc|
107
+ key, sel = o.to_data_pair
108
+ acc[key] = sel
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "literal"
5
+ require_relative "naming"
6
+ require_relative "null"
7
+ require_relative "controller"
8
+
9
+ module Vident2
10
+ module Stimulus
11
+ # `data-<ctrl>-<name>-param` fragment — same shape as Value, distinct
12
+ # semantics on the JS side (read via `event.params.<camel>`).
13
+ class Param < ::Literal::Data
14
+ prop :controller, Controller
15
+ prop :name, String
16
+ prop :serialized, String
17
+
18
+ def self.parse(*args, implied:, component_id: nil)
19
+ case args
20
+ in [Symbol => name_sym, raw]
21
+ new(
22
+ controller: implied,
23
+ name: name_sym.to_s.dasherize,
24
+ serialized: serialize(raw)
25
+ )
26
+ in [String => ctrl_path, Symbol => name_sym, raw]
27
+ new(
28
+ controller: Controller.parse(ctrl_path, implied: implied),
29
+ name: name_sym.to_s.dasherize,
30
+ serialized: serialize(raw)
31
+ )
32
+ else
33
+ raise ::Vident2::ParseError, "Param.parse: invalid arguments #{args.inspect}"
34
+ end
35
+ end
36
+
37
+ def self.serialize(raw)
38
+ raise ::Vident2::ParseError, "Param.serialize: nil is not serializable — filter nil upstream" if raw.nil?
39
+ case raw
40
+ when Array, Hash then raw.to_json
41
+ else raw.to_s
42
+ end
43
+ end
44
+
45
+ def to_s = serialized
46
+
47
+ def data_attribute_key = :"#{controller.name}-#{name}-param"
48
+
49
+ def to_data_pair = [data_attribute_key, serialized]
50
+
51
+ def to_h = {data_attribute_key => serialized}
52
+ alias_method :to_hash, :to_h
53
+
54
+ def self.to_data_hash(params)
55
+ params.each_with_object({}) do |p, acc|
56
+ key, str = p.to_data_pair
57
+ acc[key] = str
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "literal"
4
+ require_relative "naming"
5
+ require_relative "controller"
6
+
7
+ module Vident2
8
+ module Stimulus
9
+ # `data-<ctrl>-target` fragment. One per target reference; the Array
10
+ # aggregator groups by controller and space-joins same-key values.
11
+ class Target < ::Literal::Data
12
+ prop :controller, Controller
13
+ prop :name, String
14
+
15
+ # `.parse(*args, implied:)`:
16
+ # (Symbol) -> target `:name` on implied controller
17
+ # (String) -> target name as-is on implied (already js-cased)
18
+ # (String, Symbol) -> explicit (controller_path, target_name)
19
+ def self.parse(*args, implied:, component_id: nil)
20
+ case args
21
+ in [Symbol => sym]
22
+ new(controller: implied, name: Naming.js_name(sym))
23
+ in [String => str]
24
+ new(controller: implied, name: str)
25
+ in [String => ctrl_path, Symbol => sym]
26
+ new(
27
+ controller: Controller.parse(ctrl_path, implied: implied),
28
+ name: Naming.js_name(sym)
29
+ )
30
+ else
31
+ raise ::Vident2::ParseError, "Target.parse: invalid arguments #{args.inspect}"
32
+ end
33
+ end
34
+
35
+ def to_s = name
36
+
37
+ def data_attribute_key = :"#{controller.name}-target"
38
+
39
+ def to_data_pair = [data_attribute_key, name]
40
+
41
+ # Splat target for inline `data: {**target.to_h}` usage.
42
+ def to_h = {data_attribute_key => name}
43
+ alias_method :to_hash, :to_h
44
+
45
+ # Same-key concat with space. Example:
46
+ # Target(row) -> "foo-target" => "row"
47
+ # Target(row) + Target(cell) -> "foo-target" => "row cell"
48
+ # Target(row) + Target(x, on: bar) -> {"foo-target"=>"row", "bar-target"=>"x"}
49
+ def self.to_data_hash(targets)
50
+ targets.each_with_object({}) do |t, acc|
51
+ key, value = t.to_data_pair
52
+ acc[key] = acc.key?(key) ? "#{acc[key]} #{value}" : value
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "literal"
5
+ require_relative "naming"
6
+ require_relative "controller"
7
+ require_relative "null"
8
+
9
+ module Vident2
10
+ module Stimulus
11
+ # `data-<ctrl>-<name>-value` fragment. Holds the *serialised* form
12
+ # (always a String post-parse); Array/Hash inputs go through `to_json`,
13
+ # other non-nil inputs through `to_s`. The `Null` sentinel's `to_s`
14
+ # produces `"null"` naturally. Only `nil` drops at the caller —
15
+ # `false`, blank strings, and empty collections emit their serialised
16
+ # form.
17
+ class Value < ::Literal::Data
18
+ prop :controller, Controller
19
+ prop :name, String
20
+ prop :serialized, String
21
+
22
+ # `.parse(*args, implied:)` grammar:
23
+ # (Symbol, raw) -> implied controller, value named `Symbol`
24
+ # (String, Symbol, raw) -> explicit (controller_path, name, raw)
25
+ #
26
+ # The caller (Resolver / mutator) is responsible for filtering out
27
+ # `nil` before reaching here — see `serialize` for the check.
28
+ def self.parse(*args, implied:, component_id: nil)
29
+ case args
30
+ in [Symbol => name_sym, raw]
31
+ new(
32
+ controller: implied,
33
+ name: name_sym.to_s.dasherize,
34
+ serialized: serialize(raw)
35
+ )
36
+ in [String => ctrl_path, Symbol => name_sym, raw]
37
+ new(
38
+ controller: Controller.parse(ctrl_path, implied: implied),
39
+ name: name_sym.to_s.dasherize,
40
+ serialized: serialize(raw)
41
+ )
42
+ else
43
+ raise ::Vident2::ParseError, "Value.parse: invalid arguments #{args.inspect}"
44
+ end
45
+ end
46
+
47
+ # Raw -> String. `Null` sentinel serialises to `"null"` via its
48
+ # own `to_s`. `nil` should have been filtered upstream; raising
49
+ # here catches misrouted callers early.
50
+ def self.serialize(raw)
51
+ raise ::Vident2::ParseError, "Value.serialize: nil is not serializable — filter nil upstream" if raw.nil?
52
+ case raw
53
+ when Array, Hash then raw.to_json
54
+ else raw.to_s
55
+ end
56
+ end
57
+
58
+ def to_s = serialized
59
+
60
+ def data_attribute_key = :"#{controller.name}-#{name}-value"
61
+
62
+ def to_data_pair = [data_attribute_key, serialized]
63
+
64
+ def to_h = {data_attribute_key => serialized}
65
+ alias_method :to_hash, :to_h
66
+
67
+ # One entry per Value instance. Later-instance same-key wins on
68
+ # collision (Hash#merge semantics).
69
+ def self.to_data_hash(values)
70
+ values.each_with_object({}) do |v, acc|
71
+ key, str = v.to_data_pair
72
+ acc[key] = str
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident2
4
+ # Thin Tailwind CSS merge integration — exposes a lazy per-thread merger
5
+ # instance so the ClassListBuilder can collapse conflicting utilities
6
+ # when ::TailwindMerge::Merger is available.
7
+ module Tailwind
8
+ def tailwind_merger
9
+ return unless tailwind_merge_available?
10
+ return @tailwind_merger if defined?(@tailwind_merger)
11
+
12
+ @tailwind_merger = Thread.current[:vident2_tailwind_merger] ||= ::TailwindMerge::Merger.new
13
+ end
14
+
15
+ def tailwind_merge_available?
16
+ defined?(::TailwindMerge::Merger) && ::TailwindMerge::Merger.respond_to?(:new)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident2
4
+ VERSION = "2.0.0.alpha0"
5
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Vident2
6
+ module ViewComponent
7
+ class Base < ::ViewComponent::Base
8
+ include ::Vident2::Component
9
+
10
+ class << self
11
+ def cache_component_modified_time
12
+ cache_sidecar_view_modified_time + cache_rb_component_modified_time
13
+ end
14
+
15
+ def cache_sidecar_view_modified_time
16
+ ::File.exist?(template_path) ? ::File.mtime(template_path).to_i.to_s : ""
17
+ end
18
+
19
+ def cache_rb_component_modified_time
20
+ ::File.exist?(component_path) ? ::File.mtime(component_path).to_i.to_s : ""
21
+ end
22
+
23
+ def template_path
24
+ extensions = [".html.erb", ".erb", ".html.haml", ".haml", ".html.slim", ".slim"]
25
+ base_path = Rails.root.join(components_base_path, virtual_path)
26
+
27
+ extensions.each do |ext|
28
+ potential_path = "#{base_path}#{ext}"
29
+ return potential_path if File.exist?(potential_path)
30
+ end
31
+
32
+ Rails.root.join(components_base_path, "#{virtual_path}.html.erb").to_s
33
+ end
34
+
35
+ def component_path
36
+ Rails.root.join(components_base_path, "#{virtual_path}.rb").to_s
37
+ end
38
+
39
+ def components_base_path
40
+ ::Rails.configuration.view_component.view_component_path || "app/components"
41
+ end
42
+ end
43
+
44
+ SELF_CLOSING_TAGS = Set[*%i[area base br col embed hr img input link meta param source track wbr]].freeze
45
+
46
+ # ViewComponent lifecycle hook: resolve stimulus DSL procs now that
47
+ # `@view_context` is set (so `helpers` works inside them).
48
+ def before_render
49
+ resolve_stimulus_attributes_at_render_time
50
+ super
51
+ end
52
+
53
+ # Same block-capture-first ordering as the Phlex adapter. Children
54
+ # instantiated inside the block can still mutate this instance's
55
+ # Draft (outlet-host pattern) before we seal.
56
+ def root_element(**overrides, &block)
57
+ tag_type = root_element_tag_type
58
+ child_content = view_context.capture(self, &block) if block
59
+ options = build_root_element_attributes(overrides)
60
+ if SELF_CLOSING_TAGS.include?(tag_type)
61
+ view_context.tag(tag_type, options)
62
+ else
63
+ view_context.content_tag(tag_type, child_content, options)
64
+ end
65
+ end
66
+
67
+ # Inline ERB helpers: emit `data-*="..."` attribute strings for the
68
+ # given stimulus input. Thin wrappers around the plural/singular
69
+ # parsers — Collection#to_h is the splat target used by v1's helpers
70
+ # too. Returns an ActiveSupport::SafeBuffer.
71
+ def as_stimulus_targets(...) = to_data_attribute_string(**stimulus_targets(...).to_h)
72
+ def as_stimulus_target(...) = to_data_attribute_string(**stimulus_target(...).to_h)
73
+ def as_stimulus_actions(...) = to_data_attribute_string(**stimulus_actions(...).to_h)
74
+ def as_stimulus_action(...) = to_data_attribute_string(**stimulus_action(...).to_h)
75
+ def as_stimulus_controllers(...) = to_data_attribute_string(**stimulus_controllers(...).to_h)
76
+ def as_stimulus_controller(...) = to_data_attribute_string(**stimulus_controller(...).to_h)
77
+ def as_stimulus_outlets(...) = to_data_attribute_string(**stimulus_outlets(...).to_h)
78
+ def as_stimulus_outlet(...) = to_data_attribute_string(**stimulus_outlet(...).to_h)
79
+ def as_stimulus_values(...) = to_data_attribute_string(**stimulus_values(...).to_h)
80
+ def as_stimulus_value(...) = to_data_attribute_string(**stimulus_value(...).to_h)
81
+ def as_stimulus_params(...) = to_data_attribute_string(**stimulus_params(...).to_h)
82
+ def as_stimulus_param(...) = to_data_attribute_string(**stimulus_param(...).to_h)
83
+ def as_stimulus_classes(...) = to_data_attribute_string(**stimulus_classes(...).to_h)
84
+ def as_stimulus_class(...) = to_data_attribute_string(**stimulus_class(...).to_h)
85
+
86
+ # Short aliases — same semantics as the singular `as_stimulus_*` above.
87
+ alias_method :as_target, :as_stimulus_target
88
+ alias_method :as_action, :as_stimulus_action
89
+ alias_method :as_controller, :as_stimulus_controller
90
+ alias_method :as_outlet, :as_stimulus_outlet
91
+ alias_method :as_value, :as_stimulus_value
92
+ alias_method :as_param, :as_stimulus_param
93
+ alias_method :as_class, :as_stimulus_class
94
+
95
+ private
96
+
97
+ def generate_child_element(tag_name, stimulus_data_attributes, options, &block)
98
+ options[:data] ||= {}
99
+ options[:data].merge!(stimulus_data_attributes)
100
+ if SELF_CLOSING_TAGS.include?(tag_name.to_sym)
101
+ view_context.tag(tag_name, options)
102
+ elsif block
103
+ view_context.content_tag(tag_name, view_context.capture(&block), options)
104
+ else
105
+ view_context.content_tag(tag_name, nil, options)
106
+ end
107
+ end
108
+
109
+ def escape_attribute_name_for_html(name)
110
+ name.to_s.gsub(/[^a-zA-Z0-9\-_]/, "").tr("_", "-")
111
+ end
112
+
113
+ def escape_attribute_value_for_html(value)
114
+ value.to_s.gsub('"', "&quot;").gsub("'", "&#39;")
115
+ end
116
+
117
+ def to_data_attribute_string(**attributes)
118
+ attributes.map { |key, value| "data-#{escape_attribute_name_for_html(key)}=\"#{escape_attribute_value_for_html(value)}\"" }
119
+ .join(" ")
120
+ .html_safe
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "view_component"
4
+ require "vident2/view_component/base"
5
+
6
+ module Vident2
7
+ module ViewComponent
8
+ end
9
+ end
data/lib/vident2.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/concern"
5
+ require "literal"
6
+
7
+ require "vident2/version"
8
+
9
+ # Vident 2.0 — synthesis rearchitecture per doc/reviews/wave-4-synthesis.md.
10
+ # Built side-by-side with Vident 1.x during development; renamed to Vident
11
+ # at release.
12
+ module Vident2
13
+ end
14
+
15
+ # V1 still ships alongside V2 — Vident::StableId is aliased as
16
+ # Vident2::StableId in lib/vident2/stable_id.rb so the V2 namespace stays
17
+ # self-referential.
18
+ require "vident"
19
+
20
+ require "vident2/engine" if defined?(Rails::Engine)
21
+ require "vident2/error"
22
+ require "vident2/stable_id"
23
+
24
+ require "vident2/stimulus/naming"
25
+ require "vident2/stimulus/null"
26
+ require "vident2/stimulus/controller"
27
+ require "vident2/stimulus/action"
28
+ require "vident2/stimulus/target"
29
+ require "vident2/stimulus/outlet"
30
+ require "vident2/stimulus/value"
31
+ require "vident2/stimulus/param"
32
+ require "vident2/stimulus/class_map"
33
+ require "vident2/stimulus/collection"
34
+ require "vident2/internals/registry"
35
+ require "vident2/internals/declaration"
36
+ require "vident2/internals/declarations"
37
+ require "vident2/internals/dsl"
38
+ require "vident2/internals/draft"
39
+ require "vident2/internals/plan"
40
+ require "vident2/internals/resolver"
41
+ require "vident2/internals/attribute_writer"
42
+ require "vident2/internals/class_list_builder"
43
+
44
+ require "vident2/tailwind"
45
+ require "vident2/caching"
46
+
47
+ require "vident2/component"
48
+
49
+ require "vident2/phlex"
50
+ require "vident2/view_component"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Vident
3
3
  description: This skill should be used when writing or editing Rails view components in a project that depends on `vident`, `vident-view_component`, or `vident-phlex` — i.e. any class inheriting from `Vident::ViewComponent::Base` or `Vident::Phlex::HTML`, any paired `*_component_controller.js` Stimulus file next to such a component, or the `stimulus_*` props / `stimulus do ... end` DSL / `child_element` / `root_element` / `class_list_for_stimulus_classes` / `Vident::StimulusNull` / `Vident::StableId` APIs. It also covers the `bin/rails generate vident:install` initializer and the per-request ID seeding it installs on `ApplicationController`.
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  ---
6
6
 
7
7
  # Vident
@@ -10,6 +10,8 @@ Vident is a thin layer on top of ViewComponent / Phlex that gives a component th
10
10
 
11
11
  A Vident component is always a class with props, a single `root_element`, and (optionally) a `stimulus do ... end` block that declares the controllers, actions, targets, values, classes and outlets its paired JavaScript file needs.
12
12
 
13
+ > **Going deeper.** This file is the everyday reference. For end-to-end walkthroughs (dashboard, forms, slot-based parent/child wiring, ERB helper comparisons), read [`examples.md`](examples.md). For the exhaustive public-API spec (every signature, raise-condition, and argument shape), read [`api-reference.md`](api-reference.md).
14
+
13
15
  ---
14
16
 
15
17
  ## 1. Stimulus → Vident mapping
@@ -419,7 +421,10 @@ Plural (`as_stimulus_targets`, `as_stimulus_actions`, `as_stimulus_values`, `as_
419
421
 
420
422
  Opens a `Vident::StimulusBuilder` instance scoped to the class. It supports `actions`, `targets`, `values`, `values_from_props`, `classes`, `outlets`. Multiple `stimulus do` blocks on the same class are merged; a subclass's block is merged with its superclass's (subclass entries appended, values/classes/outlets merged by key, subclass wins on conflicts).
421
423
 
422
- Procs passed anywhere in the DSL are evaluated via `instance_exec` on the **component instance** at render time, so they see `@ivars` and public/private instance methods.
424
+ Procs passed anywhere in the DSL are evaluated via `instance_exec` on the **component instance** at render time (Phlex `before_template` / ViewComponent `before_render`), so they see `@ivars`, public/private instance methods, and the view context.
425
+
426
+ - **Phlex**: `helpers` is deprecated in phlex-rails. Opt in per Rails helper via `include Phlex::Rails::Helpers::NumberWithPrecision` (etc.), or use the `phlex_helpers :number_with_precision, :t, :l` class macro on `Vident::Phlex::HTML` which expands to the matching includes. Then call the helper bare inside the proc — `number_with_precision(@amount, precision: 2)`. See [phlex.fun/rails/helpers](https://www.phlex.fun/rails/helpers) for the full adapter list.
427
+ - **ViewComponent**: `helpers.<method>` and `view_context.<method>` both work.
423
428
 
424
429
  ---
425
430
 
@@ -553,6 +558,8 @@ Inside `<name>OutletConnected`, **do not iterate `this.<name>Outlets`**. Stimulu
553
558
 
554
559
  ## 8. Recipes
555
560
 
561
+ One-liners per task. For worked end-to-end versions (dashboard with outlets, slot trigger, ERB variants) see [`examples.md`](examples.md).
562
+
556
563
  **Click handler on the root** — `stimulus do; actions [:click, :select]; end` + `select(event) {…}` in JS.
557
564
 
558
565
  **Click handler on a child button** — `card.child_element(:button, stimulus_action: [:click, :promote]) { "Promote" }`.
@@ -616,6 +623,8 @@ end
616
623
 
617
624
  ## 9. Key source files
618
625
 
626
+ For the exhaustive public-API listing (every method signature, argument shape, and raise-condition, verified against current code), see [`api-reference.md`](api-reference.md). The files below are useful when you need to read the implementation itself.
627
+
619
628
  - `lib/vident/stimulus_builder.rb` — DSL evaluator.
620
629
  - `lib/vident/stimulus_attributes.rb` — parser for every `stimulus_*` input shape + `as_stimulus_*` helpers' backing.
621
630
  - `lib/vident/stimulus_{action,target,value,outlet,class,controller}.rb` — value objects; read `parse_arguments` to learn the argument shapes.