poetry-core 0.1.1 → 0.1.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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +0 -4
- data/app/components/poetry/core/component.rb +152 -8
- data/app/components/poetry/core/concerns/options.rb +1 -0
- data/app/components/poetry/core/concerns/styles.rb +46 -5
- data/app/components/poetry/core/style.rb +32 -1
- data/config/component_registry.yml +2 -0
- data/lib/poetry/core/check/stable_identity.rb +14 -1
- data/lib/poetry/core/check.rb +156 -32
- data/lib/poetry/core/config.rb +12 -5
- data/lib/poetry/core/css/modes.rb +127 -0
- data/lib/poetry/core/css/override_scan.rb +17 -2
- data/lib/poetry/core/css/resolver.rb +21 -4
- data/lib/poetry/core/css/token_collisions.rb +184 -0
- data/lib/poetry/core/engine.rb +22 -0
- data/lib/poetry/core/host_components.rb +159 -0
- data/lib/poetry/core/host_helpers.rb +110 -0
- data/lib/poetry/core/html/attributes.rb +33 -2
- data/lib/poetry/core/llms_text.rb +39 -7
- data/lib/poetry/core/registry.rb +154 -5
- data/lib/poetry/core/skill_text.rb +27 -1
- data/lib/poetry/core/stimulus/declarations.rb +8 -3
- data/lib/poetry/core/stimulus/host_manifest.rb +556 -0
- data/lib/poetry/core/stimulus/manifest.rb +24 -0
- data/lib/poetry/core/tokens/generator.rb +10 -1
- data/lib/poetry/core/version.rb +1 -1
- data/tokens/tailwind-theme.css +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68f26084516bebd43571b5f687d9fb07cce952f00aba8e41448715b66b996fba
|
|
4
|
+
data.tar.gz: d1ec44154a85d4c2b4fda384432f2e73349973dfc21862aeddf06bde0d09303b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65965b0865ade83c8b778c3ba773c6d1272bd4da2139e68cd8ab5b798903d12e3224d75df0f77f849cd72403e782d5da87ba470a4597d8654c7ab72a6bdc90ea
|
|
7
|
+
data.tar.gz: e3b822469b4ed1253193dd331b9b43b7923aa5626265ec9f6d1cdc6c4112a7306eb93e37f3a7670b9985da406ef804b5eb4b5174b03263004eced90e306e03c3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.2] - 2026-09-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `helper :name` on a component: an application's (or an engine's) own component written on the DSL names its view helper. The engine defines that helper on Action View at boot and on every reload, and `Poetry::Core::HostComponents` discovers the app's components by convention (under `app/`, outside the `poetry/` namespace, published), builds their registry live, and reads their declared helpers boot-free from source. The registry entry carries `helper`; `poetry check` lints a declared helper under its own name (options, variants, arity, stable identity); llms.txt and the generated skill take an app registry and render an App components section and a `references/app.md`.
|
|
8
|
+
- The runtime values tier. A component built with an off-list style value or a missing required option raises at construction in development and test, naming the allowed values with a did-you-mean, and logs in production while rendering as before (the passthrough guard's posture, at the same seam, so it holds whatever a component does in `before_render`). Only the DSL's own validations are enforced; a component's custom validators keep their own policy. An omitted style is nil, not an off-list value (`allow_nil` on the inclusion validations). The check is a direct lookup over the declared vocabulary (`style` variants, `required`, and `validates ... inclusion: { in: [...] }` on a declared attribute), not an ActiveModel validation pass.
|
|
9
|
+
- Render cost. Measured on 200 Buttons in one view context, a Button went from 0.122 ms to 0.089 ms: the values tier is a direct lookup instead of a validation pass, construction converts the caller's attributes once, and the Style resolver memoises its dictionary walk per variant tuple (bounded by the variant space, cleared when the dictionary changes). The remaining time is ViewComponent's own render (about forty percent) and attribute assignment (a quarter).
|
|
10
|
+
- The class-name merger follows the kit's CSS mode (`Poetry::Core::CSS::Modes.merger_for`): a `:bem` kit merges with a `BemMerger`, a `:tailwind` kit with a `TailwindMerger`. The global `classname_merger` is honoured whenever it is of the right kind, so a customised Tailwind merger still reaches poetry-ui and a host's BEM merger still reaches its BEM kit; a mismatch falls back to the stock merger of the mode. A host that set the global mode to `:bem` without a merger now gets the BEM merger on its kit, the pairing the guide always asked for. Attribute objects keep their assigned merger across copies.
|
|
11
|
+
- `css_mode` is decided per kit. A component class declares it (`css_mode :bem`, inherited), or a kit pins its namespace (`Poetry::Core::CSS::Modes.pin("Acme::Ui", :bem)`); the global `Config.current.css_mode` is the default for kits that declare nothing, and a `css_mode:` keyword on a `css` call still wins over all of them. poetry-ui and poetry-charts pin `:tailwind`, so a host's global never reaches them and a BEM kit on the DSL renders beside them in one app. Before, the global was the only switch and setting it to `:bem` silently stripped poetry-ui's styling.
|
|
12
|
+
- `OverrideScan` takes `owned:`, the theme-owned `cn-*` names; only a rule against one of those is an override. A host's own `cn-*` classes (a kit's dictionary, a page helper) are never findings. Nil keeps the old meaning (every `cn-*` name).
|
|
13
|
+
- The host controllers manifest. `Poetry::Core::Stimulus::HostManifest` reads `app/javascript/controllers` (static targets, values in both forms, classes, `static events`, the class body's own methods) and writes `config/controllers_manifest.json`, the file a gem commits; the engine registers every loaded engine's and the app's manifest by convention before components load. A host controller then validates like poetry's: `use_stimulus` by Symbol at class load, template wiring in `poetry check` (the value-attribute rule now covers any registered controller), and its API in the registry's controllers section. The reader masks comments and string, template and regex literals before it reads structure, so a comment inside `static values`, a brace in a string, a one-line method body or an arrow-function class field all read correctly; it scrubs stray bytes and a BOM. An entry is complete or absent: a parent that is another host controller (relative import) or a poetry controller (`@poetry/controllers`, file or barrel import; charts and agent by their packages) merges child-first; an unresolvable parent, a computed static, a getter or a spread yields no entry and a named skip. A hand-written entry for a skipped identifier survives regeneration and never reads as stale (the gate compares only the controllers the reader read). Events are validated only from a literal `static events`; a controller that only calls `dispatch()` keeps them unknown, never empty. A Symbol declaration resolves to a gem's namespaced controller before a host controller of the same bare name, so a host `tabs_controller.js` never hijacks `controller :tabs`; the unknown-Symbol message names `bin/rails poetry:stimulus:manifest`. `Manifest.register_roots`, `Manifest.forget`; `Registry.gem_roots(registry: false)` lists every bundled root for the manifest walk.
|
|
14
|
+
- Registry roots by convention. `Poetry::Core::Registry.roots` (booted: every loaded engine whose root carries a published registry, then the app when it committed one), `.gem_roots` (boot-free, from the bundle, plus an app root), and `.merged` (one view over several roots, block templates resolved to absolute paths). A registry can mark itself `internal: true` and consumers skip it; poetry-core's own is now marked, since its building blocks have no helpers. `HostComponents.committed_state` reports the app's committed registry as missing, fresh, or stale against the live build.
|
|
15
|
+
- `Poetry::Core::CSS::TokenCollisions`: scans a host app's stylesheets for declarations of Poetry's token names (`--primary`, `--accent`, `--radius`, ...) and Tailwind theme keys (`--color-*`, `--radius-*`), and reports each with its location and what the role paints in Poetry's components. `poetry:install` and `poetry:check` run it.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- A namespace pin follows the dictionary: a component's mode is its own declaration, else the pin of the root-most pinned ancestor (`Modes.inherited_for`), else the global, so a host subclass of a poetry-ui component stays Tailwind inside a BEM-pinned namespace while a host component written on the DSL takes the pin.
|
|
20
|
+
- A dictionary merges by its own mode at every level: the resolver takes its class merger from the Style's sidecar component (its declaration, its kit's pin, or the global), so a host's global BEM merger never reaches a Tailwind kit's element-level joins, not only its root. A style redeclared without variants is open, whatever the parent listed: an inherited inclusion validator no longer narrows it. The "Style class not found" debug line is logged once per class.
|
|
21
|
+
|
|
22
|
+
- `poetry check` validates every descriptor of a `data-action`, not only the last (a bogus first descriptor was invisible); a `data-<controller>-<name>-value` or `-target` attribute resolves to the controller the element declares when several registered identifiers fit (`foo` and `foo-bar`), a plain `data-foo-value` is the host's own attribute, a host controller's targets validate like poetry's, and an attribute whose value carries ERB skips the wiring rules instead of type-checking its fragments. A declared app helper matches bare calls only (`u.badge` is somebody's method); the stable-identity pass agrees. A file the runner cannot read or decode is one `unreadable` warning naming it, never the end of the sweep. `Check.mail_template?(path, root:)` judges the path inside the app root (an app checked out under a folder called `acme_mailer` keeps its pages) and knows Devise's `devise/mailer/`.
|
|
23
|
+
|
|
24
|
+
- Every registry names every helper. A gem entry carries its `poetry_` convention name and an app entry the name it declared with `helper :name`; `Registry.helper_for(path, entry)` is the one resolver every surface reads (llms.txt, the check, the skill, the MCP server), so an app component that declared no helper is listed by class and no surface invents a `poetry_` name for it. The committed gem registries regenerate with the added key and nothing else.
|
|
25
|
+
- Host helpers are checked before any is defined: a clash (a name Action View, poetry-ui, or the app's own `ApplicationController` helpers already carry, private methods included) or two components declaring one name raises naming both sides, leaves the module untouched, and the next reload after the fix starts clean. `Registry.read_file` is the one validated reader of a committed registry file; an empty, malformed, or wrong-shaped file reads as no registry (`Registry.invalid_at?`) instead of raising in whichever surface opened it first. The boot-free `HostComponents.declared_helpers` parses component sources with Prism, so a mention in a string, heredoc, or comment is not a declaration and `helper(:x)`, `helper "x"`, and `self.helper :x` are. `poetry:registry` leaves an engine's own components to that engine's registry.
|
|
26
|
+
|
|
27
|
+
- `poetry check` leaves a mailer template's colors alone: under a `*_mailer/` directory or in the mailer layout, an inline hex or a color literal in a utility class is the only paint an email client honours (no stylesheet, no tokens), so the raw-color rule stays quiet there while every other rule runs as on a page. `Check.lint` takes `mail:` for a source string; the runner derives it from the path (`Check.mail_template?`).
|
|
28
|
+
|
|
29
|
+
- The generated Tailwind theme mapping is `@theme inline default`. A host `@theme` value for the same key (its own `--color-primary`, a `--radius-sm` it set before Poetry arrived) wins whether it is declared before or after the mapping; Poetry's value still applies wherever the host set nothing. Before, the mapping replaced the host's keys wholesale, so every `rounded-sm` in an existing app changed size.
|
|
30
|
+
|
|
3
31
|
## [0.1.1] - 2026-09-08
|
|
4
32
|
|
|
5
33
|
### Added
|
data/README.md
CHANGED
|
@@ -20,10 +20,6 @@ After checking out the repo, run `bin/setup` to install dependencies, then `bund
|
|
|
20
20
|
|
|
21
21
|
Releases publish to [RubyGems.org](https://rubygems.org) via GitHub Actions OIDC **trusted publishing** (no API keys). Run `bundle exec rake "version:bump[X.Y.Z]"` (sets `Poetry::Core::VERSION` and package.json together; `rake version:verify` in the default chain fails on drift), commit, then push a `vX.Y.Z` tag — the `Release` workflow checks the tag against the version and publishes the gem.
|
|
22
22
|
|
|
23
|
-
## Lineage
|
|
24
|
-
|
|
25
|
-
poetry-core evolves the framework layer of `view_component_plus` (the `Plus` gem) — itself a synthesis of view_component-contrib and fox_tail.
|
|
26
|
-
|
|
27
23
|
## License
|
|
28
24
|
|
|
29
25
|
Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -54,6 +54,14 @@ module Poetry
|
|
|
54
54
|
# a subclass of an internal component stays internal.
|
|
55
55
|
class_attribute :internal_component, default: false, instance_predicate: false
|
|
56
56
|
|
|
57
|
+
# The declared vocabulary the runtime values tier checks:
|
|
58
|
+
# name => { variants: [...] or nil, required: true/false }, recorded
|
|
59
|
+
# by `style` and `option` at declaration (copy-on-write per class, so
|
|
60
|
+
# a subclass's declarations never leak up). A direct lookup, not an
|
|
61
|
+
# ActiveModel validation pass - measured at nine percent of a
|
|
62
|
+
# Button's render when it ran through `valid?`.
|
|
63
|
+
class_attribute :declared_values, instance_accessor: false, default: {}.freeze
|
|
64
|
+
|
|
57
65
|
class << self
|
|
58
66
|
# Marks this class (and its descendants) as an implementation
|
|
59
67
|
# detail - full machinery, no registry entry.
|
|
@@ -66,6 +74,86 @@ module Poetry
|
|
|
66
74
|
def internal_component!
|
|
67
75
|
self.internal_component = true
|
|
68
76
|
end
|
|
77
|
+
|
|
78
|
+
# The vocabulary the runtime values tier checks, computed once per
|
|
79
|
+
# class at first construction (the class body is complete by then):
|
|
80
|
+
# the recorded `style`/`option` declarations plus every inclusion
|
|
81
|
+
# validator with an Array (`validates :side, inclusion: { in: SIDES }`
|
|
82
|
+
# is how options declare a closed vocabulary). Method validators are
|
|
83
|
+
# not vocabulary and stay the component's own (Icon's unknown-name
|
|
84
|
+
# policy).
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash{Symbol => Hash}]
|
|
87
|
+
# @api private
|
|
88
|
+
def runtime_vocabulary
|
|
89
|
+
@runtime_vocabulary ||= begin
|
|
90
|
+
vocabulary = declared_values.dup
|
|
91
|
+
names = attribute_names.map(&:to_sym)
|
|
92
|
+
validators.each do |validator|
|
|
93
|
+
next unless validator.is_a?(ActiveModel::Validations::InclusionValidator)
|
|
94
|
+
next unless (allowed = validator.options[:in]).is_a?(Array)
|
|
95
|
+
|
|
96
|
+
validator.attributes.each do |attribute|
|
|
97
|
+
attribute = attribute.to_sym
|
|
98
|
+
next unless names.include?(attribute)
|
|
99
|
+
|
|
100
|
+
spec = vocabulary[attribute] || { variants: nil, required: false }
|
|
101
|
+
# A style declared open (no variants) stays open: an inherited
|
|
102
|
+
# inclusion validator from a parent's closed declaration never
|
|
103
|
+
# narrows it again.
|
|
104
|
+
next if spec[:open]
|
|
105
|
+
|
|
106
|
+
vocabulary[attribute] = spec.merge(variants: allowed).freeze unless spec[:variants]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
vocabulary.freeze
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Records a declared attribute's vocabulary for the runtime values
|
|
114
|
+
# tier ({#guard_declared_values}).
|
|
115
|
+
#
|
|
116
|
+
# @param name [Symbol]
|
|
117
|
+
# @param variants [Array, nil] the closed vocabulary, nil for open values
|
|
118
|
+
# @param required [Boolean]
|
|
119
|
+
# @return [void]
|
|
120
|
+
# @api private
|
|
121
|
+
def record_declared_value(name, variants:, required:, open: false)
|
|
122
|
+
spec = { variants: variants, required: required, open: open }.freeze
|
|
123
|
+
self.declared_values = declared_values.merge(name.to_sym => spec).freeze
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Names the view helper that renders this component - for a
|
|
127
|
+
# component the host application (or an engine) defines on the
|
|
128
|
+
# DSL. The poetry-core engine defines the method on Action View at
|
|
129
|
+
# boot and on every reload, and the app's registry carries the name
|
|
130
|
+
# so `poetry check`, llms.txt and the generated skill know the
|
|
131
|
+
# helper. Own-class only: a subclass declares its own or has none.
|
|
132
|
+
# The gems' components do not declare one; their helpers follow the
|
|
133
|
+
# `poetry_<name>` convention.
|
|
134
|
+
#
|
|
135
|
+
# @example
|
|
136
|
+
# class Demo::Badge::Component < Poetry::Core::Component
|
|
137
|
+
# helper :demo_badge
|
|
138
|
+
# end
|
|
139
|
+
# # <%= demo_badge(tone: :loud) { "New" } %>
|
|
140
|
+
# @param name [Symbol, String] a Ruby method name
|
|
141
|
+
# @return [void]
|
|
142
|
+
# @raise [ArgumentError] when the name is not a plain method name
|
|
143
|
+
def helper(name)
|
|
144
|
+
name = name.to_s
|
|
145
|
+
unless name.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
146
|
+
raise ArgumentError, "helper name #{name.inspect} must be a plain method name (a-z, 0-9, _)"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
@helper_name = name
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The declared helper name, or nil (the gems' components; a
|
|
153
|
+
# subclass that declares none).
|
|
154
|
+
#
|
|
155
|
+
# @return [String, nil]
|
|
156
|
+
attr_reader :helper_name
|
|
69
157
|
end
|
|
70
158
|
|
|
71
159
|
class << self
|
|
@@ -279,10 +367,14 @@ module Poetry
|
|
|
279
367
|
end
|
|
280
368
|
|
|
281
369
|
@attributes = self.class._default_attributes.deep_dup
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
370
|
+
given = attributes.with_indifferent_access
|
|
371
|
+
names = attribute_names
|
|
372
|
+
@html_attributes = Poetry::Core::HTML::Attributes.new(guard_passthrough(given.except(*names)))
|
|
373
|
+
# The root merges classes with the kit's merger (its CSS mode's).
|
|
374
|
+
@html_attributes.classname_merger = classname_merger
|
|
375
|
+
|
|
376
|
+
assign_attributes given.slice(*names)
|
|
377
|
+
guard_declared_values
|
|
286
378
|
end
|
|
287
379
|
|
|
288
380
|
# The passthrough contract, enforced at the seam. A keyword that is not
|
|
@@ -343,12 +435,55 @@ module Poetry
|
|
|
343
435
|
self.class.name ? self.class.component_title : "this component"
|
|
344
436
|
end
|
|
345
437
|
|
|
438
|
+
# The runtime values tier, beside the passthrough guard: the declared
|
|
439
|
+
# vocabulary (`style ... variants:`, `required:`) is checked at
|
|
440
|
+
# construction, where every component - gem or app, whatever it does
|
|
441
|
+
# in before_render - passes. Off-list values raise in development and
|
|
442
|
+
# test with the allowed values and a did-you-mean; in production they
|
|
443
|
+
# log and render as before. `poetry check` catches the same mistakes
|
|
444
|
+
# in templates statically; this tier catches the value that arrives
|
|
445
|
+
# from data. A direct lookup over {.declared_values}; a component's
|
|
446
|
+
# custom validators keep their own policy.
|
|
447
|
+
def guard_declared_values
|
|
448
|
+
problems = nil
|
|
449
|
+
self.class.runtime_vocabulary.each do |name, spec|
|
|
450
|
+
value = public_send(name)
|
|
451
|
+
if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
452
|
+
(problems ||= []) << "#{passthrough_owner} requires #{name}:" if spec[:required]
|
|
453
|
+
elsif spec[:variants] && !spec[:variants].include?(value)
|
|
454
|
+
(problems ||= []) << off_list_problem(name, value, spec[:variants])
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
return unless problems
|
|
458
|
+
|
|
459
|
+
message = problems.join("; ")
|
|
460
|
+
raise ArgumentError, message if strict_passthrough?
|
|
461
|
+
|
|
462
|
+
Rails.logger&.warn("poetry: #{message}") if defined?(Rails) && Rails.respond_to?(:logger)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# The message a developer reads for an off-list value: the declared
|
|
466
|
+
# vocabulary and a did-you-mean.
|
|
467
|
+
def off_list_problem(name, value, allowed)
|
|
468
|
+
"#{passthrough_owner} #{name}: #{value.inspect} is not one of " \
|
|
469
|
+
"#{allowed.map(&:inspect).join(", ")}#{variant_suggestion(value, allowed)}"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def variant_suggestion(value, allowed)
|
|
473
|
+
return "" if allowed.empty? || value.nil?
|
|
474
|
+
|
|
475
|
+
require "did_you_mean"
|
|
476
|
+
suggestion = DidYouMean::SpellChecker.new(dictionary: allowed.map(&:to_s)).correct(value.to_s).first
|
|
477
|
+
suggestion ? " (did you mean #{suggestion.to_sym.inspect}?)" : ""
|
|
478
|
+
end
|
|
479
|
+
|
|
346
480
|
# Raise (development, test) or log (everything else).
|
|
347
481
|
def strict_passthrough?
|
|
348
482
|
defined?(Rails) && Rails.respond_to?(:env) && Rails.env.local?
|
|
349
483
|
end
|
|
350
484
|
|
|
351
|
-
private :guard_passthrough, :option_suggestion, :passthrough_owner, :strict_passthrough
|
|
485
|
+
private :guard_passthrough, :option_suggestion, :passthrough_owner, :strict_passthrough?,
|
|
486
|
+
:guard_declared_values, :off_list_problem, :variant_suggestion
|
|
352
487
|
|
|
353
488
|
# Returns all component attributes, ensuring proc defaults are evaluated.
|
|
354
489
|
#
|
|
@@ -421,15 +556,24 @@ module Poetry
|
|
|
421
556
|
|
|
422
557
|
# Merges multiple class name values into a single string.
|
|
423
558
|
#
|
|
424
|
-
# Uses the
|
|
425
|
-
#
|
|
559
|
+
# Uses the kit's classname merger (Tailwind Merge in :tailwind mode,
|
|
560
|
+
# the BEM merger in :bem mode - {Poetry::Core::CSS::Modes.merger_for})
|
|
561
|
+
# to combine CSS class names, handling conflicts and duplicates.
|
|
426
562
|
#
|
|
427
563
|
# @param classnames [Array<String, nil>] class names to merge
|
|
428
564
|
# @return [String] the merged class names
|
|
429
565
|
# @example
|
|
430
566
|
# classnames("text-red-500", "text-blue-500") # => "text-blue-500"
|
|
431
567
|
def classnames(*classnames)
|
|
432
|
-
|
|
568
|
+
classname_merger.merge(*classnames)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# The class-name merger this component merges with: the one that
|
|
572
|
+
# matches its CSS mode ({Poetry::Core::CSS::Modes.merger_for}).
|
|
573
|
+
#
|
|
574
|
+
# @return [#merge]
|
|
575
|
+
def classname_merger
|
|
576
|
+
Poetry::Core::CSS::Modes.merger_for(css_mode)
|
|
433
577
|
end
|
|
434
578
|
|
|
435
579
|
# HTML-safe JSON for embedding in a `<script type="application/json">`
|
|
@@ -195,6 +195,7 @@ module Poetry
|
|
|
195
195
|
# @param _type [Symbol] the attribute type (unused; kept for signature parity)
|
|
196
196
|
# @param required [Boolean] whether the attribute is required
|
|
197
197
|
def add_option_validations(name, _type, required)
|
|
198
|
+
record_declared_value(name, variants: nil, required: required)
|
|
198
199
|
# Type validation is automatic via ActiveModel::Type
|
|
199
200
|
validates name, presence: true if required
|
|
200
201
|
end
|
|
@@ -53,6 +53,8 @@ module Poetry
|
|
|
53
53
|
STYLE_CLASS_SUFFIX = "::Style"
|
|
54
54
|
|
|
55
55
|
included do
|
|
56
|
+
# The kit-level mode declaration (`css_mode :bem`), inherited.
|
|
57
|
+
class_attribute :declared_css_mode, instance_accessor: false, default: nil
|
|
56
58
|
class_attribute :registered_styles,
|
|
57
59
|
instance_writer: true,
|
|
58
60
|
instance_predicate: false,
|
|
@@ -60,6 +62,28 @@ module Poetry
|
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
class_methods do
|
|
65
|
+
# The CSS mode this component renders in, declared per kit
|
|
66
|
+
# (inherited by subclasses): `css_mode :bem` on a kit's base
|
|
67
|
+
# class, and every component in it emits the BEM token IR. With
|
|
68
|
+
# no argument, the resolved mode: the declaration, else the mode
|
|
69
|
+
# pinned for the namespace ({Poetry::Core::CSS::Modes} - poetry-ui
|
|
70
|
+
# pins `:tailwind`), else the global
|
|
71
|
+
# `Poetry::Core::Config.current.css_mode`. A `css_mode:` keyword
|
|
72
|
+
# on a `css` call still wins over all of them.
|
|
73
|
+
#
|
|
74
|
+
# @example A kit that brings its own CSS
|
|
75
|
+
# class Acme::Ui::Base < Poetry::Core::Component
|
|
76
|
+
# css_mode :bem
|
|
77
|
+
# end
|
|
78
|
+
# @param mode [Symbol, nil] :tailwind or :bem to declare; nil to read
|
|
79
|
+
# @return [Symbol] the declared or resolved mode
|
|
80
|
+
# @raise [Poetry::Core::Error] for an unknown mode
|
|
81
|
+
def css_mode(mode = nil)
|
|
82
|
+
return self.declared_css_mode = Poetry::Core::CSS::Modes.validate!(mode) unless mode.nil?
|
|
83
|
+
|
|
84
|
+
declared_css_mode || Poetry::Core::CSS::Modes.inherited_for(self) || Poetry::Core::Config.current.css_mode
|
|
85
|
+
end
|
|
86
|
+
|
|
63
87
|
# Defines a style attribute for the component.
|
|
64
88
|
#
|
|
65
89
|
# @param name [Symbol, String] the name of the style attribute
|
|
@@ -145,7 +169,12 @@ module Poetry
|
|
|
145
169
|
style_class_name = component_module + STYLE_CLASS_SUFFIX
|
|
146
170
|
style_class_name.constantize
|
|
147
171
|
rescue NameError => e
|
|
148
|
-
|
|
172
|
+
# Once per class: a component with no dictionary (Icon) is a
|
|
173
|
+
# design, not a line per render in the development log.
|
|
174
|
+
unless @style_class_miss_logged
|
|
175
|
+
@style_class_miss_logged = true
|
|
176
|
+
log_style_class_not_found(style_class_name, e) if defined?(Rails)
|
|
177
|
+
end
|
|
149
178
|
nil
|
|
150
179
|
end
|
|
151
180
|
|
|
@@ -177,10 +206,14 @@ module Poetry
|
|
|
177
206
|
# @param variants [Object] the allowed variants
|
|
178
207
|
# @param required [Boolean] whether the attribute is required
|
|
179
208
|
def add_style_validations(name, type, variants, required)
|
|
209
|
+
record_declared_value(name, variants: type == :boolean ? nil : variants, required: required,
|
|
210
|
+
open: type != :boolean && variants.nil?)
|
|
211
|
+
# nil is an omitted value, not an off-list one; `required` adds
|
|
212
|
+
# presence for the styles that may not be omitted.
|
|
180
213
|
if type == :boolean
|
|
181
|
-
validates name, inclusion: { in: [true, false] }
|
|
214
|
+
validates name, inclusion: { in: [true, false] }, allow_nil: true
|
|
182
215
|
elsif variants
|
|
183
|
-
validates name, inclusion: { in: variants }
|
|
216
|
+
validates name, inclusion: { in: variants }, allow_nil: true
|
|
184
217
|
end
|
|
185
218
|
validates name, presence: true if required
|
|
186
219
|
end
|
|
@@ -274,7 +307,7 @@ module Poetry
|
|
|
274
307
|
# @raise [Poetry::Core::Error] for a css_mode other than :tailwind
|
|
275
308
|
# or :bem
|
|
276
309
|
def css(element = nil, **options, &)
|
|
277
|
-
mode = options.delete(:css_mode) ||
|
|
310
|
+
mode = options.delete(:css_mode) || css_mode
|
|
278
311
|
|
|
279
312
|
case mode
|
|
280
313
|
when :tailwind
|
|
@@ -288,10 +321,18 @@ module Poetry
|
|
|
288
321
|
extra = options.delete(:class)
|
|
289
322
|
[bem(element, **options), extra].compact.join(" ")
|
|
290
323
|
else
|
|
291
|
-
|
|
324
|
+
Poetry::Core::CSS::Modes.validate!(mode)
|
|
292
325
|
end
|
|
293
326
|
end
|
|
294
327
|
|
|
328
|
+
# The CSS mode this instance renders in (the class's resolved mode;
|
|
329
|
+
# see the class-level `css_mode`).
|
|
330
|
+
#
|
|
331
|
+
# @return [Symbol] :tailwind or :bem
|
|
332
|
+
def css_mode
|
|
333
|
+
self.class.css_mode
|
|
334
|
+
end
|
|
335
|
+
|
|
295
336
|
# The component's BEM block name - the stable, framework-agnostic
|
|
296
337
|
# class contract of the token IR ("poetry/core/dot" -> "poetry-core-dot").
|
|
297
338
|
#
|
|
@@ -27,7 +27,38 @@ module Poetry
|
|
|
27
27
|
#
|
|
28
28
|
# @return [CSS::Resolver]
|
|
29
29
|
def resolver
|
|
30
|
-
@resolver ||=
|
|
30
|
+
@resolver ||= begin
|
|
31
|
+
dictionary = superclass.respond_to?(:resolver) ? superclass.resolver.dup : CSS::Resolver.new
|
|
32
|
+
dictionary.owner = self
|
|
33
|
+
dictionary
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The class merger for this dictionary's mode: the sidecar
|
|
38
|
+
# component's resolved `css_mode` (its own declaration, its kit's
|
|
39
|
+
# pin, or the global), else this class's own namespace pin, else the
|
|
40
|
+
# global mode - so `Acme::Pill::Style` under a `:bem` pin merges like
|
|
41
|
+
# BEM while poetry-ui's dictionaries merge like Tailwind whatever the
|
|
42
|
+
# host set globally.
|
|
43
|
+
#
|
|
44
|
+
# @return [Object] a class-name merger
|
|
45
|
+
def merger
|
|
46
|
+
component = sidecar_component
|
|
47
|
+
mode = if component.respond_to?(:css_mode)
|
|
48
|
+
component.css_mode
|
|
49
|
+
else
|
|
50
|
+
CSS::Modes.inherited_for(self) || Poetry::Core::Config.current.css_mode
|
|
51
|
+
end
|
|
52
|
+
CSS::Modes.merger_for(mode)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The `X::Component` beside this `X::Style`, when it exists.
|
|
56
|
+
def sidecar_component
|
|
57
|
+
return nil unless name&.end_with?("::Style")
|
|
58
|
+
|
|
59
|
+
Object.const_get("#{name.delete_suffix("::Style")}::Component")
|
|
60
|
+
rescue NameError
|
|
61
|
+
nil
|
|
31
62
|
end
|
|
32
63
|
|
|
33
64
|
# -- The dictionary DSL (delegates to the resolver) --------------------
|
|
@@ -14,7 +14,9 @@ components:
|
|
|
14
14
|
description: The element to render ('div', 'section', 'span', ...); void elements
|
|
15
15
|
(br, hr, img, ...) self-close and take no content block.
|
|
16
16
|
slots: []
|
|
17
|
+
helper: poetry_box
|
|
17
18
|
identity: false
|
|
18
19
|
parts:
|
|
19
20
|
- name: box
|
|
20
21
|
description: The rendered element itself - the whole component is one part
|
|
22
|
+
internal: true
|
|
@@ -68,6 +68,19 @@ module Poetry
|
|
|
68
68
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
|
+
# The gem convention plus every helper the catalog knows by a name
|
|
72
|
+
# of its own (an app component's declared helper), so the
|
|
73
|
+
# heuristics cover app components too.
|
|
74
|
+
def helper_pattern
|
|
75
|
+
@helper_pattern ||= begin
|
|
76
|
+
declared = @catalog.helper_names.reject { |name| name.start_with?("poetry_") }
|
|
77
|
+
# A bare call: not after a dot or a colon (`u.badge`, `Foo::badge`).
|
|
78
|
+
Regexp.union(/(?<![\w.:])(poetry_[a-z0-9_]+)\b/, *declared.map do |name|
|
|
79
|
+
/(?<![\w.:])(#{Regexp.escape(name)})\b/
|
|
80
|
+
end)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
71
84
|
def frame_for(code)
|
|
72
85
|
return :cache if code.match?(CACHE_OPENER)
|
|
73
86
|
return :loop if code.match?(LOOP_OPENER)
|
|
@@ -76,7 +89,7 @@ module Poetry
|
|
|
76
89
|
end
|
|
77
90
|
|
|
78
91
|
def flag_helpers(code, line, stack, findings)
|
|
79
|
-
code.scan(
|
|
92
|
+
code.scan(helper_pattern) do |(helper)|
|
|
80
93
|
next unless @catalog.helper?(helper)
|
|
81
94
|
next if @catalog.identity_free?(helper)
|
|
82
95
|
next if code.match?(IDENTITY_ARG)
|