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.
Files changed (166) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/DESIGN.md +145 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +30 -0
  6. data/THIRD_PARTY_NOTICES.md +507 -0
  7. data/app/components/poetry/core/box/component.rb +82 -0
  8. data/app/components/poetry/core/component.rb +390 -0
  9. data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
  10. data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
  11. data/app/components/poetry/core/concerns/introspection.rb +533 -0
  12. data/app/components/poetry/core/concerns/options.rb +262 -0
  13. data/app/components/poetry/core/concerns/parts.rb +148 -0
  14. data/app/components/poetry/core/concerns/stimulus.rb +363 -0
  15. data/app/components/poetry/core/concerns/styles.rb +323 -0
  16. data/app/components/poetry/core/style.rb +150 -0
  17. data/app/components/poetry/core/wrapper/component.rb +90 -0
  18. data/app/javascript/poetry/core/accordion_controller.js +165 -0
  19. data/app/javascript/poetry/core/action_bar_controller.js +109 -0
  20. data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
  21. data/app/javascript/poetry/core/calendar_controller.js +406 -0
  22. data/app/javascript/poetry/core/carousel_controller.js +167 -0
  23. data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
  24. data/app/javascript/poetry/core/checked_controller.js +208 -0
  25. data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
  26. data/app/javascript/poetry/core/combobox_controller.js +1095 -0
  27. data/app/javascript/poetry/core/command_controller.js +510 -0
  28. data/app/javascript/poetry/core/context_menu_controller.js +204 -0
  29. data/app/javascript/poetry/core/date_field_controller.js +584 -0
  30. data/app/javascript/poetry/core/date_picker_controller.js +111 -0
  31. data/app/javascript/poetry/core/deferred_controller.js +111 -0
  32. data/app/javascript/poetry/core/dialog_controller.js +206 -0
  33. data/app/javascript/poetry/core/dismissable_controller.js +182 -0
  34. data/app/javascript/poetry/core/drawer_controller.js +365 -0
  35. data/app/javascript/poetry/core/file_input_controller.js +191 -0
  36. data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
  37. data/app/javascript/poetry/core/helpers/announce.js +228 -0
  38. data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
  39. data/app/javascript/poetry/core/helpers/collection.js +18 -0
  40. data/app/javascript/poetry/core/helpers/direction.js +15 -0
  41. data/app/javascript/poetry/core/helpers/escape.js +38 -0
  42. data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
  43. data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
  44. data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
  45. data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
  46. data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
  47. data/app/javascript/poetry/core/helpers/mask.js +283 -0
  48. data/app/javascript/poetry/core/helpers/portal.js +217 -0
  49. data/app/javascript/poetry/core/helpers/presence.js +210 -0
  50. data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
  51. data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
  52. data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
  53. data/app/javascript/poetry/core/helpers/state.js +80 -0
  54. data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
  55. data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
  56. data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
  57. data/app/javascript/poetry/core/hotkey_controller.js +55 -0
  58. data/app/javascript/poetry/core/hover_card_controller.js +431 -0
  59. data/app/javascript/poetry/core/index.js +218 -0
  60. data/app/javascript/poetry/core/mask_controller.js +500 -0
  61. data/app/javascript/poetry/core/menu_controller.js +1050 -0
  62. data/app/javascript/poetry/core/menubar_controller.js +329 -0
  63. data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
  64. data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
  65. data/app/javascript/poetry/core/number_field_controller.js +404 -0
  66. data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
  67. data/app/javascript/poetry/core/otp_controller.js +219 -0
  68. data/app/javascript/poetry/core/popover_controller.js +300 -0
  69. data/app/javascript/poetry/core/popper_controller.js +439 -0
  70. data/app/javascript/poetry/core/pressed_controller.js +74 -0
  71. data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
  72. data/app/javascript/poetry/core/radio_group_controller.js +197 -0
  73. data/app/javascript/poetry/core/resizable_controller.js +202 -0
  74. data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
  75. data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
  76. data/app/javascript/poetry/core/search_field_controller.js +95 -0
  77. data/app/javascript/poetry/core/select_controller.js +924 -0
  78. data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
  79. data/app/javascript/poetry/core/sheet_controller.js +24 -0
  80. data/app/javascript/poetry/core/sidebar_controller.js +272 -0
  81. data/app/javascript/poetry/core/slider_controller.js +419 -0
  82. data/app/javascript/poetry/core/state_controller.js +82 -0
  83. data/app/javascript/poetry/core/table_selection_controller.js +153 -0
  84. data/app/javascript/poetry/core/tabs_controller.js +151 -0
  85. data/app/javascript/poetry/core/tag_group_controller.js +134 -0
  86. data/app/javascript/poetry/core/toast_controller.js +228 -0
  87. data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
  88. data/app/javascript/poetry/core/toaster_controller.js +197 -0
  89. data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
  90. data/app/javascript/poetry/core/tooltip_controller.js +541 -0
  91. data/app/javascript/poetry/core/tree_controller.js +265 -0
  92. data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
  93. data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
  94. data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
  95. data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
  96. data/app/views/poetry/core/preview.html.erb +13 -0
  97. data/config/component_registry.yml +20 -0
  98. data/config/controllers_manifest.json +1528 -0
  99. data/config/importmap.rb +11 -0
  100. data/config/state_vocabulary.json +31 -0
  101. data/lib/active_model/type/list.rb +33 -0
  102. data/lib/active_model/type/symbol.rb +36 -0
  103. data/lib/poetry/core/check/stable_identity.rb +104 -0
  104. data/lib/poetry/core/check.rb +1445 -0
  105. data/lib/poetry/core/config.rb +286 -0
  106. data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
  107. data/lib/poetry/core/css/bem_merger.rb +45 -0
  108. data/lib/poetry/core/css/bem_reference.rb +87 -0
  109. data/lib/poetry/core/css/override_scan.rb +110 -0
  110. data/lib/poetry/core/css/resolver.rb +161 -0
  111. data/lib/poetry/core/css/safelist.rb +41 -0
  112. data/lib/poetry/core/css/tailwind_merger.rb +102 -0
  113. data/lib/poetry/core/css/template_classes.rb +102 -0
  114. data/lib/poetry/core/css/theme_coverage.rb +72 -0
  115. data/lib/poetry/core/css/var_coverage.rb +75 -0
  116. data/lib/poetry/core/css/verifier.rb +106 -0
  117. data/lib/poetry/core/design_lint.rb +720 -0
  118. data/lib/poetry/core/design_md/import.rb +250 -0
  119. data/lib/poetry/core/design_md.rb +461 -0
  120. data/lib/poetry/core/engine.rb +71 -0
  121. data/lib/poetry/core/errors.rb +53 -0
  122. data/lib/poetry/core/html/attributes.rb +667 -0
  123. data/lib/poetry/core/icons.rb +157 -0
  124. data/lib/poetry/core/llms_text.rb +354 -0
  125. data/lib/poetry/core/page_architectures.rb +304 -0
  126. data/lib/poetry/core/part_contract.rb +285 -0
  127. data/lib/poetry/core/preview/abstract.rb +121 -0
  128. data/lib/poetry/core/preview/base.rb +227 -0
  129. data/lib/poetry/core/preview/sidecarable.rb +136 -0
  130. data/lib/poetry/core/preview/template.rb +186 -0
  131. data/lib/poetry/core/recipe_items.rb +82 -0
  132. data/lib/poetry/core/registry.rb +363 -0
  133. data/lib/poetry/core/registry_address.rb +115 -0
  134. data/lib/poetry/core/registry_client.rb +217 -0
  135. data/lib/poetry/core/registry_installer.rb +234 -0
  136. data/lib/poetry/core/registry_items.rb +184 -0
  137. data/lib/poetry/core/skill_text.rb +288 -0
  138. data/lib/poetry/core/stable_id.rb +110 -0
  139. data/lib/poetry/core/stimulus/builder.rb +395 -0
  140. data/lib/poetry/core/stimulus/declarations.rb +444 -0
  141. data/lib/poetry/core/stimulus/manifest.rb +68 -0
  142. data/lib/poetry/core/stimulus/merger.rb +131 -0
  143. data/lib/poetry/core/stimulus_contract.rb +263 -0
  144. data/lib/poetry/core/tag_helper.rb +10 -0
  145. data/lib/poetry/core/template_compile.rb +74 -0
  146. data/lib/poetry/core/token_import.rb +317 -0
  147. data/lib/poetry/core/tokens/color.rb +215 -0
  148. data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
  149. data/lib/poetry/core/tokens/generator.rb +211 -0
  150. data/lib/poetry/core/tokens.rb +117 -0
  151. data/lib/poetry/core/version.rb +9 -0
  152. data/lib/poetry/core.rb +52 -0
  153. data/lib/poetry-core.rb +5 -0
  154. data/tokens/aliases.css +145 -0
  155. data/tokens/tailwind-theme.css +47 -0
  156. data/tokens/tokens.css +78 -0
  157. data/tokens/tokens.dtcg.json +779 -0
  158. data/vendor/floating-ui/LICENSE +20 -0
  159. data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
  160. data/vendor/shadcn-tailwind/LICENSE +21 -0
  161. data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
  162. data/vendor/shadcn-tailwind/tailwind.css +629 -0
  163. data/vendor/tw-animate-css/LICENSE +21 -0
  164. data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
  165. data/vendor/tw-animate-css/tw-animate.css +1 -0
  166. metadata +267 -0
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Core
5
+ # Manages configuration settings for the Poetry::Core module.
6
+ #
7
+ # This class provides a flexible configuration system using ActiveSupport::OrderedOptions
8
+ # under the hood, allowing access to configuration values using either hash-style or
9
+ # method-style syntax. It supports both a singleton pattern via {.current} for global
10
+ # configuration and the ability to create custom configuration instances.
11
+ #
12
+ # The configuration system is designed to be easily extensible while providing sensible
13
+ # defaults for all Poetry::Core components.
14
+ #
15
+ # @example Accessing global configuration
16
+ # Poetry::Core::Config.current.classname_merger
17
+ # # => #<Poetry::Core::CSS::TailwindMerger:0x00007f8b1c0a3b40>
18
+ #
19
+ # @example Modifying global configuration
20
+ # Poetry::Core::Config.current.icon_library = :my_icons
21
+ #
22
+ # @example Creating a custom configuration instance
23
+ # config = Poetry::Core::Config.new
24
+ # config.classname_merger = MyCustomMerger.new
25
+ #
26
+ # @example Using hash-style access
27
+ # Poetry::Core::Config.current[:classname_merger]
28
+ # Poetry::Core::Config.current[:custom_setting] = "value"
29
+ #
30
+ # @see Poetry::Core::CSS::TailwindMerger
31
+ # @see Poetry::Core::Stimulus::Merger
32
+ class Config
33
+ class << self
34
+ # Creates a new configuration instance with default settings.
35
+ #
36
+ # This is aliased from the standard {#initialize} method to provide a more
37
+ # semantic way to create default configurations.
38
+ #
39
+ # @return [Poetry::Core::Config] A new configuration instance with default values
40
+ # @example
41
+ # config = Poetry::Core::Config.default
42
+ alias default new
43
+
44
+ # Returns the default configuration values.
45
+ #
46
+ # These defaults are used when initializing new configuration instances and
47
+ # define the standard behavior for all Poetry::Core components.
48
+ #
49
+ # The keys and their defaults:
50
+ #
51
+ # - `classname_merger` ({Poetry::Core::CSS::TailwindMerger}) - resolves
52
+ # conflicting utility classes when caller classes meet component
53
+ # classes.
54
+ # - `stimulus_merger` ({Poetry::Core::Stimulus::Merger}) - combines
55
+ # Stimulus data attributes without duplicating controllers or
56
+ # actions.
57
+ # - `css_mode` (`:tailwind`) - `:tailwind` emits resolved utility
58
+ # classes; `:bem` emits the BEM token IR for bring-your-own-CSS
59
+ # hosts.
60
+ # - `icon_library` (`:lucide`) - the active icon set, by the key it
61
+ # registered under ({Poetry::Core::Icons.register}).
62
+ # - `raise_on_missing_icon` (`nil`) - the policy for a dynamic icon
63
+ # name that resolves to nothing: nil raises in local environments
64
+ # and degrades to the fallback elsewhere; true/false force one
65
+ # behavior.
66
+ # - `icon_fallback` (`:"circle-question-mark"`) - rendered instead
67
+ # of a missing icon when not raising; nil re-raises.
68
+ # - `on_missing_icon` (`nil`) - an optional callable
69
+ # `(name:, library:, error:)` fired before the fallback renders.
70
+ # - `webmcp_registration_budget` (`20`) - the per-document cap on
71
+ # WebMCP tool registrations a page's opted-in instances may make;
72
+ # poetry-agent's `registration_budget` setting writes through.
73
+ # - `stable_id_mode` (`:off`) - the opt-in `:sequence` mode seeds a
74
+ # per-request deterministic id sequence (read the hazards in
75
+ # StableId before enabling).
76
+ # - `stable_id_seed` - the request-to-seed callable for that mode
77
+ # (defaults to the request path).
78
+ #
79
+ # @return [ActiveSupport::OrderedOptions] the default configuration
80
+ #
81
+ # @example Getting default values
82
+ # defaults = Poetry::Core::Config.defaults
83
+ # defaults.classname_merger # => #<Poetry::Core::CSS::TailwindMerger:0x00007f8b1c0a3b40>
84
+ # defaults.css_mode # => :tailwind
85
+ def defaults
86
+ ActiveSupport::OrderedOptions.new.merge!({
87
+ classname_merger: Poetry::Core::CSS::TailwindMerger.new,
88
+ stimulus_merger: Poetry::Core::Stimulus::Merger.new,
89
+ # :tailwind emits resolved utility classes (default);
90
+ # :bem emits the BEM token IR for bring-your-own-CSS
91
+ # hosts (no :both, deliberately).
92
+ css_mode: :tailwind,
93
+ # The active icon set (Lucide default; sets
94
+ # register via Poetry::Core::Icons.register).
95
+ icon_library: :lucide,
96
+ # The missing-icon policy. Static literals
97
+ # are caught by poetry check; a DYNAMIC name (a DB
98
+ # value, a user setting) surfaces at render. nil =
99
+ # auto: raise in Rails.env.local?, degrade to
100
+ # icon_fallback elsewhere. true/false force it.
101
+ raise_on_missing_icon: nil,
102
+ # StableId sequence mode (OPT-IN, experimental):
103
+ # :off (default) or :sequence - the engine's
104
+ # around_action seeds a per-request deterministic
105
+ # id sequence. Read the hazards in StableId before
106
+ # enabling; keyed identity (key:) is the general
107
+ # answer, this mode is for byte-stable content
108
+ # pages only.
109
+ stable_id_mode: :off,
110
+ stable_id_seed: ->(request) { request.path }, # rubocop:disable Style/SymbolProc -- the documented override shape
111
+ # Rendered instead of a missing icon when not
112
+ # raising; nil re-raises. Must exist in every
113
+ # registered set (Lucide ships it).
114
+ icon_fallback: :"circle-question-mark",
115
+ # Optional callable(name:, library:, error:) fired
116
+ # before the fallback renders - the instrumentation
117
+ # seam (log, notify, count). Fires per render.
118
+ on_missing_icon: nil,
119
+ # The per-document WebMCP registration budget
120
+ # (Concerns::AgentTools::WEBMCP_DEFAULT_BUDGET):
121
+ # the registrar drops registrations past it with
122
+ # a console warning. Rides the opted-in root only
123
+ # when it differs from the runtime's default.
124
+ webmcp_registration_budget: 20
125
+ })
126
+ end
127
+
128
+ # Returns the global singleton configuration instance.
129
+ #
130
+ # This method provides access to the shared configuration used throughout the
131
+ # application. The instance is created lazily on first access and persists for
132
+ # the lifetime of the application.
133
+ #
134
+ # @return [Poetry::Core::Config] The global configuration instance
135
+ #
136
+ # @example Accessing global settings
137
+ # Poetry::Core::Config.current.css_mode
138
+ # # => :tailwind
139
+ #
140
+ # @example Modifying global settings
141
+ # Poetry::Core::Config.current.classname_merger = CustomMerger.new
142
+ #
143
+ # @note Changes to the global configuration will affect all Poetry::Core components
144
+ # throughout the application.
145
+ def current
146
+ @current ||= default
147
+ end
148
+ end
149
+
150
+ # @!method classname_merger
151
+ # The merger that resolves conflicting utility classes when caller
152
+ # classes meet component classes.
153
+ # @return [Poetry::Core::CSS::TailwindMerger] The CSS class merger instance
154
+ #
155
+ # @!method classname_merger=(merger)
156
+ # Replaces the class merger.
157
+ # @param merger [Poetry::Core::CSS::TailwindMerger] The CSS class merger instance to use
158
+ # @return [Poetry::Core::CSS::TailwindMerger]
159
+ #
160
+ # @!method stimulus_merger
161
+ # The merger that combines Stimulus data attributes without
162
+ # duplicating controllers or actions.
163
+ # @return [Poetry::Core::Stimulus::Merger] The Stimulus attribute merger instance
164
+ #
165
+ # @!method stimulus_merger=(merger)
166
+ # Replaces the Stimulus attribute merger.
167
+ # @param merger [Poetry::Core::Stimulus::Merger] The Stimulus attribute merger instance to use
168
+ # @return [Poetry::Core::Stimulus::Merger]
169
+ #
170
+ # @!method css_mode
171
+ # The class emission mode: `:tailwind` resolves style values to
172
+ # utility classes, `:bem` emits the BEM token IR.
173
+ # @return [Symbol] :tailwind or :bem
174
+ #
175
+ # @!method css_mode=(mode)
176
+ # Sets the class emission mode.
177
+ # @param mode [Symbol] :tailwind or :bem
178
+ # @return [Symbol]
179
+ #
180
+ # @!method icon_library
181
+ # The key of the active icon set ({Poetry::Core::Icons.register}).
182
+ # @return [Symbol]
183
+ #
184
+ # @!method icon_library=(key)
185
+ # Selects the active icon set.
186
+ # @param key [Symbol, String] a registered library key
187
+ # @return [Symbol, String]
188
+ #
189
+ # @!method raise_on_missing_icon
190
+ # The policy for a dynamic icon name that resolves to nothing: nil
191
+ # raises in local environments and degrades to the fallback
192
+ # elsewhere; true/false force one behavior.
193
+ # @return [Boolean, nil]
194
+ #
195
+ # @!method raise_on_missing_icon=(policy)
196
+ # Sets the missing-icon policy.
197
+ # @param policy [Boolean, nil]
198
+ # @return [Boolean, nil]
199
+ #
200
+ # @!method icon_fallback
201
+ # The icon rendered instead of a missing one when not raising; nil
202
+ # re-raises. Must exist in every registered set.
203
+ # @return [Symbol, nil]
204
+ #
205
+ # @!method icon_fallback=(name)
206
+ # Sets the fallback icon.
207
+ # @param name [Symbol, nil]
208
+ # @return [Symbol, nil]
209
+ #
210
+ # @!method webmcp_registration_budget
211
+ # The per-document WebMCP registration budget the registrar
212
+ # enforces (default 20).
213
+ # @return [Integer]
214
+ #
215
+ # @!method webmcp_registration_budget=(count)
216
+ # Sets the per-document WebMCP registration budget.
217
+ # @param count [Integer]
218
+ # @return [Integer]
219
+ #
220
+ # @!method on_missing_icon
221
+ # The instrumentation hook fired before a fallback icon renders,
222
+ # called with `name:`, `library:`, and `error:`.
223
+ # @return [#call, nil]
224
+ #
225
+ # @!method on_missing_icon=(callable)
226
+ # Sets the missing-icon hook.
227
+ # @param callable [#call, nil]
228
+ # @return [#call, nil]
229
+ #
230
+ # @!method stable_id_mode
231
+ # The StableId sequence mode: `:off`, or the opt-in `:sequence`.
232
+ # @return [Symbol]
233
+ #
234
+ # @!method stable_id_mode=(mode)
235
+ # Sets the StableId sequence mode.
236
+ # @param mode [Symbol] :off or :sequence
237
+ # @return [Symbol]
238
+ #
239
+ # @!method stable_id_seed
240
+ # The request-to-seed callable the `:sequence` mode derives its
241
+ # per-request id sequence from.
242
+ # @return [#call]
243
+ #
244
+ # @!method stable_id_seed=(callable)
245
+ # Sets the seed callable.
246
+ # @param callable [#call] receives the request, returns the seed
247
+ # @return [#call]
248
+
249
+ # The declared configuration surface: every key {.defaults} ships.
250
+ # Each is a real reader/writer pair (delegated just below); unknown
251
+ # keys still flow through delegate_missing_to, so hosts may stash
252
+ # their own values.
253
+ SETTINGS = %i[classname_merger stimulus_merger css_mode icon_library
254
+ raise_on_missing_icon icon_fallback on_missing_icon
255
+ stable_id_mode stable_id_seed
256
+ webmcp_registration_budget].freeze
257
+
258
+ delegate(*SETTINGS, *SETTINGS.map { |key| :"#{key}=" }, to: :@config)
259
+
260
+ # Delegates all method calls to the internal configuration object.
261
+ #
262
+ # This allows the Config instance to act as a transparent wrapper around
263
+ # ActiveSupport::OrderedOptions, supporting both method-style and hash-style
264
+ # access to configuration values.
265
+ #
266
+ # @api private
267
+ delegate_missing_to :@config
268
+
269
+ # Initializes a new configuration instance with default values.
270
+ #
271
+ # The new instance gets a clone of the default configuration, ensuring each
272
+ # configuration object is independent and modifications won't affect the defaults
273
+ # or other instances.
274
+ #
275
+ # @return [Poetry::Core::Config] A new configuration instance
276
+ #
277
+ # @example Creating an isolated configuration
278
+ # config = Poetry::Core::Config.new
279
+ # config.css_mode = :bem
280
+ # Poetry::Core::Config.current.css_mode # => :tailwind (unchanged)
281
+ def initialize
282
+ @config = self.class.defaults.clone
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Core
5
+ # Opt-in helper mixins shipped alongside the core.
6
+ module Contrib
7
+ # Provides a convenient method to wrap components with custom HTML code.
8
+ # Adapted from an MIT-licensed source (source and license in
9
+ # THIRD_PARTY_NOTICES.md).
10
+ #
11
+ # This module adds the `#wrapped` method to components, allowing them to be
12
+ # easily wrapped with a {Poetry::Core::Wrapper::Component}. The wrapper component
13
+ # enables adding custom HTML around a component without modifying the
14
+ # component itself, and respects the component's `render?` conditional logic.
15
+ #
16
+ # @example Basic usage
17
+ # class MyComponent < Poetry::Core::Component
18
+ # # WrappedHelper is already included via Poetry::Core::Component
19
+ # end
20
+ #
21
+ # # In a view or template
22
+ # component = MyComponent.new(title: "Hello")
23
+ # wrapper = component.wrapped
24
+ #
25
+ # @example Using in a view template with custom wrapper HTML
26
+ # <%# app/components/my_component/component.html.erb %>
27
+ # <% component = MyComponent.new(title: "Hello") %>
28
+ # <%= render component.wrapped do |wrapper| %>
29
+ # <div class="custom-wrapper">
30
+ # <h2>Wrapped Content:</h2>
31
+ # <%= wrapper.component %>
32
+ # </div>
33
+ # <% end %>
34
+ #
35
+ # @example Conditional rendering
36
+ # # The wrapper only renders if the wrapped component's render? returns true
37
+ # class ConditionalComponent < Poetry::Core::Component
38
+ # def render?
39
+ # @show_content
40
+ # end
41
+ # end
42
+ #
43
+ # # If @show_content is false, neither the wrapper nor component will render
44
+ # <%= render component.wrapped do |wrapper| %>
45
+ # <div class="wrapper"><%= wrapper.component %></div>
46
+ # <% end %>
47
+ #
48
+ # @see Poetry::Core::Wrapper::Component
49
+ module WrappedHelper
50
+ # Wraps the current component instance in a {Poetry::Core::Wrapper::Component}.
51
+ #
52
+ # This creates a wrapper that can be rendered with custom HTML surrounding
53
+ # the component. The wrapper respects the wrapped component's `render?`
54
+ # method, only rendering if it returns true.
55
+ #
56
+ # @return [Poetry::Core::Wrapper::Component] a wrapper component containing self
57
+ #
58
+ # @example
59
+ # component = MyComponent.new
60
+ # wrapper = component.wrapped
61
+ # wrapper.component_instance # => the original component
62
+ def wrapped
63
+ Poetry::Core::Wrapper::Component.new(self)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Core
5
+ module CSS
6
+ # The BEM-mode classname merger: the classname_merger a host pairs
7
+ # with `css_mode = :bem`. Same contract as {TailwindMerger}
8
+ # (flatten, stringify, drop blanks, nil for empty input) with
9
+ # exactly two behaviors on top: order-preserving token dedupe and
10
+ # a space join - no Tailwind conflict semantics applied to a BEM
11
+ # host's classes.
12
+ #
13
+ # Deliberately NO modifier-axis conflict resolution: the BEM
14
+ # modifier grammar is dictionary-dependent at the string level
15
+ # (values carry dashes - `--align-inline-start` - and names carry
16
+ # underscores), so a string merger guessing axes would be wrong.
17
+ # Style axes are driven through component options; conflicts
18
+ # between raw caller classes belong to the host stylesheet's
19
+ # cascade.
20
+ #
21
+ # @example Pairing with BEM mode (config/initializers/poetry.rb)
22
+ # Poetry::Core::Config.current.css_mode = :bem
23
+ # Poetry::Core::Config.current.classname_merger =
24
+ # Poetry::Core::CSS::BemMerger.new
25
+ #
26
+ # @example Dedupe without Tailwind semantics
27
+ # Poetry::Core::CSS::BemMerger.new.merge("pill pill--variant-danger", "pill", "p-4 p-2")
28
+ # # => "pill pill--variant-danger p-4 p-2"
29
+ class BemMerger
30
+ # Merges class lists BEM-style: normalize, dedupe at the token
31
+ # level (first occurrence keeps its position), join.
32
+ #
33
+ # @param classes [Array<String, Symbol, Array, nil>] class names,
34
+ # arrays of class names, or nils
35
+ # @return [String, nil] the merged classes, or nil when empty
36
+ def merge(*classes)
37
+ normalized = classes.flatten.compact_blank.map(&:to_s)
38
+ return nil if normalized.empty?
39
+
40
+ normalized.flat_map(&:split).uniq.join(" ")
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Core
5
+ module CSS
6
+ # Generates the reference stylesheet for `css_mode = :bem` from a Style
7
+ # dictionary: every BEM class the component emits, as a documented CSS
8
+ # selector skeleton the consumer fills with their own rules. Each
9
+ # selector carries a `tailwind-equivalent` comment - the utilities the
10
+ # default (Tailwind) theme resolves that token to - so the reference
11
+ # doubles as the class contract's documentation.
12
+ #
13
+ # The header embeds the dictionary's capsule digest as a leak-guard:
14
+ # CSS written against an older dictionary is detectable by digest
15
+ # mismatch instead of silently drifting on upgrade.
16
+ #
17
+ # @example
18
+ # Poetry::Core::CSS::BemReference.new(MyApp::Button::Style).css
19
+ # # => "/* poetry BEM reference for `.button` - capsule 1a2b3c... */\n.button { ... }"
20
+ #
21
+ # @api private
22
+ class BemReference
23
+ def initialize(style_class, block: style_class.bem_block)
24
+ raise ArgumentError, "#{style_class} has no derivable BEM block" unless block
25
+
26
+ @resolver = style_class.resolver
27
+ @block = block
28
+ end
29
+
30
+ def css
31
+ [header, root_rule, element_rules, variant_rules, compound_rules]
32
+ .flatten.compact.join("\n")
33
+ end
34
+
35
+ private
36
+
37
+ def header
38
+ <<~CSS
39
+ /* poetry BEM reference for `.#{@block}` - capsule #{@resolver.digest}
40
+ Generated from the Style dictionary; consumers on css_mode = :bem
41
+ style these selectors with their own CSS. The tailwind-equivalent
42
+ comments show what the default (Tailwind) theme resolves each
43
+ token to. */
44
+ CSS
45
+ end
46
+
47
+ def root_rule
48
+ rule(".#{@block}", @resolver.bases.join(" "))
49
+ end
50
+
51
+ def element_rules
52
+ @resolver.elements.map do |name, classes|
53
+ rule(".#{@block}__#{name}", classes.join(" "))
54
+ end
55
+ end
56
+
57
+ def variant_rules
58
+ @resolver.variants.flat_map do |attr, mapping|
59
+ mapping.map { |value, classes| rule(modifier_selector(attr, value), classes) }
60
+ end.compact
61
+ end
62
+
63
+ def compound_rules
64
+ @resolver.compounds.map do |compound|
65
+ selector = compound.criteria.map { |attr, value| modifier_selector(attr, value) }.join
66
+ rule(selector, compound.classes)
67
+ end
68
+ end
69
+
70
+ # Boolean modifiers are presence classes (`block--attr`); `false` has
71
+ # no class and produces no rule.
72
+ def modifier_selector(attr, value)
73
+ return nil if value == false
74
+
75
+ value == true ? ".#{@block}--#{attr}" : ".#{@block}--#{attr}-#{value}"
76
+ end
77
+
78
+ def rule(selector, classes)
79
+ return nil if selector.nil?
80
+
81
+ body = classes.to_s.strip.empty? ? "/* (no default styles) */" : "/* tailwind-equivalent: #{classes} */"
82
+ "#{selector} { #{body} }"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Core
5
+ module CSS
6
+ # The .cn-* override contract - an intent-vs-accident distinction for
7
+ # poetry's one un-contracted styling surface. Hosts MAY restyle
8
+ # theme-owned cn-* classes from their own CSS (any unlayered rule
9
+ # beats the theme's layer(base)) - but every such override must be
10
+ # DECLARED: a dated, reasoned, scoped entry under `overrides:` in
11
+ # config/poetry_components.yml. Undeclared overrides are drift;
12
+ # declared ones are design intent that travels into DESIGN.md's
13
+ # "Intentional deviations" section.
14
+ #
15
+ # Pure logic: {relative_path => css} sources + raw declaration hashes
16
+ # in, findings out. The poetry:design:overrides task feeds and prints
17
+ # it. Declaration rules: `reason` is required; `cn: "*"` must be
18
+ # file-scoped - a repo-wide blanket cannot happen by accident.
19
+ #
20
+ # @example
21
+ # scan = Poetry::Core::CSS::OverrideScan.new(
22
+ # sources: { "app/assets/site.css" => css },
23
+ # declarations: YAML.load_file("config/poetry_components.yml")["overrides"]
24
+ # )
25
+ # scan.ok? || scan.undeclared # => [["app/assets/site.css", ["cn-button"]]]
26
+ #
27
+ # @api private
28
+ class OverrideScan
29
+ CN_TOKEN = /\.(cn-[a-z0-9-]+)/
30
+ COMMENT = %r{/\*.*?\*/}m
31
+
32
+ Declaration = Struct.new(:cn, :files, :reason, :created, :index, :matched, keyword_init: true) do
33
+ def wildcard? = cn == "*"
34
+
35
+ def covers?(path, cn_class)
36
+ return false unless wildcard? || Array(cn).include?(cn_class)
37
+ return true if files.nil? || files.empty?
38
+
39
+ files.any? { |glob| File.fnmatch(glob, path, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
40
+ end
41
+ end
42
+
43
+ attr_reader :undeclared, :invalid, :stale, :declared_count
44
+
45
+ def initialize(sources:, declarations:)
46
+ @declarations, @invalid = normalize(Array(declarations))
47
+ @undeclared = []
48
+ @declared_count = 0
49
+ scan(sources)
50
+ @stale = @declarations.reject(&:matched)
51
+ end
52
+
53
+ def ok? = @undeclared.empty? && @invalid.empty?
54
+
55
+ # The exact YAML to paste for an undeclared override - a finding
56
+ # ships its own exception command.
57
+ def snippet_for(path, classes)
58
+ cn = classes.size == 1 ? classes.first.inspect : "\"*\""
59
+ <<~YAML
60
+ - cn: #{cn}
61
+ files: ["#{path}"]
62
+ reason: "TODO - why this override is intentional"
63
+ created: #{Time.now.strftime("%Y-%m-%d")}
64
+ YAML
65
+ end
66
+
67
+ private
68
+
69
+ def normalize(raw)
70
+ valid = []
71
+ invalid = []
72
+ raw.each_with_index do |entry, index|
73
+ unless entry.is_a?(Hash)
74
+ invalid << "overrides[#{index}]: not a mapping"
75
+ next
76
+ end
77
+ files = Array(entry["files"]).map(&:to_s)
78
+ declaration = Declaration.new(cn: entry["cn"] || "*", files: files,
79
+ reason: entry["reason"].to_s, created: entry["created"],
80
+ index: index, matched: false)
81
+ if declaration.reason.strip.empty?
82
+ invalid << "overrides[#{index}]: `reason` is required - an override without a why is drift"
83
+ elsif declaration.wildcard? && files.empty?
84
+ invalid << "overrides[#{index}]: `cn: \"*\"` must be file-scoped (add `files:`) - " \
85
+ "a repo-wide blanket cannot happen by accident"
86
+ else
87
+ valid << declaration
88
+ end
89
+ end
90
+ [valid, invalid]
91
+ end
92
+
93
+ def scan(sources)
94
+ sources.each do |path, css|
95
+ classes = css.gsub(COMMENT, "").scan(CN_TOKEN).flatten.uniq.sort
96
+ next if classes.empty?
97
+
98
+ open = classes.reject do |cn_class|
99
+ hit = @declarations.select { |declaration| declaration.covers?(path, cn_class) }
100
+ hit.each { |declaration| declaration.matched = true }
101
+ @declared_count += 1 if hit.any?
102
+ hit.any?
103
+ end
104
+ @undeclared << [path, open] if open.any?
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end