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,533 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
module Concerns
|
|
6
|
+
# The prop-introspection shim: a machine-readable
|
|
7
|
+
# description of a component's public surface - style attributes,
|
|
8
|
+
# options, and slots - derived from the metadata the Styles/Options
|
|
9
|
+
# DSLs and ViewComponent already carry. This is the single source the
|
|
10
|
+
# generated registry, the docs tables, and the MCP prop schema are
|
|
11
|
+
# built from; nothing here is hand-authored.
|
|
12
|
+
module Introspection
|
|
13
|
+
extend ActiveSupport::Concern
|
|
14
|
+
|
|
15
|
+
# The registry's marker for a proc default (its value depends on
|
|
16
|
+
# other attributes and is unknowable statically).
|
|
17
|
+
DYNAMIC_DEFAULT = :dynamic
|
|
18
|
+
# The parameter kinds counted toward positional arity.
|
|
19
|
+
POSITIONAL_KINDS = %i[req opt].freeze
|
|
20
|
+
# Parameter kinds that make a keyword surface open or unknowable
|
|
21
|
+
# (**rest accepts anything; a positional can swallow a braceless
|
|
22
|
+
# hash), and the kinds that ARE the keyword surface.
|
|
23
|
+
OPEN_PARAMETER_KINDS = %i[keyrest rest req opt].freeze
|
|
24
|
+
# The parameter kinds that ARE the keyword surface.
|
|
25
|
+
KEYWORD_PARAMETER_KINDS = %i[key keyreq].freeze
|
|
26
|
+
|
|
27
|
+
class_methods do
|
|
28
|
+
# The component's full prop surface.
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash] { styles: [...], options: [...], slots: [...] }
|
|
31
|
+
# Documents a slot declared with renders_one/renders_many. The
|
|
32
|
+
# string travels the same road as option/style doc: params - the
|
|
33
|
+
# registry, the agent surface, and the generated API docs.
|
|
34
|
+
# Prefer the doc: keyword on the declaration itself (it lands
|
|
35
|
+
# here); call slot_doc directly only when the doc and the
|
|
36
|
+
# declaration live in different modules.
|
|
37
|
+
#
|
|
38
|
+
# @param name [Symbol] the slot name as declared (plural for
|
|
39
|
+
# renders_many)
|
|
40
|
+
# @param text [String] one reference-register sentence
|
|
41
|
+
# @return [void]
|
|
42
|
+
#
|
|
43
|
+
# @example
|
|
44
|
+
# slot_doc :trigger, "The button that opens the dialog."
|
|
45
|
+
def slot_doc(name, text)
|
|
46
|
+
declared_ivar_hash(:@_slot_docs)[name.to_sym] = text
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The slot_doc strings, hierarchy-wide (nearest wins).
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash{Symbol => String}]
|
|
52
|
+
def slot_docs
|
|
53
|
+
collect_declared_map(:@_slot_docs)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# ViewComponent's renders_one, with the doc riding the
|
|
57
|
+
# declaration: doc: is lifted into {slot_doc}, renders: passes
|
|
58
|
+
# the callable as a keyword so the doc can come first, and the
|
|
59
|
+
# polymorphic types: form is re-formed into the positional hash
|
|
60
|
+
# ViewComponent expects - the ViewComponent surface underneath
|
|
61
|
+
# is unchanged. Unknown keywords raise at class load, and a
|
|
62
|
+
# positional callable cannot be combined with renders:/types:.
|
|
63
|
+
#
|
|
64
|
+
# @param slot_name [Symbol] the slot name
|
|
65
|
+
# @param callable [Object, nil] ViewComponent's positional
|
|
66
|
+
# callable (a component class, class-name string, or lambda)
|
|
67
|
+
# @param opts [Hash] doc:, renders:, and/or types:
|
|
68
|
+
# @return [void]
|
|
69
|
+
#
|
|
70
|
+
# @example A documented slot
|
|
71
|
+
# renders_one :leading, doc: "Optional leading visual."
|
|
72
|
+
#
|
|
73
|
+
# @example A documented lambda slot, doc first
|
|
74
|
+
# renders_one :trigger,
|
|
75
|
+
# doc: "The button that opens the dialog.",
|
|
76
|
+
# renders: lambda { |**options, &block| ... }
|
|
77
|
+
def renders_one(slot_name, callable = nil, **opts)
|
|
78
|
+
callable = declared_slot_callable(slot_name, callable, opts)
|
|
79
|
+
callable.nil? ? super(slot_name) : super(slot_name, callable)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ViewComponent's renders_many with the same keyword surface as
|
|
83
|
+
# {renders_one}; doc: describes the collection contract and is
|
|
84
|
+
# keyed by the plural declared name, exactly like {slot_doc}.
|
|
85
|
+
#
|
|
86
|
+
# @param slot_name [Symbol] the plural slot name
|
|
87
|
+
# @param callable [Object, nil] ViewComponent's positional
|
|
88
|
+
# callable (a component class, class-name string, or lambda)
|
|
89
|
+
# @param opts [Hash] doc:, renders:, and/or types:
|
|
90
|
+
# @return [void]
|
|
91
|
+
#
|
|
92
|
+
# @example A documented collection slot
|
|
93
|
+
# renders_many :items,
|
|
94
|
+
# doc: "The rows, rendered in call order.",
|
|
95
|
+
# renders: lambda { |label:, **options| ... }
|
|
96
|
+
def renders_many(slot_name, callable = nil, **opts)
|
|
97
|
+
callable = declared_slot_callable(slot_name, callable, opts)
|
|
98
|
+
callable.nil? ? super(slot_name) : super(slot_name, callable)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The component's full declared surface - styles, options, slots
|
|
102
|
+
# (with descriptions), required slots, and requires_any groups -
|
|
103
|
+
# as the registry generator serializes it.
|
|
104
|
+
#
|
|
105
|
+
# @return [Hash{Symbol => Object}]
|
|
106
|
+
def prop_definitions
|
|
107
|
+
slots = slot_definitions
|
|
108
|
+
{
|
|
109
|
+
styles: style_attributes.map { |name| style_definition(name) },
|
|
110
|
+
options: option_attributes.map { |name| option_definition(name) },
|
|
111
|
+
slots: slots,
|
|
112
|
+
slot_extras: slot_extras,
|
|
113
|
+
required_slots: Introspection.required_slots_surface(self, slots),
|
|
114
|
+
requires_any: Introspection.requires_any_surface(self, slots)
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Resolves a renders_one/renders_many declaration's keyword
|
|
121
|
+
# surface back to the positional callable ViewComponent expects.
|
|
122
|
+
# Everything but doc:/renders: must be the polymorphic types:
|
|
123
|
+
# hash - anything else is a typo caught at class load.
|
|
124
|
+
def declared_slot_callable(slot_name, callable, opts)
|
|
125
|
+
doc = opts.delete(:doc)
|
|
126
|
+
slot_doc(slot_name, doc) if doc
|
|
127
|
+
renders = opts.delete(:renders)
|
|
128
|
+
unknown = opts.keys - [:types]
|
|
129
|
+
if unknown.any?
|
|
130
|
+
raise Poetry::Core::Error,
|
|
131
|
+
"#{self}##{slot_name}: unknown slot option(s) #{unknown.join(", ")} - " \
|
|
132
|
+
"a slot declaration takes doc:, renders:, and types:"
|
|
133
|
+
end
|
|
134
|
+
if callable && (renders || opts.any?)
|
|
135
|
+
raise Poetry::Core::Error,
|
|
136
|
+
"#{self}##{slot_name}: pass the callable positionally OR as renders:/types:, not both"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
callable || renders || (opts unless opts.empty?)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def style_definition(name)
|
|
143
|
+
definition = { name: name, type: attribute_types[name.to_s].type }
|
|
144
|
+
variants = respond_to?("#{name}_variants") ? public_send("#{name}_variants") : nil
|
|
145
|
+
definition[:variants] = variants if variants.is_a?(Array)
|
|
146
|
+
definition.merge!(default_definition(name, style_attributes_with_static_defaults,
|
|
147
|
+
style_attributes_with_proc_defaults))
|
|
148
|
+
definition[:required] = true if required_attribute?(name)
|
|
149
|
+
doc = style_docs[name.to_sym]
|
|
150
|
+
definition[:description] = doc if doc
|
|
151
|
+
definition
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def option_definition(name)
|
|
155
|
+
definition = { name: name, type: attribute_types[name.to_s].type }
|
|
156
|
+
# An inclusion validator IS the option's enum contract:
|
|
157
|
+
# projecting it makes every enum option statically
|
|
158
|
+
# checkable - select's side:/align:, pagination's
|
|
159
|
+
# current_variant: - through the same value tier style variants
|
|
160
|
+
# already ride. Procs/ranges stay unprojected (unknowable).
|
|
161
|
+
enum = validators_on(name).find { |validator| validator.kind == :inclusion }
|
|
162
|
+
&.options&.dig(:in)
|
|
163
|
+
definition[:variants] = enum if enum.is_a?(Array)
|
|
164
|
+
definition.merge!(default_definition(name, option_attributes_with_static_defaults,
|
|
165
|
+
option_attributes_with_proc_defaults))
|
|
166
|
+
definition[:required] = true if required_attribute?(name)
|
|
167
|
+
format = option_format(name)
|
|
168
|
+
definition[:format] = format if format
|
|
169
|
+
doc = option_docs[name.to_sym]
|
|
170
|
+
definition[:description] = doc if doc
|
|
171
|
+
definition
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# ViewComponent's registered slots: renders_one -> one, renders_many
|
|
175
|
+
# -> many (ViewComponent registers the plural name with collection).
|
|
176
|
+
# A typed slot (renders_one :icon, Icon::Component) also carries the
|
|
177
|
+
# slot component's registry path - the machine-readable form of "this
|
|
178
|
+
# slot takes that component's props, not a render block" (an
|
|
179
|
+
# agent can only honor a contract a surface
|
|
180
|
+
# states). Recursion, setter arities, and builder surfaces come from
|
|
181
|
+
# the module-level walker.
|
|
182
|
+
def slot_definitions
|
|
183
|
+
Introspection.slot_surface(self)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Hand-rolled with_* conveniences (NavigationMenu#with_link) are
|
|
187
|
+
# part of the consumer call surface even though they are not
|
|
188
|
+
# registered slots.
|
|
189
|
+
def slot_extras
|
|
190
|
+
Introspection.hand_rolled_setters(self, slot_definitions)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The default, keyed three ways: a static value (from ActiveModel's
|
|
194
|
+
# default attributes, may legitimately be false), :dynamic for proc
|
|
195
|
+
# defaults (value depends on other attributes), or no key at all.
|
|
196
|
+
def default_definition(name, static_names, proc_names)
|
|
197
|
+
if proc_names.include?(name)
|
|
198
|
+
{ default: DYNAMIC_DEFAULT }
|
|
199
|
+
elsif static_names.include?(name)
|
|
200
|
+
{ default: _default_attributes[name.to_s]&.value_before_type_cast }
|
|
201
|
+
else
|
|
202
|
+
{}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def required_attribute?(name)
|
|
207
|
+
validators_on(name).any? { |validator| validator.kind == :presence }
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The recursive slot walker (composition contracts). Works on
|
|
212
|
+
# ANY slot-owning class - poetry components and their internal
|
|
213
|
+
# builder classes alike (Menubar::Menu is a plain ViewComponent::Base)
|
|
214
|
+
# - so the registry can state the full nested call surface:
|
|
215
|
+
#
|
|
216
|
+
# - types: a polymorphic slot's with_<type> setters
|
|
217
|
+
# - setter_args: max POSITIONAL arity per setter, introspected from
|
|
218
|
+
# the slot lambda / renderable class (a kwargs-only lambda is 0 -
|
|
219
|
+
# forecloses `with_item(:item, ...)`, a
|
|
220
|
+
# type-as-argument convention no setter has)
|
|
221
|
+
# - builders: a class cannot be seen through a wrapping lambda
|
|
222
|
+
# (`->(**o) { Menu.new(bar: self, **o) }`), so a slot-owning class
|
|
223
|
+
# declares SLOT_BUILDERS = { setter => BuilderClass } and the
|
|
224
|
+
# walker recurses into the builder's own surface (cycle-guarded:
|
|
225
|
+
# sub-in-sub terminates by omission, not loop)
|
|
226
|
+
# - yieldless: a slot lambda that declares &block consumes the
|
|
227
|
+
# consumer's block itself (poetry convention: capture(&block) with
|
|
228
|
+
# no arguments), so a block param at the call site is nil at render
|
|
229
|
+
# (e.g. `menu.with_item do |item|`). A lambda that
|
|
230
|
+
# stores the block and calls it WITH arguments later (DataTable's
|
|
231
|
+
# per-row cell renderer) is indistinguishable by signature, so such
|
|
232
|
+
# a class declares SLOT_BLOCK_YIELDS = { setter => what the block
|
|
233
|
+
# receives } and the walker exempts those setters
|
|
234
|
+
# - setter_kwargs: the accepted keyword names of a closed-signature
|
|
235
|
+
# slot lambda (`|classes: nil, &block|`) - any other keyword is an
|
|
236
|
+
# ArgumentError at render (e.g.
|
|
237
|
+
# `with_item(class:)`). Open signatures (**rest), positional-hash
|
|
238
|
+
# signatures, and class renderables (kwargs ride the attributes
|
|
239
|
+
# hash) are unknowable-or-open and stay unemitted.
|
|
240
|
+
# - required_content: a lambda cannot be seen raising "requires a
|
|
241
|
+
# content block" (Carousel with_item), so a slot-owning class
|
|
242
|
+
# declares SLOT_REQUIRED_CONTENT = { setter => hint } (the
|
|
243
|
+
# SLOT_BUILDERS pattern) and the registry states the requirement
|
|
244
|
+
# - required_slots: a before_render raise ("Menubar menu requires
|
|
245
|
+
# with_trigger" - an omission every other
|
|
246
|
+
# check passes silently) cannot be introspected, so a slot-owning class
|
|
247
|
+
# declares REQUIRED_SLOTS = { setter => hint } and the registry
|
|
248
|
+
# states which setters a call cannot omit. Keys must resolve to a
|
|
249
|
+
# declared setter (name / singular / type) - an unresolvable key
|
|
250
|
+
# fails registry generation rather than shipping a rule no
|
|
251
|
+
# template could ever satisfy. Only declared where satisfaction
|
|
252
|
+
# is exactly derivable: a requirement satisfiable through a
|
|
253
|
+
# hand-rolled alias (NavigationMenu's with_item-or-with_link)
|
|
254
|
+
# stays undeclared - a false "missing slot" on a legitimate
|
|
255
|
+
# template is worse than a silent gap.
|
|
256
|
+
# - requires_any: the conditional any-of contracts no
|
|
257
|
+
# single-fact rule can state (Button's
|
|
258
|
+
# "content OR icon slot OR loading:", Command's "id OR
|
|
259
|
+
# aria-label"). A class declares REQUIRES_ANY = [{ hint:,
|
|
260
|
+
# content: true, slots: [...], options: [...] }, ...] mirroring
|
|
261
|
+
# its before_render predicate; a call satisfying NO listed
|
|
262
|
+
# alternative fails poetry check statically. Slot names must
|
|
263
|
+
# resolve; each group needs a hint and at least one alternative.
|
|
264
|
+
# - SLOT_RENDERS = { setter => ComponentClass }: a lambda-wrapped
|
|
265
|
+
# slot that purely forwards **options/&block to one component
|
|
266
|
+
# (Toast's with_action -> Button) hides that component from
|
|
267
|
+
# introspection exactly like SLOT_BUILDERS hides builder classes
|
|
268
|
+
# - the declaration restores the slot's "component" fact so the
|
|
269
|
+
# whole typed-slot rule family (option values, requires_content,
|
|
270
|
+
# requires_any) applies to its callers. Declare ONLY pure
|
|
271
|
+
# forwarders - a lambda that intercepts caller keys would make
|
|
272
|
+
# the projected contract a lie.
|
|
273
|
+
class << self
|
|
274
|
+
# The registry-shaped slot contracts of one slot-owning class -
|
|
275
|
+
# see the walker notes above for every emitted key.
|
|
276
|
+
#
|
|
277
|
+
# @param klass [Class] a component or builder class
|
|
278
|
+
# @param seen [Array<Class>] the recursion guard
|
|
279
|
+
# @return [Array<Hash>]
|
|
280
|
+
# @raise [Poetry::Core::Error] when a SLOT_RENDERS entry is not a
|
|
281
|
+
# poetry component class
|
|
282
|
+
def slot_surface(klass, seen: [])
|
|
283
|
+
return [] unless klass.respond_to?(:registered_slots)
|
|
284
|
+
|
|
285
|
+
builders = declared_builders(klass)
|
|
286
|
+
required_content = declared_required_content(klass)
|
|
287
|
+
block_yields = declared_constant(klass, :SLOT_BLOCK_YIELDS)
|
|
288
|
+
renders = declared_constant(klass, :SLOT_RENDERS)
|
|
289
|
+
slot_docs = klass.respond_to?(:slot_docs) ? klass.slot_docs : {}
|
|
290
|
+
klass.registered_slots.map do |slot_name, config|
|
|
291
|
+
definition = { name: slot_name, many: config[:collection] == true }
|
|
292
|
+
doc = slot_docs[slot_name.to_sym]
|
|
293
|
+
definition[:description] = doc if doc
|
|
294
|
+
renderable = config[:renderable]
|
|
295
|
+
definition[:component] = renderable.component_path if renderable.respond_to?(:component_path)
|
|
296
|
+
# A declared pure-forwarding lambda (SLOT_RENDERS) restores
|
|
297
|
+
# the component fact a wrapping lambda hides.
|
|
298
|
+
# Polymorphic slots stay out - their types are their contract.
|
|
299
|
+
if config[:renderable_hash].nil? &&
|
|
300
|
+
(declared = renders[slot_setters(slot_name, config).first&.to_sym])
|
|
301
|
+
unless declared.respond_to?(:component_path)
|
|
302
|
+
raise Poetry::Core::Error,
|
|
303
|
+
"#{klass}::SLOT_RENDERS[#{slot_name}] must be a poetry component class"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
definition[:component] ||= declared.component_path
|
|
307
|
+
end
|
|
308
|
+
definition[:types] = config[:renderable_hash].keys if config[:renderable_hash]
|
|
309
|
+
setter_args = setter_positional_args(slot_name, config)
|
|
310
|
+
definition[:setter_args] = setter_args unless setter_args.empty?
|
|
311
|
+
setter_kwargs = setter_keyword_args(slot_name, config)
|
|
312
|
+
definition[:setter_kwargs] = setter_kwargs unless setter_kwargs.empty?
|
|
313
|
+
yieldless = yieldless_setters(slot_name, config) - block_yields.keys
|
|
314
|
+
definition[:yieldless] = yieldless unless yieldless.empty?
|
|
315
|
+
required = required_content.slice(*slot_setters(slot_name, config).map(&:to_sym))
|
|
316
|
+
definition[:required_content] = required unless required.empty?
|
|
317
|
+
surfaces = builder_surfaces(slot_name, config, builders, seen + [klass])
|
|
318
|
+
definition[:builders] = surfaces unless surfaces.empty?
|
|
319
|
+
definition
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# The validated REQUIRED_SLOTS declaration of a slot-owning class:
|
|
324
|
+
# each key must name a setter the given slot definitions actually
|
|
325
|
+
# generate (the slot itself, a collection's singular, or a
|
|
326
|
+
# polymorphic type).
|
|
327
|
+
#
|
|
328
|
+
# @param klass [Class] the slot-owning class
|
|
329
|
+
# @param definitions [Array<Hash>] the class's {slot_surface}
|
|
330
|
+
# @return [Hash{String => String}] setter name => hint
|
|
331
|
+
# @raise [Poetry::Core::Error] for a key matching no slot setter
|
|
332
|
+
def required_slots_surface(klass, definitions)
|
|
333
|
+
declared_constant(klass, :REQUIRED_SLOTS).to_h do |key, hint|
|
|
334
|
+
name = key.to_s
|
|
335
|
+
unless definitions.any? { |slot| resolves_setter?(slot, name) }
|
|
336
|
+
raise Poetry::Core::Error,
|
|
337
|
+
"#{klass}::REQUIRED_SLOTS key #{key.inspect} matches no slot setter"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
[name, hint]
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# The validated REQUIRES_ANY declaration: each group needs
|
|
345
|
+
# a hint plus at least one alternative, and slot alternatives must
|
|
346
|
+
# name setters the definitions actually generate.
|
|
347
|
+
#
|
|
348
|
+
# @param klass [Class] the slot-owning class
|
|
349
|
+
# @param definitions [Array<Hash>] the class's {slot_surface}
|
|
350
|
+
# @return [Array<Hash>] one normalized group per declaration
|
|
351
|
+
# (hint, plus content/slots/options as declared)
|
|
352
|
+
# @raise [Poetry::Core::Error] for a group without a hint or an
|
|
353
|
+
# alternative, or a slot matching no slot setter
|
|
354
|
+
def requires_any_surface(klass, definitions)
|
|
355
|
+
declared_constant(klass, :REQUIRES_ANY).map do |group|
|
|
356
|
+
group = group.transform_keys(&:to_s)
|
|
357
|
+
slots = (group["slots"] || []).map(&:to_s)
|
|
358
|
+
options = (group["options"] || []).map(&:to_s)
|
|
359
|
+
unless group["hint"] && (group["content"] || slots.any? || options.any?)
|
|
360
|
+
raise Poetry::Core::Error,
|
|
361
|
+
"#{klass}::REQUIRES_ANY group needs a hint and at least one alternative"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
slots.each do |name|
|
|
365
|
+
next if definitions.any? { |slot| resolves_setter?(slot, name) }
|
|
366
|
+
|
|
367
|
+
raise Poetry::Core::Error,
|
|
368
|
+
"#{klass}::REQUIRES_ANY slot #{name.inspect} matches no slot setter"
|
|
369
|
+
end
|
|
370
|
+
emitted = { "hint" => group["hint"] }
|
|
371
|
+
emitted["content"] = true if group["content"]
|
|
372
|
+
emitted["slots"] = slots if slots.any?
|
|
373
|
+
emitted["options"] = options if options.any?
|
|
374
|
+
emitted
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Every own with_* method that is neither a slot-generated setter
|
|
379
|
+
# (with_<name>/<singular>/<type> and their _content twins) nor
|
|
380
|
+
# inherited - NavigationMenu#with_link, PieChart's with_py.
|
|
381
|
+
#
|
|
382
|
+
# @param klass [Class] the slot-owning class
|
|
383
|
+
# @param definitions [Array<Hash>] the class's {slot_surface}
|
|
384
|
+
# @return [Array<String>] the setter names without their with_
|
|
385
|
+
# prefix, sorted
|
|
386
|
+
def hand_rolled_setters(klass, definitions)
|
|
387
|
+
generated = definitions.flat_map do |slot|
|
|
388
|
+
names = [slot[:name].to_s]
|
|
389
|
+
names << slot[:name].to_s.delete_suffix("s") if slot[:many]
|
|
390
|
+
names.concat((slot[:types] || []).map(&:to_s))
|
|
391
|
+
names
|
|
392
|
+
end
|
|
393
|
+
klass.public_instance_methods(false).map(&:to_s)
|
|
394
|
+
.select { |method| method.start_with?("with_") }
|
|
395
|
+
.reject { |method| method.end_with?("_content") }
|
|
396
|
+
.map { |method| method.delete_prefix("with_") }
|
|
397
|
+
.sort - generated
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
private
|
|
401
|
+
|
|
402
|
+
def declared_builders(klass)
|
|
403
|
+
declared_constant(klass, :SLOT_BUILDERS)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def declared_required_content(klass)
|
|
407
|
+
declared_constant(klass, :SLOT_REQUIRED_CONTENT)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def declared_constant(klass, name)
|
|
411
|
+
klass.const_defined?(name) ? klass.const_get(name) : {}
|
|
412
|
+
rescue NameError
|
|
413
|
+
{}
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Does a slot definition generate this setter name? Mirrors the
|
|
417
|
+
# checker's slot_entry resolution: exact name, a collection's
|
|
418
|
+
# singular, or a polymorphic type.
|
|
419
|
+
def resolves_setter?(slot, name)
|
|
420
|
+
slot[:name].to_s == name ||
|
|
421
|
+
(slot[:many] && slot[:name].to_s == "#{name}s") ||
|
|
422
|
+
(slot[:types] || []).map(&:to_s).include?(name)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# The per-item setter suffixes a slot generates (the plural batch
|
|
426
|
+
# setter of a collection has a different shape and is not tracked).
|
|
427
|
+
def slot_setters(slot_name, config)
|
|
428
|
+
return config[:renderable_hash].keys.map(&:to_s) if config[:renderable_hash]
|
|
429
|
+
|
|
430
|
+
name = slot_name.to_s
|
|
431
|
+
[config[:collection] ? name.delete_suffix("s") : name]
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def setter_positional_args(slot_name, config)
|
|
435
|
+
if (types = config[:renderable_hash])
|
|
436
|
+
types.filter_map do |type, definition|
|
|
437
|
+
arity = positional_arity(definition[:renderable_function] || definition[:renderable])
|
|
438
|
+
[type, arity] if arity
|
|
439
|
+
end.to_h
|
|
440
|
+
else
|
|
441
|
+
setter = slot_setters(slot_name, config).first
|
|
442
|
+
arity = positional_arity(config[:renderable_function] || config[:renderable])
|
|
443
|
+
arity ? { setter.to_sym => arity } : {}
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def setter_keyword_args(slot_name, config)
|
|
448
|
+
if (types = config[:renderable_hash])
|
|
449
|
+
types.filter_map do |type, definition|
|
|
450
|
+
names = keyword_names(definition[:renderable_function] || definition[:renderable])
|
|
451
|
+
[type, names] if names
|
|
452
|
+
end.to_h
|
|
453
|
+
else
|
|
454
|
+
setter = slot_setters(slot_name, config).first
|
|
455
|
+
names = keyword_names(config[:renderable_function] || config[:renderable])
|
|
456
|
+
names ? { setter.to_sym => names } : {}
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def yieldless_setters(slot_name, config)
|
|
461
|
+
if (types = config[:renderable_hash])
|
|
462
|
+
types.filter_map do |type, definition|
|
|
463
|
+
type if consumes_block?(definition[:renderable_function] || definition[:renderable])
|
|
464
|
+
end
|
|
465
|
+
elsif consumes_block?(config[:renderable_function] || config[:renderable])
|
|
466
|
+
slot_setters(slot_name, config).map(&:to_sym)
|
|
467
|
+
else
|
|
468
|
+
[]
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# The closed keyword surface of a slot lambda, or nil when open or
|
|
473
|
+
# unknowable: class renderables take kwargs through the attributes
|
|
474
|
+
# hash, and a lambda with no keywords at all has nothing to
|
|
475
|
+
# enumerate.
|
|
476
|
+
def keyword_names(callable)
|
|
477
|
+
return nil if callable.is_a?(Class) || !callable.respond_to?(:parameters)
|
|
478
|
+
|
|
479
|
+
parameters = callable.parameters
|
|
480
|
+
return nil if parameters.any? { |kind, _name| OPEN_PARAMETER_KINDS.include?(kind) }
|
|
481
|
+
|
|
482
|
+
names = parameters.filter_map { |kind, name| name.to_s if KEYWORD_PARAMETER_KINDS.include?(kind) }
|
|
483
|
+
names.empty? ? nil : names
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# A lambda declaring &block takes the consumer's block for itself -
|
|
487
|
+
# ViewComponent never renders it against a component, so a block
|
|
488
|
+
# param at the call site can only be nil. Class renderables and
|
|
489
|
+
# block-less lambdas returning components DO yield (render_in
|
|
490
|
+
# yields the component instance).
|
|
491
|
+
def consumes_block?(callable)
|
|
492
|
+
!callable.is_a?(Class) && callable.respond_to?(:parameters) &&
|
|
493
|
+
callable.parameters.any? { |kind, _name| kind == :block }
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Max positional argument count, or nil when unknowable (no
|
|
497
|
+
# callable, or a *rest signature).
|
|
498
|
+
def positional_arity(callable)
|
|
499
|
+
parameters =
|
|
500
|
+
if callable.is_a?(Class)
|
|
501
|
+
callable.instance_method(:initialize).parameters
|
|
502
|
+
elsif callable.respond_to?(:parameters)
|
|
503
|
+
callable.parameters
|
|
504
|
+
end
|
|
505
|
+
return nil unless parameters
|
|
506
|
+
return nil if parameters.any? { |kind, _name| kind == :rest }
|
|
507
|
+
|
|
508
|
+
parameters.count { |kind, _name| POSITIONAL_KINDS.include?(kind) }
|
|
509
|
+
rescue NameError
|
|
510
|
+
nil
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def builder_surfaces(slot_name, config, builders, seen)
|
|
514
|
+
slot_setters(slot_name, config).filter_map do |setter|
|
|
515
|
+
builder = builders[setter.to_sym]
|
|
516
|
+
next if builder.nil? || seen.include?(builder)
|
|
517
|
+
|
|
518
|
+
slots = slot_surface(builder, seen: seen)
|
|
519
|
+
extras = hand_rolled_setters(builder, slots)
|
|
520
|
+
next if slots.empty? && extras.empty?
|
|
521
|
+
|
|
522
|
+
surface = { slots: slots }
|
|
523
|
+
surface[:slot_extras] = extras unless extras.empty?
|
|
524
|
+
required = required_slots_surface(builder, slots)
|
|
525
|
+
surface[:required_slots] = required unless required.empty?
|
|
526
|
+
[setter.to_sym, surface]
|
|
527
|
+
end.to_h
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|