poetry-core 0.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 +7 -0
- data/CHANGELOG.md +3 -0
- data/DESIGN.md +145 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/THIRD_PARTY_NOTICES.md +507 -0
- data/app/components/poetry/core/box/component.rb +82 -0
- data/app/components/poetry/core/component.rb +390 -0
- data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
- data/app/components/poetry/core/concerns/introspection.rb +533 -0
- data/app/components/poetry/core/concerns/options.rb +262 -0
- data/app/components/poetry/core/concerns/parts.rb +148 -0
- data/app/components/poetry/core/concerns/stimulus.rb +363 -0
- data/app/components/poetry/core/concerns/styles.rb +323 -0
- data/app/components/poetry/core/style.rb +150 -0
- data/app/components/poetry/core/wrapper/component.rb +90 -0
- data/app/javascript/poetry/core/accordion_controller.js +165 -0
- data/app/javascript/poetry/core/action_bar_controller.js +109 -0
- data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
- data/app/javascript/poetry/core/calendar_controller.js +406 -0
- data/app/javascript/poetry/core/carousel_controller.js +167 -0
- data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
- data/app/javascript/poetry/core/checked_controller.js +208 -0
- data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
- data/app/javascript/poetry/core/combobox_controller.js +1095 -0
- data/app/javascript/poetry/core/command_controller.js +510 -0
- data/app/javascript/poetry/core/context_menu_controller.js +204 -0
- data/app/javascript/poetry/core/date_field_controller.js +584 -0
- data/app/javascript/poetry/core/date_picker_controller.js +111 -0
- data/app/javascript/poetry/core/deferred_controller.js +111 -0
- data/app/javascript/poetry/core/dialog_controller.js +206 -0
- data/app/javascript/poetry/core/dismissable_controller.js +182 -0
- data/app/javascript/poetry/core/drawer_controller.js +365 -0
- data/app/javascript/poetry/core/file_input_controller.js +191 -0
- data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
- data/app/javascript/poetry/core/helpers/announce.js +228 -0
- data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
- data/app/javascript/poetry/core/helpers/collection.js +18 -0
- data/app/javascript/poetry/core/helpers/direction.js +15 -0
- data/app/javascript/poetry/core/helpers/escape.js +38 -0
- data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
- data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
- data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
- data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
- data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
- data/app/javascript/poetry/core/helpers/mask.js +283 -0
- data/app/javascript/poetry/core/helpers/portal.js +217 -0
- data/app/javascript/poetry/core/helpers/presence.js +210 -0
- data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
- data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
- data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
- data/app/javascript/poetry/core/helpers/state.js +80 -0
- data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
- data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
- data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
- data/app/javascript/poetry/core/hotkey_controller.js +55 -0
- data/app/javascript/poetry/core/hover_card_controller.js +431 -0
- data/app/javascript/poetry/core/index.js +218 -0
- data/app/javascript/poetry/core/mask_controller.js +500 -0
- data/app/javascript/poetry/core/menu_controller.js +1050 -0
- data/app/javascript/poetry/core/menubar_controller.js +329 -0
- data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
- data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
- data/app/javascript/poetry/core/number_field_controller.js +404 -0
- data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
- data/app/javascript/poetry/core/otp_controller.js +219 -0
- data/app/javascript/poetry/core/popover_controller.js +300 -0
- data/app/javascript/poetry/core/popper_controller.js +439 -0
- data/app/javascript/poetry/core/pressed_controller.js +74 -0
- data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
- data/app/javascript/poetry/core/radio_group_controller.js +197 -0
- data/app/javascript/poetry/core/resizable_controller.js +202 -0
- data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
- data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
- data/app/javascript/poetry/core/search_field_controller.js +95 -0
- data/app/javascript/poetry/core/select_controller.js +924 -0
- data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
- data/app/javascript/poetry/core/sheet_controller.js +24 -0
- data/app/javascript/poetry/core/sidebar_controller.js +272 -0
- data/app/javascript/poetry/core/slider_controller.js +419 -0
- data/app/javascript/poetry/core/state_controller.js +82 -0
- data/app/javascript/poetry/core/table_selection_controller.js +153 -0
- data/app/javascript/poetry/core/tabs_controller.js +151 -0
- data/app/javascript/poetry/core/tag_group_controller.js +134 -0
- data/app/javascript/poetry/core/toast_controller.js +228 -0
- data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
- data/app/javascript/poetry/core/toaster_controller.js +197 -0
- data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
- data/app/javascript/poetry/core/tooltip_controller.js +541 -0
- data/app/javascript/poetry/core/tree_controller.js +265 -0
- data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
- data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
- data/app/views/poetry/core/preview.html.erb +13 -0
- data/config/component_registry.yml +20 -0
- data/config/controllers_manifest.json +1528 -0
- data/config/importmap.rb +11 -0
- data/config/state_vocabulary.json +31 -0
- data/lib/active_model/type/list.rb +33 -0
- data/lib/active_model/type/symbol.rb +36 -0
- data/lib/poetry/core/check/stable_identity.rb +104 -0
- data/lib/poetry/core/check.rb +1445 -0
- data/lib/poetry/core/config.rb +286 -0
- data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
- data/lib/poetry/core/css/bem_merger.rb +45 -0
- data/lib/poetry/core/css/bem_reference.rb +87 -0
- data/lib/poetry/core/css/override_scan.rb +110 -0
- data/lib/poetry/core/css/resolver.rb +161 -0
- data/lib/poetry/core/css/safelist.rb +41 -0
- data/lib/poetry/core/css/tailwind_merger.rb +102 -0
- data/lib/poetry/core/css/template_classes.rb +102 -0
- data/lib/poetry/core/css/theme_coverage.rb +72 -0
- data/lib/poetry/core/css/var_coverage.rb +75 -0
- data/lib/poetry/core/css/verifier.rb +106 -0
- data/lib/poetry/core/design_lint.rb +720 -0
- data/lib/poetry/core/design_md/import.rb +250 -0
- data/lib/poetry/core/design_md.rb +461 -0
- data/lib/poetry/core/engine.rb +71 -0
- data/lib/poetry/core/errors.rb +53 -0
- data/lib/poetry/core/html/attributes.rb +667 -0
- data/lib/poetry/core/icons.rb +157 -0
- data/lib/poetry/core/llms_text.rb +354 -0
- data/lib/poetry/core/page_architectures.rb +304 -0
- data/lib/poetry/core/part_contract.rb +285 -0
- data/lib/poetry/core/preview/abstract.rb +121 -0
- data/lib/poetry/core/preview/base.rb +227 -0
- data/lib/poetry/core/preview/sidecarable.rb +136 -0
- data/lib/poetry/core/preview/template.rb +186 -0
- data/lib/poetry/core/recipe_items.rb +82 -0
- data/lib/poetry/core/registry.rb +363 -0
- data/lib/poetry/core/registry_address.rb +115 -0
- data/lib/poetry/core/registry_client.rb +217 -0
- data/lib/poetry/core/registry_installer.rb +234 -0
- data/lib/poetry/core/registry_items.rb +184 -0
- data/lib/poetry/core/skill_text.rb +288 -0
- data/lib/poetry/core/stable_id.rb +110 -0
- data/lib/poetry/core/stimulus/builder.rb +395 -0
- data/lib/poetry/core/stimulus/declarations.rb +444 -0
- data/lib/poetry/core/stimulus/manifest.rb +68 -0
- data/lib/poetry/core/stimulus/merger.rb +131 -0
- data/lib/poetry/core/stimulus_contract.rb +263 -0
- data/lib/poetry/core/tag_helper.rb +10 -0
- data/lib/poetry/core/template_compile.rb +74 -0
- data/lib/poetry/core/token_import.rb +317 -0
- data/lib/poetry/core/tokens/color.rb +215 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
- data/lib/poetry/core/tokens/generator.rb +211 -0
- data/lib/poetry/core/tokens.rb +117 -0
- data/lib/poetry/core/version.rb +9 -0
- data/lib/poetry/core.rb +52 -0
- data/lib/poetry-core.rb +5 -0
- data/tokens/aliases.css +145 -0
- data/tokens/tailwind-theme.css +47 -0
- data/tokens/tokens.css +78 -0
- data/tokens/tokens.dtcg.json +779 -0
- data/vendor/floating-ui/LICENSE +20 -0
- data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
- data/vendor/shadcn-tailwind/LICENSE +21 -0
- data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
- data/vendor/shadcn-tailwind/tailwind.css +629 -0
- data/vendor/tw-animate-css/LICENSE +21 -0
- data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
- data/vendor/tw-animate-css/tw-animate.css +1 -0
- metadata +267 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
module Concerns
|
|
6
|
+
# The use_stimulus contract: a class-level, element-major declaration
|
|
7
|
+
# of the component's Stimulus wiring, replacing both the hand-rolled
|
|
8
|
+
# `<element>_stimulus_attributes` methods and the previous
|
|
9
|
+
# `stimulated_with` DSL (controller-major, root-only - it could not
|
|
10
|
+
# express multi-element wiring, and no component ever adopted it).
|
|
11
|
+
#
|
|
12
|
+
# use_stimulus do
|
|
13
|
+
# on :root do
|
|
14
|
+
# controller :hover_card do
|
|
15
|
+
# register
|
|
16
|
+
# value :open
|
|
17
|
+
# value :open_delay, unless: -> { open_delay.nil? }
|
|
18
|
+
# end
|
|
19
|
+
# controller :popper do
|
|
20
|
+
# register
|
|
21
|
+
# value :side
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
# on :trigger do
|
|
25
|
+
# controller :hover_card do
|
|
26
|
+
# action :pointer_enter, on: :pointerenter
|
|
27
|
+
# end
|
|
28
|
+
# controller :popper do
|
|
29
|
+
# target :anchor
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# Render side: `stimulus_attributes_for(:trigger)` returns the
|
|
35
|
+
# element's merged attribute hash (public - templates and slot
|
|
36
|
+
# lambdas call it directly); `stimulus_action(:open)` /
|
|
37
|
+
# `stimulus_event(:change)` build validated descriptor strings for
|
|
38
|
+
# forwarding; `stimulus_attributes(:a, :b) { |a, b| ... }` is the
|
|
39
|
+
# escape hatch for wiring too dynamic to declare - every builder
|
|
40
|
+
# shares ONE Attributes instance, so multi-controller merges are
|
|
41
|
+
# correct by construction.
|
|
42
|
+
#
|
|
43
|
+
# Declarations validate against the controllers manifest at CLASS
|
|
44
|
+
# LOAD (unknown controller/value/action/target/event raises at boot,
|
|
45
|
+
# not first render) and are declared once per element: a subclass
|
|
46
|
+
# redeclaring an element REPLACES it wholesale (the Ruby-override
|
|
47
|
+
# intuition; Sheet/Drawer re-controller their roots this way), while
|
|
48
|
+
# `on :root, extend: true` merges into the inherited element
|
|
49
|
+
# (date_field -> time_field adds values). Untouched elements inherit.
|
|
50
|
+
module Stimulus
|
|
51
|
+
extend ActiveSupport::Concern
|
|
52
|
+
|
|
53
|
+
class_methods do
|
|
54
|
+
# Declares (part of) the component's stimulus wiring. Multiple
|
|
55
|
+
# blocks compose additively within a class; shared wiring modules
|
|
56
|
+
# call this from their `included` hook.
|
|
57
|
+
#
|
|
58
|
+
# @yield evaluates the block as a
|
|
59
|
+
# {Poetry::Core::Stimulus::Declarations::RootDSL}:
|
|
60
|
+
# `on :element do controller :name do ... end end`
|
|
61
|
+
# @return [void]
|
|
62
|
+
# @raise [ArgumentError] without a block
|
|
63
|
+
def use_stimulus(&block)
|
|
64
|
+
raise ArgumentError, "use_stimulus requires a block" unless block
|
|
65
|
+
|
|
66
|
+
dsl = Poetry::Core::Stimulus::Declarations::RootDSL.new(name || to_s)
|
|
67
|
+
dsl.instance_exec(&block)
|
|
68
|
+
dsl.elements.each { |element| store_stimulus_element(element) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# This class's own declarations, element name -> Element.
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash{Symbol => Poetry::Core::Stimulus::Declarations::Element}]
|
|
74
|
+
def own_stimulus_elements
|
|
75
|
+
@own_stimulus_elements ||= {}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The effective wiring after inheritance: walk the superclass
|
|
79
|
+
# chain root-first, folding each class's declarations over the
|
|
80
|
+
# inherited set - redeclared elements replace wholesale unless
|
|
81
|
+
# declared with extend: true, which appends to the inherited
|
|
82
|
+
# element's wirings.
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash{Symbol => Poetry::Core::Stimulus::Declarations::Element}]
|
|
85
|
+
def stimulus_elements
|
|
86
|
+
chain = []
|
|
87
|
+
klass = self
|
|
88
|
+
while klass.respond_to?(:own_stimulus_elements)
|
|
89
|
+
chain.unshift(klass)
|
|
90
|
+
klass = klass.superclass
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
chain.each_with_object({}) do |ancestor, resolved|
|
|
94
|
+
ancestor.own_stimulus_elements.each do |name, element|
|
|
95
|
+
resolved[name] =
|
|
96
|
+
if element.extend_inherited && resolved[name]
|
|
97
|
+
Poetry::Core::Stimulus::Declarations::Element.new(
|
|
98
|
+
name: name, extend_inherited: false, conditions: element.conditions,
|
|
99
|
+
wirings: resolved[name].wirings + element.wirings
|
|
100
|
+
)
|
|
101
|
+
else
|
|
102
|
+
element
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Every controller identifier declared anywhere on the class, in
|
|
109
|
+
# declaration order - the search space for unqualified
|
|
110
|
+
# stimulus_action / stimulus_event resolution.
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<String>]
|
|
113
|
+
def stimulus_identifiers
|
|
114
|
+
stimulus_elements.values.flat_map { |element| element.wirings.map(&:identifier) }.uniq
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Resolves a declaration-style identifier (Symbol suffix, String,
|
|
118
|
+
# or Array) to its full manifest identifier - see
|
|
119
|
+
# {Poetry::Core::Stimulus::Declarations.resolve_identifier}.
|
|
120
|
+
#
|
|
121
|
+
# @param identifier [Symbol, String, Array] the declaration-style identifier
|
|
122
|
+
# @return [String] the full Stimulus identifier
|
|
123
|
+
def resolve_stimulus_identifier(identifier)
|
|
124
|
+
Poetry::Core::Stimulus::Declarations.resolve_identifier(identifier)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The registry-shaped projection of the RESOLVED wiring (post-
|
|
128
|
+
# inheritance, so Sheet publishes sheet controllers) - plain data
|
|
129
|
+
# for the registry, skill text, and docs tables.
|
|
130
|
+
#
|
|
131
|
+
# @return [Array<Hash>] one serialized element per declared element
|
|
132
|
+
def stimulus_definitions
|
|
133
|
+
stimulus_elements.values.map do |element|
|
|
134
|
+
Poetry::Core::Stimulus::Declarations.serialize_element(element)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Descriptor builders are STATIC facts of the declarations, so
|
|
139
|
+
# they exist at class level - helpers, generator templates, and
|
|
140
|
+
# test selectors consume them without an instance. on:/at: build
|
|
141
|
+
# the evented token ("click->id#method"); without on: the bare
|
|
142
|
+
# descriptor (element-default event).
|
|
143
|
+
#
|
|
144
|
+
# @param args [Array<Symbol, String>] `(method)` resolves the method
|
|
145
|
+
# across the declared controllers; `(controller, method)` pins one
|
|
146
|
+
# @param on [Symbol, String, Array<Symbol>, nil] the event(s) to listen for
|
|
147
|
+
# @param at [Symbol, String, nil] the event target (:window, :document)
|
|
148
|
+
# @return [String] the action descriptor ("click->poetry--core--x#open")
|
|
149
|
+
def stimulus_action(*args, on: nil, at: nil)
|
|
150
|
+
controller, method = unpack_stimulus_descriptor_args(args, :action)
|
|
151
|
+
identifier = resolve_stimulus_descriptor(controller, method, kind: :action)
|
|
152
|
+
Poetry::Core::Stimulus::Builder.new(identifier, Poetry::Core::HTML::Attributes.new)
|
|
153
|
+
.action(method, on: on, at: at)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A validated event-name string for listening markup;
|
|
157
|
+
# stimulus_event(:change) resolves across the declared
|
|
158
|
+
# controllers, stimulus_event(:controller, :change) pins one.
|
|
159
|
+
#
|
|
160
|
+
# @param args [Array<Symbol, String>] `(name)` or `(controller, name)`
|
|
161
|
+
# @return [String] the full event name
|
|
162
|
+
def stimulus_event(*args)
|
|
163
|
+
controller, name = unpack_stimulus_descriptor_args(args, :event)
|
|
164
|
+
identifier = resolve_stimulus_descriptor(controller, name, kind: :event)
|
|
165
|
+
Poetry::Core::Stimulus::Declarations.event_name(identifier, name)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def unpack_stimulus_descriptor_args(args, kind)
|
|
171
|
+
case args.size
|
|
172
|
+
when 1 then [nil, args.first]
|
|
173
|
+
when 2 then args
|
|
174
|
+
else
|
|
175
|
+
raise ArgumentError,
|
|
176
|
+
"stimulus_#{kind}(#{kind}) or stimulus_#{kind}(:controller, #{kind})"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def resolve_stimulus_descriptor(controller, name, kind:)
|
|
181
|
+
return resolve_stimulus_identifier(controller) if controller
|
|
182
|
+
|
|
183
|
+
identifiers = stimulus_identifiers
|
|
184
|
+
if identifiers.empty?
|
|
185
|
+
raise ArgumentError,
|
|
186
|
+
"no use_stimulus declarations on #{self} - pass the controller: " \
|
|
187
|
+
"stimulus_#{kind}(:controller, #{name.inspect})"
|
|
188
|
+
end
|
|
189
|
+
return identifiers.first if identifiers.size == 1
|
|
190
|
+
|
|
191
|
+
matches = identifiers.select { |id| stimulus_descriptor_match?(id, name, kind) }
|
|
192
|
+
return matches.first if matches.size == 1
|
|
193
|
+
|
|
194
|
+
raise ArgumentError,
|
|
195
|
+
"#{matches.empty? ? "no declared controller defines" : "ambiguous"} " \
|
|
196
|
+
"#{kind} #{name.inspect} (declared: #{identifiers.join(", ")}) - qualify: " \
|
|
197
|
+
"stimulus_#{kind}(:controller, #{name.inspect})"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def stimulus_descriptor_match?(identifier, name, kind)
|
|
201
|
+
definition = Poetry::Core::Stimulus::Manifest.definition(identifier)
|
|
202
|
+
return false unless definition
|
|
203
|
+
|
|
204
|
+
case kind
|
|
205
|
+
when :action
|
|
206
|
+
definition.fetch("methods", []).include?(Poetry::Core::Stimulus::Declarations.camelize(name))
|
|
207
|
+
when :event
|
|
208
|
+
# Real emitted names (poetry:<component>:<event> for component
|
|
209
|
+
# events; identifier-prefixed for layer events) - match by the
|
|
210
|
+
# ":<name>" suffix, the same rule event_name resolves by.
|
|
211
|
+
definition.fetch("events", []).any? { |event| event.end_with?(":#{name}") }
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def store_stimulus_element(element)
|
|
216
|
+
existing = own_stimulus_elements[element.name]
|
|
217
|
+
own_stimulus_elements[element.name] =
|
|
218
|
+
if existing
|
|
219
|
+
Poetry::Core::Stimulus::Declarations::Element.new(
|
|
220
|
+
name: element.name,
|
|
221
|
+
extend_inherited: existing.extend_inherited || element.extend_inherited,
|
|
222
|
+
conditions: element.conditions || existing.conditions,
|
|
223
|
+
wirings: existing.wirings + element.wirings
|
|
224
|
+
)
|
|
225
|
+
else
|
|
226
|
+
element
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The declared wiring for one element as a plain attributes hash,
|
|
232
|
+
# ready to merge into the element's tag or forward as component
|
|
233
|
+
# kwargs. Public by design - templates call it, ending the
|
|
234
|
+
# `public :inner_stimulus_attributes` juggling.
|
|
235
|
+
#
|
|
236
|
+
# @param element_name [Symbol, String] a declared element (:root, ...)
|
|
237
|
+
# @return [Hash] the element's data attributes; empty when the
|
|
238
|
+
# element's if:/unless: conditions do not hold
|
|
239
|
+
# @raise [ArgumentError] for an element no use_stimulus block declared
|
|
240
|
+
def stimulus_attributes_for(element_name)
|
|
241
|
+
element = self.class.stimulus_elements[element_name.to_sym]
|
|
242
|
+
unless element
|
|
243
|
+
known = self.class.stimulus_elements.keys
|
|
244
|
+
raise ArgumentError,
|
|
245
|
+
"undeclared stimulus element #{element_name.inspect} on #{self.class}" \
|
|
246
|
+
"#{known.any? ? " - declared: #{known.join(", ")}" : " (no use_stimulus declarations)"}"
|
|
247
|
+
end
|
|
248
|
+
return {} unless stimulus_conditions_met?(element.conditions)
|
|
249
|
+
|
|
250
|
+
attrs = Poetry::Core::HTML::Attributes.new
|
|
251
|
+
element.wirings.each do |wiring|
|
|
252
|
+
next unless stimulus_conditions_met?(wiring.conditions)
|
|
253
|
+
|
|
254
|
+
builder = Poetry::Core::Stimulus::Builder.new(wiring.identifier, attrs)
|
|
255
|
+
wiring.entries.each do |entry|
|
|
256
|
+
apply_stimulus_entry(builder, entry) if stimulus_conditions_met?(entry.conditions)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
# The agent-tool registrar rides the root's shared Attributes
|
|
260
|
+
# instance (Concerns::AgentTools) - a per-instance opt-in.
|
|
261
|
+
apply_webmcp_wiring(attrs) if element_name.to_sym == :root && respond_to?(:apply_webmcp_wiring)
|
|
262
|
+
attrs.to_attributes
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# The escape hatch for wiring too dynamic to declare: yields one
|
|
266
|
+
# Builder per controller, all sharing ONE Attributes instance.
|
|
267
|
+
# Controllers resolve like declarations (Symbol -> manifest,
|
|
268
|
+
# String/Array -> verbatim).
|
|
269
|
+
#
|
|
270
|
+
# @param controllers [Array<Symbol, String, Array>] one or more
|
|
271
|
+
# controller identifiers
|
|
272
|
+
# @yield [*builders] one {Poetry::Core::Stimulus::Builder} per
|
|
273
|
+
# controller, in order
|
|
274
|
+
# @return [Hash] the accumulated attributes
|
|
275
|
+
# @raise [ArgumentError] with no controllers
|
|
276
|
+
def stimulus_attributes(*controllers)
|
|
277
|
+
raise ArgumentError, "stimulus_attributes needs at least one controller" if controllers.empty?
|
|
278
|
+
|
|
279
|
+
attrs = Poetry::Core::HTML::Attributes.new
|
|
280
|
+
builders = controllers.map do |controller|
|
|
281
|
+
Poetry::Core::Stimulus::Builder.new(
|
|
282
|
+
self.class.resolve_stimulus_identifier(controller), attrs
|
|
283
|
+
)
|
|
284
|
+
end
|
|
285
|
+
yield(*builders) if block_given?
|
|
286
|
+
attrs.to_attributes
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Delegates to the class-level builder (descriptors are static
|
|
290
|
+
# facts of the declarations); stimulus_action(:open) resolves
|
|
291
|
+
# across declared controllers, on:/at: build the evented token.
|
|
292
|
+
#
|
|
293
|
+
# @param on [Symbol, String, Array<Symbol>, nil] the event(s) to listen for
|
|
294
|
+
# @param at [Symbol, String, nil] the event target (:window, :document)
|
|
295
|
+
# @return [String] the action descriptor
|
|
296
|
+
def stimulus_action(*, on: nil, at: nil)
|
|
297
|
+
self.class.stimulus_action(*, on: on, at: at)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Delegates to the class-level builder: stimulus_event(:change)
|
|
301
|
+
# resolves across the declared controllers,
|
|
302
|
+
# stimulus_event(:controller, :change) pins one.
|
|
303
|
+
#
|
|
304
|
+
# @return [String] the full event name
|
|
305
|
+
def stimulus_event(*)
|
|
306
|
+
self.class.stimulus_event(*)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Replays one declared Entry onto the element's attribute builder.
|
|
310
|
+
#
|
|
311
|
+
# @api private
|
|
312
|
+
def apply_stimulus_entry(builder, entry)
|
|
313
|
+
case entry.kind
|
|
314
|
+
when :register then builder.register_controller
|
|
315
|
+
when :value then builder.with_value(entry.name, stimulus_value_for(entry))
|
|
316
|
+
when :action then builder.with_action(entry.name, on: entry.on, at: entry.at)
|
|
317
|
+
when :target then builder.with_target(entry.name)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# The rendered value of a declared value entry: the literal, or the
|
|
322
|
+
# named/implicit method's return.
|
|
323
|
+
#
|
|
324
|
+
# @api private
|
|
325
|
+
def stimulus_value_for(entry)
|
|
326
|
+
source = entry.source
|
|
327
|
+
case source[:type]
|
|
328
|
+
when :literal then source[:value]
|
|
329
|
+
else send(source[:value])
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Whether a declaration's if:/unless: conditions hold for this
|
|
334
|
+
# instance; nil conditions always hold.
|
|
335
|
+
#
|
|
336
|
+
# @param conditions [Hash{Symbol => Symbol, Proc}, nil] the if:/unless: pair
|
|
337
|
+
# @return [Boolean]
|
|
338
|
+
# @api private
|
|
339
|
+
def stimulus_conditions_met?(conditions)
|
|
340
|
+
return true if conditions.nil?
|
|
341
|
+
|
|
342
|
+
if (condition = conditions[:if]) && !evaluate_stimulus_condition(condition)
|
|
343
|
+
return false
|
|
344
|
+
end
|
|
345
|
+
if (condition = conditions[:unless]) && evaluate_stimulus_condition(condition)
|
|
346
|
+
return false
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
true
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Evaluates one if:/unless: condition in instance context.
|
|
353
|
+
#
|
|
354
|
+
# @api private
|
|
355
|
+
def evaluate_stimulus_condition(condition)
|
|
356
|
+
condition.is_a?(Proc) ? instance_exec(&condition) : send(condition)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
private :apply_stimulus_entry, :stimulus_value_for, :stimulus_conditions_met?, :evaluate_stimulus_condition
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
# The concerns composed into {Poetry::Core::Component}: styles, options,
|
|
6
|
+
# slots, Stimulus wiring, introspection, and part declarations.
|
|
7
|
+
module Concerns
|
|
8
|
+
# The Styles concern provides a powerful DSL for defining style attributes in components.
|
|
9
|
+
# It extends the basic attribute functionality with support for variants, proc defaults,
|
|
10
|
+
# and tracking of which attributes have been explicitly set vs using defaults.
|
|
11
|
+
#
|
|
12
|
+
# The registration, tracking, and hierarchy machinery lives in
|
|
13
|
+
# DeclaredAttributes (shared with Options); this concern owns the
|
|
14
|
+
# style-specific surface: variants, inclusion validation, and CSS
|
|
15
|
+
# emission.
|
|
16
|
+
#
|
|
17
|
+
# @example Basic usage with variants
|
|
18
|
+
# class MyComponent < Poetry::Core::Component
|
|
19
|
+
# style :color, default: :primary, variants: [:primary, :secondary, :success]
|
|
20
|
+
# style :size, default: :md, variants: [:sm, :md, :lg]
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Proc defaults that reference other attributes
|
|
24
|
+
# class Badge::Component < Poetry::Core::Component
|
|
25
|
+
# style :color, default: :gray, variants: [...colors]
|
|
26
|
+
# style :dot_color, default: -> { color }, variants: [...colors]
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# badge = Badge::Component.new(color: :red)
|
|
30
|
+
# badge.dot_color # => :red (inherits from color)
|
|
31
|
+
# badge.color = :blue
|
|
32
|
+
# badge.dot_color # => :blue (still follows color)
|
|
33
|
+
#
|
|
34
|
+
# badge2 = Badge::Component.new(color: :red, dot_color: :green)
|
|
35
|
+
# badge2.color = :blue
|
|
36
|
+
# badge2.dot_color # => :green (explicitly set, doesn't follow color)
|
|
37
|
+
#
|
|
38
|
+
# @example Required attributes
|
|
39
|
+
# class MyComponent < Poetry::Core::Component
|
|
40
|
+
# style :type, required: true, variants: [:button, :link]
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# @example Boolean styles
|
|
44
|
+
# class MyComponent < Poetry::Core::Component
|
|
45
|
+
# style :outlined, variants: :boolean, default: false
|
|
46
|
+
# end
|
|
47
|
+
module Styles
|
|
48
|
+
extend ActiveSupport::Concern
|
|
49
|
+
include DeclaredAttributes
|
|
50
|
+
|
|
51
|
+
# The naming convention joining a component to its sidecar style
|
|
52
|
+
# dictionary (Dot::Component -> Dot::Style).
|
|
53
|
+
STYLE_CLASS_SUFFIX = "::Style"
|
|
54
|
+
|
|
55
|
+
included do
|
|
56
|
+
class_attribute :registered_styles,
|
|
57
|
+
instance_writer: true,
|
|
58
|
+
instance_predicate: false,
|
|
59
|
+
default: nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class_methods do
|
|
63
|
+
# Defines a style attribute for the component.
|
|
64
|
+
#
|
|
65
|
+
# @param name [Symbol, String] the name of the style attribute
|
|
66
|
+
# @param options [Hash] configuration options
|
|
67
|
+
# @option options [Object, Proc] :default the default value (can be a proc for dynamic defaults)
|
|
68
|
+
# @option options [Array<Symbol>, :boolean] :variants the allowed values for this attribute
|
|
69
|
+
# @option options [Boolean] :required whether this attribute must be provided
|
|
70
|
+
#
|
|
71
|
+
# @example Static default
|
|
72
|
+
# style :color, default: :primary, variants: [:primary, :secondary]
|
|
73
|
+
#
|
|
74
|
+
# @example Proc default
|
|
75
|
+
# style :dot_color, default: -> { color }, variants: [:primary, :secondary]
|
|
76
|
+
#
|
|
77
|
+
# @example Required attribute
|
|
78
|
+
# style :type, required: true, variants: [:button, :link]
|
|
79
|
+
#
|
|
80
|
+
# @example Boolean attribute
|
|
81
|
+
# style :outlined, variants: :boolean, default: false
|
|
82
|
+
# @return [void]
|
|
83
|
+
def style(name, **options)
|
|
84
|
+
register_declared_attribute(:style, name, options)
|
|
85
|
+
|
|
86
|
+
variants, required, default_value = extract_style_options(options)
|
|
87
|
+
type = determine_attribute_type(variants)
|
|
88
|
+
|
|
89
|
+
attribute(name, type, **options)
|
|
90
|
+
add_style_validations(name, type, variants, required)
|
|
91
|
+
setup_declared_tracking(:style, name, default_value)
|
|
92
|
+
define_variants_getter(name, variants)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns all style attributes that have default values (static or proc).
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<Symbol>] sorted array of attribute names with defaults
|
|
98
|
+
def style_attributes_with_defaults
|
|
99
|
+
declared_attributes_with_defaults(:style)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns style attributes that have static (non-proc) default values.
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<Symbol>] sorted array of attribute names with static defaults
|
|
105
|
+
def style_attributes_with_static_defaults
|
|
106
|
+
declared_attributes_with_static_defaults(:style)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns style attributes that have proc default values.
|
|
110
|
+
# Proc defaults allow dynamic defaults that can reference other attributes.
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<Symbol>] sorted array of attribute names with proc defaults
|
|
113
|
+
def style_attributes_with_proc_defaults
|
|
114
|
+
declared_attributes_with_proc_defaults(:style)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns all style attributes defined on this component and its ancestors.
|
|
118
|
+
#
|
|
119
|
+
# @return [Array<Symbol>] sorted array of all style attribute names
|
|
120
|
+
def style_attributes
|
|
121
|
+
declared_attributes(:style)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The doc: strings declared on this component's styles,
|
|
125
|
+
# hierarchy-wide (nearest declaration wins).
|
|
126
|
+
#
|
|
127
|
+
# @return [Hash{Symbol => String}]
|
|
128
|
+
def style_docs
|
|
129
|
+
declared_docs(:style)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Checks if the given name is a defined style attribute.
|
|
133
|
+
#
|
|
134
|
+
# @param name [Symbol, String] the attribute name to check
|
|
135
|
+
# @return [Boolean] true if the attribute is a style attribute
|
|
136
|
+
def has_style_attribute?(name)
|
|
137
|
+
style_attributes.include?(name.to_sym)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Automatically determines the corresponding Style class for this component.
|
|
141
|
+
# Example: Poetry::Core::Dot::Component -> Poetry::Core::Dot::Style
|
|
142
|
+
#
|
|
143
|
+
# @return [Class, nil] the style class if it exists, nil otherwise
|
|
144
|
+
def style_class
|
|
145
|
+
style_class_name = component_module + STYLE_CLASS_SUFFIX
|
|
146
|
+
style_class_name.constantize
|
|
147
|
+
rescue NameError => e
|
|
148
|
+
log_style_class_not_found(style_class_name, e) if defined?(Rails)
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# Extracts and processes style-specific options from the options hash.
|
|
155
|
+
#
|
|
156
|
+
# @param options [Hash] the original options hash
|
|
157
|
+
# @return [Array<(Object, Boolean, Object)>] variants, required, and default_value
|
|
158
|
+
def extract_style_options(options)
|
|
159
|
+
variants = options.delete(:variants)
|
|
160
|
+
required, default_value = extract_declared_defaults(options)
|
|
161
|
+
|
|
162
|
+
[variants, required, default_value]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Determines the attribute type based on the variants option.
|
|
166
|
+
#
|
|
167
|
+
# @param variants [Object] the variants option value
|
|
168
|
+
# @return [Symbol] :boolean or :symbol
|
|
169
|
+
def determine_attribute_type(variants)
|
|
170
|
+
variants == :boolean ? :boolean : :symbol
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Adds validations for the style attribute.
|
|
174
|
+
#
|
|
175
|
+
# @param name [Symbol] the attribute name
|
|
176
|
+
# @param type [Symbol] the attribute type
|
|
177
|
+
# @param variants [Object] the allowed variants
|
|
178
|
+
# @param required [Boolean] whether the attribute is required
|
|
179
|
+
def add_style_validations(name, type, variants, required)
|
|
180
|
+
if type == :boolean
|
|
181
|
+
validates name, inclusion: { in: [true, false] }
|
|
182
|
+
elsif variants
|
|
183
|
+
validates name, inclusion: { in: variants }
|
|
184
|
+
end
|
|
185
|
+
validates name, presence: true if required
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Defines a singleton method to access the variants for this attribute.
|
|
189
|
+
#
|
|
190
|
+
# @param name [Symbol] the attribute name
|
|
191
|
+
# @param variants [Object] the variants value
|
|
192
|
+
def define_variants_getter(name, variants)
|
|
193
|
+
define_singleton_method("#{name}_variants") { variants }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Logs when a style class cannot be found.
|
|
197
|
+
#
|
|
198
|
+
# @param style_class_name [String] the name of the style class that was not found
|
|
199
|
+
# @param error [NameError] the error that was raised
|
|
200
|
+
def log_style_class_not_found(style_class_name, error)
|
|
201
|
+
Rails.logger.debug { "Style class not found: #{style_class_name} (#{error.message})" }
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Checks if the given attribute is a style attribute.
|
|
206
|
+
#
|
|
207
|
+
# @param name [Symbol, String] the attribute name to check
|
|
208
|
+
# @return [Boolean] true if the attribute is a style attribute
|
|
209
|
+
def style_attribute?(name)
|
|
210
|
+
self.class.has_style_attribute?(name.to_sym)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Returns all style attributes defined on this component's class.
|
|
214
|
+
#
|
|
215
|
+
# @return [Array<Symbol>] sorted array of all style attribute names
|
|
216
|
+
def style_attributes
|
|
217
|
+
self.class.style_attributes
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Returns only the style attributes that have been explicitly set (not using defaults).
|
|
221
|
+
#
|
|
222
|
+
# @return [Array<Symbol>] sorted array of initialized attribute names
|
|
223
|
+
def initialized_style_attributes
|
|
224
|
+
initialized_declared_attributes(registered_styles)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Checks if a style attribute has been explicitly initialized.
|
|
228
|
+
#
|
|
229
|
+
# @param name [Symbol, String] the attribute name to check
|
|
230
|
+
# @return [Boolean] true if the attribute was explicitly set
|
|
231
|
+
def style_attribute_initialized?(name)
|
|
232
|
+
declared_attribute_registered?(registered_styles, name)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Returns all style attributes with their initialization status.
|
|
236
|
+
#
|
|
237
|
+
# @return [Hash{Symbol => Boolean}] map of attribute names to initialized status
|
|
238
|
+
def style_attributes_status
|
|
239
|
+
style_attributes.to_h do |attr|
|
|
240
|
+
[attr, style_attribute_initialized?(attr)]
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Returns a hash of all style attributes with their current values.
|
|
245
|
+
#
|
|
246
|
+
# @return [Hash{Symbol => Object}] map of attribute names to their values
|
|
247
|
+
def styles
|
|
248
|
+
style_attributes.to_h do |attr|
|
|
249
|
+
[attr, send(attr)]
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Returns the style class for this component.
|
|
254
|
+
#
|
|
255
|
+
# @return [Class, nil] the style class if it exists
|
|
256
|
+
def styler
|
|
257
|
+
self.class.style_class
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Generates CSS classes based on style attributes and additional options.
|
|
261
|
+
#
|
|
262
|
+
# The emission is governed by `css_mode`: `:tailwind` (default)
|
|
263
|
+
# resolves the style values to utility classes through the sidecar
|
|
264
|
+
# Style dictionary; `:bem` emits the stable BEM token IR instead, for
|
|
265
|
+
# hosts that bring their own CSS (styled against the generated
|
|
266
|
+
# reference stylesheet). Override per call with `css_mode:`, or
|
|
267
|
+
# globally via `Poetry::Core::Config.current.css_mode`.
|
|
268
|
+
#
|
|
269
|
+
# @param element [Symbol, nil] a named element (BEM `block__element`)
|
|
270
|
+
# @param options [Hash] additional style options to merge
|
|
271
|
+
# @yield optional block passed to the style class
|
|
272
|
+
# @return [String, nil] the generated CSS classes; nil in :tailwind
|
|
273
|
+
# mode for a component without a sidecar Style class
|
|
274
|
+
# @raise [Poetry::Core::Error] for a css_mode other than :tailwind
|
|
275
|
+
# or :bem
|
|
276
|
+
def css(element = nil, **options, &)
|
|
277
|
+
mode = options.delete(:css_mode) || Poetry::Core::Config.current.css_mode
|
|
278
|
+
|
|
279
|
+
case mode
|
|
280
|
+
when :tailwind
|
|
281
|
+
# A component with no sidecar Style class has no dictionary -
|
|
282
|
+
# it renders unstyled (caught dogfooding poetry-ui's Icon).
|
|
283
|
+
return nil unless styler
|
|
284
|
+
|
|
285
|
+
style_attributes = styles.merge(options)
|
|
286
|
+
styler.css(element, **style_attributes, &)
|
|
287
|
+
when :bem
|
|
288
|
+
extra = options.delete(:class)
|
|
289
|
+
[bem(element, **options), extra].compact.join(" ")
|
|
290
|
+
else
|
|
291
|
+
raise Poetry::Core::Error, "unknown css_mode #{mode.inspect} (expected :tailwind or :bem)"
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# The component's BEM block name - the stable, framework-agnostic
|
|
296
|
+
# class contract of the token IR ("poetry/core/dot" -> "poetry-core-dot").
|
|
297
|
+
#
|
|
298
|
+
# @return [String]
|
|
299
|
+
def bem_block
|
|
300
|
+
self.class.component_path.tr("/", "-")
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# The BEM token IR for this component (the pipeline's Step 2): the
|
|
304
|
+
# block class plus one modifier class per style value - symbols as
|
|
305
|
+
# `block--attr-value`, booleans as presence modifiers (`block--attr`).
|
|
306
|
+
# A named element returns `block__element`.
|
|
307
|
+
#
|
|
308
|
+
# @param element [Symbol, nil] a named element
|
|
309
|
+
# @param overrides [Hash] style overrides merged over the resolved values
|
|
310
|
+
# @return [String] space-separated BEM classes
|
|
311
|
+
def bem(element = nil, **overrides)
|
|
312
|
+
return "#{bem_block}__#{element}" if element
|
|
313
|
+
|
|
314
|
+
styles.merge(overrides).each_with_object([bem_block]) do |(attr, value), tokens|
|
|
315
|
+
next if value.nil? || value == false
|
|
316
|
+
|
|
317
|
+
tokens << (value == true ? "#{bem_block}--#{attr}" : "#{bem_block}--#{attr}-#{value}")
|
|
318
|
+
end.join(" ")
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|