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,390 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
# Base component class for all Poetry components.
|
|
6
|
+
#
|
|
7
|
+
# This class serves as the foundation for all Poetry components, providing:
|
|
8
|
+
# - ActiveModel integration for attributes, assignment, and validations
|
|
9
|
+
# - Style attribute management with variants and proc defaults
|
|
10
|
+
# - HTML attribute handling and merging
|
|
11
|
+
# - Translation and wrapping helpers
|
|
12
|
+
# - Classname merging functionality
|
|
13
|
+
# - Component metadata and identification methods
|
|
14
|
+
#
|
|
15
|
+
# @example Basic component usage
|
|
16
|
+
# class MyComponent < Poetry::Core::Component
|
|
17
|
+
# style :color, default: :primary, variants: [:primary, :secondary]
|
|
18
|
+
# style :size, default: :md, variants: [:sm, :md, :lg]
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# component = MyComponent.new(color: :secondary, size: :lg, class: "custom-class")
|
|
22
|
+
# component.color # => :secondary
|
|
23
|
+
# component.size # => :lg
|
|
24
|
+
# component.html_attributes # => { class: "... custom-class" }
|
|
25
|
+
#
|
|
26
|
+
# @example Component with proc defaults
|
|
27
|
+
# class Badge < Poetry::Core::Component
|
|
28
|
+
# style :color, default: :gray, variants: [:gray, :red, :blue]
|
|
29
|
+
# style :dot_color, default: -> { color }, variants: [:gray, :red, :blue]
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# badge = Badge.new(color: :red)
|
|
33
|
+
# badge.dot_color # => :red (inherits from color)
|
|
34
|
+
#
|
|
35
|
+
# @see Poetry::Core::Concerns::Styles
|
|
36
|
+
# @see Poetry::Core::Concerns::Options
|
|
37
|
+
class Component < ViewComponent::Base
|
|
38
|
+
include ActiveModel::Attributes
|
|
39
|
+
include ActiveModel::AttributeAssignment
|
|
40
|
+
include ActiveModel::Validations
|
|
41
|
+
include Poetry::Core::Contrib::WrappedHelper
|
|
42
|
+
include Poetry::Core::Concerns::Styles
|
|
43
|
+
include Poetry::Core::Concerns::Options
|
|
44
|
+
include Poetry::Core::Concerns::Stimulus
|
|
45
|
+
include Poetry::Core::Concerns::Introspection
|
|
46
|
+
include Poetry::Core::Concerns::Parts
|
|
47
|
+
include Poetry::Core::Concerns::AgentTools
|
|
48
|
+
|
|
49
|
+
# Implementation-detail component classes (a family's inner Item /
|
|
50
|
+
# Group / Sub / Menu classes) inherit the full Component machinery
|
|
51
|
+
# without becoming PUBLISHED components: registry discovery skips
|
|
52
|
+
# internal components, and everything derived from the registry
|
|
53
|
+
# (agent surface, llms, contract gates, docs) follows. Inherited, so
|
|
54
|
+
# a subclass of an internal component stays internal.
|
|
55
|
+
class_attribute :internal_component, default: false, instance_predicate: false
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
# Marks this class (and its descendants) as an implementation
|
|
59
|
+
# detail - full machinery, no registry entry.
|
|
60
|
+
#
|
|
61
|
+
# @example An inner class of a component family
|
|
62
|
+
# class DropdownMenu::Item::Component < Poetry::Core::Component
|
|
63
|
+
# internal_component!
|
|
64
|
+
# end
|
|
65
|
+
# @return [void]
|
|
66
|
+
def internal_component!
|
|
67
|
+
self.internal_component = true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
# The current Poetry::Core configuration - a shortcut to
|
|
73
|
+
# {Poetry::Core::Config.current} for components and their templates.
|
|
74
|
+
#
|
|
75
|
+
# @return [Poetry::Core::Config] the current configuration instance
|
|
76
|
+
def config
|
|
77
|
+
Poetry::Core::Config.current
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns the component name in underscored path format.
|
|
81
|
+
# Removes the "::Component" or "Component" suffix and converts to snake_case.
|
|
82
|
+
#
|
|
83
|
+
# @return [String] the component path (e.g., "poetry/core/dot")
|
|
84
|
+
# @example
|
|
85
|
+
# Poetry::Core::Dot::Component.component_path # => "poetry/core/dot"
|
|
86
|
+
# Poetry::Core::Button::Component.component_path # => "poetry/core/button"
|
|
87
|
+
def component_path
|
|
88
|
+
name.sub(/(::Component|Component)$/, "").underscore # poetry/core/dot
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the component module name without the "::Component" suffix.
|
|
92
|
+
#
|
|
93
|
+
# @return [String] the module name (e.g., "Poetry::Core::Dot")
|
|
94
|
+
# @example
|
|
95
|
+
# Poetry::Core::Dot::Component.component_module # => "Poetry::Core::Dot"
|
|
96
|
+
def component_module
|
|
97
|
+
name.sub(/::Component$/, "")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the component identifier with path segments joined by double dashes.
|
|
101
|
+
# Useful for CSS class names and HTML data attributes.
|
|
102
|
+
#
|
|
103
|
+
# @return [String] the component identifier (e.g., "poetry--core--dot")
|
|
104
|
+
# @example
|
|
105
|
+
# Poetry::Core::Dot::Component.component_identifier # => "poetry--core--dot"
|
|
106
|
+
# Poetry::Core::Button::Component.component_identifier # => "poetry--core--button"
|
|
107
|
+
def component_identifier
|
|
108
|
+
component_path.split("/").join("--") # poetry--core--dot
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns the last segment of the component path as the title.
|
|
112
|
+
#
|
|
113
|
+
# @return [String] the component title (e.g., "dot")
|
|
114
|
+
# @example
|
|
115
|
+
# Poetry::Core::Dot::Component.component_title # => "dot"
|
|
116
|
+
# Poetry::Core::Button::Component.component_title # => "button"
|
|
117
|
+
def component_title
|
|
118
|
+
component_path.split("/").last # dot
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Declares that this component cannot render without a content block,
|
|
122
|
+
# with a hint naming what the block is (Avatar: "the initials
|
|
123
|
+
# fallback"). ONE declaration feeds both enforcement layers: the
|
|
124
|
+
# component raises via #ensure_content! at render, and the registry
|
|
125
|
+
# emits `requires_content` so poetry check flags the omission
|
|
126
|
+
# statically.
|
|
127
|
+
#
|
|
128
|
+
# @param hint [String] what the content block is, for the error message
|
|
129
|
+
# @return [void]
|
|
130
|
+
# @example
|
|
131
|
+
# class Avatar::Component < Poetry::Core::Component
|
|
132
|
+
# requires_content "the initials fallback"
|
|
133
|
+
# end
|
|
134
|
+
def requires_content(hint)
|
|
135
|
+
@required_content = hint
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The declared content-block hint, inherited like other DSL state.
|
|
139
|
+
#
|
|
140
|
+
# @return [String, nil]
|
|
141
|
+
def required_content
|
|
142
|
+
return @required_content if defined?(@required_content)
|
|
143
|
+
|
|
144
|
+
superclass.respond_to?(:required_content) ? superclass.required_content : nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The self-identification markup contract, the convention every
|
|
149
|
+
# component follows: `data-component` on the component root maps live DOM
|
|
150
|
+
# back to the component that rendered it - the hook agents, the
|
|
151
|
+
# Verifier, and the browser-verification loop key on.
|
|
152
|
+
#
|
|
153
|
+
# @return [Hash] e.g. { "data-component" => "button" }
|
|
154
|
+
def component_data_attributes
|
|
155
|
+
{ "data-component" => self.class.component_title }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# `data-slot` for a named part of the component's anatomy
|
|
159
|
+
# (skeleton parts carry their role: icon, label, spinner, ...).
|
|
160
|
+
#
|
|
161
|
+
# @param part [Symbol, String] the anatomy part name
|
|
162
|
+
# @return [Hash] e.g. { "data-slot" => "icon" }
|
|
163
|
+
def slot_data_attributes(part)
|
|
164
|
+
{ "data-slot" => part.to_s }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Enforces the class-level requires_content declaration - call from
|
|
168
|
+
# before_render. The message is built from the declaration so the
|
|
169
|
+
# runtime raise and the registry's static contract can never disagree.
|
|
170
|
+
#
|
|
171
|
+
# @return [void]
|
|
172
|
+
# @raise [ArgumentError] when the component was called without a
|
|
173
|
+
# content block
|
|
174
|
+
def ensure_content!
|
|
175
|
+
return if content?
|
|
176
|
+
|
|
177
|
+
raise ArgumentError, "#{self.class.component_module.demodulize} requires a content block " \
|
|
178
|
+
"(#{self.class.required_content})"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Indicates whether this component instance is persisted.
|
|
182
|
+
# Always returns false as components are not persisted entities.
|
|
183
|
+
#
|
|
184
|
+
# @return [Boolean] always returns false
|
|
185
|
+
def persisted?
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Initializes a new component instance with the given attributes.
|
|
190
|
+
#
|
|
191
|
+
# This method:
|
|
192
|
+
# 1. Initializes the registered_styles set for tracking explicitly set attributes
|
|
193
|
+
# 2. Marks style attributes with static defaults as initialized
|
|
194
|
+
# 3. Tracks which style attributes are being explicitly initialized via parameters
|
|
195
|
+
# 4. Separates component attributes from HTML attributes
|
|
196
|
+
# 5. Assigns the component attributes to their respective instance variables
|
|
197
|
+
#
|
|
198
|
+
# @param attributes [Hash] the attributes to initialize the component with
|
|
199
|
+
# @option attributes [Symbol, String] style attributes defined via the `style` DSL
|
|
200
|
+
# @option attributes [Symbol, String] HTML attributes (e.g., :class, :id, :data)
|
|
201
|
+
#
|
|
202
|
+
# @example Initialize with style attributes
|
|
203
|
+
# component = MyComponent.new(color: :primary, size: :lg)
|
|
204
|
+
#
|
|
205
|
+
# @example Initialize with HTML attributes
|
|
206
|
+
# component = MyComponent.new(class: "my-class", data: { controller: "example" })
|
|
207
|
+
#
|
|
208
|
+
# @example Initialize with both
|
|
209
|
+
# component = MyComponent.new(color: :primary, class: "my-class")
|
|
210
|
+
#
|
|
211
|
+
# The base component intentionally does not chain to ViewComponent::Base#initialize:
|
|
212
|
+
# it fully manages its own ActiveModel-backed attribute setup.
|
|
213
|
+
def initialize(attributes = {}) # rubocop:disable Lint/MissingSuper
|
|
214
|
+
# key: is universal semantic identity, not an HTML attribute -
|
|
215
|
+
# extracted here so it never renders literally. Not an ActiveModel
|
|
216
|
+
# option (yet): keeping it out of prop_definitions defers the
|
|
217
|
+
# registry/check surface decision to a later migration.
|
|
218
|
+
@stable_key = attributes[:key] || attributes["key"]
|
|
219
|
+
attributes = attributes.except(:key, "key") if @stable_key
|
|
220
|
+
|
|
221
|
+
# webmcp: is the per-instance opt-in of the agent-tool contract
|
|
222
|
+
# (Concerns::AgentTools) - universal like key:, never an HTML
|
|
223
|
+
# attribute; the root's Stimulus wiring carries the registration.
|
|
224
|
+
@webmcp = attributes[:webmcp] || attributes["webmcp"]
|
|
225
|
+
attributes = attributes.except(:webmcp, "webmcp") unless @webmcp.nil?
|
|
226
|
+
|
|
227
|
+
# Initialize a fresh Set for this instance
|
|
228
|
+
self.registered_styles = Set.new
|
|
229
|
+
self.registered_options = Set.new
|
|
230
|
+
|
|
231
|
+
# First, mark all style attributes with static defaults as initialized
|
|
232
|
+
# (proc defaults should NOT be marked as initialized, so they get evaluated lazily)
|
|
233
|
+
self.class.style_attributes_with_static_defaults.each do |attr|
|
|
234
|
+
registered_styles << attr.to_sym
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# First, mark all option attributes with static defaults as initialized
|
|
238
|
+
# (proc defaults should NOT be marked as initialized, so they get evaluated lazily)
|
|
239
|
+
self.class.option_attributes_with_static_defaults.each do |attr|
|
|
240
|
+
registered_options << attr.to_sym
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Then track which style attributes are being explicitly initialized
|
|
244
|
+
attributes.each_key do |key|
|
|
245
|
+
registered_styles << key.to_sym if self.class.has_style_attribute?(key)
|
|
246
|
+
|
|
247
|
+
# Then track which option attributes are being explicitly initialized
|
|
248
|
+
registered_options << key.to_sym if self.class.has_option_attribute?(key)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
@attributes = self.class._default_attributes.deep_dup
|
|
252
|
+
html_attrs = attributes.with_indifferent_access.except(*attribute_names)
|
|
253
|
+
@html_attributes = Poetry::Core::HTML::Attributes.new(html_attrs)
|
|
254
|
+
|
|
255
|
+
assign_attributes attributes.with_indifferent_access.slice(*attribute_names)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Returns all component attributes, ensuring proc defaults are evaluated.
|
|
259
|
+
#
|
|
260
|
+
# This method overrides ActiveModel's attributes method to trigger evaluation
|
|
261
|
+
# of any proc-based default values that haven't been explicitly set.
|
|
262
|
+
#
|
|
263
|
+
# @return [Hash] the component's attributes with all defaults evaluated
|
|
264
|
+
def attributes
|
|
265
|
+
# Trigger evaluation of proc defaults that haven't been explicitly set
|
|
266
|
+
# This ensures they appear in the attributes hash
|
|
267
|
+
self.class.style_attributes_with_proc_defaults.each do |attr|
|
|
268
|
+
# Access the attribute to trigger proc evaluation if needed
|
|
269
|
+
send(attr) if respond_to?(attr) && !style_attribute_initialized?(attr)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Trigger evaluation of option proc defaults that haven't been explicitly set
|
|
273
|
+
self.class.option_attributes_with_proc_defaults.each do |attr|
|
|
274
|
+
# Access the attribute to trigger proc evaluation if needed
|
|
275
|
+
send(attr) if respond_to?(attr) && !option_attribute_initialized?(attr)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
super
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Returns HTML attributes with merged CSS classes.
|
|
282
|
+
#
|
|
283
|
+
# Combines the component's CSS classes (from the `css` method) with any
|
|
284
|
+
# additional classes passed via the `:class` HTML attribute.
|
|
285
|
+
#
|
|
286
|
+
# @return [Hash] HTML attributes with merged class names
|
|
287
|
+
# @example
|
|
288
|
+
# component = MyComponent.new(class: "custom-class")
|
|
289
|
+
# component.html_attributes # => { class: "component-base-class custom-class" }
|
|
290
|
+
def html_attributes
|
|
291
|
+
# Set, never merge: Attributes#merge folds a class through the merger
|
|
292
|
+
# against the caller's original, so every non-utility token the caller
|
|
293
|
+
# passed (a cn-* hook) came out twice. The resolved string already
|
|
294
|
+
# carries the caller's classes once, where they win conflicts.
|
|
295
|
+
attributes = @html_attributes.deep_dup
|
|
296
|
+
attributes[:class] = classnames(css, @html_attributes[:class])
|
|
297
|
+
attributes
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# The caller-supplied semantic identity (key:), if any.
|
|
301
|
+
#
|
|
302
|
+
# @return [Object, nil]
|
|
303
|
+
attr_reader :stable_key
|
|
304
|
+
|
|
305
|
+
# The instance-id ladder: an explicit caller root id wins; a key:
|
|
306
|
+
# derives a stable component-namespaced token (Turbo morph pairs it
|
|
307
|
+
# across renders, cached fragments stay composable); otherwise
|
|
308
|
+
# random - unkeyed components over-replace under morph, they never
|
|
309
|
+
# falsely retain. Call sites memoize (`@instance_id ||=`); this
|
|
310
|
+
# stays pure.
|
|
311
|
+
#
|
|
312
|
+
# @param prefix [String] the component-namespaced id prefix
|
|
313
|
+
# @return [String] the resolved DOM id
|
|
314
|
+
def poetry_instance_id(prefix)
|
|
315
|
+
explicit = @html_attributes["id"].presence
|
|
316
|
+
return explicit.to_s if explicit
|
|
317
|
+
|
|
318
|
+
token = Poetry::Core::StableId.key_token(stable_key)
|
|
319
|
+
return "#{prefix}-#{token}" if token
|
|
320
|
+
|
|
321
|
+
sequence = Poetry::Core::StableId.next_sequence_token
|
|
322
|
+
return "#{prefix}-#{sequence}" if sequence
|
|
323
|
+
|
|
324
|
+
"#{prefix}-#{SecureRandom.hex(8)}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Merges multiple class name values into a single string.
|
|
328
|
+
#
|
|
329
|
+
# Uses the configured classname merger (typically Tailwind Merge) to
|
|
330
|
+
# intelligently combine CSS class names, handling conflicts and duplicates.
|
|
331
|
+
#
|
|
332
|
+
# @param classnames [Array<String, nil>] class names to merge
|
|
333
|
+
# @return [String] the merged class names
|
|
334
|
+
# @example
|
|
335
|
+
# classnames("text-red-500", "text-blue-500") # => "text-blue-500"
|
|
336
|
+
def classnames(*classnames)
|
|
337
|
+
self.class.config.classname_merger.merge(*classnames)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# HTML-safe JSON for embedding in a `<script type="application/json">`
|
|
341
|
+
# data island. Escapes the script-terminating characters (`<` `>` `&`,
|
|
342
|
+
# plus the JS line separators U+2028/U+2029) to their JSON `\uXXXX`
|
|
343
|
+
# forms, INDEPENDENT of the host's
|
|
344
|
+
# `ActiveSupport.escape_html_entities_in_json` setting: that flag
|
|
345
|
+
# defaults to true (which escapes them for us) but is host-overridable
|
|
346
|
+
# to false (legitimately, e.g. in API apps), and a component must not
|
|
347
|
+
# depend on a global it neither sets nor checks. `</script>` closes a
|
|
348
|
+
# script element regardless of its `type`, so an unescaped value
|
|
349
|
+
# carrying it would break out of the island into live HTML. Idempotent
|
|
350
|
+
# when the host already escapes (the `\uXXXX` forms carry no literal
|
|
351
|
+
# `<`/`>`/`&`), and JSON.parse decodes the escapes back to the original
|
|
352
|
+
# text. Accepts a pre-serialized JSON string or any `to_json`-able object.
|
|
353
|
+
#
|
|
354
|
+
# @param json [String, Object] serialized JSON, or an object to serialize
|
|
355
|
+
# @return [ActiveSupport::SafeBuffer] escaped, HTML-safe JSON text
|
|
356
|
+
def script_json(json)
|
|
357
|
+
json = json.to_json unless json.is_a?(String)
|
|
358
|
+
json.gsub(/[<>&
]/) { |char| format('\u%04x', char.ord) }.html_safe
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Reduces a value to a token safe for a DOM id and a CSS selector:
|
|
362
|
+
# `[A-Za-z0-9_-]` only. A user-controlled id would otherwise break out
|
|
363
|
+
# of the `<style>` block or the id attribute it is interpolated into.
|
|
364
|
+
# Returns nil when nothing safe remains (callers fall back to a random
|
|
365
|
+
# token), preserving the id attribute / JS-selector match by using the
|
|
366
|
+
# same reduced value on both sides.
|
|
367
|
+
#
|
|
368
|
+
# @param value [Object] the requested id
|
|
369
|
+
# @return [String, nil] the safe token, or nil if empty
|
|
370
|
+
def dom_id_token(value)
|
|
371
|
+
value.to_s.gsub(/[^A-Za-z0-9_-]/, "").presence
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Renders the component to an HTML string.
|
|
375
|
+
#
|
|
376
|
+
# Creates a minimal controller and view context to render the component
|
|
377
|
+
# outside of a normal request cycle. Useful for testing and debugging.
|
|
378
|
+
#
|
|
379
|
+
# @return [String] the rendered HTML
|
|
380
|
+
# @example
|
|
381
|
+
# component = MyComponent.new(color: :primary)
|
|
382
|
+
# component.to_html # => "<div class=\"...\">...</div>"
|
|
383
|
+
def to_html
|
|
384
|
+
controller = ActionController::Base.new
|
|
385
|
+
controller.request = ActionDispatch::TestRequest.create
|
|
386
|
+
render_in(controller.view_context)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|