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,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
module Concerns
|
|
6
|
+
# The Options concern provides a DSL for defining typed attributes in components.
|
|
7
|
+
# It extends the basic attribute functionality with support for ActiveModel types,
|
|
8
|
+
# proc defaults, and tracking of which attributes have been explicitly set vs using defaults.
|
|
9
|
+
#
|
|
10
|
+
# The registration, tracking, and hierarchy machinery lives in
|
|
11
|
+
# DeclaredAttributes (shared with Styles); this concern owns the
|
|
12
|
+
# option-specific surface: ActiveModel types and value formats.
|
|
13
|
+
#
|
|
14
|
+
# Unlike Styles, Options:
|
|
15
|
+
# - Do not have variants
|
|
16
|
+
# - Do not generate CSS
|
|
17
|
+
# - Support all ActiveModel types (string, integer, boolean, float, etc.)
|
|
18
|
+
#
|
|
19
|
+
# @example Basic usage with types
|
|
20
|
+
# class MyComponent < Poetry::Core::Component
|
|
21
|
+
# option :title, :string, default: "Untitled"
|
|
22
|
+
# option :count, :integer, default: 0
|
|
23
|
+
# option :enabled, :boolean, default: true
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Proc defaults that reference other attributes
|
|
27
|
+
# class Card::Component < Poetry::Core::Component
|
|
28
|
+
# option :title, :string, default: "Card"
|
|
29
|
+
# option :aria_label, :string, default: -> { title }
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# card = Card::Component.new(title: "My Card")
|
|
33
|
+
# card.aria_label # => "My Card" (inherited from title)
|
|
34
|
+
# card.title = "Updated"
|
|
35
|
+
# card.aria_label # => "Updated" (still follows title)
|
|
36
|
+
#
|
|
37
|
+
# card2 = Card::Component.new(title: "Card", aria_label: "Custom Label")
|
|
38
|
+
# card2.title = "Updated"
|
|
39
|
+
# card2.aria_label # => "Custom Label" (explicitly set, doesn't follow title)
|
|
40
|
+
#
|
|
41
|
+
# @example Required attributes
|
|
42
|
+
# class MyComponent < Poetry::Core::Component
|
|
43
|
+
# option :id, :string, required: true
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# @example Various types
|
|
47
|
+
# class MyComponent < Poetry::Core::Component
|
|
48
|
+
# option :price, :decimal
|
|
49
|
+
# option :score, :float
|
|
50
|
+
# option :created_at, :datetime
|
|
51
|
+
# end
|
|
52
|
+
module Options
|
|
53
|
+
extend ActiveSupport::Concern
|
|
54
|
+
include DeclaredAttributes
|
|
55
|
+
|
|
56
|
+
included do
|
|
57
|
+
class_attribute :registered_options,
|
|
58
|
+
instance_writer: true,
|
|
59
|
+
instance_predicate: false,
|
|
60
|
+
default: nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class_methods do
|
|
64
|
+
# Defines an option attribute for the component.
|
|
65
|
+
#
|
|
66
|
+
# @param name [Symbol, String] the name of the option attribute
|
|
67
|
+
# @param type [Symbol] the ActiveModel type (:string, :integer, :boolean, :float, :decimal, :value, etc.)
|
|
68
|
+
# @param options [Hash] configuration options
|
|
69
|
+
# @option options [Object, Proc] :default the default value (can be a proc for dynamic defaults)
|
|
70
|
+
# @option options [Boolean] :required whether this attribute must be provided
|
|
71
|
+
#
|
|
72
|
+
# @example Static default
|
|
73
|
+
# option :title, :string, default: "Untitled"
|
|
74
|
+
#
|
|
75
|
+
# @example Proc default
|
|
76
|
+
# option :aria_label, :string, default: -> { title }
|
|
77
|
+
#
|
|
78
|
+
# @example Required attribute
|
|
79
|
+
# option :id, :string, required: true
|
|
80
|
+
#
|
|
81
|
+
# @example Boolean attribute
|
|
82
|
+
# option :enabled, :boolean, default: false
|
|
83
|
+
#
|
|
84
|
+
# @example Value format (machine-checkable value contract)
|
|
85
|
+
# option :name, :symbol, required: true, format: :"icon-name"
|
|
86
|
+
# @return [void]
|
|
87
|
+
def option(name, type, **options)
|
|
88
|
+
register_declared_attribute(:option, name, options)
|
|
89
|
+
|
|
90
|
+
required, default_value = extract_declared_defaults(options)
|
|
91
|
+
register_option_format(name, options.delete(:format))
|
|
92
|
+
|
|
93
|
+
attribute(name, type, **options)
|
|
94
|
+
add_option_validations(name, type, required)
|
|
95
|
+
setup_declared_tracking(:option, name, default_value)
|
|
96
|
+
define_type_getter(name, type)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns all option attributes that have default values (static or proc).
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<Symbol>] sorted array of attribute names with defaults
|
|
102
|
+
def option_attributes_with_defaults
|
|
103
|
+
declared_attributes_with_defaults(:option)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns option attributes that have static (non-proc) default values.
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<Symbol>] sorted array of attribute names with static defaults
|
|
109
|
+
def option_attributes_with_static_defaults
|
|
110
|
+
declared_attributes_with_static_defaults(:option)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns option attributes that have proc default values.
|
|
114
|
+
# Proc defaults allow dynamic defaults that can reference other attributes.
|
|
115
|
+
#
|
|
116
|
+
# @return [Array<Symbol>] sorted array of attribute names with proc defaults
|
|
117
|
+
def option_attributes_with_proc_defaults
|
|
118
|
+
declared_attributes_with_proc_defaults(:option)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns all option attributes defined on this component and its ancestors.
|
|
122
|
+
#
|
|
123
|
+
# @return [Array<Symbol>] sorted array of all option attribute names
|
|
124
|
+
def option_attributes
|
|
125
|
+
declared_attributes(:option)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The doc: strings declared on this component's options,
|
|
129
|
+
# hierarchy-wide (nearest declaration wins).
|
|
130
|
+
#
|
|
131
|
+
# @return [Hash{Symbol => String}]
|
|
132
|
+
def option_docs
|
|
133
|
+
declared_docs(:option)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Checks if the given name is a defined option attribute.
|
|
137
|
+
#
|
|
138
|
+
# @param name [Symbol, String] the attribute name to check
|
|
139
|
+
# @return [Boolean] true if the attribute is an option attribute
|
|
140
|
+
def has_option_attribute?(name)
|
|
141
|
+
option_attributes.include?(name.to_sym)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns the type for a given option attribute.
|
|
145
|
+
#
|
|
146
|
+
# @param name [Symbol, String] the attribute name
|
|
147
|
+
# @return [Symbol, nil] the type of the attribute
|
|
148
|
+
def option_type(name)
|
|
149
|
+
types = collect_option_types_from_hierarchy
|
|
150
|
+
types[name.to_sym]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Returns the declared value format for a given option attribute
|
|
154
|
+
# (e.g. :"icon-name") - the machine-checkable value contract the
|
|
155
|
+
# registry and poetry check read. Nil when the option is free-form.
|
|
156
|
+
#
|
|
157
|
+
# @param name [Symbol, String] the attribute name
|
|
158
|
+
# @return [Symbol, nil] the declared format
|
|
159
|
+
def option_format(name)
|
|
160
|
+
collect_declared_map(:@_option_formats)[name.to_sym]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# Collects option types from the class hierarchy. An ancestor's
|
|
166
|
+
# declaration wins on redeclaration (formats resolve the other
|
|
167
|
+
# way, nearest wins).
|
|
168
|
+
#
|
|
169
|
+
# @return [Hash{Symbol => Symbol}] map of attribute names to types
|
|
170
|
+
def collect_option_types_from_hierarchy
|
|
171
|
+
types = {}
|
|
172
|
+
|
|
173
|
+
declared_hierarchy do |klass|
|
|
174
|
+
klass_types = klass.instance_variable_get(:@_option_types)
|
|
175
|
+
types.merge!(klass_types) if klass_types
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
types
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Records a declared value format so option_format can surface it
|
|
182
|
+
# (hierarchy-walked like types, so subclasses inherit it).
|
|
183
|
+
#
|
|
184
|
+
# @param name [Symbol, String] the attribute name
|
|
185
|
+
# @param format [Symbol, nil] the declared format, if any
|
|
186
|
+
def register_option_format(name, format)
|
|
187
|
+
return unless format
|
|
188
|
+
|
|
189
|
+
(@_option_formats ||= {})[name.to_sym] = format.to_sym
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Adds validations for the option attribute.
|
|
193
|
+
#
|
|
194
|
+
# @param name [Symbol] the attribute name
|
|
195
|
+
# @param _type [Symbol] the attribute type (unused; kept for signature parity)
|
|
196
|
+
# @param required [Boolean] whether the attribute is required
|
|
197
|
+
def add_option_validations(name, _type, required)
|
|
198
|
+
# Type validation is automatic via ActiveModel::Type
|
|
199
|
+
validates name, presence: true if required
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Defines a singleton method to access the type for this attribute.
|
|
203
|
+
#
|
|
204
|
+
# @param name [Symbol] the attribute name
|
|
205
|
+
# @param type [Symbol] the type value
|
|
206
|
+
def define_type_getter(name, type)
|
|
207
|
+
(@_option_types ||= {})[name.to_sym] = type
|
|
208
|
+
define_singleton_method("#{name}_type") { type }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Checks if the given attribute is an option attribute.
|
|
213
|
+
#
|
|
214
|
+
# @param name [Symbol, String] the attribute name to check
|
|
215
|
+
# @return [Boolean] true if the attribute is an option attribute
|
|
216
|
+
def option_attribute?(name)
|
|
217
|
+
self.class.has_option_attribute?(name.to_sym)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Returns all option attributes defined on this component's class.
|
|
221
|
+
#
|
|
222
|
+
# @return [Array<Symbol>] sorted array of all option attribute names
|
|
223
|
+
def option_attributes
|
|
224
|
+
self.class.option_attributes
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Returns only the option attributes that have been explicitly set (not using defaults).
|
|
228
|
+
#
|
|
229
|
+
# @return [Array<Symbol>] sorted array of initialized attribute names
|
|
230
|
+
def initialized_option_attributes
|
|
231
|
+
initialized_declared_attributes(registered_options)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Checks if an option attribute has been explicitly initialized.
|
|
235
|
+
#
|
|
236
|
+
# @param name [Symbol, String] the attribute name to check
|
|
237
|
+
# @return [Boolean] true if the attribute was explicitly set
|
|
238
|
+
def option_attribute_initialized?(name)
|
|
239
|
+
declared_attribute_registered?(registered_options, name)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Returns all option attributes with their initialization status.
|
|
243
|
+
#
|
|
244
|
+
# @return [Hash{Symbol => Boolean}] map of attribute names to initialized status
|
|
245
|
+
def option_attributes_status
|
|
246
|
+
option_attributes.to_h do |attr|
|
|
247
|
+
[attr, option_attribute_initialized?(attr)]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Returns a hash of all option attributes with their current values.
|
|
252
|
+
#
|
|
253
|
+
# @return [Hash{Symbol => Object}] map of attribute names to their values
|
|
254
|
+
def options
|
|
255
|
+
option_attributes.to_h do |attr|
|
|
256
|
+
[attr, send(attr)]
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
module Concerns
|
|
6
|
+
# The part contract: a
|
|
7
|
+
# hand-authored, machine-verified declaration of the component's
|
|
8
|
+
# styling surface - the data-slot parts its DOM exposes, the state
|
|
9
|
+
# attributes each part carries (and when), and the CSS custom
|
|
10
|
+
# properties that seam the part to themes and controllers.
|
|
11
|
+
#
|
|
12
|
+
# part "dialog-content",
|
|
13
|
+
# "The <dialog> panel - the positioning and animation surface",
|
|
14
|
+
# states: {
|
|
15
|
+
# "data-open" => "panel is open (setState pairs it with data-closed)",
|
|
16
|
+
# "data-closed" => "panel is closed or animating out"
|
|
17
|
+
# }
|
|
18
|
+
#
|
|
19
|
+
# Binding such a contract with types alone would keep the keys from
|
|
20
|
+
# drifting while leaving every description and condition as
|
|
21
|
+
# unverified prose. poetry binds the declaration to RENDERED DOM
|
|
22
|
+
# instead - PartContract.verify reconciles it against every preview
|
|
23
|
+
# in both directions (rendered-but-undeclared, declared-but-never-
|
|
24
|
+
# rendered), so the published contract cannot lie about the anatomy.
|
|
25
|
+
#
|
|
26
|
+
# Declarations are OWN-CLASS ONLY, deliberately not inherited: Sheet
|
|
27
|
+
# and Drawer subclass Dialog::Component yet share none of its part
|
|
28
|
+
# names (sheet-content vs dialog-content) - inheritance would leak
|
|
29
|
+
# phantom parts into every subclass with renamed anatomy.
|
|
30
|
+
module Parts
|
|
31
|
+
extend ActiveSupport::Concern
|
|
32
|
+
|
|
33
|
+
# The kebab-case shape a declared part name (data-slot value) must match.
|
|
34
|
+
PART_NAME = /\A[a-z][a-z0-9-]*\z/
|
|
35
|
+
# The data-* shape a declared state attribute must match.
|
|
36
|
+
STATE_ATTRIBUTE = /\Adata-[a-z][a-z0-9-]*\z/
|
|
37
|
+
# A trailing * declares a dynamic family (charts' per-series
|
|
38
|
+
# --color-*), matched by prefix at verify time.
|
|
39
|
+
VAR_NAME = /\A--[a-z][a-z0-9-]*\*?\z/
|
|
40
|
+
|
|
41
|
+
class_methods do
|
|
42
|
+
# Declares one part of the component's rendered anatomy.
|
|
43
|
+
#
|
|
44
|
+
# @param name [String] the part's data-slot value
|
|
45
|
+
# @param description [String] what the part is - docs/agent prose
|
|
46
|
+
# @param states [Hash] state attribute => condition prose, or
|
|
47
|
+
# => { condition: "...", values: %w[...] } for valued
|
|
48
|
+
# attributes (data-side => top/right/bottom/left)
|
|
49
|
+
# @param vars [Hash] CSS custom property => description
|
|
50
|
+
# @return [void]
|
|
51
|
+
# @raise [Poetry::Core::Error] when the class already declares a
|
|
52
|
+
# part of that name
|
|
53
|
+
def part(name, description, states: {}, vars: {})
|
|
54
|
+
definition = Parts.build(self, name, description, states: states, vars: vars)
|
|
55
|
+
own = (@part_definitions ||= [])
|
|
56
|
+
if own.any? { |existing| existing["name"] == definition["name"] }
|
|
57
|
+
raise Poetry::Core::Error, "#{self}: part #{name.inspect} declared twice"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
own << definition
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The declared contract, registry-shaped (plain string keys, so
|
|
64
|
+
# the YAML round-trips byte-identical). Own-class only - see the
|
|
65
|
+
# module docs for why subclasses never inherit anatomy.
|
|
66
|
+
#
|
|
67
|
+
# @return [Array<Hash>]
|
|
68
|
+
def part_definitions
|
|
69
|
+
@part_definitions || []
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
# Validates one `part` declaration and returns its registry-shaped
|
|
75
|
+
# hash.
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
def build(klass, name, description, states:, vars:)
|
|
79
|
+
validate_name!(klass, name)
|
|
80
|
+
unless description.is_a?(String) && !description.strip.empty?
|
|
81
|
+
raise Poetry::Core::Error, "#{klass}: part #{name.inspect} needs a description"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
definition = { "name" => name, "description" => description }
|
|
85
|
+
built_states = states.map { |attr, spec| build_state(klass, name, attr, spec) }
|
|
86
|
+
definition["states"] = built_states unless built_states.empty?
|
|
87
|
+
built_vars = vars.map { |var, desc| build_var(klass, name, var, desc) }
|
|
88
|
+
definition["vars"] = built_vars unless built_vars.empty?
|
|
89
|
+
definition
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def validate_name!(klass, name)
|
|
95
|
+
return if name.is_a?(String) && name.match?(PART_NAME)
|
|
96
|
+
|
|
97
|
+
raise Poetry::Core::Error,
|
|
98
|
+
"#{klass}: part name #{name.inspect} must be a kebab-case data-slot value"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_state(klass, part, attr, spec)
|
|
102
|
+
unless attr.is_a?(String) && attr.match?(STATE_ATTRIBUTE)
|
|
103
|
+
raise Poetry::Core::Error,
|
|
104
|
+
"#{klass}: part #{part.inspect} state #{attr.inspect} must be a data-* attribute"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
condition, values = unpack_state(spec)
|
|
108
|
+
unless condition.is_a?(String) && !condition.strip.empty?
|
|
109
|
+
raise Poetry::Core::Error,
|
|
110
|
+
"#{klass}: part #{part.inspect} state #{attr} needs a condition"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
state = { "attr" => attr, "condition" => condition }
|
|
114
|
+
if values
|
|
115
|
+
unless values.is_a?(Array) && values.any? && values.all?(String)
|
|
116
|
+
raise Poetry::Core::Error,
|
|
117
|
+
"#{klass}: part #{part.inspect} state #{attr} values must be strings"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
state["values"] = values
|
|
121
|
+
end
|
|
122
|
+
state
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def unpack_state(spec)
|
|
126
|
+
return [spec, nil] unless spec.is_a?(Hash)
|
|
127
|
+
|
|
128
|
+
normalized = spec.transform_keys(&:to_s)
|
|
129
|
+
[normalized["condition"], normalized["values"]]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def build_var(klass, part, var, description)
|
|
133
|
+
unless var.is_a?(String) && var.match?(VAR_NAME)
|
|
134
|
+
raise Poetry::Core::Error,
|
|
135
|
+
"#{klass}: part #{part.inspect} var #{var.inspect} must be a --custom-property"
|
|
136
|
+
end
|
|
137
|
+
unless description.is_a?(String) && !description.strip.empty?
|
|
138
|
+
raise Poetry::Core::Error,
|
|
139
|
+
"#{klass}: part #{part.inspect} var #{var} needs a description"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
{ "name" => var, "description" => description }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|