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,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
module Preview
|
|
6
|
+
# Provides functionality to mark preview classes as abstract and exclude them from the preview index.
|
|
7
|
+
# Adapted from an MIT-licensed source (source and license in
|
|
8
|
+
# THIRD_PARTY_NOTICES.md).
|
|
9
|
+
#
|
|
10
|
+
# This module adds an `abstract_class` accessor to preview classes and automatically filters
|
|
11
|
+
# them out when listing all available previews. This is particularly useful for creating base
|
|
12
|
+
# preview classes that provide shared functionality but shouldn't appear in the preview UI.
|
|
13
|
+
#
|
|
14
|
+
# The module works by extending preview classes (typically ViewComponent::Preview) and prepending
|
|
15
|
+
# class methods that add the abstract_class attribute and modify the `all` method to exclude
|
|
16
|
+
# abstract previews from the results.
|
|
17
|
+
#
|
|
18
|
+
# @example Marking a base preview class as abstract
|
|
19
|
+
# class ApplicationPreview < ViewComponent::Preview
|
|
20
|
+
# extend Poetry::Core::Preview::Abstract
|
|
21
|
+
# self.abstract_class = true
|
|
22
|
+
#
|
|
23
|
+
# # Shared helper methods for all previews
|
|
24
|
+
# def render_in_container(&block)
|
|
25
|
+
# content_tag(:div, class: "preview-container", &block)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# class ButtonPreview < ApplicationPreview
|
|
30
|
+
# # This will appear in the preview index
|
|
31
|
+
# def default
|
|
32
|
+
# render ButtonComponent.new
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# ViewComponent::Preview.all
|
|
37
|
+
# # => [ButtonPreview] (ApplicationPreview is excluded)
|
|
38
|
+
#
|
|
39
|
+
# @example Using with Poetry::Core::Preview::Base
|
|
40
|
+
# # Poetry::Core::Preview::Base automatically uses this module
|
|
41
|
+
# class MyBasePreview < Poetry::Core::Preview::Base
|
|
42
|
+
# self.abstract_class = true
|
|
43
|
+
#
|
|
44
|
+
# def helper_method
|
|
45
|
+
# "shared logic"
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# class ConcretePreview < MyBasePreview
|
|
50
|
+
# def example
|
|
51
|
+
# render SomeComponent.new
|
|
52
|
+
# end
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# @example Checking if a preview is abstract
|
|
56
|
+
# ApplicationPreview.abstract_class? # => true
|
|
57
|
+
# ButtonPreview.abstract_class? # => false (or nil)
|
|
58
|
+
#
|
|
59
|
+
# @note This module is automatically applied to ViewComponent::Preview when Poetry::Core::Preview::Base
|
|
60
|
+
# is loaded, so abstract_class functionality is available throughout the preview system.
|
|
61
|
+
#
|
|
62
|
+
module Abstract
|
|
63
|
+
# Hook method called when this module is extended into a class.
|
|
64
|
+
#
|
|
65
|
+
# Prepends ClassMethods to the class's singleton class, making the abstract_class
|
|
66
|
+
# functionality available to the extending class and overriding its `all` method.
|
|
67
|
+
#
|
|
68
|
+
# @param base [Class] the class being extended with this module
|
|
69
|
+
# @return [void]
|
|
70
|
+
# @api private
|
|
71
|
+
def self.extended(base)
|
|
72
|
+
base.singleton_class.prepend(ClassMethods)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Class methods added to preview classes that extend the Abstract module.
|
|
76
|
+
#
|
|
77
|
+
# These methods provide the abstract_class attribute and modify preview collection
|
|
78
|
+
# behavior to automatically filter out abstract classes.
|
|
79
|
+
module ClassMethods
|
|
80
|
+
# @!attribute [rw] abstract_class
|
|
81
|
+
# @return [Boolean, nil] whether this preview class is abstract
|
|
82
|
+
attr_accessor :abstract_class
|
|
83
|
+
|
|
84
|
+
# Checks if this preview class is marked as abstract.
|
|
85
|
+
#
|
|
86
|
+
# @return [Boolean, nil] true if abstract, false or nil otherwise
|
|
87
|
+
# @example
|
|
88
|
+
# MyPreview.abstract_class = true
|
|
89
|
+
# MyPreview.abstract_class? # => true
|
|
90
|
+
alias abstract_class? abstract_class
|
|
91
|
+
|
|
92
|
+
# Returns all non-abstract descendant preview classes.
|
|
93
|
+
#
|
|
94
|
+
# This method overrides the default ViewComponent::Preview.all to exclude
|
|
95
|
+
# any preview classes marked as abstract. It ensures previews are loaded
|
|
96
|
+
# before filtering if no descendants exist yet.
|
|
97
|
+
#
|
|
98
|
+
# @return [Array<Class>] array of non-abstract preview classes
|
|
99
|
+
# @example
|
|
100
|
+
# class BasePreview < ViewComponent::Preview
|
|
101
|
+
# extend Poetry::Core::Preview::Abstract
|
|
102
|
+
# self.abstract_class = true
|
|
103
|
+
# end
|
|
104
|
+
#
|
|
105
|
+
# class ButtonPreview < BasePreview; end
|
|
106
|
+
# class CardPreview < BasePreview; end
|
|
107
|
+
#
|
|
108
|
+
# ViewComponent::Preview.all
|
|
109
|
+
# # => [ButtonPreview, CardPreview] (BasePreview excluded)
|
|
110
|
+
def all
|
|
111
|
+
# Loading delegates to Sidecarable#load_previews when present
|
|
112
|
+
# (poetry's engine wires both onto ViewComponent::Preview);
|
|
113
|
+
# standalone, already-loaded descendants are filtered as-is.
|
|
114
|
+
load_previews if respond_to?(:load_previews) && descendants.reject(&:abstract_class?).empty?
|
|
115
|
+
descendants.reject(&:abstract_class?)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
# The preview stack: base class, template resolution, and gallery glue.
|
|
6
|
+
# Adapted from an MIT-licensed source (source and license in
|
|
7
|
+
# THIRD_PARTY_NOTICES.md).
|
|
8
|
+
module Preview
|
|
9
|
+
# First, enable abstract classes (if not already extended)
|
|
10
|
+
unless ViewComponent::Preview.singleton_class.is_a?(Preview::Abstract)
|
|
11
|
+
ViewComponent::Preview.extend Preview::Abstract
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Enhanced base class for ViewComponent previews with additional features.
|
|
15
|
+
#
|
|
16
|
+
# Poetry::Core::Preview::Base extends the standard ViewComponent::Preview with several enhancements:
|
|
17
|
+
# - Automatic component class inference from preview class name
|
|
18
|
+
# - Template rendering support with custom layouts
|
|
19
|
+
# - Container class configuration with inheritance
|
|
20
|
+
# - Convenient shortcut methods for rendering components
|
|
21
|
+
#
|
|
22
|
+
# Theme and mode switching in Lookbook is a LAYOUT concern, not a
|
|
23
|
+
# preview-class one: the preview layout reads
|
|
24
|
+
# `lookbook_display(:theme)` / `lookbook_display(:mode)` and stamps
|
|
25
|
+
# classes on <html>; components stay theme-agnostic by construction.
|
|
26
|
+
#
|
|
27
|
+
# @example Basic usage
|
|
28
|
+
# class ButtonPreview < Poetry::Core::Preview::Base
|
|
29
|
+
# # Automatically infers Button::Component, ButtonComponent, or Button
|
|
30
|
+
# def default
|
|
31
|
+
# render_component(text: "Click me", variant: :primary)
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# def with_custom_container
|
|
35
|
+
# self.class.container_class = "p-4 bg-gray-100"
|
|
36
|
+
# render_component(text: "Styled container")
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# @example Using render_with for template locals
|
|
41
|
+
# class CardPreview < Poetry::Core::Preview::Base
|
|
42
|
+
# def default
|
|
43
|
+
# render_with(title: "Card Title", description: "Card description")
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# @example Passing a component instance directly
|
|
48
|
+
# class AlertPreview < Poetry::Core::Preview::Base
|
|
49
|
+
# def warning
|
|
50
|
+
# component = AlertComponent.new(type: :warning, dismissible: true)
|
|
51
|
+
# render_component(component)
|
|
52
|
+
# end
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
class Base < ViewComponent::Preview
|
|
56
|
+
self.abstract_class = true
|
|
57
|
+
|
|
58
|
+
include Poetry::Core::Preview::Template
|
|
59
|
+
|
|
60
|
+
# The container class previews render inside when neither the
|
|
61
|
+
# preview class nor an ancestor sets one.
|
|
62
|
+
DEFAULT_CONTAINER_CLASS = ""
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
# Ensures child preview classes inherit layout configuration.
|
|
66
|
+
#
|
|
67
|
+
# @param child [Class] the inheriting preview class
|
|
68
|
+
# @return [void]
|
|
69
|
+
# @api private
|
|
70
|
+
def inherited(child)
|
|
71
|
+
child.layout(@layout) if defined?(@layout)
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
attr_writer :container_class, :component_class_name
|
|
76
|
+
|
|
77
|
+
# Returns the configured container CSS class for the preview.
|
|
78
|
+
#
|
|
79
|
+
# Container classes are inherited from parent preview classes. If not explicitly set,
|
|
80
|
+
# uses the parent's container class or DEFAULT_CONTAINER_CLASS.
|
|
81
|
+
#
|
|
82
|
+
# @return [String] the container CSS class
|
|
83
|
+
# @example
|
|
84
|
+
# class MyPreview < Poetry::Core::Preview::Base
|
|
85
|
+
# self.container_class = "p-8 bg-white"
|
|
86
|
+
# end
|
|
87
|
+
#
|
|
88
|
+
# MyPreview.container_class # => "p-8 bg-white"
|
|
89
|
+
def container_class
|
|
90
|
+
return @container_class if defined?(@container_class)
|
|
91
|
+
|
|
92
|
+
@container_class =
|
|
93
|
+
if superclass.respond_to?(:container_class)
|
|
94
|
+
superclass.container_class
|
|
95
|
+
else
|
|
96
|
+
DEFAULT_CONTAINER_CLASS
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Prepares render arguments by building component instance and setting container class.
|
|
101
|
+
#
|
|
102
|
+
# @return [Hash] render arguments with locals configured
|
|
103
|
+
# @api private
|
|
104
|
+
def render_args(...)
|
|
105
|
+
super.tap do |res|
|
|
106
|
+
res[:locals] ||= {}
|
|
107
|
+
build_component_instance(res[:locals])
|
|
108
|
+
res[:locals][:container_class] ||= container_class
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Infers the component class name from the preview class name.
|
|
113
|
+
#
|
|
114
|
+
# Tries multiple naming conventions in order:
|
|
115
|
+
# - Namespace::ButtonPreview => Namespace::Button::Component
|
|
116
|
+
# - Namespace::ButtonPreview => Namespace::ButtonComponent
|
|
117
|
+
# - Namespace::ButtonPreview => Namespace::Button
|
|
118
|
+
# - Button::Preview => Button::Component | ButtonComponent | Button
|
|
119
|
+
#
|
|
120
|
+
# @return [String, nil] the inferred component class name or nil if not found
|
|
121
|
+
# @example
|
|
122
|
+
# class MyApp::ButtonPreview < Poetry::Core::Preview::Base
|
|
123
|
+
# end
|
|
124
|
+
#
|
|
125
|
+
# MyApp::ButtonPreview.component_class_name
|
|
126
|
+
# # => "MyApp::Button::Component" (if it exists)
|
|
127
|
+
# # or "MyApp::ButtonComponent" (if it exists)
|
|
128
|
+
# # or "MyApp::Button" (if it exists)
|
|
129
|
+
def component_class_name
|
|
130
|
+
@component_class_name ||= begin
|
|
131
|
+
component_name = name.sub(/(::Preview|Preview)$/, "")
|
|
132
|
+
[
|
|
133
|
+
"#{component_name}::Component",
|
|
134
|
+
"#{component_name}Component",
|
|
135
|
+
component_name
|
|
136
|
+
].find(&:safe_constantize)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# Builds a component instance and adds it to locals.
|
|
143
|
+
#
|
|
144
|
+
# If component is already present in locals, returns unchanged.
|
|
145
|
+
# If component class cannot be instantiated, sets error message in locals.
|
|
146
|
+
#
|
|
147
|
+
# @param locals [Hash] the locals hash to modify
|
|
148
|
+
# @return [Hash] the modified locals hash
|
|
149
|
+
# @api private
|
|
150
|
+
def build_component_instance(locals)
|
|
151
|
+
return locals unless locals[:component].nil?
|
|
152
|
+
|
|
153
|
+
locals[:component] = component_class_name.safe_constantize&.new
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
locals[:component] = nil
|
|
156
|
+
locals[:error] = e.message
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Renders a NESTED component inside a slot/content block. Plain
|
|
161
|
+
# render() there resolves to the preview DSL's own render (a
|
|
162
|
+
# declaration returning a hash) and the nested component silently
|
|
163
|
+
# vanishes - button icons, card actions, dialog footer buttons were
|
|
164
|
+
# all missing from every preview until the a11y preview rig caught it.
|
|
165
|
+
#
|
|
166
|
+
# @param component [ViewComponent::Base] the component instance to render
|
|
167
|
+
# @return [String] the rendered HTML
|
|
168
|
+
def embed(component, &)
|
|
169
|
+
ApplicationController.new.view_context.render(component, &)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Renders the preview with custom template locals.
|
|
173
|
+
#
|
|
174
|
+
# This is a convenience method that wraps render_with_template, making it easier
|
|
175
|
+
# to pass locals to your preview templates.
|
|
176
|
+
#
|
|
177
|
+
# @param locals [Hash] key-value pairs to pass as local variables to the template
|
|
178
|
+
# @return [String] the rendered template
|
|
179
|
+
# @example
|
|
180
|
+
# def default
|
|
181
|
+
# render_with(title: "Hello", message: "World")
|
|
182
|
+
# end
|
|
183
|
+
def render_with(**locals)
|
|
184
|
+
render_with_template(locals: locals)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Renders a component instance or creates one from props.
|
|
188
|
+
#
|
|
189
|
+
# This method provides a convenient way to render components in previews. It can accept
|
|
190
|
+
# either a component instance directly, or a hash of props to instantiate the component.
|
|
191
|
+
# The component class is automatically inferred from the preview class name.
|
|
192
|
+
#
|
|
193
|
+
# @param component_or_props [ViewComponent::Base, Hash, nil] either a component instance
|
|
194
|
+
# or a hash of props to pass to the component constructor
|
|
195
|
+
# @param block [Proc, nil] optional block to pass as content to the component
|
|
196
|
+
# @yield the component's content
|
|
197
|
+
# @return [String] the rendered component
|
|
198
|
+
# @example With props hash
|
|
199
|
+
# def default
|
|
200
|
+
# render_component(variant: :primary, text: "Click me")
|
|
201
|
+
# end
|
|
202
|
+
#
|
|
203
|
+
# @example With component instance
|
|
204
|
+
# def custom
|
|
205
|
+
# component = ButtonComponent.new(variant: :primary)
|
|
206
|
+
# render_component(component)
|
|
207
|
+
# end
|
|
208
|
+
#
|
|
209
|
+
# @example With content block
|
|
210
|
+
# def with_content
|
|
211
|
+
# render_component(variant: :primary) do
|
|
212
|
+
# "Custom content"
|
|
213
|
+
# end
|
|
214
|
+
# end
|
|
215
|
+
def render_component(component_or_props = nil, &block)
|
|
216
|
+
component = if component_or_props.is_a?(::ViewComponent::Base)
|
|
217
|
+
component_or_props
|
|
218
|
+
else
|
|
219
|
+
self.class.component_class_name.constantize.new(**(component_or_props || {}))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
render_with(component: component, content_block: block)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
module Preview
|
|
6
|
+
# Provides sidecar file loading and naming utilities for preview classes.
|
|
7
|
+
# Adapted from an MIT-licensed source (source and license in
|
|
8
|
+
# THIRD_PARTY_NOTICES.md).
|
|
9
|
+
#
|
|
10
|
+
# This module adds functionality to automatically load preview files from configured paths
|
|
11
|
+
# and derive component names from preview class names. It's designed to support the "sidecar"
|
|
12
|
+
# pattern where preview files are organized alongside their corresponding components.
|
|
13
|
+
#
|
|
14
|
+
# The module extends preview classes (typically ViewComponent::Preview) and prepends class
|
|
15
|
+
# methods that handle preview file discovery and name extraction, making it easier to organize
|
|
16
|
+
# and load previews in a consistent, convention-based manner.
|
|
17
|
+
#
|
|
18
|
+
# @example Basic usage with ViewComponent::Preview
|
|
19
|
+
# ViewComponent::Preview.extend Poetry::Core::Preview::Sidecarable
|
|
20
|
+
#
|
|
21
|
+
# # Load all preview files from configured paths
|
|
22
|
+
# ViewComponent::Preview.load_previews
|
|
23
|
+
#
|
|
24
|
+
# @example Preview name extraction
|
|
25
|
+
# class MyApp::Button::Preview < ViewComponent::Preview
|
|
26
|
+
# extend Poetry::Core::Preview::Sidecarable
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# MyApp::Button::Preview.preview_name
|
|
30
|
+
# # => "my_app/button"
|
|
31
|
+
#
|
|
32
|
+
# @example Namespace variations
|
|
33
|
+
# class ButtonPreview < ViewComponent::Preview
|
|
34
|
+
# extend Poetry::Core::Preview::Sidecarable
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# ButtonPreview.preview_name # => "button"
|
|
38
|
+
#
|
|
39
|
+
# class Admin::CardPreview < ViewComponent::Preview
|
|
40
|
+
# extend Poetry::Core::Preview::Sidecarable
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# Admin::CardPreview.preview_name # => "admin/card"
|
|
44
|
+
#
|
|
45
|
+
# @note Preview files are discovered using the glob pattern `**/{preview.rb,*_preview.rb}`,
|
|
46
|
+
# which matches both standalone `preview.rb` files and files ending with `_preview.rb`.
|
|
47
|
+
#
|
|
48
|
+
module Sidecarable
|
|
49
|
+
# Glob pattern for discovering preview files.
|
|
50
|
+
#
|
|
51
|
+
# Matches:
|
|
52
|
+
# - `preview.rb` - Standalone preview files
|
|
53
|
+
# - `*_preview.rb` - Named preview files (e.g., `button_preview.rb`)
|
|
54
|
+
#
|
|
55
|
+
# @return [String] the glob pattern for preview file discovery
|
|
56
|
+
PREVIEW_GLOB = "**/{preview.rb,*_preview.rb}"
|
|
57
|
+
|
|
58
|
+
# Hook method called when this module is extended into a class.
|
|
59
|
+
#
|
|
60
|
+
# Prepends ClassMethods to the class's singleton class, making the sidecar loading
|
|
61
|
+
# functionality available to the extending class.
|
|
62
|
+
#
|
|
63
|
+
# @param base [Class] the class being extended with this module
|
|
64
|
+
# @return [void]
|
|
65
|
+
# @api private
|
|
66
|
+
def self.extended(base)
|
|
67
|
+
base.singleton_class.prepend(ClassMethods)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Class methods added to preview classes that extend the Sidecarable module.
|
|
71
|
+
#
|
|
72
|
+
# These methods provide preview file loading and component name extraction functionality.
|
|
73
|
+
module ClassMethods
|
|
74
|
+
# Loads all preview files from configured preview paths.
|
|
75
|
+
#
|
|
76
|
+
# Iterates through each configured preview path and requires all files matching
|
|
77
|
+
# the PREVIEW_GLOB pattern. Files are loaded in sorted order to ensure consistent
|
|
78
|
+
# loading sequence across environments.
|
|
79
|
+
#
|
|
80
|
+
# @return [void]
|
|
81
|
+
# @example Loading previews
|
|
82
|
+
# ViewComponent::Preview.preview_paths = ["app/components"]
|
|
83
|
+
# ViewComponent::Preview.load_previews
|
|
84
|
+
# # Loads: app/components/**/preview.rb
|
|
85
|
+
# # app/components/**/*_preview.rb
|
|
86
|
+
#
|
|
87
|
+
# @example With multiple paths
|
|
88
|
+
# ViewComponent::Preview.preview_paths = [
|
|
89
|
+
# "app/components",
|
|
90
|
+
# "app/views/components"
|
|
91
|
+
# ]
|
|
92
|
+
# ViewComponent::Preview.load_previews
|
|
93
|
+
# # Loads previews from both directories
|
|
94
|
+
#
|
|
95
|
+
# @note Uses `require_dependency` to ensure proper reloading in development mode.
|
|
96
|
+
def load_previews
|
|
97
|
+
Array(preview_paths).each do |preview_path|
|
|
98
|
+
Dir["#{preview_path}/#{PREVIEW_GLOB}"].each { |file| require_dependency file }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Extracts the component name from the preview class name.
|
|
103
|
+
#
|
|
104
|
+
# Removes the "Preview" or "::Preview" suffix from the class name and converts
|
|
105
|
+
# it to underscore/snake_case format. This is useful for deriving file paths,
|
|
106
|
+
# identifiers, and component names from preview class names.
|
|
107
|
+
#
|
|
108
|
+
# @return [String] the underscored component name
|
|
109
|
+
# @example Basic usage
|
|
110
|
+
# class ButtonPreview < ViewComponent::Preview
|
|
111
|
+
# extend Poetry::Core::Preview::Sidecarable
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
# ButtonPreview.preview_name # => "button"
|
|
115
|
+
#
|
|
116
|
+
# @example With namespace
|
|
117
|
+
# class Admin::Dashboard::CardPreview < ViewComponent::Preview
|
|
118
|
+
# extend Poetry::Core::Preview::Sidecarable
|
|
119
|
+
# end
|
|
120
|
+
#
|
|
121
|
+
# Admin::Dashboard::CardPreview.preview_name # => "admin/dashboard/card"
|
|
122
|
+
#
|
|
123
|
+
# @example With trailing Preview namespace
|
|
124
|
+
# class Components::Alert::Preview < ViewComponent::Preview
|
|
125
|
+
# extend Poetry::Core::Preview::Sidecarable
|
|
126
|
+
# end
|
|
127
|
+
#
|
|
128
|
+
# Components::Alert::Preview.preview_name # => "components/alert"
|
|
129
|
+
def preview_name
|
|
130
|
+
name.sub(/(::Preview|Preview)$/, "").underscore
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
module Preview
|
|
6
|
+
# Provides flexible template resolution for ViewComponent previews.
|
|
7
|
+
# Adapted from an MIT-licensed source (source and license in
|
|
8
|
+
# THIRD_PARTY_NOTICES.md).
|
|
9
|
+
#
|
|
10
|
+
# This module extends ViewComponent's preview template system to support multiple
|
|
11
|
+
# template locations and fallback strategies. It allows previews to use:
|
|
12
|
+
# - Example-specific templates (e.g., `previews/default.html.erb`)
|
|
13
|
+
# - Shared preview templates (e.g., `preview.html.erb`)
|
|
14
|
+
# - A configurable default template
|
|
15
|
+
#
|
|
16
|
+
# The template resolution follows this priority order:
|
|
17
|
+
# 1. ViewComponent's standard template location (via super)
|
|
18
|
+
# 2. Example-specific template: `preview_name/previews/example_name.html.*`
|
|
19
|
+
# 3. Preview-level template: `preview_name/preview.html.*`
|
|
20
|
+
# 4. Default template (configurable, defaults to "poetry/core/preview")
|
|
21
|
+
#
|
|
22
|
+
# @example Basic usage with default template
|
|
23
|
+
# class ButtonPreview < Poetry::Core::Preview::Base
|
|
24
|
+
# # Uses the default template (poetry/core/preview)
|
|
25
|
+
# def default
|
|
26
|
+
# render_component(text: "Click me")
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @example Using example-specific template
|
|
31
|
+
# # File: test/components/previews/button_preview.rb
|
|
32
|
+
# class ButtonPreview < Poetry::Core::Preview::Base
|
|
33
|
+
# def complex
|
|
34
|
+
# render_with(button: ButtonComponent.new)
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # Template: test/components/previews/button_preview/previews/complex.html.erb
|
|
39
|
+
# <div class="preview-container">
|
|
40
|
+
# <%= render @button %>
|
|
41
|
+
# <p>Additional markup for this specific preview</p>
|
|
42
|
+
# </div>
|
|
43
|
+
#
|
|
44
|
+
# @example Using shared preview template
|
|
45
|
+
# # File: test/components/previews/card_preview.rb
|
|
46
|
+
# class CardPreview < Poetry::Core::Preview::Base
|
|
47
|
+
# def basic; render_component; end
|
|
48
|
+
# def featured; render_component(featured: true); end
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# # Template: test/components/previews/card_preview/preview.html.erb
|
|
52
|
+
# # This template is shared by all examples in this preview
|
|
53
|
+
# <div class="card-demo-wrapper">
|
|
54
|
+
# <%= render @component %>
|
|
55
|
+
# </div>
|
|
56
|
+
#
|
|
57
|
+
# @example Configuring custom default template
|
|
58
|
+
# class MyBasePreview < Poetry::Core::Preview::Base
|
|
59
|
+
# self.default_preview_template = "my_app/custom_preview"
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# class ButtonPreview < MyBasePreview
|
|
63
|
+
# # Inherits the custom default template
|
|
64
|
+
# def default
|
|
65
|
+
# render_component
|
|
66
|
+
# end
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# @see Poetry::Core::Preview::Base
|
|
70
|
+
module Template
|
|
71
|
+
# The default template path used when no specific template is found.
|
|
72
|
+
#
|
|
73
|
+
# This template is rendered for previews that don't have:
|
|
74
|
+
# - A ViewComponent standard template
|
|
75
|
+
# - An example-specific template
|
|
76
|
+
# - A preview-level shared template
|
|
77
|
+
#
|
|
78
|
+
# @api public
|
|
79
|
+
DEFAULT_TEMPLATE = "poetry/core/preview"
|
|
80
|
+
|
|
81
|
+
# @api private
|
|
82
|
+
def self.included(base)
|
|
83
|
+
base.singleton_class.prepend(ClassMethods)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Class-level methods added to preview classes when Template is included.
|
|
87
|
+
#
|
|
88
|
+
# These methods provide template configuration and resolution logic that
|
|
89
|
+
# operates at the class level, allowing inheritance and customization.
|
|
90
|
+
module ClassMethods
|
|
91
|
+
# @!attribute [w] default_preview_template
|
|
92
|
+
# Sets the default template path for this preview class.
|
|
93
|
+
#
|
|
94
|
+
# @return [String] the configured template path
|
|
95
|
+
#
|
|
96
|
+
# @example
|
|
97
|
+
# class MyPreview < Poetry::Core::Preview::Base
|
|
98
|
+
# self.default_preview_template = "my_app/special_preview"
|
|
99
|
+
# end
|
|
100
|
+
attr_writer :default_preview_template
|
|
101
|
+
|
|
102
|
+
# Returns the default preview template path for this class.
|
|
103
|
+
#
|
|
104
|
+
# The template path is inherited from parent classes. If not explicitly set,
|
|
105
|
+
# it walks up the class hierarchy until it finds a configured template or
|
|
106
|
+
# falls back to {DEFAULT_TEMPLATE}.
|
|
107
|
+
#
|
|
108
|
+
# @return [String] the default template path
|
|
109
|
+
#
|
|
110
|
+
# @example Using default template
|
|
111
|
+
# Poetry::Core::Preview::Base.default_preview_template
|
|
112
|
+
# # => "poetry/core/preview"
|
|
113
|
+
#
|
|
114
|
+
# @example Custom template with inheritance
|
|
115
|
+
# class BasePreview < Poetry::Core::Preview::Base
|
|
116
|
+
# self.default_preview_template = "custom/layout"
|
|
117
|
+
# end
|
|
118
|
+
#
|
|
119
|
+
# class ButtonPreview < BasePreview
|
|
120
|
+
# end
|
|
121
|
+
#
|
|
122
|
+
# ButtonPreview.default_preview_template
|
|
123
|
+
# # => "custom/layout" (inherited from BasePreview)
|
|
124
|
+
def default_preview_template
|
|
125
|
+
return @default_preview_template if defined?(@default_preview_template)
|
|
126
|
+
|
|
127
|
+
@default_preview_template =
|
|
128
|
+
if superclass.respond_to?(:default_preview_template)
|
|
129
|
+
superclass.default_preview_template
|
|
130
|
+
else
|
|
131
|
+
DEFAULT_TEMPLATE
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Resolves the template path for a preview example with multiple fallbacks.
|
|
136
|
+
#
|
|
137
|
+
# This method implements a flexible template resolution strategy that tries
|
|
138
|
+
# multiple locations in order:
|
|
139
|
+
#
|
|
140
|
+
# 1. First calls ViewComponent's default resolution (via super)
|
|
141
|
+
# 2. If not found, looks for example-specific template at:
|
|
142
|
+
# `preview_name/previews/example_name.html.*`
|
|
143
|
+
# 3. If not found, looks for shared preview template at:
|
|
144
|
+
# `preview_name/preview.html.*`
|
|
145
|
+
# 4. Falls back to {#default_preview_template}
|
|
146
|
+
#
|
|
147
|
+
# This fallback behavior allows previews to share templates across examples
|
|
148
|
+
# or use a common default template, reducing duplication.
|
|
149
|
+
#
|
|
150
|
+
# @param example [String] the name of the preview example method
|
|
151
|
+
# @return [String] the resolved template path
|
|
152
|
+
#
|
|
153
|
+
# @example Template resolution for ButtonPreview#primary
|
|
154
|
+
# # Tries in order:
|
|
155
|
+
# # 1. ViewComponent default (e.g., test/components/previews/button_preview/primary.html.erb)
|
|
156
|
+
# # 2. test/components/previews/button_preview/previews/primary.html.erb
|
|
157
|
+
# # 3. test/components/previews/button_preview/preview.html.erb
|
|
158
|
+
# # 4. the default template (poetry/core/preview)
|
|
159
|
+
#
|
|
160
|
+
# @note The method searches across all configured preview_paths
|
|
161
|
+
# @note Supports any template handler (erb, haml, slim, etc.)
|
|
162
|
+
def preview_example_template_path(example)
|
|
163
|
+
super
|
|
164
|
+
rescue ViewComponent::MissingPreviewTemplateError
|
|
165
|
+
# Look for example-specific template: preview_name/previews/example.html.*
|
|
166
|
+
has_example_preview = preview_paths.find do |path|
|
|
167
|
+
Dir.glob(File.join(path, preview_name, "previews", "#{example}.html.*")).any?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
return File.join(preview_name, "previews", example) if has_example_preview
|
|
171
|
+
|
|
172
|
+
# Look for shared preview template: preview_name/preview.html.*
|
|
173
|
+
has_root_preview = preview_paths.find do |path|
|
|
174
|
+
Dir.glob(File.join(path, preview_name, "preview.html.*")).any?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
return File.join(preview_name, "preview") if has_root_preview
|
|
178
|
+
|
|
179
|
+
# Fall back to configured default template
|
|
180
|
+
default_preview_template
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|