vident 1.0.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +4 -1
- data/lib/vident/component_attribute_resolver.rb +27 -8
- data/lib/vident/component_class_lists.rb +7 -1
- data/lib/vident/stimulus_builder.rb +28 -11
- data/lib/vident/stimulus_helper.rb +4 -4
- data/lib/vident/version.rb +1 -1
- data/lib/vident2/caching.rb +93 -0
- data/lib/vident2/component.rb +538 -0
- data/lib/vident2/engine.rb +18 -0
- data/lib/vident2/error.rb +30 -0
- data/lib/vident2/internals/action_builder.rb +101 -0
- data/lib/vident2/internals/attribute_writer.rb +22 -0
- data/lib/vident2/internals/class_list_builder.rb +79 -0
- data/lib/vident2/internals/declaration.rb +17 -0
- data/lib/vident2/internals/declarations.rb +76 -0
- data/lib/vident2/internals/draft.rb +60 -0
- data/lib/vident2/internals/dsl.rb +198 -0
- data/lib/vident2/internals/plan.rb +12 -0
- data/lib/vident2/internals/registry.rb +41 -0
- data/lib/vident2/internals/resolver.rb +306 -0
- data/lib/vident2/internals/target_builder.rb +29 -0
- data/lib/vident2/phlex/html.rb +84 -0
- data/lib/vident2/phlex.rb +9 -0
- data/lib/vident2/stimulus/action.rb +140 -0
- data/lib/vident2/stimulus/class_map.rb +69 -0
- data/lib/vident2/stimulus/collection.rb +42 -0
- data/lib/vident2/stimulus/controller.rb +59 -0
- data/lib/vident2/stimulus/naming.rb +26 -0
- data/lib/vident2/stimulus/null.rb +16 -0
- data/lib/vident2/stimulus/outlet.rb +113 -0
- data/lib/vident2/stimulus/param.rb +62 -0
- data/lib/vident2/stimulus/target.rb +57 -0
- data/lib/vident2/stimulus/value.rb +77 -0
- data/lib/vident2/tailwind.rb +19 -0
- data/lib/vident2/version.rb +5 -0
- data/lib/vident2/view_component/base.rb +124 -0
- data/lib/vident2/view_component.rb +9 -0
- data/lib/vident2.rb +50 -0
- data/skills/vident/SKILL.md +11 -2
- data/skills/vident/api-reference.md +518 -0
- data/skills/vident/examples.md +492 -0
- 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,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('"', """).gsub("'", "'")
|
|
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
|
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"
|
data/skills/vident/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|